@tanstack/electric-db-collection 0.4.10 → 0.5.2

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,12 +1,11 @@
1
1
  import { isVisibleInSnapshot, isChangeMessage, ShapeStream, isControlMessage } 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, withCollectionSyncConfigCleanup, DeduplicatedLoadSubset, and, LoadSubsetOperationAbortedError } from "@tanstack/db";
4
+ import { withCollectionSyncConfigFactory, withCollectionConfigFactory, validateSyncPersistenceCapability, warnOnce, withCollectionSyncConfigCleanup, DeduplicatedLoadSubset, and, LoadSubsetOperationAbortedError } 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";
8
8
  const debug = DebugModule.debug(`ts/db:electric`);
9
- const FORCE_DISCONNECT_AND_REFRESH_TIMEOUT_MS = 250;
10
9
  const ELECTRIC_TEST_HOOKS = /* @__PURE__ */ Symbol(`electricTestHooks`);
11
10
  function exportElectricSyncMeta(evidence) {
12
11
  const resume = evidence.hydratedResumeState;
@@ -158,6 +157,7 @@ function createLoadSubsetDedupe({
158
157
  commit,
159
158
  getCommitCursor,
160
159
  waitForCommitsAfter,
160
+ recordSnapshotRow,
161
161
  collectionId,
162
162
  encodeColumnName,
163
163
  signal
@@ -200,7 +200,9 @@ function createLoadSubsetDedupe({
200
200
  metadata: { ...row.headers }
201
201
  });
202
202
  }
203
- await commit(opts.signal);
203
+ const applied = commit(opts.signal);
204
+ for (const row of rows) recordSnapshotRow?.(row.value, row);
205
+ if (applied !== true) await applied;
204
206
  debug(`${logPrefix}Applied snapshot with ${rows.length} rows`);
205
207
  }
206
208
  } catch (error) {
@@ -216,47 +218,6 @@ function createLoadSubsetDedupe({
216
218
  return;
217
219
  }
218
220
  const { cursor, where, orderBy, limit } = opts;
219
- if (stream.isUpToDate) {
220
- let timeoutId;
221
- const abortSignals = [signal, opts.signal].filter(
222
- (candidate) => candidate !== void 0
223
- );
224
- let rejectAbort = () => {
225
- };
226
- const aborted = new Promise((_resolve, reject) => {
227
- rejectAbort = reject;
228
- });
229
- const abort = (event) => rejectAbort(abortReason(event.currentTarget));
230
- for (const abortSignal of abortSignals) {
231
- abortSignal.addEventListener(`abort`, abort, { once: true });
232
- }
233
- try {
234
- await Promise.race([
235
- stream.forceDisconnectAndRefresh(),
236
- new Promise((resolve) => {
237
- timeoutId = setTimeout(
238
- resolve,
239
- FORCE_DISCONNECT_AND_REFRESH_TIMEOUT_MS
240
- );
241
- }),
242
- aborted
243
- ]);
244
- } catch (error) {
245
- if (signal.aborted || opts.signal?.aborted) throw error;
246
- if (handleSnapshotError(error, `forceDisconnectAndRefresh`)) {
247
- return;
248
- }
249
- debug(
250
- `${logPrefix}forceDisconnectAndRefresh failed, proceeding to requestSnapshot: %o`,
251
- error
252
- );
253
- } finally {
254
- clearTimeout(timeoutId);
255
- for (const abortSignal of abortSignals) {
256
- abortSignal.removeEventListener(`abort`, abort);
257
- }
258
- }
259
- }
260
221
  throwIfAborted();
261
222
  try {
262
223
  if (cursor) {
@@ -376,7 +337,7 @@ class ElectricLifecycle {
376
337
  waiter.resolve(true);
377
338
  }
378
339
  }
379
- async awaitTxId(txId, timeout = 5e3) {
340
+ async awaitTxId(txId, timeout = 15e3) {
380
341
  debug(
381
342
  `${this.collectionId ? `[${this.collectionId}] ` : ``}awaitTxId called with txid %d`,
382
343
  txId
@@ -399,7 +360,7 @@ class ElectricLifecycle {
399
360
  });
400
361
  });
401
362
  }
402
- async awaitMatch(matchFn, timeout = 3e3) {
363
+ async awaitMatch(matchFn, timeout = 15e3) {
403
364
  debug(
404
365
  `${this.collectionId ? `[${this.collectionId}] ` : ``}awaitMatch called with custom function`
405
366
  );
@@ -495,12 +456,20 @@ function electricCollectionOptions(config) {
495
456
  });
496
457
  const sync = createSync();
497
458
  const processMatchingStrategy = async (result, waitForTxId) => {
498
- if (result && `txid` in result) {
499
- const timeout = result.timeout;
500
- if (Array.isArray(result.txid)) {
501
- await Promise.all(result.txid.map((txid) => waitForTxId(txid, timeout)));
459
+ const canHaveProperties = typeof result === `object` && result !== null || typeof result === `function`;
460
+ if (canHaveProperties && `txid` in result) {
461
+ const strategy = result;
462
+ warnOnce(
463
+ "electric-collection-txid-return",
464
+ "[TanStack DB] DEPRECATED: Returning { txid } from mutation handlers is deprecated and will be removed in v1.0. Use `await collection.utils.awaitTxId(txid)` instead of returning { txid }. See migration guide: https://tanstack.com/db/latest/docs/collections/electric-collection#persistence-handlers--synchronization"
465
+ );
466
+ const timeout = strategy.timeout;
467
+ if (Array.isArray(strategy.txid)) {
468
+ await Promise.all(
469
+ strategy.txid.map((txid) => waitForTxId(txid, timeout))
470
+ );
502
471
  } else {
503
- await waitForTxId(result.txid, timeout);
472
+ await waitForTxId(strategy.txid, timeout);
504
473
  }
505
474
  }
506
475
  };
@@ -514,7 +483,6 @@ function electricCollectionOptions(config) {
514
483
  handlerResult,
515
484
  getMutationAwaitTxId(params)
516
485
  );
517
- return handlerResult;
518
486
  } : void 0;
519
487
  const wrappedOnUpdate = config.onUpdate ? async (params) => {
520
488
  const handlerResult = await config.onUpdate(params);
@@ -522,7 +490,6 @@ function electricCollectionOptions(config) {
522
490
  handlerResult,
523
491
  getMutationAwaitTxId(params)
524
492
  );
525
- return handlerResult;
526
493
  } : void 0;
527
494
  const wrappedOnDelete = config.onDelete ? async (params) => {
528
495
  const handlerResult = await config.onDelete(params);
@@ -530,7 +497,6 @@ function electricCollectionOptions(config) {
530
497
  handlerResult,
531
498
  getMutationAwaitTxId(params)
532
499
  );
533
- return handlerResult;
534
500
  } : void 0;
535
501
  const {
536
502
  shapeOptions: _shapeOptions,
@@ -601,7 +567,6 @@ function electricCollectionOptions(config) {
601
567
  function createElectricSync(shapeOptions, options) {
602
568
  const { getLifecycle, syncMode, collectionId, testHooks } = options;
603
569
  let relationSchema;
604
- let warnedUnverifiableResume = false;
605
570
  const createTagState = () => {
606
571
  const tagCache = /* @__PURE__ */ new Map();
607
572
  const parseTag$1 = (tag) => {
@@ -843,9 +808,13 @@ function createElectricSync(shapeOptions, options) {
843
808
  const persistedResumeState2 = metadata?.collection.get(`electric:resume`);
844
809
  return parseElectricResumeState(persistedResumeState2);
845
810
  };
846
- const persistedMetadata = metadata;
847
- const scanPersisted = persistedMetadata?.row.scanPersisted;
848
- const whenHydrated = persistedMetadata?.row.whenHydrated;
811
+ const persistence = metadata === void 0 ? null : validateSyncPersistenceCapability(metadata.persistence);
812
+ const hydrateBaseline = persistence?.hydrateBaseline;
813
+ const resumeSnapshot = persistence?.resumeSnapshot;
814
+ const certifyResumeSnapshot = resumeSnapshot?.certify;
815
+ const getKeySetEvidence = resumeSnapshot?.getKeySetEvidence;
816
+ const expectCurrentCommit = resumeSnapshot?.expectCurrentCommit;
817
+ const persistedKeySetEvidence = getKeySetEvidence?.();
849
818
  const persistedResumeState = getNewestElectricResumeState(
850
819
  readPersistedResumeState(),
851
820
  lifecycle.resumeState
@@ -855,24 +824,19 @@ function createElectricSync(shapeOptions, options) {
855
824
  params: shapeOptions.params
856
825
  });
857
826
  const hasIncompatiblePersistedResume = persistedResumeState?.kind === `resume` && persistedResumeState.shapeId !== shapeIdentity;
858
- const hasUnverifiablePersistedResume = shapeOptions.offset === void 0 && shapeOptions.handle === void 0 && persistedResumeState?.kind === `resume` && scanPersisted !== void 0 && whenHydrated === void 0;
859
- if (hasUnverifiablePersistedResume && !warnedUnverifiableResume) {
860
- warnedUnverifiableResume = true;
861
- console.warn(
862
- `Electric persistence cannot verify hydration for saved resume state. Update the persistence adapter alongside Electric to enable safe resume.`
863
- );
864
- }
865
- const needsFullSnapshot = shapeOptions.offset === void 0 && shapeOptions.handle === void 0 && persistedResumeState !== void 0 && (persistedResumeState.kind === `reset` || !retainsTagState && persistedResumeState.requiresTagState !== false);
866
- const canUsePersistedResume = shapeOptions.offset === void 0 && shapeOptions.handle === void 0 && persistedResumeState?.kind === `resume` && !hasIncompatiblePersistedResume && !hasUnverifiablePersistedResume && // Cached rows do not contain authoritative tag/active-condition state.
867
- // Unknown (older) metadata is conservative; untagged shapes still resume.
827
+ const lacksCompletePersistedKeySet = persistedResumeState?.kind === `resume` && getKeySetEvidence !== void 0 && persistedKeySetEvidence?.status !== `consistent`;
828
+ const needsFullSnapshot = shapeOptions.offset === void 0 && shapeOptions.handle === void 0 && persistedResumeState !== void 0 && (persistedResumeState.kind === `reset` || lacksCompletePersistedKeySet || !retainsTagState && persistedResumeState.requiresTagState !== false);
829
+ const canUsePersistedResume = shapeOptions.offset === void 0 && shapeOptions.handle === void 0 && persistedResumeState?.kind === `resume` && !hasIncompatiblePersistedResume && // Cached rows do not contain authoritative tag/active-condition state.
830
+ // Only a complete adapter ledger can justify a persisted cursor.
868
831
  !needsFullSnapshot;
869
832
  const hasExplicitResumeOffset = shapeOptions.offset !== void 0 && shapeOptions.offset !== `-1`;
870
833
  if (!canUsePersistedResume && !hasExplicitResumeOffset) {
871
834
  clearTagTrackingState();
872
835
  }
873
836
  const receivesCompleteRows = shapeOptions.params?.replica === `full`;
837
+ const requiresKeySetCertification = canUsePersistedResume && certifyResumeSnapshot !== void 0;
874
838
  const requiresCompleteResume = syncMode !== `on-demand` && (canUsePersistedResume || hasExplicitResumeOffset && !receivesCompleteRows);
875
- let freshSnapshotPending = (syncMode === `eager` || needsFullSnapshot) && !canUsePersistedResume && !hasExplicitResumeOffset && whenHydrated !== void 0;
839
+ let freshSnapshotPending = (syncMode === `eager` || needsFullSnapshot) && !canUsePersistedResume && !hasExplicitResumeOffset && hydrateBaseline !== void 0;
876
840
  let progressiveReadyGate = null;
877
841
  let streamErrorVersion = 0;
878
842
  const wrappedMarkReady = (isBuffering, expectedErrorVersion = streamErrorVersion) => {
@@ -939,6 +903,39 @@ function createElectricSync(shapeOptions, options) {
939
903
  let resetGeneration = 0;
940
904
  const isBufferingInitialSync = () => syncMode === `progressive` && !hasReceivedUpToDate && !isResettingSnapshot;
941
905
  const bufferedMessages = [];
906
+ const progressiveSnapshotRows = /* @__PURE__ */ new Map();
907
+ const pendingPresence = /* @__PURE__ */ new Map();
908
+ let usesBaseline = true;
909
+ let pendingPresenceRevision = 0;
910
+ const recordPendingPresence = (rowId, present) => {
911
+ pendingPresenceRevision++;
912
+ pendingPresence.set(rowId, present);
913
+ };
914
+ const recordPendingTruncate = () => {
915
+ pendingPresenceRevision++;
916
+ pendingPresence.clear();
917
+ progressiveSnapshotRows.clear();
918
+ usesBaseline = false;
919
+ };
920
+ const commitSourceTransaction = () => {
921
+ const committedPresenceRevision = pendingPresenceRevision;
922
+ const applied = commit();
923
+ if (metadata?.persistence) {
924
+ const retireAppliedPresence = () => {
925
+ if (pendingPresenceRevision === committedPresenceRevision) {
926
+ pendingPresence.clear();
927
+ usesBaseline = true;
928
+ }
929
+ };
930
+ if (applied === true) retireAppliedPresence();
931
+ else void applied.then(retireAppliedPresence, () => void 0);
932
+ }
933
+ return applied;
934
+ };
935
+ const beginSourceTransaction = () => {
936
+ if (metadata?.persistence) begin({ immediate: true });
937
+ else begin();
938
+ };
942
939
  const syncedKeys = /* @__PURE__ */ new Set();
943
940
  let resumeInvalid = false;
944
941
  const stageResumeMetadata = () => {
@@ -961,7 +958,7 @@ function createElectricSync(shapeOptions, options) {
961
958
  lifecycle.resumeState = resumeState;
962
959
  metadata?.collection.set(`electric:resume`, resumeState);
963
960
  };
964
- const commitResetResumeMetadataImmediately = () => {
961
+ const commitResetResumeMetadataImmediately = (expectInResumeSnapshot = false) => {
965
962
  const resetState = {
966
963
  kind: `reset`,
967
964
  updatedAt: Date.now()
@@ -970,11 +967,14 @@ function createElectricSync(shapeOptions, options) {
970
967
  if (metadata) {
971
968
  begin({ immediate: true });
972
969
  metadata.collection.set(`electric:resume`, resetState);
970
+ if (expectInResumeSnapshot) {
971
+ expectCurrentCommit?.();
972
+ }
973
973
  commit();
974
974
  }
975
975
  };
976
- if (hasIncompatiblePersistedResume || hasUnverifiablePersistedResume || needsFullSnapshot && persistedResumeState.kind === `resume`) {
977
- commitResetResumeMetadataImmediately();
976
+ if (hasIncompatiblePersistedResume || needsFullSnapshot && persistedResumeState.kind === `resume`) {
977
+ commitResetResumeMetadataImmediately(true);
978
978
  }
979
979
  const processChangeMessage = (changeMessage) => {
980
980
  if (!isChangeMessage(changeMessage)) {
@@ -1021,6 +1021,10 @@ function createElectricSync(shapeOptions, options) {
1021
1021
  commit,
1022
1022
  getCommitCursor: () => commitSequence,
1023
1023
  waitForCommitsAfter,
1024
+ recordSnapshotRow: (row, message) => {
1025
+ recordPendingPresence(collection.getKeyFromItem(row), true);
1026
+ progressiveSnapshotRows.set(collection.getKeyFromItem(row), message);
1027
+ },
1024
1028
  collectionId,
1025
1029
  // Pass the columnMapper's encode function to transform column names
1026
1030
  // (e.g., camelCase to snake_case) when compiling SQL for subset queries
@@ -1028,7 +1032,23 @@ function createElectricSync(shapeOptions, options) {
1028
1032
  // Pass abort signal so requestSnapshot errors can be ignored during cleanup
1029
1033
  signal: abortController.signal
1030
1034
  });
1031
- const resumeKeysPromise = requiresCompleteResume || freshSnapshotPending ? whenHydrated?.() : void 0;
1035
+ const resumeKeysPromise = requiresCompleteResume || freshSnapshotPending ? hydrateBaseline ? (async () => {
1036
+ await hydrateBaseline();
1037
+ const currentKeySetEvidence = getKeySetEvidence?.();
1038
+ if (canUsePersistedResume && currentKeySetEvidence?.status !== `consistent`) {
1039
+ throw new Error(
1040
+ `Electric persisted resume baseline could not be certified during hydration`
1041
+ );
1042
+ }
1043
+ })() : void 0 : requiresKeySetCertification ? (async () => {
1044
+ await certifyResumeSnapshot();
1045
+ const currentKeySetEvidence = getKeySetEvidence?.();
1046
+ if (currentKeySetEvidence?.status !== `consistent`) {
1047
+ throw new Error(
1048
+ `Electric persisted resume baseline could not be certified`
1049
+ );
1050
+ }
1051
+ })() : void 0;
1032
1052
  let areResumeKeysReady = !resumeKeysPromise;
1033
1053
  const pendingResumeBatches = [];
1034
1054
  let unsubscribeStream = () => {
@@ -1060,31 +1080,34 @@ function createElectricSync(shapeOptions, options) {
1060
1080
  }
1061
1081
  if (freshSnapshotPending) {
1062
1082
  freshSnapshotPending = false;
1063
- begin();
1083
+ beginSourceTransaction();
1064
1084
  transactionStarted = true;
1065
1085
  truncate();
1086
+ recordPendingTruncate();
1066
1087
  syncedKeys.clear();
1067
1088
  clearTagTrackingState();
1068
1089
  isResettingSnapshot = true;
1069
1090
  resetGeneration++;
1070
1091
  }
1071
- const pendingPresence = /* @__PURE__ */ new Map();
1072
- let usesBaseline = true;
1073
- for (const pending of collection._state.pendingSyncedTransactions) {
1074
- if (pending.truncate) {
1075
- pendingPresence.clear();
1076
- usesBaseline = false;
1077
- }
1078
- for (const operation of pending.operations) {
1079
- pendingPresence.set(operation.key, operation.type !== `delete`);
1092
+ if (!metadata?.persistence && !transactionStarted) {
1093
+ pendingPresence.clear();
1094
+ usesBaseline = true;
1095
+ for (const pending of collection._state.pendingSyncedTransactions) {
1096
+ if (pending.truncate) {
1097
+ pendingPresence.clear();
1098
+ usesBaseline = false;
1099
+ }
1100
+ for (const operation of pending.operations) {
1101
+ pendingPresence.set(operation.key, operation.type !== `delete`);
1102
+ }
1080
1103
  }
1081
- }
1082
- for (const message of bufferedMessages) {
1083
- if (isChangeMessage(message)) {
1084
- pendingPresence.set(
1085
- collection.getKeyFromItem(message.value),
1086
- message.headers.operation !== `delete`
1087
- );
1104
+ for (const message of bufferedMessages) {
1105
+ if (isChangeMessage(message)) {
1106
+ pendingPresence.set(
1107
+ collection.getKeyFromItem(message.value),
1108
+ message.headers.operation !== `delete`
1109
+ );
1110
+ }
1088
1111
  }
1089
1112
  }
1090
1113
  let commitPoint = null;
@@ -1106,7 +1129,10 @@ function createElectricSync(shapeOptions, options) {
1106
1129
  }
1107
1130
  if (!receivesCompleteRows) continue;
1108
1131
  }
1109
- pendingPresence.set(rowId, operation !== `delete`);
1132
+ recordPendingPresence(rowId, operation !== `delete`);
1133
+ if (operation !== `update`) {
1134
+ progressiveSnapshotRows.delete(rowId);
1135
+ }
1110
1136
  }
1111
1137
  if (isChangeMessage(message)) {
1112
1138
  const schema = message.headers.schema;
@@ -1114,10 +1140,18 @@ function createElectricSync(shapeOptions, options) {
1114
1140
  relationSchema = schema;
1115
1141
  }
1116
1142
  if (isBufferingInitialSync() && !transactionStarted) {
1143
+ if (message.headers.operation === `update`) {
1144
+ const rowId = collection.getKeyFromItem(message.value);
1145
+ const snapshotRow = progressiveSnapshotRows.get(rowId);
1146
+ if (snapshotRow) {
1147
+ bufferedMessages.push(snapshotRow);
1148
+ progressiveSnapshotRows.delete(rowId);
1149
+ }
1150
+ }
1117
1151
  bufferedMessages.push(message);
1118
1152
  } else {
1119
1153
  if (!transactionStarted) {
1120
- begin();
1154
+ beginSourceTransaction();
1121
1155
  transactionStarted = true;
1122
1156
  }
1123
1157
  processChangeMessage(message);
@@ -1138,11 +1172,11 @@ function createElectricSync(shapeOptions, options) {
1138
1172
  } else {
1139
1173
  transactionStarted = processMoveOutEvent(
1140
1174
  message.headers.patterns,
1141
- begin,
1175
+ beginSourceTransaction,
1142
1176
  write,
1143
1177
  transactionStarted,
1144
1178
  (rowId) => {
1145
- pendingPresence.set(rowId, false);
1179
+ recordPendingPresence(rowId, false);
1146
1180
  syncedKeys.delete(rowId);
1147
1181
  }
1148
1182
  );
@@ -1159,20 +1193,20 @@ function createElectricSync(shapeOptions, options) {
1159
1193
  );
1160
1194
  commitResetResumeMetadataImmediately();
1161
1195
  if (!transactionStarted) {
1162
- begin();
1196
+ beginSourceTransaction();
1163
1197
  transactionStarted = true;
1164
1198
  }
1165
1199
  truncate();
1166
1200
  clearTagTrackingState();
1167
1201
  syncedKeys.clear();
1168
- pendingPresence.clear();
1169
- usesBaseline = false;
1202
+ recordPendingTruncate();
1170
1203
  isResettingSnapshot = true;
1171
1204
  resetGeneration++;
1172
1205
  loadSubsetDedupe?.reset();
1173
1206
  commitPoint = null;
1174
1207
  hasReceivedUpToDate = false;
1175
1208
  bufferedMessages.length = 0;
1209
+ progressiveSnapshotRows.clear();
1176
1210
  }
1177
1211
  }
1178
1212
  if (needsFullSnapshot && isResettingSnapshot && commitPoint === `subset-end`)
@@ -1186,7 +1220,7 @@ function createElectricSync(shapeOptions, options) {
1186
1220
  debug(
1187
1221
  `${collectionId ? `[${collectionId}] ` : ``}Progressive mode: Performing atomic swap with ${bufferedMessages.length} buffered messages`
1188
1222
  );
1189
- begin();
1223
+ beginSourceTransaction();
1190
1224
  truncate();
1191
1225
  clearTagTrackingState();
1192
1226
  syncedKeys.clear();
@@ -1209,7 +1243,7 @@ function createElectricSync(shapeOptions, options) {
1209
1243
  // normal-stream transactionStarted flag is still false.
1210
1244
  true,
1211
1245
  (rowId) => {
1212
- pendingPresence.set(rowId, false);
1246
+ recordPendingPresence(rowId, false);
1213
1247
  syncedKeys.delete(rowId);
1214
1248
  }
1215
1249
  );
@@ -1218,8 +1252,9 @@ function createElectricSync(shapeOptions, options) {
1218
1252
  }
1219
1253
  }
1220
1254
  stageResumeMetadata();
1221
- applied = commit();
1255
+ applied = commitSourceTransaction();
1222
1256
  bufferedMessages.length = 0;
1257
+ progressiveSnapshotRows.clear();
1223
1258
  debug(
1224
1259
  `${collectionId ? `[${collectionId}] ` : ``}Progressive mode: Atomic swap complete, now in normal sync mode`
1225
1260
  );
@@ -1228,12 +1263,12 @@ function createElectricSync(shapeOptions, options) {
1228
1263
  if (!isResettingSnapshot || finishesReset) {
1229
1264
  stageResumeMetadata();
1230
1265
  }
1231
- applied = commit();
1266
+ applied = commitSourceTransaction();
1232
1267
  transactionStarted = false;
1233
1268
  } else if (commitPoint === `up-to-date` && metadata) {
1234
- begin();
1269
+ beginSourceTransaction();
1235
1270
  stageResumeMetadata();
1236
- applied = commit();
1271
+ applied = commitSourceTransaction();
1237
1272
  }
1238
1273
  }
1239
1274
  const readyErrorVersion = streamErrorVersion;
@@ -1242,7 +1277,15 @@ function createElectricSync(shapeOptions, options) {
1242
1277
  } else {
1243
1278
  void applied.then(
1244
1279
  () => wrappedMarkReady(wasBufferingInitialSync, readyErrorVersion),
1245
- () => void 0
1280
+ (error) => {
1281
+ if (!isActiveLifecycle() || abortController.signal.aborted) {
1282
+ return;
1283
+ }
1284
+ streamErrorVersion++;
1285
+ unsubscribeStream();
1286
+ abortController.abort();
1287
+ if (collection.status !== `error`) markError(error);
1288
+ }
1246
1289
  );
1247
1290
  }
1248
1291
  if (finishesReset) {