@tanstack/db-sqlite-persistence-core 0.2.23 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +258 -7
  2. package/dist/cjs/broadcast-coordinator.cjs +1689 -0
  3. package/dist/cjs/broadcast-coordinator.cjs.map +1 -0
  4. package/dist/cjs/broadcast-coordinator.d.cts +116 -0
  5. package/dist/cjs/errors.cjs +73 -0
  6. package/dist/cjs/errors.cjs.map +1 -1
  7. package/dist/cjs/errors.d.cts +44 -0
  8. package/dist/cjs/index.cjs +19 -0
  9. package/dist/cjs/index.cjs.map +1 -1
  10. package/dist/cjs/index.d.cts +2 -0
  11. package/dist/cjs/persisted.cjs +1782 -394
  12. package/dist/cjs/persisted.cjs.map +1 -1
  13. package/dist/cjs/persisted.d.cts +176 -11
  14. package/dist/cjs/remote-subset-owner.cjs +20 -0
  15. package/dist/cjs/remote-subset-owner.cjs.map +1 -0
  16. package/dist/cjs/remote-subset-owner.d.cts +7 -0
  17. package/dist/cjs/remote-subset-wire.cjs +681 -0
  18. package/dist/cjs/remote-subset-wire.cjs.map +1 -0
  19. package/dist/cjs/remote-subset-wire.d.cts +59 -0
  20. package/dist/cjs/sqlite-core-adapter.cjs +969 -364
  21. package/dist/cjs/sqlite-core-adapter.cjs.map +1 -1
  22. package/dist/cjs/sqlite-core-adapter.d.cts +45 -1
  23. package/dist/cjs/sqlite-value.cjs +31 -0
  24. package/dist/cjs/sqlite-value.cjs.map +1 -0
  25. package/dist/cjs/sqlite-value.d.cts +10 -0
  26. package/dist/esm/broadcast-coordinator.d.ts +116 -0
  27. package/dist/esm/broadcast-coordinator.js +1689 -0
  28. package/dist/esm/broadcast-coordinator.js.map +1 -0
  29. package/dist/esm/errors.d.ts +44 -0
  30. package/dist/esm/errors.js +74 -1
  31. package/dist/esm/errors.js.map +1 -1
  32. package/dist/esm/index.d.ts +2 -0
  33. package/dist/esm/index.js +21 -2
  34. package/dist/esm/index.js.map +1 -1
  35. package/dist/esm/persisted.d.ts +176 -11
  36. package/dist/esm/persisted.js +1784 -396
  37. package/dist/esm/persisted.js.map +1 -1
  38. package/dist/esm/remote-subset-owner.d.ts +7 -0
  39. package/dist/esm/remote-subset-owner.js +20 -0
  40. package/dist/esm/remote-subset-owner.js.map +1 -0
  41. package/dist/esm/remote-subset-wire.d.ts +59 -0
  42. package/dist/esm/remote-subset-wire.js +681 -0
  43. package/dist/esm/remote-subset-wire.js.map +1 -0
  44. package/dist/esm/sqlite-core-adapter.d.ts +45 -1
  45. package/dist/esm/sqlite-core-adapter.js +955 -350
  46. package/dist/esm/sqlite-core-adapter.js.map +1 -1
  47. package/dist/esm/sqlite-value.d.ts +10 -0
  48. package/dist/esm/sqlite-value.js +31 -0
  49. package/dist/esm/sqlite-value.js.map +1 -0
  50. package/package.json +12 -2
  51. package/src/broadcast-coordinator.ts +2456 -0
  52. package/src/errors.ts +120 -0
  53. package/src/index.ts +2 -0
  54. package/src/persisted.ts +2617 -442
  55. package/src/remote-subset-owner.ts +36 -0
  56. package/src/remote-subset-wire.ts +946 -0
  57. package/src/sqlite-core-adapter.ts +1297 -410
  58. package/src/sqlite-value.ts +30 -0
package/README.md CHANGED
@@ -1,8 +1,7 @@
1
1
  # @tanstack/db-sqlite-persistence-core
2
2
 
3
- Shared SQLite persistence primitives for TanStack DB. Runtime-specific wrappers
4
- (Node, Electron, React Native, Cloudflare Durable Objects) build on top of this
5
- package.
3
+ Shared SQLite persistence primitives for TanStack DB. Runtime packages provide
4
+ the database drivers. Start with the [SQLite Persistence guide](../../docs/guides/sqlite-persistence.md) if you are adding persistence to an application.
6
5
 
7
6
  ## What this package provides
8
7
 
@@ -25,17 +24,40 @@ binding. Provide a runtime `SQLiteDriver` implementation from a wrapper package.
25
24
  - `TxCommitted`
26
25
  - `EnsureRemoteSubsetRequest`
27
26
  - `EnsureRemoteSubsetResponse`
27
+ - `ReleaseRemoteSubsetRequest`
28
+ - `ReleaseRemoteSubsetResponse`
29
+ - `RemoteSubsetOwner`
30
+ - `DuplicateRemoteSubsetOwnerError`
31
+ - `TransportedLoadSubsetOptions`
32
+ - `RemoteSubsetWirePrimitive`
33
+ - `RemoteSubsetWireTypedArray`
34
+ - `RemoteSubsetWireRecord`
35
+ - `RemoteSubsetWireValue`
36
+ - `RemoteSubsetWireExpression`
37
+ - `RemoteSubsetWireCompareOptions`
38
+ - `RemoteSubsetWireOrderByClause`
39
+ - `RemoteSubsetWireCursor`
40
+ - `RemoteSubsetWireValueError`
41
+ - `toTransportedLoadSubsetOptions(...)`
28
42
  - `ApplyLocalMutationsRequest`
29
43
  - `ApplyLocalMutationsResponse`
44
+ - `ApplyCommittedTxRequest`
45
+ - `ApplyCommittedTxResponse`
30
46
  - `PullSinceRequest`
31
47
  - `PullSinceResponse`
32
48
  - `CollectionReset`
33
49
  - `PersistedIndexSpec`
50
+ - `PersistedRowMetadataMutation<TKey>`
51
+ - `PersistedCollectionMetadataMutation`
52
+ - `ReplayableTxDelta<T, TKey>`
53
+ - `PersistedScannedRow<T, TKey>`
54
+ - `PersistedRowScanOptions`
55
+ - `PersistedKeySetEvidence`
34
56
  - `PersistedTx<T, TKey>`
35
- - `PersistenceAdapter<T, TKey>`
57
+ - `PersistenceAdapter`
36
58
  - `SQLiteDriver`
37
59
  - `PersistedCollectionCoordinator`
38
- - `PersistedCollectionPersistence<T, TKey>`
60
+ - `PersistedCollectionPersistence`
39
61
  - `PersistedCollectionMode`
40
62
  - `PersistedCollectionLeadershipState`
41
63
  - `PersistedCollectionUtils`
@@ -64,12 +86,227 @@ and resolves persistence using:
64
86
  This lets runtime wrappers expose one shared persistence instance per database
65
87
  while still handling per-collection schema versions correctly.
66
88
 
89
+ ### Network-first initial rendering
90
+
91
+ Set `initialRender` on every eager persisted source Collection in a query to
92
+ let React or Solid Suspense render restored SQLite rows when the network is
93
+ slow or unavailable:
94
+
95
+ ```ts
96
+ const todos = createCollection(
97
+ persistedCollectionOptions({
98
+ id: 'todos',
99
+ getKey: (todo) => todo.id,
100
+ persistence,
101
+ sync,
102
+ initialRender: {
103
+ strategy: 'network-first',
104
+ networkTimeoutMs: 3_000,
105
+ },
106
+ }),
107
+ )
108
+
109
+ const result = useLiveQuery((q) => q.from({ todos }))
110
+ if (result.isPersistedReady) {
111
+ // The eager persisted restore completed, even if the network is pending.
112
+ }
113
+ ```
114
+
115
+ The live-query observer and framework hooks expose `persistedStatus`
116
+ (`unavailable`, `loading`, `ready`, or `error`), `isPersistedReady`, and
117
+ `persistedError`. `ready` means every source Collection in the query opted in
118
+ and completed its current eager persisted restore; an empty database also
119
+ counts as ready. One unconfigured source makes the query's persisted status
120
+ `unavailable`. A failed restore reports its original error through
121
+ `persistedError`. An SSR seed does not count as persisted restore.
122
+
123
+ The initial render prefers Collection readiness. If the network has not made
124
+ the query ready after `networkTimeoutMs`, Suspense may use a completed
125
+ persisted restore. A network failure allows that fallback sooner. If the
126
+ restore is still loading, the query waits for it or for the network. The
127
+ deadline never cancels network sync. The default deadline is 3 seconds;
128
+ `networkTimeoutMs: 0` allows fallback as soon as restore finishes. For a query
129
+ with multiple opted-in sources, the longest deadline applies. An unconfigured
130
+ source keeps the existing network-readiness gate. A query failure that did not
131
+ come from a source is still an error, not a network fallback. Ordinary
132
+ `useLiveQuery` row updates and Collection `status`, `isReady`, and `preload()` are unchanged;
133
+ `status` is a Collection lifecycle signal, not a pure network-health signal.
134
+
135
+ In React, a client query stream failure before the first render also allows a
136
+ completed persisted restore to release Suspense. React waits if the stream
137
+ fails before restore completes, including when it failed before the component
138
+ mounted. The derived query result can be empty. A derived-query failure still
139
+ reaches the Error Boundary.
140
+
141
+ Persisted readiness does not prove current authorization. A source error can
142
+ permit restored rows to render even when that error reports an authorization
143
+ failure. Applications with user- or tenant-specific data must isolate persisted
144
+ storage by identity and clear or replace it when the identity changes. If a
145
+ policy denial must hide cached rows, gate the view separately; `initialRender`
146
+ does not classify source errors.
147
+
148
+ `initialRender` is rejected with `syncMode: 'on-demand'`, since an on-demand
149
+ Collection has no complete startup snapshot to certify.
150
+
151
+ ### Coordinator contract
152
+
153
+ Every `PersistedCollectionCoordinator` must implement
154
+ `requestApplyCommittedTx(collectionId, tx)`. A sync transaction with durable
155
+ effects is routed in full to the persistence adapter owned by that collection.
156
+ The `PersistedTx` includes any truncate, row mutations, row metadata mutations,
157
+ collection metadata mutations, and stream position. Multiprocess coordinators
158
+ must preserve the complete transaction when they forward it to the elected
159
+ writer. An effect-free source commit does not allocate a stream position,
160
+ publish an invalidation, or call persistence; the next durable transaction
161
+ retains the next coordinator-owned sequence.
162
+
163
+ This is a required invariant, not a capability to detect at commit time. The
164
+ TypeScript interface requires the method, and `persistedCollectionOptions(...)`
165
+ validates custom coordinators when it configures a collection. An untyped
166
+ integration that omits the method throws
167
+ `InvalidPersistedCollectionCoordinatorError` before the sync source starts or
168
+ publishes rows. Implementations must not fall back to a row-only route such as
169
+ `requestApplyLocalMutations(...)`, because that would discard transaction
170
+ semantics.
171
+
172
+ After a mutating RPC transport failure, replay is allowed only while the
173
+ requester can prove the same non-null leader id and term still own the route.
174
+ If either value was unknown for the first attempt, or either value changes,
175
+ the request rejects with `IndeterminateCommitError` and requires application
176
+ reconciliation. Coordinators never retry an indeterminate mutation against an
177
+ unknown or replacement leader.
178
+
179
+ `SingleProcessCoordinator` also implements the complete route. It is always
180
+ leader and has no cross-process transport, but it applies the transaction to
181
+ the resolved adapter registered for the specific collection. The Browser and
182
+ Electron coordinators provide the corresponding elected-owner route in their
183
+ runtime packages.
184
+
185
+ An adapter/storage rejection is surfaced as
186
+ `PersistedCollectionDurabilityError`, never as `CONFLICT`. The error preserves
187
+ the original local cause and its `code` and `path` fields. A transported
188
+ `PERSISTENCE_ERROR` response carries the source code/path that can cross the
189
+ coordinator boundary. Source writes remain published before their applied
190
+ receipt settles; if persistence then fails, the receipt rejects with the named
191
+ error and the collection enters its existing `error` lifecycle state. The
192
+ collection does not continue, retry, or fall back to a partial writer.
193
+
194
+ ### Remote subset wire contract
195
+
196
+ `requestEnsureRemoteSubset(collectionId, options)` accepts the full live
197
+ `LoadSubsetOptions` input. Before choosing a local owner or remote transport,
198
+ every built-in coordinator projects it once to the exported
199
+ `TransportedLoadSubsetOptions` contract. A registered remote-subset owner
200
+ receives that transported type, never live-only fields such as `signal` or
201
+ `subscription`.
202
+
203
+ Expression values may contain structured-clone-safe primitives, plain records,
204
+ arrays (including sparse arrays), `Date`, zero-position `RegExp`, fixed
205
+ `ArrayBuffer`, fixed `DataView`, the views listed by the exported
206
+ `RemoteSubsetWireTypedArray` union (`Int8Array`, `Uint8Array`,
207
+ `Uint8ClampedArray`, `Int16Array`, `Uint16Array`, `Int32Array`, `Uint32Array`,
208
+ `Float32Array`, `Float64Array`, `BigInt64Array`, and `BigUint64Array`), `Map`,
209
+ and `Set`. Cycles and aliases are preserved. Other view types; shared,
210
+ resizable, growable, or detached buffers; accessors; non-enumerable or symbol
211
+ keys; native expandos; custom prototypes; promises; weak collections;
212
+ functions; and symbols are outside the contract. A rejected value throws the exported
213
+ `RemoteSubsetWireValueError` before owner work or transport, with its exact
214
+ path in the request. The wire contract does not provide codecs, coercion, or
215
+ lossy normalization for unsupported values.
216
+
217
+ Remote subset ownership is lease-based. A coordinator gives each accepted
218
+ request object a stable acquisition identity scoped to its collection and
219
+ requester. Retrying the same acquisition coalesces with the original; a second
220
+ request object with equal values is an independent lease. Releasing a lease
221
+ calls the registered owner's required `unloadSubset(...)` method with the exact
222
+ transported options object that was loaded. Releasing it again is a no-op.
223
+ `EnsureRemoteSubsetRequest.acquisitionId` is the wire identity established by
224
+ acquisition; `ReleaseRemoteSubsetRequest.acquisitionId` must carry that same
225
+ identity so the elected owner releases the matching lease.
226
+
227
+ The owner also provides a required `onError(error)` callback. A load or unload
228
+ throw by the actual elected owner, or a runtime thenable rejection from either
229
+ operation, is reported through that callback with the original value before
230
+ the awaited request rejects. A follower transport or remote-owner admission
231
+ failure rejects the acquisition and remains eligible for the existing demand
232
+ retry; it is not a failure of that follower's local owner lifecycle. Retirement
233
+ and disposal observe and report actual owner failures while continuing cleanup
234
+ of sibling leases; they do not retry or silently recover the failed lifecycle.
235
+
236
+ Only one remote subset owner may be registered per collection in a coordinator.
237
+ A second live registration throws `DuplicateRemoteSubsetOwnerError` instead of
238
+ replacing the first, and startup reports the named failure before readiness
239
+ rather than leaving a rejected registration promise unobserved. Released
240
+ acquisition identities remain as dedupe tombstones for the coordinator's
241
+ existing RPC dedupe horizon, then expire during later subset traffic. When a
242
+ leader loses ownership or is disposed, it unloads every transferred lease. A
243
+ requester that still owns a lease replays that same acquisition when the next
244
+ leader is observed. There is no adapter fallback or direct-writer escape hatch
245
+ for missing ownership.
246
+
247
+ The named-error and exact-path guarantee covers values the boundary can
248
+ identify with standard JavaScript reflection. A fully transparent `Proxy` is
249
+ outside the public wire type and cannot be portably distinguished from its
250
+ target. Untyped callers must not rely on Proxy identity, traps, or a
251
+ Proxy-specific diagnostic; no non-standard detection is attempted.
252
+
253
+ ### Source replay ordering
254
+
255
+ The persisted wrapper buffers authoritative source sync transactions and
256
+ replays them into the Collection in FIFO order, one transaction at a time.
257
+ Local optimistic mutations remain visible immediately; this queue governs
258
+ source publication and its durable suffix. A source may mark a transaction
259
+ `immediate` when it must update the authoritative base beneath an active
260
+ optimistic layer. The wrapper still gives that transaction one FIFO turn and
261
+ writes its durable suffix before admitting the next source turn.
262
+
263
+ The wrapper deliberately rejects schedules that would require a second
264
+ ordering mechanism. An `immediate` source transaction cannot enter while an
265
+ earlier normal source publication is waiting, and a source transaction cannot
266
+ cross a hydration cycle. These cases throw
267
+ `InvalidPersistedCollectionConfigError` instead of reordering data or waiting
268
+ in a dependency cycle.
269
+
270
+ ### Atomic resume snapshots
271
+
272
+ Persistence adapters implement
273
+ `loadResumeSnapshot(collectionId, options)` to let a sync source certify a
274
+ persisted resume baseline. One call must read rows, collection metadata, stream
275
+ position, reset epoch, and key-set evidence from the same atomic database
276
+ snapshot. `includeRows: false` requests the same certification data without
277
+ materializing rows; `requiredIndexSignatures` carries the indexes needed by a
278
+ row-bearing snapshot.
279
+
280
+ `PersistedKeySetEvidence.status` has three states:
281
+
282
+ - `consistent`: the persisted rows match the adapter's durable expected-key
283
+ ledger.
284
+ - `incompatible`: row loss, substitution, or a reset-generation change makes
285
+ the saved resume baseline unsafe.
286
+ - `unknown`: the adapter has no authoritative pre-migration key set and does
287
+ not claim completeness.
288
+
289
+ The method is required because the versioned `metadata.persistence` capability
290
+ always carries hydration, durable row scanning, certification, evidence, and
291
+ generation ownership as one complete bundle. Sync wrappers must forward the
292
+ capability object unchanged rather than copying individual methods. Adapter
293
+ methods are invoked with their receiver and may rely on instance state through
294
+ `this`.
295
+
296
+ `SyncMetadataApi.persistence` is always present. Core sync sources receive
297
+ `null`, which explicitly means that no persistence bridge is active. The
298
+ persisted wrapper replaces that sentinel with the complete versioned
299
+ capability. A wrapper that omits the property is invalid and fails before a
300
+ consumer can resume or query against uncertified durable state.
301
+
67
302
  ### SQLite core adapter APIs
68
303
 
69
304
  - `SQLiteCoreAdapterOptions`
70
305
  - `SQLitePullSinceResult<TKey>`
71
- - `SQLiteCorePersistenceAdapter<T, TKey>`
72
- - `createSQLiteCorePersistenceAdapter<T, TKey>(...)`
306
+ - `SQLiteCorePersistenceAdapter`
307
+ - `DEFAULT_APPLIED_TX_PRUNE_MAX_ROWS`
308
+ - `DEFAULT_APPLIED_TX_PRUNE_MAX_AGE_SECONDS`
309
+ - `createSQLiteCorePersistenceAdapter(...)`
73
310
 
74
311
  ### Error APIs
75
312
 
@@ -78,10 +315,21 @@ while still handling per-collection schema versions correctly.
78
315
  - `InvalidSyncConfigError`
79
316
  - `InvalidPersistedCollectionCoordinatorError`
80
317
  - `InvalidPersistenceAdapterError`
318
+ - `PersistedCollectionDurabilityErrorOptions`
319
+ - `PersistedCollectionDurabilityError`
320
+ - `toPersistedCollectionDurabilityError(...)`
321
+ - `IndeterminateCommitRequestType`
322
+ - `IndeterminateCommitErrorOptions`
323
+ - `IndeterminateCommitError`
81
324
  - `InvalidPersistedStorageKeyError`
82
325
  - `InvalidPersistedStorageKeyEncodingError`
83
326
  - `PersistenceUnavailableError`
84
327
 
328
+ Adapter and coordinator commit failures reject the applied receipt and become
329
+ the collection's terminal sync error as `PersistedCollectionDurabilityError`.
330
+ That error preserves its original `cause`, `code`, and `path`. Hydration and
331
+ source errors remain distinct and are not wrapped as durability errors.
332
+
85
333
  ## Typical usage (via runtime wrappers)
86
334
 
87
335
  In most applications, use a runtime package directly:
@@ -90,6 +338,9 @@ In most applications, use a runtime package directly:
90
338
  - `@tanstack/browser-db-sqlite-persistence`
91
339
  - `@tanstack/electron-db-sqlite-persistence`
92
340
  - `@tanstack/react-native-db-sqlite-persistence`
341
+ - `@tanstack/expo-db-sqlite-persistence`
342
+ - `@tanstack/capacitor-db-sqlite-persistence`
343
+ - `@tanstack/tauri-db-sqlite-persistence`
93
344
  - `@tanstack/cloudflare-durable-objects-db-sqlite-persistence`
94
345
 
95
346
  Those packages provide concrete drivers and runtime wiring.