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