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