diff --git a/src/core/removable.ts b/src/core/removable.ts index eea3e682bc..2e7aeb1beb 100644 --- a/src/core/removable.ts +++ b/src/core/removable.ts @@ -1,4 +1,4 @@ -import { isValidTimeout } from './utils' +import { CompatWeakRef, isValidTimeout } from './utils' export abstract class Removable { cacheTime!: number @@ -12,8 +12,10 @@ export abstract class Removable { this.clearGcTimeout() if (isValidTimeout(this.cacheTime)) { + const thisRef = new CompatWeakRef(this) + this.gcTimeout = setTimeout(() => { - this.optionalRemove() + thisRef.deref()?.optionalRemove() }, this.cacheTime) } } diff --git a/src/core/tests/query.test.tsx b/src/core/tests/query.test.tsx index f53ff9ad85..9abd6a2883 100644 --- a/src/core/tests/query.test.tsx +++ b/src/core/tests/query.test.tsx @@ -881,4 +881,39 @@ describe('query', () => { expect(initialDataUpdatedAtSpy).toHaveBeenCalled() }) + + test.skip('short cacheTime should not let a query self-reference to remain in memory', async () => { + let query + + query = new QueryObserver(new QueryClient(), { + queryKey: queryKey(), + queryFn: async () => 'data', + cacheTime: 3, + }).getCurrentQuery() + + const weakQuery = new WeakRef(query) + query = null + + await sleep(30) + // global.gc() // This test needs node to run jest run with --expose-gc + expect(query).toBe(null) + expect(weakQuery.deref()).toBeUndefined() + }) + test.skip('long cacheTime should not let a query self-reference to remain in memory', async () => { + let query + + query = new QueryObserver(new QueryClient(), { + queryKey: queryKey(), + queryFn: async () => 'data', + cacheTime: 3000, + }).getCurrentQuery() + + const weakQuery = new WeakRef(query) + query = null + + await sleep(30) + // global.gc() // This test needs node to run jest run with --expose-gc + expect(query).toBe(null) + expect(weakQuery.deref()).toBeUndefined() + }) }) diff --git a/src/core/utils.ts b/src/core/utils.ts index 0c4c9b0fcf..43585da994 100644 --- a/src/core/utils.ts +++ b/src/core/utils.ts @@ -413,3 +413,21 @@ export function getAbortController(): AbortController | undefined { return new AbortController() } } + +/** + * Gracefully degrade a WeakRef to a "StrongRef" if support is missing. + */ +const StrongRef = (function StrongRef( + this: { [Symbol.toStringTag]: 'WeakRef'; deref(): T | undefined }, + target: T +) { + this.deref = () => target + + if (typeof Symbol === 'function' && typeof Symbol.toStringTag === 'symbol') { + this[Symbol.toStringTag] = 'WeakRef' + } +} as Function) as { + readonly prototype: WeakRef + new (target: T): WeakRef +} +export const CompatWeakRef = typeof WeakRef === 'function' ? WeakRef : StrongRef