@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.
@@ -1,5 +1,5 @@
1
1
  import { LoadSubsetOptions } from '@tanstack/db';
2
- import { HydrationPersistenceAdapter, PersistedIndexSpec, PersistedKeySetEvidence, PersistedRowScanOptions, PersistedScannedRow, PersistedTx, PersistenceAdapter, ReplayableTxDelta, SQLiteDriver } from './persisted.js';
2
+ import { CommittedTxAnchor, HydrationPersistenceAdapter, PersistedIndexSpec, PersistedKeySetEvidence, PersistedRowScanOptions, PersistedScannedRow, PersistedTx, PersistenceAdapter, ReconciledCommittedTx, ReplayableTxDelta, SQLiteDriver } from './persisted.js';
3
3
  type SQLiteCoreAdapterSchemaMismatchPolicy = `sync-present-reset` | `sync-absent-error` | `reset`;
4
4
  export type SQLiteCoreAdapterOptions = {
5
5
  driver: SQLiteDriver;
@@ -83,7 +83,10 @@ export declare class SQLiteCorePersistenceAdapter implements PersistenceAdapter
83
83
  }>;
84
84
  private loadResumeSnapshotUnscheduled;
85
85
  applyCommittedTx(collectionId: string, tx: PersistedTx): Promise<void>;
86
+ reconcileCommittedTx(collectionId: string, tx: PersistedTx, anchor: CommittedTxAnchor): Promise<ReconciledCommittedTx>;
86
87
  private applyCommittedTxUnscheduled;
88
+ private reconcileCommittedTxUnscheduled;
89
+ private applyCommittedTxInternal;
87
90
  loadCollectionMetadata(collectionId: string): Promise<Array<{
88
91
  key: string;
89
92
  value: unknown;
@@ -95,6 +98,12 @@ export declare class SQLiteCorePersistenceAdapter implements PersistenceAdapter
95
98
  private ensureIndexUnscheduled;
96
99
  markIndexRemoved(collectionId: string, signature: string): Promise<void>;
97
100
  private markIndexRemovedUnscheduled;
101
+ reserveLeadershipTerm(collectionId: string, observedTerm: number): Promise<{
102
+ latestTerm: number;
103
+ latestSeq: number;
104
+ latestRowVersion: number;
105
+ }>;
106
+ private reserveLeadershipTermUnscheduled;
98
107
  getStreamPosition(collectionId: string): Promise<{
99
108
  latestTerm: number;
100
109
  latestSeq: number;
@@ -1,5 +1,6 @@
1
1
  import { IR, toBooleanPredicate, compileSingleRowExpression, compareTemporalValues } from "@tanstack/db";
2
2
  import { InvalidPersistedCollectionConfigError, InvalidPersistedStorageKeyEncodingError } from "./errors.js";
3
+ import { isValidCommittedTxAnchor } from "./committed-tx-anchor.js";
3
4
  import { encodePersistedStorageKey, createPersistedTableName, decodePersistedStorageKey, SQLITE_DRIVER_SHARED_LOGICAL_SCHEDULING_KEY } from "./persisted.js";
4
5
  import { serializeSQLiteTemporal, sqliteTemporalIdentity, assertSQLiteBigIntInRange, sqliteTemporalKind, PERSISTED_TYPE_TAG, PERSISTED_VALUE_TAG, reviveSQLiteTemporal, serializeSQLiteBigInt } from "./sqlite-value.js";
5
6
  const REPLACEMENT_BATCH_SIZE = 125;
@@ -1088,6 +1089,7 @@ class SQLiteCorePersistenceAdapter {
1088
1089
  loadSubset: (collectionId, loadOptions, context) => this.loadSubsetUnscheduled(collectionId, loadOptions, context),
1089
1090
  loadResumeSnapshot: (collectionId, context) => this.loadResumeSnapshotUnscheduled(collectionId, context),
1090
1091
  applyCommittedTx: (collectionId, tx) => this.applyCommittedTxUnscheduled(collectionId, tx),
1092
+ reconcileCommittedTx: (collectionId, tx, anchor) => this.reconcileCommittedTxUnscheduled(collectionId, tx, anchor),
1091
1093
  loadCollectionMetadata: (collectionId) => this.loadCollectionMetadataUnscheduled(collectionId),
1092
1094
  scanRows: (collectionId, scanOptions) => this.scanRowsUnscheduled(collectionId, scanOptions),
1093
1095
  ensureIndex: (collectionId, signature, spec) => this.ensureIndexUnscheduled(collectionId, signature, spec),
@@ -1283,15 +1285,33 @@ class SQLiteCorePersistenceAdapter {
1283
1285
  () => this.applyCommittedTxUnscheduled(collectionId, tx)
1284
1286
  );
1285
1287
  }
1288
+ reconcileCommittedTx(collectionId, tx, anchor) {
1289
+ return this.runRegular(
1290
+ () => this.reconcileCommittedTxUnscheduled(collectionId, tx, anchor)
1291
+ );
1292
+ }
1286
1293
  async applyCommittedTxUnscheduled(collectionId, tx) {
1294
+ await this.applyCommittedTxInternal(collectionId, tx);
1295
+ }
1296
+ reconcileCommittedTxUnscheduled(collectionId, tx, anchor) {
1297
+ if (!isValidCommittedTxAnchor(anchor)) {
1298
+ throw new Error(
1299
+ `Cannot reconcile a committed transaction without a valid durable anchor`
1300
+ );
1301
+ }
1302
+ return this.applyCommittedTxInternal(collectionId, tx, anchor);
1303
+ }
1304
+ async applyCommittedTxInternal(collectionId, tx, anchor) {
1287
1305
  const tableMapping = await this.ensureCollectionReady(collectionId);
1288
1306
  const collectionTableSql = quoteIdentifier(tableMapping.tableName);
1289
1307
  const tombstoneTableSql = quoteIdentifier(tableMapping.tombstoneTableName);
1290
- await this.runInTransaction(async (transactionDriver) => {
1308
+ return this.runInTransaction(async (transactionDriver) => {
1291
1309
  const versionRows = await transactionDriver.query(
1292
1310
  `SELECT
1293
1311
  latest_row_version,
1294
1312
  key_set_evidence_available,
1313
+ (SELECT reset_epoch FROM collection_reset_epoch
1314
+ WHERE collection_id = ? LIMIT 1) AS reset_epoch,
1295
1315
  (
1296
1316
  SELECT schema_version
1297
1317
  FROM collection_registry
@@ -1306,7 +1326,14 @@ class SQLiteCorePersistenceAdapter {
1306
1326
  FROM collection_version
1307
1327
  WHERE collection_id = ?
1308
1328
  LIMIT 1`,
1309
- [collectionId, collectionId, tx.term, tx.seq, collectionId]
1329
+ [
1330
+ collectionId,
1331
+ collectionId,
1332
+ collectionId,
1333
+ tx.term,
1334
+ tx.seq,
1335
+ collectionId
1336
+ ]
1310
1337
  );
1311
1338
  const version = versionRows[0];
1312
1339
  if (!version) {
@@ -1319,8 +1346,34 @@ class SQLiteCorePersistenceAdapter {
1319
1346
  `Schema version mismatch for collection "${collectionId}": found ${version.schema_version}, expected ${this.schemaVersion}. Refusing to apply a committed transaction through a stale cached adapter.`
1320
1347
  );
1321
1348
  }
1349
+ if (anchor) {
1350
+ const sameResetEpoch = (version.reset_epoch ?? 0) === anchor.resetEpoch;
1351
+ const matching = await transactionDriver.query(
1352
+ `SELECT term, seq, row_version FROM applied_tx
1353
+ WHERE collection_id = ? AND tx_id = ? LIMIT 2`,
1354
+ [collectionId, tx.txId]
1355
+ );
1356
+ if (matching.length > 1) return { kind: `unknown` };
1357
+ if (matching[0]) {
1358
+ if (matching[0].row_version <= anchor.latestRowVersion || !sameResetEpoch) {
1359
+ return { kind: `unknown` };
1360
+ }
1361
+ return {
1362
+ kind: `already-applied`,
1363
+ committed: {
1364
+ term: matching[0].term,
1365
+ seq: matching[0].seq,
1366
+ rowVersion: matching[0].row_version
1367
+ },
1368
+ latestRowVersion: version.latest_row_version
1369
+ };
1370
+ }
1371
+ if (version.latest_row_version !== anchor.latestRowVersion || !sameResetEpoch) {
1372
+ return { kind: `unknown` };
1373
+ }
1374
+ }
1322
1375
  if (version.already_applied === 1) {
1323
- return;
1376
+ return { kind: `unknown` };
1324
1377
  }
1325
1378
  const currentRowVersion = version.latest_row_version;
1326
1379
  const nextRowVersion = Math.max(currentRowVersion + 1, tx.rowVersion);
@@ -1568,6 +1621,15 @@ class SQLiteCorePersistenceAdapter {
1568
1621
  ]
1569
1622
  );
1570
1623
  await this.pruneAppliedTxRows(collectionId, transactionDriver);
1624
+ return {
1625
+ kind: `applied-now`,
1626
+ committed: {
1627
+ term: tx.term,
1628
+ seq: tx.seq,
1629
+ rowVersion: nextRowVersion
1630
+ },
1631
+ latestRowVersion: nextRowVersion
1632
+ };
1571
1633
  });
1572
1634
  }
1573
1635
  loadCollectionMetadata(collectionId) {
@@ -1732,6 +1794,31 @@ class SQLiteCorePersistenceAdapter {
1732
1794
  }
1733
1795
  });
1734
1796
  }
1797
+ reserveLeadershipTerm(collectionId, observedTerm) {
1798
+ return this.reserveLeadershipTermUnscheduled(collectionId, observedTerm);
1799
+ }
1800
+ async reserveLeadershipTermUnscheduled(collectionId, observedTerm) {
1801
+ await this.ensureCollectionReady(collectionId);
1802
+ return this.runInTransaction(async (transactionDriver) => {
1803
+ await this.assertCurrentSchemaVersion(
1804
+ collectionId,
1805
+ transactionDriver,
1806
+ `reserve a leadership term`
1807
+ );
1808
+ await transactionDriver.run(
1809
+ `INSERT INTO leader_term (collection_id, latest_term)
1810
+ VALUES (?, ? + 1)
1811
+ ON CONFLICT(collection_id) DO UPDATE SET
1812
+ latest_term = MAX(leader_term.latest_term, ?) + 1`,
1813
+ [collectionId, observedTerm, observedTerm]
1814
+ );
1815
+ const [position, latestRowVersion] = await Promise.all([
1816
+ this.readStreamPosition(collectionId, transactionDriver),
1817
+ this.readLatestRowVersion(collectionId, transactionDriver)
1818
+ ]);
1819
+ return { ...position, latestRowVersion };
1820
+ });
1821
+ }
1735
1822
  getStreamPosition(collectionId) {
1736
1823
  return this.getStreamPositionUnscheduled(collectionId);
1737
1824
  }
@@ -2355,6 +2442,10 @@ class SQLiteCorePersistenceAdapter {
2355
2442
  PRIMARY KEY (collection_id, term, seq)
2356
2443
  )`
2357
2444
  );
2445
+ await this.driver.exec(
2446
+ `CREATE INDEX IF NOT EXISTS idx_applied_tx_collection_tx_id
2447
+ ON applied_tx (collection_id, tx_id)`
2448
+ );
2358
2449
  try {
2359
2450
  await this.driver.exec(
2360
2451
  `ALTER TABLE applied_tx ADD COLUMN replay_json TEXT`