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