@tanstack/electric-db-collection 0.4.8 → 0.4.10
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/dist/cjs/electric.cjs +648 -435
- package/dist/cjs/electric.cjs.map +1 -1
- package/dist/cjs/sql-compiler.cjs +5 -4
- package/dist/cjs/sql-compiler.cjs.map +1 -1
- package/dist/esm/electric.js +650 -437
- package/dist/esm/electric.js.map +1 -1
- package/dist/esm/sql-compiler.js +5 -4
- package/dist/esm/sql-compiler.js.map +1 -1
- package/package.json +4 -3
- package/src/electric.ts +942 -647
- package/src/sql-compiler.ts +18 -4
package/dist/cjs/electric.cjs
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
3
|
const client = require("@electric-sql/client");
|
|
4
|
-
const store = require("@tanstack/store");
|
|
5
4
|
const DebugModule = require("debug");
|
|
6
5
|
const db = require("@tanstack/db");
|
|
7
6
|
const errors = require("./errors.cjs");
|
|
@@ -10,6 +9,20 @@ const tagIndex = require("./tag-index.cjs");
|
|
|
10
9
|
const debug = DebugModule.debug(`ts/db:electric`);
|
|
11
10
|
const FORCE_DISCONNECT_AND_REFRESH_TIMEOUT_MS = 250;
|
|
12
11
|
const ELECTRIC_TEST_HOOKS = /* @__PURE__ */ Symbol(`electricTestHooks`);
|
|
12
|
+
function exportElectricSyncMeta(evidence) {
|
|
13
|
+
const resume = evidence.hydratedResumeState;
|
|
14
|
+
return {
|
|
15
|
+
version: 1,
|
|
16
|
+
...resume ? { resume } : {},
|
|
17
|
+
seenTxids: Array.from(evidence.seenTxids).sort((a, b) => a - b)
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function importElectricSyncMeta(evidence, meta) {
|
|
21
|
+
const parsed = parseElectricSyncMeta(meta);
|
|
22
|
+
if (!parsed) return;
|
|
23
|
+
evidence.hydratedResumeState = parsed.resume;
|
|
24
|
+
evidence.seenTxids = new Set(parsed.seenTxids);
|
|
25
|
+
}
|
|
13
26
|
function parseElectricResumeState(value) {
|
|
14
27
|
if (!value || typeof value !== `object`) {
|
|
15
28
|
return void 0;
|
|
@@ -21,7 +34,10 @@ function parseElectricResumeState(value) {
|
|
|
21
34
|
offset: record.offset,
|
|
22
35
|
handle: record.handle,
|
|
23
36
|
shapeId: record.shapeId,
|
|
24
|
-
updatedAt: record.updatedAt
|
|
37
|
+
updatedAt: record.updatedAt,
|
|
38
|
+
...typeof record.requiresTagState === `boolean` && {
|
|
39
|
+
requiresTagState: record.requiresTagState
|
|
40
|
+
}
|
|
25
41
|
};
|
|
26
42
|
}
|
|
27
43
|
if (record.kind === `reset` && typeof record.updatedAt === `number` && Number.isFinite(record.updatedAt)) {
|
|
@@ -76,7 +92,10 @@ function mergeElectricSyncMeta(current, incoming) {
|
|
|
76
92
|
function getNewestElectricResumeState(current, incoming) {
|
|
77
93
|
if (!current) return incoming;
|
|
78
94
|
if (!incoming) return current;
|
|
79
|
-
|
|
95
|
+
if (incoming.updatedAt > current.updatedAt) return incoming;
|
|
96
|
+
if (incoming.updatedAt < current.updatedAt) return current;
|
|
97
|
+
const isSameState = current.kind === incoming.kind && (current.kind === `reset` || incoming.kind === `resume` && current.offset === incoming.offset && current.handle === incoming.handle && current.shapeId === incoming.shapeId);
|
|
98
|
+
return isSameState ? current : { kind: `reset`, updatedAt: current.updatedAt };
|
|
80
99
|
}
|
|
81
100
|
function isUpToDateMessage(message) {
|
|
82
101
|
return client.isControlMessage(message) && message.headers.control === `up-to-date`;
|
|
@@ -279,203 +298,239 @@ function createLoadSubsetDedupe({
|
|
|
279
298
|
};
|
|
280
299
|
return new db.DeduplicatedLoadSubset({ loadSubset });
|
|
281
300
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
301
|
+
class ElectricLifecycle {
|
|
302
|
+
constructor(collectionId) {
|
|
303
|
+
this.collectionId = collectionId;
|
|
304
|
+
this.evidence = {
|
|
305
|
+
seenTxids: /* @__PURE__ */ new Set(),
|
|
306
|
+
seenSnapshots: []
|
|
307
|
+
};
|
|
308
|
+
this.pendingMatches = /* @__PURE__ */ new Map();
|
|
309
|
+
this.pendingTxidWaits = /* @__PURE__ */ new Map();
|
|
310
|
+
this.matchBuffer = {
|
|
311
|
+
committedMessages: [],
|
|
312
|
+
pendingMessages: []
|
|
313
|
+
};
|
|
314
|
+
this.nextWaiterId = 0;
|
|
315
|
+
this.epoch = 0;
|
|
316
|
+
this.active = false;
|
|
317
|
+
this.utils = {
|
|
318
|
+
awaitTxId: (txId, timeout) => this.awaitTxId(txId, timeout),
|
|
319
|
+
awaitMatch: (matchFn, timeout) => this.awaitMatch(matchFn, timeout)
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
start() {
|
|
323
|
+
if (this.active) this.retire();
|
|
324
|
+
this.active = true;
|
|
325
|
+
this.epoch++;
|
|
326
|
+
this.matchBuffer = { committedMessages: [], pendingMessages: [] };
|
|
327
|
+
return this.epoch;
|
|
328
|
+
}
|
|
329
|
+
isActive(epoch) {
|
|
330
|
+
return this.active && this.epoch === epoch;
|
|
331
|
+
}
|
|
332
|
+
retire(epoch) {
|
|
333
|
+
if (epoch !== void 0 && !this.isActive(epoch)) return;
|
|
334
|
+
this.active = false;
|
|
335
|
+
this.epoch++;
|
|
336
|
+
for (const match of this.pendingMatches.values()) {
|
|
337
|
+
clearTimeout(match.timeoutId);
|
|
338
|
+
match.reject(new errors.StreamAbortedError(this.collectionId));
|
|
300
339
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
340
|
+
this.pendingMatches.clear();
|
|
341
|
+
for (const waiter of this.pendingTxidWaits.values()) {
|
|
342
|
+
clearTimeout(waiter.timeoutId);
|
|
343
|
+
waiter.reject(new errors.StreamAbortedError(this.collectionId));
|
|
344
|
+
}
|
|
345
|
+
this.pendingTxidWaits.clear();
|
|
346
|
+
this.matchBuffer = { committedMessages: [], pendingMessages: [] };
|
|
347
|
+
this.evidence.hydratedResumeState = void 0;
|
|
348
|
+
}
|
|
349
|
+
exportMeta() {
|
|
350
|
+
return exportElectricSyncMeta(this.evidence);
|
|
351
|
+
}
|
|
352
|
+
importMeta(meta) {
|
|
353
|
+
importElectricSyncMeta(this.evidence, meta);
|
|
354
|
+
this.resolveTxidWaiters();
|
|
355
|
+
}
|
|
356
|
+
get resumeState() {
|
|
357
|
+
return this.evidence.hydratedResumeState;
|
|
358
|
+
}
|
|
359
|
+
set resumeState(value) {
|
|
360
|
+
this.evidence.hydratedResumeState = value;
|
|
361
|
+
}
|
|
362
|
+
publishEvidence(txids, snapshots) {
|
|
363
|
+
txids.forEach((txid) => this.evidence.seenTxids.add(txid));
|
|
364
|
+
this.evidence.seenSnapshots.push(...snapshots);
|
|
365
|
+
this.resolveTxidWaiters();
|
|
366
|
+
}
|
|
367
|
+
hasTxid(txId) {
|
|
368
|
+
return this.evidence.seenTxids.has(txId) || this.evidence.seenSnapshots.some(
|
|
369
|
+
(snapshot) => client.isVisibleInSnapshot(txId, snapshot)
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
resolveTxidWaiters() {
|
|
373
|
+
for (const [waitId, waiter] of this.pendingTxidWaits) {
|
|
374
|
+
if (!this.hasTxid(waiter.txId)) continue;
|
|
375
|
+
clearTimeout(waiter.timeoutId);
|
|
376
|
+
this.pendingTxidWaits.delete(waitId);
|
|
377
|
+
waiter.resolve(true);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
async awaitTxId(txId, timeout = 5e3) {
|
|
331
381
|
debug(
|
|
332
|
-
`${
|
|
382
|
+
`${this.collectionId ? `[${this.collectionId}] ` : ``}awaitTxId called with txid %d`,
|
|
333
383
|
txId
|
|
334
384
|
);
|
|
335
385
|
if (typeof txId !== `number`) {
|
|
336
|
-
throw new errors.ExpectedNumberInAwaitTxIdError(typeof txId,
|
|
386
|
+
throw new errors.ExpectedNumberInAwaitTxIdError(typeof txId, this.collectionId);
|
|
337
387
|
}
|
|
338
|
-
|
|
339
|
-
if (hasTxid) return true;
|
|
340
|
-
const hasSnapshot = seenSnapshots.state.some(
|
|
341
|
-
(snapshot) => client.isVisibleInSnapshot(txId, snapshot)
|
|
342
|
-
);
|
|
343
|
-
if (hasSnapshot) return true;
|
|
388
|
+
if (this.hasTxid(txId)) return true;
|
|
344
389
|
return new Promise((resolve, reject) => {
|
|
345
|
-
const
|
|
346
|
-
clearTimeout(timeoutId);
|
|
347
|
-
subSeenTxids.unsubscribe();
|
|
348
|
-
subSeenSnapshots.unsubscribe();
|
|
349
|
-
};
|
|
390
|
+
const waitId = this.nextWaiterId++;
|
|
350
391
|
const timeoutId = setTimeout(() => {
|
|
351
|
-
|
|
352
|
-
reject(new errors.TimeoutWaitingForTxIdError(txId,
|
|
392
|
+
this.pendingTxidWaits.delete(waitId);
|
|
393
|
+
reject(new errors.TimeoutWaitingForTxIdError(txId, this.collectionId));
|
|
353
394
|
}, timeout);
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
);
|
|
360
|
-
cleanup();
|
|
361
|
-
resolve(true);
|
|
362
|
-
}
|
|
363
|
-
});
|
|
364
|
-
const subSeenSnapshots = seenSnapshots.subscribe(() => {
|
|
365
|
-
const visibleSnapshot = seenSnapshots.state.find(
|
|
366
|
-
(snapshot) => client.isVisibleInSnapshot(txId, snapshot)
|
|
367
|
-
);
|
|
368
|
-
if (visibleSnapshot) {
|
|
369
|
-
debug(
|
|
370
|
-
`${config.id ? `[${config.id}] ` : ``}awaitTxId found match for txid %o in snapshot %o`,
|
|
371
|
-
txId,
|
|
372
|
-
visibleSnapshot
|
|
373
|
-
);
|
|
374
|
-
cleanup();
|
|
375
|
-
resolve(true);
|
|
376
|
-
}
|
|
395
|
+
this.pendingTxidWaits.set(waitId, {
|
|
396
|
+
txId,
|
|
397
|
+
resolve,
|
|
398
|
+
reject,
|
|
399
|
+
timeoutId
|
|
377
400
|
});
|
|
378
401
|
});
|
|
379
|
-
}
|
|
380
|
-
|
|
402
|
+
}
|
|
403
|
+
async awaitMatch(matchFn, timeout = 3e3) {
|
|
381
404
|
debug(
|
|
382
|
-
`${
|
|
405
|
+
`${this.collectionId ? `[${this.collectionId}] ` : ``}awaitMatch called with custom function`
|
|
383
406
|
);
|
|
407
|
+
for (const message of this.matchBuffer.committedMessages) {
|
|
408
|
+
if (!matchFn(message)) continue;
|
|
409
|
+
return true;
|
|
410
|
+
}
|
|
411
|
+
for (const message of this.matchBuffer.pendingMessages) {
|
|
412
|
+
if (matchFn(message)) return this.registerMatch(matchFn, timeout, true);
|
|
413
|
+
}
|
|
414
|
+
return this.registerMatch(matchFn, timeout, false);
|
|
415
|
+
}
|
|
416
|
+
registerMatch(matchFn, timeout, matched) {
|
|
384
417
|
return new Promise((resolve, reject) => {
|
|
385
|
-
const matchId =
|
|
386
|
-
const
|
|
387
|
-
pendingMatches.
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
};
|
|
397
|
-
const timeoutId = setTimeout(onTimeout, timeout);
|
|
398
|
-
const checkMatch = (message) => {
|
|
399
|
-
if (matchFn(message)) {
|
|
400
|
-
debug(
|
|
401
|
-
`${config.id ? `[${config.id}] ` : ``}awaitMatch found matching message, waiting for up-to-date`
|
|
402
|
-
);
|
|
403
|
-
pendingMatches.setState((current) => {
|
|
404
|
-
const newMatches = new Map(current);
|
|
405
|
-
const existing = newMatches.get(matchId);
|
|
406
|
-
if (existing) {
|
|
407
|
-
newMatches.set(matchId, { ...existing, matched: true });
|
|
408
|
-
}
|
|
409
|
-
return newMatches;
|
|
410
|
-
});
|
|
411
|
-
return true;
|
|
412
|
-
}
|
|
413
|
-
return false;
|
|
414
|
-
};
|
|
415
|
-
for (const message of currentBatchMessages.state) {
|
|
416
|
-
if (matchFn(message)) {
|
|
417
|
-
if (batchCommitted.state) {
|
|
418
|
-
debug(
|
|
419
|
-
`${config.id ? `[${config.id}] ` : ``}awaitMatch found immediate match in committed batch, resolving immediately`
|
|
420
|
-
);
|
|
421
|
-
clearTimeout(timeoutId);
|
|
422
|
-
resolve(true);
|
|
423
|
-
return;
|
|
424
|
-
}
|
|
425
|
-
debug(
|
|
426
|
-
`${config.id ? `[${config.id}] ` : ``}awaitMatch found immediate match in current batch, waiting for up-to-date`
|
|
427
|
-
);
|
|
428
|
-
pendingMatches.setState((current) => {
|
|
429
|
-
const newMatches = new Map(current);
|
|
430
|
-
newMatches.set(matchId, {
|
|
431
|
-
matchFn: checkMatch,
|
|
432
|
-
resolve,
|
|
433
|
-
reject,
|
|
434
|
-
timeoutId,
|
|
435
|
-
matched: true
|
|
436
|
-
// Already matched, will resolve on up-to-date
|
|
437
|
-
});
|
|
438
|
-
return newMatches;
|
|
439
|
-
});
|
|
440
|
-
return;
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
pendingMatches.setState((current) => {
|
|
444
|
-
const newMatches = new Map(current);
|
|
445
|
-
newMatches.set(matchId, {
|
|
446
|
-
matchFn: checkMatch,
|
|
447
|
-
resolve,
|
|
448
|
-
reject,
|
|
449
|
-
timeoutId,
|
|
450
|
-
matched: false
|
|
451
|
-
});
|
|
452
|
-
return newMatches;
|
|
418
|
+
const matchId = this.nextWaiterId++;
|
|
419
|
+
const timeoutId = setTimeout(() => {
|
|
420
|
+
this.pendingMatches.delete(matchId);
|
|
421
|
+
reject(new errors.TimeoutWaitingForMatchError(this.collectionId));
|
|
422
|
+
}, timeout);
|
|
423
|
+
this.pendingMatches.set(matchId, {
|
|
424
|
+
matchFn,
|
|
425
|
+
resolve,
|
|
426
|
+
reject,
|
|
427
|
+
timeoutId,
|
|
428
|
+
matched
|
|
453
429
|
});
|
|
454
430
|
});
|
|
455
|
-
}
|
|
456
|
-
|
|
431
|
+
}
|
|
432
|
+
beginMatchGeneration(messages) {
|
|
433
|
+
if (!messages.some(isMustRefetchMessage)) return;
|
|
434
|
+
this.matchBuffer = { committedMessages: [], pendingMessages: [] };
|
|
435
|
+
for (const match of this.pendingMatches.values()) match.matched = false;
|
|
436
|
+
}
|
|
437
|
+
observeMatchMessage(message) {
|
|
438
|
+
if (client.isChangeMessage(message) || tagIndex.isMoveOutMessage(message) || tagIndex.isMoveInMessage(message)) {
|
|
439
|
+
this.matchBuffer.pendingMessages.push(message);
|
|
440
|
+
let overflow = this.matchBuffer.committedMessages.length + this.matchBuffer.pendingMessages.length - 1e3;
|
|
441
|
+
if (overflow > 0) {
|
|
442
|
+
const committedOverflow = Math.min(
|
|
443
|
+
overflow,
|
|
444
|
+
this.matchBuffer.committedMessages.length
|
|
445
|
+
);
|
|
446
|
+
this.matchBuffer.committedMessages.splice(0, committedOverflow);
|
|
447
|
+
overflow -= committedOverflow;
|
|
448
|
+
if (overflow > 0) {
|
|
449
|
+
this.matchBuffer.pendingMessages.splice(0, overflow);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
const epoch = this.epoch;
|
|
454
|
+
for (const [matchId, match] of this.pendingMatches) {
|
|
455
|
+
if (match.matched) continue;
|
|
456
|
+
try {
|
|
457
|
+
match.matched = match.matchFn(message);
|
|
458
|
+
} catch (error) {
|
|
459
|
+
clearTimeout(match.timeoutId);
|
|
460
|
+
this.pendingMatches.delete(matchId);
|
|
461
|
+
match.reject(error instanceof Error ? error : new Error(String(error)));
|
|
462
|
+
}
|
|
463
|
+
if (!this.isActive(epoch)) return;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
commitMatches() {
|
|
467
|
+
this.matchBuffer.committedMessages.push(...this.matchBuffer.pendingMessages);
|
|
468
|
+
this.matchBuffer.pendingMessages = [];
|
|
469
|
+
if (this.matchBuffer.committedMessages.length > 1e3) {
|
|
470
|
+
this.matchBuffer.committedMessages.splice(
|
|
471
|
+
0,
|
|
472
|
+
this.matchBuffer.committedMessages.length - 1e3
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
for (const [matchId, match] of this.pendingMatches) {
|
|
476
|
+
if (!match.matched) continue;
|
|
477
|
+
clearTimeout(match.timeoutId);
|
|
478
|
+
this.pendingMatches.delete(matchId);
|
|
479
|
+
match.resolve(true);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
function electricCollectionOptions(config) {
|
|
484
|
+
let descriptorLifecycle = new ElectricLifecycle(config.id);
|
|
485
|
+
let utilityLifecycle = descriptorLifecycle;
|
|
486
|
+
const internalSyncMode = config.syncMode ?? `eager`;
|
|
487
|
+
const finalSyncMode = internalSyncMode === `progressive` ? `on-demand` : internalSyncMode;
|
|
488
|
+
const boundLifecycles = /* @__PURE__ */ new WeakMap();
|
|
489
|
+
const awaitTxId = (txId, timeout) => utilityLifecycle.utils.awaitTxId(txId, timeout);
|
|
490
|
+
const awaitMatch = (matchFn, timeout) => utilityLifecycle.utils.awaitMatch(matchFn, timeout);
|
|
491
|
+
const createSync = () => createElectricSync(config.shapeOptions, {
|
|
492
|
+
getLifecycle: (collection) => boundLifecycles.get(collection) ?? descriptorLifecycle,
|
|
493
|
+
syncMode: internalSyncMode,
|
|
494
|
+
collectionId: config.id,
|
|
495
|
+
testHooks: config[ELECTRIC_TEST_HOOKS]
|
|
496
|
+
});
|
|
497
|
+
const sync = createSync();
|
|
498
|
+
const processMatchingStrategy = async (result, waitForTxId) => {
|
|
457
499
|
if (result && `txid` in result) {
|
|
458
500
|
const timeout = result.timeout;
|
|
459
501
|
if (Array.isArray(result.txid)) {
|
|
460
|
-
await Promise.all(result.txid.map((txid) =>
|
|
502
|
+
await Promise.all(result.txid.map((txid) => waitForTxId(txid, timeout)));
|
|
461
503
|
} else {
|
|
462
|
-
await
|
|
504
|
+
await waitForTxId(result.txid, timeout);
|
|
463
505
|
}
|
|
464
506
|
}
|
|
465
507
|
};
|
|
508
|
+
const getMutationAwaitTxId = (params) => {
|
|
509
|
+
const collection = params.collection;
|
|
510
|
+
return collection?.utils?.awaitTxId ?? awaitTxId;
|
|
511
|
+
};
|
|
466
512
|
const wrappedOnInsert = config.onInsert ? async (params) => {
|
|
467
513
|
const handlerResult = await config.onInsert(params);
|
|
468
|
-
await processMatchingStrategy(
|
|
514
|
+
await processMatchingStrategy(
|
|
515
|
+
handlerResult,
|
|
516
|
+
getMutationAwaitTxId(params)
|
|
517
|
+
);
|
|
469
518
|
return handlerResult;
|
|
470
519
|
} : void 0;
|
|
471
520
|
const wrappedOnUpdate = config.onUpdate ? async (params) => {
|
|
472
521
|
const handlerResult = await config.onUpdate(params);
|
|
473
|
-
await processMatchingStrategy(
|
|
522
|
+
await processMatchingStrategy(
|
|
523
|
+
handlerResult,
|
|
524
|
+
getMutationAwaitTxId(params)
|
|
525
|
+
);
|
|
474
526
|
return handlerResult;
|
|
475
527
|
} : void 0;
|
|
476
528
|
const wrappedOnDelete = config.onDelete ? async (params) => {
|
|
477
529
|
const handlerResult = await config.onDelete(params);
|
|
478
|
-
await processMatchingStrategy(
|
|
530
|
+
await processMatchingStrategy(
|
|
531
|
+
handlerResult,
|
|
532
|
+
getMutationAwaitTxId(params)
|
|
533
|
+
);
|
|
479
534
|
return handlerResult;
|
|
480
535
|
} : void 0;
|
|
481
536
|
const {
|
|
@@ -485,236 +540,275 @@ function electricCollectionOptions(config) {
|
|
|
485
540
|
onDelete: _onDelete,
|
|
486
541
|
...restConfig
|
|
487
542
|
} = config;
|
|
488
|
-
const
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
543
|
+
const utilityTemplate = {
|
|
544
|
+
awaitTxId,
|
|
545
|
+
awaitMatch
|
|
546
|
+
};
|
|
547
|
+
const consumeDescriptorLifecycle = () => {
|
|
548
|
+
const lifecycle = descriptorLifecycle;
|
|
549
|
+
descriptorLifecycle = new ElectricLifecycle(config.id);
|
|
550
|
+
utilityLifecycle = lifecycle;
|
|
551
|
+
return lifecycle;
|
|
552
|
+
};
|
|
553
|
+
const createBoundSync = (source, utilities) => {
|
|
554
|
+
const lifecycle = consumeDescriptorLifecycle();
|
|
555
|
+
let collectionKey;
|
|
556
|
+
Object.assign(utilities, lifecycle.utils);
|
|
557
|
+
const boundSync = {
|
|
558
|
+
...source,
|
|
559
|
+
sync: (params) => {
|
|
560
|
+
if (!boundLifecycles.has(params.collection)) {
|
|
561
|
+
collectionKey = params.collection;
|
|
562
|
+
boundLifecycles.set(params.collection, lifecycle);
|
|
502
563
|
}
|
|
503
|
-
|
|
504
|
-
seenTxids.setState(() => new Set(parsed.seenTxids));
|
|
564
|
+
return source.sync(params);
|
|
505
565
|
},
|
|
566
|
+
exportSyncMeta: () => lifecycle.exportMeta(),
|
|
567
|
+
importSyncMeta: (meta) => lifecycle.importMeta(meta),
|
|
568
|
+
mergeSyncMeta: mergeElectricSyncMeta
|
|
569
|
+
};
|
|
570
|
+
return db.withCollectionSyncConfigCleanup(boundSync, () => {
|
|
571
|
+
lifecycle.retire();
|
|
572
|
+
if (collectionKey) boundLifecycles.delete(collectionKey);
|
|
573
|
+
});
|
|
574
|
+
};
|
|
575
|
+
const syncTemplate = db.withCollectionSyncConfigFactory(
|
|
576
|
+
{
|
|
577
|
+
...sync,
|
|
578
|
+
exportSyncMeta: () => descriptorLifecycle.exportMeta(),
|
|
579
|
+
importSyncMeta: (meta) => descriptorLifecycle.importMeta(meta),
|
|
506
580
|
mergeSyncMeta: mergeElectricSyncMeta
|
|
507
581
|
},
|
|
582
|
+
createBoundSync
|
|
583
|
+
);
|
|
584
|
+
const options = {
|
|
585
|
+
...restConfig,
|
|
586
|
+
syncMode: finalSyncMode,
|
|
587
|
+
sync: syncTemplate,
|
|
508
588
|
onInsert: wrappedOnInsert,
|
|
509
589
|
onUpdate: wrappedOnUpdate,
|
|
510
590
|
onDelete: wrappedOnDelete,
|
|
511
|
-
utils:
|
|
512
|
-
awaitTxId,
|
|
513
|
-
awaitMatch
|
|
514
|
-
}
|
|
591
|
+
utils: utilityTemplate
|
|
515
592
|
};
|
|
593
|
+
Object.defineProperty(options, `utils`, {
|
|
594
|
+
enumerable: true,
|
|
595
|
+
get: () => ({ ...utilityTemplate })
|
|
596
|
+
});
|
|
516
597
|
return db.withCollectionConfigFactory(
|
|
517
598
|
options,
|
|
518
599
|
() => electricCollectionOptions(config)
|
|
519
600
|
);
|
|
520
601
|
}
|
|
521
602
|
function createElectricSync(shapeOptions, options) {
|
|
522
|
-
const {
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
resolveMatchedPendingMatches,
|
|
532
|
-
collectionId,
|
|
533
|
-
testHooks
|
|
534
|
-
} = options;
|
|
535
|
-
const MAX_BATCH_MESSAGES = 1e3;
|
|
536
|
-
const relationSchema = new store.Store(void 0);
|
|
537
|
-
const tagCache = /* @__PURE__ */ new Map();
|
|
538
|
-
const parseTag = (tag) => {
|
|
539
|
-
const cachedTag = tagCache.get(tag);
|
|
540
|
-
if (cachedTag) {
|
|
541
|
-
return cachedTag;
|
|
542
|
-
}
|
|
543
|
-
const parsedTag = tagIndex.parseTag(tag);
|
|
544
|
-
tagCache.set(tag, parsedTag);
|
|
545
|
-
return parsedTag;
|
|
546
|
-
};
|
|
547
|
-
const rowTagSets = /* @__PURE__ */ new Map();
|
|
548
|
-
const tagIndex$1 = [];
|
|
549
|
-
let tagLength = void 0;
|
|
550
|
-
const rowActiveConditions = /* @__PURE__ */ new Map();
|
|
551
|
-
let disjunctPositions = void 0;
|
|
552
|
-
const initializeTagIndex = (length) => {
|
|
553
|
-
if (tagIndex$1.length < length) {
|
|
554
|
-
for (let i = tagIndex$1.length; i < length; i++) {
|
|
555
|
-
tagIndex$1[i] = /* @__PURE__ */ new Map();
|
|
603
|
+
const { getLifecycle, syncMode, collectionId, testHooks } = options;
|
|
604
|
+
let relationSchema;
|
|
605
|
+
let warnedUnverifiableResume = false;
|
|
606
|
+
const createTagState = () => {
|
|
607
|
+
const tagCache = /* @__PURE__ */ new Map();
|
|
608
|
+
const parseTag = (tag) => {
|
|
609
|
+
const cachedTag = tagCache.get(tag);
|
|
610
|
+
if (cachedTag) {
|
|
611
|
+
return cachedTag;
|
|
556
612
|
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
613
|
+
const parsedTag = tagIndex.parseTag(tag);
|
|
614
|
+
tagCache.set(tag, parsedTag);
|
|
615
|
+
return parsedTag;
|
|
616
|
+
};
|
|
617
|
+
const rowTagSets = /* @__PURE__ */ new Map();
|
|
618
|
+
const tagIndex$1 = [];
|
|
619
|
+
let tagLength = void 0;
|
|
620
|
+
const rowActiveConditions = /* @__PURE__ */ new Map();
|
|
621
|
+
let disjunctPositions = void 0;
|
|
622
|
+
const initializeTagIndex = (length) => {
|
|
623
|
+
if (tagIndex$1.length < length) {
|
|
624
|
+
for (let i = tagIndex$1.length; i < length; i++) {
|
|
625
|
+
tagIndex$1[i] = /* @__PURE__ */ new Map();
|
|
626
|
+
}
|
|
565
627
|
}
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
)
|
|
571
|
-
|
|
628
|
+
};
|
|
629
|
+
const addTagsToRow = (tags, rowId, rowTagSet) => {
|
|
630
|
+
for (const tag of tags) {
|
|
631
|
+
const parsedTag = parseTag(tag);
|
|
632
|
+
if (tagLength === void 0) {
|
|
633
|
+
tagLength = tagIndex.getTagLength(parsedTag);
|
|
634
|
+
initializeTagIndex(tagLength);
|
|
635
|
+
}
|
|
636
|
+
const currentTagLength = tagIndex.getTagLength(parsedTag);
|
|
637
|
+
if (currentTagLength !== tagLength) {
|
|
638
|
+
debug(
|
|
639
|
+
`${collectionId ? `[${collectionId}] ` : ``}Tag length mismatch: expected ${tagLength}, got ${currentTagLength}`
|
|
640
|
+
);
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
rowTagSet.add(tag);
|
|
644
|
+
tagIndex.addTagToIndex(parsedTag, rowId, tagIndex$1, tagLength);
|
|
572
645
|
}
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
const removeTagsFromRow = (removedTags, rowId, rowTagSet) => {
|
|
578
|
-
if (tagLength === void 0) {
|
|
579
|
-
return;
|
|
580
|
-
}
|
|
581
|
-
for (const tag of removedTags) {
|
|
582
|
-
const parsedTag = parseTag(tag);
|
|
583
|
-
rowTagSet.delete(tag);
|
|
584
|
-
tagIndex.removeTagFromIndex(parsedTag, rowId, tagIndex$1, tagLength);
|
|
585
|
-
tagCache.delete(tag);
|
|
586
|
-
}
|
|
587
|
-
};
|
|
588
|
-
const processTagsForChangeMessage = (tags, removedTags, rowId, activeConditions) => {
|
|
589
|
-
if (!rowTagSets.has(rowId)) {
|
|
590
|
-
rowTagSets.set(rowId, /* @__PURE__ */ new Set());
|
|
591
|
-
}
|
|
592
|
-
const rowTagSet = rowTagSets.get(rowId);
|
|
593
|
-
if (tags) {
|
|
594
|
-
addTagsToRow(tags, rowId, rowTagSet);
|
|
595
|
-
if (disjunctPositions === void 0) {
|
|
596
|
-
const parsedTags = tags.map(parseTag);
|
|
597
|
-
disjunctPositions = tagIndex.deriveDisjunctPositions(parsedTags);
|
|
646
|
+
};
|
|
647
|
+
const removeTagsFromRow = (removedTags, rowId, rowTagSet) => {
|
|
648
|
+
if (tagLength === void 0) {
|
|
649
|
+
return;
|
|
598
650
|
}
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
}
|
|
603
|
-
if (activeConditions && activeConditions.length > 0) {
|
|
604
|
-
rowActiveConditions.set(rowId, [...activeConditions]);
|
|
605
|
-
}
|
|
606
|
-
return rowTagSet;
|
|
607
|
-
};
|
|
608
|
-
const clearTagTrackingState = () => {
|
|
609
|
-
rowTagSets.clear();
|
|
610
|
-
tagIndex$1.length = 0;
|
|
611
|
-
tagLength = void 0;
|
|
612
|
-
rowActiveConditions.clear();
|
|
613
|
-
disjunctPositions = void 0;
|
|
614
|
-
};
|
|
615
|
-
const clearTagsForRow = (rowId) => {
|
|
616
|
-
if (tagLength === void 0) {
|
|
617
|
-
return;
|
|
618
|
-
}
|
|
619
|
-
const rowTagSet = rowTagSets.get(rowId);
|
|
620
|
-
if (!rowTagSet) {
|
|
621
|
-
return;
|
|
622
|
-
}
|
|
623
|
-
for (const tag of rowTagSet) {
|
|
624
|
-
const parsedTag = parseTag(tag);
|
|
625
|
-
const currentTagLength = tagIndex.getTagLength(parsedTag);
|
|
626
|
-
if (currentTagLength === tagLength) {
|
|
651
|
+
for (const tag of removedTags) {
|
|
652
|
+
const parsedTag = parseTag(tag);
|
|
653
|
+
rowTagSet.delete(tag);
|
|
627
654
|
tagIndex.removeTagFromIndex(parsedTag, rowId, tagIndex$1, tagLength);
|
|
655
|
+
tagCache.delete(tag);
|
|
628
656
|
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
if (
|
|
643
|
-
|
|
644
|
-
|
|
657
|
+
};
|
|
658
|
+
const processTagsForChangeMessage = (tags, removedTags, rowId, activeConditions) => {
|
|
659
|
+
if (!rowTagSets.has(rowId)) {
|
|
660
|
+
rowTagSets.set(rowId, /* @__PURE__ */ new Set());
|
|
661
|
+
}
|
|
662
|
+
const rowTagSet = rowTagSets.get(rowId);
|
|
663
|
+
if (tags) {
|
|
664
|
+
addTagsToRow(tags, rowId, rowTagSet);
|
|
665
|
+
if (disjunctPositions === void 0) {
|
|
666
|
+
const parsedTags = tags.map(parseTag);
|
|
667
|
+
disjunctPositions = tagIndex.deriveDisjunctPositions(parsedTags);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
if (removedTags) {
|
|
671
|
+
removeTagsFromRow(removedTags, rowId, rowTagSet);
|
|
672
|
+
}
|
|
673
|
+
if (activeConditions && activeConditions.length > 0) {
|
|
674
|
+
rowActiveConditions.set(rowId, [...activeConditions]);
|
|
675
|
+
}
|
|
676
|
+
return rowTagSet;
|
|
677
|
+
};
|
|
678
|
+
const clearTagTrackingState = () => {
|
|
679
|
+
rowTagSets.clear();
|
|
680
|
+
tagIndex$1.length = 0;
|
|
681
|
+
tagLength = void 0;
|
|
682
|
+
rowActiveConditions.clear();
|
|
683
|
+
disjunctPositions = void 0;
|
|
684
|
+
};
|
|
685
|
+
const clearTagsForRow = (rowId) => {
|
|
686
|
+
if (tagLength === void 0) {
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
const rowTagSet = rowTagSets.get(rowId);
|
|
690
|
+
if (!rowTagSet) {
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
for (const tag of rowTagSet) {
|
|
694
|
+
const parsedTag = parseTag(tag);
|
|
695
|
+
const currentTagLength = tagIndex.getTagLength(parsedTag);
|
|
696
|
+
if (currentTagLength === tagLength) {
|
|
645
697
|
tagIndex.removeTagFromIndex(parsedTag, rowId, tagIndex$1, tagLength);
|
|
646
|
-
tagCache.delete(tag);
|
|
647
698
|
}
|
|
699
|
+
tagCache.delete(tag);
|
|
700
|
+
}
|
|
701
|
+
rowTagSets.delete(rowId);
|
|
702
|
+
rowActiveConditions.delete(rowId);
|
|
703
|
+
};
|
|
704
|
+
const removeMatchingTagsFromRow = (rowId, pattern) => {
|
|
705
|
+
const rowTagSet = rowTagSets.get(rowId);
|
|
706
|
+
if (!rowTagSet) {
|
|
707
|
+
return false;
|
|
708
|
+
}
|
|
709
|
+
const activeConditions = rowActiveConditions.get(rowId);
|
|
710
|
+
if (activeConditions && disjunctPositions) {
|
|
711
|
+
activeConditions[pattern.pos] = false;
|
|
712
|
+
if (!tagIndex.rowVisible(activeConditions, disjunctPositions)) {
|
|
713
|
+
for (const tag of rowTagSet) {
|
|
714
|
+
const parsedTag = parseTag(tag);
|
|
715
|
+
tagIndex.removeTagFromIndex(parsedTag, rowId, tagIndex$1, tagLength);
|
|
716
|
+
tagCache.delete(tag);
|
|
717
|
+
}
|
|
718
|
+
rowTagSets.delete(rowId);
|
|
719
|
+
rowActiveConditions.delete(rowId);
|
|
720
|
+
return true;
|
|
721
|
+
}
|
|
722
|
+
return false;
|
|
723
|
+
}
|
|
724
|
+
for (const tag of rowTagSet) {
|
|
725
|
+
const parsedTag = parseTag(tag);
|
|
726
|
+
if (tagIndex.tagMatchesPattern(parsedTag, pattern)) {
|
|
727
|
+
rowTagSet.delete(tag);
|
|
728
|
+
tagIndex.removeTagFromIndex(parsedTag, rowId, tagIndex$1, tagLength);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
if (rowTagSet.size === 0) {
|
|
648
732
|
rowTagSets.delete(rowId);
|
|
649
|
-
rowActiveConditions.delete(rowId);
|
|
650
733
|
return true;
|
|
651
734
|
}
|
|
652
735
|
return false;
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
736
|
+
};
|
|
737
|
+
const processMoveOutEvent = (patterns, begin, write, transactionStarted, onDelete) => {
|
|
738
|
+
if (tagLength === void 0) {
|
|
739
|
+
debug(
|
|
740
|
+
`${collectionId ? `[${collectionId}] ` : ``}Received move-out message but no tag length set yet, ignoring`
|
|
741
|
+
);
|
|
742
|
+
return transactionStarted;
|
|
659
743
|
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
let txStarted = transactionStarted;
|
|
675
|
-
for (const pattern of patterns) {
|
|
676
|
-
const affectedRowIds = tagIndex.findRowsMatchingPattern(pattern, tagIndex$1);
|
|
677
|
-
for (const rowId of affectedRowIds) {
|
|
678
|
-
if (removeMatchingTagsFromRow(rowId, pattern)) {
|
|
679
|
-
if (!txStarted) {
|
|
680
|
-
begin();
|
|
681
|
-
txStarted = true;
|
|
744
|
+
let txStarted = transactionStarted;
|
|
745
|
+
for (const pattern of patterns) {
|
|
746
|
+
const affectedRowIds = tagIndex.findRowsMatchingPattern(pattern, tagIndex$1);
|
|
747
|
+
for (const rowId of affectedRowIds) {
|
|
748
|
+
if (removeMatchingTagsFromRow(rowId, pattern)) {
|
|
749
|
+
if (!txStarted) {
|
|
750
|
+
begin();
|
|
751
|
+
txStarted = true;
|
|
752
|
+
}
|
|
753
|
+
write({
|
|
754
|
+
type: `delete`,
|
|
755
|
+
key: rowId
|
|
756
|
+
});
|
|
757
|
+
onDelete(rowId);
|
|
682
758
|
}
|
|
683
|
-
write({
|
|
684
|
-
type: `delete`,
|
|
685
|
-
key: rowId
|
|
686
|
-
});
|
|
687
759
|
}
|
|
688
760
|
}
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
761
|
+
return txStarted;
|
|
762
|
+
};
|
|
763
|
+
const processMoveInEvent = (patterns) => {
|
|
764
|
+
if (tagLength === void 0) {
|
|
765
|
+
debug(
|
|
766
|
+
`${collectionId ? `[${collectionId}] ` : ``}Received move-in message but no tag length set yet, ignoring`
|
|
767
|
+
);
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
for (const pattern of patterns) {
|
|
771
|
+
const affectedRowIds = tagIndex.findRowsMatchingPattern(pattern, tagIndex$1);
|
|
772
|
+
for (const rowId of affectedRowIds) {
|
|
773
|
+
const activeConditions = rowActiveConditions.get(rowId);
|
|
774
|
+
if (activeConditions) {
|
|
775
|
+
activeConditions[pattern.pos] = true;
|
|
776
|
+
}
|
|
705
777
|
}
|
|
706
778
|
}
|
|
707
|
-
}
|
|
708
|
-
};
|
|
709
|
-
const getSyncMetadata = () => {
|
|
710
|
-
const schema = relationSchema.state || `public`;
|
|
779
|
+
};
|
|
711
780
|
return {
|
|
712
|
-
|
|
781
|
+
hasTags: () => tagLength !== void 0,
|
|
782
|
+
processTagsForChangeMessage,
|
|
783
|
+
clearTagTrackingState,
|
|
784
|
+
clearTagsForRow,
|
|
785
|
+
processMoveOutEvent,
|
|
786
|
+
processMoveInEvent
|
|
713
787
|
};
|
|
714
788
|
};
|
|
715
|
-
|
|
789
|
+
const collectionTags = /* @__PURE__ */ new WeakMap();
|
|
716
790
|
return {
|
|
791
|
+
getSyncMetadata: () => ({
|
|
792
|
+
relation: shapeOptions.params?.table ? [relationSchema || `public`, shapeOptions.params.table] : void 0
|
|
793
|
+
}),
|
|
717
794
|
sync: (params) => {
|
|
795
|
+
const retainsTagState = collectionTags.has(params.collection);
|
|
796
|
+
let tagState = collectionTags.get(params.collection);
|
|
797
|
+
if (!tagState) {
|
|
798
|
+
tagState = createTagState();
|
|
799
|
+
collectionTags.set(params.collection, tagState);
|
|
800
|
+
}
|
|
801
|
+
const {
|
|
802
|
+
processTagsForChangeMessage,
|
|
803
|
+
clearTagTrackingState,
|
|
804
|
+
clearTagsForRow,
|
|
805
|
+
processMoveOutEvent,
|
|
806
|
+
processMoveInEvent
|
|
807
|
+
} = tagState;
|
|
808
|
+
const lifecycle = getLifecycle(params.collection);
|
|
809
|
+
const lifecycleEpoch = lifecycle.start();
|
|
810
|
+
const isActiveLifecycle = () => lifecycle.isActive(lifecycleEpoch);
|
|
811
|
+
Object.assign(params.collection.utils, lifecycle.utils);
|
|
718
812
|
const {
|
|
719
813
|
begin,
|
|
720
814
|
write,
|
|
@@ -750,16 +844,36 @@ function createElectricSync(shapeOptions, options) {
|
|
|
750
844
|
const persistedResumeState2 = metadata?.collection.get(`electric:resume`);
|
|
751
845
|
return parseElectricResumeState(persistedResumeState2);
|
|
752
846
|
};
|
|
847
|
+
const persistedMetadata = metadata;
|
|
848
|
+
const scanPersisted = persistedMetadata?.row.scanPersisted;
|
|
849
|
+
const whenHydrated = persistedMetadata?.row.whenHydrated;
|
|
753
850
|
const persistedResumeState = getNewestElectricResumeState(
|
|
754
851
|
readPersistedResumeState(),
|
|
755
|
-
|
|
852
|
+
lifecycle.resumeState
|
|
756
853
|
);
|
|
757
854
|
const shapeIdentity = getStableShapeIdentity({
|
|
758
855
|
url: shapeOptions.url,
|
|
759
856
|
params: shapeOptions.params
|
|
760
857
|
});
|
|
761
858
|
const hasIncompatiblePersistedResume = persistedResumeState?.kind === `resume` && persistedResumeState.shapeId !== shapeIdentity;
|
|
762
|
-
const
|
|
859
|
+
const hasUnverifiablePersistedResume = shapeOptions.offset === void 0 && shapeOptions.handle === void 0 && persistedResumeState?.kind === `resume` && scanPersisted !== void 0 && whenHydrated === void 0;
|
|
860
|
+
if (hasUnverifiablePersistedResume && !warnedUnverifiableResume) {
|
|
861
|
+
warnedUnverifiableResume = true;
|
|
862
|
+
console.warn(
|
|
863
|
+
`Electric persistence cannot verify hydration for saved resume state. Update the persistence adapter alongside Electric to enable safe resume.`
|
|
864
|
+
);
|
|
865
|
+
}
|
|
866
|
+
const needsFullSnapshot = shapeOptions.offset === void 0 && shapeOptions.handle === void 0 && persistedResumeState !== void 0 && (persistedResumeState.kind === `reset` || !retainsTagState && persistedResumeState.requiresTagState !== false);
|
|
867
|
+
const canUsePersistedResume = shapeOptions.offset === void 0 && shapeOptions.handle === void 0 && persistedResumeState?.kind === `resume` && !hasIncompatiblePersistedResume && !hasUnverifiablePersistedResume && // Cached rows do not contain authoritative tag/active-condition state.
|
|
868
|
+
// Unknown (older) metadata is conservative; untagged shapes still resume.
|
|
869
|
+
!needsFullSnapshot;
|
|
870
|
+
const hasExplicitResumeOffset = shapeOptions.offset !== void 0 && shapeOptions.offset !== `-1`;
|
|
871
|
+
if (!canUsePersistedResume && !hasExplicitResumeOffset) {
|
|
872
|
+
clearTagTrackingState();
|
|
873
|
+
}
|
|
874
|
+
const receivesCompleteRows = shapeOptions.params?.replica === `full`;
|
|
875
|
+
const requiresCompleteResume = syncMode !== `on-demand` && (canUsePersistedResume || hasExplicitResumeOffset && !receivesCompleteRows);
|
|
876
|
+
let freshSnapshotPending = (syncMode === `eager` || needsFullSnapshot) && !canUsePersistedResume && !hasExplicitResumeOffset && whenHydrated !== void 0;
|
|
763
877
|
let progressiveReadyGate = null;
|
|
764
878
|
let streamErrorVersion = 0;
|
|
765
879
|
const wrappedMarkReady = (isBuffering, expectedErrorVersion = streamErrorVersion) => {
|
|
@@ -786,21 +900,16 @@ function createElectricSync(shapeOptions, options) {
|
|
|
786
900
|
}
|
|
787
901
|
}
|
|
788
902
|
abortController.signal.addEventListener(`abort`, () => {
|
|
789
|
-
|
|
790
|
-
current.forEach((match) => {
|
|
791
|
-
clearTimeout(match.timeoutId);
|
|
792
|
-
match.reject(new errors.StreamAbortedError());
|
|
793
|
-
});
|
|
794
|
-
return /* @__PURE__ */ new Map();
|
|
795
|
-
});
|
|
903
|
+
lifecycle.retire(lifecycleEpoch);
|
|
796
904
|
});
|
|
797
905
|
const stream = new client.ShapeStream({
|
|
798
906
|
...shapeOptions,
|
|
799
|
-
//
|
|
800
|
-
|
|
907
|
+
// Recovery needs a complete snapshot even for on-demand shapes.
|
|
908
|
+
// Normal on-demand startup still subscribes to changes only.
|
|
909
|
+
log: syncMode === `on-demand` && !needsFullSnapshot ? `changes_only` : void 0,
|
|
801
910
|
// In on-demand mode, we only need the changes from the point of time the collection was created
|
|
802
911
|
// so we default to `now` when there is no saved offset.
|
|
803
|
-
offset: shapeOptions.offset ?? (canUsePersistedResume ? persistedResumeState.offset : syncMode === `on-demand` ? `now` : void 0),
|
|
912
|
+
offset: shapeOptions.offset ?? (canUsePersistedResume ? persistedResumeState.offset : syncMode === `on-demand` && !needsFullSnapshot ? `now` : void 0),
|
|
804
913
|
handle: shapeOptions.handle ?? (canUsePersistedResume ? persistedResumeState.handle : void 0),
|
|
805
914
|
signal: abortController.signal,
|
|
806
915
|
onError: (errorParams) => {
|
|
@@ -826,11 +935,17 @@ function createElectricSync(shapeOptions, options) {
|
|
|
826
935
|
let transactionStarted = false;
|
|
827
936
|
const newTxids = /* @__PURE__ */ new Set();
|
|
828
937
|
const newSnapshots = [];
|
|
829
|
-
let hasReceivedUpToDate = syncMode === `progressive` &&
|
|
830
|
-
|
|
938
|
+
let hasReceivedUpToDate = syncMode === `progressive` && requiresCompleteResume;
|
|
939
|
+
let isResettingSnapshot = false;
|
|
940
|
+
let resetGeneration = 0;
|
|
941
|
+
const isBufferingInitialSync = () => syncMode === `progressive` && !hasReceivedUpToDate && !isResettingSnapshot;
|
|
831
942
|
const bufferedMessages = [];
|
|
832
943
|
const syncedKeys = /* @__PURE__ */ new Set();
|
|
944
|
+
let resumeInvalid = false;
|
|
833
945
|
const stageResumeMetadata = () => {
|
|
946
|
+
if (!isActiveLifecycle() || resumeInvalid) {
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
834
949
|
const shapeHandle = stream.shapeHandle;
|
|
835
950
|
const lastOffset = stream.lastOffset;
|
|
836
951
|
if (!shapeHandle || lastOffset === `-1`) {
|
|
@@ -841,9 +956,10 @@ function createElectricSync(shapeOptions, options) {
|
|
|
841
956
|
offset: lastOffset,
|
|
842
957
|
handle: shapeHandle,
|
|
843
958
|
shapeId: shapeIdentity,
|
|
844
|
-
updatedAt: Date.now()
|
|
959
|
+
updatedAt: Date.now(),
|
|
960
|
+
requiresTagState: tagState.hasTags()
|
|
845
961
|
};
|
|
846
|
-
|
|
962
|
+
lifecycle.resumeState = resumeState;
|
|
847
963
|
metadata?.collection.set(`electric:resume`, resumeState);
|
|
848
964
|
};
|
|
849
965
|
const commitResetResumeMetadataImmediately = () => {
|
|
@@ -851,14 +967,14 @@ function createElectricSync(shapeOptions, options) {
|
|
|
851
967
|
kind: `reset`,
|
|
852
968
|
updatedAt: Date.now()
|
|
853
969
|
};
|
|
854
|
-
|
|
970
|
+
lifecycle.resumeState = resetState;
|
|
855
971
|
if (metadata) {
|
|
856
972
|
begin({ immediate: true });
|
|
857
973
|
metadata.collection.set(`electric:resume`, resetState);
|
|
858
974
|
commit();
|
|
859
975
|
}
|
|
860
976
|
};
|
|
861
|
-
if (hasIncompatiblePersistedResume) {
|
|
977
|
+
if (hasIncompatiblePersistedResume || hasUnverifiablePersistedResume || needsFullSnapshot && persistedResumeState.kind === `resume`) {
|
|
862
978
|
commitResetResumeMetadataImmediately();
|
|
863
979
|
}
|
|
864
980
|
const processChangeMessage = (changeMessage) => {
|
|
@@ -913,42 +1029,90 @@ function createElectricSync(shapeOptions, options) {
|
|
|
913
1029
|
// Pass abort signal so requestSnapshot errors can be ignored during cleanup
|
|
914
1030
|
signal: abortController.signal
|
|
915
1031
|
});
|
|
916
|
-
|
|
1032
|
+
const resumeKeysPromise = requiresCompleteResume || freshSnapshotPending ? whenHydrated?.() : void 0;
|
|
1033
|
+
let areResumeKeysReady = !resumeKeysPromise;
|
|
1034
|
+
const pendingResumeBatches = [];
|
|
1035
|
+
let unsubscribeStream = () => {
|
|
1036
|
+
};
|
|
1037
|
+
const invalidateResume = () => {
|
|
1038
|
+
resumeInvalid = true;
|
|
1039
|
+
if (transactionStarted) {
|
|
1040
|
+
const cancellation = new AbortController();
|
|
1041
|
+
cancellation.abort();
|
|
1042
|
+
commit(cancellation.signal);
|
|
1043
|
+
transactionStarted = false;
|
|
1044
|
+
}
|
|
1045
|
+
syncedKeys.clear();
|
|
1046
|
+
newTxids.clear();
|
|
1047
|
+
newSnapshots.length = 0;
|
|
1048
|
+
commitResetResumeMetadataImmediately();
|
|
1049
|
+
streamErrorVersion++;
|
|
1050
|
+
unsubscribeStream();
|
|
1051
|
+
abortController.abort();
|
|
1052
|
+
markError(
|
|
1053
|
+
new Error(
|
|
1054
|
+
`Electric resume state referenced an unseen row; a full snapshot is required`
|
|
1055
|
+
)
|
|
1056
|
+
);
|
|
1057
|
+
};
|
|
1058
|
+
const processMessages = (messages) => {
|
|
1059
|
+
if (!isActiveLifecycle() || resumeInvalid) {
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
if (freshSnapshotPending) {
|
|
1063
|
+
freshSnapshotPending = false;
|
|
1064
|
+
begin();
|
|
1065
|
+
transactionStarted = true;
|
|
1066
|
+
truncate();
|
|
1067
|
+
syncedKeys.clear();
|
|
1068
|
+
clearTagTrackingState();
|
|
1069
|
+
isResettingSnapshot = true;
|
|
1070
|
+
resetGeneration++;
|
|
1071
|
+
}
|
|
1072
|
+
const pendingPresence = /* @__PURE__ */ new Map();
|
|
1073
|
+
let usesBaseline = true;
|
|
1074
|
+
for (const pending of collection._state.pendingSyncedTransactions) {
|
|
1075
|
+
if (pending.truncate) {
|
|
1076
|
+
pendingPresence.clear();
|
|
1077
|
+
usesBaseline = false;
|
|
1078
|
+
}
|
|
1079
|
+
for (const operation of pending.operations) {
|
|
1080
|
+
pendingPresence.set(operation.key, operation.type !== `delete`);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
for (const message of bufferedMessages) {
|
|
1084
|
+
if (client.isChangeMessage(message)) {
|
|
1085
|
+
pendingPresence.set(
|
|
1086
|
+
collection.getKeyFromItem(message.value),
|
|
1087
|
+
message.headers.operation !== `delete`
|
|
1088
|
+
);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
917
1091
|
let commitPoint = null;
|
|
918
|
-
|
|
1092
|
+
lifecycle.beginMatchGeneration(messages);
|
|
919
1093
|
for (const message of messages) {
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
const newBuffer = [...currentBuffer, message];
|
|
923
|
-
if (newBuffer.length > MAX_BATCH_MESSAGES) {
|
|
924
|
-
newBuffer.splice(0, newBuffer.length - MAX_BATCH_MESSAGES);
|
|
925
|
-
}
|
|
926
|
-
return newBuffer;
|
|
927
|
-
});
|
|
928
|
-
}
|
|
1094
|
+
lifecycle.observeMatchMessage(message);
|
|
1095
|
+
if (!isActiveLifecycle()) return;
|
|
929
1096
|
if (hasTxids(message) && (!isBufferingInitialSync() || transactionStarted)) {
|
|
930
1097
|
message.headers.txids?.forEach((txid) => newTxids.add(txid));
|
|
931
1098
|
}
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
err instanceof Error ? err : new Error(String(err))
|
|
941
|
-
);
|
|
942
|
-
matchesToRemove.push(matchId);
|
|
943
|
-
debug(`matchFn error: %o`, err);
|
|
1099
|
+
if (client.isChangeMessage(message)) {
|
|
1100
|
+
const rowId = collection.getKeyFromItem(message.value);
|
|
1101
|
+
const operation = message.headers.operation;
|
|
1102
|
+
const hasKnownRow = pendingPresence.get(rowId) ?? (usesBaseline && collection._state.syncedData.has(rowId));
|
|
1103
|
+
if (operation === `update` && !hasKnownRow) {
|
|
1104
|
+
if (requiresCompleteResume && !isResettingSnapshot) {
|
|
1105
|
+
invalidateResume();
|
|
1106
|
+
return;
|
|
944
1107
|
}
|
|
1108
|
+
if (!receivesCompleteRows) continue;
|
|
945
1109
|
}
|
|
946
|
-
|
|
947
|
-
|
|
1110
|
+
pendingPresence.set(rowId, operation !== `delete`);
|
|
1111
|
+
}
|
|
948
1112
|
if (client.isChangeMessage(message)) {
|
|
949
1113
|
const schema = message.headers.schema;
|
|
950
1114
|
if (schema && typeof schema === `string`) {
|
|
951
|
-
relationSchema
|
|
1115
|
+
relationSchema = schema;
|
|
952
1116
|
}
|
|
953
1117
|
if (isBufferingInitialSync() && !transactionStarted) {
|
|
954
1118
|
bufferedMessages.push(message);
|
|
@@ -977,7 +1141,11 @@ function createElectricSync(shapeOptions, options) {
|
|
|
977
1141
|
message.headers.patterns,
|
|
978
1142
|
begin,
|
|
979
1143
|
write,
|
|
980
|
-
transactionStarted
|
|
1144
|
+
transactionStarted,
|
|
1145
|
+
(rowId) => {
|
|
1146
|
+
pendingPresence.set(rowId, false);
|
|
1147
|
+
syncedKeys.delete(rowId);
|
|
1148
|
+
}
|
|
981
1149
|
);
|
|
982
1150
|
}
|
|
983
1151
|
} else if (tagIndex.isMoveInMessage(message)) {
|
|
@@ -998,15 +1166,23 @@ function createElectricSync(shapeOptions, options) {
|
|
|
998
1166
|
truncate();
|
|
999
1167
|
clearTagTrackingState();
|
|
1000
1168
|
syncedKeys.clear();
|
|
1169
|
+
pendingPresence.clear();
|
|
1170
|
+
usesBaseline = false;
|
|
1171
|
+
isResettingSnapshot = true;
|
|
1172
|
+
resetGeneration++;
|
|
1001
1173
|
loadSubsetDedupe?.reset();
|
|
1002
1174
|
commitPoint = null;
|
|
1003
1175
|
hasReceivedUpToDate = false;
|
|
1004
1176
|
bufferedMessages.length = 0;
|
|
1005
1177
|
}
|
|
1006
1178
|
}
|
|
1179
|
+
if (needsFullSnapshot && isResettingSnapshot && commitPoint === `subset-end`)
|
|
1180
|
+
return;
|
|
1007
1181
|
if (commitPoint !== null) {
|
|
1008
1182
|
let applied = true;
|
|
1009
1183
|
const wasBufferingInitialSync = isBufferingInitialSync();
|
|
1184
|
+
const finishesReset = isResettingSnapshot && commitPoint === `up-to-date`;
|
|
1185
|
+
const finishingResetGeneration = resetGeneration;
|
|
1010
1186
|
if (isBufferingInitialSync() && commitPoint === `up-to-date` && !transactionStarted) {
|
|
1011
1187
|
debug(
|
|
1012
1188
|
`${collectionId ? `[${collectionId}] ` : ``}Progressive mode: Performing atomic swap with ${bufferedMessages.length} buffered messages`
|
|
@@ -1030,7 +1206,13 @@ function createElectricSync(shapeOptions, options) {
|
|
|
1030
1206
|
bufferedMsg.headers.patterns,
|
|
1031
1207
|
begin,
|
|
1032
1208
|
write,
|
|
1033
|
-
|
|
1209
|
+
// The swap already opened a transaction, even though the
|
|
1210
|
+
// normal-stream transactionStarted flag is still false.
|
|
1211
|
+
true,
|
|
1212
|
+
(rowId) => {
|
|
1213
|
+
pendingPresence.set(rowId, false);
|
|
1214
|
+
syncedKeys.delete(rowId);
|
|
1215
|
+
}
|
|
1034
1216
|
);
|
|
1035
1217
|
} else if (tagIndex.isMoveInMessage(bufferedMsg)) {
|
|
1036
1218
|
processMoveInEvent(bufferedMsg.headers.patterns);
|
|
@@ -1044,7 +1226,9 @@ function createElectricSync(shapeOptions, options) {
|
|
|
1044
1226
|
);
|
|
1045
1227
|
} else {
|
|
1046
1228
|
if (transactionStarted) {
|
|
1047
|
-
|
|
1229
|
+
if (!isResettingSnapshot || finishesReset) {
|
|
1230
|
+
stageResumeMetadata();
|
|
1231
|
+
}
|
|
1048
1232
|
applied = commit();
|
|
1049
1233
|
transactionStarted = false;
|
|
1050
1234
|
} else if (commitPoint === `up-to-date` && metadata) {
|
|
@@ -1062,36 +1246,66 @@ function createElectricSync(shapeOptions, options) {
|
|
|
1062
1246
|
() => void 0
|
|
1063
1247
|
);
|
|
1064
1248
|
}
|
|
1249
|
+
if (finishesReset) {
|
|
1250
|
+
const finishReset = () => {
|
|
1251
|
+
if (resetGeneration === finishingResetGeneration) {
|
|
1252
|
+
isResettingSnapshot = false;
|
|
1253
|
+
}
|
|
1254
|
+
};
|
|
1255
|
+
if (applied === true) {
|
|
1256
|
+
finishReset();
|
|
1257
|
+
} else {
|
|
1258
|
+
void applied.then(finishReset, () => void 0);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1065
1261
|
if (commitPoint === `up-to-date`) {
|
|
1066
1262
|
hasReceivedUpToDate = true;
|
|
1067
1263
|
}
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
`${collectionId ? `[${collectionId}] ` : ``}new txids synced from pg %O`,
|
|
1073
|
-
Array.from(newTxids)
|
|
1074
|
-
);
|
|
1075
|
-
}
|
|
1076
|
-
newTxids.forEach((txid) => clonedSeen.add(txid));
|
|
1077
|
-
newTxids.clear();
|
|
1078
|
-
return clonedSeen;
|
|
1079
|
-
});
|
|
1080
|
-
seenSnapshots.setState((currentSnapshots) => {
|
|
1081
|
-
const seen = [...currentSnapshots, ...newSnapshots];
|
|
1082
|
-
newSnapshots.forEach(
|
|
1083
|
-
(snapshot) => debug(
|
|
1084
|
-
`${collectionId ? `[${collectionId}] ` : ``}new snapshot synced from pg %o`,
|
|
1085
|
-
snapshot
|
|
1086
|
-
)
|
|
1264
|
+
if (newTxids.size > 0) {
|
|
1265
|
+
debug(
|
|
1266
|
+
`${collectionId ? `[${collectionId}] ` : ``}new txids synced from pg %O`,
|
|
1267
|
+
Array.from(newTxids)
|
|
1087
1268
|
);
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1269
|
+
}
|
|
1270
|
+
newSnapshots.forEach(
|
|
1271
|
+
(snapshot) => debug(
|
|
1272
|
+
`${collectionId ? `[${collectionId}] ` : ``}new snapshot synced from pg %o`,
|
|
1273
|
+
snapshot
|
|
1274
|
+
)
|
|
1275
|
+
);
|
|
1276
|
+
lifecycle.publishEvidence(newTxids, newSnapshots);
|
|
1277
|
+
newTxids.clear();
|
|
1278
|
+
newSnapshots.length = 0;
|
|
1279
|
+
lifecycle.commitMatches();
|
|
1280
|
+
}
|
|
1281
|
+
};
|
|
1282
|
+
unsubscribeStream = stream.subscribe((messages) => {
|
|
1283
|
+
if (!areResumeKeysReady) {
|
|
1284
|
+
pendingResumeBatches.push([...messages]);
|
|
1285
|
+
return;
|
|
1093
1286
|
}
|
|
1287
|
+
processMessages(messages);
|
|
1094
1288
|
});
|
|
1289
|
+
if (!areResumeKeysReady && resumeKeysPromise) {
|
|
1290
|
+
void resumeKeysPromise.then(
|
|
1291
|
+
() => {
|
|
1292
|
+
if (abortController.signal.aborted) return;
|
|
1293
|
+
areResumeKeysReady = true;
|
|
1294
|
+
const queuedBatches = pendingResumeBatches.splice(0);
|
|
1295
|
+
queuedBatches.forEach(processMessages);
|
|
1296
|
+
},
|
|
1297
|
+
(error) => {
|
|
1298
|
+
if (abortController.signal.aborted) return;
|
|
1299
|
+
pendingResumeBatches.length = 0;
|
|
1300
|
+
resumeInvalid = true;
|
|
1301
|
+
commitResetResumeMetadataImmediately();
|
|
1302
|
+
streamErrorVersion++;
|
|
1303
|
+
unsubscribeStream();
|
|
1304
|
+
abortController.abort();
|
|
1305
|
+
markError(error);
|
|
1306
|
+
}
|
|
1307
|
+
);
|
|
1308
|
+
}
|
|
1095
1309
|
return {
|
|
1096
1310
|
loadSubset: loadSubsetDedupe?.loadSubset,
|
|
1097
1311
|
cleanup: () => {
|
|
@@ -1101,13 +1315,12 @@ function createElectricSync(shapeOptions, options) {
|
|
|
1101
1315
|
);
|
|
1102
1316
|
unsubscribeStream();
|
|
1103
1317
|
abortController.abort();
|
|
1318
|
+
pendingResumeBatches.length = 0;
|
|
1104
1319
|
loadSubsetDedupe?.reset();
|
|
1105
|
-
|
|
1320
|
+
lifecycle.retire(lifecycleEpoch);
|
|
1106
1321
|
}
|
|
1107
1322
|
};
|
|
1108
|
-
}
|
|
1109
|
-
// Expose the getSyncMetadata function
|
|
1110
|
-
getSyncMetadata
|
|
1323
|
+
}
|
|
1111
1324
|
};
|
|
1112
1325
|
}
|
|
1113
1326
|
Object.defineProperty(exports, "isChangeMessage", {
|