Repository navigation
Conversation
…ion persists Automatic GC checked subscribers and pending preloads but not transactions. A collection whose last subscriber left while a mutation handler was still running was cleaned up after gcTime: its optimistic rows disappeared and waits on the sync run, such as Electric's awaitTxId, rejected, so the write failed even when the server accepted it. A persisting transaction that touches the collection now holds it, and the gcTime delay starts again when the last one completes or fails. Explicit cleanup() is unchanged. A new generated-history oracle checks retirement against an independent deadline model across subscribe, unsubscribe, insert, settlement, clock steps and explicit cleanup.
…ections The GC oracle now runs every history over two Collections. An insert writes to one Collection through its onInsert handler, or to both through createTransaction with a manual commit, and the model keeps a deadline per Collection. A settlement that restarts the delay only for the first Collection a transaction touched now fails the oracle; the one-Collection grammar let it pass. The oracle also runs in the package's test:oracles campaign.
🦋 Changeset detectedLatest commit: f19f490 The changes in this PR will be included in the next version bump. This PR includes changesets to release 24 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
No actionable comments were generated in the recent review.
|
| Layer / File(s) | Summary |
|---|---|
Retain collections through transaction settlement packages/db/src/types.ts, packages/db/src/transactions.ts, packages/db/src/collection/lifecycle.ts |
The gcTime documentation describes transaction-based retention. Settled transactions start the GC timer. The garbage-collection check excludes collections with persisting transactions. |
Validate and register GC timing histories packages/db/tests/collection-gc-transaction-oracle.property.test.ts, packages/db/tests/oracle-config.ts, packages/db/tests/oracle-replay-manifest.ts, packages/db/package.json, docs/contributing/oracle-coverage.md, .changeset/gc-waits-for-persisting-transactions.md |
A property-based oracle checks retirement timing, persisting-row visibility, and explicit cleanup. The oracle is added to the test command, registry, replay manifest, and coverage documentation. A patch changeset records the behavior. |
Priority:
Estimated code review effort: 3 (Moderate) | ~20 minutes
Change: Bug fix · Severity of issue fixed: Medium
Suggested reviewers: kyleamathews
Merge Risk: 
Minimal · up to f19f4
-
Automatic collection GC now waits while a transaction touching the collection is persisting, then restarts the gcTime delay when the transaction completes or fails. Explicit cleanup behavior is unchanged. No concrete merge-blocking issue was identified, and a new oracle test covers the lifecycle.
Pre-merge checks: 
4 · 
1
-
Failed checks: 1 warning Passed checks: 4 passed-
Full details: Linked Issues check
-
Explanation
Issue
#2256requires automatic GC to retain a collection while a touching transaction is persisting, restart the gcTime delay after completion or failure, and leave explicit cleanup() unchanged. The reviewed lifecycle code adds !this.state.hasPersistingTransaction to canGarbageCollect(), and the transaction path restarts each touched collection through startGCTimerAfterSettlement() for completed or failed transactions. The PR also adds the gcTime documentation and a registered property-based oracle that checks retention, settlement timing, failure timing, shared transactions, and explicit cleanup. These changes satisfy the coding and automated-test requirements.
Full details: Out of Scope Changes check
-
Explanation
The production changes, gcTime documentation, changeset, oracle test, oracle registry, replay manifest, and test script all support Issue
#2256. The supplied change summary shows no unrelated product behavior or unrelated files. The added settlement method is an internal lifecycle mechanism for the required GC restart.
-
Full details: Docstring Coverage
-
Explanation
Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 4 functions across 6 files. (3 skipped: 3 unsupported.)
Resolution
Write docstrings for the functions missing them to satisfy the coverage threshold.
-
Finishing Touches
-
🧪 Generate unit tests (beta)
-
- Create a new PR
-
- Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
Comment @coderabbitai help to get the list of available commands.
Changes
Automatic garbage collection no longer cleans up a collection while a transaction that touches it is persisting. The
gcTimedelay starts again when the last such transaction completes or fails. An explicitcleanup()is unchanged.Before this change,
canGarbageCollect()checked for subscribers and a pending preload, but not for transactions. A screen could insert a row, unmount, and leave the collection with no subscriber while its mutation handler was still waiting on the server. AftergcTimethe collection was cleaned up: the optimistic row disappeared while the transaction was stillpersisting, and waits on the sync run were rejected. With the Electric collection, the handler'sawaitTxIdrejected withStreamAbortedError, so the write failed even though the server had accepted it.We run TanStack DB with Electric in production, in web apps and in Expo apps on iOS and Android, where screens close and apps go to the background while writes are in flight, so this window is common for us. We don't run this fix ourselves yet: today we work around the bug by holding the collection with a subscription for the length of each write, as the issue describes. This change would let us remove that hold.
Closes #2256
The change:
canGarbageCollect()also requires that no transaction touching the collection is persisting. The existing timer and idle callback already re-check it before they clean up.touchCollection()asks each collection it touched to start its GC timer again. The timer starts only if the collection has started and is not cleaned up, and only if nothing else holds it, so a collection with subscribers is unaffected.gcTimedocstring says that a persisting transaction retains the collection.A transaction that never settles now holds its collection for as long as it persists. That matches what the collection shows: its optimistic rows stay visible for that time.
Oracle Coverage
Coverage-map owner: Collection lifecycle. No existing owner modeled GC against transactions: the subscription lifecycle oracles cover acquisition and publication, and the cleanup-queue oracle covers appointment timing. So this adds a small independent owner,
collection-gc-transaction-oracle.property.test.ts, listed in that row.Law: automatic GC retires a collection once it has had no subscriber and no persisting transaction for
gcTimeelapsed milliseconds. The delay starts again when the last such transaction completes or fails. Explicitcleanup()retires the collection at once. Authority:BaseCollectionConfig.gcTime(a delay for collections with no subscribers), and the optimistic contract that a persisting transaction's rows stay visible until it settles.Model: an elapsed clock and, for each of two collections, the subscriber count, the set of persisting transactions that touch it, and a deadline that exists only while nothing holds that collection. The deadline is set when the last hold ends: the sync start, the last unsubscribe, or the last settlement. It reads no production state and does not copy the cleanup queue or idle scheduling.
Histories: property
collection-lifecycle.gc-persisting-transaction, withgcTime100 on a fake clock. Each history has 1 to 14 steps: subscribe to either collection, unsubscribe one of its subscribers, insert a row whose handler stays pending, complete or fail the oldest pending handler, and advance by 20, 60, or 120 ms. An insert goes to one collection through itsonInsert, or to both throughcreateTransactionwith a manual commit. A subscribe after retirement restarts that collection. Every history ends with an explicitcleanup()of both, or with a drain: unsubscribe everyone, settle every pending handler with one outcome, and advance past the deadlines.Production path and observation: two real collections with a synchronous ready sync, public
subscribeChanges,insert,createTransaction, andcleanup(). After each step, once due timers and their microtasks have run, the test compares whether each collection iscleaned-upwith the model, and checks that every persisting transaction's row is still visible in each collection it touched. The cleanup queue retires a collection one tick after its deadline. The model treats the deadline itself as not yet due, and every clock step is at least 20 ms, so no comparison falls inside that tick.Reach (the property fails if any state is reached fewer than once):
gc-due-while-persisting(oldgcTimepassed since the last unsubscribe while a transaction persisted)retired-after-completionretired-after-failureretired-after-shared-settlement(the delay was restarted by a transaction that touched both collections)settled-while-subscribedcleanup-while-persistingrestarted-after-retirementRandom campaigns reached every state in each of my runs; the lowest count I saw was 3.
RED on
main: both campaigns fail. The fixed-seed counterexample is an insert into one collection, then two 60 ms steps: at 120 ms the collection iscleaned-upwhile the transaction persists.GREEN: both campaigns pass.
Mutants (all assertion failures):
mainLimits: paced mutations, offline transactions, live-query collections, and wall-clock corrections are not generated. Offline restoration transactions never commit, so they stay
pendingand do not hold a collection.Replay:
TANSTACK_DB_ORACLE_SEED=<seed> TANSTACK_DB_ORACLE_PATH=<path> TANSTACK_DB_ORACLE_PROPERTY=collection-lifecycle.gc-persisting-transaction pnpm --dir packages/db exec vitest run tests/collection-gc-transaction-oracle.property.test.tsThe property is registered in
oracle-config.ts, the replay manifest, and the package'stest:oraclescampaign.Shipped size:
pnpm size:comparereports +47 B gzip for@tanstack/dband +38 B for theappscenario. The hold is one more condition incanGarbageCollect(). The rest is the timer restart on settlement, which the hold needs: without it, a collection whose last hold was a transaction would never be collected.Checklist
pnpm test.@tanstack/db: 13,793 tests pass, andtscon the test config reports no errors. The suites of@tanstack/electric-db-collection,@tanstack/query-db-collection,@tanstack/offline-transactions,@tanstack/react-db, and@tanstack/db-sqlite-persistence-corepass against this build.pnpm lintreports nothing in the touched files, andpnpm lint:changedis clean.Release Impact
Summary by CodeRabbit
gcTimecountdown starts again.cleanup()continues to retire a collection even while a transaction is persisting.