@tanstack/db-sqlite-persistence-core 0.4.7 → 0.4.8
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 +22 -2
- package/dist/cjs/broadcast-coordinator.cjs +169 -18
- package/dist/cjs/broadcast-coordinator.cjs.map +1 -1
- package/dist/cjs/broadcast-coordinator.d.cts +11 -2
- package/dist/cjs/committed-tx-anchor.cjs +9 -0
- package/dist/cjs/committed-tx-anchor.cjs.map +1 -0
- package/dist/cjs/committed-tx-anchor.d.cts +1 -0
- package/dist/cjs/persisted.cjs +199 -43
- package/dist/cjs/persisted.cjs.map +1 -1
- package/dist/cjs/persisted.d.cts +46 -0
- package/dist/cjs/sqlite-core-adapter.cjs +94 -3
- package/dist/cjs/sqlite-core-adapter.cjs.map +1 -1
- package/dist/cjs/sqlite-core-adapter.d.cts +10 -1
- package/dist/esm/broadcast-coordinator.d.ts +11 -2
- package/dist/esm/broadcast-coordinator.js +170 -19
- package/dist/esm/broadcast-coordinator.js.map +1 -1
- package/dist/esm/committed-tx-anchor.d.ts +1 -0
- package/dist/esm/committed-tx-anchor.js +9 -0
- package/dist/esm/committed-tx-anchor.js.map +1 -0
- package/dist/esm/persisted.d.ts +46 -0
- package/dist/esm/persisted.js +201 -45
- package/dist/esm/persisted.js.map +1 -1
- package/dist/esm/sqlite-core-adapter.d.ts +10 -1
- package/dist/esm/sqlite-core-adapter.js +94 -3
- package/dist/esm/sqlite-core-adapter.js.map +1 -1
- package/package.json +2 -2
- package/src/broadcast-coordinator.ts +267 -44
- package/src/committed-tx-anchor.ts +13 -0
- package/src/persisted.ts +315 -53
- package/src/sqlite-core-adapter.ts +143 -3
package/dist/esm/persisted.d.ts
CHANGED
|
@@ -49,6 +49,7 @@ export type TxCommitted = {
|
|
|
49
49
|
latestRowVersion: number;
|
|
50
50
|
} & ({
|
|
51
51
|
requiresFullReload: true;
|
|
52
|
+
reconciliationReload?: true;
|
|
52
53
|
} | {
|
|
53
54
|
requiresFullReload: false;
|
|
54
55
|
changedRows: Array<{
|
|
@@ -149,6 +150,41 @@ export type ApplyCommittedTxResponse = {
|
|
|
149
150
|
sourceCode?: string | number;
|
|
150
151
|
path?: string | ReadonlyArray<string | number>;
|
|
151
152
|
};
|
|
153
|
+
/** The durable version read before a source transaction enters coordination. */
|
|
154
|
+
export type CommittedTxAnchor = {
|
|
155
|
+
latestRowVersion: number;
|
|
156
|
+
resetEpoch: number;
|
|
157
|
+
};
|
|
158
|
+
export type ReconcileCommittedTxResponse = {
|
|
159
|
+
type: `rpc:reconcileCommittedTx:res`;
|
|
160
|
+
rpcId: string;
|
|
161
|
+
ok: true;
|
|
162
|
+
alreadyApplied: boolean;
|
|
163
|
+
committed: {
|
|
164
|
+
term: number;
|
|
165
|
+
seq: number;
|
|
166
|
+
rowVersion: number;
|
|
167
|
+
};
|
|
168
|
+
} | {
|
|
169
|
+
type: `rpc:reconcileCommittedTx:res`;
|
|
170
|
+
rpcId: string;
|
|
171
|
+
ok: false;
|
|
172
|
+
code: `NOT_LEADER` | `UNDETERMINED` | `PERSISTENCE_ERROR`;
|
|
173
|
+
error: string;
|
|
174
|
+
sourceCode?: string | number;
|
|
175
|
+
path?: string | ReadonlyArray<string | number>;
|
|
176
|
+
};
|
|
177
|
+
export type ReconciledCommittedTx = {
|
|
178
|
+
kind: `already-applied` | `applied-now`;
|
|
179
|
+
committed: {
|
|
180
|
+
term: number;
|
|
181
|
+
seq: number;
|
|
182
|
+
rowVersion: number;
|
|
183
|
+
};
|
|
184
|
+
latestRowVersion: number;
|
|
185
|
+
} | {
|
|
186
|
+
kind: `unknown`;
|
|
187
|
+
};
|
|
152
188
|
export type PullSinceRequest = {
|
|
153
189
|
type: `rpc:pullSince:req`;
|
|
154
190
|
rpcId: string;
|
|
@@ -299,6 +335,8 @@ export interface PersistenceAdapter {
|
|
|
299
335
|
resetEpoch: number;
|
|
300
336
|
}>;
|
|
301
337
|
applyCommittedTx: (collectionId: string, tx: PersistedTx) => Promise<void>;
|
|
338
|
+
/** Atomically certify or apply one source transaction after a lost response. */
|
|
339
|
+
reconcileCommittedTx?: (collectionId: string, tx: PersistedTx, anchor: CommittedTxAnchor) => Promise<ReconciledCommittedTx>;
|
|
302
340
|
loadCollectionMetadata?: (collectionId: string) => Promise<Array<{
|
|
303
341
|
key: string;
|
|
304
342
|
value: unknown;
|
|
@@ -311,6 +349,12 @@ export interface PersistenceAdapter {
|
|
|
311
349
|
latestSeq: number;
|
|
312
350
|
latestRowVersion: number;
|
|
313
351
|
}>;
|
|
352
|
+
/** Reserve a new durable election term before announcing Browser leadership. */
|
|
353
|
+
reserveLeadershipTerm?: (collectionId: string, observedTerm: number) => Promise<{
|
|
354
|
+
latestTerm: number;
|
|
355
|
+
latestSeq: number;
|
|
356
|
+
latestRowVersion: number;
|
|
357
|
+
}>;
|
|
314
358
|
/**
|
|
315
359
|
* Runs one complete logical hydrate as a non-preemptible scheduler unit.
|
|
316
360
|
* The callback must use the supplied unscheduled adapter for all nested
|
|
@@ -365,6 +409,8 @@ export interface PersistedCollectionCoordinator {
|
|
|
365
409
|
* loan after the call; ordinary work uses the registered collection adapter.
|
|
366
410
|
*/
|
|
367
411
|
requestApplyCommittedTx: (collectionId: string, tx: PersistedTx, scopedAdapter?: HydrationPersistenceAdapter) => Promise<ApplyCommittedTxResponse>;
|
|
412
|
+
/** Available only when the elected adapter can certify the durable outcome. */
|
|
413
|
+
reconcileCommittedTx?: (collectionId: string, tx: PersistedTx, anchor: CommittedTxAnchor, scopedAdapter?: HydrationPersistenceAdapter) => Promise<ReconcileCommittedTxResponse>;
|
|
368
414
|
/** The scoped adapter is leader-local and is never serialized to a follower. */
|
|
369
415
|
pullSince?: (collectionId: string, fromRowVersion: number, scopedAdapter?: HydrationPersistenceAdapter) => Promise<PullSinceResponse>;
|
|
370
416
|
}
|
package/dist/esm/persisted.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { safeRandomUUID, withCollectionConfigFactory, whenSyncAccepted, SyncTransactionAbortedError, getLoadSubsetDemandKey, compileSingleRowExpression, toBooleanPredicate, SYNC_PERSISTENCE_VERSION, SYNC_PERSISTENCE_PROTOCOL, PERSISTED_READINESS, NoPendingSyncTransactionCommitError, NoPendingSyncTransactionWriteError, withAcceptedReceipt } from "@tanstack/db";
|
|
2
|
-
import { InvalidPersistedCollectionConfigError, DuplicateRemoteSubsetOwnerError, toPersistedCollectionDurabilityError, InvalidPersistedStorageKeyEncodingError, InvalidPersistedStorageKeyError, InvalidSyncConfigError, InvalidPersistedCollectionCoordinatorError, PersistedCollectionDurabilityError, InvalidPersistenceAdapterError } from "./errors.js";
|
|
1
|
+
import { safeRandomUUID, withCollectionConfigFactory, whenSyncAccepted, SyncTransactionAbortedError, getLoadSubsetDemandKey, equalPersistedSnapshotValues, compileSingleRowExpression, toBooleanPredicate, SYNC_PERSISTENCE_VERSION, SYNC_PERSISTENCE_PROTOCOL, PERSISTED_READINESS, NoPendingSyncTransactionCommitError, NoPendingSyncTransactionWriteError, withAcceptedReceipt } from "@tanstack/db";
|
|
2
|
+
import { InvalidPersistedCollectionConfigError, DuplicateRemoteSubsetOwnerError, toPersistedCollectionDurabilityError, InvalidPersistedStorageKeyEncodingError, InvalidPersistedStorageKeyError, InvalidSyncConfigError, InvalidPersistedCollectionCoordinatorError, PersistedCollectionDurabilityError, IndeterminateCommitError, InvalidPersistenceAdapterError } from "./errors.js";
|
|
3
3
|
import { serializeSQLiteBigInt, serializeSQLiteTemporal } from "./sqlite-value.js";
|
|
4
4
|
import { toTransportedLoadSubsetOptions, toProcessLocalLoadSubsetOptions, validateRemoteSubsetOptions } from "./remote-subset-wire.js";
|
|
5
5
|
import { reportRemoteSubsetOwnerError, unloadRemoteSubsetOwner } from "./remote-subset-owner.js";
|
|
@@ -391,7 +391,7 @@ function isTxCommittedPayload(payload) {
|
|
|
391
391
|
return false;
|
|
392
392
|
}
|
|
393
393
|
if (payload.requiresFullReload) {
|
|
394
|
-
return true;
|
|
394
|
+
return payload.reconciliationReload === void 0 || payload.reconciliationReload === true;
|
|
395
395
|
}
|
|
396
396
|
return Array.isArray(payload.changedRows) && Array.isArray(payload.deletedKeys) && (payload.rowMetadataMutations === void 0 || Array.isArray(payload.rowMetadataMutations)) && (payload.collectionMetadataMutations === void 0 || Array.isArray(payload.collectionMetadataMutations));
|
|
397
397
|
}
|
|
@@ -457,6 +457,7 @@ class PersistedCollectionRuntime {
|
|
|
457
457
|
this.latestTerm = 0;
|
|
458
458
|
this.latestSeq = 0;
|
|
459
459
|
this.latestRowVersion = 0;
|
|
460
|
+
this.publicRowVersion = 0;
|
|
460
461
|
this.localTerm = 1;
|
|
461
462
|
this.localSeq = 0;
|
|
462
463
|
this.localRowVersion = 0;
|
|
@@ -1134,6 +1135,8 @@ class PersistedCollectionRuntime {
|
|
|
1134
1135
|
this.lifecycleGeneration++;
|
|
1135
1136
|
this.persistedReadiness?.set({ status: `loading` });
|
|
1136
1137
|
this.startupSettled = false;
|
|
1138
|
+
this.publicRowVersion = 0;
|
|
1139
|
+
this.resetTermFence = void 0;
|
|
1137
1140
|
this.hydratedDemands.clear();
|
|
1138
1141
|
this.startupMetadataPromise = null;
|
|
1139
1142
|
this.startPromise = null;
|
|
@@ -1178,6 +1181,7 @@ class PersistedCollectionRuntime {
|
|
|
1178
1181
|
}
|
|
1179
1182
|
async hydrateSubsetUnsafe(options, config, adapter) {
|
|
1180
1183
|
let rowsLoaded = false;
|
|
1184
|
+
let boundRowVersion;
|
|
1181
1185
|
let replayFailure;
|
|
1182
1186
|
const resetSequence = this.resetSequence;
|
|
1183
1187
|
this.hydrationSequence++;
|
|
@@ -1197,6 +1201,7 @@ class PersistedCollectionRuntime {
|
|
|
1197
1201
|
if (config.lifecycleGeneration !== this.lifecycleGeneration) return;
|
|
1198
1202
|
if (resetSequence !== this.resetSequence) return;
|
|
1199
1203
|
this.bindResumeSnapshotEvidence(snapshot);
|
|
1204
|
+
boundRowVersion = snapshot.latestRowVersion;
|
|
1200
1205
|
} else {
|
|
1201
1206
|
rows = await this.loadSubsetRowsUnsafe(options, adapter);
|
|
1202
1207
|
}
|
|
@@ -1209,6 +1214,12 @@ class PersistedCollectionRuntime {
|
|
|
1209
1214
|
}
|
|
1210
1215
|
const applied = this.applyRowsToCollection(rows);
|
|
1211
1216
|
await whenSyncAccepted(applied);
|
|
1217
|
+
if (boundRowVersion !== void 0) {
|
|
1218
|
+
this.publicRowVersion = Math.max(
|
|
1219
|
+
this.publicRowVersion,
|
|
1220
|
+
boundRowVersion
|
|
1221
|
+
);
|
|
1222
|
+
}
|
|
1212
1223
|
}
|
|
1213
1224
|
} finally {
|
|
1214
1225
|
if (this.activeHydrationContext === hydrationContext && this.hydratingGeneration === config.lifecycleGeneration) {
|
|
@@ -1372,7 +1383,9 @@ class PersistedCollectionRuntime {
|
|
|
1372
1383
|
}
|
|
1373
1384
|
isExpectedResumeGeneration(generation) {
|
|
1374
1385
|
const expected = this.persistedResumeGeneration;
|
|
1375
|
-
return expected !== void 0 && expected.
|
|
1386
|
+
return expected !== void 0 && expected.latestRowVersion === generation.latestRowVersion && expected.resetEpoch === generation.resetEpoch && (expected.latestTerm === generation.latestTerm && expected.latestSeq === generation.latestSeq || // A no-write election reserves a new term at sequence zero. It does
|
|
1387
|
+
// not change the persisted rows or the source resume cursor.
|
|
1388
|
+
generation.latestTerm > expected.latestTerm && generation.latestSeq === 0);
|
|
1376
1389
|
}
|
|
1377
1390
|
bindResumeSnapshotEvidence(snapshot) {
|
|
1378
1391
|
const generation = this.getResumeSnapshotGeneration(snapshot);
|
|
@@ -1414,6 +1427,27 @@ class PersistedCollectionRuntime {
|
|
|
1414
1427
|
return this.syncControls.commit?.() ?? true;
|
|
1415
1428
|
});
|
|
1416
1429
|
}
|
|
1430
|
+
matchesCollectionSnapshot(rows, collectionMetadata) {
|
|
1431
|
+
if (!this.collection || !this.syncControls.metadata) return false;
|
|
1432
|
+
const base = this.collection.base;
|
|
1433
|
+
if (base.size !== rows.size) return false;
|
|
1434
|
+
for (const [key, row] of rows) {
|
|
1435
|
+
if (!base.has(key) || !equalPersistedSnapshotValues(base.get(key), row.value) || !equalPersistedSnapshotValues(
|
|
1436
|
+
this.syncControls.metadata.row.get(key),
|
|
1437
|
+
row.metadata
|
|
1438
|
+
)) {
|
|
1439
|
+
return false;
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
const currentMetadata = this.syncControls.metadata.collection.list();
|
|
1443
|
+
if (currentMetadata.length !== collectionMetadata.length) return false;
|
|
1444
|
+
const expectedMetadata = new Map(
|
|
1445
|
+
collectionMetadata.map(({ key, value }) => [key, value])
|
|
1446
|
+
);
|
|
1447
|
+
return currentMetadata.every(
|
|
1448
|
+
({ key, value }) => expectedMetadata.has(key) && equalPersistedSnapshotValues(expectedMetadata.get(key), value)
|
|
1449
|
+
);
|
|
1450
|
+
}
|
|
1417
1451
|
async flushQueuedHydrationTransactionsUnsafe(adapter) {
|
|
1418
1452
|
let operationFailure;
|
|
1419
1453
|
while (this.queuedHydrationTransactions.length > 0) {
|
|
@@ -1498,16 +1532,55 @@ class PersistedCollectionRuntime {
|
|
|
1498
1532
|
if (!transaction.truncate && transaction.operations.length === 0 && transaction.rowMetadataWrites.size === 0 && transaction.collectionMetadataWrites.size === 0) {
|
|
1499
1533
|
return;
|
|
1500
1534
|
}
|
|
1535
|
+
const anchor = this.persistedResumeGeneration && {
|
|
1536
|
+
latestRowVersion: this.latestRowVersion,
|
|
1537
|
+
resetEpoch: this.persistedResumeGeneration.resetEpoch
|
|
1538
|
+
};
|
|
1501
1539
|
const streamPosition = this.nextLocalStreamPosition();
|
|
1502
1540
|
const tx = this.createPersistedTxFromOperations(transaction, streamPosition);
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1541
|
+
let response;
|
|
1542
|
+
try {
|
|
1543
|
+
response = await this.persistence.coordinator.requestApplyCommittedTx(
|
|
1544
|
+
this.collectionId,
|
|
1545
|
+
tx,
|
|
1546
|
+
// A hydration-scoped adapter is already inside the shared scheduler.
|
|
1547
|
+
// Ordinary source commits enter it before taking the writer lock.
|
|
1548
|
+
scopedAdapter
|
|
1549
|
+
);
|
|
1550
|
+
} catch (error) {
|
|
1551
|
+
if (!(error instanceof IndeterminateCommitError) || !anchor || !this.persistence.coordinator.reconcileCommittedTx) {
|
|
1552
|
+
throw error;
|
|
1553
|
+
}
|
|
1554
|
+
let reconciled;
|
|
1555
|
+
try {
|
|
1556
|
+
reconciled = await this.persistence.coordinator.reconcileCommittedTx(
|
|
1557
|
+
this.collectionId,
|
|
1558
|
+
tx,
|
|
1559
|
+
anchor,
|
|
1560
|
+
scopedAdapter
|
|
1561
|
+
);
|
|
1562
|
+
} catch (reconcileError) {
|
|
1563
|
+
if (reconcileError instanceof PersistedCollectionDurabilityError) {
|
|
1564
|
+
throw reconcileError;
|
|
1565
|
+
}
|
|
1566
|
+
throw error;
|
|
1567
|
+
}
|
|
1568
|
+
if (!reconciled.ok) {
|
|
1569
|
+
if (reconciled.code === `PERSISTENCE_ERROR`) {
|
|
1570
|
+
throw new PersistedCollectionDurabilityError(
|
|
1571
|
+
`Failed to reconcile collection "${this.collectionId}": ${reconciled.error}`,
|
|
1572
|
+
{
|
|
1573
|
+
cause: reconciled,
|
|
1574
|
+
code: reconciled.sourceCode ?? reconciled.code,
|
|
1575
|
+
path: reconciled.path
|
|
1576
|
+
}
|
|
1577
|
+
);
|
|
1578
|
+
}
|
|
1579
|
+
throw error;
|
|
1580
|
+
}
|
|
1581
|
+
this.recordDurableSourceCommit(transaction, reconciled.committed);
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1511
1584
|
if (!response.ok) {
|
|
1512
1585
|
if (response.code === `PERSISTENCE_ERROR`) {
|
|
1513
1586
|
const error = new PersistedCollectionDurabilityError(
|
|
@@ -1524,18 +1597,24 @@ class PersistedCollectionRuntime {
|
|
|
1524
1597
|
`failed to apply external sync transaction through coordinator: ${response.error}`
|
|
1525
1598
|
);
|
|
1526
1599
|
}
|
|
1600
|
+
this.recordDurableSourceCommit(transaction, {
|
|
1601
|
+
term: response.term,
|
|
1602
|
+
seq: response.seq,
|
|
1603
|
+
rowVersion: response.latestRowVersion
|
|
1604
|
+
});
|
|
1605
|
+
}
|
|
1606
|
+
recordDurableSourceCommit(transaction, position) {
|
|
1527
1607
|
if (transaction.lifecycleGeneration !== this.lifecycleGeneration) return;
|
|
1528
|
-
this.observeStreamPosition(
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
);
|
|
1608
|
+
this.observeStreamPosition(position.term, position.seq, position.rowVersion);
|
|
1609
|
+
if (position.rowVersion === this.publicRowVersion + 1) {
|
|
1610
|
+
this.publicRowVersion = position.rowVersion;
|
|
1611
|
+
}
|
|
1533
1612
|
if (transaction.expectedResumeGenerationOwner === this.resumeGenerationOwner && this.persistedResumeGeneration !== void 0) {
|
|
1534
1613
|
this.persistedResumeGeneration = {
|
|
1535
1614
|
...this.persistedResumeGeneration,
|
|
1536
|
-
latestTerm:
|
|
1537
|
-
latestSeq:
|
|
1538
|
-
latestRowVersion:
|
|
1615
|
+
latestTerm: position.term,
|
|
1616
|
+
latestSeq: position.seq,
|
|
1617
|
+
latestRowVersion: position.rowVersion
|
|
1539
1618
|
};
|
|
1540
1619
|
}
|
|
1541
1620
|
}
|
|
@@ -1898,9 +1977,17 @@ class PersistedCollectionRuntime {
|
|
|
1898
1977
|
}
|
|
1899
1978
|
if (isCollectionResetPayload(payload)) {
|
|
1900
1979
|
void this.applyMutex.run(async () => {
|
|
1901
|
-
await this.runInHydrationScope(
|
|
1902
|
-
|
|
1903
|
-
|
|
1980
|
+
await this.runInHydrationScope(async (adapter) => {
|
|
1981
|
+
await this.truncateAndReloadUnsafe(adapter, lifecycleGeneration);
|
|
1982
|
+
await this.rebaseDurableStreamFromSnapshotUnsafe(
|
|
1983
|
+
adapter,
|
|
1984
|
+
lifecycleGeneration
|
|
1985
|
+
);
|
|
1986
|
+
if (lifecycleGeneration === this.lifecycleGeneration) {
|
|
1987
|
+
this.publicRowVersion = 0;
|
|
1988
|
+
this.resetTermFence = this.latestTerm;
|
|
1989
|
+
}
|
|
1990
|
+
});
|
|
1904
1991
|
if (lifecycleGeneration === this.lifecycleGeneration) {
|
|
1905
1992
|
await this.flushQueuedTxCommittedUnsafe();
|
|
1906
1993
|
}
|
|
@@ -1923,21 +2010,42 @@ class PersistedCollectionRuntime {
|
|
|
1923
2010
|
}
|
|
1924
2011
|
async processCommittedTxUnsafe(txCommitted, lifecycleGeneration = this.lifecycleGeneration) {
|
|
1925
2012
|
if (lifecycleGeneration !== this.lifecycleGeneration) return;
|
|
1926
|
-
if (
|
|
1927
|
-
|
|
2013
|
+
if (this.resetTermFence !== void 0) {
|
|
2014
|
+
if (txCommitted.term <= this.resetTermFence) {
|
|
2015
|
+
await this.runInHydrationScope(
|
|
2016
|
+
(adapter) => this.reloadActiveSubsetsUnsafe(adapter)
|
|
2017
|
+
);
|
|
2018
|
+
return;
|
|
2019
|
+
}
|
|
1928
2020
|
}
|
|
1929
|
-
if (txCommitted.
|
|
2021
|
+
if (txCommitted.requiresFullReload && txCommitted.reconciliationReload) {
|
|
2022
|
+
await this.invalidateFromCommittedTxUnsafe(
|
|
2023
|
+
txCommitted,
|
|
2024
|
+
this.persistence.adapter
|
|
2025
|
+
);
|
|
2026
|
+
if (lifecycleGeneration === this.lifecycleGeneration) {
|
|
2027
|
+
this.observeStreamPosition(
|
|
2028
|
+
txCommitted.term,
|
|
2029
|
+
txCommitted.seq,
|
|
2030
|
+
txCommitted.latestRowVersion
|
|
2031
|
+
);
|
|
2032
|
+
this.publicRowVersion = Math.max(
|
|
2033
|
+
this.publicRowVersion,
|
|
2034
|
+
txCommitted.latestRowVersion
|
|
2035
|
+
);
|
|
2036
|
+
await this.flushQueuedTxCommittedUnsafe();
|
|
2037
|
+
}
|
|
1930
2038
|
return;
|
|
1931
2039
|
}
|
|
2040
|
+
if (txCommitted.latestRowVersion <= this.publicRowVersion) return;
|
|
2041
|
+
const observedButUnpublished = txCommitted.term < this.latestTerm || txCommitted.term === this.latestTerm && txCommitted.seq <= this.latestSeq;
|
|
1932
2042
|
const hasGapInCurrentTerm = txCommitted.term === this.latestTerm && txCommitted.seq > this.latestSeq + 1;
|
|
1933
2043
|
const hasGapAcrossTerms = txCommitted.term > this.latestTerm && txCommitted.seq > 1;
|
|
1934
|
-
const hasGap = hasGapInCurrentTerm || hasGapAcrossTerms;
|
|
2044
|
+
const hasGap = observedButUnpublished || txCommitted.latestRowVersion > this.publicRowVersion + 1 || hasGapInCurrentTerm || hasGapAcrossTerms;
|
|
1935
2045
|
if (hasGap) {
|
|
1936
2046
|
await this.recoverFromSeqGapUnsafe(lifecycleGeneration);
|
|
1937
2047
|
if (lifecycleGeneration !== this.lifecycleGeneration) return;
|
|
1938
|
-
if (txCommitted.
|
|
1939
|
-
return;
|
|
1940
|
-
}
|
|
2048
|
+
if (txCommitted.latestRowVersion <= this.publicRowVersion) return;
|
|
1941
2049
|
}
|
|
1942
2050
|
this.observeStreamPosition(
|
|
1943
2051
|
txCommitted.term,
|
|
@@ -1949,9 +2057,27 @@ class PersistedCollectionRuntime {
|
|
|
1949
2057
|
this.persistence.adapter
|
|
1950
2058
|
);
|
|
1951
2059
|
if (lifecycleGeneration === this.lifecycleGeneration) {
|
|
2060
|
+
this.publicRowVersion = Math.max(
|
|
2061
|
+
this.publicRowVersion,
|
|
2062
|
+
txCommitted.latestRowVersion
|
|
2063
|
+
);
|
|
1952
2064
|
await this.flushQueuedTxCommittedUnsafe();
|
|
1953
2065
|
}
|
|
1954
2066
|
}
|
|
2067
|
+
async rebaseDurableStreamFromSnapshotUnsafe(adapter, lifecycleGeneration) {
|
|
2068
|
+
if (lifecycleGeneration !== this.lifecycleGeneration) return;
|
|
2069
|
+
const snapshot = await adapter.loadResumeSnapshot(this.collectionId, {
|
|
2070
|
+
includeRows: false
|
|
2071
|
+
});
|
|
2072
|
+
if (lifecycleGeneration !== this.lifecycleGeneration) return;
|
|
2073
|
+
this.latestRowVersion = snapshot.latestRowVersion;
|
|
2074
|
+
this.localRowVersion = snapshot.latestRowVersion;
|
|
2075
|
+
this.observeStreamPosition(
|
|
2076
|
+
snapshot.latestTerm,
|
|
2077
|
+
snapshot.latestSeq,
|
|
2078
|
+
snapshot.latestRowVersion
|
|
2079
|
+
);
|
|
2080
|
+
}
|
|
1955
2081
|
async recoverFromSeqGapUnsafe(lifecycleGeneration) {
|
|
1956
2082
|
if (lifecycleGeneration !== this.lifecycleGeneration) return;
|
|
1957
2083
|
if (this.persistence.coordinator.pullSince && this.latestRowVersion >= 0) {
|
|
@@ -1959,7 +2085,7 @@ class PersistedCollectionRuntime {
|
|
|
1959
2085
|
try {
|
|
1960
2086
|
pullResponse = await this.persistence.coordinator.pullSince(
|
|
1961
2087
|
this.collectionId,
|
|
1962
|
-
this.
|
|
2088
|
+
this.publicRowVersion
|
|
1963
2089
|
);
|
|
1964
2090
|
} catch (error) {
|
|
1965
2091
|
console.warn(`Failed pullSince recovery attempt:`, error);
|
|
@@ -1976,6 +2102,10 @@ class PersistedCollectionRuntime {
|
|
|
1976
2102
|
await this.runInHydrationScope(
|
|
1977
2103
|
(adapter) => this.reloadActiveSubsetsUnsafe(adapter)
|
|
1978
2104
|
);
|
|
2105
|
+
this.publicRowVersion = Math.max(
|
|
2106
|
+
this.publicRowVersion,
|
|
2107
|
+
pullResponse.latestRowVersion
|
|
2108
|
+
);
|
|
1979
2109
|
return;
|
|
1980
2110
|
}
|
|
1981
2111
|
const deltas = pullResponse.deltas;
|
|
@@ -1983,6 +2113,10 @@ class PersistedCollectionRuntime {
|
|
|
1983
2113
|
await this.runInHydrationScope(
|
|
1984
2114
|
(adapter) => this.reloadActiveSubsetsUnsafe(adapter)
|
|
1985
2115
|
);
|
|
2116
|
+
this.publicRowVersion = Math.max(
|
|
2117
|
+
this.publicRowVersion,
|
|
2118
|
+
pullResponse.latestRowVersion
|
|
2119
|
+
);
|
|
1986
2120
|
return;
|
|
1987
2121
|
}
|
|
1988
2122
|
await this.runInHydrationScope(async (adapter) => {
|
|
@@ -2006,13 +2140,21 @@ class PersistedCollectionRuntime {
|
|
|
2006
2140
|
if (lifecycleGeneration !== this.lifecycleGeneration) return;
|
|
2007
2141
|
}
|
|
2008
2142
|
});
|
|
2143
|
+
this.publicRowVersion = Math.max(
|
|
2144
|
+
this.publicRowVersion,
|
|
2145
|
+
pullResponse.latestRowVersion
|
|
2146
|
+
);
|
|
2009
2147
|
return;
|
|
2010
2148
|
}
|
|
2011
2149
|
}
|
|
2012
2150
|
}
|
|
2013
2151
|
if (lifecycleGeneration !== this.lifecycleGeneration) return;
|
|
2014
2152
|
await this.runInHydrationScope(
|
|
2015
|
-
(adapter) => this.
|
|
2153
|
+
(adapter) => this.reloadActiveSubsetsUnsafe(adapter)
|
|
2154
|
+
);
|
|
2155
|
+
this.publicRowVersion = Math.max(
|
|
2156
|
+
this.publicRowVersion,
|
|
2157
|
+
this.latestRowVersion
|
|
2016
2158
|
);
|
|
2017
2159
|
if (this.mode === `sync-present`) {
|
|
2018
2160
|
for (const options of this.activeSubsets.values()) {
|
|
@@ -2036,12 +2178,12 @@ class PersistedCollectionRuntime {
|
|
|
2036
2178
|
await this.reloadActiveSubsetsUnsafe(adapter);
|
|
2037
2179
|
}
|
|
2038
2180
|
async invalidateFromCommittedTxUnsafe(txCommitted, adapter) {
|
|
2039
|
-
const reloadActiveSubsets = () => this.runInHydrationScope(
|
|
2040
|
-
(scopedAdapter) => this.reloadActiveSubsetsUnsafe(scopedAdapter),
|
|
2181
|
+
const reloadActiveSubsets = (skipUnchangedReplace = false) => this.runInHydrationScope(
|
|
2182
|
+
(scopedAdapter) => this.reloadActiveSubsetsUnsafe(scopedAdapter, skipUnchangedReplace),
|
|
2041
2183
|
adapter
|
|
2042
2184
|
);
|
|
2043
2185
|
if (txCommitted.requiresFullReload) {
|
|
2044
|
-
await reloadActiveSubsets();
|
|
2186
|
+
await reloadActiveSubsets(txCommitted.reconciliationReload === true);
|
|
2045
2187
|
return;
|
|
2046
2188
|
}
|
|
2047
2189
|
const changedKeyCount = txCommitted.changedRows.length + txCommitted.deletedKeys.length;
|
|
@@ -2105,7 +2247,7 @@ class PersistedCollectionRuntime {
|
|
|
2105
2247
|
});
|
|
2106
2248
|
await whenSyncAccepted(applied);
|
|
2107
2249
|
}
|
|
2108
|
-
async reloadActiveSubsetsUnsafe(adapter) {
|
|
2250
|
+
async reloadActiveSubsetsUnsafe(adapter, skipUnchangedReplace = false) {
|
|
2109
2251
|
const lifecycleGeneration = this.lifecycleGeneration;
|
|
2110
2252
|
const truncateGeneration = this.sourceTruncateGeneration;
|
|
2111
2253
|
const activeSubsetOptions = this.activeSubsets.size > 0 ? Array.from(this.activeSubsets.values()) : [{}];
|
|
@@ -2131,15 +2273,29 @@ class PersistedCollectionRuntime {
|
|
|
2131
2273
|
for (const key of mergedRows.keys()) {
|
|
2132
2274
|
hydrationContext.suppliedRowKeys.add(key);
|
|
2133
2275
|
}
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2276
|
+
if (skipUnchangedReplace && this.matchesCollectionSnapshot(mergedRows, collectionMetadata)) {
|
|
2277
|
+
const orphanKeys = this.collection?._syncedRowMetadataKeysOutsideBase() ?? [];
|
|
2278
|
+
if (orphanKeys.length > 0) {
|
|
2279
|
+
const applied = this.withInternalApply(() => {
|
|
2280
|
+
this.syncControls.begin?.();
|
|
2281
|
+
for (const key of orphanKeys) {
|
|
2282
|
+
this.syncControls.metadata?.row.delete(key);
|
|
2283
|
+
}
|
|
2284
|
+
return this.syncControls.commit?.() ?? true;
|
|
2285
|
+
});
|
|
2286
|
+
await whenSyncAccepted(applied);
|
|
2287
|
+
}
|
|
2288
|
+
} else {
|
|
2289
|
+
const applied = this.replaceCollectionSnapshot(
|
|
2290
|
+
Array.from(mergedRows.entries()).map(([key, row]) => ({
|
|
2291
|
+
key,
|
|
2292
|
+
value: row.value,
|
|
2293
|
+
metadata: row.metadata
|
|
2294
|
+
})),
|
|
2295
|
+
collectionMetadata
|
|
2296
|
+
);
|
|
2297
|
+
await whenSyncAccepted(applied);
|
|
2298
|
+
}
|
|
2143
2299
|
} finally {
|
|
2144
2300
|
if (this.activeHydrationContext === hydrationContext && this.hydratingGeneration === lifecycleGeneration) {
|
|
2145
2301
|
this.hydratingGeneration = null;
|