@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.
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ function isValidCommittedTxAnchor(value) {
4
+ if (!value || typeof value !== `object`) return false;
5
+ const anchor = value;
6
+ return typeof anchor.latestRowVersion === `number` && Number.isSafeInteger(anchor.latestRowVersion) && anchor.latestRowVersion >= 0 && typeof anchor.resetEpoch === `number` && Number.isSafeInteger(anchor.resetEpoch) && anchor.resetEpoch >= 0;
7
+ }
8
+ exports.isValidCommittedTxAnchor = isValidCommittedTxAnchor;
9
+ //# sourceMappingURL=committed-tx-anchor.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"committed-tx-anchor.cjs","sources":["../../src/committed-tx-anchor.ts"],"sourcesContent":["// RPC payloads and direct adapter calls can bypass TypeScript's anchor type.\nexport function isValidCommittedTxAnchor(value: unknown): boolean {\n if (!value || typeof value !== `object`) return false\n const anchor = value as Record<string, unknown>\n return (\n typeof anchor.latestRowVersion === `number` &&\n Number.isSafeInteger(anchor.latestRowVersion) &&\n anchor.latestRowVersion >= 0 &&\n typeof anchor.resetEpoch === `number` &&\n Number.isSafeInteger(anchor.resetEpoch) &&\n anchor.resetEpoch >= 0\n )\n}\n"],"names":[],"mappings":";;AACO,SAAS,yBAAyB,OAAyB;AAChE,MAAI,CAAC,SAAS,OAAO,UAAU,SAAU,QAAO;AAChD,QAAM,SAAS;AACf,SACE,OAAO,OAAO,qBAAqB,YACnC,OAAO,cAAc,OAAO,gBAAgB,KAC5C,OAAO,oBAAoB,KAC3B,OAAO,OAAO,eAAe,YAC7B,OAAO,cAAc,OAAO,UAAU,KACtC,OAAO,cAAc;AAEzB;;"}
@@ -0,0 +1 @@
1
+ export declare function isValidCommittedTxAnchor(value: unknown): boolean;
@@ -393,7 +393,7 @@ function isTxCommittedPayload(payload) {
393
393
  return false;
394
394
  }
395
395
  if (payload.requiresFullReload) {
396
- return true;
396
+ return payload.reconciliationReload === void 0 || payload.reconciliationReload === true;
397
397
  }
398
398
  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));
399
399
  }
@@ -459,6 +459,7 @@ class PersistedCollectionRuntime {
459
459
  this.latestTerm = 0;
460
460
  this.latestSeq = 0;
461
461
  this.latestRowVersion = 0;
462
+ this.publicRowVersion = 0;
462
463
  this.localTerm = 1;
463
464
  this.localSeq = 0;
464
465
  this.localRowVersion = 0;
@@ -1136,6 +1137,8 @@ class PersistedCollectionRuntime {
1136
1137
  this.lifecycleGeneration++;
1137
1138
  this.persistedReadiness?.set({ status: `loading` });
1138
1139
  this.startupSettled = false;
1140
+ this.publicRowVersion = 0;
1141
+ this.resetTermFence = void 0;
1139
1142
  this.hydratedDemands.clear();
1140
1143
  this.startupMetadataPromise = null;
1141
1144
  this.startPromise = null;
@@ -1180,6 +1183,7 @@ class PersistedCollectionRuntime {
1180
1183
  }
1181
1184
  async hydrateSubsetUnsafe(options, config, adapter) {
1182
1185
  let rowsLoaded = false;
1186
+ let boundRowVersion;
1183
1187
  let replayFailure;
1184
1188
  const resetSequence = this.resetSequence;
1185
1189
  this.hydrationSequence++;
@@ -1199,6 +1203,7 @@ class PersistedCollectionRuntime {
1199
1203
  if (config.lifecycleGeneration !== this.lifecycleGeneration) return;
1200
1204
  if (resetSequence !== this.resetSequence) return;
1201
1205
  this.bindResumeSnapshotEvidence(snapshot);
1206
+ boundRowVersion = snapshot.latestRowVersion;
1202
1207
  } else {
1203
1208
  rows = await this.loadSubsetRowsUnsafe(options, adapter);
1204
1209
  }
@@ -1211,6 +1216,12 @@ class PersistedCollectionRuntime {
1211
1216
  }
1212
1217
  const applied = this.applyRowsToCollection(rows);
1213
1218
  await db.whenSyncAccepted(applied);
1219
+ if (boundRowVersion !== void 0) {
1220
+ this.publicRowVersion = Math.max(
1221
+ this.publicRowVersion,
1222
+ boundRowVersion
1223
+ );
1224
+ }
1214
1225
  }
1215
1226
  } finally {
1216
1227
  if (this.activeHydrationContext === hydrationContext && this.hydratingGeneration === config.lifecycleGeneration) {
@@ -1374,7 +1385,9 @@ class PersistedCollectionRuntime {
1374
1385
  }
1375
1386
  isExpectedResumeGeneration(generation) {
1376
1387
  const expected = this.persistedResumeGeneration;
1377
- return expected !== void 0 && expected.latestTerm === generation.latestTerm && expected.latestSeq === generation.latestSeq && expected.latestRowVersion === generation.latestRowVersion && expected.resetEpoch === generation.resetEpoch;
1388
+ 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
1389
+ // not change the persisted rows or the source resume cursor.
1390
+ generation.latestTerm > expected.latestTerm && generation.latestSeq === 0);
1378
1391
  }
1379
1392
  bindResumeSnapshotEvidence(snapshot) {
1380
1393
  const generation = this.getResumeSnapshotGeneration(snapshot);
@@ -1416,6 +1429,27 @@ class PersistedCollectionRuntime {
1416
1429
  return this.syncControls.commit?.() ?? true;
1417
1430
  });
1418
1431
  }
1432
+ matchesCollectionSnapshot(rows, collectionMetadata) {
1433
+ if (!this.collection || !this.syncControls.metadata) return false;
1434
+ const base = this.collection.base;
1435
+ if (base.size !== rows.size) return false;
1436
+ for (const [key, row] of rows) {
1437
+ if (!base.has(key) || !db.equalPersistedSnapshotValues(base.get(key), row.value) || !db.equalPersistedSnapshotValues(
1438
+ this.syncControls.metadata.row.get(key),
1439
+ row.metadata
1440
+ )) {
1441
+ return false;
1442
+ }
1443
+ }
1444
+ const currentMetadata = this.syncControls.metadata.collection.list();
1445
+ if (currentMetadata.length !== collectionMetadata.length) return false;
1446
+ const expectedMetadata = new Map(
1447
+ collectionMetadata.map(({ key, value }) => [key, value])
1448
+ );
1449
+ return currentMetadata.every(
1450
+ ({ key, value }) => expectedMetadata.has(key) && db.equalPersistedSnapshotValues(expectedMetadata.get(key), value)
1451
+ );
1452
+ }
1419
1453
  async flushQueuedHydrationTransactionsUnsafe(adapter) {
1420
1454
  let operationFailure;
1421
1455
  while (this.queuedHydrationTransactions.length > 0) {
@@ -1500,16 +1534,55 @@ class PersistedCollectionRuntime {
1500
1534
  if (!transaction.truncate && transaction.operations.length === 0 && transaction.rowMetadataWrites.size === 0 && transaction.collectionMetadataWrites.size === 0) {
1501
1535
  return;
1502
1536
  }
1537
+ const anchor = this.persistedResumeGeneration && {
1538
+ latestRowVersion: this.latestRowVersion,
1539
+ resetEpoch: this.persistedResumeGeneration.resetEpoch
1540
+ };
1503
1541
  const streamPosition = this.nextLocalStreamPosition();
1504
1542
  const tx = this.createPersistedTxFromOperations(transaction, streamPosition);
1505
- const response = await this.persistence.coordinator.requestApplyCommittedTx(
1506
- this.collectionId,
1507
- tx,
1508
- // A hydration-scoped adapter is already inside the shared scheduler.
1509
- // Ordinary source commits pass no scoped adapter so the coordinator
1510
- // enters that scheduler before taking the database-wide writer lock.
1511
- scopedAdapter
1512
- );
1543
+ let response;
1544
+ try {
1545
+ response = await this.persistence.coordinator.requestApplyCommittedTx(
1546
+ this.collectionId,
1547
+ tx,
1548
+ // A hydration-scoped adapter is already inside the shared scheduler.
1549
+ // Ordinary source commits enter it before taking the writer lock.
1550
+ scopedAdapter
1551
+ );
1552
+ } catch (error) {
1553
+ if (!(error instanceof errors.IndeterminateCommitError) || !anchor || !this.persistence.coordinator.reconcileCommittedTx) {
1554
+ throw error;
1555
+ }
1556
+ let reconciled;
1557
+ try {
1558
+ reconciled = await this.persistence.coordinator.reconcileCommittedTx(
1559
+ this.collectionId,
1560
+ tx,
1561
+ anchor,
1562
+ scopedAdapter
1563
+ );
1564
+ } catch (reconcileError) {
1565
+ if (reconcileError instanceof errors.PersistedCollectionDurabilityError) {
1566
+ throw reconcileError;
1567
+ }
1568
+ throw error;
1569
+ }
1570
+ if (!reconciled.ok) {
1571
+ if (reconciled.code === `PERSISTENCE_ERROR`) {
1572
+ throw new errors.PersistedCollectionDurabilityError(
1573
+ `Failed to reconcile collection "${this.collectionId}": ${reconciled.error}`,
1574
+ {
1575
+ cause: reconciled,
1576
+ code: reconciled.sourceCode ?? reconciled.code,
1577
+ path: reconciled.path
1578
+ }
1579
+ );
1580
+ }
1581
+ throw error;
1582
+ }
1583
+ this.recordDurableSourceCommit(transaction, reconciled.committed);
1584
+ return;
1585
+ }
1513
1586
  if (!response.ok) {
1514
1587
  if (response.code === `PERSISTENCE_ERROR`) {
1515
1588
  const error = new errors.PersistedCollectionDurabilityError(
@@ -1526,18 +1599,24 @@ class PersistedCollectionRuntime {
1526
1599
  `failed to apply external sync transaction through coordinator: ${response.error}`
1527
1600
  );
1528
1601
  }
1602
+ this.recordDurableSourceCommit(transaction, {
1603
+ term: response.term,
1604
+ seq: response.seq,
1605
+ rowVersion: response.latestRowVersion
1606
+ });
1607
+ }
1608
+ recordDurableSourceCommit(transaction, position) {
1529
1609
  if (transaction.lifecycleGeneration !== this.lifecycleGeneration) return;
1530
- this.observeStreamPosition(
1531
- response.term,
1532
- response.seq,
1533
- response.latestRowVersion
1534
- );
1610
+ this.observeStreamPosition(position.term, position.seq, position.rowVersion);
1611
+ if (position.rowVersion === this.publicRowVersion + 1) {
1612
+ this.publicRowVersion = position.rowVersion;
1613
+ }
1535
1614
  if (transaction.expectedResumeGenerationOwner === this.resumeGenerationOwner && this.persistedResumeGeneration !== void 0) {
1536
1615
  this.persistedResumeGeneration = {
1537
1616
  ...this.persistedResumeGeneration,
1538
- latestTerm: response.term,
1539
- latestSeq: response.seq,
1540
- latestRowVersion: response.latestRowVersion
1617
+ latestTerm: position.term,
1618
+ latestSeq: position.seq,
1619
+ latestRowVersion: position.rowVersion
1541
1620
  };
1542
1621
  }
1543
1622
  }
@@ -1900,9 +1979,17 @@ class PersistedCollectionRuntime {
1900
1979
  }
1901
1980
  if (isCollectionResetPayload(payload)) {
1902
1981
  void this.applyMutex.run(async () => {
1903
- await this.runInHydrationScope(
1904
- (adapter) => this.truncateAndReloadUnsafe(adapter, lifecycleGeneration)
1905
- );
1982
+ await this.runInHydrationScope(async (adapter) => {
1983
+ await this.truncateAndReloadUnsafe(adapter, lifecycleGeneration);
1984
+ await this.rebaseDurableStreamFromSnapshotUnsafe(
1985
+ adapter,
1986
+ lifecycleGeneration
1987
+ );
1988
+ if (lifecycleGeneration === this.lifecycleGeneration) {
1989
+ this.publicRowVersion = 0;
1990
+ this.resetTermFence = this.latestTerm;
1991
+ }
1992
+ });
1906
1993
  if (lifecycleGeneration === this.lifecycleGeneration) {
1907
1994
  await this.flushQueuedTxCommittedUnsafe();
1908
1995
  }
@@ -1925,21 +2012,42 @@ class PersistedCollectionRuntime {
1925
2012
  }
1926
2013
  async processCommittedTxUnsafe(txCommitted, lifecycleGeneration = this.lifecycleGeneration) {
1927
2014
  if (lifecycleGeneration !== this.lifecycleGeneration) return;
1928
- if (txCommitted.term < this.latestTerm) {
1929
- return;
2015
+ if (this.resetTermFence !== void 0) {
2016
+ if (txCommitted.term <= this.resetTermFence) {
2017
+ await this.runInHydrationScope(
2018
+ (adapter) => this.reloadActiveSubsetsUnsafe(adapter)
2019
+ );
2020
+ return;
2021
+ }
1930
2022
  }
1931
- if (txCommitted.term === this.latestTerm && txCommitted.seq <= this.latestSeq) {
2023
+ if (txCommitted.requiresFullReload && txCommitted.reconciliationReload) {
2024
+ await this.invalidateFromCommittedTxUnsafe(
2025
+ txCommitted,
2026
+ this.persistence.adapter
2027
+ );
2028
+ if (lifecycleGeneration === this.lifecycleGeneration) {
2029
+ this.observeStreamPosition(
2030
+ txCommitted.term,
2031
+ txCommitted.seq,
2032
+ txCommitted.latestRowVersion
2033
+ );
2034
+ this.publicRowVersion = Math.max(
2035
+ this.publicRowVersion,
2036
+ txCommitted.latestRowVersion
2037
+ );
2038
+ await this.flushQueuedTxCommittedUnsafe();
2039
+ }
1932
2040
  return;
1933
2041
  }
2042
+ if (txCommitted.latestRowVersion <= this.publicRowVersion) return;
2043
+ const observedButUnpublished = txCommitted.term < this.latestTerm || txCommitted.term === this.latestTerm && txCommitted.seq <= this.latestSeq;
1934
2044
  const hasGapInCurrentTerm = txCommitted.term === this.latestTerm && txCommitted.seq > this.latestSeq + 1;
1935
2045
  const hasGapAcrossTerms = txCommitted.term > this.latestTerm && txCommitted.seq > 1;
1936
- const hasGap = hasGapInCurrentTerm || hasGapAcrossTerms;
2046
+ const hasGap = observedButUnpublished || txCommitted.latestRowVersion > this.publicRowVersion + 1 || hasGapInCurrentTerm || hasGapAcrossTerms;
1937
2047
  if (hasGap) {
1938
2048
  await this.recoverFromSeqGapUnsafe(lifecycleGeneration);
1939
2049
  if (lifecycleGeneration !== this.lifecycleGeneration) return;
1940
- if (txCommitted.term < this.latestTerm || txCommitted.term === this.latestTerm && txCommitted.seq <= this.latestSeq) {
1941
- return;
1942
- }
2050
+ if (txCommitted.latestRowVersion <= this.publicRowVersion) return;
1943
2051
  }
1944
2052
  this.observeStreamPosition(
1945
2053
  txCommitted.term,
@@ -1951,9 +2059,27 @@ class PersistedCollectionRuntime {
1951
2059
  this.persistence.adapter
1952
2060
  );
1953
2061
  if (lifecycleGeneration === this.lifecycleGeneration) {
2062
+ this.publicRowVersion = Math.max(
2063
+ this.publicRowVersion,
2064
+ txCommitted.latestRowVersion
2065
+ );
1954
2066
  await this.flushQueuedTxCommittedUnsafe();
1955
2067
  }
1956
2068
  }
2069
+ async rebaseDurableStreamFromSnapshotUnsafe(adapter, lifecycleGeneration) {
2070
+ if (lifecycleGeneration !== this.lifecycleGeneration) return;
2071
+ const snapshot = await adapter.loadResumeSnapshot(this.collectionId, {
2072
+ includeRows: false
2073
+ });
2074
+ if (lifecycleGeneration !== this.lifecycleGeneration) return;
2075
+ this.latestRowVersion = snapshot.latestRowVersion;
2076
+ this.localRowVersion = snapshot.latestRowVersion;
2077
+ this.observeStreamPosition(
2078
+ snapshot.latestTerm,
2079
+ snapshot.latestSeq,
2080
+ snapshot.latestRowVersion
2081
+ );
2082
+ }
1957
2083
  async recoverFromSeqGapUnsafe(lifecycleGeneration) {
1958
2084
  if (lifecycleGeneration !== this.lifecycleGeneration) return;
1959
2085
  if (this.persistence.coordinator.pullSince && this.latestRowVersion >= 0) {
@@ -1961,7 +2087,7 @@ class PersistedCollectionRuntime {
1961
2087
  try {
1962
2088
  pullResponse = await this.persistence.coordinator.pullSince(
1963
2089
  this.collectionId,
1964
- this.latestRowVersion
2090
+ this.publicRowVersion
1965
2091
  );
1966
2092
  } catch (error) {
1967
2093
  console.warn(`Failed pullSince recovery attempt:`, error);
@@ -1978,6 +2104,10 @@ class PersistedCollectionRuntime {
1978
2104
  await this.runInHydrationScope(
1979
2105
  (adapter) => this.reloadActiveSubsetsUnsafe(adapter)
1980
2106
  );
2107
+ this.publicRowVersion = Math.max(
2108
+ this.publicRowVersion,
2109
+ pullResponse.latestRowVersion
2110
+ );
1981
2111
  return;
1982
2112
  }
1983
2113
  const deltas = pullResponse.deltas;
@@ -1985,6 +2115,10 @@ class PersistedCollectionRuntime {
1985
2115
  await this.runInHydrationScope(
1986
2116
  (adapter) => this.reloadActiveSubsetsUnsafe(adapter)
1987
2117
  );
2118
+ this.publicRowVersion = Math.max(
2119
+ this.publicRowVersion,
2120
+ pullResponse.latestRowVersion
2121
+ );
1988
2122
  return;
1989
2123
  }
1990
2124
  await this.runInHydrationScope(async (adapter) => {
@@ -2008,13 +2142,21 @@ class PersistedCollectionRuntime {
2008
2142
  if (lifecycleGeneration !== this.lifecycleGeneration) return;
2009
2143
  }
2010
2144
  });
2145
+ this.publicRowVersion = Math.max(
2146
+ this.publicRowVersion,
2147
+ pullResponse.latestRowVersion
2148
+ );
2011
2149
  return;
2012
2150
  }
2013
2151
  }
2014
2152
  }
2015
2153
  if (lifecycleGeneration !== this.lifecycleGeneration) return;
2016
2154
  await this.runInHydrationScope(
2017
- (adapter) => this.truncateAndReloadUnsafe(adapter, lifecycleGeneration)
2155
+ (adapter) => this.reloadActiveSubsetsUnsafe(adapter)
2156
+ );
2157
+ this.publicRowVersion = Math.max(
2158
+ this.publicRowVersion,
2159
+ this.latestRowVersion
2018
2160
  );
2019
2161
  if (this.mode === `sync-present`) {
2020
2162
  for (const options of this.activeSubsets.values()) {
@@ -2038,12 +2180,12 @@ class PersistedCollectionRuntime {
2038
2180
  await this.reloadActiveSubsetsUnsafe(adapter);
2039
2181
  }
2040
2182
  async invalidateFromCommittedTxUnsafe(txCommitted, adapter) {
2041
- const reloadActiveSubsets = () => this.runInHydrationScope(
2042
- (scopedAdapter) => this.reloadActiveSubsetsUnsafe(scopedAdapter),
2183
+ const reloadActiveSubsets = (skipUnchangedReplace = false) => this.runInHydrationScope(
2184
+ (scopedAdapter) => this.reloadActiveSubsetsUnsafe(scopedAdapter, skipUnchangedReplace),
2043
2185
  adapter
2044
2186
  );
2045
2187
  if (txCommitted.requiresFullReload) {
2046
- await reloadActiveSubsets();
2188
+ await reloadActiveSubsets(txCommitted.reconciliationReload === true);
2047
2189
  return;
2048
2190
  }
2049
2191
  const changedKeyCount = txCommitted.changedRows.length + txCommitted.deletedKeys.length;
@@ -2107,7 +2249,7 @@ class PersistedCollectionRuntime {
2107
2249
  });
2108
2250
  await db.whenSyncAccepted(applied);
2109
2251
  }
2110
- async reloadActiveSubsetsUnsafe(adapter) {
2252
+ async reloadActiveSubsetsUnsafe(adapter, skipUnchangedReplace = false) {
2111
2253
  const lifecycleGeneration = this.lifecycleGeneration;
2112
2254
  const truncateGeneration = this.sourceTruncateGeneration;
2113
2255
  const activeSubsetOptions = this.activeSubsets.size > 0 ? Array.from(this.activeSubsets.values()) : [{}];
@@ -2133,15 +2275,29 @@ class PersistedCollectionRuntime {
2133
2275
  for (const key of mergedRows.keys()) {
2134
2276
  hydrationContext.suppliedRowKeys.add(key);
2135
2277
  }
2136
- const applied = this.replaceCollectionSnapshot(
2137
- Array.from(mergedRows.entries()).map(([key, row]) => ({
2138
- key,
2139
- value: row.value,
2140
- metadata: row.metadata
2141
- })),
2142
- collectionMetadata
2143
- );
2144
- await db.whenSyncAccepted(applied);
2278
+ if (skipUnchangedReplace && this.matchesCollectionSnapshot(mergedRows, collectionMetadata)) {
2279
+ const orphanKeys = this.collection?._syncedRowMetadataKeysOutsideBase() ?? [];
2280
+ if (orphanKeys.length > 0) {
2281
+ const applied = this.withInternalApply(() => {
2282
+ this.syncControls.begin?.();
2283
+ for (const key of orphanKeys) {
2284
+ this.syncControls.metadata?.row.delete(key);
2285
+ }
2286
+ return this.syncControls.commit?.() ?? true;
2287
+ });
2288
+ await db.whenSyncAccepted(applied);
2289
+ }
2290
+ } else {
2291
+ const applied = this.replaceCollectionSnapshot(
2292
+ Array.from(mergedRows.entries()).map(([key, row]) => ({
2293
+ key,
2294
+ value: row.value,
2295
+ metadata: row.metadata
2296
+ })),
2297
+ collectionMetadata
2298
+ );
2299
+ await db.whenSyncAccepted(applied);
2300
+ }
2145
2301
  } finally {
2146
2302
  if (this.activeHydrationContext === hydrationContext && this.hydratingGeneration === lifecycleGeneration) {
2147
2303
  this.hydratingGeneration = null;