@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/cjs/electric.cjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
246
|
-
|
|
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
|
|
299
|
+
await requestSnapshotInOrder(snapshotParams);
|
|
252
300
|
}
|
|
253
301
|
} catch (error) {
|
|
254
|
-
if (opts.signal?.aborted)
|
|
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
|
-
|
|
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
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
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
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
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
|
-
|
|
826
|
-
|
|
827
|
-
|
|
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
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
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
|
-
|
|
865
|
-
|
|
866
|
-
}
|
|
929
|
+
} else {
|
|
930
|
+
markReady();
|
|
867
931
|
}
|
|
868
|
-
|
|
869
|
-
|
|
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
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
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 (
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
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
|
-
|
|
896
|
-
|
|
897
|
-
}
|
|
898
|
-
return;
|
|
1002
|
+
errorParams
|
|
1003
|
+
);
|
|
899
1004
|
}
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
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
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
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
|
-
|
|
1081
|
+
lifecycle.resumeState = resetState;
|
|
1082
|
+
if (metadata) {
|
|
939
1083
|
begin();
|
|
940
|
-
metadata
|
|
941
|
-
|
|
942
|
-
|
|
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
|
-
|
|
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
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
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
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
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
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
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
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
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
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
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 (
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
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
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1275
|
+
bufferedMessages.push(message);
|
|
1276
|
+
} else {
|
|
1277
|
+
if (!transactionStarted) {
|
|
1278
|
+
beginSourceTransaction();
|
|
1279
|
+
transactionStarted = true;
|
|
1138
1280
|
}
|
|
1281
|
+
processChangeMessage(message);
|
|
1139
1282
|
}
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
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
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
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
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
if (
|
|
1223
|
-
|
|
1224
|
-
|
|
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
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
)
|
|
1300
|
-
|
|
1301
|
-
|
|
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
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
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
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
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
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
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
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
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
|
}
|