@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
|
@@ -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
|
-
|
|
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
|
-
[
|
|
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`,
|