@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.
- package/dist/cjs/electric.cjs +819 -573
- package/dist/cjs/electric.cjs.map +1 -1
- package/dist/cjs/electric.d.cts +2 -0
- package/dist/esm/electric.d.ts +2 -0
- package/dist/esm/electric.js +820 -574
- package/dist/esm/electric.js.map +1 -1
- package/package.json +2 -2
- package/src/electric.ts +1155 -805
package/dist/esm/electric.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
245
|
-
|
|
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
|
|
298
|
+
await requestSnapshotInOrder(snapshotParams);
|
|
251
299
|
}
|
|
252
300
|
} catch (error) {
|
|
253
|
-
if (opts.signal?.aborted)
|
|
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
|
-
|
|
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
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
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
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
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
|
-
|
|
825
|
-
|
|
826
|
-
|
|
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
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
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
|
-
|
|
864
|
-
|
|
865
|
-
}
|
|
928
|
+
} else {
|
|
929
|
+
markReady();
|
|
866
930
|
}
|
|
867
|
-
|
|
868
|
-
|
|
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
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
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 (
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
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
|
-
|
|
895
|
-
|
|
896
|
-
}
|
|
897
|
-
return;
|
|
1001
|
+
errorParams
|
|
1002
|
+
);
|
|
898
1003
|
}
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
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
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
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
|
-
|
|
1080
|
+
lifecycle.resumeState = resetState;
|
|
1081
|
+
if (metadata) {
|
|
938
1082
|
begin();
|
|
939
|
-
metadata
|
|
940
|
-
|
|
941
|
-
|
|
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
|
-
|
|
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
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
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
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
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
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
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
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
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
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
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
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
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
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
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 (
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
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
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1274
|
+
bufferedMessages.push(message);
|
|
1275
|
+
} else {
|
|
1276
|
+
if (!transactionStarted) {
|
|
1277
|
+
beginSourceTransaction();
|
|
1278
|
+
transactionStarted = true;
|
|
1137
1279
|
}
|
|
1280
|
+
processChangeMessage(message);
|
|
1138
1281
|
}
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
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
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
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
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
if (
|
|
1222
|
-
|
|
1223
|
-
|
|
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
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
)
|
|
1299
|
-
|
|
1300
|
-
|
|
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
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
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
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
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
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
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
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
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
|
}
|