@tanstack/electric-db-collection 0.5.9 → 0.6.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.
@@ -1,7 +1,7 @@
1
1
  import { isVisibleInSnapshot, isChangeMessage, isControlMessage, ShapeStream } from "@electric-sql/client";
2
2
  import { isChangeMessage as isChangeMessage2, isControlMessage as isControlMessage2 } from "@electric-sql/client";
3
3
  import DebugModule from "debug";
4
- import { withCollectionSyncConfigFactory, withCollectionConfigFactory, warnOnce, withCollectionSyncConfigCleanup, validateSyncPersistenceCapability, DeduplicatedLoadSubset, whenSyncAccepted, and, LoadSubsetOperationAbortedError } from "@tanstack/db";
4
+ import { withCollectionSyncConfigFactory, withCollectionConfigFactory, warnOnce, withCollectionSyncConfigCleanup, SyncTransactionAbortedError, LoadSubsetOperationAbortedError, validateSyncPersistenceCapability, DeduplicatedLoadSubset, whenSyncAccepted, and } from "@tanstack/db";
5
5
  import { StreamAbortedError, ExpectedNumberInAwaitTxIdError, TimeoutWaitingForTxIdError, TimeoutWaitingForMatchError } from "./errors.js";
6
6
  import { compileSQL } from "./sql-compiler.js";
7
7
  import { isMoveOutMessage, isMoveInMessage, deriveDisjunctPositions, getTagLength, addTagToIndex, parseTag, removeTagFromIndex, findRowsMatchingPattern, rowVisible, tagMatchesPattern } from "./tag-index.js";
@@ -148,10 +148,25 @@ function getStableShapeIdentity(shapeOptions) {
148
148
  function hasTxids(message) {
149
149
  return `txids` in message.headers && Array.isArray(message.headers.txids);
150
150
  }
151
+ const abortReason = (abortedSignal) => abortedSignal.reason ?? new LoadSubsetOperationAbortedError();
152
+ const waitForDemandOrAbort = (work, demandSignal) => {
153
+ if (!demandSignal) return work;
154
+ let removeAbortListener = () => {
155
+ };
156
+ const aborted = new Promise((_, reject) => {
157
+ const onAbort = () => reject(abortReason(demandSignal));
158
+ demandSignal.addEventListener(`abort`, onAbort, { once: true });
159
+ removeAbortListener = () => demandSignal.removeEventListener(`abort`, onAbort);
160
+ if (demandSignal.aborted) onAbort();
161
+ });
162
+ return Promise.race([work, aborted]).finally(removeAbortListener);
163
+ };
151
164
  function createLoadSubsetDedupe({
152
165
  stream,
153
166
  syncMode,
154
167
  isBufferingInitialSync,
168
+ waitForFullSnapshot,
169
+ waitForEstablishedSnapshot,
155
170
  begin,
156
171
  write,
157
172
  commit,
@@ -160,16 +175,18 @@ function createLoadSubsetDedupe({
160
175
  recordSnapshotRow,
161
176
  collectionId,
162
177
  encodeColumnName,
163
- signal
178
+ signal,
179
+ isCleanupAbort,
180
+ beforeSnapshot
164
181
  }) {
165
182
  if (syncMode === `eager`) {
166
183
  return null;
167
184
  }
168
185
  const compileOptions = encodeColumnName ? { encodeColumnName } : void 0;
169
186
  const logPrefix = collectionId ? `[${collectionId}] ` : ``;
170
- const abortReason = (abortedSignal) => abortedSignal.reason ?? new LoadSubsetOperationAbortedError();
187
+ let snapshotTail;
171
188
  function handleSnapshotError(error, operation) {
172
- if (signal.aborted) {
189
+ if (signal.aborted && isCleanupAbort()) {
173
190
  debug(`${logPrefix}Ignoring ${operation} error during cleanup: %o`, error);
174
191
  return true;
175
192
  }
@@ -182,14 +199,48 @@ function createLoadSubsetDedupe({
182
199
  if (signal.aborted) throw abortReason(signal);
183
200
  if (opts.signal?.aborted) throw abortReason(opts.signal);
184
201
  };
202
+ const stopAfterStreamAbort = () => {
203
+ if (!signal.aborted) return false;
204
+ if (!isCleanupAbort()) throw abortReason(signal);
205
+ return true;
206
+ };
207
+ const requestSnapshotInOrder = (params) => {
208
+ const invoke = () => {
209
+ throwIfAborted();
210
+ return stream.requestSnapshot(params);
211
+ };
212
+ const request = snapshotTail ? snapshotTail.then(invoke) : invoke();
213
+ snapshotTail = request.then(
214
+ () => void 0,
215
+ () => void 0
216
+ );
217
+ return waitForDemandOrAbort(request, opts.signal);
218
+ };
185
219
  throwIfAborted();
220
+ if (beforeSnapshot) {
221
+ await beforeSnapshot();
222
+ throwIfAborted();
223
+ }
224
+ const settleWith = async (establishing) => {
225
+ const demandSignal = opts.signal;
226
+ try {
227
+ await waitForDemandOrAbort(establishing, demandSignal);
228
+ } catch (error) {
229
+ if (signal.aborted && isCleanupAbort()) return;
230
+ if (demandSignal?.aborted) throw abortReason(demandSignal);
231
+ throw error;
232
+ }
233
+ if (signal.aborted && isCleanupAbort()) return;
234
+ if (demandSignal?.aborted) throw abortReason(demandSignal);
235
+ };
236
+ if (waitForFullSnapshot) return settleWith(waitForFullSnapshot());
186
237
  if (isBufferingInitialSync()) {
187
238
  const snapshotParams = compileSQL(opts, compileOptions);
188
239
  try {
189
240
  const { data: rows } = await stream.fetchSnapshot(snapshotParams);
190
241
  if (!isBufferingInitialSync()) {
191
242
  debug(`${logPrefix}Ignoring snapshot - sync completed while fetching`);
192
- return;
243
+ return settleWith(waitForEstablishedSnapshot());
193
244
  }
194
245
  throwIfAborted();
195
246
  if (rows.length > 0) {
@@ -216,9 +267,8 @@ function createLoadSubsetDedupe({
216
267
  if (opts.signal?.aborted) throw abortReason(opts.signal);
217
268
  return;
218
269
  }
219
- if (syncMode === `progressive`) {
220
- return;
221
- }
270
+ if (syncMode === `progressive`)
271
+ return settleWith(waitForEstablishedSnapshot());
222
272
  const { cursor, where, orderBy, limit } = opts;
223
273
  throwIfAborted();
224
274
  try {
@@ -241,22 +291,24 @@ function createLoadSubsetDedupe({
241
291
  debug(
242
292
  `${logPrefix}Requesting cursor.whereFrom snapshot (with limit ${limit})`
243
293
  );
244
- await Promise.all([
245
- stream.requestSnapshot(whereCurrentParams),
246
- stream.requestSnapshot(whereFromParams)
247
- ]);
294
+ await requestSnapshotInOrder(whereCurrentParams);
295
+ await requestSnapshotInOrder(whereFromParams);
248
296
  } else {
249
297
  const snapshotParams = compileSQL(opts, compileOptions);
250
- await stream.requestSnapshot(snapshotParams);
298
+ await requestSnapshotInOrder(snapshotParams);
251
299
  }
252
300
  } catch (error) {
253
- if (opts.signal?.aborted) return;
301
+ if (opts.signal?.aborted) throw abortReason(opts.signal);
254
302
  if (handleSnapshotError(error, `requestSnapshot`)) {
255
303
  return;
256
304
  }
257
305
  throw error;
258
306
  }
259
- await waitForCommitsAfter(commitCursor);
307
+ if (opts.signal?.aborted) throw abortReason(opts.signal);
308
+ if (stopAfterStreamAbort()) return;
309
+ await waitForDemandOrAbort(waitForCommitsAfter(commitCursor), opts.signal);
310
+ if (opts.signal?.aborted) throw abortReason(opts.signal);
311
+ if (stopAfterStreamAbort()) return;
260
312
  };
261
313
  return new DeduplicatedLoadSubset({ loadSubset });
262
314
  }
@@ -308,6 +360,11 @@ class ElectricLifecycle {
308
360
  this.matchBuffer = { committedMessages: [], pendingMessages: [] };
309
361
  this.evidence.hydratedResumeState = void 0;
310
362
  }
363
+ forgetEvidenceAfterScopedRecovery() {
364
+ this.evidence.seenTxids.clear();
365
+ this.evidence.seenSnapshots.length = 0;
366
+ this.evidence.hydratedResumeState = void 0;
367
+ }
311
368
  exportMeta() {
312
369
  return exportElectricSyncMeta(this.evidence);
313
370
  }
@@ -753,622 +810,811 @@ function createElectricSync(shapeOptions, options) {
753
810
  };
754
811
  };
755
812
  const collectionTags = /* @__PURE__ */ new WeakMap();
756
- return {
757
- getSyncMetadata: () => ({
758
- relation: shapeOptions.params?.table ? [relationSchema || `public`, shapeOptions.params.table] : void 0
759
- }),
760
- sync: (params) => {
761
- const retainsTagState = collectionTags.has(params.collection);
762
- let tagState = collectionTags.get(params.collection);
763
- if (!tagState) {
764
- tagState = createTagState();
765
- collectionTags.set(params.collection, tagState);
813
+ const startSession = (params, freshScopedGeneration = false) => {
814
+ const retainsTagState = collectionTags.has(params.collection);
815
+ let tagState = collectionTags.get(params.collection);
816
+ if (!tagState) {
817
+ tagState = createTagState();
818
+ collectionTags.set(params.collection, tagState);
819
+ }
820
+ const {
821
+ processTagsForChangeMessage,
822
+ clearTagTrackingState,
823
+ clearTagsForRow,
824
+ processMoveOutEvent,
825
+ processMoveInEvent
826
+ } = tagState;
827
+ if (freshScopedGeneration) clearTagTrackingState();
828
+ const lifecycle = getLifecycle(params.collection);
829
+ if (freshScopedGeneration) lifecycle.forgetEvidenceAfterScopedRecovery();
830
+ const lifecycleEpoch = lifecycle.start();
831
+ const isActiveLifecycle = () => lifecycle.isActive(lifecycleEpoch);
832
+ Object.assign(params.collection.utils, lifecycle.utils);
833
+ const {
834
+ begin,
835
+ write,
836
+ commit: commitSyncTransaction,
837
+ markReady,
838
+ markError,
839
+ truncate,
840
+ collection,
841
+ metadata
842
+ } = params;
843
+ let commitSequence = 0;
844
+ const pendingAppliedReceipts = /* @__PURE__ */ new Map();
845
+ const commit = (signal) => {
846
+ const sequence = ++commitSequence;
847
+ const applied = commitSyncTransaction(signal);
848
+ if (applied === true) {
849
+ return true;
766
850
  }
767
- const {
768
- processTagsForChangeMessage,
769
- clearTagTrackingState,
770
- clearTagsForRow,
771
- processMoveOutEvent,
772
- processMoveInEvent
773
- } = tagState;
774
- const lifecycle = getLifecycle(params.collection);
775
- const lifecycleEpoch = lifecycle.start();
776
- const isActiveLifecycle = () => lifecycle.isActive(lifecycleEpoch);
777
- Object.assign(params.collection.utils, lifecycle.utils);
778
- const {
779
- begin,
780
- write,
781
- commit: commitSyncTransaction,
782
- markReady,
783
- markError,
784
- truncate,
785
- collection,
786
- metadata
787
- } = params;
788
- let commitSequence = 0;
789
- const pendingAppliedReceipts = /* @__PURE__ */ new Map();
790
- const commit = (signal) => {
791
- const sequence = ++commitSequence;
792
- const applied = commitSyncTransaction(signal);
793
- if (applied === true) {
794
- return true;
795
- }
796
- pendingAppliedReceipts.set(sequence, applied);
797
- const removeReceipt = () => pendingAppliedReceipts.delete(sequence);
798
- void applied.then(removeReceipt, removeReceipt);
799
- return applied;
800
- };
801
- const waitForCommitsAfter = async (cursor) => {
802
- await Promise.all(
803
- Array.from(
804
- pendingAppliedReceipts,
805
- ([sequence, applied]) => sequence > cursor ? applied : void 0
806
- )
807
- );
808
- };
809
- const readPersistedResumeState = () => {
810
- const persistedResumeState2 = metadata?.collection.get(`electric:resume`);
811
- return parseElectricResumeState(persistedResumeState2);
812
- };
813
- const persistence = metadata === void 0 ? null : validateSyncPersistenceCapability(metadata.persistence);
814
- const hydrateBaseline = persistence?.hydrateBaseline;
815
- const resumeSnapshot = persistence?.resumeSnapshot;
816
- const certifyResumeSnapshot = resumeSnapshot?.certify;
817
- const getKeySetEvidence = resumeSnapshot?.getKeySetEvidence;
818
- const expectCurrentCommit = resumeSnapshot?.expectCurrentCommit;
819
- const persistedKeySetEvidence = getKeySetEvidence?.();
820
- const persistedResumeState = getNewestElectricResumeState(
821
- readPersistedResumeState(),
822
- lifecycle.resumeState
851
+ pendingAppliedReceipts.set(sequence, applied);
852
+ const removeReceipt = () => pendingAppliedReceipts.delete(sequence);
853
+ void applied.then(removeReceipt, removeReceipt);
854
+ return applied;
855
+ };
856
+ const waitForCommitsAfter = async (cursor) => {
857
+ await Promise.all(
858
+ Array.from(
859
+ pendingAppliedReceipts,
860
+ ([sequence, applied]) => sequence > cursor ? applied : void 0
861
+ )
823
862
  );
824
- const shapeIdentity = getStableShapeIdentity({
825
- url: shapeOptions.url,
826
- params: shapeOptions.params
863
+ };
864
+ const persistence = metadata === void 0 ? null : validateSyncPersistenceCapability(metadata.persistence);
865
+ const hydrateBaseline = persistence?.hydrateBaseline;
866
+ const resumeSnapshot = persistence?.resumeSnapshot;
867
+ const certifyResumeSnapshot = resumeSnapshot?.certify;
868
+ const getKeySetEvidence = resumeSnapshot?.getKeySetEvidence;
869
+ const expectCurrentCommit = resumeSnapshot?.expectCurrentCommit;
870
+ const persistedKeySetEvidence = getKeySetEvidence?.();
871
+ const storedResumeRecord = freshScopedGeneration ? void 0 : metadata?.collection.get(`electric:resume`);
872
+ const storedResumeState = parseElectricResumeState(storedResumeRecord);
873
+ const malformedPersistedResume = storedResumeRecord !== void 0 && storedResumeState === void 0;
874
+ const persistedResumeState = getNewestElectricResumeState(
875
+ storedResumeState,
876
+ lifecycle.resumeState
877
+ );
878
+ const shapeIdentity = getStableShapeIdentity({
879
+ url: shapeOptions.url,
880
+ params: shapeOptions.params
881
+ });
882
+ const hasIncompatiblePersistedResume = persistedResumeState?.kind === `resume` && persistedResumeState.shapeId !== shapeIdentity;
883
+ const lacksCompletePersistedKeySet = persistedResumeState?.kind === `resume` && getKeySetEvidence !== void 0 && persistedKeySetEvidence?.status !== `consistent`;
884
+ const hasIncompatibleManagedCache = persistence?.managedCacheGeneration === true && persistedKeySetEvidence?.status === `incompatible`;
885
+ const requiresFreshSourceEvidence = !freshScopedGeneration && (hasIncompatibleManagedCache || shapeOptions.offset === void 0 && shapeOptions.handle === void 0 && (persistedResumeState !== void 0 || malformedPersistedResume) && (malformedPersistedResume || hasIncompatiblePersistedResume || persistedResumeState?.kind === `reset` || lacksCompletePersistedKeySet || !retainsTagState && persistedResumeState?.kind === `resume` && persistedResumeState.requiresTagState !== false));
886
+ const scopedRecovery = syncMode === `on-demand` && requiresFreshSourceEvidence && persistence?.managedCacheGeneration === true && persistence.startScopedRecovery !== void 0;
887
+ if (scopedRecovery) lifecycle.forgetEvidenceAfterScopedRecovery();
888
+ const usesChangesOnlyLog = syncMode === `on-demand` && (!requiresFreshSourceEvidence || scopedRecovery);
889
+ const usesFullLog = syncMode === `on-demand` && !usesChangesOnlyLog;
890
+ const tracksFullSnapshot = usesFullLog || syncMode === `progressive`;
891
+ let resolveFullSnapshot = () => {
892
+ };
893
+ let rejectFullSnapshot = () => {
894
+ };
895
+ let fullSnapshotReady = Promise.resolve();
896
+ const resetFullSnapshot = () => {
897
+ if (!tracksFullSnapshot) return;
898
+ fullSnapshotReady = new Promise((resolve, reject) => {
899
+ resolveFullSnapshot = (receipt) => resolve(receipt === true ? void 0 : receipt);
900
+ rejectFullSnapshot = reject;
827
901
  });
828
- const hasIncompatiblePersistedResume = persistedResumeState?.kind === `resume` && persistedResumeState.shapeId !== shapeIdentity;
829
- const lacksCompletePersistedKeySet = persistedResumeState?.kind === `resume` && getKeySetEvidence !== void 0 && persistedKeySetEvidence?.status !== `consistent`;
830
- const needsFullSnapshot = shapeOptions.offset === void 0 && shapeOptions.handle === void 0 && persistedResumeState !== void 0 && (persistedResumeState.kind === `reset` || lacksCompletePersistedKeySet || !retainsTagState && persistedResumeState.requiresTagState !== false);
831
- const canUsePersistedResume = shapeOptions.offset === void 0 && shapeOptions.handle === void 0 && persistedResumeState?.kind === `resume` && !hasIncompatiblePersistedResume && // Cached rows do not contain authoritative tag/active-condition state.
832
- // Only a complete adapter ledger can justify a persisted cursor.
833
- !needsFullSnapshot;
834
- const hasExplicitResumeOffset = shapeOptions.offset !== void 0 && shapeOptions.offset !== `-1`;
835
- if (!canUsePersistedResume && !hasExplicitResumeOffset) {
836
- clearTagTrackingState();
837
- }
838
- const receivesCompleteRows = shapeOptions.params?.replica === `full`;
839
- const requiresKeySetCertification = canUsePersistedResume && certifyResumeSnapshot !== void 0;
840
- const requiresCompleteResume = syncMode !== `on-demand` && (canUsePersistedResume || hasExplicitResumeOffset && !receivesCompleteRows);
841
- let freshSnapshotPending = (syncMode === `eager` || needsFullSnapshot) && !canUsePersistedResume && !hasExplicitResumeOffset && hydrateBaseline !== void 0;
842
- let progressiveReadyGate = null;
843
- let streamErrorVersion = 0;
844
- const wrappedMarkReady = (isBuffering, expectedErrorVersion = streamErrorVersion) => {
845
- if (streamErrorVersion !== expectedErrorVersion) return;
846
- if (isBuffering && syncMode === `progressive` && testHooks?.beforeMarkingReady) {
847
- progressiveReadyGate = testHooks.beforeMarkingReady();
848
- progressiveReadyGate.then(() => {
849
- if (streamErrorVersion === expectedErrorVersion) {
850
- markReady();
851
- }
852
- });
853
- } else {
854
- markReady();
855
- }
856
- };
857
- const abortController = new AbortController();
858
- const forwardExternalAbort = () => abortController.abort();
859
- if (shapeOptions.signal) {
860
- shapeOptions.signal.addEventListener(`abort`, forwardExternalAbort, {
861
- once: true
902
+ void fullSnapshotReady.catch(() => void 0);
903
+ };
904
+ resetFullSnapshot();
905
+ const canUsePersistedResume = shapeOptions.offset === void 0 && shapeOptions.handle === void 0 && persistedResumeState?.kind === `resume` && !hasIncompatiblePersistedResume && // Cached rows do not contain authoritative tag/active-condition state.
906
+ // Only a complete adapter ledger can justify a persisted cursor.
907
+ !requiresFreshSourceEvidence;
908
+ const hasExplicitResumeOffset = shapeOptions.offset !== void 0 && shapeOptions.offset !== `-1`;
909
+ if (!canUsePersistedResume && !hasExplicitResumeOffset) {
910
+ clearTagTrackingState();
911
+ }
912
+ const receivesCompleteRows = shapeOptions.params?.replica === `full`;
913
+ const requiresKeySetCertification = canUsePersistedResume && certifyResumeSnapshot !== void 0;
914
+ const requiresCompleteResume = syncMode !== `on-demand` && (canUsePersistedResume || hasExplicitResumeOffset && !receivesCompleteRows);
915
+ let freshSnapshotPending = (syncMode === `eager` || requiresFreshSourceEvidence && !scopedRecovery) && !canUsePersistedResume && !hasExplicitResumeOffset && hydrateBaseline !== void 0;
916
+ let progressiveReadyGate = null;
917
+ let streamErrorVersion = 0;
918
+ const wrappedMarkReady = (isBuffering, expectedErrorVersion = streamErrorVersion) => {
919
+ if (!isActiveLifecycle() || streamErrorVersion !== expectedErrorVersion)
920
+ return;
921
+ if (isBuffering && syncMode === `progressive` && testHooks?.beforeMarkingReady) {
922
+ progressiveReadyGate = testHooks.beforeMarkingReady();
923
+ progressiveReadyGate.then(() => {
924
+ if (streamErrorVersion === expectedErrorVersion) {
925
+ markReady();
926
+ }
862
927
  });
863
- if (shapeOptions.signal.aborted) {
864
- abortController.abort();
865
- }
928
+ } else {
929
+ markReady();
866
930
  }
867
- abortController.signal.addEventListener(`abort`, () => {
868
- lifecycle.retire(lifecycleEpoch);
931
+ };
932
+ const abortController = new AbortController();
933
+ let cleanupAbortedStream = false;
934
+ let initialSyncFailedFinally = false;
935
+ let firstSnapshotCommitted = false;
936
+ let sessionCleanedUp = false;
937
+ const failInitialSync = (cause) => {
938
+ if (firstSnapshotCommitted) return;
939
+ initialSyncFailedFinally = true;
940
+ markError(cause);
941
+ };
942
+ const settleFirstSnapshot = (isBuffering, failure) => {
943
+ if (sessionCleanedUp || collection.status !== `loading`) return;
944
+ if (failure !== void 0) {
945
+ initialSyncFailedFinally = true;
946
+ markError(failure);
947
+ } else if (isActiveLifecycle()) {
948
+ wrappedMarkReady(isBuffering, streamErrorVersion);
949
+ } else markReady();
950
+ };
951
+ const forwardExternalAbort = () => {
952
+ const fails = isActiveLifecycle() && collection.status === `loading`;
953
+ abortController.abort();
954
+ if (fails) failInitialSync(new StreamAbortedError(collectionId));
955
+ };
956
+ if (shapeOptions.signal) {
957
+ shapeOptions.signal.addEventListener(`abort`, forwardExternalAbort, {
958
+ once: true
869
959
  });
870
- const stream = new ShapeStream({
871
- ...shapeOptions,
872
- // Recovery needs a complete snapshot even for on-demand shapes.
873
- // Normal on-demand startup still subscribes to changes only.
874
- log: syncMode === `on-demand` && !needsFullSnapshot ? `changes_only` : void 0,
875
- // In on-demand mode, we only need the changes from the point of time the collection was created
876
- // so we default to `now` when there is no saved offset.
877
- offset: shapeOptions.offset ?? (canUsePersistedResume ? persistedResumeState.offset : syncMode === `on-demand` && !needsFullSnapshot ? `now` : void 0),
878
- handle: shapeOptions.handle ?? (canUsePersistedResume ? persistedResumeState.handle : void 0),
879
- signal: abortController.signal,
880
- onError: (errorParams) => {
881
- streamErrorVersion++;
882
- const initialSyncFailed = collection.status === `loading`;
883
- if (initialSyncFailed) {
884
- markError(errorParams);
960
+ if (shapeOptions.signal.aborted) forwardExternalAbort();
961
+ }
962
+ abortController.signal.addEventListener(`abort`, () => {
963
+ rejectFullSnapshot(new StreamAbortedError(collectionId));
964
+ lifecycle.retire(lifecycleEpoch);
965
+ });
966
+ const stream = new ShapeStream({
967
+ ...shapeOptions,
968
+ // Uncertified on-demand cache rows are quarantined and loaded only
969
+ // through fresh subset snapshots.
970
+ log: usesChangesOnlyLog ? `changes_only` : void 0,
971
+ // In on-demand mode, we only need the changes from the point of time the collection was created
972
+ // so we default to `now` when there is no saved offset.
973
+ offset: shapeOptions.offset ?? (canUsePersistedResume ? persistedResumeState.offset : usesChangesOnlyLog ? `now` : void 0),
974
+ handle: shapeOptions.handle ?? (canUsePersistedResume ? persistedResumeState.handle : void 0),
975
+ signal: abortController.signal,
976
+ onError: async (errorParams) => {
977
+ if (!isActiveLifecycle() || abortController.signal.aborted) return;
978
+ streamErrorVersion++;
979
+ const fail = (cause) => {
980
+ rejectFullSnapshot(cause);
981
+ if (collection.status === `loading`) failInitialSync(cause);
982
+ };
983
+ if (shapeOptions.onError) {
984
+ let retry;
985
+ try {
986
+ retry = await shapeOptions.onError(errorParams);
987
+ } catch (error) {
988
+ fail(error);
989
+ throw error;
885
990
  }
886
- if (shapeOptions.onError) {
887
- return shapeOptions.onError(errorParams);
888
- } else {
889
- console.error(
890
- `An error occurred while syncing collection: ${collection.id},
991
+ if (!retry || typeof retry !== `object`) fail(errorParams);
992
+ return retry;
993
+ } else {
994
+ const initialSyncFailed = collection.status === `loading`;
995
+ fail(errorParams);
996
+ console.error(
997
+ `An error occurred while syncing collection: ${collection.id},
891
998
  ` + (initialSyncFailed ? `the initial sync has been marked as failed.
892
999
  ` : `the last ready snapshot has been preserved.
893
1000
  `) + `You can provide an 'onError' handler on the shapeOptions to handle this error, and this message will not be logged.`,
894
- errorParams
895
- );
896
- }
897
- return;
1001
+ errorParams
1002
+ );
898
1003
  }
899
- });
900
- let transactionStarted = false;
901
- const newTxids = /* @__PURE__ */ new Set();
902
- const newSnapshots = [];
903
- let hasReceivedUpToDate = syncMode === `progressive` && requiresCompleteResume;
904
- let isResettingSnapshot = false;
905
- let resetGeneration = 0;
906
- const isBufferingInitialSync = () => syncMode === `progressive` && !hasReceivedUpToDate && !isResettingSnapshot;
907
- const bufferedMessages = [];
908
- const progressiveSnapshotRows = /* @__PURE__ */ new Map();
909
- const pendingPresence = /* @__PURE__ */ new Map();
910
- let usesBaseline = true;
911
- let pendingPresenceRevision = 0;
912
- const recordPendingPresence = (rowId, present) => {
913
- pendingPresenceRevision++;
914
- pendingPresence.set(rowId, present);
915
- };
916
- const recordPendingTruncate = () => {
917
- pendingPresenceRevision++;
918
- pendingPresence.clear();
919
- progressiveSnapshotRows.clear();
920
- usesBaseline = false;
1004
+ return;
1005
+ }
1006
+ });
1007
+ let transactionStarted = false;
1008
+ const newTxids = /* @__PURE__ */ new Set();
1009
+ const newSnapshots = [];
1010
+ let hasReceivedUpToDate = syncMode === `progressive` && requiresCompleteResume;
1011
+ if (hasReceivedUpToDate) resolveFullSnapshot(true);
1012
+ let isResettingSnapshot = false;
1013
+ let resetGeneration = 0;
1014
+ const isBufferingInitialSync = () => syncMode === `progressive` && !hasReceivedUpToDate && !isResettingSnapshot;
1015
+ const bufferedMessages = [];
1016
+ const progressiveSnapshotRows = /* @__PURE__ */ new Map();
1017
+ const pendingPresence = /* @__PURE__ */ new Map();
1018
+ let usesBaseline = true;
1019
+ let pendingPresenceRevision = 0;
1020
+ const recordPendingPresence = (rowId, present) => {
1021
+ pendingPresenceRevision++;
1022
+ pendingPresence.set(rowId, present);
1023
+ };
1024
+ const recordPendingTruncate = () => {
1025
+ pendingPresenceRevision++;
1026
+ pendingPresence.clear();
1027
+ progressiveSnapshotRows.clear();
1028
+ usesBaseline = false;
1029
+ };
1030
+ const commitSourceTransaction = () => {
1031
+ const committedPresenceRevision = pendingPresenceRevision;
1032
+ const applied = commit();
1033
+ if (metadata?.persistence) {
1034
+ const retireAppliedPresence = () => {
1035
+ if (pendingPresenceRevision === committedPresenceRevision) {
1036
+ pendingPresence.clear();
1037
+ usesBaseline = true;
1038
+ }
1039
+ };
1040
+ if (applied === true) retireAppliedPresence();
1041
+ else void applied.then(retireAppliedPresence, () => void 0);
1042
+ }
1043
+ return applied;
1044
+ };
1045
+ const beginSourceTransaction = () => {
1046
+ begin();
1047
+ metadata?.persistence?.reserveCommitTurn();
1048
+ };
1049
+ const syncedKeys = /* @__PURE__ */ new Set();
1050
+ let resumeInvalid = false;
1051
+ const stageResumeMetadata = () => {
1052
+ if (!isActiveLifecycle() || resumeInvalid || scopedRecovery || freshScopedGeneration) {
1053
+ return;
1054
+ }
1055
+ const shapeHandle = stream.shapeHandle;
1056
+ const lastOffset = stream.lastOffset;
1057
+ if (!shapeHandle || lastOffset === `-1`) {
1058
+ return;
1059
+ }
1060
+ const resumeState = {
1061
+ kind: `resume`,
1062
+ offset: lastOffset,
1063
+ handle: shapeHandle,
1064
+ shapeId: shapeIdentity,
1065
+ updatedAt: Date.now(),
1066
+ requiresTagState: tagState.hasTags()
921
1067
  };
922
- const commitSourceTransaction = () => {
923
- const committedPresenceRevision = pendingPresenceRevision;
924
- const applied = commit();
925
- if (metadata?.persistence) {
926
- const retireAppliedPresence = () => {
927
- if (pendingPresenceRevision === committedPresenceRevision) {
928
- pendingPresence.clear();
929
- usesBaseline = true;
930
- }
931
- };
932
- if (applied === true) retireAppliedPresence();
933
- else void applied.then(retireAppliedPresence, () => void 0);
934
- }
935
- return applied;
1068
+ lifecycle.resumeState = resumeState;
1069
+ metadata?.collection.set(`electric:resume`, resumeState);
1070
+ };
1071
+ const commitResetResumeMetadataImmediately = (expectInResumeSnapshot = false) => {
1072
+ const resetState = {
1073
+ kind: `reset`,
1074
+ updatedAt: Math.max(
1075
+ Date.now(),
1076
+ persistedResumeState?.updatedAt ?? 0,
1077
+ lifecycle.resumeState?.updatedAt ?? 0
1078
+ ) + 1
936
1079
  };
937
- const beginSourceTransaction = () => {
1080
+ lifecycle.resumeState = resetState;
1081
+ if (metadata) {
938
1082
  begin();
939
- metadata?.persistence?.reserveCommitTurn();
940
- };
941
- const syncedKeys = /* @__PURE__ */ new Set();
942
- let resumeInvalid = false;
943
- const stageResumeMetadata = () => {
944
- if (!isActiveLifecycle() || resumeInvalid) {
945
- return;
1083
+ metadata.collection.set(`electric:resume`, resetState);
1084
+ if (expectInResumeSnapshot) {
1085
+ expectCurrentCommit?.();
946
1086
  }
947
- const shapeHandle = stream.shapeHandle;
948
- const lastOffset = stream.lastOffset;
949
- if (!shapeHandle || lastOffset === `-1`) {
950
- return;
951
- }
952
- const resumeState = {
953
- kind: `resume`,
954
- offset: lastOffset,
955
- handle: shapeHandle,
956
- shapeId: shapeIdentity,
957
- updatedAt: Date.now(),
958
- requiresTagState: tagState.hasTags()
959
- };
960
- lifecycle.resumeState = resumeState;
961
- metadata?.collection.set(`electric:resume`, resumeState);
962
- };
963
- const commitResetResumeMetadataImmediately = (expectInResumeSnapshot = false) => {
964
- const resetState = {
965
- kind: `reset`,
966
- updatedAt: Date.now()
967
- };
968
- lifecycle.resumeState = resetState;
969
- if (metadata) {
970
- begin();
971
- metadata.collection.set(`electric:resume`, resetState);
972
- if (expectInResumeSnapshot) {
973
- expectCurrentCommit?.();
974
- }
975
- commit();
976
- }
977
- };
978
- if (hasIncompatiblePersistedResume || needsFullSnapshot && persistedResumeState.kind === `resume`) {
979
- commitResetResumeMetadataImmediately(true);
1087
+ commit();
980
1088
  }
981
- const processChangeMessage = (changeMessage) => {
982
- if (!isChangeMessage(changeMessage)) {
983
- return;
984
- }
985
- const tags = changeMessage.headers.tags;
986
- const removedTags = changeMessage.headers.removed_tags;
987
- const hasTags = tags || removedTags;
988
- const activeConditions = changeMessage.headers.active_conditions;
989
- const rowId = collection.getKeyFromItem(changeMessage.value);
990
- const operation = changeMessage.headers.operation;
991
- const isDelete = operation === `delete`;
992
- const isDuplicateInsert = operation === `insert` && syncedKeys.has(rowId);
993
- if (isDelete) {
994
- syncedKeys.delete(rowId);
995
- } else {
996
- syncedKeys.add(rowId);
997
- }
998
- if (isDelete) {
999
- clearTagsForRow(rowId);
1000
- } else if (hasTags) {
1001
- processTagsForChangeMessage(
1002
- tags,
1003
- removedTags,
1004
- rowId,
1005
- activeConditions
1006
- );
1089
+ };
1090
+ if (requiresFreshSourceEvidence && !scopedRecovery && (malformedPersistedResume || hasIncompatiblePersistedResume || persistedResumeState?.kind === `resume`)) {
1091
+ commitResetResumeMetadataImmediately(true);
1092
+ }
1093
+ const scopedResetState = scopedRecovery ? persistedResumeState?.kind === `reset` ? persistedResumeState : lifecycle.resumeState?.kind === `reset` ? lifecycle.resumeState : {
1094
+ kind: `reset`,
1095
+ updatedAt: Date.now()
1096
+ } : void 0;
1097
+ if (scopedResetState) lifecycle.resumeState = scopedResetState;
1098
+ const scopedRecoveryPromise = scopedResetState && persistence?.startScopedRecovery ? persistence.startScopedRecovery({
1099
+ key: `electric:resume`,
1100
+ value: scopedResetState
1101
+ }) : void 0;
1102
+ const processChangeMessage = (changeMessage) => {
1103
+ if (!isChangeMessage(changeMessage)) {
1104
+ return;
1105
+ }
1106
+ const tags = changeMessage.headers.tags;
1107
+ const removedTags = changeMessage.headers.removed_tags;
1108
+ const hasTags = tags || removedTags;
1109
+ const activeConditions = changeMessage.headers.active_conditions;
1110
+ const rowId = collection.getKeyFromItem(changeMessage.value);
1111
+ const operation = changeMessage.headers.operation;
1112
+ const isDelete = operation === `delete`;
1113
+ const isDuplicateInsert = operation === `insert` && syncedKeys.has(rowId);
1114
+ if (isDelete) {
1115
+ syncedKeys.delete(rowId);
1116
+ } else {
1117
+ syncedKeys.add(rowId);
1118
+ }
1119
+ if (isDelete) {
1120
+ clearTagsForRow(rowId);
1121
+ } else if (hasTags) {
1122
+ processTagsForChangeMessage(tags, removedTags, rowId, activeConditions);
1123
+ }
1124
+ write({
1125
+ type: isDuplicateInsert ? `update` : operation,
1126
+ value: changeMessage.value,
1127
+ // Include the primary key and relation info in the metadata
1128
+ metadata: {
1129
+ ...changeMessage.headers
1007
1130
  }
1008
- write({
1009
- type: isDuplicateInsert ? `update` : operation,
1010
- value: changeMessage.value,
1011
- // Include the primary key and relation info in the metadata
1012
- metadata: {
1013
- ...changeMessage.headers
1014
- }
1015
- });
1016
- };
1017
- const loadSubsetDedupe = createLoadSubsetDedupe({
1018
- stream,
1019
- syncMode,
1020
- isBufferingInitialSync,
1021
- begin,
1022
- write,
1023
- commit,
1024
- getCommitCursor: () => commitSequence,
1025
- waitForCommitsAfter,
1026
- recordSnapshotRow: (row, message) => {
1027
- recordPendingPresence(collection.getKeyFromItem(row), true);
1028
- progressiveSnapshotRows.set(collection.getKeyFromItem(row), message);
1029
- },
1030
- collectionId,
1031
- // Pass the columnMapper's encode function to transform column names
1032
- // (e.g., camelCase to snake_case) when compiling SQL for subset queries
1033
- encodeColumnName: shapeOptions.columnMapper?.encode,
1034
- // Pass abort signal so requestSnapshot errors can be ignored during cleanup
1035
- signal: abortController.signal
1036
1131
  });
1037
- const resumeKeysPromise = requiresCompleteResume || freshSnapshotPending ? hydrateBaseline ? (async () => {
1038
- await hydrateBaseline();
1039
- const currentKeySetEvidence = getKeySetEvidence?.();
1040
- if (canUsePersistedResume && currentKeySetEvidence?.status !== `consistent`) {
1041
- throw new Error(
1042
- `Electric persisted resume baseline could not be certified during hydration`
1043
- );
1044
- }
1045
- })() : void 0 : requiresKeySetCertification ? (async () => {
1046
- await certifyResumeSnapshot();
1047
- const currentKeySetEvidence = getKeySetEvidence?.();
1048
- if (currentKeySetEvidence?.status !== `consistent`) {
1049
- throw new Error(
1050
- `Electric persisted resume baseline could not be certified`
1051
- );
1052
- }
1053
- })() : void 0;
1054
- let areResumeKeysReady = !resumeKeysPromise;
1055
- const pendingResumeBatches = [];
1056
- let unsubscribeStream = () => {
1057
- };
1058
- const invalidateResume = () => {
1059
- resumeInvalid = true;
1060
- if (transactionStarted) {
1061
- const cancellation = new AbortController();
1062
- cancellation.abort();
1063
- commit(cancellation.signal);
1064
- transactionStarted = false;
1065
- }
1066
- syncedKeys.clear();
1067
- newTxids.clear();
1068
- newSnapshots.length = 0;
1069
- commitResetResumeMetadataImmediately();
1070
- streamErrorVersion++;
1071
- unsubscribeStream();
1072
- abortController.abort();
1073
- markError(
1074
- new Error(
1075
- `Electric resume state referenced an unseen row; a full snapshot is required`
1076
- )
1132
+ };
1133
+ const loadSubsetDedupe = createLoadSubsetDedupe({
1134
+ stream,
1135
+ syncMode,
1136
+ isBufferingInitialSync,
1137
+ waitForFullSnapshot: usesFullLog ? () => fullSnapshotReady : void 0,
1138
+ waitForEstablishedSnapshot: () => fullSnapshotReady,
1139
+ begin,
1140
+ write,
1141
+ commit,
1142
+ getCommitCursor: () => commitSequence,
1143
+ waitForCommitsAfter,
1144
+ recordSnapshotRow: (row, message) => {
1145
+ recordPendingPresence(collection.getKeyFromItem(row), true);
1146
+ progressiveSnapshotRows.set(collection.getKeyFromItem(row), message);
1147
+ },
1148
+ collectionId,
1149
+ // Pass the columnMapper's encode function to transform column names
1150
+ // (e.g., camelCase to snake_case) when compiling SQL for subset queries
1151
+ encodeColumnName: shapeOptions.columnMapper?.encode,
1152
+ // Pass abort ownership so cleanup stays quiet without certifying an
1153
+ // externally aborted acquisition as successful.
1154
+ signal: abortController.signal,
1155
+ isCleanupAbort: () => cleanupAbortedStream,
1156
+ beforeSnapshot: scopedRecoveryPromise ? () => scopedRecoveryPromise : void 0
1157
+ });
1158
+ const resumeKeysPromise = scopedRecoveryPromise ?? (requiresCompleteResume || freshSnapshotPending ? hydrateBaseline ? (async () => {
1159
+ await hydrateBaseline();
1160
+ const currentKeySetEvidence = getKeySetEvidence?.();
1161
+ if (canUsePersistedResume && currentKeySetEvidence?.status !== `consistent`) {
1162
+ throw new Error(
1163
+ `Electric persisted resume baseline could not be certified during hydration`
1077
1164
  );
1078
- };
1079
- const processMessages = (messages) => {
1080
- if (!isActiveLifecycle() || resumeInvalid) {
1081
- return;
1165
+ }
1166
+ })() : void 0 : requiresKeySetCertification ? (async () => {
1167
+ await certifyResumeSnapshot();
1168
+ const currentKeySetEvidence = getKeySetEvidence?.();
1169
+ if (currentKeySetEvidence?.status !== `consistent`) {
1170
+ throw new Error(
1171
+ `Electric persisted resume baseline could not be certified`
1172
+ );
1173
+ }
1174
+ })() : void 0);
1175
+ let areResumeKeysReady = !resumeKeysPromise;
1176
+ const pendingResumeBatches = [];
1177
+ let unsubscribeStream = () => {
1178
+ };
1179
+ const invalidateResume = () => {
1180
+ resumeInvalid = true;
1181
+ if (transactionStarted) {
1182
+ const cancellation = new AbortController();
1183
+ cancellation.abort();
1184
+ commit(cancellation.signal);
1185
+ transactionStarted = false;
1186
+ }
1187
+ syncedKeys.clear();
1188
+ newTxids.clear();
1189
+ newSnapshots.length = 0;
1190
+ commitResetResumeMetadataImmediately();
1191
+ streamErrorVersion++;
1192
+ unsubscribeStream();
1193
+ abortController.abort();
1194
+ markError(
1195
+ new Error(
1196
+ `Electric resume state referenced an unseen row; a full snapshot is required`
1197
+ )
1198
+ );
1199
+ };
1200
+ const processMessages = (messages) => {
1201
+ if (!isActiveLifecycle() || resumeInvalid) {
1202
+ return;
1203
+ }
1204
+ if (freshSnapshotPending) {
1205
+ freshSnapshotPending = false;
1206
+ beginSourceTransaction();
1207
+ transactionStarted = true;
1208
+ truncate();
1209
+ recordPendingTruncate();
1210
+ syncedKeys.clear();
1211
+ clearTagTrackingState();
1212
+ isResettingSnapshot = true;
1213
+ resetGeneration++;
1214
+ }
1215
+ if (!metadata?.persistence && !transactionStarted) {
1216
+ pendingPresence.clear();
1217
+ usesBaseline = true;
1218
+ for (const pending of collection._state.pendingSyncedTransactions) {
1219
+ if (pending.truncate) {
1220
+ pendingPresence.clear();
1221
+ usesBaseline = false;
1222
+ }
1223
+ for (const operation of pending.operations) {
1224
+ pendingPresence.set(operation.key, operation.type !== `delete`);
1225
+ }
1082
1226
  }
1083
- if (freshSnapshotPending) {
1084
- freshSnapshotPending = false;
1085
- beginSourceTransaction();
1086
- transactionStarted = true;
1087
- truncate();
1088
- recordPendingTruncate();
1089
- syncedKeys.clear();
1090
- clearTagTrackingState();
1091
- isResettingSnapshot = true;
1092
- resetGeneration++;
1227
+ for (const message of bufferedMessages) {
1228
+ if (isChangeMessage(message)) {
1229
+ pendingPresence.set(
1230
+ collection.getKeyFromItem(message.value),
1231
+ message.headers.operation !== `delete`
1232
+ );
1233
+ }
1093
1234
  }
1094
- if (!metadata?.persistence && !transactionStarted) {
1095
- pendingPresence.clear();
1096
- usesBaseline = true;
1097
- for (const pending of collection._state.pendingSyncedTransactions) {
1098
- if (pending.truncate) {
1099
- pendingPresence.clear();
1100
- usesBaseline = false;
1101
- }
1102
- for (const operation of pending.operations) {
1103
- pendingPresence.set(operation.key, operation.type !== `delete`);
1235
+ }
1236
+ let commitPoint = null;
1237
+ lifecycle.beginMatchGeneration(messages);
1238
+ for (const message of messages) {
1239
+ lifecycle.observeMatchMessage(message);
1240
+ if (!isActiveLifecycle()) return;
1241
+ if (hasTxids(message) && (!isBufferingInitialSync() || transactionStarted)) {
1242
+ message.headers.txids?.forEach((txid) => newTxids.add(txid));
1243
+ }
1244
+ if (isChangeMessage(message)) {
1245
+ const rowId = collection.getKeyFromItem(message.value);
1246
+ const operation = message.headers.operation;
1247
+ const hasKnownRow = pendingPresence.get(rowId) ?? (usesBaseline && collection._state.syncedData.has(rowId));
1248
+ if (operation === `update` && !hasKnownRow) {
1249
+ if (requiresCompleteResume && !isResettingSnapshot) {
1250
+ invalidateResume();
1251
+ return;
1104
1252
  }
1253
+ if (!receivesCompleteRows) continue;
1105
1254
  }
1106
- for (const message of bufferedMessages) {
1107
- if (isChangeMessage(message)) {
1108
- pendingPresence.set(
1109
- collection.getKeyFromItem(message.value),
1110
- message.headers.operation !== `delete`
1111
- );
1112
- }
1255
+ recordPendingPresence(rowId, operation !== `delete`);
1256
+ if (operation !== `update`) {
1257
+ progressiveSnapshotRows.delete(rowId);
1113
1258
  }
1114
1259
  }
1115
- let commitPoint = null;
1116
- lifecycle.beginMatchGeneration(messages);
1117
- for (const message of messages) {
1118
- lifecycle.observeMatchMessage(message);
1119
- if (!isActiveLifecycle()) return;
1120
- if (hasTxids(message) && (!isBufferingInitialSync() || transactionStarted)) {
1121
- message.headers.txids?.forEach((txid) => newTxids.add(txid));
1260
+ if (isChangeMessage(message)) {
1261
+ const schema = message.headers.schema;
1262
+ if (schema && typeof schema === `string`) {
1263
+ relationSchema = schema;
1122
1264
  }
1123
- if (isChangeMessage(message)) {
1124
- const rowId = collection.getKeyFromItem(message.value);
1125
- const operation = message.headers.operation;
1126
- const hasKnownRow = pendingPresence.get(rowId) ?? (usesBaseline && collection._state.syncedData.has(rowId));
1127
- if (operation === `update` && !hasKnownRow) {
1128
- if (requiresCompleteResume && !isResettingSnapshot) {
1129
- invalidateResume();
1130
- return;
1265
+ if (isBufferingInitialSync() && !transactionStarted) {
1266
+ if (message.headers.operation === `update`) {
1267
+ const rowId = collection.getKeyFromItem(message.value);
1268
+ const snapshotRow = progressiveSnapshotRows.get(rowId);
1269
+ if (snapshotRow) {
1270
+ bufferedMessages.push(snapshotRow);
1271
+ progressiveSnapshotRows.delete(rowId);
1131
1272
  }
1132
- if (!receivesCompleteRows) continue;
1133
1273
  }
1134
- recordPendingPresence(rowId, operation !== `delete`);
1135
- if (operation !== `update`) {
1136
- progressiveSnapshotRows.delete(rowId);
1274
+ bufferedMessages.push(message);
1275
+ } else {
1276
+ if (!transactionStarted) {
1277
+ beginSourceTransaction();
1278
+ transactionStarted = true;
1137
1279
  }
1280
+ processChangeMessage(message);
1138
1281
  }
1139
- if (isChangeMessage(message)) {
1140
- const schema = message.headers.schema;
1141
- if (schema && typeof schema === `string`) {
1142
- relationSchema = schema;
1143
- }
1144
- if (isBufferingInitialSync() && !transactionStarted) {
1145
- if (message.headers.operation === `update`) {
1146
- const rowId = collection.getKeyFromItem(message.value);
1147
- const snapshotRow = progressiveSnapshotRows.get(rowId);
1148
- if (snapshotRow) {
1149
- bufferedMessages.push(snapshotRow);
1150
- progressiveSnapshotRows.delete(rowId);
1151
- }
1282
+ } else if (isSnapshotEndMessage(message)) {
1283
+ if (!isBufferingInitialSync() || transactionStarted) {
1284
+ newSnapshots.push(parseSnapshotMessage(message));
1285
+ }
1286
+ } else if (isUpToDateMessage(message)) {
1287
+ commitPoint = `up-to-date`;
1288
+ } else if (isSubsetEndMessage(message)) {
1289
+ if (commitPoint !== `up-to-date`) {
1290
+ commitPoint = `subset-end`;
1291
+ }
1292
+ } else if (isMoveOutMessage(message)) {
1293
+ if (isBufferingInitialSync() && !transactionStarted) {
1294
+ bufferedMessages.push(message);
1295
+ } else {
1296
+ transactionStarted = processMoveOutEvent(
1297
+ message.headers.patterns,
1298
+ beginSourceTransaction,
1299
+ write,
1300
+ transactionStarted,
1301
+ (rowId) => {
1302
+ recordPendingPresence(rowId, false);
1303
+ syncedKeys.delete(rowId);
1152
1304
  }
1153
- bufferedMessages.push(message);
1154
- } else {
1155
- if (!transactionStarted) {
1156
- beginSourceTransaction();
1157
- transactionStarted = true;
1305
+ );
1306
+ }
1307
+ } else if (isMoveInMessage(message)) {
1308
+ if (isBufferingInitialSync() && !transactionStarted) {
1309
+ bufferedMessages.push(message);
1310
+ } else {
1311
+ processMoveInEvent(message.headers.patterns);
1312
+ }
1313
+ } else if (isMustRefetchMessage(message)) {
1314
+ if (hasReceivedUpToDate) resetFullSnapshot();
1315
+ debug(
1316
+ `${collectionId ? `[${collectionId}] ` : ``}Received must-refetch message, starting transaction with truncate`
1317
+ );
1318
+ commitResetResumeMetadataImmediately();
1319
+ if (!transactionStarted) {
1320
+ beginSourceTransaction();
1321
+ transactionStarted = true;
1322
+ }
1323
+ truncate();
1324
+ clearTagTrackingState();
1325
+ syncedKeys.clear();
1326
+ recordPendingTruncate();
1327
+ isResettingSnapshot = true;
1328
+ resetGeneration++;
1329
+ loadSubsetDedupe?.reset();
1330
+ commitPoint = null;
1331
+ hasReceivedUpToDate = false;
1332
+ bufferedMessages.length = 0;
1333
+ progressiveSnapshotRows.clear();
1334
+ }
1335
+ }
1336
+ if (requiresFreshSourceEvidence && !scopedRecovery && isResettingSnapshot && commitPoint === `subset-end`)
1337
+ return;
1338
+ if (commitPoint !== null) {
1339
+ let applied = true;
1340
+ const wasBufferingInitialSync = isBufferingInitialSync();
1341
+ const finishesReset = isResettingSnapshot && commitPoint === `up-to-date`;
1342
+ const finishingResetGeneration = resetGeneration;
1343
+ if (isBufferingInitialSync() && commitPoint === `up-to-date` && !transactionStarted) {
1344
+ debug(
1345
+ `${collectionId ? `[${collectionId}] ` : ``}Progressive mode: Performing atomic swap with ${bufferedMessages.length} buffered messages`
1346
+ );
1347
+ beginSourceTransaction();
1348
+ truncate();
1349
+ clearTagTrackingState();
1350
+ syncedKeys.clear();
1351
+ for (const bufferedMsg of bufferedMessages) {
1352
+ if (isChangeMessage(bufferedMsg)) {
1353
+ processChangeMessage(bufferedMsg);
1354
+ if (hasTxids(bufferedMsg)) {
1355
+ bufferedMsg.headers.txids?.forEach((txid) => newTxids.add(txid));
1158
1356
  }
1159
- processChangeMessage(message);
1160
- }
1161
- } else if (isSnapshotEndMessage(message)) {
1162
- if (!isBufferingInitialSync() || transactionStarted) {
1163
- newSnapshots.push(parseSnapshotMessage(message));
1164
- }
1165
- } else if (isUpToDateMessage(message)) {
1166
- commitPoint = `up-to-date`;
1167
- } else if (isSubsetEndMessage(message)) {
1168
- if (commitPoint !== `up-to-date`) {
1169
- commitPoint = `subset-end`;
1170
- }
1171
- } else if (isMoveOutMessage(message)) {
1172
- if (isBufferingInitialSync() && !transactionStarted) {
1173
- bufferedMessages.push(message);
1174
- } else {
1175
- transactionStarted = processMoveOutEvent(
1176
- message.headers.patterns,
1177
- beginSourceTransaction,
1357
+ } else if (isSnapshotEndMessage(bufferedMsg)) {
1358
+ newSnapshots.push(parseSnapshotMessage(bufferedMsg));
1359
+ } else if (isMoveOutMessage(bufferedMsg)) {
1360
+ processMoveOutEvent(
1361
+ bufferedMsg.headers.patterns,
1362
+ begin,
1178
1363
  write,
1179
- transactionStarted,
1364
+ // The swap already opened a transaction, even though the
1365
+ // normal-stream transactionStarted flag is still false.
1366
+ true,
1180
1367
  (rowId) => {
1181
1368
  recordPendingPresence(rowId, false);
1182
1369
  syncedKeys.delete(rowId);
1183
1370
  }
1184
1371
  );
1372
+ } else if (isMoveInMessage(bufferedMsg)) {
1373
+ processMoveInEvent(bufferedMsg.headers.patterns);
1185
1374
  }
1186
- } else if (isMoveInMessage(message)) {
1187
- if (isBufferingInitialSync() && !transactionStarted) {
1188
- bufferedMessages.push(message);
1189
- } else {
1190
- processMoveInEvent(message.headers.patterns);
1191
- }
1192
- } else if (isMustRefetchMessage(message)) {
1193
- debug(
1194
- `${collectionId ? `[${collectionId}] ` : ``}Received must-refetch message, starting transaction with truncate`
1195
- );
1196
- commitResetResumeMetadataImmediately();
1197
- if (!transactionStarted) {
1198
- beginSourceTransaction();
1199
- transactionStarted = true;
1200
- }
1201
- truncate();
1202
- clearTagTrackingState();
1203
- syncedKeys.clear();
1204
- recordPendingTruncate();
1205
- isResettingSnapshot = true;
1206
- resetGeneration++;
1207
- loadSubsetDedupe?.reset();
1208
- commitPoint = null;
1209
- hasReceivedUpToDate = false;
1210
- bufferedMessages.length = 0;
1211
- progressiveSnapshotRows.clear();
1212
1375
  }
1213
- }
1214
- if (needsFullSnapshot && isResettingSnapshot && commitPoint === `subset-end`)
1215
- return;
1216
- if (commitPoint !== null) {
1217
- let applied = true;
1218
- const wasBufferingInitialSync = isBufferingInitialSync();
1219
- const finishesReset = isResettingSnapshot && commitPoint === `up-to-date`;
1220
- const finishingResetGeneration = resetGeneration;
1221
- if (isBufferingInitialSync() && commitPoint === `up-to-date` && !transactionStarted) {
1222
- debug(
1223
- `${collectionId ? `[${collectionId}] ` : ``}Progressive mode: Performing atomic swap with ${bufferedMessages.length} buffered messages`
1224
- );
1225
- beginSourceTransaction();
1226
- truncate();
1227
- clearTagTrackingState();
1228
- syncedKeys.clear();
1229
- for (const bufferedMsg of bufferedMessages) {
1230
- if (isChangeMessage(bufferedMsg)) {
1231
- processChangeMessage(bufferedMsg);
1232
- if (hasTxids(bufferedMsg)) {
1233
- bufferedMsg.headers.txids?.forEach(
1234
- (txid) => newTxids.add(txid)
1235
- );
1236
- }
1237
- } else if (isSnapshotEndMessage(bufferedMsg)) {
1238
- newSnapshots.push(parseSnapshotMessage(bufferedMsg));
1239
- } else if (isMoveOutMessage(bufferedMsg)) {
1240
- processMoveOutEvent(
1241
- bufferedMsg.headers.patterns,
1242
- begin,
1243
- write,
1244
- // The swap already opened a transaction, even though the
1245
- // normal-stream transactionStarted flag is still false.
1246
- true,
1247
- (rowId) => {
1248
- recordPendingPresence(rowId, false);
1249
- syncedKeys.delete(rowId);
1250
- }
1251
- );
1252
- } else if (isMoveInMessage(bufferedMsg)) {
1253
- processMoveInEvent(bufferedMsg.headers.patterns);
1254
- }
1376
+ stageResumeMetadata();
1377
+ applied = commitSourceTransaction();
1378
+ bufferedMessages.length = 0;
1379
+ progressiveSnapshotRows.clear();
1380
+ debug(
1381
+ `${collectionId ? `[${collectionId}] ` : ``}Progressive mode: Atomic swap complete, now in normal sync mode`
1382
+ );
1383
+ } else {
1384
+ if (transactionStarted) {
1385
+ if (!isResettingSnapshot || finishesReset) {
1386
+ stageResumeMetadata();
1255
1387
  }
1388
+ applied = commitSourceTransaction();
1389
+ transactionStarted = false;
1390
+ } else if (commitPoint === `up-to-date` && metadata) {
1391
+ beginSourceTransaction();
1256
1392
  stageResumeMetadata();
1257
1393
  applied = commitSourceTransaction();
1258
- bufferedMessages.length = 0;
1259
- progressiveSnapshotRows.clear();
1260
- debug(
1261
- `${collectionId ? `[${collectionId}] ` : ``}Progressive mode: Atomic swap complete, now in normal sync mode`
1262
- );
1263
- } else {
1264
- if (transactionStarted) {
1265
- if (!isResettingSnapshot || finishesReset) {
1266
- stageResumeMetadata();
1267
- }
1268
- applied = commitSourceTransaction();
1269
- transactionStarted = false;
1270
- } else if (commitPoint === `up-to-date` && metadata) {
1271
- beginSourceTransaction();
1272
- stageResumeMetadata();
1273
- applied = commitSourceTransaction();
1274
- }
1275
1394
  }
1276
- const readyErrorVersion = streamErrorVersion;
1277
- const accepted = whenSyncAccepted(applied);
1278
- if (accepted === true) {
1279
- wrappedMarkReady(wasBufferingInitialSync, readyErrorVersion);
1280
- } else {
1281
- void accepted.then(
1282
- () => wrappedMarkReady(wasBufferingInitialSync, readyErrorVersion),
1283
- () => void 0
1284
- );
1285
- }
1286
- if (applied !== true) {
1287
- void applied.then(
1288
- () => void 0,
1289
- (error) => {
1290
- if (!isActiveLifecycle() || abortController.signal.aborted) {
1291
- return;
1292
- }
1293
- streamErrorVersion++;
1294
- unsubscribeStream();
1295
- abortController.abort();
1296
- if (collection.status !== `error`) markError(error);
1297
- }
1298
- );
1299
- }
1300
- if (finishesReset) {
1301
- const finishReset = () => {
1302
- if (resetGeneration === finishingResetGeneration) {
1303
- isResettingSnapshot = false;
1395
+ }
1396
+ const readyErrorVersion = streamErrorVersion;
1397
+ const accepted = whenSyncAccepted(applied);
1398
+ const firstSnapshot = commitPoint === `up-to-date` && collection.status === `loading`;
1399
+ if (commitPoint === `up-to-date`) resolveFullSnapshot(applied);
1400
+ if (accepted === true) {
1401
+ wrappedMarkReady(wasBufferingInitialSync, readyErrorVersion);
1402
+ } else if (firstSnapshot) {
1403
+ firstSnapshotCommitted = true;
1404
+ void accepted.then(
1405
+ () => settleFirstSnapshot(wasBufferingInitialSync),
1406
+ (error) => settleFirstSnapshot(wasBufferingInitialSync, error)
1407
+ );
1408
+ } else {
1409
+ void accepted.then(
1410
+ () => wrappedMarkReady(wasBufferingInitialSync, readyErrorVersion),
1411
+ () => void 0
1412
+ );
1413
+ }
1414
+ if (applied !== true) {
1415
+ void applied.then(
1416
+ () => void 0,
1417
+ (error) => {
1418
+ if (!isActiveLifecycle() || abortController.signal.aborted) {
1419
+ return;
1304
1420
  }
1305
- };
1306
- if (applied === true) {
1307
- finishReset();
1308
- } else {
1309
- void applied.then(finishReset, () => void 0);
1421
+ streamErrorVersion++;
1422
+ unsubscribeStream();
1423
+ abortController.abort();
1424
+ if (collection.status !== `error`) markError(error);
1310
1425
  }
1426
+ );
1427
+ }
1428
+ if (finishesReset) {
1429
+ const finishReset = () => {
1430
+ if (resetGeneration === finishingResetGeneration) {
1431
+ isResettingSnapshot = false;
1432
+ }
1433
+ };
1434
+ if (applied === true) {
1435
+ finishReset();
1436
+ } else {
1437
+ void applied.then(finishReset, () => void 0);
1311
1438
  }
1312
- if (commitPoint === `up-to-date`) {
1313
- hasReceivedUpToDate = true;
1314
- }
1315
- if (newTxids.size > 0) {
1316
- debug(
1317
- `${collectionId ? `[${collectionId}] ` : ``}new txids synced from pg %O`,
1318
- Array.from(newTxids)
1319
- );
1320
- }
1321
- newSnapshots.forEach(
1322
- (snapshot) => debug(
1323
- `${collectionId ? `[${collectionId}] ` : ``}new snapshot synced from pg %o`,
1324
- snapshot
1325
- )
1439
+ }
1440
+ if (commitPoint === `up-to-date`) {
1441
+ hasReceivedUpToDate = true;
1442
+ }
1443
+ if (newTxids.size > 0) {
1444
+ debug(
1445
+ `${collectionId ? `[${collectionId}] ` : ``}new txids synced from pg %O`,
1446
+ Array.from(newTxids)
1326
1447
  );
1327
- lifecycle.publishEvidence(newTxids, newSnapshots);
1328
- newTxids.clear();
1329
- newSnapshots.length = 0;
1330
- lifecycle.commitMatches();
1331
1448
  }
1332
- };
1333
- unsubscribeStream = stream.subscribe((messages) => {
1449
+ newSnapshots.forEach(
1450
+ (snapshot) => debug(
1451
+ `${collectionId ? `[${collectionId}] ` : ``}new snapshot synced from pg %o`,
1452
+ snapshot
1453
+ )
1454
+ );
1455
+ lifecycle.publishEvidence(newTxids, newSnapshots);
1456
+ newTxids.clear();
1457
+ newSnapshots.length = 0;
1458
+ lifecycle.commitMatches();
1459
+ }
1460
+ };
1461
+ unsubscribeStream = stream.subscribe(
1462
+ (messages) => {
1334
1463
  if (!areResumeKeysReady) {
1335
1464
  pendingResumeBatches.push([...messages]);
1336
1465
  return;
1337
1466
  }
1338
1467
  processMessages(messages);
1339
- });
1340
- if (!areResumeKeysReady && resumeKeysPromise) {
1341
- void resumeKeysPromise.then(
1342
- () => {
1343
- if (abortController.signal.aborted) return;
1344
- areResumeKeysReady = true;
1345
- const queuedBatches = pendingResumeBatches.splice(0);
1346
- queuedBatches.forEach(processMessages);
1347
- },
1348
- (error) => {
1349
- if (abortController.signal.aborted) return;
1350
- pendingResumeBatches.length = 0;
1351
- resumeInvalid = true;
1352
- commitResetResumeMetadataImmediately();
1353
- streamErrorVersion++;
1354
- unsubscribeStream();
1355
- abortController.abort();
1356
- markError(error);
1357
- }
1358
- );
1468
+ },
1469
+ (error) => {
1470
+ rejectFullSnapshot(error);
1471
+ if (isActiveLifecycle() && !abortController.signal.aborted && collection.status === `loading`)
1472
+ failInitialSync(error);
1359
1473
  }
1360
- return {
1361
- loadSubset: loadSubsetDedupe?.loadSubset,
1362
- cleanup: () => {
1363
- shapeOptions.signal?.removeEventListener(
1364
- `abort`,
1365
- forwardExternalAbort
1366
- );
1474
+ );
1475
+ if (!areResumeKeysReady && resumeKeysPromise) {
1476
+ void resumeKeysPromise.then(
1477
+ () => {
1478
+ if (abortController.signal.aborted) return;
1479
+ areResumeKeysReady = true;
1480
+ const queuedBatches = pendingResumeBatches.splice(0);
1481
+ queuedBatches.forEach(processMessages);
1482
+ },
1483
+ (error) => {
1484
+ if (abortController.signal.aborted) return;
1485
+ pendingResumeBatches.length = 0;
1486
+ resumeInvalid = true;
1487
+ rejectFullSnapshot(error);
1488
+ commitResetResumeMetadataImmediately();
1489
+ streamErrorVersion++;
1367
1490
  unsubscribeStream();
1368
1491
  abortController.abort();
1369
- pendingResumeBatches.length = 0;
1370
- loadSubsetDedupe?.reset();
1371
- lifecycle.retire(lifecycleEpoch);
1492
+ markError(error);
1493
+ }
1494
+ );
1495
+ }
1496
+ return {
1497
+ loadSubset: loadSubsetDedupe?.loadSubset,
1498
+ cleanup: () => {
1499
+ sessionCleanedUp = true;
1500
+ if (!abortController.signal.aborted && !initialSyncFailedFinally)
1501
+ cleanupAbortedStream = true;
1502
+ shapeOptions.signal?.removeEventListener(`abort`, forwardExternalAbort);
1503
+ unsubscribeStream();
1504
+ abortController.abort();
1505
+ if (transactionStarted) {
1506
+ transactionStarted = false;
1507
+ const cancellation = new AbortController();
1508
+ cancellation.abort();
1509
+ const abandoned = commit(cancellation.signal);
1510
+ if (abandoned !== true) void abandoned.catch(() => void 0);
1511
+ }
1512
+ pendingResumeBatches.length = 0;
1513
+ loadSubsetDedupe?.reset();
1514
+ lifecycle.retire(lifecycleEpoch);
1515
+ }
1516
+ };
1517
+ };
1518
+ return {
1519
+ getSyncMetadata: () => ({
1520
+ relation: shapeOptions.params?.table ? [relationSchema || `public`, shapeOptions.params.table] : void 0
1521
+ }),
1522
+ sync: (params) => {
1523
+ if (syncMode !== `on-demand` || !params.metadata?.persistence?.managedCacheGeneration) {
1524
+ return startSession(params);
1525
+ }
1526
+ let session = startSession(params);
1527
+ let retireSession;
1528
+ let retired = new Promise((resolve) => {
1529
+ retireSession = resolve;
1530
+ });
1531
+ let restartInFlight;
1532
+ let restartFailure;
1533
+ let restartSequence = 0;
1534
+ let sessionCleaned = false;
1535
+ let cleanedUp = false;
1536
+ const isCleanedUp = () => cleanedUp;
1537
+ const cleanupCurrentSession = () => {
1538
+ if (sessionCleaned) return Promise.resolve();
1539
+ sessionCleaned = true;
1540
+ try {
1541
+ return Promise.resolve(session.cleanup());
1542
+ } catch (error) {
1543
+ return Promise.reject(error);
1544
+ }
1545
+ };
1546
+ const trackRestart = (work) => {
1547
+ const tracked = work.catch((error) => {
1548
+ restartFailure = { error };
1549
+ throw error;
1550
+ }).finally(() => {
1551
+ if (restartInFlight === tracked) restartInFlight = void 0;
1552
+ });
1553
+ restartInFlight = tracked;
1554
+ return tracked;
1555
+ };
1556
+ const startReplacement = (sequence) => {
1557
+ if (isCleanedUp() || sequence !== restartSequence) return;
1558
+ session = startSession(params, true);
1559
+ sessionCleaned = false;
1560
+ retired = new Promise((resolve) => {
1561
+ retireSession = resolve;
1562
+ });
1563
+ };
1564
+ return {
1565
+ loadSubset: async (demand) => {
1566
+ while (!isCleanedUp()) {
1567
+ if (restartFailure) throw restartFailure.error;
1568
+ if (demand.signal?.aborted) {
1569
+ throw demand.signal.reason ?? new LoadSubsetOperationAbortedError();
1570
+ }
1571
+ const current = session;
1572
+ const currentRetired = retired;
1573
+ const result = current.loadSubset?.(demand) ?? true;
1574
+ const outcome = await Promise.race([
1575
+ Promise.resolve(result).then(() => `settled`),
1576
+ currentRetired.then(() => `retired`)
1577
+ ]);
1578
+ if (outcome === `settled`) return;
1579
+ if (restartInFlight) {
1580
+ await waitForDemandOrAbort(restartInFlight, demand.signal);
1581
+ }
1582
+ }
1583
+ if (restartFailure) throw restartFailure.error;
1584
+ },
1585
+ restartAfterScopedRecovery: (cacheRotated) => {
1586
+ if (isCleanedUp()) return Promise.resolve();
1587
+ if (restartFailure) return Promise.reject(restartFailure.error);
1588
+ const sequence = ++restartSequence;
1589
+ retireSession();
1590
+ const oldSessionCleanup = cleanupCurrentSession();
1591
+ if (restartInFlight) {
1592
+ const previousRestart = restartInFlight;
1593
+ return trackRestart(
1594
+ (async () => {
1595
+ await Promise.all([previousRestart, oldSessionCleanup]);
1596
+ await cacheRotated;
1597
+ startReplacement(sequence);
1598
+ })()
1599
+ );
1600
+ }
1601
+ return trackRestart(
1602
+ (async () => {
1603
+ await oldSessionCleanup;
1604
+ await cacheRotated;
1605
+ startReplacement(sequence);
1606
+ })()
1607
+ );
1608
+ },
1609
+ cleanup: async () => {
1610
+ cleanedUp = true;
1611
+ retireSession();
1612
+ try {
1613
+ await restartInFlight;
1614
+ } catch (error) {
1615
+ if (!(error instanceof SyncTransactionAbortedError)) throw error;
1616
+ }
1617
+ await cleanupCurrentSession();
1372
1618
  }
1373
1619
  };
1374
1620
  }