@tanstack/db-sqlite-persistence-core 0.4.7 → 0.4.9

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.
Files changed (40) hide show
  1. package/README.md +32 -3
  2. package/dist/cjs/broadcast-coordinator.cjs +282 -43
  3. package/dist/cjs/broadcast-coordinator.cjs.map +1 -1
  4. package/dist/cjs/broadcast-coordinator.d.cts +25 -8
  5. package/dist/cjs/committed-tx-anchor.cjs +9 -0
  6. package/dist/cjs/committed-tx-anchor.cjs.map +1 -0
  7. package/dist/cjs/committed-tx-anchor.d.cts +1 -0
  8. package/dist/cjs/index.cjs +4 -0
  9. package/dist/cjs/index.cjs.map +1 -1
  10. package/dist/cjs/persisted.cjs +1599 -313
  11. package/dist/cjs/persisted.cjs.map +1 -1
  12. package/dist/cjs/persisted.d.cts +106 -13
  13. package/dist/cjs/remote-subset-wire.cjs +11 -0
  14. package/dist/cjs/remote-subset-wire.cjs.map +1 -1
  15. package/dist/cjs/remote-subset-wire.d.cts +1 -0
  16. package/dist/cjs/sqlite-core-adapter.cjs +705 -64
  17. package/dist/cjs/sqlite-core-adapter.cjs.map +1 -1
  18. package/dist/cjs/sqlite-core-adapter.d.cts +53 -10
  19. package/dist/esm/broadcast-coordinator.d.ts +25 -8
  20. package/dist/esm/broadcast-coordinator.js +283 -44
  21. package/dist/esm/broadcast-coordinator.js.map +1 -1
  22. package/dist/esm/committed-tx-anchor.d.ts +1 -0
  23. package/dist/esm/committed-tx-anchor.js +9 -0
  24. package/dist/esm/committed-tx-anchor.js.map +1 -0
  25. package/dist/esm/index.js +6 -2
  26. package/dist/esm/persisted.d.ts +106 -13
  27. package/dist/esm/persisted.js +1601 -315
  28. package/dist/esm/persisted.js.map +1 -1
  29. package/dist/esm/remote-subset-wire.d.ts +1 -0
  30. package/dist/esm/remote-subset-wire.js +11 -0
  31. package/dist/esm/remote-subset-wire.js.map +1 -1
  32. package/dist/esm/sqlite-core-adapter.d.ts +53 -10
  33. package/dist/esm/sqlite-core-adapter.js +708 -67
  34. package/dist/esm/sqlite-core-adapter.js.map +1 -1
  35. package/package.json +2 -2
  36. package/src/broadcast-coordinator.ts +415 -62
  37. package/src/committed-tx-anchor.ts +13 -0
  38. package/src/persisted.ts +2283 -370
  39. package/src/remote-subset-wire.ts +12 -0
  40. package/src/sqlite-core-adapter.ts +891 -68
@@ -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;
@@ -107,6 +108,12 @@ function observeSharedLogicalSchedulingSupport(driver, onSupport) {
107
108
  };
108
109
  }
109
110
  const DEFAULT_SCHEMA_VERSION = 1;
111
+ const CACHE_GENERATION_ID_PREFIX = `tanstack-db-cache:`;
112
+ const LEGACY_CACHE_GENERATION_ID_PREFIX = `\0tanstack-db-cache:`;
113
+ function hasCacheGenerationIdPrefix(collectionId) {
114
+ return collectionId.startsWith(CACHE_GENERATION_ID_PREFIX) || collectionId.startsWith(LEGACY_CACHE_GENERATION_ID_PREFIX);
115
+ }
116
+ const DEFAULT_CACHE_GENERATION_CLAIM_TTL_MS = 5 * 6e4;
110
117
  const DEFAULT_PULL_SINCE_RELOAD_THRESHOLD = 128;
111
118
  const DEFAULT_APPLIED_TX_PRUNE_MAX_ROWS = 1e3;
112
119
  const DEFAULT_APPLIED_TX_PRUNE_MAX_AGE_SECONDS = 24 * 60 * 60;
@@ -1048,6 +1055,13 @@ class SQLiteCorePersistenceAdapter {
1048
1055
  );
1049
1056
  }
1050
1057
  this.replacementBatchSize = maxBoundParameters === void 0 ? REPLACEMENT_BATCH_SIZE : Math.min(REPLACEMENT_BATCH_SIZE, Math.floor(maxBoundParameters / 4));
1058
+ this.cacheGenerationClaimTtlMs = options.cacheGenerationClaimTtlMs ?? DEFAULT_CACHE_GENERATION_CLAIM_TTL_MS;
1059
+ if (!Number.isSafeInteger(this.cacheGenerationClaimTtlMs) || this.cacheGenerationClaimTtlMs <= 0) {
1060
+ throw new errors.InvalidPersistedCollectionConfigError(
1061
+ `SQLite adapter cacheGenerationClaimTtlMs must be a positive safe integer`
1062
+ );
1063
+ }
1064
+ this.now = options.now ?? Date.now;
1051
1065
  const schemaVersion = options.schemaVersion ?? DEFAULT_SCHEMA_VERSION;
1052
1066
  if (!Number.isInteger(schemaVersion) || schemaVersion < 0) {
1053
1067
  throw new errors.InvalidPersistedCollectionConfigError(
@@ -1090,15 +1104,257 @@ class SQLiteCorePersistenceAdapter {
1090
1104
  loadSubset: (collectionId, loadOptions, context) => this.loadSubsetUnscheduled(collectionId, loadOptions, context),
1091
1105
  loadResumeSnapshot: (collectionId, context) => this.loadResumeSnapshotUnscheduled(collectionId, context),
1092
1106
  applyCommittedTx: (collectionId, tx) => this.applyCommittedTxUnscheduled(collectionId, tx),
1093
- loadCollectionMetadata: (collectionId) => this.loadCollectionMetadataUnscheduled(collectionId),
1094
- scanRows: (collectionId, scanOptions) => this.scanRowsUnscheduled(collectionId, scanOptions),
1095
- ensureIndex: (collectionId, signature, spec) => this.ensureIndexUnscheduled(collectionId, signature, spec),
1096
- markIndexRemoved: (collectionId, signature) => this.markIndexRemovedUnscheduled(collectionId, signature),
1097
- getStreamPosition: (collectionId) => this.getStreamPositionUnscheduled(collectionId),
1098
- pullSince: (collectionId, fromRowVersion) => this.pullSinceUnscheduled(collectionId, fromRowVersion),
1107
+ renewCacheGenerationClaim: (storageCollectionId, claimId) => this.renewCacheGenerationClaimUnscheduled(storageCollectionId, claimId),
1108
+ reconcileCommittedTx: (collectionId, tx, anchor) => this.reconcileCommittedTxUnscheduled(collectionId, tx, anchor),
1109
+ loadCollectionMetadata: (collectionId, context) => this.loadCollectionMetadataUnscheduled(collectionId, context),
1110
+ scanRows: (collectionId, scanOptions, context) => this.scanRowsUnscheduled(collectionId, scanOptions, context),
1111
+ ensureIndex: (collectionId, signature, spec, ctx) => this.ensureIndexUnscheduled(collectionId, signature, spec, ctx),
1112
+ markIndexRemoved: (collectionId, signature, ctx) => this.markIndexRemovedUnscheduled(collectionId, signature, ctx),
1113
+ getStreamPosition: (collectionId, ctx) => this.getStreamPositionUnscheduled(collectionId, ctx),
1114
+ pullSince: (collectionId, fromRowVersion, ctx) => this.pullSinceUnscheduled(collectionId, fromRowVersion, ctx),
1099
1115
  runInHydrationScope: async (task) => task(this.hydrationAdapter)
1100
1116
  };
1101
1117
  }
1118
+ claimCacheGeneration(collectionId) {
1119
+ return this.runRegular(async () => {
1120
+ await this.ensureInitialized();
1121
+ return this.driver.transaction(async (driver) => {
1122
+ const expiresAtMs = this.now() + this.cacheGenerationClaimTtlMs;
1123
+ const initialPhysicalId = `${CACHE_GENERATION_ID_PREFIX}${db.safeRandomUUID()}`;
1124
+ await driver.run(
1125
+ `INSERT INTO cache_generation (logical_id, generation, physical_id, retired)
1126
+ SELECT ?, 0, ?, 0
1127
+ WHERE NOT EXISTS (
1128
+ SELECT 1 FROM cache_generation WHERE logical_id = ?
1129
+ )`,
1130
+ [collectionId, initialPhysicalId, collectionId]
1131
+ );
1132
+ const head = await driver.query(
1133
+ `SELECT physical_id FROM cache_generation
1134
+ WHERE logical_id = ? AND retired = 0`,
1135
+ [collectionId]
1136
+ );
1137
+ const storageCollectionId = head[0]?.physical_id;
1138
+ if (!storageCollectionId) {
1139
+ throw new errors.InvalidPersistedCollectionConfigError(
1140
+ `No active persisted cache generation for collection "${collectionId}"`
1141
+ );
1142
+ }
1143
+ const claimId = db.safeRandomUUID();
1144
+ await driver.run(
1145
+ `INSERT INTO cache_generation_claim
1146
+ (claim_id, logical_id, physical_id, expires_at_ms)
1147
+ VALUES (?, ?, ?, ?)`,
1148
+ [claimId, collectionId, storageCollectionId, expiresAtMs]
1149
+ );
1150
+ await this.collectRetiredCacheGenerations(driver);
1151
+ return { storageCollectionId, claimId, expiresAtMs };
1152
+ });
1153
+ });
1154
+ }
1155
+ rotateCacheGeneration(collectionId, claimId, resetMetadata, expectedStorageCollectionId) {
1156
+ return this.runRegular(async () => {
1157
+ await this.ensureInitialized();
1158
+ return this.driver.transaction(async (driver) => {
1159
+ const now = this.now();
1160
+ const expiresAtMs = now + this.cacheGenerationClaimTtlMs;
1161
+ const claimed = await driver.query(
1162
+ `SELECT physical_id, expires_at_ms
1163
+ FROM cache_generation_claim
1164
+ WHERE claim_id = ? AND logical_id = ?`,
1165
+ [claimId, collectionId]
1166
+ );
1167
+ const claimedPhysicalId = claimed[0] && claimed[0].expires_at_ms > now ? claimed[0].physical_id : void 0;
1168
+ if (claimedPhysicalId && expectedStorageCollectionId && claimedPhysicalId !== expectedStorageCollectionId) {
1169
+ throw new errors.InvalidPersistedCollectionConfigError(
1170
+ `Persisted cache claim changed generations before recovery`
1171
+ );
1172
+ }
1173
+ const head = await driver.query(
1174
+ `SELECT generation, physical_id FROM cache_generation
1175
+ WHERE logical_id = ? AND retired = 0`,
1176
+ [collectionId]
1177
+ );
1178
+ const active = head[0];
1179
+ if (!active) {
1180
+ throw new errors.InvalidPersistedCollectionConfigError(
1181
+ `No active persisted cache generation for collection "${collectionId}"`
1182
+ );
1183
+ }
1184
+ await driver.run(
1185
+ `INSERT INTO cache_generation_sequence (logical_id, next_generation)
1186
+ SELECT ?, COALESCE(MAX(generation), -1) + 1
1187
+ FROM cache_generation WHERE logical_id = ?
1188
+ ON CONFLICT(logical_id) DO NOTHING`,
1189
+ [collectionId, collectionId]
1190
+ );
1191
+ const nextRows = await driver.query(
1192
+ `SELECT next_generation FROM cache_generation_sequence
1193
+ WHERE logical_id = ?`,
1194
+ [collectionId]
1195
+ );
1196
+ const nextGeneration = nextRows[0].next_generation;
1197
+ await driver.run(
1198
+ `UPDATE cache_generation_sequence SET next_generation = ?
1199
+ WHERE logical_id = ?`,
1200
+ [nextGeneration + 1, collectionId]
1201
+ );
1202
+ const ownedHead = claimedPhysicalId === active.physical_id;
1203
+ const storageCollectionId = `${CACHE_GENERATION_ID_PREFIX}${db.safeRandomUUID()}`;
1204
+ if (ownedHead) {
1205
+ await driver.run(
1206
+ `UPDATE cache_generation SET retired = 1
1207
+ WHERE logical_id = ? AND generation = ?`,
1208
+ [collectionId, active.generation]
1209
+ );
1210
+ }
1211
+ await driver.run(
1212
+ `INSERT INTO cache_generation (logical_id, generation, physical_id, retired)
1213
+ VALUES (?, ?, ?, ?)`,
1214
+ [
1215
+ collectionId,
1216
+ nextGeneration,
1217
+ storageCollectionId,
1218
+ ownedHead ? 0 : 1
1219
+ ]
1220
+ );
1221
+ const nextClaimId = claimed[0] ? claimId : db.safeRandomUUID();
1222
+ if (claimed[0]) {
1223
+ await driver.run(
1224
+ `UPDATE cache_generation_claim
1225
+ SET physical_id = ?, expires_at_ms = ? WHERE claim_id = ?`,
1226
+ [storageCollectionId, expiresAtMs, claimId]
1227
+ );
1228
+ } else {
1229
+ await driver.run(
1230
+ `INSERT INTO cache_generation_claim
1231
+ (claim_id, logical_id, physical_id, expires_at_ms)
1232
+ VALUES (?, ?, ?, ?)`,
1233
+ [nextClaimId, collectionId, storageCollectionId, expiresAtMs]
1234
+ );
1235
+ }
1236
+ await driver.run(
1237
+ `INSERT INTO collection_version (
1238
+ collection_id, latest_row_version,
1239
+ key_set_evidence_available, key_set_evidence_incompatible
1240
+ ) VALUES (?, 0, 1, 1)`,
1241
+ [storageCollectionId]
1242
+ );
1243
+ if (resetMetadata) {
1244
+ await driver.run(
1245
+ `INSERT INTO collection_metadata (collection_id, key, value, updated_at)
1246
+ VALUES (?, ?, ?, CAST(strftime('%s', 'now') AS INTEGER))`,
1247
+ [
1248
+ storageCollectionId,
1249
+ resetMetadata.key,
1250
+ serializePersistedRowValue(resetMetadata.value)
1251
+ ]
1252
+ );
1253
+ }
1254
+ await this.collectRetiredCacheGenerations(driver);
1255
+ return { storageCollectionId, claimId: nextClaimId, expiresAtMs };
1256
+ });
1257
+ });
1258
+ }
1259
+ releaseCacheGenerationClaim(claimId) {
1260
+ return this.runRegular(async () => {
1261
+ await this.ensureInitialized();
1262
+ await this.runInTransaction(async (driver) => {
1263
+ await driver.run(
1264
+ `DELETE FROM cache_generation_claim WHERE claim_id = ?`,
1265
+ [claimId]
1266
+ );
1267
+ await this.collectRetiredCacheGenerations(driver);
1268
+ });
1269
+ });
1270
+ }
1271
+ renewCacheGenerationClaim(storageCollectionId, claimId) {
1272
+ return this.runRegular(
1273
+ () => this.renewCacheGenerationClaimUnscheduled(storageCollectionId, claimId)
1274
+ );
1275
+ }
1276
+ async renewCacheGenerationClaimUnscheduled(storageCollectionId, claimId) {
1277
+ await this.ensureInitialized();
1278
+ return this.runInTransaction(async (driver) => {
1279
+ const now = this.now();
1280
+ const rows = await driver.query(
1281
+ `SELECT claim_id FROM cache_generation_claim
1282
+ WHERE claim_id = ? AND physical_id = ? AND expires_at_ms > ?`,
1283
+ [claimId, storageCollectionId, now]
1284
+ );
1285
+ if (!rows[0]) {
1286
+ await this.collectRetiredCacheGenerations(driver);
1287
+ return void 0;
1288
+ }
1289
+ const expiresAtMs = now + this.cacheGenerationClaimTtlMs;
1290
+ await driver.run(
1291
+ `UPDATE cache_generation_claim SET expires_at_ms = ?
1292
+ WHERE claim_id = ?`,
1293
+ [expiresAtMs, claimId]
1294
+ );
1295
+ return expiresAtMs;
1296
+ });
1297
+ }
1298
+ async collectRetiredCacheGenerations(driver) {
1299
+ await driver.run(
1300
+ `DELETE FROM cache_generation_claim WHERE expires_at_ms <= ?`,
1301
+ [this.now()]
1302
+ );
1303
+ const retired = await driver.query(
1304
+ `SELECT generation.physical_id, registry.table_name,
1305
+ registry.tombstone_table_name
1306
+ FROM cache_generation AS generation
1307
+ LEFT JOIN collection_registry AS registry
1308
+ ON registry.collection_id = generation.physical_id
1309
+ WHERE generation.retired = 1
1310
+ AND NOT EXISTS (
1311
+ SELECT 1 FROM cache_generation_claim AS claim
1312
+ WHERE claim.physical_id = generation.physical_id
1313
+ )`
1314
+ );
1315
+ for (const generation of retired) {
1316
+ if (generation.table_name) {
1317
+ await driver.exec(
1318
+ `DROP TABLE IF EXISTS ${quoteIdentifier(generation.table_name)}`
1319
+ );
1320
+ }
1321
+ if (generation.tombstone_table_name) {
1322
+ await driver.exec(
1323
+ `DROP TABLE IF EXISTS ${quoteIdentifier(generation.tombstone_table_name)}`
1324
+ );
1325
+ }
1326
+ for (const table of [
1327
+ `applied_tx`,
1328
+ `collection_expected_keys`,
1329
+ `collection_metadata`,
1330
+ `persisted_index_registry`,
1331
+ `leader_term`
1332
+ ]) {
1333
+ await driver.run(`DELETE FROM ${table} WHERE collection_id = ?`, [
1334
+ generation.physical_id
1335
+ ]);
1336
+ }
1337
+ await driver.run(
1338
+ `DELETE FROM collection_version WHERE collection_id = ?`,
1339
+ [generation.physical_id]
1340
+ );
1341
+ await driver.run(
1342
+ `DELETE FROM collection_reset_epoch WHERE collection_id = ?`,
1343
+ [generation.physical_id]
1344
+ );
1345
+ await driver.run(
1346
+ `DELETE FROM collection_registry WHERE collection_id = ?`,
1347
+ [generation.physical_id]
1348
+ );
1349
+ await driver.run(`DELETE FROM cache_generation WHERE physical_id = ?`, [
1350
+ generation.physical_id
1351
+ ]);
1352
+ this.collectionTableCache.delete(generation.physical_id);
1353
+ }
1354
+ }
1355
+ getCacheGenerationNow() {
1356
+ return this.now();
1357
+ }
1102
1358
  runInHydrationScope(task) {
1103
1359
  const scheduler = this.resolveScheduler();
1104
1360
  if (scheduler) {
@@ -1153,14 +1409,63 @@ class SQLiteCorePersistenceAdapter {
1153
1409
  );
1154
1410
  }
1155
1411
  }
1156
- loadSubset(collectionId, options, ctx) {
1412
+ async assertCacheGenerationReadClaim(driver, storageCollectionId, claimId) {
1413
+ await this.ensureInitialized();
1414
+ if (!claimId) {
1415
+ if (!hasCacheGenerationIdPrefix(storageCollectionId)) return;
1416
+ const generation = await driver.query(
1417
+ `SELECT physical_id FROM cache_generation WHERE physical_id = ?`,
1418
+ [storageCollectionId]
1419
+ );
1420
+ if (generation[0]) {
1421
+ throw new errors.InvalidPersistedCollectionConfigError(
1422
+ `Persisted cache claim is required for collection "${storageCollectionId}"`
1423
+ );
1424
+ }
1425
+ return;
1426
+ }
1427
+ const rows = await driver.query(
1428
+ `SELECT claim.claim_id
1429
+ FROM cache_generation_claim AS claim
1430
+ JOIN cache_generation AS generation
1431
+ ON generation.physical_id = claim.physical_id
1432
+ WHERE claim.claim_id = ? AND claim.physical_id = ?
1433
+ AND claim.expires_at_ms > ?`,
1434
+ [claimId, storageCollectionId, this.now()]
1435
+ );
1436
+ if (!rows[0]) {
1437
+ throw new errors.InvalidPersistedCollectionConfigError(
1438
+ `Persisted cache claim is no longer active for collection "${storageCollectionId}"`
1439
+ );
1440
+ }
1441
+ }
1442
+ loadSubset(target, options, ctx) {
1157
1443
  return this.runRegular(
1158
- () => this.loadSubsetUnscheduled(collectionId, options, ctx)
1444
+ () => this.loadSubsetUnscheduled(target, options, ctx)
1159
1445
  );
1160
1446
  }
1161
- async loadSubsetUnscheduled(collectionId, options, ctx) {
1162
- const tableMapping = await this.ensureCollectionReady(collectionId);
1447
+ async loadSubsetUnscheduled(target, options, ctx) {
1448
+ const access = persisted.resolvePersistedStorageTarget(
1449
+ target,
1450
+ ctx?.cacheGenerationClaimId
1451
+ );
1452
+ const collectionId = access.collectionId;
1453
+ ctx = access.claimId ? { ...ctx, cacheGenerationClaimId: access.claimId } : ctx;
1454
+ await this.assertCacheGenerationReadClaim(
1455
+ this.driver,
1456
+ collectionId,
1457
+ ctx?.cacheGenerationClaimId
1458
+ );
1459
+ const tableMapping = await this.ensureCollectionReady(
1460
+ collectionId,
1461
+ ctx?.cacheGenerationClaimId
1462
+ );
1163
1463
  return this.runInTransaction(async (transactionDriver) => {
1464
+ await this.assertCacheGenerationReadClaim(
1465
+ transactionDriver,
1466
+ collectionId,
1467
+ ctx?.cacheGenerationClaimId
1468
+ );
1164
1469
  await this.assertCurrentSchemaVersion(
1165
1470
  collectionId,
1166
1471
  transactionDriver,
@@ -1219,15 +1524,34 @@ class SQLiteCorePersistenceAdapter {
1219
1524
  }));
1220
1525
  });
1221
1526
  }
1222
- loadResumeSnapshot(collectionId, ctx) {
1527
+ loadResumeSnapshot(target, ctx) {
1223
1528
  return this.runRegular(
1224
- () => this.loadResumeSnapshotUnscheduled(collectionId, ctx)
1529
+ () => this.loadResumeSnapshotUnscheduled(target, ctx)
1225
1530
  );
1226
1531
  }
1227
- async loadResumeSnapshotUnscheduled(collectionId, ctx) {
1228
- const tableMapping = await this.ensureCollectionReady(collectionId);
1532
+ async loadResumeSnapshotUnscheduled(target, ctx) {
1533
+ const access = persisted.resolvePersistedStorageTarget(
1534
+ target,
1535
+ ctx?.cacheGenerationClaimId
1536
+ );
1537
+ const collectionId = access.collectionId;
1538
+ ctx = access.claimId ? { ...ctx, cacheGenerationClaimId: access.claimId } : ctx;
1539
+ await this.assertCacheGenerationReadClaim(
1540
+ this.driver,
1541
+ collectionId,
1542
+ ctx?.cacheGenerationClaimId
1543
+ );
1544
+ const tableMapping = await this.ensureCollectionReady(
1545
+ collectionId,
1546
+ ctx?.cacheGenerationClaimId
1547
+ );
1229
1548
  const includeRows = ctx?.includeRows !== false;
1230
1549
  return this.runInTransaction(async (transactionDriver) => {
1550
+ await this.assertCacheGenerationReadClaim(
1551
+ transactionDriver,
1552
+ collectionId,
1553
+ ctx?.cacheGenerationClaimId
1554
+ );
1231
1555
  await this.assertCurrentSchemaVersion(
1232
1556
  collectionId,
1233
1557
  transactionDriver,
@@ -1280,20 +1604,55 @@ class SQLiteCorePersistenceAdapter {
1280
1604
  };
1281
1605
  });
1282
1606
  }
1283
- applyCommittedTx(collectionId, tx) {
1607
+ applyCommittedTx(target, tx) {
1608
+ return this.runRegular(() => this.applyCommittedTxUnscheduled(target, tx));
1609
+ }
1610
+ reconcileCommittedTx(target, tx, anchor) {
1284
1611
  return this.runRegular(
1285
- () => this.applyCommittedTxUnscheduled(collectionId, tx)
1612
+ () => this.reconcileCommittedTxUnscheduled(target, tx, anchor)
1286
1613
  );
1287
1614
  }
1288
- async applyCommittedTxUnscheduled(collectionId, tx) {
1289
- const tableMapping = await this.ensureCollectionReady(collectionId);
1615
+ async applyCommittedTxUnscheduled(target, tx) {
1616
+ await this.applyCommittedTxInternal(target, tx);
1617
+ }
1618
+ reconcileCommittedTxUnscheduled(target, tx, anchor) {
1619
+ if (!committedTxAnchor.isValidCommittedTxAnchor(anchor)) {
1620
+ throw new Error(
1621
+ `Cannot reconcile a committed transaction without a valid durable anchor`
1622
+ );
1623
+ }
1624
+ return this.applyCommittedTxInternal(target, tx, anchor);
1625
+ }
1626
+ async applyCommittedTxInternal(target, tx, anchor) {
1627
+ const access = persisted.resolvePersistedStorageTarget(
1628
+ target,
1629
+ tx.cacheGenerationClaimId
1630
+ );
1631
+ const collectionId = access.collectionId;
1632
+ tx = { ...tx, cacheGenerationClaimId: access.claimId };
1633
+ await this.assertCacheGenerationReadClaim(
1634
+ this.driver,
1635
+ collectionId,
1636
+ tx.cacheGenerationClaimId
1637
+ );
1638
+ const tableMapping = await this.ensureCollectionReady(
1639
+ collectionId,
1640
+ tx.cacheGenerationClaimId
1641
+ );
1290
1642
  const collectionTableSql = quoteIdentifier(tableMapping.tableName);
1291
1643
  const tombstoneTableSql = quoteIdentifier(tableMapping.tombstoneTableName);
1292
- await this.runInTransaction(async (transactionDriver) => {
1644
+ return this.runInTransaction(async (transactionDriver) => {
1645
+ await this.assertCacheGenerationReadClaim(
1646
+ transactionDriver,
1647
+ collectionId,
1648
+ tx.cacheGenerationClaimId
1649
+ );
1293
1650
  const versionRows = await transactionDriver.query(
1294
1651
  `SELECT
1295
1652
  latest_row_version,
1296
1653
  key_set_evidence_available,
1654
+ (SELECT reset_epoch FROM collection_reset_epoch
1655
+ WHERE collection_id = ? LIMIT 1) AS reset_epoch,
1297
1656
  (
1298
1657
  SELECT schema_version
1299
1658
  FROM collection_registry
@@ -1308,7 +1667,14 @@ class SQLiteCorePersistenceAdapter {
1308
1667
  FROM collection_version
1309
1668
  WHERE collection_id = ?
1310
1669
  LIMIT 1`,
1311
- [collectionId, collectionId, tx.term, tx.seq, collectionId]
1670
+ [
1671
+ collectionId,
1672
+ collectionId,
1673
+ collectionId,
1674
+ tx.term,
1675
+ tx.seq,
1676
+ collectionId
1677
+ ]
1312
1678
  );
1313
1679
  const version = versionRows[0];
1314
1680
  if (!version) {
@@ -1321,8 +1687,34 @@ class SQLiteCorePersistenceAdapter {
1321
1687
  `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
1688
  );
1323
1689
  }
1690
+ if (anchor) {
1691
+ const sameResetEpoch = (version.reset_epoch ?? 0) === anchor.resetEpoch;
1692
+ const matching = await transactionDriver.query(
1693
+ `SELECT term, seq, row_version FROM applied_tx
1694
+ WHERE collection_id = ? AND tx_id = ? LIMIT 2`,
1695
+ [collectionId, tx.txId]
1696
+ );
1697
+ if (matching.length > 1) return { kind: `unknown` };
1698
+ if (matching[0]) {
1699
+ if (matching[0].row_version <= anchor.latestRowVersion || !sameResetEpoch) {
1700
+ return { kind: `unknown` };
1701
+ }
1702
+ return {
1703
+ kind: `already-applied`,
1704
+ committed: {
1705
+ term: matching[0].term,
1706
+ seq: matching[0].seq,
1707
+ rowVersion: matching[0].row_version
1708
+ },
1709
+ latestRowVersion: version.latest_row_version
1710
+ };
1711
+ }
1712
+ if (version.latest_row_version !== anchor.latestRowVersion || !sameResetEpoch) {
1713
+ return { kind: `unknown` };
1714
+ }
1715
+ }
1324
1716
  if (version.already_applied === 1) {
1325
- return;
1717
+ return { kind: `unknown` };
1326
1718
  }
1327
1719
  const currentRowVersion = version.latest_row_version;
1328
1720
  const nextRowVersion = Math.max(currentRowVersion + 1, tx.rowVersion);
@@ -1570,16 +1962,41 @@ class SQLiteCorePersistenceAdapter {
1570
1962
  ]
1571
1963
  );
1572
1964
  await this.pruneAppliedTxRows(collectionId, transactionDriver);
1965
+ return {
1966
+ kind: `applied-now`,
1967
+ committed: {
1968
+ term: tx.term,
1969
+ seq: tx.seq,
1970
+ rowVersion: nextRowVersion
1971
+ },
1972
+ latestRowVersion: nextRowVersion
1973
+ };
1573
1974
  });
1574
1975
  }
1575
- loadCollectionMetadata(collectionId) {
1976
+ loadCollectionMetadata(target, ctx) {
1576
1977
  return this.runRegular(
1577
- () => this.loadCollectionMetadataUnscheduled(collectionId)
1978
+ () => this.loadCollectionMetadataUnscheduled(target, ctx)
1578
1979
  );
1579
1980
  }
1580
- async loadCollectionMetadataUnscheduled(collectionId) {
1581
- await this.ensureCollectionReady(collectionId);
1981
+ async loadCollectionMetadataUnscheduled(target, ctx) {
1982
+ const access = persisted.resolvePersistedStorageTarget(
1983
+ target,
1984
+ ctx?.cacheGenerationClaimId
1985
+ );
1986
+ const collectionId = access.collectionId;
1987
+ ctx = access.claimId ? { ...ctx, cacheGenerationClaimId: access.claimId } : ctx;
1988
+ await this.assertCacheGenerationReadClaim(
1989
+ this.driver,
1990
+ collectionId,
1991
+ ctx?.cacheGenerationClaimId
1992
+ );
1993
+ await this.ensureCollectionReady(collectionId, ctx?.cacheGenerationClaimId);
1582
1994
  return this.runInTransaction(async (transactionDriver) => {
1995
+ await this.assertCacheGenerationReadClaim(
1996
+ transactionDriver,
1997
+ collectionId,
1998
+ ctx?.cacheGenerationClaimId
1999
+ );
1583
2000
  await this.assertCurrentSchemaVersion(
1584
2001
  collectionId,
1585
2002
  transactionDriver,
@@ -1597,15 +2014,32 @@ class SQLiteCorePersistenceAdapter {
1597
2014
  }));
1598
2015
  });
1599
2016
  }
1600
- scanRows(collectionId, options) {
1601
- return this.runRegular(
1602
- () => this.scanRowsUnscheduled(collectionId, options)
1603
- );
2017
+ scanRows(target, options, ctx) {
2018
+ return this.runRegular(() => this.scanRowsUnscheduled(target, options, ctx));
1604
2019
  }
1605
- async scanRowsUnscheduled(collectionId, options) {
1606
- const tableMapping = await this.ensureCollectionReady(collectionId);
2020
+ async scanRowsUnscheduled(target, options, ctx) {
2021
+ const access = persisted.resolvePersistedStorageTarget(
2022
+ target,
2023
+ ctx?.cacheGenerationClaimId
2024
+ );
2025
+ const collectionId = access.collectionId;
2026
+ ctx = access.claimId ? { ...ctx, cacheGenerationClaimId: access.claimId } : ctx;
2027
+ await this.assertCacheGenerationReadClaim(
2028
+ this.driver,
2029
+ collectionId,
2030
+ ctx?.cacheGenerationClaimId
2031
+ );
2032
+ const tableMapping = await this.ensureCollectionReady(
2033
+ collectionId,
2034
+ ctx?.cacheGenerationClaimId
2035
+ );
1607
2036
  const collectionTableSql = quoteIdentifier(tableMapping.tableName);
1608
2037
  return this.runInTransaction(async (transactionDriver) => {
2038
+ await this.assertCacheGenerationReadClaim(
2039
+ transactionDriver,
2040
+ collectionId,
2041
+ ctx?.cacheGenerationClaimId
2042
+ );
1609
2043
  await this.assertCurrentSchemaVersion(
1610
2044
  collectionId,
1611
2045
  transactionDriver,
@@ -1624,13 +2058,27 @@ class SQLiteCorePersistenceAdapter {
1624
2058
  }));
1625
2059
  });
1626
2060
  }
1627
- ensureIndex(collectionId, signature, spec) {
2061
+ ensureIndex(target, signature, spec, ctx) {
1628
2062
  return this.runRegular(
1629
- () => this.ensureIndexUnscheduled(collectionId, signature, spec)
2063
+ () => this.ensureIndexUnscheduled(target, signature, spec, ctx)
1630
2064
  );
1631
2065
  }
1632
- async ensureIndexUnscheduled(collectionId, signature, spec) {
1633
- const tableMapping = await this.ensureCollectionReady(collectionId);
2066
+ async ensureIndexUnscheduled(target, signature, spec, ctx) {
2067
+ const access = persisted.resolvePersistedStorageTarget(
2068
+ target,
2069
+ ctx?.cacheGenerationClaimId
2070
+ );
2071
+ const collectionId = access.collectionId;
2072
+ ctx = access.claimId ? { ...ctx, cacheGenerationClaimId: access.claimId } : ctx;
2073
+ await this.assertCacheGenerationReadClaim(
2074
+ this.driver,
2075
+ collectionId,
2076
+ ctx?.cacheGenerationClaimId
2077
+ );
2078
+ const tableMapping = await this.ensureCollectionReady(
2079
+ collectionId,
2080
+ ctx?.cacheGenerationClaimId
2081
+ );
1634
2082
  const collectionTableSql = quoteIdentifier(tableMapping.tableName);
1635
2083
  const indexName = buildIndexName(collectionId, signature);
1636
2084
  const indexNameSql = quoteIdentifier(indexName);
@@ -1642,6 +2090,11 @@ class SQLiteCorePersistenceAdapter {
1642
2090
  const whereSql = spec.whereSql ? normalizeIndexSqlFragment(spec.whereSql) : void 0;
1643
2091
  const persistedWhereSql = whereSql ?? null;
1644
2092
  await this.runInTransaction(async (transactionDriver) => {
2093
+ await this.assertCacheGenerationReadClaim(
2094
+ transactionDriver,
2095
+ collectionId,
2096
+ ctx?.cacheGenerationClaimId
2097
+ );
1645
2098
  await this.assertCurrentSchemaVersion(
1646
2099
  collectionId,
1647
2100
  transactionDriver,
@@ -1698,14 +2151,30 @@ class SQLiteCorePersistenceAdapter {
1698
2151
  await transactionDriver.exec(createIndexSql);
1699
2152
  });
1700
2153
  }
1701
- markIndexRemoved(collectionId, signature) {
2154
+ markIndexRemoved(target, signature, ctx) {
1702
2155
  return this.runRegular(
1703
- () => this.markIndexRemovedUnscheduled(collectionId, signature)
2156
+ () => this.markIndexRemovedUnscheduled(target, signature, ctx)
1704
2157
  );
1705
2158
  }
1706
- async markIndexRemovedUnscheduled(collectionId, signature) {
1707
- await this.ensureCollectionReady(collectionId);
2159
+ async markIndexRemovedUnscheduled(target, signature, ctx) {
2160
+ const access = persisted.resolvePersistedStorageTarget(
2161
+ target,
2162
+ ctx?.cacheGenerationClaimId
2163
+ );
2164
+ const collectionId = access.collectionId;
2165
+ ctx = access.claimId ? { ...ctx, cacheGenerationClaimId: access.claimId } : ctx;
2166
+ await this.assertCacheGenerationReadClaim(
2167
+ this.driver,
2168
+ collectionId,
2169
+ ctx?.cacheGenerationClaimId
2170
+ );
2171
+ await this.ensureCollectionReady(collectionId, ctx?.cacheGenerationClaimId);
1708
2172
  await this.runInTransaction(async (transactionDriver) => {
2173
+ await this.assertCacheGenerationReadClaim(
2174
+ transactionDriver,
2175
+ collectionId,
2176
+ ctx?.cacheGenerationClaimId
2177
+ );
1709
2178
  await this.assertCurrentSchemaVersion(
1710
2179
  collectionId,
1711
2180
  transactionDriver,
@@ -1734,12 +2203,69 @@ class SQLiteCorePersistenceAdapter {
1734
2203
  }
1735
2204
  });
1736
2205
  }
1737
- getStreamPosition(collectionId) {
1738
- return this.getStreamPositionUnscheduled(collectionId);
2206
+ reserveLeadershipTerm(target, observedTerm, ctx) {
2207
+ return this.reserveLeadershipTermUnscheduled(target, observedTerm, ctx);
1739
2208
  }
1740
- async getStreamPositionUnscheduled(collectionId) {
1741
- await this.ensureCollectionReady(collectionId);
2209
+ async reserveLeadershipTermUnscheduled(target, observedTerm, ctx) {
2210
+ const access = persisted.resolvePersistedStorageTarget(
2211
+ target,
2212
+ ctx?.cacheGenerationClaimId
2213
+ );
2214
+ const collectionId = access.collectionId;
2215
+ ctx = access.claimId ? { ...ctx, cacheGenerationClaimId: access.claimId } : ctx;
2216
+ await this.assertCacheGenerationReadClaim(
2217
+ this.driver,
2218
+ collectionId,
2219
+ ctx?.cacheGenerationClaimId
2220
+ );
2221
+ await this.ensureCollectionReady(collectionId, ctx?.cacheGenerationClaimId);
2222
+ return this.runInTransaction(async (transactionDriver) => {
2223
+ await this.assertCacheGenerationReadClaim(
2224
+ transactionDriver,
2225
+ collectionId,
2226
+ ctx?.cacheGenerationClaimId
2227
+ );
2228
+ await this.assertCurrentSchemaVersion(
2229
+ collectionId,
2230
+ transactionDriver,
2231
+ `reserve a leadership term`
2232
+ );
2233
+ await transactionDriver.run(
2234
+ `INSERT INTO leader_term (collection_id, latest_term)
2235
+ VALUES (?, ? + 1)
2236
+ ON CONFLICT(collection_id) DO UPDATE SET
2237
+ latest_term = MAX(leader_term.latest_term, ?) + 1`,
2238
+ [collectionId, observedTerm, observedTerm]
2239
+ );
2240
+ const [position, latestRowVersion] = await Promise.all([
2241
+ this.readStreamPosition(collectionId, transactionDriver),
2242
+ this.readLatestRowVersion(collectionId, transactionDriver)
2243
+ ]);
2244
+ return { ...position, latestRowVersion };
2245
+ });
2246
+ }
2247
+ getStreamPosition(target, ctx) {
2248
+ return this.getStreamPositionUnscheduled(target, ctx);
2249
+ }
2250
+ async getStreamPositionUnscheduled(target, ctx) {
2251
+ const access = persisted.resolvePersistedStorageTarget(
2252
+ target,
2253
+ ctx?.cacheGenerationClaimId
2254
+ );
2255
+ const collectionId = access.collectionId;
2256
+ ctx = access.claimId ? { ...ctx, cacheGenerationClaimId: access.claimId } : ctx;
2257
+ await this.assertCacheGenerationReadClaim(
2258
+ this.driver,
2259
+ collectionId,
2260
+ ctx?.cacheGenerationClaimId
2261
+ );
2262
+ await this.ensureCollectionReady(collectionId, ctx?.cacheGenerationClaimId);
1742
2263
  return this.runInTransaction(async (transactionDriver) => {
2264
+ await this.assertCacheGenerationReadClaim(
2265
+ transactionDriver,
2266
+ collectionId,
2267
+ ctx?.cacheGenerationClaimId
2268
+ );
1743
2269
  await this.assertCurrentSchemaVersion(
1744
2270
  collectionId,
1745
2271
  transactionDriver,
@@ -1807,16 +2333,35 @@ class SQLiteCorePersistenceAdapter {
1807
2333
  }
1808
2334
  };
1809
2335
  }
1810
- pullSince(collectionId, fromRowVersion) {
2336
+ pullSince(target, fromRowVersion, ctx) {
1811
2337
  return this.runRegular(
1812
- () => this.pullSinceUnscheduled(collectionId, fromRowVersion)
2338
+ () => this.pullSinceUnscheduled(target, fromRowVersion, ctx)
1813
2339
  );
1814
2340
  }
1815
- async pullSinceUnscheduled(collectionId, fromRowVersion) {
1816
- const tableMapping = await this.ensureCollectionReady(collectionId);
2341
+ async pullSinceUnscheduled(target, fromRowVersion, ctx) {
2342
+ const access = persisted.resolvePersistedStorageTarget(
2343
+ target,
2344
+ ctx?.cacheGenerationClaimId
2345
+ );
2346
+ const collectionId = access.collectionId;
2347
+ ctx = access.claimId ? { ...ctx, cacheGenerationClaimId: access.claimId } : ctx;
2348
+ await this.assertCacheGenerationReadClaim(
2349
+ this.driver,
2350
+ collectionId,
2351
+ ctx?.cacheGenerationClaimId
2352
+ );
2353
+ const tableMapping = await this.ensureCollectionReady(
2354
+ collectionId,
2355
+ ctx?.cacheGenerationClaimId
2356
+ );
1817
2357
  const collectionTableSql = quoteIdentifier(tableMapping.tableName);
1818
2358
  const tombstoneTableSql = quoteIdentifier(tableMapping.tombstoneTableName);
1819
2359
  return this.runInTransaction(async (transactionDriver) => {
2360
+ await this.assertCacheGenerationReadClaim(
2361
+ transactionDriver,
2362
+ collectionId,
2363
+ ctx?.cacheGenerationClaimId
2364
+ );
1820
2365
  await this.assertCurrentSchemaVersion(
1821
2366
  collectionId,
1822
2367
  transactionDriver,
@@ -2052,7 +2597,7 @@ class SQLiteCorePersistenceAdapter {
2052
2597
  [collectionId, excessRows]
2053
2598
  );
2054
2599
  }
2055
- async ensureCollectionReady(collectionId) {
2600
+ async ensureCollectionReady(collectionId, cacheGenerationClaimId) {
2056
2601
  await this.ensureInitialized();
2057
2602
  const cached = this.collectionTableCache.get(collectionId);
2058
2603
  if (cached) {
@@ -2062,7 +2607,10 @@ class SQLiteCorePersistenceAdapter {
2062
2607
  if (inFlight) {
2063
2608
  return inFlight;
2064
2609
  }
2065
- const loadPromise = this.ensureCollectionReadyInternal(collectionId);
2610
+ const loadPromise = this.ensureCollectionReadyInternal(
2611
+ collectionId,
2612
+ cacheGenerationClaimId
2613
+ );
2066
2614
  this.collectionTableLoads.set(collectionId, loadPromise);
2067
2615
  try {
2068
2616
  return await loadPromise;
@@ -2080,23 +2628,34 @@ class SQLiteCorePersistenceAdapter {
2080
2628
  );
2081
2629
  return rows[0];
2082
2630
  }
2083
- async ensureCollectionReadyInternal(collectionId) {
2631
+ async ensureCollectionReadyInternal(collectionId, cacheGenerationClaimId) {
2632
+ const managedAtAdmission = cacheGenerationClaimId !== void 0 || hasCacheGenerationIdPrefix(collectionId) && (await this.driver.query(
2633
+ `SELECT physical_id FROM cache_generation WHERE physical_id = ?`,
2634
+ [collectionId]
2635
+ ))[0] !== void 0;
2084
2636
  let registration = await this.loadCollectionRegistration(collectionId);
2085
2637
  if (!registration) {
2086
2638
  const tableName2 = persisted.createPersistedTableName(collectionId, `c`);
2087
2639
  const tombstoneTableName2 = persisted.createPersistedTableName(collectionId, `t`);
2088
- await this.driver.run(
2089
- `INSERT INTO collection_registry (
2090
- collection_id,
2091
- table_name,
2092
- tombstone_table_name,
2093
- schema_version,
2094
- updated_at
2095
- )
2096
- VALUES (?, ?, ?, ?, CAST(strftime('%s', 'now') AS INTEGER))
2097
- ON CONFLICT DO NOTHING`,
2098
- [collectionId, tableName2, tombstoneTableName2, this.schemaVersion]
2099
- );
2640
+ await this.runInTransaction(async (transactionDriver) => {
2641
+ await this.assertCacheGenerationReadClaim(
2642
+ transactionDriver,
2643
+ collectionId,
2644
+ cacheGenerationClaimId
2645
+ );
2646
+ await transactionDriver.run(
2647
+ `INSERT INTO collection_registry (
2648
+ collection_id,
2649
+ table_name,
2650
+ tombstone_table_name,
2651
+ schema_version,
2652
+ updated_at
2653
+ )
2654
+ VALUES (?, ?, ?, ?, CAST(strftime('%s', 'now') AS INTEGER))
2655
+ ON CONFLICT DO NOTHING`,
2656
+ [collectionId, tableName2, tombstoneTableName2, this.schemaVersion]
2657
+ );
2658
+ });
2100
2659
  registration = await this.loadCollectionRegistration(collectionId);
2101
2660
  if (!registration) {
2102
2661
  throw new errors.InvalidPersistedCollectionConfigError(
@@ -2112,7 +2671,8 @@ class SQLiteCorePersistenceAdapter {
2112
2671
  registration.schema_version,
2113
2672
  this.schemaVersion,
2114
2673
  tableName,
2115
- tombstoneTableName
2674
+ tombstoneTableName,
2675
+ cacheGenerationClaimId
2116
2676
  );
2117
2677
  }
2118
2678
  const collectionTableSql = quoteIdentifier(tableName);
@@ -2159,6 +2719,35 @@ class SQLiteCorePersistenceAdapter {
2159
2719
  [collectionId]
2160
2720
  );
2161
2721
  await this.ensureCollectionKeyEvidenceTriggers(collectionId, tableName);
2722
+ if (managedAtAdmission) {
2723
+ const generation = await this.driver.query(
2724
+ `SELECT physical_id FROM cache_generation WHERE physical_id = ?`,
2725
+ [collectionId]
2726
+ );
2727
+ if (!generation[0]) {
2728
+ await this.driver.exec(
2729
+ `DROP TABLE IF EXISTS ${quoteIdentifier(tableName)}`
2730
+ );
2731
+ await this.driver.exec(
2732
+ `DROP TABLE IF EXISTS ${quoteIdentifier(tombstoneTableName)}`
2733
+ );
2734
+ await this.driver.run(
2735
+ `DELETE FROM collection_registry WHERE collection_id = ?`,
2736
+ [collectionId]
2737
+ );
2738
+ await this.driver.run(
2739
+ `DELETE FROM collection_version WHERE collection_id = ?`,
2740
+ [collectionId]
2741
+ );
2742
+ await this.driver.run(
2743
+ `DELETE FROM collection_reset_epoch WHERE collection_id = ?`,
2744
+ [collectionId]
2745
+ );
2746
+ throw new errors.InvalidPersistedCollectionConfigError(
2747
+ `Persisted cache generation was collected before registration finished`
2748
+ );
2749
+ }
2750
+ }
2162
2751
  const mapping = {
2163
2752
  tableName,
2164
2753
  tombstoneTableName
@@ -2228,7 +2817,7 @@ class SQLiteCorePersistenceAdapter {
2228
2817
  END`
2229
2818
  );
2230
2819
  }
2231
- async handleSchemaMismatch(collectionId, previousSchemaVersion, nextSchemaVersion, tableName, tombstoneTableName) {
2820
+ async handleSchemaMismatch(collectionId, previousSchemaVersion, nextSchemaVersion, tableName, tombstoneTableName, cacheGenerationClaimId) {
2232
2821
  if (this.schemaMismatchPolicy === `sync-absent-error`) {
2233
2822
  throw new errors.InvalidPersistedCollectionConfigError(
2234
2823
  `Schema version mismatch for collection "${collectionId}": found ${previousSchemaVersion}, expected ${nextSchemaVersion}. Set schemaMismatchPolicy to "sync-present-reset" or "reset" to allow automatic reset.`
@@ -2237,6 +2826,11 @@ class SQLiteCorePersistenceAdapter {
2237
2826
  const collectionTableSql = quoteIdentifier(tableName);
2238
2827
  const tombstoneTableSql = quoteIdentifier(tombstoneTableName);
2239
2828
  await this.runInTransaction(async (transactionDriver) => {
2829
+ await this.assertCacheGenerationReadClaim(
2830
+ transactionDriver,
2831
+ collectionId,
2832
+ cacheGenerationClaimId
2833
+ );
2240
2834
  const currentSchemaRows = await transactionDriver.query(
2241
2835
  `SELECT schema_version
2242
2836
  FROM collection_registry
@@ -2321,6 +2915,48 @@ class SQLiteCorePersistenceAdapter {
2321
2915
  if (this.initialized) {
2322
2916
  return;
2323
2917
  }
2918
+ await this.driver.exec(
2919
+ `CREATE TABLE IF NOT EXISTS cache_generation (
2920
+ logical_id TEXT NOT NULL,
2921
+ generation INTEGER NOT NULL,
2922
+ physical_id TEXT NOT NULL UNIQUE,
2923
+ retired INTEGER NOT NULL DEFAULT 0,
2924
+ PRIMARY KEY (logical_id, generation)
2925
+ )`
2926
+ );
2927
+ await this.driver.exec(
2928
+ `CREATE UNIQUE INDEX IF NOT EXISTS cache_generation_head
2929
+ ON cache_generation (logical_id) WHERE retired = 0`
2930
+ );
2931
+ await this.driver.exec(
2932
+ `CREATE TABLE IF NOT EXISTS cache_generation_sequence (
2933
+ logical_id TEXT PRIMARY KEY,
2934
+ next_generation INTEGER NOT NULL
2935
+ )`
2936
+ );
2937
+ await this.driver.exec(
2938
+ `CREATE TABLE IF NOT EXISTS cache_generation_claim (
2939
+ claim_id TEXT PRIMARY KEY,
2940
+ logical_id TEXT NOT NULL,
2941
+ physical_id TEXT NOT NULL,
2942
+ expires_at_ms INTEGER NOT NULL
2943
+ )`
2944
+ );
2945
+ const claimColumns = await this.driver.query(
2946
+ `PRAGMA table_info(cache_generation_claim)`
2947
+ );
2948
+ if (!claimColumns.some(({ name }) => name === `logical_id`)) {
2949
+ await this.driver.exec(
2950
+ `ALTER TABLE cache_generation_claim
2951
+ ADD COLUMN logical_id TEXT NOT NULL DEFAULT ''`
2952
+ );
2953
+ }
2954
+ if (!claimColumns.some(({ name }) => name === `expires_at_ms`)) {
2955
+ await this.driver.exec(
2956
+ `ALTER TABLE cache_generation_claim
2957
+ ADD COLUMN expires_at_ms INTEGER NOT NULL DEFAULT 0`
2958
+ );
2959
+ }
2324
2960
  await this.driver.exec(
2325
2961
  `CREATE TABLE IF NOT EXISTS collection_registry (
2326
2962
  collection_id TEXT PRIMARY KEY,
@@ -2357,6 +2993,10 @@ class SQLiteCorePersistenceAdapter {
2357
2993
  PRIMARY KEY (collection_id, term, seq)
2358
2994
  )`
2359
2995
  );
2996
+ await this.driver.exec(
2997
+ `CREATE INDEX IF NOT EXISTS idx_applied_tx_collection_tx_id
2998
+ ON applied_tx (collection_id, tx_id)`
2999
+ );
2360
3000
  try {
2361
3001
  await this.driver.exec(
2362
3002
  `ALTER TABLE applied_tx ADD COLUMN replay_json TEXT`
@@ -2456,6 +3096,7 @@ function createSQLiteCorePersistenceAdapter(options) {
2456
3096
  }
2457
3097
  exports.DEFAULT_APPLIED_TX_PRUNE_MAX_AGE_SECONDS = DEFAULT_APPLIED_TX_PRUNE_MAX_AGE_SECONDS;
2458
3098
  exports.DEFAULT_APPLIED_TX_PRUNE_MAX_ROWS = DEFAULT_APPLIED_TX_PRUNE_MAX_ROWS;
3099
+ exports.DEFAULT_CACHE_GENERATION_CLAIM_TTL_MS = DEFAULT_CACHE_GENERATION_CLAIM_TTL_MS;
2459
3100
  exports.SQLiteCorePersistenceAdapter = SQLiteCorePersistenceAdapter;
2460
3101
  exports.createSQLiteCorePersistenceAdapter = createSQLiteCorePersistenceAdapter;
2461
3102
  //# sourceMappingURL=sqlite-core-adapter.cjs.map