Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
8 changes: 8 additions & 0 deletions .changeset/enforce-crash-only-persistence-coordination.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@tanstack/browser-db-sqlite-persistence': patch
'@tanstack/db-sqlite-persistence-core': patch
'@tanstack/electron-db-sqlite-persistence': patch
'@tanstack/db': patch
---

Require coordinators to route complete committed transactions through the per-collection persistence owner, with named fail-stop errors for indeterminate commits and durability failures. Add clone-safe remote-subset leases with exact release, recursive wire validation, and matching Browser and Electron coordination.
19 changes: 18 additions & 1 deletion docs/contributing/oracle-coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,32 @@ comment and the current API/architecture contract before extending its model.
| Drafts and native values | [proxy](../../packages/db/tests/proxy.test.ts), [detachment](../../packages/db/tests/proxy-detachment-contract.test.ts), [iteration](../../packages/db/tests/proxy-iteration-contract.test.ts) | Native-operation controls, exact patches and actual stored rows; alias/cycle/adversarial-key histories. General native-mutator and symbol-write support is not established by a plain-object oracle. |
| Query DB and observer | [ownership](../../packages/query-db-collection/tests/ownership-lifecycle.oracle.test.ts), [load lifecycle](../../packages/query-db-collection/tests/load-subset-lifecycle-oracle.test.ts), [observer histories](../../packages/db/tests/live-query-observer-history.property.test.ts) | Real QueryClient boundary and a per-listener eligibility ledger, not a duplicate dispatch queue. Check reentry, peer survival, FIFO and disposal independently of final rows. |
| Ordered acquisition | [pagination](../../packages/db/tests/query/pagination-oracle.property.test.ts), [ordered work](../../packages/db/tests/query/ordered-work-oracle.property.test.ts), [ordered lifecycle](../../packages/db/tests/query/ordered-lifecycle-oracle.property.test.ts) | Complete finite provider results, pending windows, ties/nulls, ownership and documented repair timing. Request completion is not proof of unrequested source extent. |
| Opaque backend pagination | [window oracle](../../packages/query-db-collection/tests/cursor-pagination.oracle.test.ts), [cache histories](../../packages/query-db-collection/tests/cursor-pagination.cache-oracle.test.ts), [cache publication](../../packages/query-db-collection/tests/cursor-pagination.publication-oracle.test.ts), [browser acquisition boundaries](../../packages/query-db-collection/tests/cursor-pagination.boundary-oracle.test.ts), [QueryCollection integration](../../packages/query-db-collection/tests/cursor-pagination.integration.test.ts) | Full filter/sort/slice reference, opaque token transport, actual Query cache expiry/invalidation/GC, forced refresh during growth, protocol failure publication/recovery, bounded slice work, nested cancellation/replacement, reader abort, browser retry defaults, manual-write cache isolation, and production window publications. Stable backend sequences; not snapshot guarantees for changing endpoints. Peek-ahead remains enabled. |
| Electric and TrailBase | [Electric histories](../../packages/electric-db-collection/tests/electric-oracle.property.test.ts), [PostgreSQL semantics](../../packages/electric-db-collection/e2e/sql-predicate-semantics.e2e.test.ts), [TrailBase contract](../../packages/trailbase-db-collection/tests/ORACLE.md) | Installed SDK delivery/framing, independent predicates, exact subscription arguments and late errors. SDK fixtures and a real service test earn different credit. |
| PowerSync | [tests](../../packages/powersync-db-collection/tests) | Applied receipt positions crossed with held peers, native SQLite/SDK and cleanup evidence. A timeout mutant proves a progress failure, not every value assertion. |
| SQLite persistence and native hosts | [persisted histories](../../packages/db-sqlite-persistence-core/tests/persisted.test.ts), [driver contracts](../../packages/db-sqlite-persistence-core/tests/contracts/sqlite-driver-contract.ts), [113-law manifest](../../packages/db-collection-e2e/src/fixtures/persisted-conformance-manifest.ts) | Cache/remote rejection/peer/reopen histories and exact driver results. The manifest excludes progressive and move suites; registration and shim runs are not device execution. |
| SQLite persistence and native hosts | [persisted histories](../../packages/db-sqlite-persistence-core/tests/persisted.test.ts), [Browser composed-owner histories](../../packages/browser-db-sqlite-persistence/tests/per-collection-coordinator-oracle.test.ts), [Browser coordinator RPC](../../packages/browser-db-sqlite-persistence/tests/browser-coordinator.test.ts), [Electron IPC and composed owner](../../packages/electron-db-sqlite-persistence/tests/electron-ipc.test.ts), [driver contracts](../../packages/db-sqlite-persistence-core/tests/contracts/sqlite-driver-contract.ts), [113-law manifest](../../packages/db-collection-e2e/src/fixtures/persisted-conformance-manifest.ts) | Core cache/remote rejection/peer/reopen histories and exact driver results. Browser composes public source commits with per-collection elected-owner routing and covers the complete committed-transaction wire partition through deterministic Node transport seams. Electron composes source commits with a per-collection renderer owner, IPC adapter, real SQLite, and reopen checks. The Browser seams are not real multi-context/OPFS-worker execution; the Electron harness is not an actual Electron process unless its explicit runtime-bridge mode runs. The manifest excludes progressive and move suites; registration and shim runs are not device execution. |
| Offline execution | [scheduler](../../packages/offline-transactions/tests/KeyScheduler.property.test.ts), [leadership](../../packages/offline-transactions/tests/leadership-replay.property.test.ts), [settlement](../../packages/offline-transactions/tests/transaction-settlement.property.test.ts), [serialization](../../packages/offline-transactions/tests/transaction-serializer.property.test.ts) | Declarative FIFO eligibility, per-transaction outcomes, durable state and typed wire trees. Issued work may finish after ownership loss, but new work must not start. Exactly-once network execution is not promised. |
| Frameworks | [React conformance](../../packages/react-db/tests/conformance.test.tsx), [React pagination](../../packages/react-db/tests/infinite-query-conformance.test.tsx), [shared suites](../../packages/db-collection-e2e/src/suites) | Exact exposed rows/pages and each framework's own lifecycle cuts. A React witness does not prove Vue/Solid/Angular/Svelte scheduling. Preserve their receiving registrations. |
| Small structures and test mechanics | [SortedMap](../../packages/db/tests/SortedMap.test.ts), [cleanup queue](../../packages/db/tests/cleanup-queue.property.test.ts), [guarded replay](../../packages/db/tests/oracle-replay.test.ts) | Map/full-sort and appointment-list models; executed target/seed/path checks. Callback-reentrant scheduling is outside the initial cleanup-queue domain. |

## Acceptance map

The post-merge review added three missing domains to existing owners:

- [Top-K batch contracts](../../packages/db-ivm/tests/operators/topk-batch-contract.test.ts)
cross sparse-array length/holes and RegExp source/flags/position with equal
controls, replacement order, hash consolidation, and actual retained graph
output. Ordinary replacements also run without the global `File` constructor.
- [Leadership replay](../../packages/offline-transactions/tests/leadership-replay.property.test.ts)
holds real storage-read delivery across successful and permanently rejected
durable removals, with bounded scans, concurrent loads, and unfinished peers.
This is distinct from exactly-once execution across independent owners.
- [Accepted-snapshot retention](../../packages/db/tests/collection-state-retention-oracle.property.test.ts)
varies truncate before/during/after an optimistic delete, rejection versus
rollback, post-capture direct insertion, and later ordinary sync/key reuse. A
hidden accepted insert returns after rollback; an uncaptured insert retires,
and neither snapshot is rebased onto synced fields.

| Issue obligation | Implemented evidence | Limit |
| --- | --- | --- |
| Metamorphic laws | Includes cross-formulation/partition, D2 independent-key commutation, DBSP incremental/full recomputation, pagination provider/UI boundaries, optimistic snapshot stability | Equivalence premises are explicit; not arbitrary query rewrites. |
Expand Down
56 changes: 55 additions & 1 deletion packages/browser-db-sqlite-persistence/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ is opt-in by passing a `BrowserCollectionCoordinator`.

- `createBrowserWASQLitePersistence(...)`
- `openBrowserWASQLiteOPFSDatabase(...)`
- `BrowserCollectionCoordinator`
- `persistedCollectionOptions(...)` (re-exported from core)

## Quick start (single-tab)
Expand Down Expand Up @@ -97,13 +98,66 @@ export const todosCollection = createCollection(
See [`examples/react/offline-transactions`](../../examples/react/offline-transactions/src/db/persisted-todos.ts)
for a full multi-tab example.

### Committed transaction ownership

The persisted sync wrapper sends every source transaction with durable effects
through the coordinator's required
`requestApplyCommittedTx(collectionId, tx)` method.
`BrowserCollectionCoordinator` routes the complete transaction to the current
leader for that collection. The leader applies it with the adapter registered
for the same collection id, including that collection's resolved mode and
`schemaVersion`.

The route preserves truncation, row changes, row metadata, collection metadata,
and stream position as one `PersistedTx`. It does not feature-detect a partial
route or fall back to row-only mutation RPC. A custom coordinator that omits
`requestApplyCommittedTx` is rejected while the collection is configured,
before its sync source can publish rows. An effect-free source commit neither
publishes nor consumes a coordinator sequence.

Single-tab mode uses the same complete transaction contract. Its
`SingleProcessCoordinator` skips election and channel traffic but still routes
the transaction to the resolved adapter for that collection.

If a mutating RPC loses its response, Browser coordination replays it only
while the requester still knows the same non-null leader id and term. An
unknown initial route or any leader/term change rejects with
`IndeterminateCommitError`; the application must reconcile the outcome. The
coordinator does not retry that mutation against an unknown or replacement
leader.

### Remote subset requests

`BrowserCollectionCoordinator.requestEnsureRemoteSubset(...)` validates and
projects the request before it chooses the local leader or `BroadcastChannel`
route. Registered owners receive the exported
`TransportedLoadSubsetOptions` type. It contains the supported
structured-clone wire data and excludes live `signal` and `subscription`
fields. Unsupported nested values fail immediately with
`RemoteSubsetWireValueError` and the exact value path; no owner callback or
channel post occurs. See the core package's remote subset wire contract for the
complete supported domain.

Each accepted request is an explicit lease. Retries of the same request object
reuse its acquisition identity, while distinct equal request objects remain
independent. Release is routed to the elected collection owner and unloads the
exact acquired options once. Leadership loss unloads the retiring owner's live
leases, and requesters replay still-live acquisitions against the next leader.
Registering a second owner for one collection throws
`DuplicateRemoteSubsetOwnerError`; no adapter fallback replaces the owner.
Remote follower transport/admission failures reject and retain demand for its
normal retry without entering the follower's local owner lifecycle. Terminal
release identities expire after the existing RPC dedupe horizon while delayed
duplicates inside that horizon remain idempotent.

## Notes

- `openBrowserWASQLiteOPFSDatabase(...)` starts a dedicated Web Worker and
routes SQL operations through it. OPFS sync access handle APIs are used in
that worker context.
- Single-tab mode does not require `BroadcastChannel` or Web Locks for
correctness.
election, but committed transactions still go through the collection's
registered persistence owner.
- Multi-tab mode requires `BroadcastChannel` and the Web Locks API; both are
available in all modern browsers.
- OPFS capability failures are surfaced as `PersistenceUnavailableError`.
Loading
Loading