Version
@tanstack/db@0.9.2 (also visible in current main).
Collection.cleanup() is declared async and its doc comment says to wait until cleanup completes before starting a new sync session. But the sync CleanupFn is typed () => void, and CollectionImpl.cleanup() calls the synchronous lifecycle cleanup then returns Promise.resolve(); it does not wait for a Promise returned by a source cleanup callback.
Minimal runtime reproduction:
import { createCollection } from '@tanstack/db'
const gate = Promise.withResolvers<void>()
const collection = createCollection({
id: 'cleanup-repro',
getKey: (row: { id: string }) => row.id,
sync: {
sync: ({ markReady }) => {
markReady()
return { cleanup: () => gate.promise }
},
},
})
await collection.preload()
let sourceCleanupDone = false
gate.promise.then(() => { sourceCleanupDone = true })
await collection.cleanup()
console.log(sourceCleanupDone) // false
Returning a Promise from a void callback type is accepted by TypeScript, but it is silently ignored. The same issue affects adapters that initiate asynchronous teardown/durable work from a synchronous cleanup callback.
Impact: An app that awaits collection.cleanup() before constructing a replacement collection over the same SQLite database can still have old-session cleanup in flight. That work may reach the replacement session or persist row deletes after new-session hydration. We currently need a separate application-level drain/cancel barrier before same-user replacement.
Expected: Either support CleanupFn: () => void | Promise<void> and make await collection.cleanup() wait for source cleanup, or explicitly document that cleanup only initiates teardown and provide an awaitable settlement hook/barrier. The current async return shape and comment suggest a stronger guarantee than the implementation gives.
This is separate from #1889: that issue is a late startup callback invoking markReady after cleanup; this issue is the absence of an awaitable source-cleanup completion contract.
Version
@tanstack/db@0.9.2(also visible in current main).Collection.cleanup()is declared async and its doc comment says to wait until cleanup completes before starting a new sync session. But the syncCleanupFnis typed() => void, andCollectionImpl.cleanup()calls the synchronous lifecycle cleanup then returnsPromise.resolve(); it does not wait for a Promise returned by a source cleanup callback.Minimal runtime reproduction:
Returning a Promise from a
voidcallback type is accepted by TypeScript, but it is silently ignored. The same issue affects adapters that initiate asynchronous teardown/durable work from a synchronous cleanup callback.Impact: An app that awaits
collection.cleanup()before constructing a replacement collection over the same SQLite database can still have old-session cleanup in flight. That work may reach the replacement session or persist row deletes after new-session hydration. We currently need a separate application-level drain/cancel barrier before same-user replacement.Expected: Either support
CleanupFn: () => void | Promise<void>and makeawait collection.cleanup()wait for source cleanup, or explicitly document that cleanup only initiates teardown and provide an awaitable settlement hook/barrier. The current async return shape and comment suggest a stronger guarantee than the implementation gives.This is separate from #1889: that issue is a late startup callback invoking
markReadyafter cleanup; this issue is the absence of an awaitable source-cleanup completion contract.