@tanstack/electric-db-collection 0.3.18 → 0.4.1
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 +113 -35
- package/dist/cjs/electric.cjs.map +1 -1
- package/dist/esm/electric.js +114 -36
- package/dist/esm/electric.js.map +1 -1
- package/package.json +2 -2
- package/src/electric.ts +186 -47
package/dist/esm/electric.js
CHANGED
|
@@ -2,13 +2,81 @@ import { ShapeStream, isChangeMessage, isVisibleInSnapshot, isControlMessage } f
|
|
|
2
2
|
import { isChangeMessage as isChangeMessage2, isControlMessage as isControlMessage2 } from "@electric-sql/client";
|
|
3
3
|
import { Store } from "@tanstack/store";
|
|
4
4
|
import DebugModule from "debug";
|
|
5
|
-
import { DeduplicatedLoadSubset, and } from "@tanstack/db";
|
|
5
|
+
import { withCollectionConfigFactory, DeduplicatedLoadSubset, and } from "@tanstack/db";
|
|
6
6
|
import { StreamAbortedError, ExpectedNumberInAwaitTxIdError, TimeoutWaitingForTxIdError, TimeoutWaitingForMatchError } from "./errors.js";
|
|
7
7
|
import { compileSQL } from "./sql-compiler.js";
|
|
8
8
|
import { isMoveOutMessage, isMoveInMessage, deriveDisjunctPositions, getTagLength, addTagToIndex, parseTag, removeTagFromIndex, findRowsMatchingPattern, rowVisible, tagMatchesPattern } from "./tag-index.js";
|
|
9
9
|
const debug = DebugModule.debug(`ts/db:electric`);
|
|
10
10
|
const FORCE_DISCONNECT_AND_REFRESH_TIMEOUT_MS = 250;
|
|
11
11
|
const ELECTRIC_TEST_HOOKS = /* @__PURE__ */ Symbol(`electricTestHooks`);
|
|
12
|
+
function parseElectricResumeState(value) {
|
|
13
|
+
if (!value || typeof value !== `object`) {
|
|
14
|
+
return void 0;
|
|
15
|
+
}
|
|
16
|
+
const record = value;
|
|
17
|
+
if (record.kind === `resume` && typeof record.offset === `string` && typeof record.handle === `string` && typeof record.shapeId === `string` && typeof record.updatedAt === `number` && Number.isFinite(record.updatedAt)) {
|
|
18
|
+
return {
|
|
19
|
+
kind: `resume`,
|
|
20
|
+
offset: record.offset,
|
|
21
|
+
handle: record.handle,
|
|
22
|
+
shapeId: record.shapeId,
|
|
23
|
+
updatedAt: record.updatedAt
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
if (record.kind === `reset` && typeof record.updatedAt === `number` && Number.isFinite(record.updatedAt)) {
|
|
27
|
+
return {
|
|
28
|
+
kind: `reset`,
|
|
29
|
+
updatedAt: record.updatedAt
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
return void 0;
|
|
33
|
+
}
|
|
34
|
+
function parseElectricSyncMeta(value) {
|
|
35
|
+
if (!value || typeof value !== `object`) {
|
|
36
|
+
return void 0;
|
|
37
|
+
}
|
|
38
|
+
const record = value;
|
|
39
|
+
if (record.version !== 1 || !Array.isArray(record.seenTxids) || !record.seenTxids.every(
|
|
40
|
+
(txid) => typeof txid === `number` && Number.isFinite(txid)
|
|
41
|
+
)) {
|
|
42
|
+
return void 0;
|
|
43
|
+
}
|
|
44
|
+
const resume = record.resume === void 0 ? void 0 : parseElectricResumeState(record.resume);
|
|
45
|
+
if (record.resume !== void 0 && resume === void 0) {
|
|
46
|
+
return void 0;
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
version: 1,
|
|
50
|
+
...resume ? { resume } : {},
|
|
51
|
+
seenTxids: Array.from(new Set(record.seenTxids)).sort((a, b) => a - b)
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function mergeElectricSyncMeta(current, incoming) {
|
|
55
|
+
const currentMeta = parseElectricSyncMeta(current);
|
|
56
|
+
const incomingMeta = parseElectricSyncMeta(incoming);
|
|
57
|
+
if (!incomingMeta) {
|
|
58
|
+
return current;
|
|
59
|
+
}
|
|
60
|
+
if (!currentMeta) {
|
|
61
|
+
return incomingMeta;
|
|
62
|
+
}
|
|
63
|
+
const resume = getNewestElectricResumeState(
|
|
64
|
+
currentMeta.resume,
|
|
65
|
+
incomingMeta.resume
|
|
66
|
+
);
|
|
67
|
+
return {
|
|
68
|
+
version: 1,
|
|
69
|
+
...resume ? { resume } : {},
|
|
70
|
+
seenTxids: Array.from(
|
|
71
|
+
/* @__PURE__ */ new Set([...currentMeta.seenTxids, ...incomingMeta.seenTxids])
|
|
72
|
+
).sort((a, b) => a - b)
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function getNewestElectricResumeState(current, incoming) {
|
|
76
|
+
if (!current) return incoming;
|
|
77
|
+
if (!incoming) return current;
|
|
78
|
+
return incoming.updatedAt >= current.updatedAt ? incoming : current;
|
|
79
|
+
}
|
|
12
80
|
function isUpToDateMessage(message) {
|
|
13
81
|
return isControlMessage(message) && message.headers.control === `up-to-date`;
|
|
14
82
|
}
|
|
@@ -183,6 +251,9 @@ function createLoadSubsetDedupe({
|
|
|
183
251
|
function electricCollectionOptions(config) {
|
|
184
252
|
const seenTxids = new Store(/* @__PURE__ */ new Set([]));
|
|
185
253
|
const seenSnapshots = new Store([]);
|
|
254
|
+
const hydratedResumeState = new Store(
|
|
255
|
+
void 0
|
|
256
|
+
);
|
|
186
257
|
const internalSyncMode = config.syncMode ?? `eager`;
|
|
187
258
|
const finalSyncMode = internalSyncMode === `progressive` ? `on-demand` : internalSyncMode;
|
|
188
259
|
const pendingMatches = new Store(/* @__PURE__ */ new Map());
|
|
@@ -215,6 +286,7 @@ function electricCollectionOptions(config) {
|
|
|
215
286
|
const sync = createElectricSync(config.shapeOptions, {
|
|
216
287
|
seenTxids,
|
|
217
288
|
seenSnapshots,
|
|
289
|
+
hydratedResumeState,
|
|
218
290
|
syncMode: internalSyncMode,
|
|
219
291
|
pendingMatches,
|
|
220
292
|
currentBatchMessages,
|
|
@@ -382,10 +454,26 @@ function electricCollectionOptions(config) {
|
|
|
382
454
|
onDelete: _onDelete,
|
|
383
455
|
...restConfig
|
|
384
456
|
} = config;
|
|
385
|
-
|
|
457
|
+
const options = {
|
|
386
458
|
...restConfig,
|
|
387
459
|
syncMode: finalSyncMode,
|
|
388
|
-
sync
|
|
460
|
+
sync: {
|
|
461
|
+
...sync,
|
|
462
|
+
exportSyncMeta: () => ({
|
|
463
|
+
version: 1,
|
|
464
|
+
...hydratedResumeState.state ? { resume: hydratedResumeState.state } : {},
|
|
465
|
+
seenTxids: Array.from(seenTxids.state).sort((a, b) => a - b)
|
|
466
|
+
}),
|
|
467
|
+
importSyncMeta: (meta) => {
|
|
468
|
+
const parsed = parseElectricSyncMeta(meta);
|
|
469
|
+
if (!parsed) {
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
hydratedResumeState.setState(() => parsed.resume);
|
|
473
|
+
seenTxids.setState(() => new Set(parsed.seenTxids));
|
|
474
|
+
},
|
|
475
|
+
mergeSyncMeta: mergeElectricSyncMeta
|
|
476
|
+
},
|
|
389
477
|
onInsert: wrappedOnInsert,
|
|
390
478
|
onUpdate: wrappedOnUpdate,
|
|
391
479
|
onDelete: wrappedOnDelete,
|
|
@@ -394,11 +482,16 @@ function electricCollectionOptions(config) {
|
|
|
394
482
|
awaitMatch
|
|
395
483
|
}
|
|
396
484
|
};
|
|
485
|
+
return withCollectionConfigFactory(
|
|
486
|
+
options,
|
|
487
|
+
() => electricCollectionOptions(config)
|
|
488
|
+
);
|
|
397
489
|
}
|
|
398
490
|
function createElectricSync(shapeOptions, options) {
|
|
399
491
|
const {
|
|
400
492
|
seenTxids,
|
|
401
493
|
seenSnapshots,
|
|
494
|
+
hydratedResumeState,
|
|
402
495
|
syncMode,
|
|
403
496
|
pendingMatches,
|
|
404
497
|
currentBatchMessages,
|
|
@@ -602,28 +695,12 @@ function createElectricSync(shapeOptions, options) {
|
|
|
602
695
|
} = params;
|
|
603
696
|
const readPersistedResumeState = () => {
|
|
604
697
|
const persistedResumeState2 = metadata?.collection.get(`electric:resume`);
|
|
605
|
-
|
|
606
|
-
return void 0;
|
|
607
|
-
}
|
|
608
|
-
const record = persistedResumeState2;
|
|
609
|
-
if (record.kind === `resume` && typeof record.offset === `string` && typeof record.handle === `string` && typeof record.shapeId === `string` && typeof record.updatedAt === `number`) {
|
|
610
|
-
return {
|
|
611
|
-
kind: `resume`,
|
|
612
|
-
offset: record.offset,
|
|
613
|
-
handle: record.handle,
|
|
614
|
-
shapeId: record.shapeId,
|
|
615
|
-
updatedAt: record.updatedAt
|
|
616
|
-
};
|
|
617
|
-
}
|
|
618
|
-
if (record.kind === `reset` && typeof record.updatedAt === `number`) {
|
|
619
|
-
return {
|
|
620
|
-
kind: `reset`,
|
|
621
|
-
updatedAt: record.updatedAt
|
|
622
|
-
};
|
|
623
|
-
}
|
|
624
|
-
return void 0;
|
|
698
|
+
return parseElectricResumeState(persistedResumeState2);
|
|
625
699
|
};
|
|
626
|
-
const persistedResumeState =
|
|
700
|
+
const persistedResumeState = getNewestElectricResumeState(
|
|
701
|
+
readPersistedResumeState(),
|
|
702
|
+
hydratedResumeState.state
|
|
703
|
+
);
|
|
627
704
|
const shapeIdentity = getStableShapeIdentity({
|
|
628
705
|
url: shapeOptions.url,
|
|
629
706
|
params: shapeOptions.params
|
|
@@ -697,32 +774,32 @@ You can provide an 'onError' handler on the shapeOptions to handle this error, a
|
|
|
697
774
|
const bufferedMessages = [];
|
|
698
775
|
const syncedKeys = /* @__PURE__ */ new Set();
|
|
699
776
|
const stageResumeMetadata = () => {
|
|
700
|
-
if (!metadata) {
|
|
701
|
-
return;
|
|
702
|
-
}
|
|
703
777
|
const shapeHandle = stream.shapeHandle;
|
|
704
778
|
const lastOffset = stream.lastOffset;
|
|
705
779
|
if (!shapeHandle || lastOffset === `-1`) {
|
|
706
780
|
return;
|
|
707
781
|
}
|
|
708
|
-
|
|
782
|
+
const resumeState = {
|
|
709
783
|
kind: `resume`,
|
|
710
784
|
offset: lastOffset,
|
|
711
785
|
handle: shapeHandle,
|
|
712
786
|
shapeId: shapeIdentity,
|
|
713
787
|
updatedAt: Date.now()
|
|
714
|
-
}
|
|
788
|
+
};
|
|
789
|
+
hydratedResumeState.setState(() => resumeState);
|
|
790
|
+
metadata?.collection.set(`electric:resume`, resumeState);
|
|
715
791
|
};
|
|
716
792
|
const commitResetResumeMetadataImmediately = () => {
|
|
717
|
-
|
|
718
|
-
return;
|
|
719
|
-
}
|
|
720
|
-
begin({ immediate: true });
|
|
721
|
-
metadata.collection.set(`electric:resume`, {
|
|
793
|
+
const resetState = {
|
|
722
794
|
kind: `reset`,
|
|
723
795
|
updatedAt: Date.now()
|
|
724
|
-
}
|
|
725
|
-
|
|
796
|
+
};
|
|
797
|
+
hydratedResumeState.setState(() => resetState);
|
|
798
|
+
if (metadata) {
|
|
799
|
+
begin({ immediate: true });
|
|
800
|
+
metadata.collection.set(`electric:resume`, resetState);
|
|
801
|
+
commit();
|
|
802
|
+
}
|
|
726
803
|
};
|
|
727
804
|
if (hasIncompatiblePersistedResume) {
|
|
728
805
|
commitResetResumeMetadataImmediately();
|
|
@@ -952,6 +1029,7 @@ You can provide an 'onError' handler on the shapeOptions to handle this error, a
|
|
|
952
1029
|
unsubscribeStream();
|
|
953
1030
|
abortController.abort();
|
|
954
1031
|
loadSubsetDedupe?.reset();
|
|
1032
|
+
hydratedResumeState.setState(() => void 0);
|
|
955
1033
|
}
|
|
956
1034
|
};
|
|
957
1035
|
},
|