@korajs/sync 1.0.0-beta.6 → 1.0.0-beta.9
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 +102 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +22 -2
- package/dist/index.d.ts +22 -2
- package/dist/index.js +102 -9
- package/dist/index.js.map +1 -1
- package/dist/internal.d.cts +1 -1
- package/dist/internal.d.ts +1 -1
- package/dist/{transport-ByWfWBbB.d.cts → transport-B2GQerqJ.d.cts} +3 -1
- package/dist/{transport-ByWfWBbB.d.ts → transport-B2GQerqJ.d.ts} +3 -1
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -106,6 +106,7 @@ var SYNC_STATES = [
|
|
|
106
106
|
];
|
|
107
107
|
var SYNC_STATUSES = [
|
|
108
108
|
"connected",
|
|
109
|
+
"reconnecting",
|
|
109
110
|
"syncing",
|
|
110
111
|
"synced",
|
|
111
112
|
"offline",
|
|
@@ -2604,6 +2605,32 @@ var OutboundQueue = class {
|
|
|
2604
2605
|
const ids = ops.map((op) => op.id);
|
|
2605
2606
|
await this.storage.dequeue(ids);
|
|
2606
2607
|
}
|
|
2608
|
+
/**
|
|
2609
|
+
* Acknowledge only operations in an in-flight batch whose local sequence number is
|
|
2610
|
+
* covered by the server's ack. Any later operations return to the front of the
|
|
2611
|
+
* queue for retry. This is required when the server accepts a prefix but rejects a
|
|
2612
|
+
* later operation retriably (for example a stale-scope push after auth changed).
|
|
2613
|
+
*/
|
|
2614
|
+
async acknowledgeThrough(batchId, lastSequenceNumber) {
|
|
2615
|
+
const ops = this.inFlight.get(batchId);
|
|
2616
|
+
if (!ops) return;
|
|
2617
|
+
this.inFlight.delete(batchId);
|
|
2618
|
+
const acknowledged = ops.filter((op) => op.sequenceNumber <= lastSequenceNumber);
|
|
2619
|
+
const retry = ops.filter((op) => op.sequenceNumber > lastSequenceNumber);
|
|
2620
|
+
if (retry.length > 0) {
|
|
2621
|
+
this.queue.unshift(...retry);
|
|
2622
|
+
if (this.queue.length > 1) {
|
|
2623
|
+
this.queue = (0, import_internal.topologicalSort)(this.queue);
|
|
2624
|
+
}
|
|
2625
|
+
}
|
|
2626
|
+
if (acknowledged.length > 0) {
|
|
2627
|
+
const ids = acknowledged.map((op) => op.id);
|
|
2628
|
+
for (const id of ids) {
|
|
2629
|
+
this.seen.delete(id);
|
|
2630
|
+
}
|
|
2631
|
+
await this.storage.dequeue(ids);
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2607
2634
|
/**
|
|
2608
2635
|
* Return a failed batch to the front of the queue for retry.
|
|
2609
2636
|
* Prepends the operations to maintain priority.
|
|
@@ -2785,6 +2812,7 @@ var SyncEngine = class {
|
|
|
2785
2812
|
lastSuccessfulPull = null;
|
|
2786
2813
|
conflictCount = 0;
|
|
2787
2814
|
currentBatch = null;
|
|
2815
|
+
currentBatchRequiresPartialAck = false;
|
|
2788
2816
|
reconnecting = false;
|
|
2789
2817
|
schemaBlocked = false;
|
|
2790
2818
|
clockBlocked = false;
|
|
@@ -2793,6 +2821,7 @@ var SyncEngine = class {
|
|
|
2793
2821
|
outboundAckTimer = null;
|
|
2794
2822
|
startPromise = null;
|
|
2795
2823
|
stopPromise = null;
|
|
2824
|
+
reconnectPromise = null;
|
|
2796
2825
|
// Track delta exchange state
|
|
2797
2826
|
deltaBatchesReceived = 0;
|
|
2798
2827
|
deltaReceiveComplete = false;
|
|
@@ -2832,6 +2861,8 @@ var SyncEngine = class {
|
|
|
2832
2861
|
* so a view switch resolves synchronously (no race with the reconnect it triggers).
|
|
2833
2862
|
*/
|
|
2834
2863
|
deliverySignatureWatermarks = /* @__PURE__ */ new Map();
|
|
2864
|
+
deliveryGapRepeatCount = 0;
|
|
2865
|
+
lastDeliveryGapKey = null;
|
|
2835
2866
|
constructor(options) {
|
|
2836
2867
|
this.transport = options.transport;
|
|
2837
2868
|
this.store = options.store;
|
|
@@ -2979,6 +3010,7 @@ var SyncEngine = class {
|
|
|
2979
3010
|
if (this.currentBatch) {
|
|
2980
3011
|
this.outboundQueue.returnBatch(this.currentBatch.batchId);
|
|
2981
3012
|
this.currentBatch = null;
|
|
3013
|
+
this.currentBatchRequiresPartialAck = false;
|
|
2982
3014
|
}
|
|
2983
3015
|
try {
|
|
2984
3016
|
await this.transport.disconnect();
|
|
@@ -3028,6 +3060,7 @@ var SyncEngine = class {
|
|
|
3028
3060
|
getStatus() {
|
|
3029
3061
|
const pendingOperations = this.syncState ? this.cachedUnsyncedCount : this.outboundQueue.totalPending;
|
|
3030
3062
|
const base = {
|
|
3063
|
+
reconnecting: this.reconnecting,
|
|
3031
3064
|
pendingOperations,
|
|
3032
3065
|
lastSyncedAt: this.lastSyncedAt,
|
|
3033
3066
|
lastSuccessfulPush: this.lastSuccessfulPush,
|
|
@@ -3047,7 +3080,7 @@ var SyncEngine = class {
|
|
|
3047
3080
|
case "connecting":
|
|
3048
3081
|
case "handshaking":
|
|
3049
3082
|
case "syncing":
|
|
3050
|
-
return { ...base, status: this.reconnecting ? "
|
|
3083
|
+
return { ...base, status: this.reconnecting ? "reconnecting" : "syncing" };
|
|
3051
3084
|
case "streaming":
|
|
3052
3085
|
return { ...base, status: pendingOperations > 0 ? "syncing" : "synced" };
|
|
3053
3086
|
case "error":
|
|
@@ -3132,6 +3165,24 @@ var SyncEngine = class {
|
|
|
3132
3165
|
await this.start();
|
|
3133
3166
|
}
|
|
3134
3167
|
}
|
|
3168
|
+
/**
|
|
3169
|
+
* Atomically refresh the transport session. Overlapping callers share one
|
|
3170
|
+
* reconnect, in-flight outbound batches are returned to the queue by stop(), and the
|
|
3171
|
+
* next start handshakes with the latest auth token, sync scope, and query subsets.
|
|
3172
|
+
*/
|
|
3173
|
+
async reconnect() {
|
|
3174
|
+
if (this.schemaBlocked) return;
|
|
3175
|
+
if (this.reconnectPromise) return this.reconnectPromise;
|
|
3176
|
+
this.reconnecting = false;
|
|
3177
|
+
this.reconnectPromise = this.reconnectInternal().finally(() => {
|
|
3178
|
+
this.reconnectPromise = null;
|
|
3179
|
+
});
|
|
3180
|
+
return this.reconnectPromise;
|
|
3181
|
+
}
|
|
3182
|
+
async reconnectInternal() {
|
|
3183
|
+
await this.stop();
|
|
3184
|
+
await this.start();
|
|
3185
|
+
}
|
|
3135
3186
|
/**
|
|
3136
3187
|
* Export a diagnostics snapshot for debugging and support tickets.
|
|
3137
3188
|
* Contains connection state, timing info, and queue metrics.
|
|
@@ -3150,6 +3201,8 @@ var SyncEngine = class {
|
|
|
3150
3201
|
pendingOperations: this.outboundQueue.totalPending,
|
|
3151
3202
|
hasInFlightBatch: this.currentBatch !== null,
|
|
3152
3203
|
reconnecting: this.reconnecting,
|
|
3204
|
+
deliveryWatermark: this.deliveryWatermark,
|
|
3205
|
+
deliveryGapRepeatCount: this.deliveryGapRepeatCount,
|
|
3153
3206
|
timestamp: Date.now()
|
|
3154
3207
|
};
|
|
3155
3208
|
}
|
|
@@ -3339,7 +3392,7 @@ var SyncEngine = class {
|
|
|
3339
3392
|
await this.handleOperationBatch(message);
|
|
3340
3393
|
break;
|
|
3341
3394
|
case "acknowledgment":
|
|
3342
|
-
this.handleAcknowledgment(message);
|
|
3395
|
+
await this.handleAcknowledgment(message);
|
|
3343
3396
|
break;
|
|
3344
3397
|
case "error":
|
|
3345
3398
|
this.handleError(message);
|
|
@@ -3578,6 +3631,17 @@ var SyncEngine = class {
|
|
|
3578
3631
|
if (isDeliveryBatch) {
|
|
3579
3632
|
const base = msg.baseDeliverySequence ?? 0;
|
|
3580
3633
|
if (base > this.deliveryWatermark) {
|
|
3634
|
+
const gapKey = `${this.deliveryWatermark}:${base}`;
|
|
3635
|
+
this.deliveryGapRepeatCount = this.lastDeliveryGapKey === gapKey ? this.deliveryGapRepeatCount + 1 : 1;
|
|
3636
|
+
this.lastDeliveryGapKey = gapKey;
|
|
3637
|
+
this.emitter?.emit({
|
|
3638
|
+
type: "sync:delivery-gap",
|
|
3639
|
+
expectedBase: this.deliveryWatermark,
|
|
3640
|
+
receivedBase: base,
|
|
3641
|
+
currentWatermark: this.deliveryWatermark,
|
|
3642
|
+
messageId: msg.messageId,
|
|
3643
|
+
repeatCount: this.deliveryGapRepeatCount
|
|
3644
|
+
});
|
|
3581
3645
|
return;
|
|
3582
3646
|
}
|
|
3583
3647
|
if (base < this.deliveryWatermark) {
|
|
@@ -3642,6 +3706,8 @@ var SyncEngine = class {
|
|
|
3642
3706
|
});
|
|
3643
3707
|
}
|
|
3644
3708
|
if (isDeliveryBatch && fullyApplied && msg.maxDeliverySequence !== void 0) {
|
|
3709
|
+
this.deliveryGapRepeatCount = 0;
|
|
3710
|
+
this.lastDeliveryGapKey = null;
|
|
3645
3711
|
this.deliveryWatermark = msg.maxDeliverySequence;
|
|
3646
3712
|
this.setViewWatermark(this.deliverySignature(), this.deliveryWatermark);
|
|
3647
3713
|
await this.persistDeliveryWatermark(this.deliveryWatermark);
|
|
@@ -3683,22 +3749,29 @@ var SyncEngine = class {
|
|
|
3683
3749
|
};
|
|
3684
3750
|
this.transport.send(ack);
|
|
3685
3751
|
}
|
|
3686
|
-
handleAcknowledgment(msg) {
|
|
3752
|
+
async handleAcknowledgment(msg) {
|
|
3687
3753
|
this.clearOutboundAckTimer();
|
|
3688
3754
|
if (this.state === "syncing" && this.config.strictHandshake) {
|
|
3689
3755
|
this.pendingDeltaBatchAcks.delete(msg.acknowledgedMessageId);
|
|
3690
3756
|
this.markDeltaSendCompleteIfReady();
|
|
3691
3757
|
}
|
|
3692
3758
|
if (this.currentBatch) {
|
|
3693
|
-
|
|
3759
|
+
if (this.currentBatchRequiresPartialAck) {
|
|
3760
|
+
await this.outboundQueue.acknowledgeThrough(
|
|
3761
|
+
this.currentBatch.batchId,
|
|
3762
|
+
msg.lastSequenceNumber
|
|
3763
|
+
);
|
|
3764
|
+
} else {
|
|
3765
|
+
await this.outboundQueue.acknowledge(this.currentBatch.batchId);
|
|
3766
|
+
}
|
|
3694
3767
|
this.currentBatch = null;
|
|
3768
|
+
this.currentBatchRequiresPartialAck = false;
|
|
3695
3769
|
const now = Date.now();
|
|
3696
3770
|
this.lastSyncedAt = now;
|
|
3697
3771
|
this.lastSuccessfulPush = now;
|
|
3698
3772
|
}
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
);
|
|
3773
|
+
await this.advanceLastAckedForLocalNode(msg.lastSequenceNumber);
|
|
3774
|
+
await this.refreshPendingCount();
|
|
3702
3775
|
if (this.state === "streaming" && this.outboundQueue.hasOperations) {
|
|
3703
3776
|
this.flushQueue();
|
|
3704
3777
|
}
|
|
@@ -3708,6 +3781,7 @@ var SyncEngine = class {
|
|
|
3708
3781
|
if (this.currentBatch) {
|
|
3709
3782
|
this.outboundQueue.returnBatch(this.currentBatch.batchId);
|
|
3710
3783
|
this.currentBatch = null;
|
|
3784
|
+
this.currentBatchRequiresPartialAck = false;
|
|
3711
3785
|
}
|
|
3712
3786
|
this.transitionTo("error");
|
|
3713
3787
|
if (msg.code === "AUTH_FAILED") {
|
|
@@ -3735,6 +3809,20 @@ var SyncEngine = class {
|
|
|
3735
3809
|
* signal, so the connection stays up.
|
|
3736
3810
|
*/
|
|
3737
3811
|
async handleOperationRejected(msg) {
|
|
3812
|
+
if (msg.retriable) {
|
|
3813
|
+
this.currentBatchRequiresPartialAck = true;
|
|
3814
|
+
await this.refreshPendingCount();
|
|
3815
|
+
this.emitter?.emit({
|
|
3816
|
+
type: "sync:operation-rejected",
|
|
3817
|
+
operationId: msg.operationId,
|
|
3818
|
+
collection: msg.collection,
|
|
3819
|
+
recordId: msg.recordId,
|
|
3820
|
+
code: msg.code,
|
|
3821
|
+
message: msg.message,
|
|
3822
|
+
retriable: true
|
|
3823
|
+
});
|
|
3824
|
+
return;
|
|
3825
|
+
}
|
|
3738
3826
|
await this.outboundQueue.reject(msg.operationId);
|
|
3739
3827
|
await this.rejectedStorage.record({
|
|
3740
3828
|
operationId: msg.operationId,
|
|
@@ -3880,6 +3968,7 @@ var SyncEngine = class {
|
|
|
3880
3968
|
const batch = this.outboundQueue.takeBatch(this.batchSize);
|
|
3881
3969
|
if (!batch) return;
|
|
3882
3970
|
this.currentBatch = batch;
|
|
3971
|
+
this.currentBatchRequiresPartialAck = false;
|
|
3883
3972
|
if (this.encryptor) {
|
|
3884
3973
|
this.encryptor.encryptBatch(batch.operations).then(
|
|
3885
3974
|
(encrypted) => {
|
|
@@ -3902,6 +3991,7 @@ var SyncEngine = class {
|
|
|
3902
3991
|
(err) => {
|
|
3903
3992
|
this.outboundQueue.returnBatch(batch.batchId);
|
|
3904
3993
|
this.currentBatch = null;
|
|
3994
|
+
this.currentBatchRequiresPartialAck = false;
|
|
3905
3995
|
this.emitter?.emit({
|
|
3906
3996
|
type: "sync:disconnected",
|
|
3907
3997
|
reason: err instanceof Error ? err.message : "Encryption failed"
|
|
@@ -3942,6 +4032,7 @@ var SyncEngine = class {
|
|
|
3942
4032
|
if (!this.currentBatch || this.currentBatch.batchId !== batchId) return;
|
|
3943
4033
|
this.outboundQueue.returnBatch(batchId);
|
|
3944
4034
|
this.currentBatch = null;
|
|
4035
|
+
this.currentBatchRequiresPartialAck = false;
|
|
3945
4036
|
this.clearOutboundAckTimer();
|
|
3946
4037
|
await this.refreshPendingCount();
|
|
3947
4038
|
if (this.state === "disconnected") return;
|
|
@@ -3966,6 +4057,7 @@ var SyncEngine = class {
|
|
|
3966
4057
|
if (this.currentBatch) {
|
|
3967
4058
|
this.outboundQueue.returnBatch(this.currentBatch.batchId);
|
|
3968
4059
|
this.currentBatch = null;
|
|
4060
|
+
this.currentBatchRequiresPartialAck = false;
|
|
3969
4061
|
}
|
|
3970
4062
|
if (this.schemaBlocked) {
|
|
3971
4063
|
return;
|
|
@@ -4056,8 +4148,7 @@ var SyncEngine = class {
|
|
|
4056
4148
|
}, 500);
|
|
4057
4149
|
}
|
|
4058
4150
|
async reconnectForQuerySubsets() {
|
|
4059
|
-
await this.
|
|
4060
|
-
await this.start();
|
|
4151
|
+
await this.reconnect();
|
|
4061
4152
|
}
|
|
4062
4153
|
};
|
|
4063
4154
|
function awarenessStatesToWire(states) {
|
|
@@ -4738,6 +4829,7 @@ function fromBase642(str) {
|
|
|
4738
4829
|
// src/reactivity/sync-status-controller.ts
|
|
4739
4830
|
var OFFLINE_SYNC_STATUS = Object.freeze({
|
|
4740
4831
|
status: "offline",
|
|
4832
|
+
reconnecting: false,
|
|
4741
4833
|
pendingOperations: 0,
|
|
4742
4834
|
lastSyncedAt: null,
|
|
4743
4835
|
lastSuccessfulPush: null,
|
|
@@ -4755,6 +4847,7 @@ var SYNC_STATUS_EVENT_TYPES = [
|
|
|
4755
4847
|
"sync:received",
|
|
4756
4848
|
"sync:acknowledged",
|
|
4757
4849
|
"sync:apply-failed",
|
|
4850
|
+
"sync:delivery-gap",
|
|
4758
4851
|
"sync:diagnostics",
|
|
4759
4852
|
"sync:initial-sync-progress"
|
|
4760
4853
|
];
|