@tanstack/db-sqlite-persistence-core 0.4.8 → 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.
@@ -1,8 +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
3
  import { isValidCommittedTxAnchor } from "./committed-tx-anchor.js";
4
- import { encodePersistedStorageKey, createPersistedTableName, decodePersistedStorageKey, SQLITE_DRIVER_SHARED_LOGICAL_SCHEDULING_KEY } from "./persisted.js";
5
- import { serializeSQLiteTemporal, sqliteTemporalIdentity, assertSQLiteBigIntInRange, sqliteTemporalKind, PERSISTED_TYPE_TAG, PERSISTED_VALUE_TAG, reviveSQLiteTemporal, serializeSQLiteBigInt } from "./sqlite-value.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";
6
6
  const REPLACEMENT_BATCH_SIZE = 125;
7
7
  class SharedPersistenceScheduler {
8
8
  constructor() {
@@ -106,6 +106,12 @@ function observeSharedLogicalSchedulingSupport(driver, onSupport) {
106
106
  };
107
107
  }
108
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;
109
115
  const DEFAULT_PULL_SINCE_RELOAD_THRESHOLD = 128;
110
116
  const DEFAULT_APPLIED_TX_PRUNE_MAX_ROWS = 1e3;
111
117
  const DEFAULT_APPLIED_TX_PRUNE_MAX_AGE_SECONDS = 24 * 60 * 60;
@@ -1047,6 +1053,13 @@ class SQLiteCorePersistenceAdapter {
1047
1053
  );
1048
1054
  }
1049
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;
1050
1063
  const schemaVersion = options.schemaVersion ?? DEFAULT_SCHEMA_VERSION;
1051
1064
  if (!Number.isInteger(schemaVersion) || schemaVersion < 0) {
1052
1065
  throw new InvalidPersistedCollectionConfigError(
@@ -1089,16 +1102,257 @@ class SQLiteCorePersistenceAdapter {
1089
1102
  loadSubset: (collectionId, loadOptions, context) => this.loadSubsetUnscheduled(collectionId, loadOptions, context),
1090
1103
  loadResumeSnapshot: (collectionId, context) => this.loadResumeSnapshotUnscheduled(collectionId, context),
1091
1104
  applyCommittedTx: (collectionId, tx) => this.applyCommittedTxUnscheduled(collectionId, tx),
1105
+ renewCacheGenerationClaim: (storageCollectionId, claimId) => this.renewCacheGenerationClaimUnscheduled(storageCollectionId, claimId),
1092
1106
  reconcileCommittedTx: (collectionId, tx, anchor) => this.reconcileCommittedTxUnscheduled(collectionId, tx, anchor),
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
+ 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),
1099
1113
  runInHydrationScope: async (task) => task(this.hydrationAdapter)
1100
1114
  };
1101
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
+ }
1102
1356
  runInHydrationScope(task) {
1103
1357
  const scheduler = this.resolveScheduler();
1104
1358
  if (scheduler) {
@@ -1153,14 +1407,63 @@ class SQLiteCorePersistenceAdapter {
1153
1407
  );
1154
1408
  }
1155
1409
  }
1156
- 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) {
1157
1441
  return this.runRegular(
1158
- () => this.loadSubsetUnscheduled(collectionId, options, ctx)
1442
+ () => this.loadSubsetUnscheduled(target, options, ctx)
1159
1443
  );
1160
1444
  }
1161
- async loadSubsetUnscheduled(collectionId, options, ctx) {
1162
- 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
+ );
1163
1461
  return this.runInTransaction(async (transactionDriver) => {
1462
+ await this.assertCacheGenerationReadClaim(
1463
+ transactionDriver,
1464
+ collectionId,
1465
+ ctx?.cacheGenerationClaimId
1466
+ );
1164
1467
  await this.assertCurrentSchemaVersion(
1165
1468
  collectionId,
1166
1469
  transactionDriver,
@@ -1219,15 +1522,34 @@ class SQLiteCorePersistenceAdapter {
1219
1522
  }));
1220
1523
  });
1221
1524
  }
1222
- loadResumeSnapshot(collectionId, ctx) {
1525
+ loadResumeSnapshot(target, ctx) {
1223
1526
  return this.runRegular(
1224
- () => this.loadResumeSnapshotUnscheduled(collectionId, ctx)
1527
+ () => this.loadResumeSnapshotUnscheduled(target, ctx)
1225
1528
  );
1226
1529
  }
1227
- async loadResumeSnapshotUnscheduled(collectionId, ctx) {
1228
- 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
+ );
1229
1546
  const includeRows = ctx?.includeRows !== false;
1230
1547
  return this.runInTransaction(async (transactionDriver) => {
1548
+ await this.assertCacheGenerationReadClaim(
1549
+ transactionDriver,
1550
+ collectionId,
1551
+ ctx?.cacheGenerationClaimId
1552
+ );
1231
1553
  await this.assertCurrentSchemaVersion(
1232
1554
  collectionId,
1233
1555
  transactionDriver,
@@ -1280,32 +1602,49 @@ class SQLiteCorePersistenceAdapter {
1280
1602
  };
1281
1603
  });
1282
1604
  }
1283
- applyCommittedTx(collectionId, tx) {
1284
- return this.runRegular(
1285
- () => this.applyCommittedTxUnscheduled(collectionId, tx)
1286
- );
1605
+ applyCommittedTx(target, tx) {
1606
+ return this.runRegular(() => this.applyCommittedTxUnscheduled(target, tx));
1287
1607
  }
1288
- reconcileCommittedTx(collectionId, tx, anchor) {
1608
+ reconcileCommittedTx(target, tx, anchor) {
1289
1609
  return this.runRegular(
1290
- () => this.reconcileCommittedTxUnscheduled(collectionId, tx, anchor)
1610
+ () => this.reconcileCommittedTxUnscheduled(target, tx, anchor)
1291
1611
  );
1292
1612
  }
1293
- async applyCommittedTxUnscheduled(collectionId, tx) {
1294
- await this.applyCommittedTxInternal(collectionId, tx);
1613
+ async applyCommittedTxUnscheduled(target, tx) {
1614
+ await this.applyCommittedTxInternal(target, tx);
1295
1615
  }
1296
- reconcileCommittedTxUnscheduled(collectionId, tx, anchor) {
1616
+ reconcileCommittedTxUnscheduled(target, tx, anchor) {
1297
1617
  if (!isValidCommittedTxAnchor(anchor)) {
1298
1618
  throw new Error(
1299
1619
  `Cannot reconcile a committed transaction without a valid durable anchor`
1300
1620
  );
1301
1621
  }
1302
- return this.applyCommittedTxInternal(collectionId, tx, anchor);
1622
+ return this.applyCommittedTxInternal(target, tx, anchor);
1303
1623
  }
1304
- async applyCommittedTxInternal(collectionId, tx, anchor) {
1305
- const tableMapping = await this.ensureCollectionReady(collectionId);
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
+ );
1306
1640
  const collectionTableSql = quoteIdentifier(tableMapping.tableName);
1307
1641
  const tombstoneTableSql = quoteIdentifier(tableMapping.tombstoneTableName);
1308
1642
  return this.runInTransaction(async (transactionDriver) => {
1643
+ await this.assertCacheGenerationReadClaim(
1644
+ transactionDriver,
1645
+ collectionId,
1646
+ tx.cacheGenerationClaimId
1647
+ );
1309
1648
  const versionRows = await transactionDriver.query(
1310
1649
  `SELECT
1311
1650
  latest_row_version,
@@ -1632,14 +1971,30 @@ class SQLiteCorePersistenceAdapter {
1632
1971
  };
1633
1972
  });
1634
1973
  }
1635
- loadCollectionMetadata(collectionId) {
1974
+ loadCollectionMetadata(target, ctx) {
1636
1975
  return this.runRegular(
1637
- () => this.loadCollectionMetadataUnscheduled(collectionId)
1976
+ () => this.loadCollectionMetadataUnscheduled(target, ctx)
1638
1977
  );
1639
1978
  }
1640
- async loadCollectionMetadataUnscheduled(collectionId) {
1641
- 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);
1642
1992
  return this.runInTransaction(async (transactionDriver) => {
1993
+ await this.assertCacheGenerationReadClaim(
1994
+ transactionDriver,
1995
+ collectionId,
1996
+ ctx?.cacheGenerationClaimId
1997
+ );
1643
1998
  await this.assertCurrentSchemaVersion(
1644
1999
  collectionId,
1645
2000
  transactionDriver,
@@ -1657,15 +2012,32 @@ class SQLiteCorePersistenceAdapter {
1657
2012
  }));
1658
2013
  });
1659
2014
  }
1660
- scanRows(collectionId, options) {
1661
- return this.runRegular(
1662
- () => this.scanRowsUnscheduled(collectionId, options)
1663
- );
2015
+ scanRows(target, options, ctx) {
2016
+ return this.runRegular(() => this.scanRowsUnscheduled(target, options, ctx));
1664
2017
  }
1665
- async scanRowsUnscheduled(collectionId, options) {
1666
- 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
+ );
1667
2034
  const collectionTableSql = quoteIdentifier(tableMapping.tableName);
1668
2035
  return this.runInTransaction(async (transactionDriver) => {
2036
+ await this.assertCacheGenerationReadClaim(
2037
+ transactionDriver,
2038
+ collectionId,
2039
+ ctx?.cacheGenerationClaimId
2040
+ );
1669
2041
  await this.assertCurrentSchemaVersion(
1670
2042
  collectionId,
1671
2043
  transactionDriver,
@@ -1684,13 +2056,27 @@ class SQLiteCorePersistenceAdapter {
1684
2056
  }));
1685
2057
  });
1686
2058
  }
1687
- ensureIndex(collectionId, signature, spec) {
2059
+ ensureIndex(target, signature, spec, ctx) {
1688
2060
  return this.runRegular(
1689
- () => this.ensureIndexUnscheduled(collectionId, signature, spec)
2061
+ () => this.ensureIndexUnscheduled(target, signature, spec, ctx)
1690
2062
  );
1691
2063
  }
1692
- async ensureIndexUnscheduled(collectionId, signature, spec) {
1693
- 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
+ );
1694
2080
  const collectionTableSql = quoteIdentifier(tableMapping.tableName);
1695
2081
  const indexName = buildIndexName(collectionId, signature);
1696
2082
  const indexNameSql = quoteIdentifier(indexName);
@@ -1702,6 +2088,11 @@ class SQLiteCorePersistenceAdapter {
1702
2088
  const whereSql = spec.whereSql ? normalizeIndexSqlFragment(spec.whereSql) : void 0;
1703
2089
  const persistedWhereSql = whereSql ?? null;
1704
2090
  await this.runInTransaction(async (transactionDriver) => {
2091
+ await this.assertCacheGenerationReadClaim(
2092
+ transactionDriver,
2093
+ collectionId,
2094
+ ctx?.cacheGenerationClaimId
2095
+ );
1705
2096
  await this.assertCurrentSchemaVersion(
1706
2097
  collectionId,
1707
2098
  transactionDriver,
@@ -1758,14 +2149,30 @@ class SQLiteCorePersistenceAdapter {
1758
2149
  await transactionDriver.exec(createIndexSql);
1759
2150
  });
1760
2151
  }
1761
- markIndexRemoved(collectionId, signature) {
2152
+ markIndexRemoved(target, signature, ctx) {
1762
2153
  return this.runRegular(
1763
- () => this.markIndexRemovedUnscheduled(collectionId, signature)
2154
+ () => this.markIndexRemovedUnscheduled(target, signature, ctx)
1764
2155
  );
1765
2156
  }
1766
- async markIndexRemovedUnscheduled(collectionId, signature) {
1767
- 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);
1768
2170
  await this.runInTransaction(async (transactionDriver) => {
2171
+ await this.assertCacheGenerationReadClaim(
2172
+ transactionDriver,
2173
+ collectionId,
2174
+ ctx?.cacheGenerationClaimId
2175
+ );
1769
2176
  await this.assertCurrentSchemaVersion(
1770
2177
  collectionId,
1771
2178
  transactionDriver,
@@ -1794,12 +2201,28 @@ class SQLiteCorePersistenceAdapter {
1794
2201
  }
1795
2202
  });
1796
2203
  }
1797
- reserveLeadershipTerm(collectionId, observedTerm) {
1798
- return this.reserveLeadershipTermUnscheduled(collectionId, observedTerm);
2204
+ reserveLeadershipTerm(target, observedTerm, ctx) {
2205
+ return this.reserveLeadershipTermUnscheduled(target, observedTerm, ctx);
1799
2206
  }
1800
- async reserveLeadershipTermUnscheduled(collectionId, observedTerm) {
1801
- 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);
1802
2220
  return this.runInTransaction(async (transactionDriver) => {
2221
+ await this.assertCacheGenerationReadClaim(
2222
+ transactionDriver,
2223
+ collectionId,
2224
+ ctx?.cacheGenerationClaimId
2225
+ );
1803
2226
  await this.assertCurrentSchemaVersion(
1804
2227
  collectionId,
1805
2228
  transactionDriver,
@@ -1819,12 +2242,28 @@ class SQLiteCorePersistenceAdapter {
1819
2242
  return { ...position, latestRowVersion };
1820
2243
  });
1821
2244
  }
1822
- getStreamPosition(collectionId) {
1823
- return this.getStreamPositionUnscheduled(collectionId);
2245
+ getStreamPosition(target, ctx) {
2246
+ return this.getStreamPositionUnscheduled(target, ctx);
1824
2247
  }
1825
- async getStreamPositionUnscheduled(collectionId) {
1826
- await this.ensureCollectionReady(collectionId);
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);
1827
2261
  return this.runInTransaction(async (transactionDriver) => {
2262
+ await this.assertCacheGenerationReadClaim(
2263
+ transactionDriver,
2264
+ collectionId,
2265
+ ctx?.cacheGenerationClaimId
2266
+ );
1828
2267
  await this.assertCurrentSchemaVersion(
1829
2268
  collectionId,
1830
2269
  transactionDriver,
@@ -1892,16 +2331,35 @@ class SQLiteCorePersistenceAdapter {
1892
2331
  }
1893
2332
  };
1894
2333
  }
1895
- pullSince(collectionId, fromRowVersion) {
2334
+ pullSince(target, fromRowVersion, ctx) {
1896
2335
  return this.runRegular(
1897
- () => this.pullSinceUnscheduled(collectionId, fromRowVersion)
2336
+ () => this.pullSinceUnscheduled(target, fromRowVersion, ctx)
1898
2337
  );
1899
2338
  }
1900
- async pullSinceUnscheduled(collectionId, fromRowVersion) {
1901
- 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
+ );
1902
2355
  const collectionTableSql = quoteIdentifier(tableMapping.tableName);
1903
2356
  const tombstoneTableSql = quoteIdentifier(tableMapping.tombstoneTableName);
1904
2357
  return this.runInTransaction(async (transactionDriver) => {
2358
+ await this.assertCacheGenerationReadClaim(
2359
+ transactionDriver,
2360
+ collectionId,
2361
+ ctx?.cacheGenerationClaimId
2362
+ );
1905
2363
  await this.assertCurrentSchemaVersion(
1906
2364
  collectionId,
1907
2365
  transactionDriver,
@@ -2137,7 +2595,7 @@ class SQLiteCorePersistenceAdapter {
2137
2595
  [collectionId, excessRows]
2138
2596
  );
2139
2597
  }
2140
- async ensureCollectionReady(collectionId) {
2598
+ async ensureCollectionReady(collectionId, cacheGenerationClaimId) {
2141
2599
  await this.ensureInitialized();
2142
2600
  const cached = this.collectionTableCache.get(collectionId);
2143
2601
  if (cached) {
@@ -2147,7 +2605,10 @@ class SQLiteCorePersistenceAdapter {
2147
2605
  if (inFlight) {
2148
2606
  return inFlight;
2149
2607
  }
2150
- const loadPromise = this.ensureCollectionReadyInternal(collectionId);
2608
+ const loadPromise = this.ensureCollectionReadyInternal(
2609
+ collectionId,
2610
+ cacheGenerationClaimId
2611
+ );
2151
2612
  this.collectionTableLoads.set(collectionId, loadPromise);
2152
2613
  try {
2153
2614
  return await loadPromise;
@@ -2165,23 +2626,34 @@ class SQLiteCorePersistenceAdapter {
2165
2626
  );
2166
2627
  return rows[0];
2167
2628
  }
2168
- 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;
2169
2634
  let registration = await this.loadCollectionRegistration(collectionId);
2170
2635
  if (!registration) {
2171
2636
  const tableName2 = createPersistedTableName(collectionId, `c`);
2172
2637
  const tombstoneTableName2 = createPersistedTableName(collectionId, `t`);
2173
- await this.driver.run(
2174
- `INSERT INTO collection_registry (
2175
- collection_id,
2176
- table_name,
2177
- tombstone_table_name,
2178
- schema_version,
2179
- updated_at
2180
- )
2181
- VALUES (?, ?, ?, ?, CAST(strftime('%s', 'now') AS INTEGER))
2182
- ON CONFLICT DO NOTHING`,
2183
- [collectionId, tableName2, tombstoneTableName2, this.schemaVersion]
2184
- );
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
+ });
2185
2657
  registration = await this.loadCollectionRegistration(collectionId);
2186
2658
  if (!registration) {
2187
2659
  throw new InvalidPersistedCollectionConfigError(
@@ -2197,7 +2669,8 @@ class SQLiteCorePersistenceAdapter {
2197
2669
  registration.schema_version,
2198
2670
  this.schemaVersion,
2199
2671
  tableName,
2200
- tombstoneTableName
2672
+ tombstoneTableName,
2673
+ cacheGenerationClaimId
2201
2674
  );
2202
2675
  }
2203
2676
  const collectionTableSql = quoteIdentifier(tableName);
@@ -2244,6 +2717,35 @@ class SQLiteCorePersistenceAdapter {
2244
2717
  [collectionId]
2245
2718
  );
2246
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
+ }
2247
2749
  const mapping = {
2248
2750
  tableName,
2249
2751
  tombstoneTableName
@@ -2313,7 +2815,7 @@ class SQLiteCorePersistenceAdapter {
2313
2815
  END`
2314
2816
  );
2315
2817
  }
2316
- async handleSchemaMismatch(collectionId, previousSchemaVersion, nextSchemaVersion, tableName, tombstoneTableName) {
2818
+ async handleSchemaMismatch(collectionId, previousSchemaVersion, nextSchemaVersion, tableName, tombstoneTableName, cacheGenerationClaimId) {
2317
2819
  if (this.schemaMismatchPolicy === `sync-absent-error`) {
2318
2820
  throw new InvalidPersistedCollectionConfigError(
2319
2821
  `Schema version mismatch for collection "${collectionId}": found ${previousSchemaVersion}, expected ${nextSchemaVersion}. Set schemaMismatchPolicy to "sync-present-reset" or "reset" to allow automatic reset.`
@@ -2322,6 +2824,11 @@ class SQLiteCorePersistenceAdapter {
2322
2824
  const collectionTableSql = quoteIdentifier(tableName);
2323
2825
  const tombstoneTableSql = quoteIdentifier(tombstoneTableName);
2324
2826
  await this.runInTransaction(async (transactionDriver) => {
2827
+ await this.assertCacheGenerationReadClaim(
2828
+ transactionDriver,
2829
+ collectionId,
2830
+ cacheGenerationClaimId
2831
+ );
2325
2832
  const currentSchemaRows = await transactionDriver.query(
2326
2833
  `SELECT schema_version
2327
2834
  FROM collection_registry
@@ -2406,6 +2913,48 @@ class SQLiteCorePersistenceAdapter {
2406
2913
  if (this.initialized) {
2407
2914
  return;
2408
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
+ }
2409
2958
  await this.driver.exec(
2410
2959
  `CREATE TABLE IF NOT EXISTS collection_registry (
2411
2960
  collection_id TEXT PRIMARY KEY,
@@ -2546,6 +3095,7 @@ function createSQLiteCorePersistenceAdapter(options) {
2546
3095
  export {
2547
3096
  DEFAULT_APPLIED_TX_PRUNE_MAX_AGE_SECONDS,
2548
3097
  DEFAULT_APPLIED_TX_PRUNE_MAX_ROWS,
3098
+ DEFAULT_CACHE_GENERATION_CLAIM_TTL_MS,
2549
3099
  SQLiteCorePersistenceAdapter,
2550
3100
  createSQLiteCorePersistenceAdapter
2551
3101
  };