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