@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.
@@ -8,6 +8,7 @@ import {
8
8
  InvalidPersistedCollectionConfigError,
9
9
  InvalidPersistedStorageKeyEncodingError,
10
10
  } from './errors'
11
+ import { isValidCommittedTxAnchor } from './committed-tx-anchor'
11
12
  import {
12
13
  SQLITE_DRIVER_SHARED_LOGICAL_SCHEDULING_KEY,
13
14
  createPersistedTableName,
@@ -26,6 +27,7 @@ import {
26
27
  } from './sqlite-value'
27
28
  import type { LoadSubsetOptions } from '@tanstack/db'
28
29
  import type {
30
+ CommittedTxAnchor,
29
31
  HydrationPersistenceAdapter,
30
32
  PersistedIndexSpec,
31
33
  PersistedKeySetEvidence,
@@ -33,6 +35,7 @@ import type {
33
35
  PersistedScannedRow,
34
36
  PersistedTx,
35
37
  PersistenceAdapter,
38
+ ReconciledCommittedTx,
36
39
  ReplayableTxDelta,
37
40
  SQLiteDriver,
38
41
  } from './persisted'
@@ -1713,6 +1716,8 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
1713
1716
  this.loadResumeSnapshotUnscheduled(collectionId, context),
1714
1717
  applyCommittedTx: (collectionId, tx) =>
1715
1718
  this.applyCommittedTxUnscheduled(collectionId, tx),
1719
+ reconcileCommittedTx: (collectionId, tx, anchor) =>
1720
+ this.reconcileCommittedTxUnscheduled(collectionId, tx, anchor),
1716
1721
  loadCollectionMetadata: (collectionId) =>
1717
1722
  this.loadCollectionMetadataUnscheduled(collectionId),
1718
1723
  scanRows: (collectionId, scanOptions) =>
@@ -2006,24 +2011,58 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
2006
2011
  )
2007
2012
  }
2008
2013
 
2014
+ reconcileCommittedTx(
2015
+ collectionId: string,
2016
+ tx: PersistedTx,
2017
+ anchor: CommittedTxAnchor,
2018
+ ): Promise<ReconciledCommittedTx> {
2019
+ return this.runRegular(() =>
2020
+ this.reconcileCommittedTxUnscheduled(collectionId, tx, anchor),
2021
+ )
2022
+ }
2023
+
2009
2024
  private async applyCommittedTxUnscheduled(
2010
2025
  collectionId: string,
2011
2026
  tx: PersistedTx,
2012
2027
  ): Promise<void> {
2028
+ await this.applyCommittedTxInternal(collectionId, tx)
2029
+ }
2030
+
2031
+ private reconcileCommittedTxUnscheduled(
2032
+ collectionId: string,
2033
+ tx: PersistedTx,
2034
+ anchor: CommittedTxAnchor,
2035
+ ): Promise<ReconciledCommittedTx> {
2036
+ if (!isValidCommittedTxAnchor(anchor)) {
2037
+ throw new Error(
2038
+ `Cannot reconcile a committed transaction without a valid durable anchor`,
2039
+ )
2040
+ }
2041
+ return this.applyCommittedTxInternal(collectionId, tx, anchor)
2042
+ }
2043
+
2044
+ private async applyCommittedTxInternal(
2045
+ collectionId: string,
2046
+ tx: PersistedTx,
2047
+ anchor?: CommittedTxAnchor,
2048
+ ): Promise<ReconciledCommittedTx> {
2013
2049
  const tableMapping = await this.ensureCollectionReady(collectionId)
2014
2050
  const collectionTableSql = quoteIdentifier(tableMapping.tableName)
2015
2051
  const tombstoneTableSql = quoteIdentifier(tableMapping.tombstoneTableName)
2016
2052
 
2017
- await this.runInTransaction(async (transactionDriver) => {
2053
+ return this.runInTransaction(async (transactionDriver) => {
2018
2054
  const versionRows = await transactionDriver.query<{
2019
2055
  latest_row_version: number
2020
2056
  key_set_evidence_available: number
2021
2057
  schema_version: number
2022
2058
  already_applied: number
2059
+ reset_epoch: number | null
2023
2060
  }>(
2024
2061
  `SELECT
2025
2062
  latest_row_version,
2026
2063
  key_set_evidence_available,
2064
+ (SELECT reset_epoch FROM collection_reset_epoch
2065
+ WHERE collection_id = ? LIMIT 1) AS reset_epoch,
2027
2066
  (
2028
2067
  SELECT schema_version
2029
2068
  FROM collection_registry
@@ -2038,7 +2077,14 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
2038
2077
  FROM collection_version
2039
2078
  WHERE collection_id = ?
2040
2079
  LIMIT 1`,
2041
- [collectionId, collectionId, tx.term, tx.seq, collectionId],
2080
+ [
2081
+ collectionId,
2082
+ collectionId,
2083
+ collectionId,
2084
+ tx.term,
2085
+ tx.seq,
2086
+ collectionId,
2087
+ ],
2042
2088
  )
2043
2089
  const version = versionRows[0]
2044
2090
 
@@ -2055,8 +2101,47 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
2055
2101
  )
2056
2102
  }
2057
2103
 
2104
+ if (anchor) {
2105
+ const sameResetEpoch = (version.reset_epoch ?? 0) === anchor.resetEpoch
2106
+ const matching = await transactionDriver.query<{
2107
+ term: number
2108
+ seq: number
2109
+ row_version: number
2110
+ }>(
2111
+ `SELECT term, seq, row_version FROM applied_tx
2112
+ WHERE collection_id = ? AND tx_id = ? LIMIT 2`,
2113
+ [collectionId, tx.txId],
2114
+ )
2115
+ if (matching.length > 1) return { kind: `unknown` }
2116
+ if (matching[0]) {
2117
+ if (
2118
+ matching[0].row_version <= anchor.latestRowVersion ||
2119
+ !sameResetEpoch
2120
+ ) {
2121
+ return { kind: `unknown` }
2122
+ }
2123
+ return {
2124
+ kind: `already-applied`,
2125
+ committed: {
2126
+ term: matching[0].term,
2127
+ seq: matching[0].seq,
2128
+ rowVersion: matching[0].row_version,
2129
+ },
2130
+ latestRowVersion: version.latest_row_version,
2131
+ }
2132
+ }
2133
+ // A different durable write may have superseded this source row or
2134
+ // its opaque cursor. Absence alone cannot authorize late application.
2135
+ if (
2136
+ version.latest_row_version !== anchor.latestRowVersion ||
2137
+ !sameResetEpoch
2138
+ ) {
2139
+ return { kind: `unknown` }
2140
+ }
2141
+ }
2142
+
2058
2143
  if (version.already_applied === 1) {
2059
- return
2144
+ return { kind: `unknown` }
2060
2145
  }
2061
2146
 
2062
2147
  const currentRowVersion = version.latest_row_version
@@ -2366,6 +2451,15 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
2366
2451
  )
2367
2452
 
2368
2453
  await this.pruneAppliedTxRows(collectionId, transactionDriver)
2454
+ return {
2455
+ kind: `applied-now`,
2456
+ committed: {
2457
+ term: tx.term,
2458
+ seq: tx.seq,
2459
+ rowVersion: nextRowVersion,
2460
+ },
2461
+ latestRowVersion: nextRowVersion,
2462
+ }
2369
2463
  })
2370
2464
  }
2371
2465
 
@@ -2588,6 +2682,48 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
2588
2682
  })
2589
2683
  }
2590
2684
 
2685
+ reserveLeadershipTerm(
2686
+ collectionId: string,
2687
+ observedTerm: number,
2688
+ ): Promise<{
2689
+ latestTerm: number
2690
+ latestSeq: number
2691
+ latestRowVersion: number
2692
+ }> {
2693
+ // Election may run while hydration waits for its first writer route.
2694
+ return this.reserveLeadershipTermUnscheduled(collectionId, observedTerm)
2695
+ }
2696
+
2697
+ private async reserveLeadershipTermUnscheduled(
2698
+ collectionId: string,
2699
+ observedTerm: number,
2700
+ ): Promise<{
2701
+ latestTerm: number
2702
+ latestSeq: number
2703
+ latestRowVersion: number
2704
+ }> {
2705
+ await this.ensureCollectionReady(collectionId)
2706
+ return this.runInTransaction(async (transactionDriver) => {
2707
+ await this.assertCurrentSchemaVersion(
2708
+ collectionId,
2709
+ transactionDriver,
2710
+ `reserve a leadership term`,
2711
+ )
2712
+ await transactionDriver.run(
2713
+ `INSERT INTO leader_term (collection_id, latest_term)
2714
+ VALUES (?, ? + 1)
2715
+ ON CONFLICT(collection_id) DO UPDATE SET
2716
+ latest_term = MAX(leader_term.latest_term, ?) + 1`,
2717
+ [collectionId, observedTerm, observedTerm],
2718
+ )
2719
+ const [position, latestRowVersion] = await Promise.all([
2720
+ this.readStreamPosition(collectionId, transactionDriver),
2721
+ this.readLatestRowVersion(collectionId, transactionDriver),
2722
+ ])
2723
+ return { ...position, latestRowVersion }
2724
+ })
2725
+ }
2726
+
2591
2727
  getStreamPosition(collectionId: string): Promise<{
2592
2728
  latestTerm: number
2593
2729
  latestSeq: number
@@ -3397,6 +3533,10 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
3397
3533
  PRIMARY KEY (collection_id, term, seq)
3398
3534
  )`,
3399
3535
  )
3536
+ await this.driver.exec(
3537
+ `CREATE INDEX IF NOT EXISTS idx_applied_tx_collection_tx_id
3538
+ ON applied_tx (collection_id, tx_id)`,
3539
+ )
3400
3540
  try {
3401
3541
  await this.driver.exec(
3402
3542
  `ALTER TABLE applied_tx ADD COLUMN replay_json TEXT`,