Skip to content

Commit

Permalink
Clean up subscriptions using requestIdleCallback instead of setTimeout
Browse files Browse the repository at this point in the history
  • Loading branch information
raymondwang committed Sep 19, 2023
1 parent b69d348 commit 531a959
Show file tree
Hide file tree
Showing 5 changed files with 64 additions and 3 deletions.
5 changes: 5 additions & 0 deletions .changeset/brown-cats-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@apollo/client": patch
---

Clean up subscriptions using requestIdleCallback instead of setTimeout
7 changes: 4 additions & 3 deletions src/core/ObservableQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
iterateObserversSafely,
fixObservableSubclass,
getQueryDefinition,
requestIdleCallback,
} from "../utilities/index.js";
import type { ApolloError } from "../errors/index.js";
import type { QueryManager } from "./QueryManager.js";
Expand Down Expand Up @@ -204,7 +205,7 @@ export class ObservableQuery<
//
// We do this in order to prevent observers piling up within
// the QueryManager. Notice that we only fully unsubscribe
// from the subscription in a setTimeout(..., 0) call. This call can
// from the subscription in an idle callback. This callback can
// actually be handled by the browser at a much later time. If queries
// are fired in the meantime, observers that should have been removed
// from the QueryManager will continue to fire, causing an unnecessary
Expand All @@ -214,9 +215,9 @@ export class ObservableQuery<
this.queryManager.removeQuery(this.queryId);
}

setTimeout(() => {
requestIdleCallback(() => {
subscription.unsubscribe();
}, 0);
});
},
error: reject,
};
Expand Down
39 changes: 39 additions & 0 deletions src/utilities/common/__tests__/requestIdleCallback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { requestIdleCallback } from "../requestIdleCallback";

describe("requestIdleCallback", () => {
const originalRequestIdleCallback = window.requestIdleCallback;

afterAll(() => {
Object.defineProperty(window, "requestIdleCallback", {
value: originalRequestIdleCallback,
});
});

it("should use the window method when possible", () => {
Object.defineProperty(window, "requestIdleCallback", {
value: jest.fn((callback) => callback()),
configurable: true,
});

const task = jest.fn();
requestIdleCallback(task);
expect(task).toHaveBeenCalled();
});

it("should fall back to setTimeout when the window method is not available", () => {
// @ts-expect-error
delete window.requestIdleCallback;

jest.useFakeTimers();

const task = jest.fn();
requestIdleCallback(task);

expect(task).not.toHaveBeenCalled();

jest.runAllTimers();
expect(task).toHaveBeenCalled();

jest.useRealTimers();
});
});
15 changes: 15 additions & 0 deletions src/utilities/common/requestIdleCallback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* Light polyfill for requestIdleCallback when used in non-browser environments.
*/
export function requestIdleCallback(
callback: () => void,
options?: IdleRequestOptions
) {
if (
!Object.prototype.hasOwnProperty.call(window, "requestIdleCallback") ||
typeof window.requestIdleCallback === "undefined"
) {
return setTimeout(callback, options?.timeout ?? 0);
}
return window.requestIdleCallback(callback, options);
}
1 change: 1 addition & 0 deletions src/utilities/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ export * from "./common/makeUniqueId.js";
export * from "./common/stringifyForDisplay.js";
export * from "./common/mergeOptions.js";
export * from "./common/incrementalResult.js";
export * from "./common/requestIdleCallback.js";

export { omitDeep } from "./common/omitDeep.js";
export { stripTypename } from "./common/stripTypename.js";
Expand Down

0 comments on commit 531a959

Please sign in to comment.