Skip to content

preserve optimistic mutations during truncate operations - #659

Merged
samwillis merged 4 commits into
mainfrom
samwillis/truncate-bugs
Oct 9, 2025
Merged

preserve optimistic mutations during truncate operations#659
samwillis merged 4 commits into
mainfrom
samwillis/truncate-bugs

Conversation

@samwillis

@samwillissamwillis commented Oct 8, 2025

Copy link
Copy Markdown
Collaborator

issue found by @balegas

Problem

When a collection receives a truncate operation (typically triggered by a "must-refetch" signal from the server), optimistic mutations could be permanently lost if their async handlers (onInsert, onUpdate, onDelete) completed during the truncate processing.

Root Causes Identified

Bug #1: Optimistic Data Loss During Truncate

  • When an optimistic transaction's async handler completed during truncate processing, its data was lost
  • The commitPendingTransactions logic was rebuilding optimistic state from "active" transactions only
  • If a transaction changed from pendingcompleted during truncate processing, it was skipped during re-application
  • Impact: Users' optimistic changes disappeared from the UI

Bug #2: Missing Delete Events for Optimistic-Only Data

  • Truncate delete phase only iterated over syncedData.keys() to emit delete events
  • Items that existed only in optimisticUpserts (not yet synced) didn't get delete events
  • Impact: UI components didn't receive delete events for optimistic-only items during truncate

Solution

Snapshot-Based Approach

The fix implements a snapshot mechanism that captures optimistic state at the exact moment truncate() is called:

  1. Capture snapshot when truncate is called (packages/db/src/collection/sync.ts)

    truncate: ()=>{// ... existing truncate logic ...// Capture optimistic state NOWpendingTransaction.optimisticSnapshot={upserts: newMap(this.state.optimisticUpserts),deletes: newSet(this.state.optimisticDeletes),}}
  2. Use snapshot for delete events (packages/db/src/collection/state.ts)

    // Emit deletes for ALL visible items (synced + optimistic)constvisibleKeys=newSet([
    ...this.syncedData.keys(),
    ...(truncateOptimisticSnapshot?.upserts.keys()||[]),])
  3. Restore snapshot, then overlay active transactions

    if(hasTruncateSync&&truncateOptimisticSnapshot){// Restore optimistic state from snapshotfor(const[key,value]oftruncateOptimisticSnapshot.upserts){this.optimisticUpserts.set(key,value)}for(constkeyoftruncateOptimisticSnapshot.deletes){this.optimisticDeletes.add(key)}}// Always overlay currently active transactions// This captures late-arriving mutations that started after truncate()for(consttransactionofthis.transactions.values()){if(![`completed`,`failed`].includes(transaction.state)){// Re-apply active mutations}}

Why This Works

Timing Independence:

  • Snapshot is taken at truncate() call time, before any transactions can complete
  • Mutations that complete during processing are preserved in the snapshot
  • Late-arriving mutations (started after truncate) are captured by the active overlay

Client Intent Preservation:

  • Always overlaying active transactions ensures the newest client intent wins
  • If same key is modified after snapshot, the latest mutation takes precedence
  • Optimistic deletes prevent server re-insertions from being visible

Tests Added

Added 11 comprehensive tests in packages/db/tests/collection-truncate.test.ts:

Core Scenarios

  1. Preserve optimistic inserts when truncate completes before async handler
  2. Preserve optimistic inserts when handler completes during truncate processing (Bug ci: Introduce changesets #1)
  3. Handle truncate on empty collection
  4. Emit delete events for optimistic-only data during truncate (Bug Prep for initial release #2)
  5. Preserve optimistic inserts started after truncate begins (late arrivals)

Edge Cases

  1. Preserve optimistic delete when transaction active during truncate
    • Server re-inserts item but client delete is preserved
  2. Preserve optimistic value over server value during truncate
    • Client intent wins when optimistic differs from server
  3. Handle multiple consecutive truncate operations
    • Sequential truncates preserve all optimistic state
  4. Handle new mutation on same key after truncate snapshot
    • Newest mutation wins when same key modified multiple times
  5. Handle transaction completing between truncate and commit
    • Snapshot approach correctly handles this timing
  6. Preserve all optimistic inserts when truncate occurs during async handler
    • Multiple items with delayed handlers all preserved

Test Results

  • ✅ All 11 truncate tests pass
  • ✅ All 221 collection tests pass (12 test files)
  • ✅ No regressions introduced

Files Changed

Core Logic

  • packages/db/src/collection/state.ts

    • Added optimisticSnapshot field to PendingSyncedTransaction interface
    • Modified truncate delete phase to include optimistic keys from snapshot
    • Changed re-application logic to restore snapshot then overlay active transactions
  • packages/db/src/collection/sync.ts

    • Added snapshot capture when truncate() is called

Tests

  • packages/db/tests/collection-truncate.test.ts (new file)

    • 11 comprehensive tests covering all scenarios
  • packages/db/tests/collection-subscribe-changes.test.ts

    • Updated test expectation to match correct behavior (5 events including delete for optimistic-only item)

Performance Impact

Minimal overhead:

  • Snapshot only created during truncate operations (rare)
  • No overhead for normal insert/update/delete operations
  • Snapshot memory is cleaned up after truncate commit
  • Just 2 Maps/Sets during truncate processing

Breaking Changes

None. This is a bug fix that corrects existing behavior.

Migration Guide

No migration needed. The fix is transparent to existing code.

@changeset-bot

changeset-botBot commented Oct 8, 2025

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 01977a7

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 12 packages
NameType
@tanstack/dbPatch
@tanstack/angular-dbPatch
@tanstack/electric-db-collectionPatch
@tanstack/query-db-collectionPatch
@tanstack/react-dbPatch
@tanstack/rxdb-db-collectionPatch
@tanstack/solid-dbPatch
@tanstack/svelte-dbPatch
@tanstack/trailbase-db-collectionPatch
@tanstack/vue-dbPatch
todosPatch
@tanstack/db-example-react-todoPatch

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

@pkg-pr-new

pkg-pr-newBot commented Oct 8, 2025

Copy link
Copy Markdown
More templates

@tanstack/angular-db

npm i https://pkg.pr.new/@tanstack/angular-db@659

@tanstack/db

npm i https://pkg.pr.new/@tanstack/db@659

@tanstack/db-ivm

npm i https://pkg.pr.new/@tanstack/db-ivm@659

@tanstack/electric-db-collection

npm i https://pkg.pr.new/@tanstack/electric-db-collection@659

@tanstack/query-db-collection

npm i https://pkg.pr.new/@tanstack/query-db-collection@659

@tanstack/react-db

npm i https://pkg.pr.new/@tanstack/react-db@659

@tanstack/rxdb-db-collection

npm i https://pkg.pr.new/@tanstack/rxdb-db-collection@659

@tanstack/solid-db

npm i https://pkg.pr.new/@tanstack/solid-db@659

@tanstack/svelte-db

npm i https://pkg.pr.new/@tanstack/svelte-db@659

@tanstack/trailbase-db-collection

npm i https://pkg.pr.new/@tanstack/trailbase-db-collection@659

@tanstack/vue-db

npm i https://pkg.pr.new/@tanstack/vue-db@659

commit: 01977a7

@github-actions

github-actionsBot commented Oct 8, 2025

Copy link
Copy Markdown
Contributor

Size Change: +59 B (+0.08%)

Total Size: 76.5 kB

FilenameSizeChange
./packages/db/dist/esm/collection/state.js3.83 kB+7 B (+0.18%)
./packages/db/dist/esm/collection/sync.js1.7 kB+52 B (+3.15%)
ℹ️ View Unchanged
FilenameSize
./packages/db/dist/esm/collection/change-events.js963 B
./packages/db/dist/esm/collection/changes.js1.01 kB
./packages/db/dist/esm/collection/events.js660 B
./packages/db/dist/esm/collection/index.js3.31 kB
./packages/db/dist/esm/collection/indexes.js1.16 kB
./packages/db/dist/esm/collection/lifecycle.js1.82 kB
./packages/db/dist/esm/collection/mutations.js2.52 kB
./packages/db/dist/esm/collection/subscription.js1.83 kB
./packages/db/dist/esm/deferred.js230 B
./packages/db/dist/esm/errors.js3.1 kB
./packages/db/dist/esm/index.js1.58 kB
./packages/db/dist/esm/indexes/auto-index.js828 B
./packages/db/dist/esm/indexes/base-index.js835 B
./packages/db/dist/esm/indexes/btree-index.js2 kB
./packages/db/dist/esm/indexes/lazy-index.js1.21 kB
./packages/db/dist/esm/indexes/reverse-index.js577 B
./packages/db/dist/esm/local-only.js967 B
./packages/db/dist/esm/local-storage.js2.33 kB
./packages/db/dist/esm/optimistic-action.js294 B
./packages/db/dist/esm/proxy.js3.86 kB
./packages/db/dist/esm/query/builder/functions.js615 B
./packages/db/dist/esm/query/builder/index.js4.04 kB
./packages/db/dist/esm/query/builder/ref-proxy.js938 B
./packages/db/dist/esm/query/compiler/evaluators.js1.55 kB
./packages/db/dist/esm/query/compiler/expressions.js760 B
./packages/db/dist/esm/query/compiler/group-by.js2.04 kB
./packages/db/dist/esm/query/compiler/index.js2.04 kB
./packages/db/dist/esm/query/compiler/joins.js2.52 kB
./packages/db/dist/esm/query/compiler/order-by.js1.21 kB
./packages/db/dist/esm/query/compiler/select.js1.28 kB
./packages/db/dist/esm/query/ir.js785 B
./packages/db/dist/esm/query/live-query-collection.js340 B
./packages/db/dist/esm/query/live/collection-config-builder.js2.69 kB
./packages/db/dist/esm/query/live/collection-subscriber.js1.92 kB
./packages/db/dist/esm/query/optimizer.js3.08 kB
./packages/db/dist/esm/SortedMap.js1.24 kB
./packages/db/dist/esm/transactions.js3 kB
./packages/db/dist/esm/utils.js1.01 kB
./packages/db/dist/esm/utils/browser-polyfills.js365 B
./packages/db/dist/esm/utils/btree.js6.01 kB
./packages/db/dist/esm/utils/comparison.js754 B
./packages/db/dist/esm/utils/index-optimization.js1.73 kB

compressed-size-action::db-package-size

@github-actions

github-actionsBot commented Oct 8, 2025

Copy link
Copy Markdown
Contributor

Size Change: 0 B

Total Size: 1.47 kB

ℹ️ View Unchanged
FilenameSize
./packages/react-db/dist/esm/index.js152 B
./packages/react-db/dist/esm/useLiveQuery.js1.32 kB

compressed-size-action::react-db-package-size

@samwillissamwillis changed the title WIP fix truncate bugspreserve optimistic mutations during truncate operationsOct 8, 2025
@samwillis
samwillis marked this pull request as ready for review October 8, 2025 19:58

@KyleAMathewsKyleAMathews left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:shipit:

@samwillis
samwillis merged commit d9ae7b7 into mainOct 9, 2025
6 checks passed
@samwillis
samwillis deleted the samwillis/truncate-bugs branch October 9, 2025 11:04
@github-actionsgithub-actionsBot mentioned this pull request Oct 9, 2025
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@samwillis@KyleAMathews