@korajs/sync 1.0.0-beta.11 → 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 +164 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +27 -2
- package/dist/index.d.ts +27 -2
- package/dist/index.js +164 -12
- package/dist/index.js.map +1 -1
- package/dist/internal.d.cts +1 -1
- package/dist/internal.d.ts +1 -1
- package/dist/{transport-iXX0KA_W.d.cts → transport-CJvRO5ix.d.cts} +18 -0
- package/dist/{transport-iXX0KA_W.d.ts → transport-CJvRO5ix.d.ts} +18 -0
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -684,7 +684,8 @@ function toProtoEnvelope(message) {
|
|
|
684
684
|
schemaVersion: message.schemaVersion,
|
|
685
685
|
authToken: message.authToken,
|
|
686
686
|
supportedWireFormats: message.supportedWireFormats,
|
|
687
|
-
...message.lastDeliverySequence !== void 0 ? { lastDeliverySequence: message.lastDeliverySequence } : {}
|
|
687
|
+
...message.lastDeliverySequence !== void 0 ? { lastDeliverySequence: message.lastDeliverySequence } : {},
|
|
688
|
+
scopeExitPolicy: message.scopeExitPolicy
|
|
688
689
|
};
|
|
689
690
|
case "handshake-response":
|
|
690
691
|
return {
|
|
@@ -701,7 +702,10 @@ function toProtoEnvelope(message) {
|
|
|
701
702
|
selectedWireFormat: message.selectedWireFormat,
|
|
702
703
|
serverTime: message.serverTime,
|
|
703
704
|
blobStorageEnabled: message.blobStorageEnabled,
|
|
704
|
-
...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
|
|
705
709
|
};
|
|
706
710
|
case "operation-batch":
|
|
707
711
|
return {
|
|
@@ -711,7 +715,8 @@ function toProtoEnvelope(message) {
|
|
|
711
715
|
isFinal: message.isFinal,
|
|
712
716
|
batchIndex: message.batchIndex,
|
|
713
717
|
...message.baseDeliverySequence !== void 0 ? { baseDeliverySequence: message.baseDeliverySequence } : {},
|
|
714
|
-
...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
|
|
715
720
|
};
|
|
716
721
|
case "acknowledgment":
|
|
717
722
|
return {
|
|
@@ -785,7 +790,8 @@ function fromProtoEnvelope(envelope) {
|
|
|
785
790
|
supportedWireFormats: envelope.supportedWireFormats?.filter(
|
|
786
791
|
(format) => format === "json" || format === "protobuf"
|
|
787
792
|
),
|
|
788
|
-
...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 } : {}
|
|
789
795
|
};
|
|
790
796
|
case "handshake-response":
|
|
791
797
|
return {
|
|
@@ -803,7 +809,10 @@ function fromProtoEnvelope(envelope) {
|
|
|
803
809
|
// automatic timestamp rebase work over the protobuf wire, not just JSON.
|
|
804
810
|
...envelope.serverTime !== void 0 ? { serverTime: envelope.serverTime } : {},
|
|
805
811
|
...envelope.blobStorageEnabled !== void 0 ? { blobStorageEnabled: envelope.blobStorageEnabled } : {},
|
|
806
|
-
...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) } : {}
|
|
807
816
|
};
|
|
808
817
|
case "operation-batch":
|
|
809
818
|
return {
|
|
@@ -813,7 +822,8 @@ function fromProtoEnvelope(envelope) {
|
|
|
813
822
|
isFinal: envelope.isFinal ?? false,
|
|
814
823
|
batchIndex: envelope.batchIndex ?? 0,
|
|
815
824
|
...envelope.baseDeliverySequence !== void 0 ? { baseDeliverySequence: envelope.baseDeliverySequence } : {},
|
|
816
|
-
...envelope.maxDeliverySequence !== void 0 ? { maxDeliverySequence: envelope.maxDeliverySequence } : {}
|
|
825
|
+
...envelope.maxDeliverySequence !== void 0 ? { maxDeliverySequence: envelope.maxDeliverySequence } : {},
|
|
826
|
+
...envelope.retractionsJson ? { retractions: JSON.parse(envelope.retractionsJson) } : {}
|
|
817
827
|
};
|
|
818
828
|
case "acknowledgment":
|
|
819
829
|
return {
|
|
@@ -1031,6 +1041,13 @@ function encodeEnvelope(envelope) {
|
|
|
1031
1041
|
if (envelope.deliverySequence !== void 0) writer.uint32(256).int64(envelope.deliverySequence);
|
|
1032
1042
|
if (envelope.serverMaxDeliverySequence !== void 0)
|
|
1033
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);
|
|
1034
1051
|
return writer.finish();
|
|
1035
1052
|
}
|
|
1036
1053
|
function decodeEnvelope(bytes) {
|
|
@@ -1141,6 +1158,21 @@ function decodeEnvelope(bytes) {
|
|
|
1141
1158
|
case 33:
|
|
1142
1159
|
envelope.serverMaxDeliverySequence = longToNumber(reader.int64());
|
|
1143
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;
|
|
1144
1176
|
default:
|
|
1145
1177
|
reader.skipType(tag & 7);
|
|
1146
1178
|
}
|
|
@@ -2806,6 +2838,18 @@ var OutboundQueue = class {
|
|
|
2806
2838
|
await this.storage.dequeue([opId]);
|
|
2807
2839
|
return removed;
|
|
2808
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
|
+
}
|
|
2809
2853
|
/**
|
|
2810
2854
|
* Remove operations by id from queue and persistent storage.
|
|
2811
2855
|
* Used when ops were already sent during handshake delta exchange.
|
|
@@ -2825,6 +2869,8 @@ var OutboundQueue = class {
|
|
|
2825
2869
|
var DEFAULT_BATCH_SIZE = 100;
|
|
2826
2870
|
var DEFAULT_SCHEMA_VERSION = 1;
|
|
2827
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;
|
|
2828
2874
|
var VALID_TRANSITIONS = {
|
|
2829
2875
|
disconnected: ["connecting"],
|
|
2830
2876
|
connecting: ["handshaking", "error", "disconnected"],
|
|
@@ -2860,6 +2906,8 @@ var SyncEngine = class {
|
|
|
2860
2906
|
rejectedStorage;
|
|
2861
2907
|
batchSize;
|
|
2862
2908
|
outboundAckTimeoutMs;
|
|
2909
|
+
outboundRetryBaseDelayMs;
|
|
2910
|
+
outboundRetryMaxDelayMs;
|
|
2863
2911
|
encryptor;
|
|
2864
2912
|
awarenessManager;
|
|
2865
2913
|
richtextDocChannel;
|
|
@@ -2875,6 +2923,9 @@ var SyncEngine = class {
|
|
|
2875
2923
|
conflictCount = 0;
|
|
2876
2924
|
currentBatch = null;
|
|
2877
2925
|
currentBatchRequiresPartialAck = false;
|
|
2926
|
+
currentBatchRequiresRetryBackoff = false;
|
|
2927
|
+
outboundRetryAttempt = 0;
|
|
2928
|
+
outboundRetryTimer = null;
|
|
2878
2929
|
reconnecting = false;
|
|
2879
2930
|
schemaBlocked = false;
|
|
2880
2931
|
clockBlocked = false;
|
|
@@ -2903,6 +2954,9 @@ var SyncEngine = class {
|
|
|
2903
2954
|
* with the server-accepted scope (server is authoritative).
|
|
2904
2955
|
*/
|
|
2905
2956
|
activeScope;
|
|
2957
|
+
/** Server-authoritative upload authorization, separate from the downloaded view. */
|
|
2958
|
+
activeUplinkScope;
|
|
2959
|
+
hasDirectionalScopes = false;
|
|
2906
2960
|
/** Live query subsets registered from reactive subscriptions */
|
|
2907
2961
|
querySubsets = /* @__PURE__ */ new Map();
|
|
2908
2962
|
staticQuerySubsets = [];
|
|
@@ -2939,9 +2993,18 @@ var SyncEngine = class {
|
|
|
2939
2993
|
this.emitter = options.emitter ?? null;
|
|
2940
2994
|
this.batchSize = options.config.batchSize ?? DEFAULT_BATCH_SIZE;
|
|
2941
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
|
+
);
|
|
2942
3004
|
this.encryptor = options.encryptor ?? null;
|
|
2943
3005
|
this.syncState = options.syncState ?? null;
|
|
2944
3006
|
this.activeScope = options.config.scopeMap;
|
|
3007
|
+
this.activeUplinkScope = options.config.scopeMap;
|
|
2945
3008
|
const queueStorage = options.queueStorage ?? new MemoryQueueStorage();
|
|
2946
3009
|
this.outboundQueue = new OutboundQueue(queueStorage);
|
|
2947
3010
|
this.rejectedStorage = options.rejectedStorage ?? new MemoryRejectedOperationStorage();
|
|
@@ -3064,6 +3127,7 @@ var SyncEngine = class {
|
|
|
3064
3127
|
authToken,
|
|
3065
3128
|
supportedWireFormats: ["json", "protobuf"],
|
|
3066
3129
|
...this.config.scopeMap ? { syncScope: this.config.scopeMap } : {},
|
|
3130
|
+
...this.config.scopeExit ? { scopeExitPolicy: this.config.scopeExit } : {},
|
|
3067
3131
|
...activeQuerySubsets.length > 0 ? { syncQueries: activeQuerySubsets } : {},
|
|
3068
3132
|
...this.resumeDeltaCursor ? { deltaCursor: encodeDeltaCursor(this.resumeDeltaCursor) } : {},
|
|
3069
3133
|
// Resume the server's gap-free delivery stream from our watermark. A server
|
|
@@ -3088,13 +3152,15 @@ var SyncEngine = class {
|
|
|
3088
3152
|
return this.stopPromise;
|
|
3089
3153
|
}
|
|
3090
3154
|
async stopInternal() {
|
|
3091
|
-
if (this.state === "disconnected") return;
|
|
3092
3155
|
this.clearOutboundAckTimer();
|
|
3156
|
+
this.clearOutboundRetryTimer();
|
|
3157
|
+
if (this.state === "disconnected") return;
|
|
3093
3158
|
this.awarenessManager.stopCleanupTimer();
|
|
3094
3159
|
if (this.currentBatch) {
|
|
3095
3160
|
this.outboundQueue.returnBatch(this.currentBatch.batchId);
|
|
3096
3161
|
this.currentBatch = null;
|
|
3097
3162
|
this.currentBatchRequiresPartialAck = false;
|
|
3163
|
+
this.currentBatchRequiresRetryBackoff = false;
|
|
3098
3164
|
}
|
|
3099
3165
|
try {
|
|
3100
3166
|
await this.transport.disconnect();
|
|
@@ -3377,6 +3443,8 @@ var SyncEngine = class {
|
|
|
3377
3443
|
updateScope(scopeMap) {
|
|
3378
3444
|
const previousSignature = this.deliverySignature();
|
|
3379
3445
|
this.activeScope = scopeMap;
|
|
3446
|
+
this.activeUplinkScope = scopeMap;
|
|
3447
|
+
this.hasDirectionalScopes = false;
|
|
3380
3448
|
this.config.scopeMap = scopeMap;
|
|
3381
3449
|
this.switchDeliveryView(previousSignature);
|
|
3382
3450
|
}
|
|
@@ -3460,6 +3528,10 @@ var SyncEngine = class {
|
|
|
3460
3528
|
getActiveScope() {
|
|
3461
3529
|
return this.activeScope;
|
|
3462
3530
|
}
|
|
3531
|
+
/** Get the server-accepted upload authorization scope. */
|
|
3532
|
+
getActiveUplinkScope() {
|
|
3533
|
+
return this.activeUplinkScope;
|
|
3534
|
+
}
|
|
3463
3535
|
/**
|
|
3464
3536
|
* Register a live query subset that narrows synced data for a collection.
|
|
3465
3537
|
* Takes effect on the next connection; reconnects when already connected.
|
|
@@ -3662,8 +3734,17 @@ var SyncEngine = class {
|
|
|
3662
3734
|
if (msg.selectedWireFormat) {
|
|
3663
3735
|
this.setSerializerWireFormat(msg.selectedWireFormat);
|
|
3664
3736
|
}
|
|
3665
|
-
if (msg.acceptedScope) {
|
|
3666
|
-
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();
|
|
3667
3748
|
}
|
|
3668
3749
|
this.emitter?.emit({ type: "sync:connected", nodeId: this.store.getNodeId() });
|
|
3669
3750
|
this.metricsCollector.recordConnected();
|
|
@@ -3828,6 +3909,14 @@ var SyncEngine = class {
|
|
|
3828
3909
|
const targetSchemaVersion = this.config.schemaVersion ?? DEFAULT_SCHEMA_VERSION;
|
|
3829
3910
|
const transforms = this.config.operationTransforms ?? [];
|
|
3830
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();
|
|
3831
3920
|
for (const op of inScopeOps) {
|
|
3832
3921
|
const transformed = transforms.length > 0 ? (0, import_core7.applyOperationTransforms)(op, targetSchemaVersion, transforms) : op;
|
|
3833
3922
|
if (transformed === null) {
|
|
@@ -3935,6 +4024,7 @@ var SyncEngine = class {
|
|
|
3935
4024
|
}
|
|
3936
4025
|
async handleAcknowledgment(msg) {
|
|
3937
4026
|
this.clearOutboundAckTimer();
|
|
4027
|
+
const requiresRetryBackoff = this.currentBatchRequiresRetryBackoff;
|
|
3938
4028
|
if (this.state === "syncing" && this.config.strictHandshake) {
|
|
3939
4029
|
this.pendingDeltaBatchAcks.delete(msg.acknowledgedMessageId);
|
|
3940
4030
|
this.markDeltaSendCompleteIfReady();
|
|
@@ -3950,6 +4040,7 @@ var SyncEngine = class {
|
|
|
3950
4040
|
}
|
|
3951
4041
|
this.currentBatch = null;
|
|
3952
4042
|
this.currentBatchRequiresPartialAck = false;
|
|
4043
|
+
this.currentBatchRequiresRetryBackoff = false;
|
|
3953
4044
|
const now = Date.now();
|
|
3954
4045
|
this.lastSyncedAt = now;
|
|
3955
4046
|
this.lastSuccessfulPush = now;
|
|
@@ -3957,7 +4048,14 @@ var SyncEngine = class {
|
|
|
3957
4048
|
await this.advanceLastAckedForLocalNode(msg.lastSequenceNumber);
|
|
3958
4049
|
await this.refreshPendingCount();
|
|
3959
4050
|
if (this.state === "streaming" && this.outboundQueue.hasOperations) {
|
|
3960
|
-
|
|
4051
|
+
if (requiresRetryBackoff) {
|
|
4052
|
+
this.scheduleOutboundRetry();
|
|
4053
|
+
} else {
|
|
4054
|
+
this.outboundRetryAttempt = 0;
|
|
4055
|
+
this.flushQueue();
|
|
4056
|
+
}
|
|
4057
|
+
} else if (!requiresRetryBackoff) {
|
|
4058
|
+
this.outboundRetryAttempt = 0;
|
|
3961
4059
|
}
|
|
3962
4060
|
}
|
|
3963
4061
|
handleError(msg) {
|
|
@@ -3966,6 +4064,7 @@ var SyncEngine = class {
|
|
|
3966
4064
|
this.outboundQueue.returnBatch(this.currentBatch.batchId);
|
|
3967
4065
|
this.currentBatch = null;
|
|
3968
4066
|
this.currentBatchRequiresPartialAck = false;
|
|
4067
|
+
this.currentBatchRequiresRetryBackoff = false;
|
|
3969
4068
|
}
|
|
3970
4069
|
this.transitionTo("error");
|
|
3971
4070
|
if (msg.code === "AUTH_FAILED" || msg.code === "DEVICE_REVOKED") {
|
|
@@ -3995,9 +4094,39 @@ var SyncEngine = class {
|
|
|
3995
4094
|
* the app can reconcile. Unlike {@link handleError}, this is a normal per-op
|
|
3996
4095
|
* signal, so the connection stays up.
|
|
3997
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
|
+
}
|
|
3998
4126
|
async handleOperationRejected(msg) {
|
|
3999
4127
|
if (msg.retriable) {
|
|
4000
4128
|
this.currentBatchRequiresPartialAck = true;
|
|
4129
|
+
this.currentBatchRequiresRetryBackoff = true;
|
|
4001
4130
|
await this.refreshPendingCount();
|
|
4002
4131
|
this.emitter?.emit({
|
|
4003
4132
|
type: "sync:operation-rejected",
|
|
@@ -4151,11 +4280,13 @@ var SyncEngine = class {
|
|
|
4151
4280
|
}
|
|
4152
4281
|
flushQueue() {
|
|
4153
4282
|
if (this.currentBatch) return;
|
|
4283
|
+
if (this.outboundRetryTimer) return;
|
|
4154
4284
|
if (!this.outboundQueue.hasOperations) return;
|
|
4155
4285
|
const batch = this.outboundQueue.takeBatch(this.batchSize);
|
|
4156
4286
|
if (!batch) return;
|
|
4157
4287
|
this.currentBatch = batch;
|
|
4158
4288
|
this.currentBatchRequiresPartialAck = false;
|
|
4289
|
+
this.currentBatchRequiresRetryBackoff = false;
|
|
4159
4290
|
if (this.encryptor) {
|
|
4160
4291
|
this.encryptor.encryptBatch(batch.operations).then(
|
|
4161
4292
|
(encrypted) => {
|
|
@@ -4179,6 +4310,7 @@ var SyncEngine = class {
|
|
|
4179
4310
|
this.outboundQueue.returnBatch(batch.batchId);
|
|
4180
4311
|
this.currentBatch = null;
|
|
4181
4312
|
this.currentBatchRequiresPartialAck = false;
|
|
4313
|
+
this.currentBatchRequiresRetryBackoff = false;
|
|
4182
4314
|
this.emitter?.emit({
|
|
4183
4315
|
type: "sync:disconnected",
|
|
4184
4316
|
reason: err instanceof Error ? err.message : "Encryption failed"
|
|
@@ -4215,11 +4347,29 @@ var SyncEngine = class {
|
|
|
4215
4347
|
clearTimeout(this.outboundAckTimer);
|
|
4216
4348
|
this.outboundAckTimer = null;
|
|
4217
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
|
+
}
|
|
4218
4367
|
async handleOutboundAckTimeout(batchId) {
|
|
4219
4368
|
if (!this.currentBatch || this.currentBatch.batchId !== batchId) return;
|
|
4220
4369
|
this.outboundQueue.returnBatch(batchId);
|
|
4221
4370
|
this.currentBatch = null;
|
|
4222
4371
|
this.currentBatchRequiresPartialAck = false;
|
|
4372
|
+
this.currentBatchRequiresRetryBackoff = false;
|
|
4223
4373
|
this.clearOutboundAckTimer();
|
|
4224
4374
|
await this.refreshPendingCount();
|
|
4225
4375
|
if (this.state === "disconnected") return;
|
|
@@ -4241,10 +4391,12 @@ var SyncEngine = class {
|
|
|
4241
4391
|
}
|
|
4242
4392
|
handleTransportClose(reason) {
|
|
4243
4393
|
this.clearOutboundAckTimer();
|
|
4394
|
+
this.clearOutboundRetryTimer();
|
|
4244
4395
|
if (this.currentBatch) {
|
|
4245
4396
|
this.outboundQueue.returnBatch(this.currentBatch.batchId);
|
|
4246
4397
|
this.currentBatch = null;
|
|
4247
4398
|
this.currentBatchRequiresPartialAck = false;
|
|
4399
|
+
this.currentBatchRequiresRetryBackoff = false;
|
|
4248
4400
|
}
|
|
4249
4401
|
if (this.schemaBlocked) {
|
|
4250
4402
|
return;
|
|
@@ -4287,10 +4439,10 @@ var SyncEngine = class {
|
|
|
4287
4439
|
return this.matchesScopeAndSubsets(op, fullRecord);
|
|
4288
4440
|
}
|
|
4289
4441
|
matchesScopeAndSubsets(op, fullRecord) {
|
|
4290
|
-
if (!operationMatchesScope(op, this.
|
|
4442
|
+
if (!operationMatchesScope(op, this.activeUplinkScope, fullRecord)) {
|
|
4291
4443
|
return false;
|
|
4292
4444
|
}
|
|
4293
|
-
return operationMatchesQuerySubsets(op, this.getActiveQuerySubsets(), fullRecord);
|
|
4445
|
+
return this.hasDirectionalScopes || operationMatchesQuerySubsets(op, this.getActiveQuerySubsets(), fullRecord);
|
|
4294
4446
|
}
|
|
4295
4447
|
async readRecordForBackfill(op) {
|
|
4296
4448
|
if (!this.store.readRecordFields) {
|