Open
Conversation
…cate tx skipping The PersistedCollectionRuntime never restored its stream position from the database on startup, always beginning at localTerm=1, localSeq=0. After a page reload, the first new mutation would collide with a previously applied transaction (term=1, seq=1), causing the SQLite adapter's applyCommittedTx to silently skip it as a duplicate. Add getStreamPosition to PersistenceAdapter (optional) and implement it in SQLiteCorePersistenceAdapter. Call it from startInternal() so that observeStreamPosition seeds the local counters before any mutations. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Include full row values in the tx:committed message so receiving tabs can apply changes directly without a SQLite round-trip via loadRowsByKeys. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ti-tab support Web Locks for per-collection leadership election, BroadcastChannel for cross-tab RPC transport, DB writer lock for SQLite write serialization, envelope dedup for exactly-once mutations, and leader heartbeats. Includes 15 unit tests with Web Locks/BroadcastChannel mocks. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Previously, `state.isLeader = true` was set before the setup code that calls `getStreamPosition()`. If `getStreamPosition` threw (e.g. due to a UNIQUE constraint violation from React StrictMode double-mounting), `isLeader` remained permanently stuck at `true` because the `finally` block that resets it was inside an inner try/finally that was never entered. Fix: Wrap the entire lock callback body in a single try/finally. Set `state.isLeader = true` only after successful setup (stream position restore and term increment). The finally block always runs and resets `isLeader = false` + cleans up the heartbeat timer. Also refactors the coordinator to support lazy adapter wiring via `setAdapter()`, allowing `createBrowserWASQLitePersistence` to inject the adapter after construction. This enables the demo to construct the coordinator without requiring the adapter upfront. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…nt seq collisions The leader tab had two mutation paths: a "direct" path (write to SQLite and broadcast) and an RPC path (through the coordinator). Previously, only follower tabs used the RPC path — the leader bypassed the coordinator and wrote directly. This caused a seq collision: the leader's direct writes incremented the runtime's `localSeq` but left the coordinator's `state.latestSeq` at 0. When a follower later sent an RPC, the coordinator assigned seq starting from 1 again, producing duplicate seq numbers. The leader then skipped these "already-seen" tx:committed messages, causing follower mutations to silently disappear. Fix: Always route through `requestApplyLocalMutations` when available, regardless of leader/follower status. This keeps the coordinator's seq counter in sync with all writes. Also removes `requestApplyLocalMutations` from `SingleProcessCoordinator` — it was a stub that returned success without persisting, which would break now that the leader uses this path. Single-process mode correctly falls back to the direct path since it has no multi-tab coordination. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…mmitted messages The leader tab's `onCoordinatorMessage` handler skipped ALL messages where `senderId` matched the coordinator's own node ID. But when the coordinator processes a follower's RPC in `handleApplyLocalMutations`, it delivers the resulting `tx:committed` to local subscribers using the coordinator's own `senderId`. This caused the leader's runtime to silently ignore follower mutations — they were written to SQLite but never applied to the leader's in-memory collection. Fix: Allow `tx:committed` messages from self to pass through the filter. The seq dedup logic in `processCommittedTxUnsafe` already prevents double-processing: when the leader's own mutations go through the coordinator, `observeStreamPosition` is called with the response's term/seq before the local `tx:committed` delivery runs under the mutex, so the duplicate is detected via `txCommitted.seq <= this.latestSeq`. Other message types (heartbeats, resets) from self are still skipped. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…riteMessage
When queryCollectionOptions detects a server-side deletion, it sends
{ type: 'delete', value: oldItem } through the sync. The persistence
layer only checked for 'key' in message to detect deletes, causing
value-based deletes to be misclassified as updates. Also use optional
chaining for process.versions in React Native where process exists but
versions may not.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ss-window sync Add ElectronCollectionCoordinator using BroadcastChannel + Web Locks for leader election and cross-window coordination in Electron renderer windows. Wire coordinator into renderer persistence via setAdapter(), add getStreamPosition to the IPC protocol, and export from package index. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… follow-up branches Remove db-electron-sqlite-persisted-collection, db-node-sqlite-persisted-collection, and db-cloudflare-do-sqlite-persisted-collection from this branch so it contains only the core persistence packages (db, db-sqlite-persisted-collection-core, db-browser-wa-sqlite-persisted-collection, db-react-native-sqlite-persisted-collection). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
This was referenced Mar 12, 2026
More templates
@tanstack/angular-db
@tanstack/db
@tanstack/db-browser-wa-sqlite-persisted-collection
@tanstack/db-ivm
@tanstack/db-react-native-sqlite-persisted-collection
@tanstack/db-sqlite-persisted-collection-core
@tanstack/electric-db-collection
@tanstack/offline-transactions
@tanstack/powersync-db-collection
@tanstack/query-db-collection
@tanstack/react-db
@tanstack/rxdb-db-collection
@tanstack/solid-db
@tanstack/svelte-db
@tanstack/trailbase-db-collection
@tanstack/vue-db
commit: |
Contributor
|
Size Change: +1.93 kB (+2.07%) Total Size: 95.1 kB
ℹ️ View Unchanged
|
Contributor
|
Size Change: 0 B Total Size: 3.85 kB ℹ️ View Unchanged
|
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
38ea25f to
9ce6ae0
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Replaces #1230 — this PR contains the core persistence packages only, split out for easier review:
db-sqlite-persisted-collection-core— shared SQLite adapter, persistence runtime, tx pruning, and coordinator protocoldb-browser-wa-sqlite-persisted-collection— browser persistence via wa-sqlite (OPFS) with multi-tab coordinator (BroadcastChannel + Web Locks)db-react-native-sqlite-persisted-collection— React Native/Expo persistence via expo-sqlitedb— index lifecycle events, removeIndex API, and hydration support for persistenceRuntime-specific persistence layers are in follow-up PRs:
Implements #865 (comment) from #865
Test plan
db-sqlite-persisted-collection-coredbpackage index lifecycle tests passpnpm test:prpasses🤖 Generated with Claude Code