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