@tanstack/electric-db-collection 0.3.18 → 0.4.0

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.
@@ -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
- return {
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
- if (!persistedResumeState2 || typeof persistedResumeState2 !== `object`) {
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 = readPersistedResumeState();
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
- metadata.collection.set(`electric:resume`, {
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
- if (!metadata) {
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
- commit();
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
  },