@tanstack/db-sqlite-persistence-core 0.4.5 → 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/cjs/persisted.d.cts
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
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
3
|
const db = require("@tanstack/db");
|
|
4
4
|
const errors = require("./errors.cjs");
|
|
5
|
+
const committedTxAnchor = require("./committed-tx-anchor.cjs");
|
|
5
6
|
const persisted = require("./persisted.cjs");
|
|
6
7
|
const sqliteValue = require("./sqlite-value.cjs");
|
|
7
8
|
const REPLACEMENT_BATCH_SIZE = 125;
|
|
@@ -1090,6 +1091,7 @@ class SQLiteCorePersistenceAdapter {
|
|
|
1090
1091
|
loadSubset: (collectionId, loadOptions, context) => this.loadSubsetUnscheduled(collectionId, loadOptions, context),
|
|
1091
1092
|
loadResumeSnapshot: (collectionId, context) => this.loadResumeSnapshotUnscheduled(collectionId, context),
|
|
1092
1093
|
applyCommittedTx: (collectionId, tx) => this.applyCommittedTxUnscheduled(collectionId, tx),
|
|
1094
|
+
reconcileCommittedTx: (collectionId, tx, anchor) => this.reconcileCommittedTxUnscheduled(collectionId, tx, anchor),
|
|
1093
1095
|
loadCollectionMetadata: (collectionId) => this.loadCollectionMetadataUnscheduled(collectionId),
|
|
1094
1096
|
scanRows: (collectionId, scanOptions) => this.scanRowsUnscheduled(collectionId, scanOptions),
|
|
1095
1097
|
ensureIndex: (collectionId, signature, spec) => this.ensureIndexUnscheduled(collectionId, signature, spec),
|
|
@@ -1285,15 +1287,33 @@ class SQLiteCorePersistenceAdapter {
|
|
|
1285
1287
|
() => this.applyCommittedTxUnscheduled(collectionId, tx)
|
|
1286
1288
|
);
|
|
1287
1289
|
}
|
|
1290
|
+
reconcileCommittedTx(collectionId, tx, anchor) {
|
|
1291
|
+
return this.runRegular(
|
|
1292
|
+
() => this.reconcileCommittedTxUnscheduled(collectionId, tx, anchor)
|
|
1293
|
+
);
|
|
1294
|
+
}
|
|
1288
1295
|
async applyCommittedTxUnscheduled(collectionId, tx) {
|
|
1296
|
+
await this.applyCommittedTxInternal(collectionId, tx);
|
|
1297
|
+
}
|
|
1298
|
+
reconcileCommittedTxUnscheduled(collectionId, tx, anchor) {
|
|
1299
|
+
if (!committedTxAnchor.isValidCommittedTxAnchor(anchor)) {
|
|
1300
|
+
throw new Error(
|
|
1301
|
+
`Cannot reconcile a committed transaction without a valid durable anchor`
|
|
1302
|
+
);
|
|
1303
|
+
}
|
|
1304
|
+
return this.applyCommittedTxInternal(collectionId, tx, anchor);
|
|
1305
|
+
}
|
|
1306
|
+
async applyCommittedTxInternal(collectionId, tx, anchor) {
|
|
1289
1307
|
const tableMapping = await this.ensureCollectionReady(collectionId);
|
|
1290
1308
|
const collectionTableSql = quoteIdentifier(tableMapping.tableName);
|
|
1291
1309
|
const tombstoneTableSql = quoteIdentifier(tableMapping.tombstoneTableName);
|
|
1292
|
-
|
|
1310
|
+
return this.runInTransaction(async (transactionDriver) => {
|
|
1293
1311
|
const versionRows = await transactionDriver.query(
|
|
1294
1312
|
`SELECT
|
|
1295
1313
|
latest_row_version,
|
|
1296
1314
|
key_set_evidence_available,
|
|
1315
|
+
(SELECT reset_epoch FROM collection_reset_epoch
|
|
1316
|
+
WHERE collection_id = ? LIMIT 1) AS reset_epoch,
|
|
1297
1317
|
(
|
|
1298
1318
|
SELECT schema_version
|
|
1299
1319
|
FROM collection_registry
|
|
@@ -1308,7 +1328,14 @@ class SQLiteCorePersistenceAdapter {
|
|
|
1308
1328
|
FROM collection_version
|
|
1309
1329
|
WHERE collection_id = ?
|
|
1310
1330
|
LIMIT 1`,
|
|
1311
|
-
[
|
|
1331
|
+
[
|
|
1332
|
+
collectionId,
|
|
1333
|
+
collectionId,
|
|
1334
|
+
collectionId,
|
|
1335
|
+
tx.term,
|
|
1336
|
+
tx.seq,
|
|
1337
|
+
collectionId
|
|
1338
|
+
]
|
|
1312
1339
|
);
|
|
1313
1340
|
const version = versionRows[0];
|
|
1314
1341
|
if (!version) {
|
|
@@ -1321,8 +1348,34 @@ class SQLiteCorePersistenceAdapter {
|
|
|
1321
1348
|
`Schema version mismatch for collection "${collectionId}": found ${version.schema_version}, expected ${this.schemaVersion}. Refusing to apply a committed transaction through a stale cached adapter.`
|
|
1322
1349
|
);
|
|
1323
1350
|
}
|
|
1351
|
+
if (anchor) {
|
|
1352
|
+
const sameResetEpoch = (version.reset_epoch ?? 0) === anchor.resetEpoch;
|
|
1353
|
+
const matching = await transactionDriver.query(
|
|
1354
|
+
`SELECT term, seq, row_version FROM applied_tx
|
|
1355
|
+
WHERE collection_id = ? AND tx_id = ? LIMIT 2`,
|
|
1356
|
+
[collectionId, tx.txId]
|
|
1357
|
+
);
|
|
1358
|
+
if (matching.length > 1) return { kind: `unknown` };
|
|
1359
|
+
if (matching[0]) {
|
|
1360
|
+
if (matching[0].row_version <= anchor.latestRowVersion || !sameResetEpoch) {
|
|
1361
|
+
return { kind: `unknown` };
|
|
1362
|
+
}
|
|
1363
|
+
return {
|
|
1364
|
+
kind: `already-applied`,
|
|
1365
|
+
committed: {
|
|
1366
|
+
term: matching[0].term,
|
|
1367
|
+
seq: matching[0].seq,
|
|
1368
|
+
rowVersion: matching[0].row_version
|
|
1369
|
+
},
|
|
1370
|
+
latestRowVersion: version.latest_row_version
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1373
|
+
if (version.latest_row_version !== anchor.latestRowVersion || !sameResetEpoch) {
|
|
1374
|
+
return { kind: `unknown` };
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1324
1377
|
if (version.already_applied === 1) {
|
|
1325
|
-
return;
|
|
1378
|
+
return { kind: `unknown` };
|
|
1326
1379
|
}
|
|
1327
1380
|
const currentRowVersion = version.latest_row_version;
|
|
1328
1381
|
const nextRowVersion = Math.max(currentRowVersion + 1, tx.rowVersion);
|
|
@@ -1570,6 +1623,15 @@ class SQLiteCorePersistenceAdapter {
|
|
|
1570
1623
|
]
|
|
1571
1624
|
);
|
|
1572
1625
|
await this.pruneAppliedTxRows(collectionId, transactionDriver);
|
|
1626
|
+
return {
|
|
1627
|
+
kind: `applied-now`,
|
|
1628
|
+
committed: {
|
|
1629
|
+
term: tx.term,
|
|
1630
|
+
seq: tx.seq,
|
|
1631
|
+
rowVersion: nextRowVersion
|
|
1632
|
+
},
|
|
1633
|
+
latestRowVersion: nextRowVersion
|
|
1634
|
+
};
|
|
1573
1635
|
});
|
|
1574
1636
|
}
|
|
1575
1637
|
loadCollectionMetadata(collectionId) {
|
|
@@ -1734,6 +1796,31 @@ class SQLiteCorePersistenceAdapter {
|
|
|
1734
1796
|
}
|
|
1735
1797
|
});
|
|
1736
1798
|
}
|
|
1799
|
+
reserveLeadershipTerm(collectionId, observedTerm) {
|
|
1800
|
+
return this.reserveLeadershipTermUnscheduled(collectionId, observedTerm);
|
|
1801
|
+
}
|
|
1802
|
+
async reserveLeadershipTermUnscheduled(collectionId, observedTerm) {
|
|
1803
|
+
await this.ensureCollectionReady(collectionId);
|
|
1804
|
+
return this.runInTransaction(async (transactionDriver) => {
|
|
1805
|
+
await this.assertCurrentSchemaVersion(
|
|
1806
|
+
collectionId,
|
|
1807
|
+
transactionDriver,
|
|
1808
|
+
`reserve a leadership term`
|
|
1809
|
+
);
|
|
1810
|
+
await transactionDriver.run(
|
|
1811
|
+
`INSERT INTO leader_term (collection_id, latest_term)
|
|
1812
|
+
VALUES (?, ? + 1)
|
|
1813
|
+
ON CONFLICT(collection_id) DO UPDATE SET
|
|
1814
|
+
latest_term = MAX(leader_term.latest_term, ?) + 1`,
|
|
1815
|
+
[collectionId, observedTerm, observedTerm]
|
|
1816
|
+
);
|
|
1817
|
+
const [position, latestRowVersion] = await Promise.all([
|
|
1818
|
+
this.readStreamPosition(collectionId, transactionDriver),
|
|
1819
|
+
this.readLatestRowVersion(collectionId, transactionDriver)
|
|
1820
|
+
]);
|
|
1821
|
+
return { ...position, latestRowVersion };
|
|
1822
|
+
});
|
|
1823
|
+
}
|
|
1737
1824
|
getStreamPosition(collectionId) {
|
|
1738
1825
|
return this.getStreamPositionUnscheduled(collectionId);
|
|
1739
1826
|
}
|
|
@@ -2357,6 +2444,10 @@ class SQLiteCorePersistenceAdapter {
|
|
|
2357
2444
|
PRIMARY KEY (collection_id, term, seq)
|
|
2358
2445
|
)`
|
|
2359
2446
|
);
|
|
2447
|
+
await this.driver.exec(
|
|
2448
|
+
`CREATE INDEX IF NOT EXISTS idx_applied_tx_collection_tx_id
|
|
2449
|
+
ON applied_tx (collection_id, tx_id)`
|
|
2450
|
+
);
|
|
2360
2451
|
try {
|
|
2361
2452
|
await this.driver.exec(
|
|
2362
2453
|
`ALTER TABLE applied_tx ADD COLUMN replay_json TEXT`
|