@korajs/sync 1.0.0-beta.10 → 1.0.0-beta.12
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/index.cjs +389 -36
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +39 -2
- package/dist/index.d.ts +39 -2
- package/dist/index.js +388 -36
- package/dist/index.js.map +1 -1
- package/dist/internal.d.cts +1 -1
- package/dist/internal.d.ts +1 -1
- package/dist/{transport-B2GQerqJ.d.cts → transport-CJvRO5ix.d.cts} +84 -5
- package/dist/{transport-B2GQerqJ.d.ts → transport-CJvRO5ix.d.ts} +84 -5
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -86,6 +86,7 @@ __export(index_exports, {
|
|
|
86
86
|
isYjsDocUpdateMessage: () => isYjsDocUpdateMessage,
|
|
87
87
|
operationMatchesQuerySubsets: () => operationMatchesQuerySubsets,
|
|
88
88
|
operationMatchesScope: () => operationMatchesScope,
|
|
89
|
+
querySubsetContains: () => querySubsetContains,
|
|
89
90
|
richtextDocKey: () => richtextDocKey,
|
|
90
91
|
sliceOperationsAfterCursor: () => sliceOperationsAfterCursor,
|
|
91
92
|
subscribeRemoteAwarenessStates: () => subscribeRemoteAwarenessStates,
|
|
@@ -112,7 +113,8 @@ var SYNC_STATUSES = [
|
|
|
112
113
|
"offline",
|
|
113
114
|
"clock-error",
|
|
114
115
|
"error",
|
|
115
|
-
"schema-mismatch"
|
|
116
|
+
"schema-mismatch",
|
|
117
|
+
"auth-required"
|
|
116
118
|
];
|
|
117
119
|
var MemoryRejectedOperationStorage = class {
|
|
118
120
|
rejected = /* @__PURE__ */ new Map();
|
|
@@ -160,12 +162,19 @@ function operationMatchesScope(op, scopeMap, fullRecord) {
|
|
|
160
162
|
const snapshot = buildSnapshot(op, fullRecord ?? void 0);
|
|
161
163
|
if (!snapshot) return false;
|
|
162
164
|
for (const [field, expected] of Object.entries(collectionScope)) {
|
|
163
|
-
if (snapshot[field]
|
|
165
|
+
if (!matchesPredicate(snapshot[field], expected)) {
|
|
164
166
|
return false;
|
|
165
167
|
}
|
|
166
168
|
}
|
|
167
169
|
return true;
|
|
168
170
|
}
|
|
171
|
+
function matchesPredicate(actual, expected) {
|
|
172
|
+
if (expected && typeof expected === "object" && !Array.isArray(expected) && "$in" in expected) {
|
|
173
|
+
const values = expected.$in;
|
|
174
|
+
return Array.isArray(values) && values.some((value) => Object.is(actual, value));
|
|
175
|
+
}
|
|
176
|
+
return Object.is(actual, expected);
|
|
177
|
+
}
|
|
169
178
|
function filterOperationsByScope(operations, scopeMap) {
|
|
170
179
|
if (!scopeMap) return operations;
|
|
171
180
|
return operations.filter((op) => operationMatchesScope(op, scopeMap));
|
|
@@ -222,24 +231,77 @@ function operationMatchesQuerySubsets(op, subsets, fullRecord) {
|
|
|
222
231
|
if (collectionSubsets.length === 0) {
|
|
223
232
|
return true;
|
|
224
233
|
}
|
|
234
|
+
if (collectionSubsets.some((subset) => Object.keys(subset.where).length === 0)) {
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
225
237
|
const snapshot = buildSnapshot2(op, fullRecord);
|
|
226
238
|
if (!snapshot) {
|
|
227
239
|
return false;
|
|
228
240
|
}
|
|
229
241
|
return collectionSubsets.some((subset) => recordMatchesWhere(snapshot, subset.where));
|
|
230
242
|
}
|
|
243
|
+
function stableValue(value) {
|
|
244
|
+
if (Array.isArray(value)) return value.map(stableValue);
|
|
245
|
+
if (value && typeof value === "object") {
|
|
246
|
+
return Object.fromEntries(
|
|
247
|
+
Object.entries(value).filter(([, entry]) => entry !== void 0).sort(([a], [b]) => a.localeCompare(b)).map(([key, entry]) => [key, stableValue(entry)])
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
return value;
|
|
251
|
+
}
|
|
252
|
+
function whereContains(broad, narrow) {
|
|
253
|
+
return Object.entries(broad).every(
|
|
254
|
+
([field, expected]) => JSON.stringify(stableValue(narrow[field])) === JSON.stringify(stableValue(expected))
|
|
255
|
+
);
|
|
256
|
+
}
|
|
231
257
|
function dedupeQuerySubsets(subsets) {
|
|
232
|
-
const
|
|
258
|
+
const normalized = subsets.map((subset) => ({
|
|
259
|
+
collection: subset.collection,
|
|
260
|
+
where: stableValue(subset.where)
|
|
261
|
+
}));
|
|
233
262
|
const result = [];
|
|
234
|
-
for (const
|
|
235
|
-
|
|
236
|
-
|
|
263
|
+
for (const candidate of normalized) {
|
|
264
|
+
if (normalized.some(
|
|
265
|
+
(other) => other !== candidate && other.collection === candidate.collection && whereContains(other.where, candidate.where) && (Object.keys(other.where).length < Object.keys(candidate.where).length || JSON.stringify(other.where) === JSON.stringify(candidate.where))
|
|
266
|
+
)) {
|
|
267
|
+
const duplicateAlreadyKept = result.some(
|
|
268
|
+
(item) => item.collection === candidate.collection && JSON.stringify(item.where) === JSON.stringify(candidate.where)
|
|
269
|
+
);
|
|
270
|
+
if (duplicateAlreadyKept || normalized.some(
|
|
271
|
+
(other) => other !== candidate && other.collection === candidate.collection && Object.keys(other.where).length < Object.keys(candidate.where).length && whereContains(other.where, candidate.where)
|
|
272
|
+
))
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (!result.some(
|
|
276
|
+
(item) => item.collection === candidate.collection && JSON.stringify(item.where) === JSON.stringify(candidate.where)
|
|
277
|
+
))
|
|
278
|
+
result.push(candidate);
|
|
279
|
+
}
|
|
280
|
+
return result.sort(
|
|
281
|
+
(a, b) => `${a.collection}:${JSON.stringify(a.where)}`.localeCompare(
|
|
282
|
+
`${b.collection}:${JSON.stringify(b.where)}`
|
|
283
|
+
)
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
function querySubsetContains(broad, narrow) {
|
|
287
|
+
const collections = /* @__PURE__ */ new Set([
|
|
288
|
+
...broad.map((subset) => subset.collection),
|
|
289
|
+
...narrow.map((subset) => subset.collection)
|
|
290
|
+
]);
|
|
291
|
+
for (const collection of collections) {
|
|
292
|
+
const broadForCollection = broad.filter((subset) => subset.collection === collection);
|
|
293
|
+
const narrowForCollection = narrow.filter((subset) => subset.collection === collection);
|
|
294
|
+
if (narrowForCollection.length === 0) {
|
|
295
|
+
if (broadForCollection.length > 0) return false;
|
|
237
296
|
continue;
|
|
238
297
|
}
|
|
239
|
-
|
|
240
|
-
|
|
298
|
+
if (broadForCollection.length === 0) continue;
|
|
299
|
+
if (!narrowForCollection.every(
|
|
300
|
+
(candidate) => broadForCollection.some((other) => whereContains(other.where, candidate.where))
|
|
301
|
+
))
|
|
302
|
+
return false;
|
|
241
303
|
}
|
|
242
|
-
return
|
|
304
|
+
return true;
|
|
243
305
|
}
|
|
244
306
|
|
|
245
307
|
// src/delta/delta-cursor.ts
|
|
@@ -622,7 +684,8 @@ function toProtoEnvelope(message) {
|
|
|
622
684
|
schemaVersion: message.schemaVersion,
|
|
623
685
|
authToken: message.authToken,
|
|
624
686
|
supportedWireFormats: message.supportedWireFormats,
|
|
625
|
-
...message.lastDeliverySequence !== void 0 ? { lastDeliverySequence: message.lastDeliverySequence } : {}
|
|
687
|
+
...message.lastDeliverySequence !== void 0 ? { lastDeliverySequence: message.lastDeliverySequence } : {},
|
|
688
|
+
scopeExitPolicy: message.scopeExitPolicy
|
|
626
689
|
};
|
|
627
690
|
case "handshake-response":
|
|
628
691
|
return {
|
|
@@ -639,7 +702,10 @@ function toProtoEnvelope(message) {
|
|
|
639
702
|
selectedWireFormat: message.selectedWireFormat,
|
|
640
703
|
serverTime: message.serverTime,
|
|
641
704
|
blobStorageEnabled: message.blobStorageEnabled,
|
|
642
|
-
...message.serverMaxDeliverySequence !== void 0 ? { serverMaxDeliverySequence: message.serverMaxDeliverySequence } : {}
|
|
705
|
+
...message.serverMaxDeliverySequence !== void 0 ? { serverMaxDeliverySequence: message.serverMaxDeliverySequence } : {},
|
|
706
|
+
acceptedScopeJson: message.acceptedScope ? JSON.stringify(message.acceptedScope) : void 0,
|
|
707
|
+
acceptedDownlinkScopesJson: message.acceptedDownlinkScopes ? JSON.stringify(message.acceptedDownlinkScopes) : void 0,
|
|
708
|
+
acceptedUplinkScopesJson: message.acceptedUplinkScopes ? JSON.stringify(message.acceptedUplinkScopes) : void 0
|
|
643
709
|
};
|
|
644
710
|
case "operation-batch":
|
|
645
711
|
return {
|
|
@@ -649,7 +715,8 @@ function toProtoEnvelope(message) {
|
|
|
649
715
|
isFinal: message.isFinal,
|
|
650
716
|
batchIndex: message.batchIndex,
|
|
651
717
|
...message.baseDeliverySequence !== void 0 ? { baseDeliverySequence: message.baseDeliverySequence } : {},
|
|
652
|
-
...message.maxDeliverySequence !== void 0 ? { maxDeliverySequence: message.maxDeliverySequence } : {}
|
|
718
|
+
...message.maxDeliverySequence !== void 0 ? { maxDeliverySequence: message.maxDeliverySequence } : {},
|
|
719
|
+
retractionsJson: message.retractions ? JSON.stringify(message.retractions) : void 0
|
|
653
720
|
};
|
|
654
721
|
case "acknowledgment":
|
|
655
722
|
return {
|
|
@@ -723,7 +790,8 @@ function fromProtoEnvelope(envelope) {
|
|
|
723
790
|
supportedWireFormats: envelope.supportedWireFormats?.filter(
|
|
724
791
|
(format) => format === "json" || format === "protobuf"
|
|
725
792
|
),
|
|
726
|
-
...envelope.lastDeliverySequence !== void 0 ? { lastDeliverySequence: envelope.lastDeliverySequence } : {}
|
|
793
|
+
...envelope.lastDeliverySequence !== void 0 ? { lastDeliverySequence: envelope.lastDeliverySequence } : {},
|
|
794
|
+
...envelope.scopeExitPolicy === "retain" || envelope.scopeExitPolicy === "retract" ? { scopeExitPolicy: envelope.scopeExitPolicy } : {}
|
|
727
795
|
};
|
|
728
796
|
case "handshake-response":
|
|
729
797
|
return {
|
|
@@ -741,7 +809,10 @@ function fromProtoEnvelope(envelope) {
|
|
|
741
809
|
// automatic timestamp rebase work over the protobuf wire, not just JSON.
|
|
742
810
|
...envelope.serverTime !== void 0 ? { serverTime: envelope.serverTime } : {},
|
|
743
811
|
...envelope.blobStorageEnabled !== void 0 ? { blobStorageEnabled: envelope.blobStorageEnabled } : {},
|
|
744
|
-
...envelope.serverMaxDeliverySequence !== void 0 ? { serverMaxDeliverySequence: envelope.serverMaxDeliverySequence } : {}
|
|
812
|
+
...envelope.serverMaxDeliverySequence !== void 0 ? { serverMaxDeliverySequence: envelope.serverMaxDeliverySequence } : {},
|
|
813
|
+
...envelope.acceptedScopeJson ? { acceptedScope: JSON.parse(envelope.acceptedScopeJson) } : {},
|
|
814
|
+
...envelope.acceptedDownlinkScopesJson ? { acceptedDownlinkScopes: JSON.parse(envelope.acceptedDownlinkScopesJson) } : {},
|
|
815
|
+
...envelope.acceptedUplinkScopesJson ? { acceptedUplinkScopes: JSON.parse(envelope.acceptedUplinkScopesJson) } : {}
|
|
745
816
|
};
|
|
746
817
|
case "operation-batch":
|
|
747
818
|
return {
|
|
@@ -751,7 +822,8 @@ function fromProtoEnvelope(envelope) {
|
|
|
751
822
|
isFinal: envelope.isFinal ?? false,
|
|
752
823
|
batchIndex: envelope.batchIndex ?? 0,
|
|
753
824
|
...envelope.baseDeliverySequence !== void 0 ? { baseDeliverySequence: envelope.baseDeliverySequence } : {},
|
|
754
|
-
...envelope.maxDeliverySequence !== void 0 ? { maxDeliverySequence: envelope.maxDeliverySequence } : {}
|
|
825
|
+
...envelope.maxDeliverySequence !== void 0 ? { maxDeliverySequence: envelope.maxDeliverySequence } : {},
|
|
826
|
+
...envelope.retractionsJson ? { retractions: JSON.parse(envelope.retractionsJson) } : {}
|
|
755
827
|
};
|
|
756
828
|
case "acknowledgment":
|
|
757
829
|
return {
|
|
@@ -969,6 +1041,13 @@ function encodeEnvelope(envelope) {
|
|
|
969
1041
|
if (envelope.deliverySequence !== void 0) writer.uint32(256).int64(envelope.deliverySequence);
|
|
970
1042
|
if (envelope.serverMaxDeliverySequence !== void 0)
|
|
971
1043
|
writer.uint32(264).int64(envelope.serverMaxDeliverySequence);
|
|
1044
|
+
if (envelope.acceptedScopeJson) writer.uint32(274).string(envelope.acceptedScopeJson);
|
|
1045
|
+
if (envelope.acceptedDownlinkScopesJson)
|
|
1046
|
+
writer.uint32(282).string(envelope.acceptedDownlinkScopesJson);
|
|
1047
|
+
if (envelope.acceptedUplinkScopesJson)
|
|
1048
|
+
writer.uint32(290).string(envelope.acceptedUplinkScopesJson);
|
|
1049
|
+
if (envelope.retractionsJson) writer.uint32(298).string(envelope.retractionsJson);
|
|
1050
|
+
if (envelope.scopeExitPolicy) writer.uint32(306).string(envelope.scopeExitPolicy);
|
|
972
1051
|
return writer.finish();
|
|
973
1052
|
}
|
|
974
1053
|
function decodeEnvelope(bytes) {
|
|
@@ -1079,6 +1158,21 @@ function decodeEnvelope(bytes) {
|
|
|
1079
1158
|
case 33:
|
|
1080
1159
|
envelope.serverMaxDeliverySequence = longToNumber(reader.int64());
|
|
1081
1160
|
break;
|
|
1161
|
+
case 34:
|
|
1162
|
+
envelope.acceptedScopeJson = reader.string();
|
|
1163
|
+
break;
|
|
1164
|
+
case 35:
|
|
1165
|
+
envelope.acceptedDownlinkScopesJson = reader.string();
|
|
1166
|
+
break;
|
|
1167
|
+
case 36:
|
|
1168
|
+
envelope.acceptedUplinkScopesJson = reader.string();
|
|
1169
|
+
break;
|
|
1170
|
+
case 37:
|
|
1171
|
+
envelope.retractionsJson = reader.string();
|
|
1172
|
+
break;
|
|
1173
|
+
case 38:
|
|
1174
|
+
envelope.scopeExitPolicy = reader.string();
|
|
1175
|
+
break;
|
|
1082
1176
|
default:
|
|
1083
1177
|
reader.skipType(tag & 7);
|
|
1084
1178
|
}
|
|
@@ -2744,6 +2838,18 @@ var OutboundQueue = class {
|
|
|
2744
2838
|
await this.storage.dequeue([opId]);
|
|
2745
2839
|
return removed;
|
|
2746
2840
|
}
|
|
2841
|
+
/** Remove every pending/in-flight operation for a record so authorization loss cannot upload it. */
|
|
2842
|
+
async rejectRecord(collection, recordId) {
|
|
2843
|
+
const matching = [...this.queue, ...[...this.inFlight.values()].flat()].filter(
|
|
2844
|
+
(op) => op.collection === collection && op.recordId === recordId
|
|
2845
|
+
);
|
|
2846
|
+
const removed = [];
|
|
2847
|
+
for (const operation of matching) {
|
|
2848
|
+
const rejected = await this.reject(operation.id);
|
|
2849
|
+
if (rejected) removed.push(rejected);
|
|
2850
|
+
}
|
|
2851
|
+
return removed;
|
|
2852
|
+
}
|
|
2747
2853
|
/**
|
|
2748
2854
|
* Remove operations by id from queue and persistent storage.
|
|
2749
2855
|
* Used when ops were already sent during handshake delta exchange.
|
|
@@ -2763,6 +2869,8 @@ var OutboundQueue = class {
|
|
|
2763
2869
|
var DEFAULT_BATCH_SIZE = 100;
|
|
2764
2870
|
var DEFAULT_SCHEMA_VERSION = 1;
|
|
2765
2871
|
var DEFAULT_OUTBOUND_ACK_TIMEOUT_MS = 3e4;
|
|
2872
|
+
var DEFAULT_OUTBOUND_RETRY_BASE_DELAY_MS = 250;
|
|
2873
|
+
var DEFAULT_OUTBOUND_RETRY_MAX_DELAY_MS = 3e4;
|
|
2766
2874
|
var VALID_TRANSITIONS = {
|
|
2767
2875
|
disconnected: ["connecting"],
|
|
2768
2876
|
connecting: ["handshaking", "error", "disconnected"],
|
|
@@ -2798,6 +2906,8 @@ var SyncEngine = class {
|
|
|
2798
2906
|
rejectedStorage;
|
|
2799
2907
|
batchSize;
|
|
2800
2908
|
outboundAckTimeoutMs;
|
|
2909
|
+
outboundRetryBaseDelayMs;
|
|
2910
|
+
outboundRetryMaxDelayMs;
|
|
2801
2911
|
encryptor;
|
|
2802
2912
|
awarenessManager;
|
|
2803
2913
|
richtextDocChannel;
|
|
@@ -2813,11 +2923,19 @@ var SyncEngine = class {
|
|
|
2813
2923
|
conflictCount = 0;
|
|
2814
2924
|
currentBatch = null;
|
|
2815
2925
|
currentBatchRequiresPartialAck = false;
|
|
2926
|
+
currentBatchRequiresRetryBackoff = false;
|
|
2927
|
+
outboundRetryAttempt = 0;
|
|
2928
|
+
outboundRetryTimer = null;
|
|
2816
2929
|
reconnecting = false;
|
|
2817
2930
|
schemaBlocked = false;
|
|
2818
2931
|
clockBlocked = false;
|
|
2819
2932
|
clockSkewMs = null;
|
|
2820
2933
|
blobStorageEnabled = false;
|
|
2934
|
+
suspensionReason = null;
|
|
2935
|
+
authRejected = false;
|
|
2936
|
+
serverFrontier = null;
|
|
2937
|
+
hasInFlightDeliveryBatch = false;
|
|
2938
|
+
blockedFailure = null;
|
|
2821
2939
|
outboundAckTimer = null;
|
|
2822
2940
|
startPromise = null;
|
|
2823
2941
|
stopPromise = null;
|
|
@@ -2836,8 +2954,12 @@ var SyncEngine = class {
|
|
|
2836
2954
|
* with the server-accepted scope (server is authoritative).
|
|
2837
2955
|
*/
|
|
2838
2956
|
activeScope;
|
|
2957
|
+
/** Server-authoritative upload authorization, separate from the downloaded view. */
|
|
2958
|
+
activeUplinkScope;
|
|
2959
|
+
hasDirectionalScopes = false;
|
|
2839
2960
|
/** Live query subsets registered from reactive subscriptions */
|
|
2840
2961
|
querySubsets = /* @__PURE__ */ new Map();
|
|
2962
|
+
staticQuerySubsets = [];
|
|
2841
2963
|
querySubsetReconnectTimer = null;
|
|
2842
2964
|
/** Resume cursor for paginated initial sync (persisted across reconnects) */
|
|
2843
2965
|
resumeDeltaCursor = null;
|
|
@@ -2871,9 +2993,18 @@ var SyncEngine = class {
|
|
|
2871
2993
|
this.emitter = options.emitter ?? null;
|
|
2872
2994
|
this.batchSize = options.config.batchSize ?? DEFAULT_BATCH_SIZE;
|
|
2873
2995
|
this.outboundAckTimeoutMs = options.config.outboundAckTimeoutMs ?? DEFAULT_OUTBOUND_ACK_TIMEOUT_MS;
|
|
2996
|
+
this.outboundRetryBaseDelayMs = Math.max(
|
|
2997
|
+
0,
|
|
2998
|
+
options.config.outboundRetryBaseDelayMs ?? DEFAULT_OUTBOUND_RETRY_BASE_DELAY_MS
|
|
2999
|
+
);
|
|
3000
|
+
this.outboundRetryMaxDelayMs = Math.max(
|
|
3001
|
+
this.outboundRetryBaseDelayMs,
|
|
3002
|
+
options.config.outboundRetryMaxDelayMs ?? DEFAULT_OUTBOUND_RETRY_MAX_DELAY_MS
|
|
3003
|
+
);
|
|
2874
3004
|
this.encryptor = options.encryptor ?? null;
|
|
2875
3005
|
this.syncState = options.syncState ?? null;
|
|
2876
3006
|
this.activeScope = options.config.scopeMap;
|
|
3007
|
+
this.activeUplinkScope = options.config.scopeMap;
|
|
2877
3008
|
const queueStorage = options.queueStorage ?? new MemoryQueueStorage();
|
|
2878
3009
|
this.outboundQueue = new OutboundQueue(queueStorage);
|
|
2879
3010
|
this.rejectedStorage = options.rejectedStorage ?? new MemoryRejectedOperationStorage();
|
|
@@ -2931,6 +3062,22 @@ var SyncEngine = class {
|
|
|
2931
3062
|
if (this.state !== "disconnected") {
|
|
2932
3063
|
await this.stop();
|
|
2933
3064
|
}
|
|
3065
|
+
if (this.authRejected) {
|
|
3066
|
+
this.emitter?.emit({
|
|
3067
|
+
type: "sync:suspended",
|
|
3068
|
+
reason: this.suspensionReason ?? "auth-rejected"
|
|
3069
|
+
});
|
|
3070
|
+
return;
|
|
3071
|
+
}
|
|
3072
|
+
if (this.config.authState) {
|
|
3073
|
+
const authState = await this.config.authState();
|
|
3074
|
+
if (authState.state === "loading" || authState.state === "signed-out") {
|
|
3075
|
+
this.suspensionReason = authState.state === "loading" ? "auth-loading" : "auth-required";
|
|
3076
|
+
this.emitter?.emit({ type: "sync:suspended", reason: this.suspensionReason });
|
|
3077
|
+
return;
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
3080
|
+
this.suspensionReason = null;
|
|
2934
3081
|
await this.outboundQueue.initialize();
|
|
2935
3082
|
if (this.syncState) {
|
|
2936
3083
|
this.lastAckedServerVector = await this.syncState.loadLastAckedServerVector();
|
|
@@ -2980,6 +3127,7 @@ var SyncEngine = class {
|
|
|
2980
3127
|
authToken,
|
|
2981
3128
|
supportedWireFormats: ["json", "protobuf"],
|
|
2982
3129
|
...this.config.scopeMap ? { syncScope: this.config.scopeMap } : {},
|
|
3130
|
+
...this.config.scopeExit ? { scopeExitPolicy: this.config.scopeExit } : {},
|
|
2983
3131
|
...activeQuerySubsets.length > 0 ? { syncQueries: activeQuerySubsets } : {},
|
|
2984
3132
|
...this.resumeDeltaCursor ? { deltaCursor: encodeDeltaCursor(this.resumeDeltaCursor) } : {},
|
|
2985
3133
|
// Resume the server's gap-free delivery stream from our watermark. A server
|
|
@@ -3004,13 +3152,15 @@ var SyncEngine = class {
|
|
|
3004
3152
|
return this.stopPromise;
|
|
3005
3153
|
}
|
|
3006
3154
|
async stopInternal() {
|
|
3007
|
-
if (this.state === "disconnected") return;
|
|
3008
3155
|
this.clearOutboundAckTimer();
|
|
3156
|
+
this.clearOutboundRetryTimer();
|
|
3157
|
+
if (this.state === "disconnected") return;
|
|
3009
3158
|
this.awarenessManager.stopCleanupTimer();
|
|
3010
3159
|
if (this.currentBatch) {
|
|
3011
3160
|
this.outboundQueue.returnBatch(this.currentBatch.batchId);
|
|
3012
3161
|
this.currentBatch = null;
|
|
3013
3162
|
this.currentBatchRequiresPartialAck = false;
|
|
3163
|
+
this.currentBatchRequiresRetryBackoff = false;
|
|
3014
3164
|
}
|
|
3015
3165
|
try {
|
|
3016
3166
|
await this.transport.disconnect();
|
|
@@ -3060,14 +3210,30 @@ var SyncEngine = class {
|
|
|
3060
3210
|
getStatus() {
|
|
3061
3211
|
const pendingOperations = this.syncState ? this.cachedUnsyncedCount : this.outboundQueue.totalPending;
|
|
3062
3212
|
const base = {
|
|
3213
|
+
phase: this.resolvePhase(),
|
|
3214
|
+
...this.suspensionReason ? { reason: this.suspensionReason } : {},
|
|
3063
3215
|
reconnecting: this.reconnecting,
|
|
3064
3216
|
pendingOperations,
|
|
3065
3217
|
lastSyncedAt: this.lastSyncedAt,
|
|
3066
3218
|
lastSuccessfulPush: this.lastSuccessfulPush,
|
|
3067
3219
|
lastSuccessfulPull: this.lastSuccessfulPull,
|
|
3068
3220
|
conflicts: this.conflictCount,
|
|
3069
|
-
clockSkewMs: this.clockSkewMs
|
|
3221
|
+
clockSkewMs: this.clockSkewMs,
|
|
3222
|
+
inFlightUploadOperations: this.currentBatch?.operations.length ?? 0,
|
|
3223
|
+
hasInFlightDeliveryBatch: this.hasInFlightDeliveryBatch,
|
|
3224
|
+
activeViewId: this.deliverySignature(),
|
|
3225
|
+
activeViewComplete: this.state === "streaming" && !this.hasInFlightDeliveryBatch && this.blockedFailure === null && (this.serverFrontier === null || this.deliveryWatermark >= this.serverFrontier),
|
|
3226
|
+
initialSync: {
|
|
3227
|
+
complete: this.deltaReceiveComplete && this.deltaSendComplete,
|
|
3228
|
+
receivedBatches: this.deltaBatchesReceived,
|
|
3229
|
+
totalBatches: this.initialSyncTotalBatches || null,
|
|
3230
|
+
progress: this.initialSyncTotalBatches > 0 ? Math.min(1, this.deltaBatchesReceived / this.initialSyncTotalBatches) : null
|
|
3231
|
+
},
|
|
3232
|
+
deliveryWatermark: this.deliveryWatermark,
|
|
3233
|
+
serverFrontier: this.serverFrontier,
|
|
3234
|
+
blockedFailure: this.blockedFailure
|
|
3070
3235
|
};
|
|
3236
|
+
if (this.suspensionReason) return { ...base, status: "auth-required" };
|
|
3071
3237
|
switch (this.state) {
|
|
3072
3238
|
case "disconnected":
|
|
3073
3239
|
if (this.clockBlocked) {
|
|
@@ -3090,6 +3256,26 @@ var SyncEngine = class {
|
|
|
3090
3256
|
return { ...base, status: this.schemaBlocked ? "schema-mismatch" : "error" };
|
|
3091
3257
|
}
|
|
3092
3258
|
}
|
|
3259
|
+
resolvePhase() {
|
|
3260
|
+
if (this.suspensionReason) return "suspended";
|
|
3261
|
+
if (this.blockedFailure || this.schemaBlocked || this.clockBlocked) return "blocked";
|
|
3262
|
+
if (this.hasInFlightDeliveryBatch) return "applying";
|
|
3263
|
+
if (this.currentBatch) return "uploading";
|
|
3264
|
+
switch (this.state) {
|
|
3265
|
+
case "disconnected":
|
|
3266
|
+
return "offline";
|
|
3267
|
+
case "connecting":
|
|
3268
|
+
return "connecting";
|
|
3269
|
+
case "handshaking":
|
|
3270
|
+
return "handshaking";
|
|
3271
|
+
case "syncing":
|
|
3272
|
+
return "receiving";
|
|
3273
|
+
case "streaming":
|
|
3274
|
+
return "streaming";
|
|
3275
|
+
case "error":
|
|
3276
|
+
return "blocked";
|
|
3277
|
+
}
|
|
3278
|
+
}
|
|
3093
3279
|
/**
|
|
3094
3280
|
* True when the server rejected the client's schema version at handshake.
|
|
3095
3281
|
* Sync stays blocked until the app schema is upgraded or sync config changes.
|
|
@@ -3142,8 +3328,25 @@ var SyncEngine = class {
|
|
|
3142
3328
|
await this.refreshPendingCount();
|
|
3143
3329
|
return this.getStatus().pendingOperations;
|
|
3144
3330
|
}
|
|
3145
|
-
emitApplyFailure(op, result, overrides) {
|
|
3331
|
+
emitApplyFailure(op, result, overrides, blocking = false) {
|
|
3146
3332
|
const reason = (0, import_core7.defaultApplyFailureReason)(result, overrides);
|
|
3333
|
+
if (blocking || reason.retriable) {
|
|
3334
|
+
const prior = this.blockedFailure;
|
|
3335
|
+
this.blockedFailure = {
|
|
3336
|
+
operationId: op.id,
|
|
3337
|
+
collection: op.collection,
|
|
3338
|
+
recordId: op.recordId,
|
|
3339
|
+
code: reason.code,
|
|
3340
|
+
message: reason.message,
|
|
3341
|
+
retriable: reason.retriable,
|
|
3342
|
+
firstSeenAt: prior?.operationId === op.id ? prior.firstSeenAt : Date.now(),
|
|
3343
|
+
retryCount: prior?.operationId === op.id ? prior.retryCount + 1 : 0
|
|
3344
|
+
};
|
|
3345
|
+
this.emitter?.emit({
|
|
3346
|
+
type: prior?.operationId === op.id ? "sync:apply-retrying" : "sync:apply-blocked",
|
|
3347
|
+
failure: this.blockedFailure
|
|
3348
|
+
});
|
|
3349
|
+
}
|
|
3147
3350
|
this.emitter?.emit({
|
|
3148
3351
|
type: "sync:apply-failed",
|
|
3149
3352
|
operationId: op.id,
|
|
@@ -3160,11 +3363,18 @@ var SyncEngine = class {
|
|
|
3160
3363
|
*/
|
|
3161
3364
|
async retryNow() {
|
|
3162
3365
|
if (this.schemaBlocked) return;
|
|
3366
|
+
this.authRejected = false;
|
|
3367
|
+
this.suspensionReason = null;
|
|
3163
3368
|
if (this.state === "disconnected" || this.state === "error") {
|
|
3164
3369
|
this.reconnecting = false;
|
|
3165
3370
|
await this.start();
|
|
3166
3371
|
}
|
|
3167
3372
|
}
|
|
3373
|
+
/** Wake a permanent auth suspension after the binding reports a new auth state. */
|
|
3374
|
+
notifyAuthChanged() {
|
|
3375
|
+
this.authRejected = false;
|
|
3376
|
+
this.suspensionReason = null;
|
|
3377
|
+
}
|
|
3168
3378
|
/**
|
|
3169
3379
|
* Atomically refresh the transport session. Overlapping callers share one
|
|
3170
3380
|
* reconnect, in-flight outbound batches are returned to the queue by stop(), and the
|
|
@@ -3233,6 +3443,8 @@ var SyncEngine = class {
|
|
|
3233
3443
|
updateScope(scopeMap) {
|
|
3234
3444
|
const previousSignature = this.deliverySignature();
|
|
3235
3445
|
this.activeScope = scopeMap;
|
|
3446
|
+
this.activeUplinkScope = scopeMap;
|
|
3447
|
+
this.hasDirectionalScopes = false;
|
|
3236
3448
|
this.config.scopeMap = scopeMap;
|
|
3237
3449
|
this.switchDeliveryView(previousSignature);
|
|
3238
3450
|
}
|
|
@@ -3316,28 +3528,58 @@ var SyncEngine = class {
|
|
|
3316
3528
|
getActiveScope() {
|
|
3317
3529
|
return this.activeScope;
|
|
3318
3530
|
}
|
|
3531
|
+
/** Get the server-accepted upload authorization scope. */
|
|
3532
|
+
getActiveUplinkScope() {
|
|
3533
|
+
return this.activeUplinkScope;
|
|
3534
|
+
}
|
|
3319
3535
|
/**
|
|
3320
3536
|
* Register a live query subset that narrows synced data for a collection.
|
|
3321
3537
|
* Takes effect on the next connection; reconnects when already connected.
|
|
3322
3538
|
*/
|
|
3323
3539
|
registerQuerySubset(subset) {
|
|
3540
|
+
if ((this.config.querySubsets?.mode ?? "reactive") !== "reactive") return () => {
|
|
3541
|
+
};
|
|
3324
3542
|
const id = `query-${nextQuerySubsetId++}`;
|
|
3325
3543
|
const previousSignature = this.deliverySignature();
|
|
3544
|
+
const previousEffective = this.getActiveQuerySubsets();
|
|
3326
3545
|
this.querySubsets.set(id, subset);
|
|
3327
|
-
this.
|
|
3328
|
-
this.scheduleQuerySubsetReconnect();
|
|
3546
|
+
this.switchDeliveryViewWithInheritance(previousSignature, previousEffective);
|
|
3547
|
+
if (this.deliverySignature() !== previousSignature) this.scheduleQuerySubsetReconnect();
|
|
3329
3548
|
return () => {
|
|
3330
3549
|
const sigBeforeRemove = this.deliverySignature();
|
|
3550
|
+
const effectiveBeforeRemove = this.getActiveQuerySubsets();
|
|
3331
3551
|
this.querySubsets.delete(id);
|
|
3332
|
-
this.
|
|
3333
|
-
this.scheduleQuerySubsetReconnect();
|
|
3552
|
+
this.switchDeliveryViewWithInheritance(sigBeforeRemove, effectiveBeforeRemove);
|
|
3553
|
+
if (this.deliverySignature() !== sigBeforeRemove) this.scheduleQuerySubsetReconnect();
|
|
3334
3554
|
};
|
|
3335
3555
|
}
|
|
3556
|
+
switchDeliveryViewWithInheritance(previousSignature, previousSubsets) {
|
|
3557
|
+
const nextSubsets = this.getActiveQuerySubsets();
|
|
3558
|
+
const previousWatermark = this.deliveryWatermark;
|
|
3559
|
+
this.switchDeliveryView(previousSignature);
|
|
3560
|
+
if (this.deliveryWatermark === 0 && querySubsetContains(previousSubsets, nextSubsets)) {
|
|
3561
|
+
this.deliveryWatermark = previousWatermark;
|
|
3562
|
+
this.setViewWatermark(this.deliverySignature(), previousWatermark);
|
|
3563
|
+
void this.persistDeliveryWatermark(previousWatermark);
|
|
3564
|
+
}
|
|
3565
|
+
}
|
|
3566
|
+
/** Atomically replace the manifest used by static query-subset mode. */
|
|
3567
|
+
setQuerySubsets(subsets) {
|
|
3568
|
+
if ((this.config.querySubsets?.mode ?? "reactive") !== "static")
|
|
3569
|
+
throw new Error('setQuerySubsets() requires sync.querySubsets.mode = "static"');
|
|
3570
|
+
const previousSignature = this.deliverySignature();
|
|
3571
|
+
const previousEffective = this.getActiveQuerySubsets();
|
|
3572
|
+
this.staticQuerySubsets = dedupeQuerySubsets(subsets);
|
|
3573
|
+
this.switchDeliveryViewWithInheritance(previousSignature, previousEffective);
|
|
3574
|
+
if (this.deliverySignature() !== previousSignature) this.scheduleQuerySubsetReconnect();
|
|
3575
|
+
}
|
|
3336
3576
|
/**
|
|
3337
3577
|
* Returns deduplicated active query subsets from registered subscriptions.
|
|
3338
3578
|
*/
|
|
3339
3579
|
getActiveQuerySubsets() {
|
|
3340
|
-
|
|
3580
|
+
const mode = this.config.querySubsets?.mode ?? "reactive";
|
|
3581
|
+
if (mode === "disabled") return [];
|
|
3582
|
+
return mode === "static" ? this.staticQuerySubsets : dedupeQuerySubsets([...this.querySubsets.values()]);
|
|
3341
3583
|
}
|
|
3342
3584
|
/**
|
|
3343
3585
|
* Get the awareness manager for collaborative presence.
|
|
@@ -3414,6 +3656,7 @@ var SyncEngine = class {
|
|
|
3414
3656
|
}
|
|
3415
3657
|
}
|
|
3416
3658
|
handleMessageFailure(error) {
|
|
3659
|
+
this.hasInFlightDeliveryBatch = false;
|
|
3417
3660
|
const reason = error instanceof Error ? error.message : "Message handling failed";
|
|
3418
3661
|
this.handleTransportClose(reason);
|
|
3419
3662
|
}
|
|
@@ -3487,11 +3730,21 @@ var SyncEngine = class {
|
|
|
3487
3730
|
this.setViewWatermark(this.deliverySignature(), 0);
|
|
3488
3731
|
await this.persistDeliveryWatermark(0);
|
|
3489
3732
|
}
|
|
3733
|
+
this.serverFrontier = msg.serverMaxDeliverySequence ?? null;
|
|
3490
3734
|
if (msg.selectedWireFormat) {
|
|
3491
3735
|
this.setSerializerWireFormat(msg.selectedWireFormat);
|
|
3492
3736
|
}
|
|
3493
|
-
if (msg.acceptedScope) {
|
|
3494
|
-
this.activeScope = msg.acceptedScope;
|
|
3737
|
+
if (msg.acceptedDownlinkScopes ?? msg.acceptedScope) {
|
|
3738
|
+
this.activeScope = msg.acceptedDownlinkScopes ?? msg.acceptedScope;
|
|
3739
|
+
}
|
|
3740
|
+
this.hasDirectionalScopes = msg.acceptedUplinkScopes !== void 0;
|
|
3741
|
+
this.activeUplinkScope = msg.acceptedUplinkScopes ?? msg.acceptedScope ?? this.activeScope;
|
|
3742
|
+
if (this.config.scopeExit === "retract" && this.activeScope && this.store.applyScopeNarrowing) {
|
|
3743
|
+
const narrowed = await this.store.applyScopeNarrowing(this.activeScope);
|
|
3744
|
+
for (const retraction of narrowed) {
|
|
3745
|
+
await this.applyScopeRetraction(retraction, false);
|
|
3746
|
+
}
|
|
3747
|
+
await this.refreshPendingCount();
|
|
3495
3748
|
}
|
|
3496
3749
|
this.emitter?.emit({ type: "sync:connected", nodeId: this.store.getNodeId() });
|
|
3497
3750
|
this.metricsCollector.recordConnected();
|
|
@@ -3648,6 +3901,7 @@ var SyncEngine = class {
|
|
|
3648
3901
|
this.sendDeliveryAck(msg.messageId, this.deliveryWatermark);
|
|
3649
3902
|
return;
|
|
3650
3903
|
}
|
|
3904
|
+
this.hasInFlightDeliveryBatch = true;
|
|
3651
3905
|
}
|
|
3652
3906
|
const deserialized = msg.operations.map((s) => this.serializer.decodeOperation(s));
|
|
3653
3907
|
const operations = this.encryptor ? await this.encryptor.decryptBatch(deserialized) : deserialized;
|
|
@@ -3655,6 +3909,14 @@ var SyncEngine = class {
|
|
|
3655
3909
|
const targetSchemaVersion = this.config.schemaVersion ?? DEFAULT_SCHEMA_VERSION;
|
|
3656
3910
|
const transforms = this.config.operationTransforms ?? [];
|
|
3657
3911
|
let fullyApplied = true;
|
|
3912
|
+
for (const retraction of msg.retractions ?? []) {
|
|
3913
|
+
try {
|
|
3914
|
+
await this.applyScopeRetraction(retraction, true);
|
|
3915
|
+
} catch {
|
|
3916
|
+
fullyApplied = false;
|
|
3917
|
+
}
|
|
3918
|
+
}
|
|
3919
|
+
await this.refreshPendingCount();
|
|
3658
3920
|
for (const op of inScopeOps) {
|
|
3659
3921
|
const transformed = transforms.length > 0 ? (0, import_core7.applyOperationTransforms)(op, targetSchemaVersion, transforms) : op;
|
|
3660
3922
|
if (transformed === null) {
|
|
@@ -3668,17 +3930,22 @@ var SyncEngine = class {
|
|
|
3668
3930
|
} catch (error) {
|
|
3669
3931
|
fullyApplied = false;
|
|
3670
3932
|
if (error instanceof import_core7.ClockDriftError) {
|
|
3671
|
-
this.emitApplyFailure(
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3933
|
+
this.emitApplyFailure(
|
|
3934
|
+
transformed,
|
|
3935
|
+
"rejected",
|
|
3936
|
+
{
|
|
3937
|
+
code: import_core7.APPLY_FAILURE_CODES.CLOCK_DRIFT,
|
|
3938
|
+
message: error.message,
|
|
3939
|
+
retriable: false
|
|
3940
|
+
},
|
|
3941
|
+
true
|
|
3942
|
+
);
|
|
3676
3943
|
continue;
|
|
3677
3944
|
}
|
|
3678
3945
|
const message = error instanceof Error ? error.message : "Apply failed";
|
|
3679
3946
|
const code = error instanceof import_core7.SyncError ? error.code : error instanceof import_core7.KoraError ? error.code : error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : import_core7.APPLY_FAILURE_CODES.APPLY_FAILED;
|
|
3680
3947
|
const retriable = code !== import_core7.APPLY_FAILURE_CODES.REFERENTIAL_INTEGRITY;
|
|
3681
|
-
this.emitApplyFailure(transformed, "rejected", { code, message, retriable });
|
|
3948
|
+
this.emitApplyFailure(transformed, "rejected", { code, message, retriable }, true);
|
|
3682
3949
|
}
|
|
3683
3950
|
}
|
|
3684
3951
|
if (inScopeOps.length > 0) {
|
|
@@ -3711,7 +3978,13 @@ var SyncEngine = class {
|
|
|
3711
3978
|
this.deliveryWatermark = msg.maxDeliverySequence;
|
|
3712
3979
|
this.setViewWatermark(this.deliverySignature(), this.deliveryWatermark);
|
|
3713
3980
|
await this.persistDeliveryWatermark(this.deliveryWatermark);
|
|
3981
|
+
if (this.blockedFailure && inScopeOps.some((op) => op.id === this.blockedFailure?.operationId)) {
|
|
3982
|
+
const recovered = this.blockedFailure;
|
|
3983
|
+
this.blockedFailure = null;
|
|
3984
|
+
this.emitter?.emit({ type: "sync:apply-recovered", failure: recovered });
|
|
3985
|
+
}
|
|
3714
3986
|
}
|
|
3987
|
+
this.hasInFlightDeliveryBatch = false;
|
|
3715
3988
|
if (this.state === "syncing") {
|
|
3716
3989
|
this.deltaBatchesReceived++;
|
|
3717
3990
|
const totalBatches = msg.totalBatches ?? this.initialSyncTotalBatches;
|
|
@@ -3751,6 +4024,7 @@ var SyncEngine = class {
|
|
|
3751
4024
|
}
|
|
3752
4025
|
async handleAcknowledgment(msg) {
|
|
3753
4026
|
this.clearOutboundAckTimer();
|
|
4027
|
+
const requiresRetryBackoff = this.currentBatchRequiresRetryBackoff;
|
|
3754
4028
|
if (this.state === "syncing" && this.config.strictHandshake) {
|
|
3755
4029
|
this.pendingDeltaBatchAcks.delete(msg.acknowledgedMessageId);
|
|
3756
4030
|
this.markDeltaSendCompleteIfReady();
|
|
@@ -3766,6 +4040,7 @@ var SyncEngine = class {
|
|
|
3766
4040
|
}
|
|
3767
4041
|
this.currentBatch = null;
|
|
3768
4042
|
this.currentBatchRequiresPartialAck = false;
|
|
4043
|
+
this.currentBatchRequiresRetryBackoff = false;
|
|
3769
4044
|
const now = Date.now();
|
|
3770
4045
|
this.lastSyncedAt = now;
|
|
3771
4046
|
this.lastSuccessfulPush = now;
|
|
@@ -3773,7 +4048,14 @@ var SyncEngine = class {
|
|
|
3773
4048
|
await this.advanceLastAckedForLocalNode(msg.lastSequenceNumber);
|
|
3774
4049
|
await this.refreshPendingCount();
|
|
3775
4050
|
if (this.state === "streaming" && this.outboundQueue.hasOperations) {
|
|
3776
|
-
|
|
4051
|
+
if (requiresRetryBackoff) {
|
|
4052
|
+
this.scheduleOutboundRetry();
|
|
4053
|
+
} else {
|
|
4054
|
+
this.outboundRetryAttempt = 0;
|
|
4055
|
+
this.flushQueue();
|
|
4056
|
+
}
|
|
4057
|
+
} else if (!requiresRetryBackoff) {
|
|
4058
|
+
this.outboundRetryAttempt = 0;
|
|
3777
4059
|
}
|
|
3778
4060
|
}
|
|
3779
4061
|
handleError(msg) {
|
|
@@ -3782,10 +4064,14 @@ var SyncEngine = class {
|
|
|
3782
4064
|
this.outboundQueue.returnBatch(this.currentBatch.batchId);
|
|
3783
4065
|
this.currentBatch = null;
|
|
3784
4066
|
this.currentBatchRequiresPartialAck = false;
|
|
4067
|
+
this.currentBatchRequiresRetryBackoff = false;
|
|
3785
4068
|
}
|
|
3786
4069
|
this.transitionTo("error");
|
|
3787
|
-
if (msg.code === "AUTH_FAILED") {
|
|
4070
|
+
if (msg.code === "AUTH_FAILED" || msg.code === "DEVICE_REVOKED") {
|
|
4071
|
+
this.authRejected = true;
|
|
4072
|
+
this.suspensionReason = msg.code === "DEVICE_REVOKED" ? "device-revoked" : "auth-rejected";
|
|
3788
4073
|
this.emitter?.emit({ type: "sync:auth-failed", reason: msg.message });
|
|
4074
|
+
this.emitter?.emit({ type: "sync:suspended", reason: this.suspensionReason });
|
|
3789
4075
|
}
|
|
3790
4076
|
if (msg.code === "INVALID_TIMESTAMP") {
|
|
3791
4077
|
this.clockBlocked = true;
|
|
@@ -3808,9 +4094,39 @@ var SyncEngine = class {
|
|
|
3808
4094
|
* the app can reconcile. Unlike {@link handleError}, this is a normal per-op
|
|
3809
4095
|
* signal, so the connection stays up.
|
|
3810
4096
|
*/
|
|
4097
|
+
async applyScopeRetraction(retraction, applyToStore) {
|
|
4098
|
+
if (applyToStore) {
|
|
4099
|
+
if (!this.store.applyScopeRetraction) {
|
|
4100
|
+
throw new Error("The configured sync store does not support scope retractions");
|
|
4101
|
+
}
|
|
4102
|
+
await this.store.applyScopeRetraction(retraction.collection, retraction.recordId);
|
|
4103
|
+
}
|
|
4104
|
+
const quarantined = await this.outboundQueue.rejectRecord(
|
|
4105
|
+
retraction.collection,
|
|
4106
|
+
retraction.recordId
|
|
4107
|
+
);
|
|
4108
|
+
for (const operation of quarantined) {
|
|
4109
|
+
await this.rejectedStorage.record({
|
|
4110
|
+
operationId: operation.id,
|
|
4111
|
+
collection: operation.collection,
|
|
4112
|
+
recordId: operation.recordId,
|
|
4113
|
+
code: "SCOPE_RETRACTED",
|
|
4114
|
+
message: "Authorization changed before this operation could be uploaded.",
|
|
4115
|
+
retriable: false,
|
|
4116
|
+
rejectedAt: Date.now()
|
|
4117
|
+
});
|
|
4118
|
+
}
|
|
4119
|
+
this.emitter?.emit({
|
|
4120
|
+
type: "sync:scope-retracted",
|
|
4121
|
+
collection: retraction.collection,
|
|
4122
|
+
recordId: retraction.recordId,
|
|
4123
|
+
quarantinedOperationIds: quarantined.map((operation) => operation.id)
|
|
4124
|
+
});
|
|
4125
|
+
}
|
|
3811
4126
|
async handleOperationRejected(msg) {
|
|
3812
4127
|
if (msg.retriable) {
|
|
3813
4128
|
this.currentBatchRequiresPartialAck = true;
|
|
4129
|
+
this.currentBatchRequiresRetryBackoff = true;
|
|
3814
4130
|
await this.refreshPendingCount();
|
|
3815
4131
|
this.emitter?.emit({
|
|
3816
4132
|
type: "sync:operation-rejected",
|
|
@@ -3964,11 +4280,13 @@ var SyncEngine = class {
|
|
|
3964
4280
|
}
|
|
3965
4281
|
flushQueue() {
|
|
3966
4282
|
if (this.currentBatch) return;
|
|
4283
|
+
if (this.outboundRetryTimer) return;
|
|
3967
4284
|
if (!this.outboundQueue.hasOperations) return;
|
|
3968
4285
|
const batch = this.outboundQueue.takeBatch(this.batchSize);
|
|
3969
4286
|
if (!batch) return;
|
|
3970
4287
|
this.currentBatch = batch;
|
|
3971
4288
|
this.currentBatchRequiresPartialAck = false;
|
|
4289
|
+
this.currentBatchRequiresRetryBackoff = false;
|
|
3972
4290
|
if (this.encryptor) {
|
|
3973
4291
|
this.encryptor.encryptBatch(batch.operations).then(
|
|
3974
4292
|
(encrypted) => {
|
|
@@ -3992,6 +4310,7 @@ var SyncEngine = class {
|
|
|
3992
4310
|
this.outboundQueue.returnBatch(batch.batchId);
|
|
3993
4311
|
this.currentBatch = null;
|
|
3994
4312
|
this.currentBatchRequiresPartialAck = false;
|
|
4313
|
+
this.currentBatchRequiresRetryBackoff = false;
|
|
3995
4314
|
this.emitter?.emit({
|
|
3996
4315
|
type: "sync:disconnected",
|
|
3997
4316
|
reason: err instanceof Error ? err.message : "Encryption failed"
|
|
@@ -4028,11 +4347,29 @@ var SyncEngine = class {
|
|
|
4028
4347
|
clearTimeout(this.outboundAckTimer);
|
|
4029
4348
|
this.outboundAckTimer = null;
|
|
4030
4349
|
}
|
|
4350
|
+
scheduleOutboundRetry() {
|
|
4351
|
+
if (this.outboundRetryTimer || !this.outboundQueue.hasOperations) return;
|
|
4352
|
+
const delay = Math.min(
|
|
4353
|
+
this.outboundRetryMaxDelayMs,
|
|
4354
|
+
this.outboundRetryBaseDelayMs * 2 ** this.outboundRetryAttempt
|
|
4355
|
+
);
|
|
4356
|
+
this.outboundRetryAttempt += 1;
|
|
4357
|
+
this.outboundRetryTimer = setTimeout(() => {
|
|
4358
|
+
this.outboundRetryTimer = null;
|
|
4359
|
+
if (this.state === "streaming") this.flushQueue();
|
|
4360
|
+
}, delay);
|
|
4361
|
+
}
|
|
4362
|
+
clearOutboundRetryTimer() {
|
|
4363
|
+
if (!this.outboundRetryTimer) return;
|
|
4364
|
+
clearTimeout(this.outboundRetryTimer);
|
|
4365
|
+
this.outboundRetryTimer = null;
|
|
4366
|
+
}
|
|
4031
4367
|
async handleOutboundAckTimeout(batchId) {
|
|
4032
4368
|
if (!this.currentBatch || this.currentBatch.batchId !== batchId) return;
|
|
4033
4369
|
this.outboundQueue.returnBatch(batchId);
|
|
4034
4370
|
this.currentBatch = null;
|
|
4035
4371
|
this.currentBatchRequiresPartialAck = false;
|
|
4372
|
+
this.currentBatchRequiresRetryBackoff = false;
|
|
4036
4373
|
this.clearOutboundAckTimer();
|
|
4037
4374
|
await this.refreshPendingCount();
|
|
4038
4375
|
if (this.state === "disconnected") return;
|
|
@@ -4054,10 +4391,12 @@ var SyncEngine = class {
|
|
|
4054
4391
|
}
|
|
4055
4392
|
handleTransportClose(reason) {
|
|
4056
4393
|
this.clearOutboundAckTimer();
|
|
4394
|
+
this.clearOutboundRetryTimer();
|
|
4057
4395
|
if (this.currentBatch) {
|
|
4058
4396
|
this.outboundQueue.returnBatch(this.currentBatch.batchId);
|
|
4059
4397
|
this.currentBatch = null;
|
|
4060
4398
|
this.currentBatchRequiresPartialAck = false;
|
|
4399
|
+
this.currentBatchRequiresRetryBackoff = false;
|
|
4061
4400
|
}
|
|
4062
4401
|
if (this.schemaBlocked) {
|
|
4063
4402
|
return;
|
|
@@ -4100,10 +4439,10 @@ var SyncEngine = class {
|
|
|
4100
4439
|
return this.matchesScopeAndSubsets(op, fullRecord);
|
|
4101
4440
|
}
|
|
4102
4441
|
matchesScopeAndSubsets(op, fullRecord) {
|
|
4103
|
-
if (!operationMatchesScope(op, this.
|
|
4442
|
+
if (!operationMatchesScope(op, this.activeUplinkScope, fullRecord)) {
|
|
4104
4443
|
return false;
|
|
4105
4444
|
}
|
|
4106
|
-
return operationMatchesQuerySubsets(op, this.getActiveQuerySubsets(), fullRecord);
|
|
4445
|
+
return this.hasDirectionalScopes || operationMatchesQuerySubsets(op, this.getActiveQuerySubsets(), fullRecord);
|
|
4107
4446
|
}
|
|
4108
4447
|
async readRecordForBackfill(op) {
|
|
4109
4448
|
if (!this.store.readRecordFields) {
|
|
@@ -4829,13 +5168,22 @@ function fromBase642(str) {
|
|
|
4829
5168
|
// src/reactivity/sync-status-controller.ts
|
|
4830
5169
|
var OFFLINE_SYNC_STATUS = Object.freeze({
|
|
4831
5170
|
status: "offline",
|
|
5171
|
+
phase: "offline",
|
|
4832
5172
|
reconnecting: false,
|
|
4833
5173
|
pendingOperations: 0,
|
|
4834
5174
|
lastSyncedAt: null,
|
|
4835
5175
|
lastSuccessfulPush: null,
|
|
4836
5176
|
lastSuccessfulPull: null,
|
|
4837
5177
|
conflicts: 0,
|
|
4838
|
-
clockSkewMs: null
|
|
5178
|
+
clockSkewMs: null,
|
|
5179
|
+
inFlightUploadOperations: 0,
|
|
5180
|
+
hasInFlightDeliveryBatch: false,
|
|
5181
|
+
activeViewId: "",
|
|
5182
|
+
activeViewComplete: false,
|
|
5183
|
+
initialSync: { complete: false, receivedBatches: 0, totalBatches: null, progress: null },
|
|
5184
|
+
deliveryWatermark: 0,
|
|
5185
|
+
serverFrontier: null,
|
|
5186
|
+
blockedFailure: null
|
|
4839
5187
|
});
|
|
4840
5188
|
var SYNC_STATUS_EVENT_TYPES = [
|
|
4841
5189
|
"sync:connected",
|
|
@@ -4847,6 +5195,10 @@ var SYNC_STATUS_EVENT_TYPES = [
|
|
|
4847
5195
|
"sync:received",
|
|
4848
5196
|
"sync:acknowledged",
|
|
4849
5197
|
"sync:apply-failed",
|
|
5198
|
+
"sync:suspended",
|
|
5199
|
+
"sync:apply-blocked",
|
|
5200
|
+
"sync:apply-retrying",
|
|
5201
|
+
"sync:apply-recovered",
|
|
4850
5202
|
"sync:delivery-gap",
|
|
4851
5203
|
"sync:diagnostics",
|
|
4852
5204
|
"sync:initial-sync-progress"
|
|
@@ -5039,6 +5391,7 @@ function subscribeRemoteAwarenessStates(awareness, listener) {
|
|
|
5039
5391
|
isYjsDocUpdateMessage,
|
|
5040
5392
|
operationMatchesQuerySubsets,
|
|
5041
5393
|
operationMatchesScope,
|
|
5394
|
+
querySubsetContains,
|
|
5042
5395
|
richtextDocKey,
|
|
5043
5396
|
sliceOperationsAfterCursor,
|
|
5044
5397
|
subscribeRemoteAwarenessStates,
|