Skip to content
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

fix(mem): Stop leak from cacheTime outliving Query given WeakRef support (v4) #3370

Closed
wants to merge 2 commits into from
Closed
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
fix(mem): Stop leak from cacheTime outliving Query
klarstrup committed Mar 4, 2022
commit 84b1635a0433e1901e5527091546d362374c73e7
6 changes: 4 additions & 2 deletions src/core/removable.ts
Original file line number Diff line number Diff line change
@@ -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)
}
}
35 changes: 35 additions & 0 deletions src/core/tests/query.test.tsx
Original file line number Diff line number Diff line change
@@ -881,4 +881,39 @@ describe('query', () => {

expect(initialDataUpdatedAtSpy).toHaveBeenCalled()
})

test('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('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()
})
})
18 changes: 18 additions & 0 deletions src/core/utils.ts
Original file line number Diff line number Diff line change
@@ -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<T extends object>(
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<any>
new <T extends object>(target: T): WeakRef<T>
}
export const CompatWeakRef = typeof WeakRef === 'function' ? WeakRef : StrongRef