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