@tanstack/db-sqlite-persistence-core 0.4.8 → 0.4.9
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.
- package/README.md +10 -1
- package/dist/cjs/broadcast-coordinator.cjs +117 -29
- package/dist/cjs/broadcast-coordinator.cjs.map +1 -1
- package/dist/cjs/broadcast-coordinator.d.cts +16 -8
- package/dist/cjs/index.cjs +4 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/persisted.cjs +1408 -278
- package/dist/cjs/persisted.cjs.map +1 -1
- package/dist/cjs/persisted.d.cts +62 -15
- package/dist/cjs/remote-subset-wire.cjs +11 -0
- package/dist/cjs/remote-subset-wire.cjs.map +1 -1
- package/dist/cjs/remote-subset-wire.d.cts +1 -0
- package/dist/cjs/sqlite-core-adapter.cjs +623 -73
- package/dist/cjs/sqlite-core-adapter.cjs.map +1 -1
- package/dist/cjs/sqlite-core-adapter.d.cts +46 -12
- package/dist/esm/broadcast-coordinator.d.ts +16 -8
- package/dist/esm/broadcast-coordinator.js +118 -30
- package/dist/esm/broadcast-coordinator.js.map +1 -1
- package/dist/esm/index.js +6 -2
- package/dist/esm/persisted.d.ts +62 -15
- package/dist/esm/persisted.js +1409 -279
- package/dist/esm/persisted.js.map +1 -1
- package/dist/esm/remote-subset-wire.d.ts +1 -0
- package/dist/esm/remote-subset-wire.js +11 -0
- package/dist/esm/remote-subset-wire.js.map +1 -1
- package/dist/esm/sqlite-core-adapter.d.ts +46 -12
- package/dist/esm/sqlite-core-adapter.js +626 -76
- package/dist/esm/sqlite-core-adapter.js.map +1 -1
- package/package.json +2 -2
- package/src/broadcast-coordinator.ts +153 -23
- package/src/persisted.ts +1973 -322
- package/src/remote-subset-wire.ts +12 -0
- package/src/sqlite-core-adapter.ts +760 -77
package/README.md
CHANGED
|
@@ -291,13 +291,22 @@ separate from the accepted-transaction precedence rule.
|
|
|
291
291
|
### Atomic resume snapshots
|
|
292
292
|
|
|
293
293
|
Persistence adapters implement
|
|
294
|
-
`loadResumeSnapshot(
|
|
294
|
+
`loadResumeSnapshot(target, options)` to let a sync source certify a
|
|
295
295
|
persisted resume baseline. One call must read rows, collection metadata, stream
|
|
296
296
|
position, reset epoch, and key-set evidence from the same atomic database
|
|
297
297
|
snapshot. `includeRows: false` requests the same certification data without
|
|
298
298
|
materializing rows; `requiredIndexSignatures` carries the indexes needed by a
|
|
299
299
|
row-bearing snapshot.
|
|
300
300
|
|
|
301
|
+
The adapter's data methods require an explicit `PersistedStorageTarget`. Use
|
|
302
|
+
`{ kind: 'eager', collectionId }` for an eager Collection, or
|
|
303
|
+
`{ kind: 'managed', storageCollectionId, claimId }` for a claimed on-demand
|
|
304
|
+
persisted cache generation. A bare string ID is rejected. The managed target
|
|
305
|
+
checks the claim even after its generation has been collected from SQLite;
|
|
306
|
+
choosing an eager target explicitly may reuse the same ID. Claim and
|
|
307
|
+
rotation methods take the logical Collection ID; renewal takes the physical
|
|
308
|
+
storage ID and release takes the claim ID.
|
|
309
|
+
|
|
301
310
|
`PersistedKeySetEvidence.status` has three states:
|
|
302
311
|
|
|
303
312
|
- `consistent`: the persisted rows match the adapter's durable expected-key
|
|
@@ -5,6 +5,7 @@ const committedTxAnchor = require("./committed-tx-anchor.cjs");
|
|
|
5
5
|
const errors = require("./errors.cjs");
|
|
6
6
|
const remoteSubsetWire = require("./remote-subset-wire.cjs");
|
|
7
7
|
const remoteSubsetOwner = require("./remote-subset-owner.cjs");
|
|
8
|
+
const persisted = require("./persisted.cjs");
|
|
8
9
|
const HEARTBEAT_INTERVAL_MS = 3e3;
|
|
9
10
|
const LEADERSHIP_RETRY_DELAY_MS = 200;
|
|
10
11
|
const RPC_TIMEOUT_MS = 1e4;
|
|
@@ -23,6 +24,7 @@ class BroadcastCollectionCoordinator {
|
|
|
23
24
|
constructor(options) {
|
|
24
25
|
this.nodeId = db.safeRandomUUID();
|
|
25
26
|
this.collectionAdapters = /* @__PURE__ */ new Map();
|
|
27
|
+
this.collectionClaimIds = /* @__PURE__ */ new Map();
|
|
26
28
|
this.remoteSubsetOwners = /* @__PURE__ */ new Map();
|
|
27
29
|
this.remoteSubsetIds = /* @__PURE__ */ new Map();
|
|
28
30
|
this.outboundRemoteSubsetAcquisitions = /* @__PURE__ */ new Map();
|
|
@@ -78,9 +80,14 @@ class BroadcastCollectionCoordinator {
|
|
|
78
80
|
this.defaultAdapter = adapter;
|
|
79
81
|
}
|
|
80
82
|
/** Register the persistence adapter that owns one collection. */
|
|
81
|
-
setAdapterForCollection(collectionId, adapter) {
|
|
83
|
+
setAdapterForCollection(collectionId, adapter, cacheGenerationClaimId) {
|
|
82
84
|
requireDurableElection(adapter);
|
|
83
85
|
this.collectionAdapters.set(collectionId, adapter);
|
|
86
|
+
if (cacheGenerationClaimId) {
|
|
87
|
+
this.collectionClaimIds.set(collectionId, cacheGenerationClaimId);
|
|
88
|
+
} else {
|
|
89
|
+
this.collectionClaimIds.delete(collectionId);
|
|
90
|
+
}
|
|
84
91
|
}
|
|
85
92
|
registerRemoteSubsetOwner(collectionId, owner) {
|
|
86
93
|
if (this.remoteSubsetOwners.has(collectionId)) {
|
|
@@ -224,10 +231,29 @@ class BroadcastCollectionCoordinator {
|
|
|
224
231
|
}
|
|
225
232
|
try {
|
|
226
233
|
await acquisition.release;
|
|
234
|
+
} catch (error) {
|
|
235
|
+
this.scheduleRemoteSubsetReleaseRetry(acquisition, options);
|
|
236
|
+
throw error;
|
|
227
237
|
} finally {
|
|
228
238
|
acquisition.release = null;
|
|
229
239
|
}
|
|
230
240
|
}
|
|
241
|
+
scheduleRemoteSubsetReleaseRetry(acquisition, options) {
|
|
242
|
+
const key = remoteSubsetAcquisitionKey(
|
|
243
|
+
acquisition.collectionId,
|
|
244
|
+
acquisition.acquisitionId
|
|
245
|
+
);
|
|
246
|
+
if (this.isDisposed() || this.outboundRemoteSubsetAcquisitions.get(key) !== acquisition || acquisition.retryTimer !== null) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
acquisition.retryTimer = setTimeout(() => {
|
|
250
|
+
acquisition.retryTimer = null;
|
|
251
|
+
void this.requestReleaseRemoteSubset(
|
|
252
|
+
acquisition.collectionId,
|
|
253
|
+
options
|
|
254
|
+
).catch(() => void 0);
|
|
255
|
+
}, RPC_RETRY_DELAY_MS);
|
|
256
|
+
}
|
|
231
257
|
async acquireRemoteSubset(acquisition) {
|
|
232
258
|
if (acquisition.inFlight) return acquisition.inFlight;
|
|
233
259
|
const route = { localOwner: false };
|
|
@@ -287,7 +313,9 @@ class BroadcastCollectionCoordinator {
|
|
|
287
313
|
try {
|
|
288
314
|
await work;
|
|
289
315
|
acquired = true;
|
|
290
|
-
|
|
316
|
+
if (!acquisition.releaseRequested) {
|
|
317
|
+
this.cancelRemoteSubsetReplayRetry(acquisition);
|
|
318
|
+
}
|
|
291
319
|
} finally {
|
|
292
320
|
if (acquisition.inFlight === work) acquisition.inFlight = null;
|
|
293
321
|
const current = this.collections.get(acquisition.collectionId);
|
|
@@ -304,21 +332,34 @@ class BroadcastCollectionCoordinator {
|
|
|
304
332
|
}
|
|
305
333
|
}
|
|
306
334
|
}
|
|
307
|
-
async requestEnsurePersistedIndex(collectionId, signature, spec, scopedAdapter, localEnsureCompleted = false) {
|
|
335
|
+
async requestEnsurePersistedIndex(collectionId, signature, spec, scopedAdapter, localEnsureCompleted = false, cacheGenerationClaimId) {
|
|
308
336
|
if (this.isLeader(collectionId)) {
|
|
309
337
|
if (localEnsureCompleted) return;
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
338
|
+
const adapter = scopedAdapter ?? this.requireAdapter(collectionId);
|
|
339
|
+
if (cacheGenerationClaimId) {
|
|
340
|
+
await adapter.ensureIndex(
|
|
341
|
+
persisted.managedStorageTarget(collectionId, cacheGenerationClaimId),
|
|
342
|
+
signature,
|
|
343
|
+
spec,
|
|
344
|
+
{
|
|
345
|
+
cacheGenerationClaimId
|
|
346
|
+
}
|
|
347
|
+
);
|
|
348
|
+
} else {
|
|
349
|
+
await adapter.ensureIndex(
|
|
350
|
+
persisted.eagerStorageTarget(collectionId),
|
|
351
|
+
signature,
|
|
352
|
+
spec
|
|
353
|
+
);
|
|
354
|
+
}
|
|
315
355
|
return;
|
|
316
356
|
}
|
|
317
357
|
const response = await this.sendRPC(collectionId, {
|
|
318
358
|
type: `rpc:ensurePersistedIndex:req`,
|
|
319
359
|
rpcId: db.safeRandomUUID(),
|
|
320
360
|
signature,
|
|
321
|
-
spec
|
|
361
|
+
spec,
|
|
362
|
+
cacheGenerationClaimId
|
|
322
363
|
});
|
|
323
364
|
if (!response.ok) {
|
|
324
365
|
throw new Error(
|
|
@@ -326,7 +367,7 @@ class BroadcastCollectionCoordinator {
|
|
|
326
367
|
);
|
|
327
368
|
}
|
|
328
369
|
}
|
|
329
|
-
async requestApplyLocalMutations(collectionId, mutations) {
|
|
370
|
+
async requestApplyLocalMutations(collectionId, mutations, cacheGenerationClaimId) {
|
|
330
371
|
const initialRoute = this.waitForInitialRoute(collectionId);
|
|
331
372
|
if (initialRoute) await initialRoute;
|
|
332
373
|
if (this.isLeader(collectionId)) {
|
|
@@ -334,14 +375,16 @@ class BroadcastCollectionCoordinator {
|
|
|
334
375
|
type: `rpc:applyLocalMutations:req`,
|
|
335
376
|
rpcId: db.safeRandomUUID(),
|
|
336
377
|
envelopeId: db.safeRandomUUID(),
|
|
337
|
-
mutations
|
|
378
|
+
mutations,
|
|
379
|
+
cacheGenerationClaimId
|
|
338
380
|
});
|
|
339
381
|
}
|
|
340
382
|
return this.sendRPC(collectionId, {
|
|
341
383
|
type: `rpc:applyLocalMutations:req`,
|
|
342
384
|
rpcId: db.safeRandomUUID(),
|
|
343
385
|
envelopeId: db.safeRandomUUID(),
|
|
344
|
-
mutations
|
|
386
|
+
mutations,
|
|
387
|
+
cacheGenerationClaimId
|
|
345
388
|
});
|
|
346
389
|
}
|
|
347
390
|
async requestApplyCommittedTx(collectionId, tx, scopedAdapter) {
|
|
@@ -382,14 +425,15 @@ class BroadcastCollectionCoordinator {
|
|
|
382
425
|
}
|
|
383
426
|
return response;
|
|
384
427
|
}
|
|
385
|
-
async pullSince(collectionId, fromRowVersion, scopedAdapter) {
|
|
428
|
+
async pullSince(collectionId, fromRowVersion, scopedAdapter, cacheGenerationClaimId) {
|
|
386
429
|
if (this.isLeader(collectionId)) {
|
|
387
430
|
return this.handlePullSince(
|
|
388
431
|
collectionId,
|
|
389
432
|
{
|
|
390
433
|
type: `rpc:pullSince:req`,
|
|
391
434
|
rpcId: db.safeRandomUUID(),
|
|
392
|
-
fromRowVersion
|
|
435
|
+
fromRowVersion,
|
|
436
|
+
cacheGenerationClaimId
|
|
393
437
|
},
|
|
394
438
|
scopedAdapter
|
|
395
439
|
);
|
|
@@ -397,7 +441,8 @@ class BroadcastCollectionCoordinator {
|
|
|
397
441
|
return this.sendRPC(collectionId, {
|
|
398
442
|
type: `rpc:pullSince:req`,
|
|
399
443
|
rpcId: db.safeRandomUUID(),
|
|
400
|
-
fromRowVersion
|
|
444
|
+
fromRowVersion,
|
|
445
|
+
cacheGenerationClaimId
|
|
401
446
|
});
|
|
402
447
|
}
|
|
403
448
|
// -----------------------------------------------------------------------
|
|
@@ -430,6 +475,7 @@ class BroadcastCollectionCoordinator {
|
|
|
430
475
|
this.channel.close();
|
|
431
476
|
this.collections.clear();
|
|
432
477
|
this.collectionAdapters.clear();
|
|
478
|
+
this.collectionClaimIds.clear();
|
|
433
479
|
for (const collectionId of this.remoteSubsetOwners.keys()) {
|
|
434
480
|
this.releaseInboundRemoteSubsetAcquisitions(collectionId);
|
|
435
481
|
}
|
|
@@ -490,9 +536,15 @@ class BroadcastCollectionCoordinator {
|
|
|
490
536
|
const reserveTerm = adapter.reserveLeadershipTerm;
|
|
491
537
|
if (!reserveTerm)
|
|
492
538
|
throw new errors.InvalidPersistenceAdapterError(`reserveLeadershipTerm`);
|
|
493
|
-
const pos = await this.withWriterLock(
|
|
494
|
-
|
|
495
|
-
|
|
539
|
+
const pos = await this.withWriterLock(() => {
|
|
540
|
+
const claimId = this.collectionClaimIds.get(collectionId);
|
|
541
|
+
return reserveTerm.call(
|
|
542
|
+
adapter,
|
|
543
|
+
claimId ? persisted.managedStorageTarget(collectionId, claimId) : persisted.eagerStorageTarget(collectionId),
|
|
544
|
+
state.latestTerm,
|
|
545
|
+
{ cacheGenerationClaimId: claimId }
|
|
546
|
+
);
|
|
547
|
+
});
|
|
496
548
|
state.latestTerm = pos.latestTerm;
|
|
497
549
|
state.latestSeq = pos.latestSeq;
|
|
498
550
|
state.latestRowVersion = pos.latestRowVersion;
|
|
@@ -639,6 +691,7 @@ class BroadcastCollectionCoordinator {
|
|
|
639
691
|
this.releaseLeadership(collectionId, state);
|
|
640
692
|
this.collections.delete(collectionId);
|
|
641
693
|
this.collectionAdapters.delete(collectionId);
|
|
694
|
+
this.collectionClaimIds.delete(collectionId);
|
|
642
695
|
const prefix = `${JSON.stringify([collectionId]).slice(0, -1)},`;
|
|
643
696
|
for (const key of this.appliedEnvelopes.keys()) {
|
|
644
697
|
if (key.startsWith(prefix)) this.appliedEnvelopes.delete(key);
|
|
@@ -1052,10 +1105,7 @@ class BroadcastCollectionCoordinator {
|
|
|
1052
1105
|
acquisition.released = true;
|
|
1053
1106
|
acquisition.release = (async () => {
|
|
1054
1107
|
try {
|
|
1055
|
-
await acquisition.
|
|
1056
|
-
} catch {
|
|
1057
|
-
}
|
|
1058
|
-
try {
|
|
1108
|
+
await acquisition.admitted;
|
|
1059
1109
|
if (acquisition.transferred) {
|
|
1060
1110
|
await remoteSubsetOwner.unloadRemoteSubsetOwner(acquisition.owner, acquisition.options);
|
|
1061
1111
|
}
|
|
@@ -1171,6 +1221,7 @@ class BroadcastCollectionCoordinator {
|
|
|
1171
1221
|
owner: options.owner,
|
|
1172
1222
|
options: options.options,
|
|
1173
1223
|
load: Promise.resolve(),
|
|
1224
|
+
admitted: Promise.resolve(),
|
|
1174
1225
|
transferred: false,
|
|
1175
1226
|
released: false,
|
|
1176
1227
|
terminalRelease: false,
|
|
@@ -1178,6 +1229,10 @@ class BroadcastCollectionCoordinator {
|
|
|
1178
1229
|
};
|
|
1179
1230
|
this.releasedRemoteSubsetAcquisitionTimes.delete(key);
|
|
1180
1231
|
this.inboundRemoteSubsetAcquisitions.set(key, acquisition);
|
|
1232
|
+
let resolveAdmission;
|
|
1233
|
+
acquisition.admitted = new Promise((resolve) => {
|
|
1234
|
+
resolveAdmission = resolve;
|
|
1235
|
+
});
|
|
1181
1236
|
let resolveLoad;
|
|
1182
1237
|
let rejectLoad;
|
|
1183
1238
|
acquisition.load = new Promise((resolve, reject) => {
|
|
@@ -1190,6 +1245,8 @@ class BroadcastCollectionCoordinator {
|
|
|
1190
1245
|
void Promise.resolve(load).then(resolveLoad, rejectLoad);
|
|
1191
1246
|
} catch (error) {
|
|
1192
1247
|
rejectLoad(error);
|
|
1248
|
+
} finally {
|
|
1249
|
+
resolveAdmission();
|
|
1193
1250
|
}
|
|
1194
1251
|
try {
|
|
1195
1252
|
await acquisition.load;
|
|
@@ -1214,10 +1271,23 @@ class BroadcastCollectionCoordinator {
|
|
|
1214
1271
|
}
|
|
1215
1272
|
}
|
|
1216
1273
|
async handleEnsurePersistedIndex(collectionId, request) {
|
|
1217
|
-
await this.withScheduledWriterLock(
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1274
|
+
await this.withScheduledWriterLock(collectionId, (adapter) => {
|
|
1275
|
+
if (request.cacheGenerationClaimId) {
|
|
1276
|
+
return adapter.ensureIndex(
|
|
1277
|
+
persisted.managedStorageTarget(collectionId, request.cacheGenerationClaimId),
|
|
1278
|
+
request.signature,
|
|
1279
|
+
request.spec,
|
|
1280
|
+
{
|
|
1281
|
+
cacheGenerationClaimId: request.cacheGenerationClaimId
|
|
1282
|
+
}
|
|
1283
|
+
);
|
|
1284
|
+
}
|
|
1285
|
+
return adapter.ensureIndex(
|
|
1286
|
+
persisted.eagerStorageTarget(collectionId),
|
|
1287
|
+
request.signature,
|
|
1288
|
+
request.spec
|
|
1289
|
+
);
|
|
1290
|
+
});
|
|
1221
1291
|
return {
|
|
1222
1292
|
type: `rpc:ensurePersistedIndex:res`,
|
|
1223
1293
|
rpcId: request.rpcId,
|
|
@@ -1246,6 +1316,11 @@ class BroadcastCollectionCoordinator {
|
|
|
1246
1316
|
});
|
|
1247
1317
|
}
|
|
1248
1318
|
async applyLocalMutationsOnce(collectionId, request) {
|
|
1319
|
+
if (this.collectionClaimIds.has(collectionId) && !request.cacheGenerationClaimId) {
|
|
1320
|
+
throw new errors.InvalidPersistedCollectionConfigError(
|
|
1321
|
+
`A managed local mutation requires a persisted cache claim`
|
|
1322
|
+
);
|
|
1323
|
+
}
|
|
1249
1324
|
const state = this.collections.get(collectionId);
|
|
1250
1325
|
if (!state || !state.isLeader) {
|
|
1251
1326
|
return {
|
|
@@ -1271,6 +1346,7 @@ class BroadcastCollectionCoordinator {
|
|
|
1271
1346
|
}
|
|
1272
1347
|
const pendingTx = {
|
|
1273
1348
|
txId: db.safeRandomUUID(),
|
|
1349
|
+
cacheGenerationClaimId: request.cacheGenerationClaimId,
|
|
1274
1350
|
mutations: request.mutations.map((m) => ({
|
|
1275
1351
|
type: m.type,
|
|
1276
1352
|
key: m.key,
|
|
@@ -1503,7 +1579,10 @@ class BroadcastCollectionCoordinator {
|
|
|
1503
1579
|
if (!adapter.reconcileCommittedTx) return { kind: `unknown` };
|
|
1504
1580
|
try {
|
|
1505
1581
|
const reconciled = await adapter.reconcileCommittedTx(
|
|
1506
|
-
|
|
1582
|
+
candidate.cacheGenerationClaimId ? persisted.managedStorageTarget(
|
|
1583
|
+
collectionId,
|
|
1584
|
+
candidate.cacheGenerationClaimId
|
|
1585
|
+
) : persisted.eagerStorageTarget(collectionId),
|
|
1507
1586
|
candidate,
|
|
1508
1587
|
request.anchor
|
|
1509
1588
|
);
|
|
@@ -1579,7 +1658,10 @@ class BroadcastCollectionCoordinator {
|
|
|
1579
1658
|
);
|
|
1580
1659
|
if (tx === null) return null;
|
|
1581
1660
|
try {
|
|
1582
|
-
await adapter.applyCommittedTx(
|
|
1661
|
+
await adapter.applyCommittedTx(
|
|
1662
|
+
tx.cacheGenerationClaimId ? persisted.managedStorageTarget(collectionId, tx.cacheGenerationClaimId) : persisted.eagerStorageTarget(collectionId),
|
|
1663
|
+
tx
|
|
1664
|
+
);
|
|
1583
1665
|
} catch (error) {
|
|
1584
1666
|
throw errors.toPersistedCollectionDurabilityError(collectionId, error);
|
|
1585
1667
|
}
|
|
@@ -1604,7 +1686,13 @@ class BroadcastCollectionCoordinator {
|
|
|
1604
1686
|
requiresFullReload: true
|
|
1605
1687
|
};
|
|
1606
1688
|
}
|
|
1607
|
-
const result = await adapter.pullSince(
|
|
1689
|
+
const result = await adapter.pullSince(
|
|
1690
|
+
request.cacheGenerationClaimId ? persisted.managedStorageTarget(collectionId, request.cacheGenerationClaimId) : persisted.eagerStorageTarget(collectionId),
|
|
1691
|
+
request.fromRowVersion,
|
|
1692
|
+
{
|
|
1693
|
+
cacheGenerationClaimId: request.cacheGenerationClaimId
|
|
1694
|
+
}
|
|
1695
|
+
);
|
|
1608
1696
|
if (result.requiresFullReload) {
|
|
1609
1697
|
return {
|
|
1610
1698
|
type: `rpc:pullSince:res`,
|