-
-
Couldn't load subscription status.
- Fork 3.5k
feat(query-core): add timeoutManager to allow changing setTimeout/setInterval #9612
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
8fc4d3d
2fb1f1f
acb595c
96813b6
530504e
223371b
d3d7d1a
a953c70
0fac7b2
47962fd
62b1dd0
8d5e050
fea6cce
1606b58
fc9092b
5d6fe4d
ad1fb2b
932c3a2
a6d38f8
81d35ac
b6fffb4
948c646
08a2c5f
841ac54
fb22c67
09a787e
7fb57b1
4b1e8af
b6ca80d
73014f5
3f452ea
cca861f
d58e28f
7922966
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,7 @@ | ||
| // TYPES | ||
|
|
||
| import { systemSetTimeoutZero } from './timeoutManager' | ||
|
|
||
| type NotifyCallback = () => void | ||
|
|
||
| type NotifyFunction = (callback: () => void) => void | ||
|
|
@@ -10,7 +12,8 @@ type BatchCallsCallback<T extends Array<unknown>> = (...args: T) => void | |
|
|
||
| type ScheduleFunction = (callback: () => void) => void | ||
|
|
||
| export const defaultScheduler: ScheduleFunction = (cb) => setTimeout(cb, 0) | ||
| export const defaultScheduler: ScheduleFunction = (cb) => | ||
|
||
| systemSetTimeoutZero(cb) | ||
justjake marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| export function createNotifyManager() { | ||
| let queue: Array<NotifyCallback> = [] | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,174 @@ | ||
| /** | ||
| * Timeout manager does not support passing arguments to the callback. | ||
| * (`void` is the argument type inferred by TypeScript's default typings for `setTimeout(cb, number)`) | ||
| */ | ||
| export type TimeoutCallback = (_: void) => void | ||
|
|
||
| /** | ||
| * Wrapping `setTimeout` is awkward from a typing perspective because platform | ||
| * typings may extend the return type of `setTimeout`. For example, NodeJS | ||
| * typings add `NodeJS.Timeout`; but a non-default `timeoutManager` may not be | ||
| * able to return such a type. | ||
| * | ||
| * Still, we can downlevel `NodeJS.Timeout` to `number` as it implements | ||
| * Symbol.toPrimitive. | ||
| */ | ||
| export type TimeoutProviderId = number | { [Symbol.toPrimitive]: () => number } | ||
|
|
||
| /** | ||
| * Backend for timer functions. | ||
| */ | ||
| export type TimeoutProvider = { | ||
| /** Used in error messages. */ | ||
| readonly name: string | ||
|
|
||
| readonly setTimeout: ( | ||
| callback: TimeoutCallback, | ||
| delay: number, | ||
| ) => TimeoutProviderId | ||
| readonly clearTimeout: (timeoutId: number | undefined) => void | ||
|
|
||
| readonly setInterval: ( | ||
| callback: TimeoutCallback, | ||
| delay: number, | ||
| ) => TimeoutProviderId | ||
| readonly clearInterval: (intervalId: number | undefined) => void | ||
| } | ||
|
|
||
| export const defaultTimeoutProvider: TimeoutProvider = { | ||
justjake marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| name: 'default', | ||
justjake marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| setTimeout: (callback, delay) => setTimeout(callback, delay), | ||
TkDodo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| clearTimeout: (timeoutId) => clearTimeout(timeoutId), | ||
|
|
||
| setInterval: (callback, delay) => setInterval(callback, delay), | ||
| clearInterval: (intervalId) => clearInterval(intervalId), | ||
| } | ||
|
|
||
| /** Timeout ID returned by {@link TimeoutManager} */ | ||
| export type ManagedTimerId = number | ||
|
|
||
| /** | ||
| * Allows customization of how timeouts are created. | ||
| * | ||
| * @tanstack/query-core makes liberal use of timeouts to implement `staleTime` | ||
| * and `gcTime`. The default TimeoutManager provider uses the platform's global | ||
| * `setTimeout` implementation, which is known to have scalability issues with | ||
| * thousands of timeouts on the event loop. | ||
| * | ||
| * If you hit this limitation, consider providing a custom TimeoutProvider that | ||
| * coalesces timeouts. | ||
| */ | ||
| export class TimeoutManager implements Omit<TimeoutProvider, 'name'> { | ||
| #provider: TimeoutProvider = defaultTimeoutProvider | ||
| #providerCalled = false | ||
|
|
||
| setTimeoutProvider(provider: TimeoutProvider): void { | ||
| if (provider === this.#provider) { | ||
| return | ||
| } | ||
|
||
|
|
||
| if (this.#providerCalled) { | ||
| // After changing providers, `clearTimeout` will not work as expected for | ||
| // timeouts from the previous provider. | ||
| // | ||
| // Since they may allocate the same timeout ID, clearTimeout may cancel an | ||
| // arbitrary different timeout, or unexpected no-op. | ||
| // | ||
| // We could protect against this by mixing the timeout ID bits | ||
| // deterministically with some per-provider bits. | ||
| // | ||
| // We could internally queue `setTimeout` calls to `TimeoutManager` until | ||
| // some API call to set the initial provider. | ||
| console.warn( | ||
| `[timeoutManager]: Switching to ${provider.name} provider after calls to ${this.#provider.name} provider might result in unexpected behavior.`, | ||
| ) | ||
| } | ||
justjake marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| this.#provider = provider | ||
| this.#providerCalled = false | ||
| } | ||
|
|
||
| setTimeout(callback: TimeoutCallback, delay: number): ManagedTimerId { | ||
| this.#providerCalled = true | ||
| return providerIdToNumber( | ||
| this.#provider, | ||
| this.#provider.setTimeout(callback, delay), | ||
| ) | ||
| } | ||
|
|
||
| clearTimeout(timeoutId: ManagedTimerId | undefined): void { | ||
| this.#provider.clearTimeout(timeoutId) | ||
| } | ||
|
|
||
| setInterval(callback: TimeoutCallback, delay: number): ManagedTimerId { | ||
| this.#providerCalled = true | ||
justjake marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| return providerIdToNumber( | ||
| this.#provider, | ||
| this.#provider.setInterval(callback, delay), | ||
| ) | ||
| } | ||
|
|
||
| clearInterval(intervalId: ManagedTimerId | undefined): void { | ||
| this.#provider.clearInterval(intervalId) | ||
| } | ||
| } | ||
|
|
||
| function providerIdToNumber( | ||
| provider: TimeoutProvider, | ||
| providerId: TimeoutProviderId, | ||
| ): ManagedTimerId { | ||
| const numberId = Number(providerId) | ||
| if (isNaN(numberId)) { | ||
| throw new Error( | ||
| `TimeoutManager: could not convert ${provider.name} provider timeout ID to valid number`, | ||
| ) | ||
| } | ||
justjake marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| return numberId | ||
| } | ||
|
|
||
| export const timeoutManager = new TimeoutManager() | ||
|
|
||
| // Exporting functions that use `setTimeout` to reduce bundle size impact, since | ||
| // method names on objects are usually not minified. | ||
|
|
||
| /** A version of `setTimeout` controlled by {@link timeoutManager}. */ | ||
| export function managedSetTimeout( | ||
| callback: TimeoutCallback, | ||
| delay: number, | ||
| ): ManagedTimerId { | ||
| return timeoutManager.setTimeout(callback, delay) | ||
| } | ||
|
|
||
| /** A version of `clearTimeout` controlled by {@link timeoutManager}. */ | ||
| export function managedClearTimeout( | ||
| timeoutId: ManagedTimerId | undefined, | ||
| ): void { | ||
| timeoutManager.clearTimeout(timeoutId) | ||
| } | ||
|
|
||
| /** A version of `setInterval` controlled by {@link timeoutManager}. */ | ||
| export function managedSetInterval( | ||
| callback: TimeoutCallback, | ||
| delay: number, | ||
| ): ManagedTimerId { | ||
| return timeoutManager.setInterval(callback, delay) | ||
| } | ||
|
|
||
| /** A version of `clearInterval` controlled by {@link timeoutManager}. */ | ||
| export function managedClearInterval( | ||
| intervalId: ManagedTimerId | undefined, | ||
| ): void { | ||
| timeoutManager.clearInterval(intervalId) | ||
| } | ||
|
|
||
| /** | ||
| * In many cases code wants to delay to the next event loop tick; this is not | ||
| * mediated by {@link timeoutManager}. | ||
| * | ||
| * This function is provided to make auditing the `tanstack/query-core` for | ||
| * incorrect use of system `setTimeout` easier. | ||
| */ | ||
| export function systemSetTimeoutZero(callback: TimeoutCallback): void { | ||
| setTimeout(callback, 0) | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.