@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.
@@ -108,6 +108,12 @@ function observeSharedLogicalSchedulingSupport(driver, onSupport) {
108
108
  };
109
109
  }
110
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;
111
117
  const DEFAULT_PULL_SINCE_RELOAD_THRESHOLD = 128;
112
118
  const DEFAULT_APPLIED_TX_PRUNE_MAX_ROWS = 1e3;
113
119
  const DEFAULT_APPLIED_TX_PRUNE_MAX_AGE_SECONDS = 24 * 60 * 60;
@@ -1049,6 +1055,13 @@ class SQLiteCorePersistenceAdapter {
1049
1055
  );
1050
1056
  }
1051
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;
1052
1065
  const schemaVersion = options.schemaVersion ?? DEFAULT_SCHEMA_VERSION;
1053
1066
  if (!Number.isInteger(schemaVersion) || schemaVersion < 0) {
1054
1067
  throw new errors.InvalidPersistedCollectionConfigError(
@@ -1091,16 +1104,257 @@ class SQLiteCorePersistenceAdapter {
1091
1104
  loadSubset: (collectionId, loadOptions, context) => this.loadSubsetUnscheduled(collectionId, loadOptions, context),
1092
1105
  loadResumeSnapshot: (collectionId, context) => this.loadResumeSnapshotUnscheduled(collectionId, context),
1093
1106
  applyCommittedTx: (collectionId, tx) => this.applyCommittedTxUnscheduled(collectionId, tx),
1107
+ renewCacheGenerationClaim: (storageCollectionId, claimId) => this.renewCacheGenerationClaimUnscheduled(storageCollectionId, claimId),
1094
1108
  reconcileCommittedTx: (collectionId, tx, anchor) => this.reconcileCommittedTxUnscheduled(collectionId, tx, anchor),
1095
- loadCollectionMetadata: (collectionId) => this.loadCollectionMetadataUnscheduled(collectionId),
1096
- scanRows: (collectionId, scanOptions) => this.scanRowsUnscheduled(collectionId, scanOptions),
1097
- ensureIndex: (collectionId, signature, spec) => this.ensureIndexUnscheduled(collectionId, signature, spec),
1098
- markIndexRemoved: (collectionId, signature) => this.markIndexRemovedUnscheduled(collectionId, signature),
1099
- getStreamPosition: (collectionId) => this.getStreamPositionUnscheduled(collectionId),
1100
- pullSince: (collectionId, fromRowVersion) => this.pullSinceUnscheduled(collectionId, fromRowVersion),
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),
1101
1115
  runInHydrationScope: async (task) => task(this.hydrationAdapter)
1102
1116
  };
1103
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
+ }
1104
1358
  runInHydrationScope(task) {
1105
1359
  const scheduler = this.resolveScheduler();
1106
1360
  if (scheduler) {
@@ -1155,14 +1409,63 @@ class SQLiteCorePersistenceAdapter {
1155
1409
  );
1156
1410
  }
1157
1411
  }
1158
- 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) {
1159
1443
  return this.runRegular(
1160
- () => this.loadSubsetUnscheduled(collectionId, options, ctx)
1444
+ () => this.loadSubsetUnscheduled(target, options, ctx)
1161
1445
  );
1162
1446
  }
1163
- async loadSubsetUnscheduled(collectionId, options, ctx) {
1164
- 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
+ );
1165
1463
  return this.runInTransaction(async (transactionDriver) => {
1464
+ await this.assertCacheGenerationReadClaim(
1465
+ transactionDriver,
1466
+ collectionId,
1467
+ ctx?.cacheGenerationClaimId
1468
+ );
1166
1469
  await this.assertCurrentSchemaVersion(
1167
1470
  collectionId,
1168
1471
  transactionDriver,
@@ -1221,15 +1524,34 @@ class SQLiteCorePersistenceAdapter {
1221
1524
  }));
1222
1525
  });
1223
1526
  }
1224
- loadResumeSnapshot(collectionId, ctx) {
1527
+ loadResumeSnapshot(target, ctx) {
1225
1528
  return this.runRegular(
1226
- () => this.loadResumeSnapshotUnscheduled(collectionId, ctx)
1529
+ () => this.loadResumeSnapshotUnscheduled(target, ctx)
1227
1530
  );
1228
1531
  }
1229
- async loadResumeSnapshotUnscheduled(collectionId, ctx) {
1230
- 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
+ );
1231
1548
  const includeRows = ctx?.includeRows !== false;
1232
1549
  return this.runInTransaction(async (transactionDriver) => {
1550
+ await this.assertCacheGenerationReadClaim(
1551
+ transactionDriver,
1552
+ collectionId,
1553
+ ctx?.cacheGenerationClaimId
1554
+ );
1233
1555
  await this.assertCurrentSchemaVersion(
1234
1556
  collectionId,
1235
1557
  transactionDriver,
@@ -1282,32 +1604,49 @@ class SQLiteCorePersistenceAdapter {
1282
1604
  };
1283
1605
  });
1284
1606
  }
1285
- applyCommittedTx(collectionId, tx) {
1286
- return this.runRegular(
1287
- () => this.applyCommittedTxUnscheduled(collectionId, tx)
1288
- );
1607
+ applyCommittedTx(target, tx) {
1608
+ return this.runRegular(() => this.applyCommittedTxUnscheduled(target, tx));
1289
1609
  }
1290
- reconcileCommittedTx(collectionId, tx, anchor) {
1610
+ reconcileCommittedTx(target, tx, anchor) {
1291
1611
  return this.runRegular(
1292
- () => this.reconcileCommittedTxUnscheduled(collectionId, tx, anchor)
1612
+ () => this.reconcileCommittedTxUnscheduled(target, tx, anchor)
1293
1613
  );
1294
1614
  }
1295
- async applyCommittedTxUnscheduled(collectionId, tx) {
1296
- await this.applyCommittedTxInternal(collectionId, tx);
1615
+ async applyCommittedTxUnscheduled(target, tx) {
1616
+ await this.applyCommittedTxInternal(target, tx);
1297
1617
  }
1298
- reconcileCommittedTxUnscheduled(collectionId, tx, anchor) {
1618
+ reconcileCommittedTxUnscheduled(target, tx, anchor) {
1299
1619
  if (!committedTxAnchor.isValidCommittedTxAnchor(anchor)) {
1300
1620
  throw new Error(
1301
1621
  `Cannot reconcile a committed transaction without a valid durable anchor`
1302
1622
  );
1303
1623
  }
1304
- return this.applyCommittedTxInternal(collectionId, tx, anchor);
1624
+ return this.applyCommittedTxInternal(target, tx, anchor);
1305
1625
  }
1306
- async applyCommittedTxInternal(collectionId, tx, anchor) {
1307
- const tableMapping = await this.ensureCollectionReady(collectionId);
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
+ );
1308
1642
  const collectionTableSql = quoteIdentifier(tableMapping.tableName);
1309
1643
  const tombstoneTableSql = quoteIdentifier(tableMapping.tombstoneTableName);
1310
1644
  return this.runInTransaction(async (transactionDriver) => {
1645
+ await this.assertCacheGenerationReadClaim(
1646
+ transactionDriver,
1647
+ collectionId,
1648
+ tx.cacheGenerationClaimId
1649
+ );
1311
1650
  const versionRows = await transactionDriver.query(
1312
1651
  `SELECT
1313
1652
  latest_row_version,
@@ -1634,14 +1973,30 @@ class SQLiteCorePersistenceAdapter {
1634
1973
  };
1635
1974
  });
1636
1975
  }
1637
- loadCollectionMetadata(collectionId) {
1976
+ loadCollectionMetadata(target, ctx) {
1638
1977
  return this.runRegular(
1639
- () => this.loadCollectionMetadataUnscheduled(collectionId)
1978
+ () => this.loadCollectionMetadataUnscheduled(target, ctx)
1640
1979
  );
1641
1980
  }
1642
- async loadCollectionMetadataUnscheduled(collectionId) {
1643
- 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);
1644
1994
  return this.runInTransaction(async (transactionDriver) => {
1995
+ await this.assertCacheGenerationReadClaim(
1996
+ transactionDriver,
1997
+ collectionId,
1998
+ ctx?.cacheGenerationClaimId
1999
+ );
1645
2000
  await this.assertCurrentSchemaVersion(
1646
2001
  collectionId,
1647
2002
  transactionDriver,
@@ -1659,15 +2014,32 @@ class SQLiteCorePersistenceAdapter {
1659
2014
  }));
1660
2015
  });
1661
2016
  }
1662
- scanRows(collectionId, options) {
1663
- return this.runRegular(
1664
- () => this.scanRowsUnscheduled(collectionId, options)
1665
- );
2017
+ scanRows(target, options, ctx) {
2018
+ return this.runRegular(() => this.scanRowsUnscheduled(target, options, ctx));
1666
2019
  }
1667
- async scanRowsUnscheduled(collectionId, options) {
1668
- 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
+ );
1669
2036
  const collectionTableSql = quoteIdentifier(tableMapping.tableName);
1670
2037
  return this.runInTransaction(async (transactionDriver) => {
2038
+ await this.assertCacheGenerationReadClaim(
2039
+ transactionDriver,
2040
+ collectionId,
2041
+ ctx?.cacheGenerationClaimId
2042
+ );
1671
2043
  await this.assertCurrentSchemaVersion(
1672
2044
  collectionId,
1673
2045
  transactionDriver,
@@ -1686,13 +2058,27 @@ class SQLiteCorePersistenceAdapter {
1686
2058
  }));
1687
2059
  });
1688
2060
  }
1689
- ensureIndex(collectionId, signature, spec) {
2061
+ ensureIndex(target, signature, spec, ctx) {
1690
2062
  return this.runRegular(
1691
- () => this.ensureIndexUnscheduled(collectionId, signature, spec)
2063
+ () => this.ensureIndexUnscheduled(target, signature, spec, ctx)
1692
2064
  );
1693
2065
  }
1694
- async ensureIndexUnscheduled(collectionId, signature, spec) {
1695
- 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
+ );
1696
2082
  const collectionTableSql = quoteIdentifier(tableMapping.tableName);
1697
2083
  const indexName = buildIndexName(collectionId, signature);
1698
2084
  const indexNameSql = quoteIdentifier(indexName);
@@ -1704,6 +2090,11 @@ class SQLiteCorePersistenceAdapter {
1704
2090
  const whereSql = spec.whereSql ? normalizeIndexSqlFragment(spec.whereSql) : void 0;
1705
2091
  const persistedWhereSql = whereSql ?? null;
1706
2092
  await this.runInTransaction(async (transactionDriver) => {
2093
+ await this.assertCacheGenerationReadClaim(
2094
+ transactionDriver,
2095
+ collectionId,
2096
+ ctx?.cacheGenerationClaimId
2097
+ );
1707
2098
  await this.assertCurrentSchemaVersion(
1708
2099
  collectionId,
1709
2100
  transactionDriver,
@@ -1760,14 +2151,30 @@ class SQLiteCorePersistenceAdapter {
1760
2151
  await transactionDriver.exec(createIndexSql);
1761
2152
  });
1762
2153
  }
1763
- markIndexRemoved(collectionId, signature) {
2154
+ markIndexRemoved(target, signature, ctx) {
1764
2155
  return this.runRegular(
1765
- () => this.markIndexRemovedUnscheduled(collectionId, signature)
2156
+ () => this.markIndexRemovedUnscheduled(target, signature, ctx)
1766
2157
  );
1767
2158
  }
1768
- async markIndexRemovedUnscheduled(collectionId, signature) {
1769
- 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);
1770
2172
  await this.runInTransaction(async (transactionDriver) => {
2173
+ await this.assertCacheGenerationReadClaim(
2174
+ transactionDriver,
2175
+ collectionId,
2176
+ ctx?.cacheGenerationClaimId
2177
+ );
1771
2178
  await this.assertCurrentSchemaVersion(
1772
2179
  collectionId,
1773
2180
  transactionDriver,
@@ -1796,12 +2203,28 @@ class SQLiteCorePersistenceAdapter {
1796
2203
  }
1797
2204
  });
1798
2205
  }
1799
- reserveLeadershipTerm(collectionId, observedTerm) {
1800
- return this.reserveLeadershipTermUnscheduled(collectionId, observedTerm);
2206
+ reserveLeadershipTerm(target, observedTerm, ctx) {
2207
+ return this.reserveLeadershipTermUnscheduled(target, observedTerm, ctx);
1801
2208
  }
1802
- async reserveLeadershipTermUnscheduled(collectionId, observedTerm) {
1803
- 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);
1804
2222
  return this.runInTransaction(async (transactionDriver) => {
2223
+ await this.assertCacheGenerationReadClaim(
2224
+ transactionDriver,
2225
+ collectionId,
2226
+ ctx?.cacheGenerationClaimId
2227
+ );
1805
2228
  await this.assertCurrentSchemaVersion(
1806
2229
  collectionId,
1807
2230
  transactionDriver,
@@ -1821,12 +2244,28 @@ class SQLiteCorePersistenceAdapter {
1821
2244
  return { ...position, latestRowVersion };
1822
2245
  });
1823
2246
  }
1824
- getStreamPosition(collectionId) {
1825
- return this.getStreamPositionUnscheduled(collectionId);
2247
+ getStreamPosition(target, ctx) {
2248
+ return this.getStreamPositionUnscheduled(target, ctx);
1826
2249
  }
1827
- async getStreamPositionUnscheduled(collectionId) {
1828
- await this.ensureCollectionReady(collectionId);
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);
1829
2263
  return this.runInTransaction(async (transactionDriver) => {
2264
+ await this.assertCacheGenerationReadClaim(
2265
+ transactionDriver,
2266
+ collectionId,
2267
+ ctx?.cacheGenerationClaimId
2268
+ );
1830
2269
  await this.assertCurrentSchemaVersion(
1831
2270
  collectionId,
1832
2271
  transactionDriver,
@@ -1894,16 +2333,35 @@ class SQLiteCorePersistenceAdapter {
1894
2333
  }
1895
2334
  };
1896
2335
  }
1897
- pullSince(collectionId, fromRowVersion) {
2336
+ pullSince(target, fromRowVersion, ctx) {
1898
2337
  return this.runRegular(
1899
- () => this.pullSinceUnscheduled(collectionId, fromRowVersion)
2338
+ () => this.pullSinceUnscheduled(target, fromRowVersion, ctx)
1900
2339
  );
1901
2340
  }
1902
- async pullSinceUnscheduled(collectionId, fromRowVersion) {
1903
- 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
+ );
1904
2357
  const collectionTableSql = quoteIdentifier(tableMapping.tableName);
1905
2358
  const tombstoneTableSql = quoteIdentifier(tableMapping.tombstoneTableName);
1906
2359
  return this.runInTransaction(async (transactionDriver) => {
2360
+ await this.assertCacheGenerationReadClaim(
2361
+ transactionDriver,
2362
+ collectionId,
2363
+ ctx?.cacheGenerationClaimId
2364
+ );
1907
2365
  await this.assertCurrentSchemaVersion(
1908
2366
  collectionId,
1909
2367
  transactionDriver,
@@ -2139,7 +2597,7 @@ class SQLiteCorePersistenceAdapter {
2139
2597
  [collectionId, excessRows]
2140
2598
  );
2141
2599
  }
2142
- async ensureCollectionReady(collectionId) {
2600
+ async ensureCollectionReady(collectionId, cacheGenerationClaimId) {
2143
2601
  await this.ensureInitialized();
2144
2602
  const cached = this.collectionTableCache.get(collectionId);
2145
2603
  if (cached) {
@@ -2149,7 +2607,10 @@ class SQLiteCorePersistenceAdapter {
2149
2607
  if (inFlight) {
2150
2608
  return inFlight;
2151
2609
  }
2152
- const loadPromise = this.ensureCollectionReadyInternal(collectionId);
2610
+ const loadPromise = this.ensureCollectionReadyInternal(
2611
+ collectionId,
2612
+ cacheGenerationClaimId
2613
+ );
2153
2614
  this.collectionTableLoads.set(collectionId, loadPromise);
2154
2615
  try {
2155
2616
  return await loadPromise;
@@ -2167,23 +2628,34 @@ class SQLiteCorePersistenceAdapter {
2167
2628
  );
2168
2629
  return rows[0];
2169
2630
  }
2170
- 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;
2171
2636
  let registration = await this.loadCollectionRegistration(collectionId);
2172
2637
  if (!registration) {
2173
2638
  const tableName2 = persisted.createPersistedTableName(collectionId, `c`);
2174
2639
  const tombstoneTableName2 = persisted.createPersistedTableName(collectionId, `t`);
2175
- await this.driver.run(
2176
- `INSERT INTO collection_registry (
2177
- collection_id,
2178
- table_name,
2179
- tombstone_table_name,
2180
- schema_version,
2181
- updated_at
2182
- )
2183
- VALUES (?, ?, ?, ?, CAST(strftime('%s', 'now') AS INTEGER))
2184
- ON CONFLICT DO NOTHING`,
2185
- [collectionId, tableName2, tombstoneTableName2, this.schemaVersion]
2186
- );
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
+ });
2187
2659
  registration = await this.loadCollectionRegistration(collectionId);
2188
2660
  if (!registration) {
2189
2661
  throw new errors.InvalidPersistedCollectionConfigError(
@@ -2199,7 +2671,8 @@ class SQLiteCorePersistenceAdapter {
2199
2671
  registration.schema_version,
2200
2672
  this.schemaVersion,
2201
2673
  tableName,
2202
- tombstoneTableName
2674
+ tombstoneTableName,
2675
+ cacheGenerationClaimId
2203
2676
  );
2204
2677
  }
2205
2678
  const collectionTableSql = quoteIdentifier(tableName);
@@ -2246,6 +2719,35 @@ class SQLiteCorePersistenceAdapter {
2246
2719
  [collectionId]
2247
2720
  );
2248
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
+ }
2249
2751
  const mapping = {
2250
2752
  tableName,
2251
2753
  tombstoneTableName
@@ -2315,7 +2817,7 @@ class SQLiteCorePersistenceAdapter {
2315
2817
  END`
2316
2818
  );
2317
2819
  }
2318
- async handleSchemaMismatch(collectionId, previousSchemaVersion, nextSchemaVersion, tableName, tombstoneTableName) {
2820
+ async handleSchemaMismatch(collectionId, previousSchemaVersion, nextSchemaVersion, tableName, tombstoneTableName, cacheGenerationClaimId) {
2319
2821
  if (this.schemaMismatchPolicy === `sync-absent-error`) {
2320
2822
  throw new errors.InvalidPersistedCollectionConfigError(
2321
2823
  `Schema version mismatch for collection "${collectionId}": found ${previousSchemaVersion}, expected ${nextSchemaVersion}. Set schemaMismatchPolicy to "sync-present-reset" or "reset" to allow automatic reset.`
@@ -2324,6 +2826,11 @@ class SQLiteCorePersistenceAdapter {
2324
2826
  const collectionTableSql = quoteIdentifier(tableName);
2325
2827
  const tombstoneTableSql = quoteIdentifier(tombstoneTableName);
2326
2828
  await this.runInTransaction(async (transactionDriver) => {
2829
+ await this.assertCacheGenerationReadClaim(
2830
+ transactionDriver,
2831
+ collectionId,
2832
+ cacheGenerationClaimId
2833
+ );
2327
2834
  const currentSchemaRows = await transactionDriver.query(
2328
2835
  `SELECT schema_version
2329
2836
  FROM collection_registry
@@ -2408,6 +2915,48 @@ class SQLiteCorePersistenceAdapter {
2408
2915
  if (this.initialized) {
2409
2916
  return;
2410
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
+ }
2411
2960
  await this.driver.exec(
2412
2961
  `CREATE TABLE IF NOT EXISTS collection_registry (
2413
2962
  collection_id TEXT PRIMARY KEY,
@@ -2547,6 +3096,7 @@ function createSQLiteCorePersistenceAdapter(options) {
2547
3096
  }
2548
3097
  exports.DEFAULT_APPLIED_TX_PRUNE_MAX_AGE_SECONDS = DEFAULT_APPLIED_TX_PRUNE_MAX_AGE_SECONDS;
2549
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;
2550
3100
  exports.SQLiteCorePersistenceAdapter = SQLiteCorePersistenceAdapter;
2551
3101
  exports.createSQLiteCorePersistenceAdapter = createSQLiteCorePersistenceAdapter;
2552
3102
  //# sourceMappingURL=sqlite-core-adapter.cjs.map