@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.js
CHANGED
|
@@ -20,7 +20,8 @@ var SYNC_STATUSES = [
|
|
|
20
20
|
"offline",
|
|
21
21
|
"clock-error",
|
|
22
22
|
"error",
|
|
23
|
-
"schema-mismatch"
|
|
23
|
+
"schema-mismatch",
|
|
24
|
+
"auth-required"
|
|
24
25
|
];
|
|
25
26
|
var MemoryRejectedOperationStorage = class {
|
|
26
27
|
rejected = /* @__PURE__ */ new Map();
|
|
@@ -68,12 +69,19 @@ function operationMatchesScope(op, scopeMap, fullRecord) {
|
|
|
68
69
|
const snapshot = buildSnapshot(op, fullRecord ?? void 0);
|
|
69
70
|
if (!snapshot) return false;
|
|
70
71
|
for (const [field, expected] of Object.entries(collectionScope)) {
|
|
71
|
-
if (snapshot[field]
|
|
72
|
+
if (!matchesPredicate(snapshot[field], expected)) {
|
|
72
73
|
return false;
|
|
73
74
|
}
|
|
74
75
|
}
|
|
75
76
|
return true;
|
|
76
77
|
}
|
|
78
|
+
function matchesPredicate(actual, expected) {
|
|
79
|
+
if (expected && typeof expected === "object" && !Array.isArray(expected) && "$in" in expected) {
|
|
80
|
+
const values = expected.$in;
|
|
81
|
+
return Array.isArray(values) && values.some((value) => Object.is(actual, value));
|
|
82
|
+
}
|
|
83
|
+
return Object.is(actual, expected);
|
|
84
|
+
}
|
|
77
85
|
function filterOperationsByScope(operations, scopeMap) {
|
|
78
86
|
if (!scopeMap) return operations;
|
|
79
87
|
return operations.filter((op) => operationMatchesScope(op, scopeMap));
|
|
@@ -130,24 +138,77 @@ function operationMatchesQuerySubsets(op, subsets, fullRecord) {
|
|
|
130
138
|
if (collectionSubsets.length === 0) {
|
|
131
139
|
return true;
|
|
132
140
|
}
|
|
141
|
+
if (collectionSubsets.some((subset) => Object.keys(subset.where).length === 0)) {
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
133
144
|
const snapshot = buildSnapshot2(op, fullRecord);
|
|
134
145
|
if (!snapshot) {
|
|
135
146
|
return false;
|
|
136
147
|
}
|
|
137
148
|
return collectionSubsets.some((subset) => recordMatchesWhere(snapshot, subset.where));
|
|
138
149
|
}
|
|
150
|
+
function stableValue(value) {
|
|
151
|
+
if (Array.isArray(value)) return value.map(stableValue);
|
|
152
|
+
if (value && typeof value === "object") {
|
|
153
|
+
return Object.fromEntries(
|
|
154
|
+
Object.entries(value).filter(([, entry]) => entry !== void 0).sort(([a], [b]) => a.localeCompare(b)).map(([key, entry]) => [key, stableValue(entry)])
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
return value;
|
|
158
|
+
}
|
|
159
|
+
function whereContains(broad, narrow) {
|
|
160
|
+
return Object.entries(broad).every(
|
|
161
|
+
([field, expected]) => JSON.stringify(stableValue(narrow[field])) === JSON.stringify(stableValue(expected))
|
|
162
|
+
);
|
|
163
|
+
}
|
|
139
164
|
function dedupeQuerySubsets(subsets) {
|
|
140
|
-
const
|
|
165
|
+
const normalized = subsets.map((subset) => ({
|
|
166
|
+
collection: subset.collection,
|
|
167
|
+
where: stableValue(subset.where)
|
|
168
|
+
}));
|
|
141
169
|
const result = [];
|
|
142
|
-
for (const
|
|
143
|
-
|
|
144
|
-
|
|
170
|
+
for (const candidate of normalized) {
|
|
171
|
+
if (normalized.some(
|
|
172
|
+
(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))
|
|
173
|
+
)) {
|
|
174
|
+
const duplicateAlreadyKept = result.some(
|
|
175
|
+
(item) => item.collection === candidate.collection && JSON.stringify(item.where) === JSON.stringify(candidate.where)
|
|
176
|
+
);
|
|
177
|
+
if (duplicateAlreadyKept || normalized.some(
|
|
178
|
+
(other) => other !== candidate && other.collection === candidate.collection && Object.keys(other.where).length < Object.keys(candidate.where).length && whereContains(other.where, candidate.where)
|
|
179
|
+
))
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (!result.some(
|
|
183
|
+
(item) => item.collection === candidate.collection && JSON.stringify(item.where) === JSON.stringify(candidate.where)
|
|
184
|
+
))
|
|
185
|
+
result.push(candidate);
|
|
186
|
+
}
|
|
187
|
+
return result.sort(
|
|
188
|
+
(a, b) => `${a.collection}:${JSON.stringify(a.where)}`.localeCompare(
|
|
189
|
+
`${b.collection}:${JSON.stringify(b.where)}`
|
|
190
|
+
)
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
function querySubsetContains(broad, narrow) {
|
|
194
|
+
const collections = /* @__PURE__ */ new Set([
|
|
195
|
+
...broad.map((subset) => subset.collection),
|
|
196
|
+
...narrow.map((subset) => subset.collection)
|
|
197
|
+
]);
|
|
198
|
+
for (const collection of collections) {
|
|
199
|
+
const broadForCollection = broad.filter((subset) => subset.collection === collection);
|
|
200
|
+
const narrowForCollection = narrow.filter((subset) => subset.collection === collection);
|
|
201
|
+
if (narrowForCollection.length === 0) {
|
|
202
|
+
if (broadForCollection.length > 0) return false;
|
|
145
203
|
continue;
|
|
146
204
|
}
|
|
147
|
-
|
|
148
|
-
|
|
205
|
+
if (broadForCollection.length === 0) continue;
|
|
206
|
+
if (!narrowForCollection.every(
|
|
207
|
+
(candidate) => broadForCollection.some((other) => whereContains(other.where, candidate.where))
|
|
208
|
+
))
|
|
209
|
+
return false;
|
|
149
210
|
}
|
|
150
|
-
return
|
|
211
|
+
return true;
|
|
151
212
|
}
|
|
152
213
|
|
|
153
214
|
// src/delta/delta-cursor.ts
|
|
@@ -530,7 +591,8 @@ function toProtoEnvelope(message) {
|
|
|
530
591
|
schemaVersion: message.schemaVersion,
|
|
531
592
|
authToken: message.authToken,
|
|
532
593
|
supportedWireFormats: message.supportedWireFormats,
|
|
533
|
-
...message.lastDeliverySequence !== void 0 ? { lastDeliverySequence: message.lastDeliverySequence } : {}
|
|
594
|
+
...message.lastDeliverySequence !== void 0 ? { lastDeliverySequence: message.lastDeliverySequence } : {},
|
|
595
|
+
scopeExitPolicy: message.scopeExitPolicy
|
|
534
596
|
};
|
|
535
597
|
case "handshake-response":
|
|
536
598
|
return {
|
|
@@ -547,7 +609,10 @@ function toProtoEnvelope(message) {
|
|
|
547
609
|
selectedWireFormat: message.selectedWireFormat,
|
|
548
610
|
serverTime: message.serverTime,
|
|
549
611
|
blobStorageEnabled: message.blobStorageEnabled,
|
|
550
|
-
...message.serverMaxDeliverySequence !== void 0 ? { serverMaxDeliverySequence: message.serverMaxDeliverySequence } : {}
|
|
612
|
+
...message.serverMaxDeliverySequence !== void 0 ? { serverMaxDeliverySequence: message.serverMaxDeliverySequence } : {},
|
|
613
|
+
acceptedScopeJson: message.acceptedScope ? JSON.stringify(message.acceptedScope) : void 0,
|
|
614
|
+
acceptedDownlinkScopesJson: message.acceptedDownlinkScopes ? JSON.stringify(message.acceptedDownlinkScopes) : void 0,
|
|
615
|
+
acceptedUplinkScopesJson: message.acceptedUplinkScopes ? JSON.stringify(message.acceptedUplinkScopes) : void 0
|
|
551
616
|
};
|
|
552
617
|
case "operation-batch":
|
|
553
618
|
return {
|
|
@@ -557,7 +622,8 @@ function toProtoEnvelope(message) {
|
|
|
557
622
|
isFinal: message.isFinal,
|
|
558
623
|
batchIndex: message.batchIndex,
|
|
559
624
|
...message.baseDeliverySequence !== void 0 ? { baseDeliverySequence: message.baseDeliverySequence } : {},
|
|
560
|
-
...message.maxDeliverySequence !== void 0 ? { maxDeliverySequence: message.maxDeliverySequence } : {}
|
|
625
|
+
...message.maxDeliverySequence !== void 0 ? { maxDeliverySequence: message.maxDeliverySequence } : {},
|
|
626
|
+
retractionsJson: message.retractions ? JSON.stringify(message.retractions) : void 0
|
|
561
627
|
};
|
|
562
628
|
case "acknowledgment":
|
|
563
629
|
return {
|
|
@@ -631,7 +697,8 @@ function fromProtoEnvelope(envelope) {
|
|
|
631
697
|
supportedWireFormats: envelope.supportedWireFormats?.filter(
|
|
632
698
|
(format) => format === "json" || format === "protobuf"
|
|
633
699
|
),
|
|
634
|
-
...envelope.lastDeliverySequence !== void 0 ? { lastDeliverySequence: envelope.lastDeliverySequence } : {}
|
|
700
|
+
...envelope.lastDeliverySequence !== void 0 ? { lastDeliverySequence: envelope.lastDeliverySequence } : {},
|
|
701
|
+
...envelope.scopeExitPolicy === "retain" || envelope.scopeExitPolicy === "retract" ? { scopeExitPolicy: envelope.scopeExitPolicy } : {}
|
|
635
702
|
};
|
|
636
703
|
case "handshake-response":
|
|
637
704
|
return {
|
|
@@ -649,7 +716,10 @@ function fromProtoEnvelope(envelope) {
|
|
|
649
716
|
// automatic timestamp rebase work over the protobuf wire, not just JSON.
|
|
650
717
|
...envelope.serverTime !== void 0 ? { serverTime: envelope.serverTime } : {},
|
|
651
718
|
...envelope.blobStorageEnabled !== void 0 ? { blobStorageEnabled: envelope.blobStorageEnabled } : {},
|
|
652
|
-
...envelope.serverMaxDeliverySequence !== void 0 ? { serverMaxDeliverySequence: envelope.serverMaxDeliverySequence } : {}
|
|
719
|
+
...envelope.serverMaxDeliverySequence !== void 0 ? { serverMaxDeliverySequence: envelope.serverMaxDeliverySequence } : {},
|
|
720
|
+
...envelope.acceptedScopeJson ? { acceptedScope: JSON.parse(envelope.acceptedScopeJson) } : {},
|
|
721
|
+
...envelope.acceptedDownlinkScopesJson ? { acceptedDownlinkScopes: JSON.parse(envelope.acceptedDownlinkScopesJson) } : {},
|
|
722
|
+
...envelope.acceptedUplinkScopesJson ? { acceptedUplinkScopes: JSON.parse(envelope.acceptedUplinkScopesJson) } : {}
|
|
653
723
|
};
|
|
654
724
|
case "operation-batch":
|
|
655
725
|
return {
|
|
@@ -659,7 +729,8 @@ function fromProtoEnvelope(envelope) {
|
|
|
659
729
|
isFinal: envelope.isFinal ?? false,
|
|
660
730
|
batchIndex: envelope.batchIndex ?? 0,
|
|
661
731
|
...envelope.baseDeliverySequence !== void 0 ? { baseDeliverySequence: envelope.baseDeliverySequence } : {},
|
|
662
|
-
...envelope.maxDeliverySequence !== void 0 ? { maxDeliverySequence: envelope.maxDeliverySequence } : {}
|
|
732
|
+
...envelope.maxDeliverySequence !== void 0 ? { maxDeliverySequence: envelope.maxDeliverySequence } : {},
|
|
733
|
+
...envelope.retractionsJson ? { retractions: JSON.parse(envelope.retractionsJson) } : {}
|
|
663
734
|
};
|
|
664
735
|
case "acknowledgment":
|
|
665
736
|
return {
|
|
@@ -877,6 +948,13 @@ function encodeEnvelope(envelope) {
|
|
|
877
948
|
if (envelope.deliverySequence !== void 0) writer.uint32(256).int64(envelope.deliverySequence);
|
|
878
949
|
if (envelope.serverMaxDeliverySequence !== void 0)
|
|
879
950
|
writer.uint32(264).int64(envelope.serverMaxDeliverySequence);
|
|
951
|
+
if (envelope.acceptedScopeJson) writer.uint32(274).string(envelope.acceptedScopeJson);
|
|
952
|
+
if (envelope.acceptedDownlinkScopesJson)
|
|
953
|
+
writer.uint32(282).string(envelope.acceptedDownlinkScopesJson);
|
|
954
|
+
if (envelope.acceptedUplinkScopesJson)
|
|
955
|
+
writer.uint32(290).string(envelope.acceptedUplinkScopesJson);
|
|
956
|
+
if (envelope.retractionsJson) writer.uint32(298).string(envelope.retractionsJson);
|
|
957
|
+
if (envelope.scopeExitPolicy) writer.uint32(306).string(envelope.scopeExitPolicy);
|
|
880
958
|
return writer.finish();
|
|
881
959
|
}
|
|
882
960
|
function decodeEnvelope(bytes) {
|
|
@@ -987,6 +1065,21 @@ function decodeEnvelope(bytes) {
|
|
|
987
1065
|
case 33:
|
|
988
1066
|
envelope.serverMaxDeliverySequence = longToNumber(reader.int64());
|
|
989
1067
|
break;
|
|
1068
|
+
case 34:
|
|
1069
|
+
envelope.acceptedScopeJson = reader.string();
|
|
1070
|
+
break;
|
|
1071
|
+
case 35:
|
|
1072
|
+
envelope.acceptedDownlinkScopesJson = reader.string();
|
|
1073
|
+
break;
|
|
1074
|
+
case 36:
|
|
1075
|
+
envelope.acceptedUplinkScopesJson = reader.string();
|
|
1076
|
+
break;
|
|
1077
|
+
case 37:
|
|
1078
|
+
envelope.retractionsJson = reader.string();
|
|
1079
|
+
break;
|
|
1080
|
+
case 38:
|
|
1081
|
+
envelope.scopeExitPolicy = reader.string();
|
|
1082
|
+
break;
|
|
990
1083
|
default:
|
|
991
1084
|
reader.skipType(tag & 7);
|
|
992
1085
|
}
|
|
@@ -2640,6 +2733,18 @@ var OutboundQueue = class {
|
|
|
2640
2733
|
await this.storage.dequeue([opId]);
|
|
2641
2734
|
return removed;
|
|
2642
2735
|
}
|
|
2736
|
+
/** Remove every pending/in-flight operation for a record so authorization loss cannot upload it. */
|
|
2737
|
+
async rejectRecord(collection, recordId) {
|
|
2738
|
+
const matching = [...this.queue, ...[...this.inFlight.values()].flat()].filter(
|
|
2739
|
+
(op) => op.collection === collection && op.recordId === recordId
|
|
2740
|
+
);
|
|
2741
|
+
const removed = [];
|
|
2742
|
+
for (const operation of matching) {
|
|
2743
|
+
const rejected = await this.reject(operation.id);
|
|
2744
|
+
if (rejected) removed.push(rejected);
|
|
2745
|
+
}
|
|
2746
|
+
return removed;
|
|
2747
|
+
}
|
|
2643
2748
|
/**
|
|
2644
2749
|
* Remove operations by id from queue and persistent storage.
|
|
2645
2750
|
* Used when ops were already sent during handshake delta exchange.
|
|
@@ -2659,6 +2764,8 @@ var OutboundQueue = class {
|
|
|
2659
2764
|
var DEFAULT_BATCH_SIZE = 100;
|
|
2660
2765
|
var DEFAULT_SCHEMA_VERSION = 1;
|
|
2661
2766
|
var DEFAULT_OUTBOUND_ACK_TIMEOUT_MS = 3e4;
|
|
2767
|
+
var DEFAULT_OUTBOUND_RETRY_BASE_DELAY_MS = 250;
|
|
2768
|
+
var DEFAULT_OUTBOUND_RETRY_MAX_DELAY_MS = 3e4;
|
|
2662
2769
|
var VALID_TRANSITIONS = {
|
|
2663
2770
|
disconnected: ["connecting"],
|
|
2664
2771
|
connecting: ["handshaking", "error", "disconnected"],
|
|
@@ -2694,6 +2801,8 @@ var SyncEngine = class {
|
|
|
2694
2801
|
rejectedStorage;
|
|
2695
2802
|
batchSize;
|
|
2696
2803
|
outboundAckTimeoutMs;
|
|
2804
|
+
outboundRetryBaseDelayMs;
|
|
2805
|
+
outboundRetryMaxDelayMs;
|
|
2697
2806
|
encryptor;
|
|
2698
2807
|
awarenessManager;
|
|
2699
2808
|
richtextDocChannel;
|
|
@@ -2709,11 +2818,19 @@ var SyncEngine = class {
|
|
|
2709
2818
|
conflictCount = 0;
|
|
2710
2819
|
currentBatch = null;
|
|
2711
2820
|
currentBatchRequiresPartialAck = false;
|
|
2821
|
+
currentBatchRequiresRetryBackoff = false;
|
|
2822
|
+
outboundRetryAttempt = 0;
|
|
2823
|
+
outboundRetryTimer = null;
|
|
2712
2824
|
reconnecting = false;
|
|
2713
2825
|
schemaBlocked = false;
|
|
2714
2826
|
clockBlocked = false;
|
|
2715
2827
|
clockSkewMs = null;
|
|
2716
2828
|
blobStorageEnabled = false;
|
|
2829
|
+
suspensionReason = null;
|
|
2830
|
+
authRejected = false;
|
|
2831
|
+
serverFrontier = null;
|
|
2832
|
+
hasInFlightDeliveryBatch = false;
|
|
2833
|
+
blockedFailure = null;
|
|
2717
2834
|
outboundAckTimer = null;
|
|
2718
2835
|
startPromise = null;
|
|
2719
2836
|
stopPromise = null;
|
|
@@ -2732,8 +2849,12 @@ var SyncEngine = class {
|
|
|
2732
2849
|
* with the server-accepted scope (server is authoritative).
|
|
2733
2850
|
*/
|
|
2734
2851
|
activeScope;
|
|
2852
|
+
/** Server-authoritative upload authorization, separate from the downloaded view. */
|
|
2853
|
+
activeUplinkScope;
|
|
2854
|
+
hasDirectionalScopes = false;
|
|
2735
2855
|
/** Live query subsets registered from reactive subscriptions */
|
|
2736
2856
|
querySubsets = /* @__PURE__ */ new Map();
|
|
2857
|
+
staticQuerySubsets = [];
|
|
2737
2858
|
querySubsetReconnectTimer = null;
|
|
2738
2859
|
/** Resume cursor for paginated initial sync (persisted across reconnects) */
|
|
2739
2860
|
resumeDeltaCursor = null;
|
|
@@ -2767,9 +2888,18 @@ var SyncEngine = class {
|
|
|
2767
2888
|
this.emitter = options.emitter ?? null;
|
|
2768
2889
|
this.batchSize = options.config.batchSize ?? DEFAULT_BATCH_SIZE;
|
|
2769
2890
|
this.outboundAckTimeoutMs = options.config.outboundAckTimeoutMs ?? DEFAULT_OUTBOUND_ACK_TIMEOUT_MS;
|
|
2891
|
+
this.outboundRetryBaseDelayMs = Math.max(
|
|
2892
|
+
0,
|
|
2893
|
+
options.config.outboundRetryBaseDelayMs ?? DEFAULT_OUTBOUND_RETRY_BASE_DELAY_MS
|
|
2894
|
+
);
|
|
2895
|
+
this.outboundRetryMaxDelayMs = Math.max(
|
|
2896
|
+
this.outboundRetryBaseDelayMs,
|
|
2897
|
+
options.config.outboundRetryMaxDelayMs ?? DEFAULT_OUTBOUND_RETRY_MAX_DELAY_MS
|
|
2898
|
+
);
|
|
2770
2899
|
this.encryptor = options.encryptor ?? null;
|
|
2771
2900
|
this.syncState = options.syncState ?? null;
|
|
2772
2901
|
this.activeScope = options.config.scopeMap;
|
|
2902
|
+
this.activeUplinkScope = options.config.scopeMap;
|
|
2773
2903
|
const queueStorage = options.queueStorage ?? new MemoryQueueStorage();
|
|
2774
2904
|
this.outboundQueue = new OutboundQueue(queueStorage);
|
|
2775
2905
|
this.rejectedStorage = options.rejectedStorage ?? new MemoryRejectedOperationStorage();
|
|
@@ -2827,6 +2957,22 @@ var SyncEngine = class {
|
|
|
2827
2957
|
if (this.state !== "disconnected") {
|
|
2828
2958
|
await this.stop();
|
|
2829
2959
|
}
|
|
2960
|
+
if (this.authRejected) {
|
|
2961
|
+
this.emitter?.emit({
|
|
2962
|
+
type: "sync:suspended",
|
|
2963
|
+
reason: this.suspensionReason ?? "auth-rejected"
|
|
2964
|
+
});
|
|
2965
|
+
return;
|
|
2966
|
+
}
|
|
2967
|
+
if (this.config.authState) {
|
|
2968
|
+
const authState = await this.config.authState();
|
|
2969
|
+
if (authState.state === "loading" || authState.state === "signed-out") {
|
|
2970
|
+
this.suspensionReason = authState.state === "loading" ? "auth-loading" : "auth-required";
|
|
2971
|
+
this.emitter?.emit({ type: "sync:suspended", reason: this.suspensionReason });
|
|
2972
|
+
return;
|
|
2973
|
+
}
|
|
2974
|
+
}
|
|
2975
|
+
this.suspensionReason = null;
|
|
2830
2976
|
await this.outboundQueue.initialize();
|
|
2831
2977
|
if (this.syncState) {
|
|
2832
2978
|
this.lastAckedServerVector = await this.syncState.loadLastAckedServerVector();
|
|
@@ -2876,6 +3022,7 @@ var SyncEngine = class {
|
|
|
2876
3022
|
authToken,
|
|
2877
3023
|
supportedWireFormats: ["json", "protobuf"],
|
|
2878
3024
|
...this.config.scopeMap ? { syncScope: this.config.scopeMap } : {},
|
|
3025
|
+
...this.config.scopeExit ? { scopeExitPolicy: this.config.scopeExit } : {},
|
|
2879
3026
|
...activeQuerySubsets.length > 0 ? { syncQueries: activeQuerySubsets } : {},
|
|
2880
3027
|
...this.resumeDeltaCursor ? { deltaCursor: encodeDeltaCursor(this.resumeDeltaCursor) } : {},
|
|
2881
3028
|
// Resume the server's gap-free delivery stream from our watermark. A server
|
|
@@ -2900,13 +3047,15 @@ var SyncEngine = class {
|
|
|
2900
3047
|
return this.stopPromise;
|
|
2901
3048
|
}
|
|
2902
3049
|
async stopInternal() {
|
|
2903
|
-
if (this.state === "disconnected") return;
|
|
2904
3050
|
this.clearOutboundAckTimer();
|
|
3051
|
+
this.clearOutboundRetryTimer();
|
|
3052
|
+
if (this.state === "disconnected") return;
|
|
2905
3053
|
this.awarenessManager.stopCleanupTimer();
|
|
2906
3054
|
if (this.currentBatch) {
|
|
2907
3055
|
this.outboundQueue.returnBatch(this.currentBatch.batchId);
|
|
2908
3056
|
this.currentBatch = null;
|
|
2909
3057
|
this.currentBatchRequiresPartialAck = false;
|
|
3058
|
+
this.currentBatchRequiresRetryBackoff = false;
|
|
2910
3059
|
}
|
|
2911
3060
|
try {
|
|
2912
3061
|
await this.transport.disconnect();
|
|
@@ -2956,14 +3105,30 @@ var SyncEngine = class {
|
|
|
2956
3105
|
getStatus() {
|
|
2957
3106
|
const pendingOperations = this.syncState ? this.cachedUnsyncedCount : this.outboundQueue.totalPending;
|
|
2958
3107
|
const base = {
|
|
3108
|
+
phase: this.resolvePhase(),
|
|
3109
|
+
...this.suspensionReason ? { reason: this.suspensionReason } : {},
|
|
2959
3110
|
reconnecting: this.reconnecting,
|
|
2960
3111
|
pendingOperations,
|
|
2961
3112
|
lastSyncedAt: this.lastSyncedAt,
|
|
2962
3113
|
lastSuccessfulPush: this.lastSuccessfulPush,
|
|
2963
3114
|
lastSuccessfulPull: this.lastSuccessfulPull,
|
|
2964
3115
|
conflicts: this.conflictCount,
|
|
2965
|
-
clockSkewMs: this.clockSkewMs
|
|
3116
|
+
clockSkewMs: this.clockSkewMs,
|
|
3117
|
+
inFlightUploadOperations: this.currentBatch?.operations.length ?? 0,
|
|
3118
|
+
hasInFlightDeliveryBatch: this.hasInFlightDeliveryBatch,
|
|
3119
|
+
activeViewId: this.deliverySignature(),
|
|
3120
|
+
activeViewComplete: this.state === "streaming" && !this.hasInFlightDeliveryBatch && this.blockedFailure === null && (this.serverFrontier === null || this.deliveryWatermark >= this.serverFrontier),
|
|
3121
|
+
initialSync: {
|
|
3122
|
+
complete: this.deltaReceiveComplete && this.deltaSendComplete,
|
|
3123
|
+
receivedBatches: this.deltaBatchesReceived,
|
|
3124
|
+
totalBatches: this.initialSyncTotalBatches || null,
|
|
3125
|
+
progress: this.initialSyncTotalBatches > 0 ? Math.min(1, this.deltaBatchesReceived / this.initialSyncTotalBatches) : null
|
|
3126
|
+
},
|
|
3127
|
+
deliveryWatermark: this.deliveryWatermark,
|
|
3128
|
+
serverFrontier: this.serverFrontier,
|
|
3129
|
+
blockedFailure: this.blockedFailure
|
|
2966
3130
|
};
|
|
3131
|
+
if (this.suspensionReason) return { ...base, status: "auth-required" };
|
|
2967
3132
|
switch (this.state) {
|
|
2968
3133
|
case "disconnected":
|
|
2969
3134
|
if (this.clockBlocked) {
|
|
@@ -2986,6 +3151,26 @@ var SyncEngine = class {
|
|
|
2986
3151
|
return { ...base, status: this.schemaBlocked ? "schema-mismatch" : "error" };
|
|
2987
3152
|
}
|
|
2988
3153
|
}
|
|
3154
|
+
resolvePhase() {
|
|
3155
|
+
if (this.suspensionReason) return "suspended";
|
|
3156
|
+
if (this.blockedFailure || this.schemaBlocked || this.clockBlocked) return "blocked";
|
|
3157
|
+
if (this.hasInFlightDeliveryBatch) return "applying";
|
|
3158
|
+
if (this.currentBatch) return "uploading";
|
|
3159
|
+
switch (this.state) {
|
|
3160
|
+
case "disconnected":
|
|
3161
|
+
return "offline";
|
|
3162
|
+
case "connecting":
|
|
3163
|
+
return "connecting";
|
|
3164
|
+
case "handshaking":
|
|
3165
|
+
return "handshaking";
|
|
3166
|
+
case "syncing":
|
|
3167
|
+
return "receiving";
|
|
3168
|
+
case "streaming":
|
|
3169
|
+
return "streaming";
|
|
3170
|
+
case "error":
|
|
3171
|
+
return "blocked";
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
2989
3174
|
/**
|
|
2990
3175
|
* True when the server rejected the client's schema version at handshake.
|
|
2991
3176
|
* Sync stays blocked until the app schema is upgraded or sync config changes.
|
|
@@ -3038,8 +3223,25 @@ var SyncEngine = class {
|
|
|
3038
3223
|
await this.refreshPendingCount();
|
|
3039
3224
|
return this.getStatus().pendingOperations;
|
|
3040
3225
|
}
|
|
3041
|
-
emitApplyFailure(op, result, overrides) {
|
|
3226
|
+
emitApplyFailure(op, result, overrides, blocking = false) {
|
|
3042
3227
|
const reason = defaultApplyFailureReason(result, overrides);
|
|
3228
|
+
if (blocking || reason.retriable) {
|
|
3229
|
+
const prior = this.blockedFailure;
|
|
3230
|
+
this.blockedFailure = {
|
|
3231
|
+
operationId: op.id,
|
|
3232
|
+
collection: op.collection,
|
|
3233
|
+
recordId: op.recordId,
|
|
3234
|
+
code: reason.code,
|
|
3235
|
+
message: reason.message,
|
|
3236
|
+
retriable: reason.retriable,
|
|
3237
|
+
firstSeenAt: prior?.operationId === op.id ? prior.firstSeenAt : Date.now(),
|
|
3238
|
+
retryCount: prior?.operationId === op.id ? prior.retryCount + 1 : 0
|
|
3239
|
+
};
|
|
3240
|
+
this.emitter?.emit({
|
|
3241
|
+
type: prior?.operationId === op.id ? "sync:apply-retrying" : "sync:apply-blocked",
|
|
3242
|
+
failure: this.blockedFailure
|
|
3243
|
+
});
|
|
3244
|
+
}
|
|
3043
3245
|
this.emitter?.emit({
|
|
3044
3246
|
type: "sync:apply-failed",
|
|
3045
3247
|
operationId: op.id,
|
|
@@ -3056,11 +3258,18 @@ var SyncEngine = class {
|
|
|
3056
3258
|
*/
|
|
3057
3259
|
async retryNow() {
|
|
3058
3260
|
if (this.schemaBlocked) return;
|
|
3261
|
+
this.authRejected = false;
|
|
3262
|
+
this.suspensionReason = null;
|
|
3059
3263
|
if (this.state === "disconnected" || this.state === "error") {
|
|
3060
3264
|
this.reconnecting = false;
|
|
3061
3265
|
await this.start();
|
|
3062
3266
|
}
|
|
3063
3267
|
}
|
|
3268
|
+
/** Wake a permanent auth suspension after the binding reports a new auth state. */
|
|
3269
|
+
notifyAuthChanged() {
|
|
3270
|
+
this.authRejected = false;
|
|
3271
|
+
this.suspensionReason = null;
|
|
3272
|
+
}
|
|
3064
3273
|
/**
|
|
3065
3274
|
* Atomically refresh the transport session. Overlapping callers share one
|
|
3066
3275
|
* reconnect, in-flight outbound batches are returned to the queue by stop(), and the
|
|
@@ -3129,6 +3338,8 @@ var SyncEngine = class {
|
|
|
3129
3338
|
updateScope(scopeMap) {
|
|
3130
3339
|
const previousSignature = this.deliverySignature();
|
|
3131
3340
|
this.activeScope = scopeMap;
|
|
3341
|
+
this.activeUplinkScope = scopeMap;
|
|
3342
|
+
this.hasDirectionalScopes = false;
|
|
3132
3343
|
this.config.scopeMap = scopeMap;
|
|
3133
3344
|
this.switchDeliveryView(previousSignature);
|
|
3134
3345
|
}
|
|
@@ -3212,28 +3423,58 @@ var SyncEngine = class {
|
|
|
3212
3423
|
getActiveScope() {
|
|
3213
3424
|
return this.activeScope;
|
|
3214
3425
|
}
|
|
3426
|
+
/** Get the server-accepted upload authorization scope. */
|
|
3427
|
+
getActiveUplinkScope() {
|
|
3428
|
+
return this.activeUplinkScope;
|
|
3429
|
+
}
|
|
3215
3430
|
/**
|
|
3216
3431
|
* Register a live query subset that narrows synced data for a collection.
|
|
3217
3432
|
* Takes effect on the next connection; reconnects when already connected.
|
|
3218
3433
|
*/
|
|
3219
3434
|
registerQuerySubset(subset) {
|
|
3435
|
+
if ((this.config.querySubsets?.mode ?? "reactive") !== "reactive") return () => {
|
|
3436
|
+
};
|
|
3220
3437
|
const id = `query-${nextQuerySubsetId++}`;
|
|
3221
3438
|
const previousSignature = this.deliverySignature();
|
|
3439
|
+
const previousEffective = this.getActiveQuerySubsets();
|
|
3222
3440
|
this.querySubsets.set(id, subset);
|
|
3223
|
-
this.
|
|
3224
|
-
this.scheduleQuerySubsetReconnect();
|
|
3441
|
+
this.switchDeliveryViewWithInheritance(previousSignature, previousEffective);
|
|
3442
|
+
if (this.deliverySignature() !== previousSignature) this.scheduleQuerySubsetReconnect();
|
|
3225
3443
|
return () => {
|
|
3226
3444
|
const sigBeforeRemove = this.deliverySignature();
|
|
3445
|
+
const effectiveBeforeRemove = this.getActiveQuerySubsets();
|
|
3227
3446
|
this.querySubsets.delete(id);
|
|
3228
|
-
this.
|
|
3229
|
-
this.scheduleQuerySubsetReconnect();
|
|
3447
|
+
this.switchDeliveryViewWithInheritance(sigBeforeRemove, effectiveBeforeRemove);
|
|
3448
|
+
if (this.deliverySignature() !== sigBeforeRemove) this.scheduleQuerySubsetReconnect();
|
|
3230
3449
|
};
|
|
3231
3450
|
}
|
|
3451
|
+
switchDeliveryViewWithInheritance(previousSignature, previousSubsets) {
|
|
3452
|
+
const nextSubsets = this.getActiveQuerySubsets();
|
|
3453
|
+
const previousWatermark = this.deliveryWatermark;
|
|
3454
|
+
this.switchDeliveryView(previousSignature);
|
|
3455
|
+
if (this.deliveryWatermark === 0 && querySubsetContains(previousSubsets, nextSubsets)) {
|
|
3456
|
+
this.deliveryWatermark = previousWatermark;
|
|
3457
|
+
this.setViewWatermark(this.deliverySignature(), previousWatermark);
|
|
3458
|
+
void this.persistDeliveryWatermark(previousWatermark);
|
|
3459
|
+
}
|
|
3460
|
+
}
|
|
3461
|
+
/** Atomically replace the manifest used by static query-subset mode. */
|
|
3462
|
+
setQuerySubsets(subsets) {
|
|
3463
|
+
if ((this.config.querySubsets?.mode ?? "reactive") !== "static")
|
|
3464
|
+
throw new Error('setQuerySubsets() requires sync.querySubsets.mode = "static"');
|
|
3465
|
+
const previousSignature = this.deliverySignature();
|
|
3466
|
+
const previousEffective = this.getActiveQuerySubsets();
|
|
3467
|
+
this.staticQuerySubsets = dedupeQuerySubsets(subsets);
|
|
3468
|
+
this.switchDeliveryViewWithInheritance(previousSignature, previousEffective);
|
|
3469
|
+
if (this.deliverySignature() !== previousSignature) this.scheduleQuerySubsetReconnect();
|
|
3470
|
+
}
|
|
3232
3471
|
/**
|
|
3233
3472
|
* Returns deduplicated active query subsets from registered subscriptions.
|
|
3234
3473
|
*/
|
|
3235
3474
|
getActiveQuerySubsets() {
|
|
3236
|
-
|
|
3475
|
+
const mode = this.config.querySubsets?.mode ?? "reactive";
|
|
3476
|
+
if (mode === "disabled") return [];
|
|
3477
|
+
return mode === "static" ? this.staticQuerySubsets : dedupeQuerySubsets([...this.querySubsets.values()]);
|
|
3237
3478
|
}
|
|
3238
3479
|
/**
|
|
3239
3480
|
* Get the awareness manager for collaborative presence.
|
|
@@ -3310,6 +3551,7 @@ var SyncEngine = class {
|
|
|
3310
3551
|
}
|
|
3311
3552
|
}
|
|
3312
3553
|
handleMessageFailure(error) {
|
|
3554
|
+
this.hasInFlightDeliveryBatch = false;
|
|
3313
3555
|
const reason = error instanceof Error ? error.message : "Message handling failed";
|
|
3314
3556
|
this.handleTransportClose(reason);
|
|
3315
3557
|
}
|
|
@@ -3383,11 +3625,21 @@ var SyncEngine = class {
|
|
|
3383
3625
|
this.setViewWatermark(this.deliverySignature(), 0);
|
|
3384
3626
|
await this.persistDeliveryWatermark(0);
|
|
3385
3627
|
}
|
|
3628
|
+
this.serverFrontier = msg.serverMaxDeliverySequence ?? null;
|
|
3386
3629
|
if (msg.selectedWireFormat) {
|
|
3387
3630
|
this.setSerializerWireFormat(msg.selectedWireFormat);
|
|
3388
3631
|
}
|
|
3389
|
-
if (msg.acceptedScope) {
|
|
3390
|
-
this.activeScope = msg.acceptedScope;
|
|
3632
|
+
if (msg.acceptedDownlinkScopes ?? msg.acceptedScope) {
|
|
3633
|
+
this.activeScope = msg.acceptedDownlinkScopes ?? msg.acceptedScope;
|
|
3634
|
+
}
|
|
3635
|
+
this.hasDirectionalScopes = msg.acceptedUplinkScopes !== void 0;
|
|
3636
|
+
this.activeUplinkScope = msg.acceptedUplinkScopes ?? msg.acceptedScope ?? this.activeScope;
|
|
3637
|
+
if (this.config.scopeExit === "retract" && this.activeScope && this.store.applyScopeNarrowing) {
|
|
3638
|
+
const narrowed = await this.store.applyScopeNarrowing(this.activeScope);
|
|
3639
|
+
for (const retraction of narrowed) {
|
|
3640
|
+
await this.applyScopeRetraction(retraction, false);
|
|
3641
|
+
}
|
|
3642
|
+
await this.refreshPendingCount();
|
|
3391
3643
|
}
|
|
3392
3644
|
this.emitter?.emit({ type: "sync:connected", nodeId: this.store.getNodeId() });
|
|
3393
3645
|
this.metricsCollector.recordConnected();
|
|
@@ -3544,6 +3796,7 @@ var SyncEngine = class {
|
|
|
3544
3796
|
this.sendDeliveryAck(msg.messageId, this.deliveryWatermark);
|
|
3545
3797
|
return;
|
|
3546
3798
|
}
|
|
3799
|
+
this.hasInFlightDeliveryBatch = true;
|
|
3547
3800
|
}
|
|
3548
3801
|
const deserialized = msg.operations.map((s) => this.serializer.decodeOperation(s));
|
|
3549
3802
|
const operations = this.encryptor ? await this.encryptor.decryptBatch(deserialized) : deserialized;
|
|
@@ -3551,6 +3804,14 @@ var SyncEngine = class {
|
|
|
3551
3804
|
const targetSchemaVersion = this.config.schemaVersion ?? DEFAULT_SCHEMA_VERSION;
|
|
3552
3805
|
const transforms = this.config.operationTransforms ?? [];
|
|
3553
3806
|
let fullyApplied = true;
|
|
3807
|
+
for (const retraction of msg.retractions ?? []) {
|
|
3808
|
+
try {
|
|
3809
|
+
await this.applyScopeRetraction(retraction, true);
|
|
3810
|
+
} catch {
|
|
3811
|
+
fullyApplied = false;
|
|
3812
|
+
}
|
|
3813
|
+
}
|
|
3814
|
+
await this.refreshPendingCount();
|
|
3554
3815
|
for (const op of inScopeOps) {
|
|
3555
3816
|
const transformed = transforms.length > 0 ? applyOperationTransforms(op, targetSchemaVersion, transforms) : op;
|
|
3556
3817
|
if (transformed === null) {
|
|
@@ -3564,17 +3825,22 @@ var SyncEngine = class {
|
|
|
3564
3825
|
} catch (error) {
|
|
3565
3826
|
fullyApplied = false;
|
|
3566
3827
|
if (error instanceof ClockDriftError) {
|
|
3567
|
-
this.emitApplyFailure(
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3828
|
+
this.emitApplyFailure(
|
|
3829
|
+
transformed,
|
|
3830
|
+
"rejected",
|
|
3831
|
+
{
|
|
3832
|
+
code: APPLY_FAILURE_CODES.CLOCK_DRIFT,
|
|
3833
|
+
message: error.message,
|
|
3834
|
+
retriable: false
|
|
3835
|
+
},
|
|
3836
|
+
true
|
|
3837
|
+
);
|
|
3572
3838
|
continue;
|
|
3573
3839
|
}
|
|
3574
3840
|
const message = error instanceof Error ? error.message : "Apply failed";
|
|
3575
3841
|
const code = error instanceof SyncError4 ? error.code : error instanceof KoraError2 ? error.code : error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : APPLY_FAILURE_CODES.APPLY_FAILED;
|
|
3576
3842
|
const retriable = code !== APPLY_FAILURE_CODES.REFERENTIAL_INTEGRITY;
|
|
3577
|
-
this.emitApplyFailure(transformed, "rejected", { code, message, retriable });
|
|
3843
|
+
this.emitApplyFailure(transformed, "rejected", { code, message, retriable }, true);
|
|
3578
3844
|
}
|
|
3579
3845
|
}
|
|
3580
3846
|
if (inScopeOps.length > 0) {
|
|
@@ -3607,7 +3873,13 @@ var SyncEngine = class {
|
|
|
3607
3873
|
this.deliveryWatermark = msg.maxDeliverySequence;
|
|
3608
3874
|
this.setViewWatermark(this.deliverySignature(), this.deliveryWatermark);
|
|
3609
3875
|
await this.persistDeliveryWatermark(this.deliveryWatermark);
|
|
3876
|
+
if (this.blockedFailure && inScopeOps.some((op) => op.id === this.blockedFailure?.operationId)) {
|
|
3877
|
+
const recovered = this.blockedFailure;
|
|
3878
|
+
this.blockedFailure = null;
|
|
3879
|
+
this.emitter?.emit({ type: "sync:apply-recovered", failure: recovered });
|
|
3880
|
+
}
|
|
3610
3881
|
}
|
|
3882
|
+
this.hasInFlightDeliveryBatch = false;
|
|
3611
3883
|
if (this.state === "syncing") {
|
|
3612
3884
|
this.deltaBatchesReceived++;
|
|
3613
3885
|
const totalBatches = msg.totalBatches ?? this.initialSyncTotalBatches;
|
|
@@ -3647,6 +3919,7 @@ var SyncEngine = class {
|
|
|
3647
3919
|
}
|
|
3648
3920
|
async handleAcknowledgment(msg) {
|
|
3649
3921
|
this.clearOutboundAckTimer();
|
|
3922
|
+
const requiresRetryBackoff = this.currentBatchRequiresRetryBackoff;
|
|
3650
3923
|
if (this.state === "syncing" && this.config.strictHandshake) {
|
|
3651
3924
|
this.pendingDeltaBatchAcks.delete(msg.acknowledgedMessageId);
|
|
3652
3925
|
this.markDeltaSendCompleteIfReady();
|
|
@@ -3662,6 +3935,7 @@ var SyncEngine = class {
|
|
|
3662
3935
|
}
|
|
3663
3936
|
this.currentBatch = null;
|
|
3664
3937
|
this.currentBatchRequiresPartialAck = false;
|
|
3938
|
+
this.currentBatchRequiresRetryBackoff = false;
|
|
3665
3939
|
const now = Date.now();
|
|
3666
3940
|
this.lastSyncedAt = now;
|
|
3667
3941
|
this.lastSuccessfulPush = now;
|
|
@@ -3669,7 +3943,14 @@ var SyncEngine = class {
|
|
|
3669
3943
|
await this.advanceLastAckedForLocalNode(msg.lastSequenceNumber);
|
|
3670
3944
|
await this.refreshPendingCount();
|
|
3671
3945
|
if (this.state === "streaming" && this.outboundQueue.hasOperations) {
|
|
3672
|
-
|
|
3946
|
+
if (requiresRetryBackoff) {
|
|
3947
|
+
this.scheduleOutboundRetry();
|
|
3948
|
+
} else {
|
|
3949
|
+
this.outboundRetryAttempt = 0;
|
|
3950
|
+
this.flushQueue();
|
|
3951
|
+
}
|
|
3952
|
+
} else if (!requiresRetryBackoff) {
|
|
3953
|
+
this.outboundRetryAttempt = 0;
|
|
3673
3954
|
}
|
|
3674
3955
|
}
|
|
3675
3956
|
handleError(msg) {
|
|
@@ -3678,10 +3959,14 @@ var SyncEngine = class {
|
|
|
3678
3959
|
this.outboundQueue.returnBatch(this.currentBatch.batchId);
|
|
3679
3960
|
this.currentBatch = null;
|
|
3680
3961
|
this.currentBatchRequiresPartialAck = false;
|
|
3962
|
+
this.currentBatchRequiresRetryBackoff = false;
|
|
3681
3963
|
}
|
|
3682
3964
|
this.transitionTo("error");
|
|
3683
|
-
if (msg.code === "AUTH_FAILED") {
|
|
3965
|
+
if (msg.code === "AUTH_FAILED" || msg.code === "DEVICE_REVOKED") {
|
|
3966
|
+
this.authRejected = true;
|
|
3967
|
+
this.suspensionReason = msg.code === "DEVICE_REVOKED" ? "device-revoked" : "auth-rejected";
|
|
3684
3968
|
this.emitter?.emit({ type: "sync:auth-failed", reason: msg.message });
|
|
3969
|
+
this.emitter?.emit({ type: "sync:suspended", reason: this.suspensionReason });
|
|
3685
3970
|
}
|
|
3686
3971
|
if (msg.code === "INVALID_TIMESTAMP") {
|
|
3687
3972
|
this.clockBlocked = true;
|
|
@@ -3704,9 +3989,39 @@ var SyncEngine = class {
|
|
|
3704
3989
|
* the app can reconcile. Unlike {@link handleError}, this is a normal per-op
|
|
3705
3990
|
* signal, so the connection stays up.
|
|
3706
3991
|
*/
|
|
3992
|
+
async applyScopeRetraction(retraction, applyToStore) {
|
|
3993
|
+
if (applyToStore) {
|
|
3994
|
+
if (!this.store.applyScopeRetraction) {
|
|
3995
|
+
throw new Error("The configured sync store does not support scope retractions");
|
|
3996
|
+
}
|
|
3997
|
+
await this.store.applyScopeRetraction(retraction.collection, retraction.recordId);
|
|
3998
|
+
}
|
|
3999
|
+
const quarantined = await this.outboundQueue.rejectRecord(
|
|
4000
|
+
retraction.collection,
|
|
4001
|
+
retraction.recordId
|
|
4002
|
+
);
|
|
4003
|
+
for (const operation of quarantined) {
|
|
4004
|
+
await this.rejectedStorage.record({
|
|
4005
|
+
operationId: operation.id,
|
|
4006
|
+
collection: operation.collection,
|
|
4007
|
+
recordId: operation.recordId,
|
|
4008
|
+
code: "SCOPE_RETRACTED",
|
|
4009
|
+
message: "Authorization changed before this operation could be uploaded.",
|
|
4010
|
+
retriable: false,
|
|
4011
|
+
rejectedAt: Date.now()
|
|
4012
|
+
});
|
|
4013
|
+
}
|
|
4014
|
+
this.emitter?.emit({
|
|
4015
|
+
type: "sync:scope-retracted",
|
|
4016
|
+
collection: retraction.collection,
|
|
4017
|
+
recordId: retraction.recordId,
|
|
4018
|
+
quarantinedOperationIds: quarantined.map((operation) => operation.id)
|
|
4019
|
+
});
|
|
4020
|
+
}
|
|
3707
4021
|
async handleOperationRejected(msg) {
|
|
3708
4022
|
if (msg.retriable) {
|
|
3709
4023
|
this.currentBatchRequiresPartialAck = true;
|
|
4024
|
+
this.currentBatchRequiresRetryBackoff = true;
|
|
3710
4025
|
await this.refreshPendingCount();
|
|
3711
4026
|
this.emitter?.emit({
|
|
3712
4027
|
type: "sync:operation-rejected",
|
|
@@ -3860,11 +4175,13 @@ var SyncEngine = class {
|
|
|
3860
4175
|
}
|
|
3861
4176
|
flushQueue() {
|
|
3862
4177
|
if (this.currentBatch) return;
|
|
4178
|
+
if (this.outboundRetryTimer) return;
|
|
3863
4179
|
if (!this.outboundQueue.hasOperations) return;
|
|
3864
4180
|
const batch = this.outboundQueue.takeBatch(this.batchSize);
|
|
3865
4181
|
if (!batch) return;
|
|
3866
4182
|
this.currentBatch = batch;
|
|
3867
4183
|
this.currentBatchRequiresPartialAck = false;
|
|
4184
|
+
this.currentBatchRequiresRetryBackoff = false;
|
|
3868
4185
|
if (this.encryptor) {
|
|
3869
4186
|
this.encryptor.encryptBatch(batch.operations).then(
|
|
3870
4187
|
(encrypted) => {
|
|
@@ -3888,6 +4205,7 @@ var SyncEngine = class {
|
|
|
3888
4205
|
this.outboundQueue.returnBatch(batch.batchId);
|
|
3889
4206
|
this.currentBatch = null;
|
|
3890
4207
|
this.currentBatchRequiresPartialAck = false;
|
|
4208
|
+
this.currentBatchRequiresRetryBackoff = false;
|
|
3891
4209
|
this.emitter?.emit({
|
|
3892
4210
|
type: "sync:disconnected",
|
|
3893
4211
|
reason: err instanceof Error ? err.message : "Encryption failed"
|
|
@@ -3924,11 +4242,29 @@ var SyncEngine = class {
|
|
|
3924
4242
|
clearTimeout(this.outboundAckTimer);
|
|
3925
4243
|
this.outboundAckTimer = null;
|
|
3926
4244
|
}
|
|
4245
|
+
scheduleOutboundRetry() {
|
|
4246
|
+
if (this.outboundRetryTimer || !this.outboundQueue.hasOperations) return;
|
|
4247
|
+
const delay = Math.min(
|
|
4248
|
+
this.outboundRetryMaxDelayMs,
|
|
4249
|
+
this.outboundRetryBaseDelayMs * 2 ** this.outboundRetryAttempt
|
|
4250
|
+
);
|
|
4251
|
+
this.outboundRetryAttempt += 1;
|
|
4252
|
+
this.outboundRetryTimer = setTimeout(() => {
|
|
4253
|
+
this.outboundRetryTimer = null;
|
|
4254
|
+
if (this.state === "streaming") this.flushQueue();
|
|
4255
|
+
}, delay);
|
|
4256
|
+
}
|
|
4257
|
+
clearOutboundRetryTimer() {
|
|
4258
|
+
if (!this.outboundRetryTimer) return;
|
|
4259
|
+
clearTimeout(this.outboundRetryTimer);
|
|
4260
|
+
this.outboundRetryTimer = null;
|
|
4261
|
+
}
|
|
3927
4262
|
async handleOutboundAckTimeout(batchId) {
|
|
3928
4263
|
if (!this.currentBatch || this.currentBatch.batchId !== batchId) return;
|
|
3929
4264
|
this.outboundQueue.returnBatch(batchId);
|
|
3930
4265
|
this.currentBatch = null;
|
|
3931
4266
|
this.currentBatchRequiresPartialAck = false;
|
|
4267
|
+
this.currentBatchRequiresRetryBackoff = false;
|
|
3932
4268
|
this.clearOutboundAckTimer();
|
|
3933
4269
|
await this.refreshPendingCount();
|
|
3934
4270
|
if (this.state === "disconnected") return;
|
|
@@ -3950,10 +4286,12 @@ var SyncEngine = class {
|
|
|
3950
4286
|
}
|
|
3951
4287
|
handleTransportClose(reason) {
|
|
3952
4288
|
this.clearOutboundAckTimer();
|
|
4289
|
+
this.clearOutboundRetryTimer();
|
|
3953
4290
|
if (this.currentBatch) {
|
|
3954
4291
|
this.outboundQueue.returnBatch(this.currentBatch.batchId);
|
|
3955
4292
|
this.currentBatch = null;
|
|
3956
4293
|
this.currentBatchRequiresPartialAck = false;
|
|
4294
|
+
this.currentBatchRequiresRetryBackoff = false;
|
|
3957
4295
|
}
|
|
3958
4296
|
if (this.schemaBlocked) {
|
|
3959
4297
|
return;
|
|
@@ -3996,10 +4334,10 @@ var SyncEngine = class {
|
|
|
3996
4334
|
return this.matchesScopeAndSubsets(op, fullRecord);
|
|
3997
4335
|
}
|
|
3998
4336
|
matchesScopeAndSubsets(op, fullRecord) {
|
|
3999
|
-
if (!operationMatchesScope(op, this.
|
|
4337
|
+
if (!operationMatchesScope(op, this.activeUplinkScope, fullRecord)) {
|
|
4000
4338
|
return false;
|
|
4001
4339
|
}
|
|
4002
|
-
return operationMatchesQuerySubsets(op, this.getActiveQuerySubsets(), fullRecord);
|
|
4340
|
+
return this.hasDirectionalScopes || operationMatchesQuerySubsets(op, this.getActiveQuerySubsets(), fullRecord);
|
|
4003
4341
|
}
|
|
4004
4342
|
async readRecordForBackfill(op) {
|
|
4005
4343
|
if (!this.store.readRecordFields) {
|
|
@@ -4725,13 +5063,22 @@ function fromBase642(str) {
|
|
|
4725
5063
|
// src/reactivity/sync-status-controller.ts
|
|
4726
5064
|
var OFFLINE_SYNC_STATUS = Object.freeze({
|
|
4727
5065
|
status: "offline",
|
|
5066
|
+
phase: "offline",
|
|
4728
5067
|
reconnecting: false,
|
|
4729
5068
|
pendingOperations: 0,
|
|
4730
5069
|
lastSyncedAt: null,
|
|
4731
5070
|
lastSuccessfulPush: null,
|
|
4732
5071
|
lastSuccessfulPull: null,
|
|
4733
5072
|
conflicts: 0,
|
|
4734
|
-
clockSkewMs: null
|
|
5073
|
+
clockSkewMs: null,
|
|
5074
|
+
inFlightUploadOperations: 0,
|
|
5075
|
+
hasInFlightDeliveryBatch: false,
|
|
5076
|
+
activeViewId: "",
|
|
5077
|
+
activeViewComplete: false,
|
|
5078
|
+
initialSync: { complete: false, receivedBatches: 0, totalBatches: null, progress: null },
|
|
5079
|
+
deliveryWatermark: 0,
|
|
5080
|
+
serverFrontier: null,
|
|
5081
|
+
blockedFailure: null
|
|
4735
5082
|
});
|
|
4736
5083
|
var SYNC_STATUS_EVENT_TYPES = [
|
|
4737
5084
|
"sync:connected",
|
|
@@ -4743,6 +5090,10 @@ var SYNC_STATUS_EVENT_TYPES = [
|
|
|
4743
5090
|
"sync:received",
|
|
4744
5091
|
"sync:acknowledged",
|
|
4745
5092
|
"sync:apply-failed",
|
|
5093
|
+
"sync:suspended",
|
|
5094
|
+
"sync:apply-blocked",
|
|
5095
|
+
"sync:apply-retrying",
|
|
5096
|
+
"sync:apply-recovered",
|
|
4746
5097
|
"sync:delivery-gap",
|
|
4747
5098
|
"sync:diagnostics",
|
|
4748
5099
|
"sync:initial-sync-progress"
|
|
@@ -4934,6 +5285,7 @@ export {
|
|
|
4934
5285
|
isYjsDocUpdateMessage,
|
|
4935
5286
|
operationMatchesQuerySubsets,
|
|
4936
5287
|
operationMatchesScope,
|
|
5288
|
+
querySubsetContains,
|
|
4937
5289
|
richtextDocKey,
|
|
4938
5290
|
sliceOperationsAfterCursor,
|
|
4939
5291
|
subscribeRemoteAwarenessStates,
|