@korajs/sync 1.0.0-beta.0 → 1.0.0-beta.10
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 +837 -68
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +228 -3
- package/dist/index.d.ts +228 -3
- package/dist/index.js +816 -55
- package/dist/index.js.map +1 -1
- package/dist/internal.d.cts +1 -1
- package/dist/internal.d.ts +1 -1
- package/dist/{transport-BMesMB6s.d.cts → transport-B2GQerqJ.d.cts} +200 -3
- package/dist/{transport-BMesMB6s.d.ts → transport-B2GQerqJ.d.ts} +200 -3
- package/package.json +4 -3
package/dist/index.cjs
CHANGED
|
@@ -31,6 +31,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
AwarenessManager: () => AwarenessManager,
|
|
34
|
+
BlobChunkChannel: () => BlobChunkChannel,
|
|
34
35
|
ChaosTransport: () => ChaosTransport,
|
|
35
36
|
ConnectionMonitor: () => ConnectionMonitor,
|
|
36
37
|
DEFAULT_RICHTEXT_DOC_CHANNEL_THRESHOLD: () => DEFAULT_RICHTEXT_DOC_CHANNEL_THRESHOLD,
|
|
@@ -40,6 +41,7 @@ __export(index_exports, {
|
|
|
40
41
|
InvalidScopeError: () => InvalidScopeError,
|
|
41
42
|
JsonMessageSerializer: () => JsonMessageSerializer,
|
|
42
43
|
KeyDerivationError: () => KeyDerivationError,
|
|
44
|
+
MemoryRejectedOperationStorage: () => MemoryRejectedOperationStorage,
|
|
43
45
|
NegotiatedMessageSerializer: () => NegotiatedMessageSerializer,
|
|
44
46
|
OFFLINE_SYNC_STATUS: () => OFFLINE_SYNC_STATUS,
|
|
45
47
|
OutboundQueue: () => OutboundQueue,
|
|
@@ -55,11 +57,13 @@ __export(index_exports, {
|
|
|
55
57
|
WebSocketTransport: () => WebSocketTransport,
|
|
56
58
|
createDeltaCursorFromBatch: () => createDeltaCursorFromBatch,
|
|
57
59
|
createSyncStatusController: () => createSyncStatusController,
|
|
60
|
+
decodeBlobChunkBytes: () => decodeBlobChunkBytes,
|
|
58
61
|
decodeDeltaCursor: () => decodeDeltaCursor,
|
|
59
62
|
decodeYjsUpdate: () => decodeYjsUpdate,
|
|
60
63
|
dedupeQuerySubsets: () => dedupeQuerySubsets,
|
|
61
64
|
deriveKey: () => deriveKey,
|
|
62
65
|
deriveVersionedKey: () => deriveVersionedKey,
|
|
66
|
+
encodeBlobChunkBytes: () => encodeBlobChunkBytes,
|
|
63
67
|
encodeDeltaCursor: () => encodeDeltaCursor,
|
|
64
68
|
encodeYjsUpdate: () => encodeYjsUpdate,
|
|
65
69
|
filterOperationsByScope: () => filterOperationsByScope,
|
|
@@ -67,12 +71,16 @@ __export(index_exports, {
|
|
|
67
71
|
getRemoteAwarenessStates: () => getRemoteAwarenessStates,
|
|
68
72
|
isAcknowledgmentMessage: () => isAcknowledgmentMessage,
|
|
69
73
|
isAwarenessUpdateMessage: () => isAwarenessUpdateMessage,
|
|
74
|
+
isBlobChunkPushMessage: () => isBlobChunkPushMessage,
|
|
75
|
+
isBlobChunkRequestMessage: () => isBlobChunkRequestMessage,
|
|
76
|
+
isBlobChunkResponseMessage: () => isBlobChunkResponseMessage,
|
|
70
77
|
isClientSchemaVersionSupported: () => isClientSchemaVersionSupported,
|
|
71
78
|
isEncryptedPayload: () => isEncryptedPayload,
|
|
72
79
|
isErrorMessage: () => isErrorMessage,
|
|
73
80
|
isHandshakeMessage: () => isHandshakeMessage,
|
|
74
81
|
isHandshakeResponseMessage: () => isHandshakeResponseMessage,
|
|
75
82
|
isOperationBatchMessage: () => isOperationBatchMessage,
|
|
83
|
+
isOperationRejectedMessage: () => isOperationRejectedMessage,
|
|
76
84
|
isSchemaMismatchReject: () => isSchemaMismatchReject,
|
|
77
85
|
isSyncMessage: () => isSyncMessage,
|
|
78
86
|
isYjsDocUpdateMessage: () => isYjsDocUpdateMessage,
|
|
@@ -98,6 +106,7 @@ var SYNC_STATES = [
|
|
|
98
106
|
];
|
|
99
107
|
var SYNC_STATUSES = [
|
|
100
108
|
"connected",
|
|
109
|
+
"reconnecting",
|
|
101
110
|
"syncing",
|
|
102
111
|
"synced",
|
|
103
112
|
"offline",
|
|
@@ -105,6 +114,20 @@ var SYNC_STATUSES = [
|
|
|
105
114
|
"error",
|
|
106
115
|
"schema-mismatch"
|
|
107
116
|
];
|
|
117
|
+
var MemoryRejectedOperationStorage = class {
|
|
118
|
+
rejected = /* @__PURE__ */ new Map();
|
|
119
|
+
async record(rejected) {
|
|
120
|
+
this.rejected.set(rejected.operationId, rejected);
|
|
121
|
+
}
|
|
122
|
+
async list() {
|
|
123
|
+
return [...this.rejected.values()];
|
|
124
|
+
}
|
|
125
|
+
async remove(operationIds) {
|
|
126
|
+
for (const id of operationIds) {
|
|
127
|
+
this.rejected.delete(id);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
};
|
|
108
131
|
var ScopeViolationError = class extends import_core.KoraError {
|
|
109
132
|
constructor(operationId, collection, scope, message) {
|
|
110
133
|
super(
|
|
@@ -302,10 +325,18 @@ function isSyncMessage(value) {
|
|
|
302
325
|
return isAcknowledgmentMessage(value);
|
|
303
326
|
case "error":
|
|
304
327
|
return isErrorMessage(value);
|
|
328
|
+
case "operation-rejected":
|
|
329
|
+
return isOperationRejectedMessage(value);
|
|
305
330
|
case "awareness-update":
|
|
306
331
|
return isAwarenessUpdateMessage(value);
|
|
307
332
|
case "yjs-doc-update":
|
|
308
333
|
return isYjsDocUpdateMessage(value);
|
|
334
|
+
case "blob-chunk-request":
|
|
335
|
+
return isBlobChunkRequestMessage(value);
|
|
336
|
+
case "blob-chunk-response":
|
|
337
|
+
return isBlobChunkResponseMessage(value);
|
|
338
|
+
case "blob-chunk-push":
|
|
339
|
+
return isBlobChunkPushMessage(value);
|
|
309
340
|
default:
|
|
310
341
|
return false;
|
|
311
342
|
}
|
|
@@ -335,6 +366,11 @@ function isErrorMessage(value) {
|
|
|
335
366
|
const msg = value;
|
|
336
367
|
return msg.type === "error" && typeof msg.messageId === "string" && typeof msg.code === "string" && typeof msg.message === "string" && typeof msg.retriable === "boolean";
|
|
337
368
|
}
|
|
369
|
+
function isOperationRejectedMessage(value) {
|
|
370
|
+
if (typeof value !== "object" || value === null) return false;
|
|
371
|
+
const msg = value;
|
|
372
|
+
return msg.type === "operation-rejected" && typeof msg.messageId === "string" && typeof msg.operationId === "string" && typeof msg.collection === "string" && typeof msg.recordId === "string" && typeof msg.code === "string" && typeof msg.message === "string" && typeof msg.retriable === "boolean";
|
|
373
|
+
}
|
|
338
374
|
function isAwarenessUpdateMessage(value) {
|
|
339
375
|
if (typeof value !== "object" || value === null) return false;
|
|
340
376
|
const msg = value;
|
|
@@ -345,6 +381,21 @@ function isYjsDocUpdateMessage(value) {
|
|
|
345
381
|
const msg = value;
|
|
346
382
|
return msg.type === "yjs-doc-update" && typeof msg.messageId === "string" && typeof msg.collection === "string" && typeof msg.recordId === "string" && typeof msg.field === "string" && typeof msg.update === "string";
|
|
347
383
|
}
|
|
384
|
+
function isBlobChunkRequestMessage(value) {
|
|
385
|
+
if (typeof value !== "object" || value === null) return false;
|
|
386
|
+
const msg = value;
|
|
387
|
+
return msg.type === "blob-chunk-request" && typeof msg.messageId === "string" && typeof msg.requestId === "string" && typeof msg.hash === "string";
|
|
388
|
+
}
|
|
389
|
+
function isBlobChunkResponseMessage(value) {
|
|
390
|
+
if (typeof value !== "object" || value === null) return false;
|
|
391
|
+
const msg = value;
|
|
392
|
+
return msg.type === "blob-chunk-response" && typeof msg.messageId === "string" && typeof msg.requestId === "string" && (typeof msg.bytes === "string" || msg.bytes === null);
|
|
393
|
+
}
|
|
394
|
+
function isBlobChunkPushMessage(value) {
|
|
395
|
+
if (typeof value !== "object" || value === null) return false;
|
|
396
|
+
const msg = value;
|
|
397
|
+
return msg.type === "blob-chunk-push" && typeof msg.messageId === "string" && typeof msg.hash === "string" && typeof msg.bytes === "string";
|
|
398
|
+
}
|
|
348
399
|
|
|
349
400
|
// src/protocol/serializer.ts
|
|
350
401
|
var import_core2 = require("@korajs/core");
|
|
@@ -570,7 +621,8 @@ function toProtoEnvelope(message) {
|
|
|
570
621
|
})),
|
|
571
622
|
schemaVersion: message.schemaVersion,
|
|
572
623
|
authToken: message.authToken,
|
|
573
|
-
supportedWireFormats: message.supportedWireFormats
|
|
624
|
+
supportedWireFormats: message.supportedWireFormats,
|
|
625
|
+
...message.lastDeliverySequence !== void 0 ? { lastDeliverySequence: message.lastDeliverySequence } : {}
|
|
574
626
|
};
|
|
575
627
|
case "handshake-response":
|
|
576
628
|
return {
|
|
@@ -585,7 +637,9 @@ function toProtoEnvelope(message) {
|
|
|
585
637
|
accepted: message.accepted,
|
|
586
638
|
rejectReason: message.rejectReason,
|
|
587
639
|
selectedWireFormat: message.selectedWireFormat,
|
|
588
|
-
serverTime: message.serverTime
|
|
640
|
+
serverTime: message.serverTime,
|
|
641
|
+
blobStorageEnabled: message.blobStorageEnabled,
|
|
642
|
+
...message.serverMaxDeliverySequence !== void 0 ? { serverMaxDeliverySequence: message.serverMaxDeliverySequence } : {}
|
|
589
643
|
};
|
|
590
644
|
case "operation-batch":
|
|
591
645
|
return {
|
|
@@ -593,14 +647,17 @@ function toProtoEnvelope(message) {
|
|
|
593
647
|
messageId: message.messageId,
|
|
594
648
|
operations: message.operations.map(serializeProtoOperation),
|
|
595
649
|
isFinal: message.isFinal,
|
|
596
|
-
batchIndex: message.batchIndex
|
|
650
|
+
batchIndex: message.batchIndex,
|
|
651
|
+
...message.baseDeliverySequence !== void 0 ? { baseDeliverySequence: message.baseDeliverySequence } : {},
|
|
652
|
+
...message.maxDeliverySequence !== void 0 ? { maxDeliverySequence: message.maxDeliverySequence } : {}
|
|
597
653
|
};
|
|
598
654
|
case "acknowledgment":
|
|
599
655
|
return {
|
|
600
656
|
type: message.type,
|
|
601
657
|
messageId: message.messageId,
|
|
602
658
|
acknowledgedMessageId: message.acknowledgedMessageId,
|
|
603
|
-
lastSequenceNumber: message.lastSequenceNumber
|
|
659
|
+
lastSequenceNumber: message.lastSequenceNumber,
|
|
660
|
+
...message.deliverySequence !== void 0 ? { deliverySequence: message.deliverySequence } : {}
|
|
604
661
|
};
|
|
605
662
|
case "error":
|
|
606
663
|
return {
|
|
@@ -610,12 +667,45 @@ function toProtoEnvelope(message) {
|
|
|
610
667
|
errorMessage: message.message,
|
|
611
668
|
retriable: message.retriable
|
|
612
669
|
};
|
|
670
|
+
case "operation-rejected":
|
|
671
|
+
return {
|
|
672
|
+
type: message.type,
|
|
673
|
+
messageId: message.messageId,
|
|
674
|
+
operationId: message.operationId,
|
|
675
|
+
collection: message.collection,
|
|
676
|
+
recordId: message.recordId,
|
|
677
|
+
errorCode: message.code,
|
|
678
|
+
errorMessage: message.message,
|
|
679
|
+
retriable: message.retriable
|
|
680
|
+
};
|
|
613
681
|
case "awareness-update":
|
|
614
682
|
case "yjs-doc-update":
|
|
615
683
|
return {
|
|
616
684
|
type: message.type,
|
|
617
685
|
messageId: message.messageId
|
|
618
686
|
};
|
|
687
|
+
case "blob-chunk-request":
|
|
688
|
+
return {
|
|
689
|
+
type: message.type,
|
|
690
|
+
messageId: message.messageId,
|
|
691
|
+
requestId: message.requestId,
|
|
692
|
+
hash: message.hash
|
|
693
|
+
};
|
|
694
|
+
case "blob-chunk-response":
|
|
695
|
+
return {
|
|
696
|
+
type: message.type,
|
|
697
|
+
messageId: message.messageId,
|
|
698
|
+
requestId: message.requestId,
|
|
699
|
+
hasBytes: message.bytes !== null,
|
|
700
|
+
...message.bytes !== null ? { chunkBytes: message.bytes } : {}
|
|
701
|
+
};
|
|
702
|
+
case "blob-chunk-push":
|
|
703
|
+
return {
|
|
704
|
+
type: message.type,
|
|
705
|
+
messageId: message.messageId,
|
|
706
|
+
hash: message.hash,
|
|
707
|
+
chunkBytes: message.bytes
|
|
708
|
+
};
|
|
619
709
|
}
|
|
620
710
|
}
|
|
621
711
|
function fromProtoEnvelope(envelope) {
|
|
@@ -632,7 +722,8 @@ function fromProtoEnvelope(envelope) {
|
|
|
632
722
|
authToken: envelope.authToken,
|
|
633
723
|
supportedWireFormats: envelope.supportedWireFormats?.filter(
|
|
634
724
|
(format) => format === "json" || format === "protobuf"
|
|
635
|
-
)
|
|
725
|
+
),
|
|
726
|
+
...envelope.lastDeliverySequence !== void 0 ? { lastDeliverySequence: envelope.lastDeliverySequence } : {}
|
|
636
727
|
};
|
|
637
728
|
case "handshake-response":
|
|
638
729
|
return {
|
|
@@ -648,7 +739,9 @@ function fromProtoEnvelope(envelope) {
|
|
|
648
739
|
selectedWireFormat: envelope.selectedWireFormat === "json" || envelope.selectedWireFormat === "protobuf" ? envelope.selectedWireFormat : void 0,
|
|
649
740
|
// Preserve the server's wall-clock time so clock-skew detection and
|
|
650
741
|
// automatic timestamp rebase work over the protobuf wire, not just JSON.
|
|
651
|
-
...envelope.serverTime !== void 0 ? { serverTime: envelope.serverTime } : {}
|
|
742
|
+
...envelope.serverTime !== void 0 ? { serverTime: envelope.serverTime } : {},
|
|
743
|
+
...envelope.blobStorageEnabled !== void 0 ? { blobStorageEnabled: envelope.blobStorageEnabled } : {},
|
|
744
|
+
...envelope.serverMaxDeliverySequence !== void 0 ? { serverMaxDeliverySequence: envelope.serverMaxDeliverySequence } : {}
|
|
652
745
|
};
|
|
653
746
|
case "operation-batch":
|
|
654
747
|
return {
|
|
@@ -656,14 +749,17 @@ function fromProtoEnvelope(envelope) {
|
|
|
656
749
|
messageId: envelope.messageId,
|
|
657
750
|
operations: (envelope.operations ?? []).map(deserializeProtoOperation),
|
|
658
751
|
isFinal: envelope.isFinal ?? false,
|
|
659
|
-
batchIndex: envelope.batchIndex ?? 0
|
|
752
|
+
batchIndex: envelope.batchIndex ?? 0,
|
|
753
|
+
...envelope.baseDeliverySequence !== void 0 ? { baseDeliverySequence: envelope.baseDeliverySequence } : {},
|
|
754
|
+
...envelope.maxDeliverySequence !== void 0 ? { maxDeliverySequence: envelope.maxDeliverySequence } : {}
|
|
660
755
|
};
|
|
661
756
|
case "acknowledgment":
|
|
662
757
|
return {
|
|
663
758
|
type: "acknowledgment",
|
|
664
759
|
messageId: envelope.messageId,
|
|
665
760
|
acknowledgedMessageId: envelope.acknowledgedMessageId ?? "",
|
|
666
|
-
lastSequenceNumber: envelope.lastSequenceNumber ?? 0
|
|
761
|
+
lastSequenceNumber: envelope.lastSequenceNumber ?? 0,
|
|
762
|
+
...envelope.deliverySequence !== void 0 ? { deliverySequence: envelope.deliverySequence } : {}
|
|
667
763
|
};
|
|
668
764
|
case "error":
|
|
669
765
|
return {
|
|
@@ -673,6 +769,38 @@ function fromProtoEnvelope(envelope) {
|
|
|
673
769
|
message: envelope.errorMessage ?? "Unknown error",
|
|
674
770
|
retriable: envelope.retriable ?? false
|
|
675
771
|
};
|
|
772
|
+
case "operation-rejected":
|
|
773
|
+
return {
|
|
774
|
+
type: "operation-rejected",
|
|
775
|
+
messageId: envelope.messageId,
|
|
776
|
+
operationId: envelope.operationId ?? "",
|
|
777
|
+
collection: envelope.collection ?? "",
|
|
778
|
+
recordId: envelope.recordId ?? "",
|
|
779
|
+
code: envelope.errorCode ?? "REJECTED",
|
|
780
|
+
message: envelope.errorMessage ?? "",
|
|
781
|
+
retriable: envelope.retriable ?? false
|
|
782
|
+
};
|
|
783
|
+
case "blob-chunk-request":
|
|
784
|
+
return {
|
|
785
|
+
type: "blob-chunk-request",
|
|
786
|
+
messageId: envelope.messageId,
|
|
787
|
+
requestId: envelope.requestId ?? "",
|
|
788
|
+
hash: envelope.hash ?? ""
|
|
789
|
+
};
|
|
790
|
+
case "blob-chunk-response":
|
|
791
|
+
return {
|
|
792
|
+
type: "blob-chunk-response",
|
|
793
|
+
messageId: envelope.messageId,
|
|
794
|
+
requestId: envelope.requestId ?? "",
|
|
795
|
+
bytes: envelope.hasBytes ? envelope.chunkBytes ?? "" : null
|
|
796
|
+
};
|
|
797
|
+
case "blob-chunk-push":
|
|
798
|
+
return {
|
|
799
|
+
type: "blob-chunk-push",
|
|
800
|
+
messageId: envelope.messageId,
|
|
801
|
+
hash: envelope.hash ?? "",
|
|
802
|
+
bytes: envelope.chunkBytes ?? ""
|
|
803
|
+
};
|
|
676
804
|
default:
|
|
677
805
|
throw new import_core2.SyncError("Failed to decode sync message: unknown protobuf type", {
|
|
678
806
|
type: envelope.type
|
|
@@ -818,6 +946,29 @@ function encodeEnvelope(envelope) {
|
|
|
818
946
|
}
|
|
819
947
|
if (envelope.retriable !== void 0) writer.uint32(144).bool(envelope.retriable);
|
|
820
948
|
if (envelope.serverTime !== void 0) writer.uint32(152).int64(envelope.serverTime);
|
|
949
|
+
if (envelope.requestId && envelope.requestId.length > 0)
|
|
950
|
+
writer.uint32(162).string(envelope.requestId);
|
|
951
|
+
if (envelope.hash && envelope.hash.length > 0) writer.uint32(170).string(envelope.hash);
|
|
952
|
+
if (envelope.chunkBytes && envelope.chunkBytes.length > 0)
|
|
953
|
+
writer.uint32(178).string(envelope.chunkBytes);
|
|
954
|
+
if (envelope.hasBytes !== void 0) writer.uint32(184).bool(envelope.hasBytes);
|
|
955
|
+
if (envelope.blobStorageEnabled !== void 0)
|
|
956
|
+
writer.uint32(200).bool(envelope.blobStorageEnabled);
|
|
957
|
+
if (envelope.operationId && envelope.operationId.length > 0)
|
|
958
|
+
writer.uint32(210).string(envelope.operationId);
|
|
959
|
+
if (envelope.collection && envelope.collection.length > 0)
|
|
960
|
+
writer.uint32(218).string(envelope.collection);
|
|
961
|
+
if (envelope.recordId && envelope.recordId.length > 0)
|
|
962
|
+
writer.uint32(226).string(envelope.recordId);
|
|
963
|
+
if (envelope.lastDeliverySequence !== void 0)
|
|
964
|
+
writer.uint32(232).int64(envelope.lastDeliverySequence);
|
|
965
|
+
if (envelope.baseDeliverySequence !== void 0)
|
|
966
|
+
writer.uint32(240).int64(envelope.baseDeliverySequence);
|
|
967
|
+
if (envelope.maxDeliverySequence !== void 0)
|
|
968
|
+
writer.uint32(248).int64(envelope.maxDeliverySequence);
|
|
969
|
+
if (envelope.deliverySequence !== void 0) writer.uint32(256).int64(envelope.deliverySequence);
|
|
970
|
+
if (envelope.serverMaxDeliverySequence !== void 0)
|
|
971
|
+
writer.uint32(264).int64(envelope.serverMaxDeliverySequence);
|
|
821
972
|
return writer.finish();
|
|
822
973
|
}
|
|
823
974
|
function decodeEnvelope(bytes) {
|
|
@@ -889,6 +1040,45 @@ function decodeEnvelope(bytes) {
|
|
|
889
1040
|
case 19:
|
|
890
1041
|
envelope.serverTime = longToNumber(reader.int64());
|
|
891
1042
|
break;
|
|
1043
|
+
case 20:
|
|
1044
|
+
envelope.requestId = reader.string();
|
|
1045
|
+
break;
|
|
1046
|
+
case 21:
|
|
1047
|
+
envelope.hash = reader.string();
|
|
1048
|
+
break;
|
|
1049
|
+
case 22:
|
|
1050
|
+
envelope.chunkBytes = reader.string();
|
|
1051
|
+
break;
|
|
1052
|
+
case 23:
|
|
1053
|
+
envelope.hasBytes = reader.bool();
|
|
1054
|
+
break;
|
|
1055
|
+
case 25:
|
|
1056
|
+
envelope.blobStorageEnabled = reader.bool();
|
|
1057
|
+
break;
|
|
1058
|
+
case 26:
|
|
1059
|
+
envelope.operationId = reader.string();
|
|
1060
|
+
break;
|
|
1061
|
+
case 27:
|
|
1062
|
+
envelope.collection = reader.string();
|
|
1063
|
+
break;
|
|
1064
|
+
case 28:
|
|
1065
|
+
envelope.recordId = reader.string();
|
|
1066
|
+
break;
|
|
1067
|
+
case 29:
|
|
1068
|
+
envelope.lastDeliverySequence = longToNumber(reader.int64());
|
|
1069
|
+
break;
|
|
1070
|
+
case 30:
|
|
1071
|
+
envelope.baseDeliverySequence = longToNumber(reader.int64());
|
|
1072
|
+
break;
|
|
1073
|
+
case 31:
|
|
1074
|
+
envelope.maxDeliverySequence = longToNumber(reader.int64());
|
|
1075
|
+
break;
|
|
1076
|
+
case 32:
|
|
1077
|
+
envelope.deliverySequence = longToNumber(reader.int64());
|
|
1078
|
+
break;
|
|
1079
|
+
case 33:
|
|
1080
|
+
envelope.serverMaxDeliverySequence = longToNumber(reader.int64());
|
|
1081
|
+
break;
|
|
892
1082
|
default:
|
|
893
1083
|
reader.skipType(tag & 7);
|
|
894
1084
|
}
|
|
@@ -1388,6 +1578,7 @@ var ChaosTransport = class {
|
|
|
1388
1578
|
reorderRate;
|
|
1389
1579
|
maxLatency;
|
|
1390
1580
|
random;
|
|
1581
|
+
dropPredicate;
|
|
1391
1582
|
messageHandler = null;
|
|
1392
1583
|
reorderBuffer = [];
|
|
1393
1584
|
pending = [];
|
|
@@ -1398,6 +1589,7 @@ var ChaosTransport = class {
|
|
|
1398
1589
|
this.reorderRate = config?.reorderRate ?? 0;
|
|
1399
1590
|
this.maxLatency = config?.maxLatency ?? 0;
|
|
1400
1591
|
this.random = config?.randomSource ?? Math.random;
|
|
1592
|
+
this.dropPredicate = config?.dropPredicate ?? null;
|
|
1401
1593
|
}
|
|
1402
1594
|
async connect(url, options) {
|
|
1403
1595
|
this.inner.onMessage((msg) => this.handleIncoming(msg));
|
|
@@ -1416,6 +1608,9 @@ var ChaosTransport = class {
|
|
|
1416
1608
|
return this.inner.disconnect();
|
|
1417
1609
|
}
|
|
1418
1610
|
send(message) {
|
|
1611
|
+
if (this.dropPredicate?.(message, "outgoing")) {
|
|
1612
|
+
return;
|
|
1613
|
+
}
|
|
1419
1614
|
if (this.random() < this.dropRate) {
|
|
1420
1615
|
return;
|
|
1421
1616
|
}
|
|
@@ -1443,6 +1638,9 @@ var ChaosTransport = class {
|
|
|
1443
1638
|
}
|
|
1444
1639
|
handleIncoming(message) {
|
|
1445
1640
|
if (!this.messageHandler) return;
|
|
1641
|
+
if (this.dropPredicate?.(message, "incoming")) {
|
|
1642
|
+
return;
|
|
1643
|
+
}
|
|
1446
1644
|
if (this.random() < this.dropRate) {
|
|
1447
1645
|
return;
|
|
1448
1646
|
}
|
|
@@ -1480,7 +1678,7 @@ var ChaosTransport = class {
|
|
|
1480
1678
|
};
|
|
1481
1679
|
|
|
1482
1680
|
// src/engine/sync-engine.ts
|
|
1483
|
-
var
|
|
1681
|
+
var import_core7 = require("@korajs/core");
|
|
1484
1682
|
var import_internal2 = require("@korajs/core/internal");
|
|
1485
1683
|
|
|
1486
1684
|
// src/awareness/awareness-manager.ts
|
|
@@ -1689,6 +1887,95 @@ var AwarenessManager = class {
|
|
|
1689
1887
|
}
|
|
1690
1888
|
};
|
|
1691
1889
|
|
|
1890
|
+
// src/blob/blob-chunk-channel.ts
|
|
1891
|
+
var import_core5 = require("@korajs/core");
|
|
1892
|
+
function encodeBlobChunkBytes(bytes) {
|
|
1893
|
+
let binary = "";
|
|
1894
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
1895
|
+
binary += String.fromCharCode(bytes[i]);
|
|
1896
|
+
}
|
|
1897
|
+
return btoa(binary);
|
|
1898
|
+
}
|
|
1899
|
+
function decodeBlobChunkBytes(encoded) {
|
|
1900
|
+
const binary = atob(encoded);
|
|
1901
|
+
const out = new Uint8Array(binary.length);
|
|
1902
|
+
for (let i = 0; i < binary.length; i++) {
|
|
1903
|
+
out[i] = binary.charCodeAt(i);
|
|
1904
|
+
}
|
|
1905
|
+
return out;
|
|
1906
|
+
}
|
|
1907
|
+
var BlobChunkChannel = class {
|
|
1908
|
+
onSend;
|
|
1909
|
+
handlers = /* @__PURE__ */ new Set();
|
|
1910
|
+
constructor(options) {
|
|
1911
|
+
this.onSend = options?.onSend ?? null;
|
|
1912
|
+
}
|
|
1913
|
+
/**
|
|
1914
|
+
* Send a blob-chunk message to the peer. Response bytes are base64-encoded
|
|
1915
|
+
* onto the wire here; callers pass a `Uint8Array`.
|
|
1916
|
+
*/
|
|
1917
|
+
send(message) {
|
|
1918
|
+
if (!this.onSend) {
|
|
1919
|
+
return;
|
|
1920
|
+
}
|
|
1921
|
+
if (message.type === "blob-chunk-request") {
|
|
1922
|
+
this.onSend({
|
|
1923
|
+
type: "blob-chunk-request",
|
|
1924
|
+
messageId: (0, import_core5.generateUUIDv7)(),
|
|
1925
|
+
requestId: message.requestId,
|
|
1926
|
+
hash: message.hash
|
|
1927
|
+
});
|
|
1928
|
+
return;
|
|
1929
|
+
}
|
|
1930
|
+
if (message.type === "blob-chunk-push") {
|
|
1931
|
+
this.onSend({
|
|
1932
|
+
type: "blob-chunk-push",
|
|
1933
|
+
messageId: (0, import_core5.generateUUIDv7)(),
|
|
1934
|
+
hash: message.hash,
|
|
1935
|
+
bytes: encodeBlobChunkBytes(message.bytes)
|
|
1936
|
+
});
|
|
1937
|
+
return;
|
|
1938
|
+
}
|
|
1939
|
+
this.onSend({
|
|
1940
|
+
type: "blob-chunk-response",
|
|
1941
|
+
messageId: (0, import_core5.generateUUIDv7)(),
|
|
1942
|
+
requestId: message.requestId,
|
|
1943
|
+
bytes: message.bytes === null ? null : encodeBlobChunkBytes(message.bytes)
|
|
1944
|
+
});
|
|
1945
|
+
}
|
|
1946
|
+
/**
|
|
1947
|
+
* Register a handler for inbound blob-chunk messages. Multiple handlers are
|
|
1948
|
+
* allowed; each receives every delivered message.
|
|
1949
|
+
*/
|
|
1950
|
+
onMessage(handler) {
|
|
1951
|
+
this.handlers.add(handler);
|
|
1952
|
+
return () => {
|
|
1953
|
+
this.handlers.delete(handler);
|
|
1954
|
+
};
|
|
1955
|
+
}
|
|
1956
|
+
/**
|
|
1957
|
+
* Deliver an inbound wire message to registered handlers, decoding response
|
|
1958
|
+
* bytes from base64 back to a `Uint8Array` first.
|
|
1959
|
+
*/
|
|
1960
|
+
deliver(message) {
|
|
1961
|
+
if (this.handlers.size === 0) {
|
|
1962
|
+
return;
|
|
1963
|
+
}
|
|
1964
|
+
const decoded = message.type === "blob-chunk-request" ? { type: "blob-chunk-request", requestId: message.requestId, hash: message.hash } : message.type === "blob-chunk-push" ? {
|
|
1965
|
+
type: "blob-chunk-push",
|
|
1966
|
+
hash: message.hash,
|
|
1967
|
+
bytes: decodeBlobChunkBytes(message.bytes)
|
|
1968
|
+
} : {
|
|
1969
|
+
type: "blob-chunk-response",
|
|
1970
|
+
requestId: message.requestId,
|
|
1971
|
+
bytes: message.bytes === null ? null : decodeBlobChunkBytes(message.bytes)
|
|
1972
|
+
};
|
|
1973
|
+
for (const handler of this.handlers) {
|
|
1974
|
+
handler(decoded);
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
};
|
|
1978
|
+
|
|
1692
1979
|
// src/diagnostics/bandwidth-estimator.ts
|
|
1693
1980
|
var BandwidthEstimator = class {
|
|
1694
1981
|
maxSamples;
|
|
@@ -2132,7 +2419,7 @@ var SyncMetricsCollector = class {
|
|
|
2132
2419
|
};
|
|
2133
2420
|
|
|
2134
2421
|
// src/richtext/richtext-doc-channel.ts
|
|
2135
|
-
var
|
|
2422
|
+
var import_core6 = require("@korajs/core");
|
|
2136
2423
|
|
|
2137
2424
|
// src/richtext/doc-channel-wire.ts
|
|
2138
2425
|
function encodeYjsUpdate(update) {
|
|
@@ -2211,7 +2498,7 @@ var RichtextDocChannel = class {
|
|
|
2211
2498
|
}
|
|
2212
2499
|
this.onSend({
|
|
2213
2500
|
type: "yjs-doc-update",
|
|
2214
|
-
messageId: (0,
|
|
2501
|
+
messageId: (0, import_core6.generateUUIDv7)(),
|
|
2215
2502
|
collection,
|
|
2216
2503
|
recordId,
|
|
2217
2504
|
field,
|
|
@@ -2318,6 +2605,32 @@ var OutboundQueue = class {
|
|
|
2318
2605
|
const ids = ops.map((op) => op.id);
|
|
2319
2606
|
await this.storage.dequeue(ids);
|
|
2320
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
|
+
}
|
|
2321
2634
|
/**
|
|
2322
2635
|
* Return a failed batch to the front of the queue for retry.
|
|
2323
2636
|
* Prepends the operations to maintain priority.
|
|
@@ -2400,6 +2713,37 @@ var OutboundQueue = class {
|
|
|
2400
2713
|
get isInitialized() {
|
|
2401
2714
|
return this.initialized;
|
|
2402
2715
|
}
|
|
2716
|
+
/**
|
|
2717
|
+
* Remove a single operation the server permanently rejected — from the queue,
|
|
2718
|
+
* from any in-flight batch, and from durable storage — so a later batch ack or
|
|
2719
|
+
* a reconnect `returnBatch` can never resend or resurrect it. Returns the
|
|
2720
|
+
* removed operation so the caller can record it in a durable rejected store,
|
|
2721
|
+
* or null if it was not present (already acknowledged and removed).
|
|
2722
|
+
*/
|
|
2723
|
+
async reject(opId) {
|
|
2724
|
+
let removed = null;
|
|
2725
|
+
for (const [batchId, ops] of this.inFlight) {
|
|
2726
|
+
const idx = ops.findIndex((op) => op.id === opId);
|
|
2727
|
+
if (idx !== -1) {
|
|
2728
|
+
removed = ops[idx] ?? null;
|
|
2729
|
+
ops.splice(idx, 1);
|
|
2730
|
+
if (ops.length === 0) {
|
|
2731
|
+
this.inFlight.delete(batchId);
|
|
2732
|
+
}
|
|
2733
|
+
break;
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
if (!removed) {
|
|
2737
|
+
const idx = this.queue.findIndex((op) => op.id === opId);
|
|
2738
|
+
if (idx !== -1) {
|
|
2739
|
+
removed = this.queue[idx] ?? null;
|
|
2740
|
+
this.queue.splice(idx, 1);
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
this.seen.delete(opId);
|
|
2744
|
+
await this.storage.dequeue([opId]);
|
|
2745
|
+
return removed;
|
|
2746
|
+
}
|
|
2403
2747
|
/**
|
|
2404
2748
|
* Remove operations by id from queue and persistent storage.
|
|
2405
2749
|
* Used when ops were already sent during handshake delta exchange.
|
|
@@ -2418,6 +2762,7 @@ var OutboundQueue = class {
|
|
|
2418
2762
|
// src/engine/sync-engine.ts
|
|
2419
2763
|
var DEFAULT_BATCH_SIZE = 100;
|
|
2420
2764
|
var DEFAULT_SCHEMA_VERSION = 1;
|
|
2765
|
+
var DEFAULT_OUTBOUND_ACK_TIMEOUT_MS = 3e4;
|
|
2421
2766
|
var VALID_TRANSITIONS = {
|
|
2422
2767
|
disconnected: ["connecting"],
|
|
2423
2768
|
connecting: ["handshaking", "error", "disconnected"],
|
|
@@ -2431,6 +2776,17 @@ var nextQuerySubsetId = 0;
|
|
|
2431
2776
|
function generateMessageId() {
|
|
2432
2777
|
return `msg-${Date.now()}-${nextMessageId++}`;
|
|
2433
2778
|
}
|
|
2779
|
+
var MAX_DELIVERY_VIEW_WATERMARKS = 64;
|
|
2780
|
+
function stableStringify(value) {
|
|
2781
|
+
if (value === null || typeof value !== "object") {
|
|
2782
|
+
return JSON.stringify(value) ?? "null";
|
|
2783
|
+
}
|
|
2784
|
+
if (Array.isArray(value)) {
|
|
2785
|
+
return `[${value.map(stableStringify).join(",")}]`;
|
|
2786
|
+
}
|
|
2787
|
+
const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`);
|
|
2788
|
+
return `{${entries.join(",")}}`;
|
|
2789
|
+
}
|
|
2434
2790
|
var SyncEngine = class {
|
|
2435
2791
|
state = "disconnected";
|
|
2436
2792
|
transport;
|
|
@@ -2439,10 +2795,13 @@ var SyncEngine = class {
|
|
|
2439
2795
|
serializer;
|
|
2440
2796
|
emitter;
|
|
2441
2797
|
outboundQueue;
|
|
2798
|
+
rejectedStorage;
|
|
2442
2799
|
batchSize;
|
|
2800
|
+
outboundAckTimeoutMs;
|
|
2443
2801
|
encryptor;
|
|
2444
2802
|
awarenessManager;
|
|
2445
2803
|
richtextDocChannel;
|
|
2804
|
+
blobChunkChannel;
|
|
2446
2805
|
metricsCollector;
|
|
2447
2806
|
syncState;
|
|
2448
2807
|
remoteVector = /* @__PURE__ */ new Map();
|
|
@@ -2453,10 +2812,16 @@ var SyncEngine = class {
|
|
|
2453
2812
|
lastSuccessfulPull = null;
|
|
2454
2813
|
conflictCount = 0;
|
|
2455
2814
|
currentBatch = null;
|
|
2815
|
+
currentBatchRequiresPartialAck = false;
|
|
2456
2816
|
reconnecting = false;
|
|
2457
2817
|
schemaBlocked = false;
|
|
2458
2818
|
clockBlocked = false;
|
|
2459
2819
|
clockSkewMs = null;
|
|
2820
|
+
blobStorageEnabled = false;
|
|
2821
|
+
outboundAckTimer = null;
|
|
2822
|
+
startPromise = null;
|
|
2823
|
+
stopPromise = null;
|
|
2824
|
+
reconnectPromise = null;
|
|
2460
2825
|
// Track delta exchange state
|
|
2461
2826
|
deltaBatchesReceived = 0;
|
|
2462
2827
|
deltaReceiveComplete = false;
|
|
@@ -2477,6 +2842,27 @@ var SyncEngine = class {
|
|
|
2477
2842
|
/** Resume cursor for paginated initial sync (persisted across reconnects) */
|
|
2478
2843
|
resumeDeltaCursor = null;
|
|
2479
2844
|
initialSyncTotalBatches = 0;
|
|
2845
|
+
/**
|
|
2846
|
+
* The delivery watermark for the CURRENT sync view: the highest server delivery
|
|
2847
|
+
* sequence up to which every operation in the current view (scope + active query
|
|
2848
|
+
* subscriptions) has been contiguously applied. Advanced only through the server's
|
|
2849
|
+
* gap-free delivery stream (never through live relay), so it is a durable lower bound
|
|
2850
|
+
* on what this client holds for this view. Reported at handshake to resume the stream.
|
|
2851
|
+
*
|
|
2852
|
+
* The watermark is keyed by view: switching views (a scope or subscription change)
|
|
2853
|
+
* saves the current view's watermark and loads the target view's, so returning to a
|
|
2854
|
+
* previously-synced view resumes exactly where it left off instead of re-syncing. This
|
|
2855
|
+
* is correct because a view's watermark is a lower bound specific to that view's
|
|
2856
|
+
* filter; a widened view is simply a different (initially unsynced) view.
|
|
2857
|
+
*/
|
|
2858
|
+
deliveryWatermark = 0;
|
|
2859
|
+
/**
|
|
2860
|
+
* In-memory cache of watermark per view signature, preloaded from persistence at start
|
|
2861
|
+
* so a view switch resolves synchronously (no race with the reconnect it triggers).
|
|
2862
|
+
*/
|
|
2863
|
+
deliverySignatureWatermarks = /* @__PURE__ */ new Map();
|
|
2864
|
+
deliveryGapRepeatCount = 0;
|
|
2865
|
+
lastDeliveryGapKey = null;
|
|
2480
2866
|
constructor(options) {
|
|
2481
2867
|
this.transport = options.transport;
|
|
2482
2868
|
this.store = options.store;
|
|
@@ -2484,11 +2870,13 @@ var SyncEngine = class {
|
|
|
2484
2870
|
this.serializer = options.serializer ?? new NegotiatedMessageSerializer("json");
|
|
2485
2871
|
this.emitter = options.emitter ?? null;
|
|
2486
2872
|
this.batchSize = options.config.batchSize ?? DEFAULT_BATCH_SIZE;
|
|
2873
|
+
this.outboundAckTimeoutMs = options.config.outboundAckTimeoutMs ?? DEFAULT_OUTBOUND_ACK_TIMEOUT_MS;
|
|
2487
2874
|
this.encryptor = options.encryptor ?? null;
|
|
2488
2875
|
this.syncState = options.syncState ?? null;
|
|
2489
2876
|
this.activeScope = options.config.scopeMap;
|
|
2490
2877
|
const queueStorage = options.queueStorage ?? new MemoryQueueStorage();
|
|
2491
2878
|
this.outboundQueue = new OutboundQueue(queueStorage);
|
|
2879
|
+
this.rejectedStorage = options.rejectedStorage ?? new MemoryRejectedOperationStorage();
|
|
2492
2880
|
this.metricsCollector = new SyncMetricsCollector(options.metricsConfig);
|
|
2493
2881
|
if (this.emitter) {
|
|
2494
2882
|
this.metricsCollector.attachEmitter(this.emitter);
|
|
@@ -2505,6 +2893,14 @@ var SyncEngine = class {
|
|
|
2505
2893
|
this.transport.send(message);
|
|
2506
2894
|
}
|
|
2507
2895
|
});
|
|
2896
|
+
this.blobChunkChannel = new BlobChunkChannel({
|
|
2897
|
+
onSend: (message) => {
|
|
2898
|
+
if (this.state !== "streaming") {
|
|
2899
|
+
return;
|
|
2900
|
+
}
|
|
2901
|
+
this.transport.send(message);
|
|
2902
|
+
}
|
|
2903
|
+
});
|
|
2508
2904
|
this.awarenessManager.onSend((message) => {
|
|
2509
2905
|
if (this.state !== "streaming") return;
|
|
2510
2906
|
const wireMessage = {
|
|
@@ -2520,10 +2916,20 @@ var SyncEngine = class {
|
|
|
2520
2916
|
* Start the sync engine: connect → handshake → delta exchange → streaming.
|
|
2521
2917
|
*/
|
|
2522
2918
|
async start() {
|
|
2919
|
+
if (this.state === "streaming") return;
|
|
2920
|
+
if (this.startPromise) return this.startPromise;
|
|
2921
|
+
this.startPromise = this.startInternal().finally(() => {
|
|
2922
|
+
this.startPromise = null;
|
|
2923
|
+
});
|
|
2924
|
+
return this.startPromise;
|
|
2925
|
+
}
|
|
2926
|
+
async startInternal() {
|
|
2927
|
+
if (this.stopPromise) {
|
|
2928
|
+
await this.stopPromise;
|
|
2929
|
+
}
|
|
2930
|
+
if (this.state === "streaming") return;
|
|
2523
2931
|
if (this.state !== "disconnected") {
|
|
2524
|
-
|
|
2525
|
-
currentState: this.state
|
|
2526
|
-
});
|
|
2932
|
+
await this.stop();
|
|
2527
2933
|
}
|
|
2528
2934
|
await this.outboundQueue.initialize();
|
|
2529
2935
|
if (this.syncState) {
|
|
@@ -2531,6 +2937,16 @@ var SyncEngine = class {
|
|
|
2531
2937
|
if (this.syncState.loadDeltaCursor) {
|
|
2532
2938
|
this.resumeDeltaCursor = await this.syncState.loadDeltaCursor();
|
|
2533
2939
|
}
|
|
2940
|
+
if (this.syncState.loadAllDeliveryWatermarks) {
|
|
2941
|
+
const all = await this.syncState.loadAllDeliveryWatermarks();
|
|
2942
|
+
for (const [signature, watermark] of Object.entries(all)) {
|
|
2943
|
+
this.deliverySignatureWatermarks.set(signature, watermark);
|
|
2944
|
+
}
|
|
2945
|
+
} else if (this.syncState.loadDeliveryWatermark) {
|
|
2946
|
+
this.deliverySignatureWatermarks.set("", await this.syncState.loadDeliveryWatermark(""));
|
|
2947
|
+
}
|
|
2948
|
+
this.deliveryWatermark = this.deliverySignatureWatermarks.get(this.deliverySignature()) ?? 0;
|
|
2949
|
+
this.evictColdViewWatermarks();
|
|
2534
2950
|
}
|
|
2535
2951
|
await this.reconcileOutboundFromOpLog();
|
|
2536
2952
|
await this.refreshPendingCount();
|
|
@@ -2538,7 +2954,7 @@ var SyncEngine = class {
|
|
|
2538
2954
|
this.transport.onClose((reason) => this.handleTransportClose(reason));
|
|
2539
2955
|
this.transport.onError((err) => this.handleTransportError(err));
|
|
2540
2956
|
if (this.schemaBlocked) {
|
|
2541
|
-
throw new
|
|
2957
|
+
throw new import_core7.SyncError(
|
|
2542
2958
|
"Sync is blocked due to schema version mismatch. Upgrade the app schema or align sync.schemaVersion with the server.",
|
|
2543
2959
|
{ code: "SCHEMA_MISMATCH_BLOCKED" }
|
|
2544
2960
|
);
|
|
@@ -2546,7 +2962,12 @@ var SyncEngine = class {
|
|
|
2546
2962
|
this.transitionTo("connecting");
|
|
2547
2963
|
try {
|
|
2548
2964
|
const authToken = this.config.auth ? (await this.config.auth()).token : void 0;
|
|
2965
|
+
if (this.state !== "connecting") return;
|
|
2549
2966
|
await this.transport.connect(this.config.url, { authToken });
|
|
2967
|
+
if (this.state !== "connecting") {
|
|
2968
|
+
await this.transport.disconnect();
|
|
2969
|
+
return;
|
|
2970
|
+
}
|
|
2550
2971
|
this.transitionTo("handshaking");
|
|
2551
2972
|
const localVector = this.store.getVersionVector();
|
|
2552
2973
|
const activeQuerySubsets = this.getActiveQuerySubsets();
|
|
@@ -2560,7 +2981,11 @@ var SyncEngine = class {
|
|
|
2560
2981
|
supportedWireFormats: ["json", "protobuf"],
|
|
2561
2982
|
...this.config.scopeMap ? { syncScope: this.config.scopeMap } : {},
|
|
2562
2983
|
...activeQuerySubsets.length > 0 ? { syncQueries: activeQuerySubsets } : {},
|
|
2563
|
-
...this.resumeDeltaCursor ? { deltaCursor: encodeDeltaCursor(this.resumeDeltaCursor) } : {}
|
|
2984
|
+
...this.resumeDeltaCursor ? { deltaCursor: encodeDeltaCursor(this.resumeDeltaCursor) } : {},
|
|
2985
|
+
// Resume the server's gap-free delivery stream from our watermark. A server
|
|
2986
|
+
// that understands it drives server->client sync from delivery sequences; an
|
|
2987
|
+
// older server ignores it and falls back to the version-vector delta.
|
|
2988
|
+
lastDeliverySequence: this.deliveryWatermark
|
|
2564
2989
|
};
|
|
2565
2990
|
this.transport.send(handshake);
|
|
2566
2991
|
} catch (err) {
|
|
@@ -2572,11 +2997,20 @@ var SyncEngine = class {
|
|
|
2572
2997
|
* Stop the sync engine. Disconnects the transport.
|
|
2573
2998
|
*/
|
|
2574
2999
|
async stop() {
|
|
3000
|
+
if (this.stopPromise) return this.stopPromise;
|
|
3001
|
+
this.stopPromise = this.stopInternal().finally(() => {
|
|
3002
|
+
this.stopPromise = null;
|
|
3003
|
+
});
|
|
3004
|
+
return this.stopPromise;
|
|
3005
|
+
}
|
|
3006
|
+
async stopInternal() {
|
|
2575
3007
|
if (this.state === "disconnected") return;
|
|
3008
|
+
this.clearOutboundAckTimer();
|
|
2576
3009
|
this.awarenessManager.stopCleanupTimer();
|
|
2577
3010
|
if (this.currentBatch) {
|
|
2578
3011
|
this.outboundQueue.returnBatch(this.currentBatch.batchId);
|
|
2579
3012
|
this.currentBatch = null;
|
|
3013
|
+
this.currentBatchRequiresPartialAck = false;
|
|
2580
3014
|
}
|
|
2581
3015
|
try {
|
|
2582
3016
|
await this.transport.disconnect();
|
|
@@ -2597,7 +3031,7 @@ var SyncEngine = class {
|
|
|
2597
3031
|
* because they should remain local-only and not be sent to the server.
|
|
2598
3032
|
*/
|
|
2599
3033
|
async pushOperation(op) {
|
|
2600
|
-
if (!this.operationAllowedForSync(op)) {
|
|
3034
|
+
if (!await this.operationAllowedForSync(op)) {
|
|
2601
3035
|
return;
|
|
2602
3036
|
}
|
|
2603
3037
|
if (this.syncState) {
|
|
@@ -2626,6 +3060,7 @@ var SyncEngine = class {
|
|
|
2626
3060
|
getStatus() {
|
|
2627
3061
|
const pendingOperations = this.syncState ? this.cachedUnsyncedCount : this.outboundQueue.totalPending;
|
|
2628
3062
|
const base = {
|
|
3063
|
+
reconnecting: this.reconnecting,
|
|
2629
3064
|
pendingOperations,
|
|
2630
3065
|
lastSyncedAt: this.lastSyncedAt,
|
|
2631
3066
|
lastSuccessfulPush: this.lastSuccessfulPush,
|
|
@@ -2645,7 +3080,7 @@ var SyncEngine = class {
|
|
|
2645
3080
|
case "connecting":
|
|
2646
3081
|
case "handshaking":
|
|
2647
3082
|
case "syncing":
|
|
2648
|
-
return { ...base, status: this.reconnecting ? "
|
|
3083
|
+
return { ...base, status: this.reconnecting ? "reconnecting" : "syncing" };
|
|
2649
3084
|
case "streaming":
|
|
2650
3085
|
return { ...base, status: pendingOperations > 0 ? "syncing" : "synced" };
|
|
2651
3086
|
case "error":
|
|
@@ -2708,7 +3143,7 @@ var SyncEngine = class {
|
|
|
2708
3143
|
return this.getStatus().pendingOperations;
|
|
2709
3144
|
}
|
|
2710
3145
|
emitApplyFailure(op, result, overrides) {
|
|
2711
|
-
const reason = (0,
|
|
3146
|
+
const reason = (0, import_core7.defaultApplyFailureReason)(result, overrides);
|
|
2712
3147
|
this.emitter?.emit({
|
|
2713
3148
|
type: "sync:apply-failed",
|
|
2714
3149
|
operationId: op.id,
|
|
@@ -2730,6 +3165,24 @@ var SyncEngine = class {
|
|
|
2730
3165
|
await this.start();
|
|
2731
3166
|
}
|
|
2732
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
|
+
}
|
|
2733
3186
|
/**
|
|
2734
3187
|
* Export a diagnostics snapshot for debugging and support tickets.
|
|
2735
3188
|
* Contains connection state, timing info, and queue metrics.
|
|
@@ -2748,6 +3201,8 @@ var SyncEngine = class {
|
|
|
2748
3201
|
pendingOperations: this.outboundQueue.totalPending,
|
|
2749
3202
|
hasInFlightBatch: this.currentBatch !== null,
|
|
2750
3203
|
reconnecting: this.reconnecting,
|
|
3204
|
+
deliveryWatermark: this.deliveryWatermark,
|
|
3205
|
+
deliveryGapRepeatCount: this.deliveryGapRepeatCount,
|
|
2751
3206
|
timestamp: Date.now()
|
|
2752
3207
|
};
|
|
2753
3208
|
}
|
|
@@ -2776,8 +3231,84 @@ var SyncEngine = class {
|
|
|
2776
3231
|
* @param scopeMap - New per-collection scope filters, or undefined to remove scope
|
|
2777
3232
|
*/
|
|
2778
3233
|
updateScope(scopeMap) {
|
|
3234
|
+
const previousSignature = this.deliverySignature();
|
|
2779
3235
|
this.activeScope = scopeMap;
|
|
2780
3236
|
this.config.scopeMap = scopeMap;
|
|
3237
|
+
this.switchDeliveryView(previousSignature);
|
|
3238
|
+
}
|
|
3239
|
+
/**
|
|
3240
|
+
* A stable identifier for the current sync view: the auth/tenant scope plus the set of
|
|
3241
|
+
* active query subscriptions, canonicalized so the same view always produces the same
|
|
3242
|
+
* string. The delivery watermark is keyed by this, so each distinct view resumes from
|
|
3243
|
+
* its own last position.
|
|
3244
|
+
*/
|
|
3245
|
+
deliverySignature() {
|
|
3246
|
+
const subsets = this.getActiveQuerySubsets();
|
|
3247
|
+
if (!this.activeScope && subsets.length === 0) {
|
|
3248
|
+
return "";
|
|
3249
|
+
}
|
|
3250
|
+
const normalizedSubsets = subsets.map((s) => `${s.collection}:${stableStringify(s.where)}`).sort();
|
|
3251
|
+
return stableStringify({ scope: this.activeScope ?? null, subsets: normalizedSubsets });
|
|
3252
|
+
}
|
|
3253
|
+
/**
|
|
3254
|
+
* Switch the active delivery watermark to the current view after a scope or subscription
|
|
3255
|
+
* change. The prior view's watermark is saved so returning to it resumes; the new view's
|
|
3256
|
+
* watermark is restored from the in-memory cache (0 for a never-synced view, which then
|
|
3257
|
+
* does a one-time resync under that view). No data is lost: a view's watermark is a lower
|
|
3258
|
+
* bound for that specific view's filter.
|
|
3259
|
+
*/
|
|
3260
|
+
switchDeliveryView(previousSignature) {
|
|
3261
|
+
const nextSignature = this.deliverySignature();
|
|
3262
|
+
if (nextSignature === previousSignature) {
|
|
3263
|
+
return;
|
|
3264
|
+
}
|
|
3265
|
+
this.setViewWatermark(previousSignature, this.deliveryWatermark);
|
|
3266
|
+
void this.persistDeliveryWatermark(this.deliveryWatermark, previousSignature);
|
|
3267
|
+
this.deliveryWatermark = this.deliverySignatureWatermarks.get(nextSignature) ?? 0;
|
|
3268
|
+
}
|
|
3269
|
+
/**
|
|
3270
|
+
* Record a view's watermark in the in-memory cache as the most-recently-used entry (the
|
|
3271
|
+
* Map preserves insertion order, so re-inserting moves it to the tail), then evict cold
|
|
3272
|
+
* views past the retention cap. This is the single place the cache is written, so LRU
|
|
3273
|
+
* order and the size bound are always maintained together.
|
|
3274
|
+
*/
|
|
3275
|
+
setViewWatermark(signature, watermark) {
|
|
3276
|
+
this.deliverySignatureWatermarks.delete(signature);
|
|
3277
|
+
this.deliverySignatureWatermarks.set(signature, watermark);
|
|
3278
|
+
this.evictColdViewWatermarks();
|
|
3279
|
+
}
|
|
3280
|
+
/**
|
|
3281
|
+
* Trim the per-view watermark cache to `MAX_DELIVERY_VIEW_WATERMARKS`, evicting the
|
|
3282
|
+
* least-recently-used views. The default view ('') and the live view are never evicted
|
|
3283
|
+
* (evicting the live view would force an immediate re-scan of what we are actively
|
|
3284
|
+
* syncing). Each eviction also removes the persisted row, bounding storage. Safe by
|
|
3285
|
+
* construction: an evicted view simply back-fills from 0 (deduplicated) when next seen.
|
|
3286
|
+
*/
|
|
3287
|
+
evictColdViewWatermarks() {
|
|
3288
|
+
if (this.deliverySignatureWatermarks.size <= MAX_DELIVERY_VIEW_WATERMARKS) {
|
|
3289
|
+
return;
|
|
3290
|
+
}
|
|
3291
|
+
const liveSignature = this.deliverySignature();
|
|
3292
|
+
for (const signature of this.deliverySignatureWatermarks.keys()) {
|
|
3293
|
+
if (this.deliverySignatureWatermarks.size <= MAX_DELIVERY_VIEW_WATERMARKS) {
|
|
3294
|
+
break;
|
|
3295
|
+
}
|
|
3296
|
+
if (signature === "" || signature === liveSignature) {
|
|
3297
|
+
continue;
|
|
3298
|
+
}
|
|
3299
|
+
this.deliverySignatureWatermarks.delete(signature);
|
|
3300
|
+
void this.deleteDeliveryWatermark(signature);
|
|
3301
|
+
}
|
|
3302
|
+
}
|
|
3303
|
+
/**
|
|
3304
|
+
* Remove a view's persisted watermark row (best-effort; a persistence layer that omits
|
|
3305
|
+
* the delete simply keeps the row, which is harmless).
|
|
3306
|
+
*/
|
|
3307
|
+
async deleteDeliveryWatermark(signature) {
|
|
3308
|
+
if (!this.syncState?.deleteDeliveryWatermark) {
|
|
3309
|
+
return;
|
|
3310
|
+
}
|
|
3311
|
+
await this.syncState.deleteDeliveryWatermark(signature);
|
|
2781
3312
|
}
|
|
2782
3313
|
/**
|
|
2783
3314
|
* Get the currently active scope map. Returns undefined if no scope is configured.
|
|
@@ -2791,10 +3322,14 @@ var SyncEngine = class {
|
|
|
2791
3322
|
*/
|
|
2792
3323
|
registerQuerySubset(subset) {
|
|
2793
3324
|
const id = `query-${nextQuerySubsetId++}`;
|
|
3325
|
+
const previousSignature = this.deliverySignature();
|
|
2794
3326
|
this.querySubsets.set(id, subset);
|
|
3327
|
+
this.switchDeliveryView(previousSignature);
|
|
2795
3328
|
this.scheduleQuerySubsetReconnect();
|
|
2796
3329
|
return () => {
|
|
3330
|
+
const sigBeforeRemove = this.deliverySignature();
|
|
2797
3331
|
this.querySubsets.delete(id);
|
|
3332
|
+
this.switchDeliveryView(sigBeforeRemove);
|
|
2798
3333
|
this.scheduleQuerySubsetReconnect();
|
|
2799
3334
|
};
|
|
2800
3335
|
}
|
|
@@ -2818,6 +3353,31 @@ var SyncEngine = class {
|
|
|
2818
3353
|
getRichtextDocChannel() {
|
|
2819
3354
|
return this.richtextDocChannel;
|
|
2820
3355
|
}
|
|
3356
|
+
/**
|
|
3357
|
+
* Side channel for out-of-band blob chunk transfer over the sync connection.
|
|
3358
|
+
* The app layer bridges this to a `ChunkMessagePort` to pull/serve blob bytes.
|
|
3359
|
+
*/
|
|
3360
|
+
getBlobChunkChannel() {
|
|
3361
|
+
return this.blobChunkChannel;
|
|
3362
|
+
}
|
|
3363
|
+
/**
|
|
3364
|
+
* Whether the connected server persists blob bytes centrally (learned from the
|
|
3365
|
+
* handshake response). When true, the app uploads the bytes behind `blob`
|
|
3366
|
+
* fields so they stay available after the authoring device goes offline.
|
|
3367
|
+
*/
|
|
3368
|
+
isBlobStorageEnabled() {
|
|
3369
|
+
return this.blobStorageEnabled;
|
|
3370
|
+
}
|
|
3371
|
+
/**
|
|
3372
|
+
* Upload a blob chunk (or manifest) to the server for central persistence.
|
|
3373
|
+
* A no-op unless connected and the server advertised blob storage.
|
|
3374
|
+
*/
|
|
3375
|
+
uploadBlobChunk(hash, bytes) {
|
|
3376
|
+
if (this.state !== "streaming" || !this.blobStorageEnabled) {
|
|
3377
|
+
return;
|
|
3378
|
+
}
|
|
3379
|
+
this.blobChunkChannel.send({ type: "blob-chunk-push", hash, bytes });
|
|
3380
|
+
}
|
|
2821
3381
|
// --- Private methods ---
|
|
2822
3382
|
messageChain = Promise.resolve();
|
|
2823
3383
|
enqueueMessage(message) {
|
|
@@ -2832,17 +3392,25 @@ var SyncEngine = class {
|
|
|
2832
3392
|
await this.handleOperationBatch(message);
|
|
2833
3393
|
break;
|
|
2834
3394
|
case "acknowledgment":
|
|
2835
|
-
this.handleAcknowledgment(message);
|
|
3395
|
+
await this.handleAcknowledgment(message);
|
|
2836
3396
|
break;
|
|
2837
3397
|
case "error":
|
|
2838
3398
|
this.handleError(message);
|
|
2839
3399
|
break;
|
|
3400
|
+
case "operation-rejected":
|
|
3401
|
+
await this.handleOperationRejected(message);
|
|
3402
|
+
break;
|
|
2840
3403
|
case "awareness-update":
|
|
2841
3404
|
this.handleAwarenessUpdate(message);
|
|
2842
3405
|
break;
|
|
2843
3406
|
case "yjs-doc-update":
|
|
2844
3407
|
this.richtextDocChannel.deliver(message);
|
|
2845
3408
|
break;
|
|
3409
|
+
case "blob-chunk-request":
|
|
3410
|
+
case "blob-chunk-response":
|
|
3411
|
+
case "blob-chunk-push":
|
|
3412
|
+
this.blobChunkChannel.deliver(message);
|
|
3413
|
+
break;
|
|
2846
3414
|
}
|
|
2847
3415
|
}
|
|
2848
3416
|
handleMessageFailure(error) {
|
|
@@ -2875,6 +3443,7 @@ var SyncEngine = class {
|
|
|
2875
3443
|
}
|
|
2876
3444
|
async handleHandshakeResponse(msg) {
|
|
2877
3445
|
if (this.state !== "handshaking") return;
|
|
3446
|
+
this.blobStorageEnabled = msg.blobStorageEnabled === true;
|
|
2878
3447
|
if (typeof msg.serverTime === "number") {
|
|
2879
3448
|
this.evaluateClockSkew(msg.serverTime);
|
|
2880
3449
|
if (this.clockBlocked) {
|
|
@@ -2913,6 +3482,11 @@ var SyncEngine = class {
|
|
|
2913
3482
|
}
|
|
2914
3483
|
this.remoteVector = wireToVersionVector(msg.versionVector);
|
|
2915
3484
|
void this.persistLastAckedServerVector(this.remoteVector);
|
|
3485
|
+
if (typeof msg.serverMaxDeliverySequence === "number" && this.deliveryWatermark > msg.serverMaxDeliverySequence) {
|
|
3486
|
+
this.deliveryWatermark = 0;
|
|
3487
|
+
this.setViewWatermark(this.deliverySignature(), 0);
|
|
3488
|
+
await this.persistDeliveryWatermark(0);
|
|
3489
|
+
}
|
|
2916
3490
|
if (msg.selectedWireFormat) {
|
|
2917
3491
|
this.setSerializerWireFormat(msg.selectedWireFormat);
|
|
2918
3492
|
}
|
|
@@ -2978,7 +3552,7 @@ var SyncEngine = class {
|
|
|
2978
3552
|
async sendDelta() {
|
|
2979
3553
|
const localVector = this.store.getVersionVector();
|
|
2980
3554
|
const allMissingOps = await this.collectDelta(localVector, this.remoteVector);
|
|
2981
|
-
const missingOps =
|
|
3555
|
+
const missingOps = await this.filterAllowedForSync(allMissingOps);
|
|
2982
3556
|
this.deltaSentOpIds = missingOps.map((op) => op.id);
|
|
2983
3557
|
if (missingOps.length === 0) {
|
|
2984
3558
|
const messageId = generateMessageId();
|
|
@@ -3053,13 +3627,36 @@ var SyncEngine = class {
|
|
|
3053
3627
|
return missing;
|
|
3054
3628
|
}
|
|
3055
3629
|
async handleOperationBatch(msg) {
|
|
3630
|
+
const isDeliveryBatch = msg.maxDeliverySequence !== void 0;
|
|
3631
|
+
if (isDeliveryBatch) {
|
|
3632
|
+
const base = msg.baseDeliverySequence ?? 0;
|
|
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
|
+
});
|
|
3645
|
+
return;
|
|
3646
|
+
}
|
|
3647
|
+
if (base < this.deliveryWatermark) {
|
|
3648
|
+
this.sendDeliveryAck(msg.messageId, this.deliveryWatermark);
|
|
3649
|
+
return;
|
|
3650
|
+
}
|
|
3651
|
+
}
|
|
3056
3652
|
const deserialized = msg.operations.map((s) => this.serializer.decodeOperation(s));
|
|
3057
3653
|
const operations = this.encryptor ? await this.encryptor.decryptBatch(deserialized) : deserialized;
|
|
3058
|
-
const inScopeOps =
|
|
3654
|
+
const inScopeOps = await this.filterAllowedForSync(operations);
|
|
3059
3655
|
const targetSchemaVersion = this.config.schemaVersion ?? DEFAULT_SCHEMA_VERSION;
|
|
3060
3656
|
const transforms = this.config.operationTransforms ?? [];
|
|
3657
|
+
let fullyApplied = true;
|
|
3061
3658
|
for (const op of inScopeOps) {
|
|
3062
|
-
const transformed = transforms.length > 0 ? (0,
|
|
3659
|
+
const transformed = transforms.length > 0 ? (0, import_core7.applyOperationTransforms)(op, targetSchemaVersion, transforms) : op;
|
|
3063
3660
|
if (transformed === null) {
|
|
3064
3661
|
continue;
|
|
3065
3662
|
}
|
|
@@ -3069,21 +3666,19 @@ var SyncEngine = class {
|
|
|
3069
3666
|
this.emitApplyFailure(transformed, result);
|
|
3070
3667
|
}
|
|
3071
3668
|
} catch (error) {
|
|
3072
|
-
|
|
3669
|
+
fullyApplied = false;
|
|
3670
|
+
if (error instanceof import_core7.ClockDriftError) {
|
|
3073
3671
|
this.emitApplyFailure(transformed, "rejected", {
|
|
3074
|
-
code:
|
|
3672
|
+
code: import_core7.APPLY_FAILURE_CODES.CLOCK_DRIFT,
|
|
3075
3673
|
message: error.message,
|
|
3076
3674
|
retriable: false
|
|
3077
3675
|
});
|
|
3078
3676
|
continue;
|
|
3079
3677
|
}
|
|
3080
3678
|
const message = error instanceof Error ? error.message : "Apply failed";
|
|
3081
|
-
const code = error instanceof
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
message,
|
|
3085
|
-
retriable: code !== import_core6.APPLY_FAILURE_CODES.REFERENTIAL_INTEGRITY
|
|
3086
|
-
});
|
|
3679
|
+
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
|
+
const retriable = code !== import_core7.APPLY_FAILURE_CODES.REFERENTIAL_INTEGRITY;
|
|
3681
|
+
this.emitApplyFailure(transformed, "rejected", { code, message, retriable });
|
|
3087
3682
|
}
|
|
3088
3683
|
}
|
|
3089
3684
|
if (inScopeOps.length > 0) {
|
|
@@ -3094,18 +3689,29 @@ var SyncEngine = class {
|
|
|
3094
3689
|
batchSize: inScopeOps.length
|
|
3095
3690
|
});
|
|
3096
3691
|
}
|
|
3097
|
-
const
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3692
|
+
const deliveryFullyApplied = isDeliveryBatch && fullyApplied;
|
|
3693
|
+
if (!isDeliveryBatch || deliveryFullyApplied) {
|
|
3694
|
+
const lastOp = operations[operations.length - 1];
|
|
3695
|
+
const ack = {
|
|
3696
|
+
type: "acknowledgment",
|
|
3697
|
+
messageId: generateMessageId(),
|
|
3698
|
+
acknowledgedMessageId: msg.messageId,
|
|
3699
|
+
lastSequenceNumber: lastOp ? lastOp.sequenceNumber : 0,
|
|
3700
|
+
...deliveryFullyApplied && msg.maxDeliverySequence !== void 0 ? { deliverySequence: msg.maxDeliverySequence } : {}
|
|
3701
|
+
};
|
|
3702
|
+
this.transport.send(ack);
|
|
3703
|
+
this.emitter?.emit({
|
|
3704
|
+
type: "sync:acknowledged",
|
|
3705
|
+
sequenceNumber: lastOp ? lastOp.sequenceNumber : 0
|
|
3706
|
+
});
|
|
3707
|
+
}
|
|
3708
|
+
if (isDeliveryBatch && fullyApplied && msg.maxDeliverySequence !== void 0) {
|
|
3709
|
+
this.deliveryGapRepeatCount = 0;
|
|
3710
|
+
this.lastDeliveryGapKey = null;
|
|
3711
|
+
this.deliveryWatermark = msg.maxDeliverySequence;
|
|
3712
|
+
this.setViewWatermark(this.deliverySignature(), this.deliveryWatermark);
|
|
3713
|
+
await this.persistDeliveryWatermark(this.deliveryWatermark);
|
|
3714
|
+
}
|
|
3109
3715
|
if (this.state === "syncing") {
|
|
3110
3716
|
this.deltaBatchesReceived++;
|
|
3111
3717
|
const totalBatches = msg.totalBatches ?? this.initialSyncTotalBatches;
|
|
@@ -3113,15 +3719,17 @@ var SyncEngine = class {
|
|
|
3113
3719
|
this.initialSyncTotalBatches = msg.totalBatches;
|
|
3114
3720
|
}
|
|
3115
3721
|
this.metricsCollector.updateInitialSyncProgress(this.deltaBatchesReceived, totalBatches);
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3722
|
+
if (!isDeliveryBatch) {
|
|
3723
|
+
const cursorFromBatch = msg.cursor !== void 0 ? decodeDeltaCursor(msg.cursor) : createDeltaCursorFromBatch(
|
|
3724
|
+
inScopeOps.length > 0 ? inScopeOps : operations,
|
|
3725
|
+
msg.batchIndex
|
|
3726
|
+
);
|
|
3727
|
+
if (cursorFromBatch) {
|
|
3728
|
+
this.resumeDeltaCursor = cursorFromBatch;
|
|
3729
|
+
await this.persistDeltaCursor(cursorFromBatch);
|
|
3730
|
+
}
|
|
3123
3731
|
}
|
|
3124
|
-
if (msg.isFinal) {
|
|
3732
|
+
if (msg.isFinal && (!isDeliveryBatch || fullyApplied)) {
|
|
3125
3733
|
this.deltaReceiveComplete = true;
|
|
3126
3734
|
this.resumeDeltaCursor = null;
|
|
3127
3735
|
await this.persistDeltaCursor(null);
|
|
@@ -3130,26 +3738,51 @@ var SyncEngine = class {
|
|
|
3130
3738
|
}
|
|
3131
3739
|
}
|
|
3132
3740
|
}
|
|
3133
|
-
|
|
3741
|
+
/** Acknowledge a delivery batch (used for duplicates) without re-applying it. */
|
|
3742
|
+
sendDeliveryAck(acknowledgedMessageId, deliverySequence) {
|
|
3743
|
+
const ack = {
|
|
3744
|
+
type: "acknowledgment",
|
|
3745
|
+
messageId: generateMessageId(),
|
|
3746
|
+
acknowledgedMessageId,
|
|
3747
|
+
lastSequenceNumber: 0,
|
|
3748
|
+
deliverySequence
|
|
3749
|
+
};
|
|
3750
|
+
this.transport.send(ack);
|
|
3751
|
+
}
|
|
3752
|
+
async handleAcknowledgment(msg) {
|
|
3753
|
+
this.clearOutboundAckTimer();
|
|
3134
3754
|
if (this.state === "syncing" && this.config.strictHandshake) {
|
|
3135
3755
|
this.pendingDeltaBatchAcks.delete(msg.acknowledgedMessageId);
|
|
3136
3756
|
this.markDeltaSendCompleteIfReady();
|
|
3137
3757
|
}
|
|
3138
3758
|
if (this.currentBatch) {
|
|
3139
|
-
|
|
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
|
+
}
|
|
3140
3767
|
this.currentBatch = null;
|
|
3768
|
+
this.currentBatchRequiresPartialAck = false;
|
|
3141
3769
|
const now = Date.now();
|
|
3142
3770
|
this.lastSyncedAt = now;
|
|
3143
3771
|
this.lastSuccessfulPush = now;
|
|
3144
3772
|
}
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
);
|
|
3773
|
+
await this.advanceLastAckedForLocalNode(msg.lastSequenceNumber);
|
|
3774
|
+
await this.refreshPendingCount();
|
|
3148
3775
|
if (this.state === "streaming" && this.outboundQueue.hasOperations) {
|
|
3149
3776
|
this.flushQueue();
|
|
3150
3777
|
}
|
|
3151
3778
|
}
|
|
3152
3779
|
handleError(msg) {
|
|
3780
|
+
this.clearOutboundAckTimer();
|
|
3781
|
+
if (this.currentBatch) {
|
|
3782
|
+
this.outboundQueue.returnBatch(this.currentBatch.batchId);
|
|
3783
|
+
this.currentBatch = null;
|
|
3784
|
+
this.currentBatchRequiresPartialAck = false;
|
|
3785
|
+
}
|
|
3153
3786
|
this.transitionTo("error");
|
|
3154
3787
|
if (msg.code === "AUTH_FAILED") {
|
|
3155
3788
|
this.emitter?.emit({ type: "sync:auth-failed", reason: msg.message });
|
|
@@ -3168,6 +3801,64 @@ var SyncEngine = class {
|
|
|
3168
3801
|
this.emitter?.emit({ type: "sync:disconnected", reason: msg.message });
|
|
3169
3802
|
this.transitionTo("disconnected");
|
|
3170
3803
|
}
|
|
3804
|
+
/**
|
|
3805
|
+
* Handle a per-operation rejection: divert the op out of the pending outbound
|
|
3806
|
+
* queue (so it is never retried or resurrected on reconnect), record it in the
|
|
3807
|
+
* durable rejected store (so it is kept and explainable), and emit an event so
|
|
3808
|
+
* the app can reconcile. Unlike {@link handleError}, this is a normal per-op
|
|
3809
|
+
* signal, so the connection stays up.
|
|
3810
|
+
*/
|
|
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
|
+
}
|
|
3826
|
+
await this.outboundQueue.reject(msg.operationId);
|
|
3827
|
+
await this.rejectedStorage.record({
|
|
3828
|
+
operationId: msg.operationId,
|
|
3829
|
+
collection: msg.collection,
|
|
3830
|
+
recordId: msg.recordId,
|
|
3831
|
+
code: msg.code,
|
|
3832
|
+
message: msg.message,
|
|
3833
|
+
retriable: msg.retriable,
|
|
3834
|
+
rejectedAt: Date.now()
|
|
3835
|
+
});
|
|
3836
|
+
await this.refreshPendingCount();
|
|
3837
|
+
this.emitter?.emit({
|
|
3838
|
+
type: "sync:operation-rejected",
|
|
3839
|
+
operationId: msg.operationId,
|
|
3840
|
+
collection: msg.collection,
|
|
3841
|
+
recordId: msg.recordId,
|
|
3842
|
+
code: msg.code,
|
|
3843
|
+
message: msg.message,
|
|
3844
|
+
retriable: msg.retriable
|
|
3845
|
+
});
|
|
3846
|
+
}
|
|
3847
|
+
/**
|
|
3848
|
+
* Every operation the server rejected that has not yet been reconciled. The app
|
|
3849
|
+
* uses this (alongside the `sync:operation-rejected` event) to surface failed
|
|
3850
|
+
* submissions and decide whether to roll back or resubmit.
|
|
3851
|
+
*/
|
|
3852
|
+
getRejectedOperations() {
|
|
3853
|
+
return this.rejectedStorage.list();
|
|
3854
|
+
}
|
|
3855
|
+
/**
|
|
3856
|
+
* Forget rejected operations by id once the app has reconciled them (rolled the
|
|
3857
|
+
* optimistic write back or resubmitted a corrected op).
|
|
3858
|
+
*/
|
|
3859
|
+
clearRejectedOperations(operationIds) {
|
|
3860
|
+
return this.rejectedStorage.remove(operationIds);
|
|
3861
|
+
}
|
|
3171
3862
|
async checkDeltaComplete() {
|
|
3172
3863
|
if (!this.deltaSendComplete || !this.deltaReceiveComplete) {
|
|
3173
3864
|
return;
|
|
@@ -3266,7 +3957,7 @@ var SyncEngine = class {
|
|
|
3266
3957
|
return;
|
|
3267
3958
|
}
|
|
3268
3959
|
const ops = await this.store.getOperationRange(localNodeId, 1, localSeq);
|
|
3269
|
-
const inScope =
|
|
3960
|
+
const inScope = await this.filterAllowedForSync(ops);
|
|
3270
3961
|
for (const op of inScope) {
|
|
3271
3962
|
await this.outboundQueue.enqueue(op);
|
|
3272
3963
|
}
|
|
@@ -3277,6 +3968,7 @@ var SyncEngine = class {
|
|
|
3277
3968
|
const batch = this.outboundQueue.takeBatch(this.batchSize);
|
|
3278
3969
|
if (!batch) return;
|
|
3279
3970
|
this.currentBatch = batch;
|
|
3971
|
+
this.currentBatchRequiresPartialAck = false;
|
|
3280
3972
|
if (this.encryptor) {
|
|
3281
3973
|
this.encryptor.encryptBatch(batch.operations).then(
|
|
3282
3974
|
(encrypted) => {
|
|
@@ -3289,6 +3981,7 @@ var SyncEngine = class {
|
|
|
3289
3981
|
batchIndex: 0
|
|
3290
3982
|
};
|
|
3291
3983
|
this.transport.send(batchMsg);
|
|
3984
|
+
this.startOutboundAckTimer(batch.batchId);
|
|
3292
3985
|
this.emitter?.emit({
|
|
3293
3986
|
type: "sync:sent",
|
|
3294
3987
|
operations: batch.operations,
|
|
@@ -3298,6 +3991,7 @@ var SyncEngine = class {
|
|
|
3298
3991
|
(err) => {
|
|
3299
3992
|
this.outboundQueue.returnBatch(batch.batchId);
|
|
3300
3993
|
this.currentBatch = null;
|
|
3994
|
+
this.currentBatchRequiresPartialAck = false;
|
|
3301
3995
|
this.emitter?.emit({
|
|
3302
3996
|
type: "sync:disconnected",
|
|
3303
3997
|
reason: err instanceof Error ? err.message : "Encryption failed"
|
|
@@ -3314,6 +4008,7 @@ var SyncEngine = class {
|
|
|
3314
4008
|
batchIndex: 0
|
|
3315
4009
|
};
|
|
3316
4010
|
this.transport.send(batchMsg);
|
|
4011
|
+
this.startOutboundAckTimer(batch.batchId);
|
|
3317
4012
|
this.emitter?.emit({
|
|
3318
4013
|
type: "sync:sent",
|
|
3319
4014
|
operations: batch.operations,
|
|
@@ -3321,6 +4016,34 @@ var SyncEngine = class {
|
|
|
3321
4016
|
});
|
|
3322
4017
|
}
|
|
3323
4018
|
}
|
|
4019
|
+
startOutboundAckTimer(batchId) {
|
|
4020
|
+
this.clearOutboundAckTimer();
|
|
4021
|
+
if (this.outboundAckTimeoutMs <= 0) return;
|
|
4022
|
+
this.outboundAckTimer = setTimeout(() => {
|
|
4023
|
+
void this.handleOutboundAckTimeout(batchId);
|
|
4024
|
+
}, this.outboundAckTimeoutMs);
|
|
4025
|
+
}
|
|
4026
|
+
clearOutboundAckTimer() {
|
|
4027
|
+
if (!this.outboundAckTimer) return;
|
|
4028
|
+
clearTimeout(this.outboundAckTimer);
|
|
4029
|
+
this.outboundAckTimer = null;
|
|
4030
|
+
}
|
|
4031
|
+
async handleOutboundAckTimeout(batchId) {
|
|
4032
|
+
if (!this.currentBatch || this.currentBatch.batchId !== batchId) return;
|
|
4033
|
+
this.outboundQueue.returnBatch(batchId);
|
|
4034
|
+
this.currentBatch = null;
|
|
4035
|
+
this.currentBatchRequiresPartialAck = false;
|
|
4036
|
+
this.clearOutboundAckTimer();
|
|
4037
|
+
await this.refreshPendingCount();
|
|
4038
|
+
if (this.state === "disconnected") return;
|
|
4039
|
+
const reason = `Timed out waiting for acknowledgment of outbound batch ${batchId}`;
|
|
4040
|
+
try {
|
|
4041
|
+
await this.transport.disconnect();
|
|
4042
|
+
} finally {
|
|
4043
|
+
this.ensureDisconnected();
|
|
4044
|
+
this.emitter?.emit({ type: "sync:disconnected", reason });
|
|
4045
|
+
}
|
|
4046
|
+
}
|
|
3324
4047
|
handleAwarenessUpdate(msg) {
|
|
3325
4048
|
const awarenessMessage = {
|
|
3326
4049
|
type: "awareness",
|
|
@@ -3330,9 +4053,11 @@ var SyncEngine = class {
|
|
|
3330
4053
|
this.awarenessManager.handleRemoteMessage(awarenessMessage);
|
|
3331
4054
|
}
|
|
3332
4055
|
handleTransportClose(reason) {
|
|
4056
|
+
this.clearOutboundAckTimer();
|
|
3333
4057
|
if (this.currentBatch) {
|
|
3334
4058
|
this.outboundQueue.returnBatch(this.currentBatch.batchId);
|
|
3335
4059
|
this.currentBatch = null;
|
|
4060
|
+
this.currentBatchRequiresPartialAck = false;
|
|
3336
4061
|
}
|
|
3337
4062
|
if (this.schemaBlocked) {
|
|
3338
4063
|
return;
|
|
@@ -3352,7 +4077,7 @@ var SyncEngine = class {
|
|
|
3352
4077
|
transitionTo(newState) {
|
|
3353
4078
|
const validTargets = VALID_TRANSITIONS[this.state];
|
|
3354
4079
|
if (!validTargets.includes(newState)) {
|
|
3355
|
-
throw new
|
|
4080
|
+
throw new import_core7.SyncError(`Invalid sync state transition: ${this.state} \u2192 ${newState}`, {
|
|
3356
4081
|
from: this.state,
|
|
3357
4082
|
to: newState
|
|
3358
4083
|
});
|
|
@@ -3364,11 +4089,40 @@ var SyncEngine = class {
|
|
|
3364
4089
|
this.serializer.setWireFormat(format);
|
|
3365
4090
|
}
|
|
3366
4091
|
}
|
|
3367
|
-
operationAllowedForSync(op) {
|
|
3368
|
-
if (
|
|
4092
|
+
async operationAllowedForSync(op) {
|
|
4093
|
+
if (this.matchesScopeAndSubsets(op)) {
|
|
4094
|
+
return true;
|
|
4095
|
+
}
|
|
4096
|
+
const fullRecord = await this.readRecordForBackfill(op);
|
|
4097
|
+
if (!fullRecord) {
|
|
4098
|
+
return false;
|
|
4099
|
+
}
|
|
4100
|
+
return this.matchesScopeAndSubsets(op, fullRecord);
|
|
4101
|
+
}
|
|
4102
|
+
matchesScopeAndSubsets(op, fullRecord) {
|
|
4103
|
+
if (!operationMatchesScope(op, this.activeScope, fullRecord)) {
|
|
3369
4104
|
return false;
|
|
3370
4105
|
}
|
|
3371
|
-
return operationMatchesQuerySubsets(op, this.getActiveQuerySubsets());
|
|
4106
|
+
return operationMatchesQuerySubsets(op, this.getActiveQuerySubsets(), fullRecord);
|
|
4107
|
+
}
|
|
4108
|
+
async readRecordForBackfill(op) {
|
|
4109
|
+
if (!this.store.readRecordFields) {
|
|
4110
|
+
return null;
|
|
4111
|
+
}
|
|
4112
|
+
try {
|
|
4113
|
+
return await this.store.readRecordFields(op.collection, op.recordId);
|
|
4114
|
+
} catch {
|
|
4115
|
+
return null;
|
|
4116
|
+
}
|
|
4117
|
+
}
|
|
4118
|
+
async filterAllowedForSync(ops) {
|
|
4119
|
+
const allowed = [];
|
|
4120
|
+
for (const op of ops) {
|
|
4121
|
+
if (await this.operationAllowedForSync(op)) {
|
|
4122
|
+
allowed.push(op);
|
|
4123
|
+
}
|
|
4124
|
+
}
|
|
4125
|
+
return allowed;
|
|
3372
4126
|
}
|
|
3373
4127
|
async persistDeltaCursor(cursor) {
|
|
3374
4128
|
if (!this.syncState?.saveDeltaCursor) {
|
|
@@ -3376,6 +4130,12 @@ var SyncEngine = class {
|
|
|
3376
4130
|
}
|
|
3377
4131
|
await this.syncState.saveDeltaCursor(cursor);
|
|
3378
4132
|
}
|
|
4133
|
+
async persistDeliveryWatermark(watermark, signature = this.deliverySignature()) {
|
|
4134
|
+
if (!this.syncState?.saveDeliveryWatermark) {
|
|
4135
|
+
return;
|
|
4136
|
+
}
|
|
4137
|
+
await this.syncState.saveDeliveryWatermark(signature, watermark);
|
|
4138
|
+
}
|
|
3379
4139
|
scheduleQuerySubsetReconnect() {
|
|
3380
4140
|
if (this.querySubsetReconnectTimer) {
|
|
3381
4141
|
clearTimeout(this.querySubsetReconnectTimer);
|
|
@@ -3388,8 +4148,7 @@ var SyncEngine = class {
|
|
|
3388
4148
|
}, 500);
|
|
3389
4149
|
}
|
|
3390
4150
|
async reconnectForQuerySubsets() {
|
|
3391
|
-
await this.
|
|
3392
|
-
await this.start();
|
|
4151
|
+
await this.reconnect();
|
|
3393
4152
|
}
|
|
3394
4153
|
};
|
|
3395
4154
|
function awarenessStatesToWire(states) {
|
|
@@ -3634,11 +4393,11 @@ var ReconnectionManager = class {
|
|
|
3634
4393
|
};
|
|
3635
4394
|
|
|
3636
4395
|
// src/encryption/sync-encryptor.ts
|
|
3637
|
-
var
|
|
4396
|
+
var import_core9 = require("@korajs/core");
|
|
3638
4397
|
|
|
3639
4398
|
// src/encryption/key-derivation.ts
|
|
3640
|
-
var
|
|
3641
|
-
var KeyDerivationError = class extends
|
|
4399
|
+
var import_core8 = require("@korajs/core");
|
|
4400
|
+
var KeyDerivationError = class extends import_core8.SyncError {
|
|
3642
4401
|
constructor(message, context) {
|
|
3643
4402
|
super(message, { ...context, errorType: "KEY_DERIVATION_ERROR" });
|
|
3644
4403
|
this.name = "KeyDerivationError";
|
|
@@ -3720,13 +4479,13 @@ async function deriveVersionedKey(passphrase, version, salt, iterations = DEFAUL
|
|
|
3720
4479
|
}
|
|
3721
4480
|
|
|
3722
4481
|
// src/encryption/sync-encryptor.ts
|
|
3723
|
-
var EncryptionError = class extends
|
|
4482
|
+
var EncryptionError = class extends import_core9.SyncError {
|
|
3724
4483
|
constructor(message, context) {
|
|
3725
4484
|
super(message, { ...context, errorType: "ENCRYPTION_ERROR" });
|
|
3726
4485
|
this.name = "EncryptionError";
|
|
3727
4486
|
}
|
|
3728
4487
|
};
|
|
3729
|
-
var DecryptionError = class extends
|
|
4488
|
+
var DecryptionError = class extends import_core9.SyncError {
|
|
3730
4489
|
constructor(message, context) {
|
|
3731
4490
|
super(message, { ...context, errorType: "DECRYPTION_ERROR" });
|
|
3732
4491
|
this.name = "DecryptionError";
|
|
@@ -4070,6 +4829,7 @@ function fromBase642(str) {
|
|
|
4070
4829
|
// src/reactivity/sync-status-controller.ts
|
|
4071
4830
|
var OFFLINE_SYNC_STATUS = Object.freeze({
|
|
4072
4831
|
status: "offline",
|
|
4832
|
+
reconnecting: false,
|
|
4073
4833
|
pendingOperations: 0,
|
|
4074
4834
|
lastSyncedAt: null,
|
|
4075
4835
|
lastSuccessfulPush: null,
|
|
@@ -4087,6 +4847,7 @@ var SYNC_STATUS_EVENT_TYPES = [
|
|
|
4087
4847
|
"sync:received",
|
|
4088
4848
|
"sync:acknowledged",
|
|
4089
4849
|
"sync:apply-failed",
|
|
4850
|
+
"sync:delivery-gap",
|
|
4090
4851
|
"sync:diagnostics",
|
|
4091
4852
|
"sync:initial-sync-progress"
|
|
4092
4853
|
];
|
|
@@ -4223,6 +4984,7 @@ function subscribeRemoteAwarenessStates(awareness, listener) {
|
|
|
4223
4984
|
// Annotate the CommonJS export names for ESM import in node:
|
|
4224
4985
|
0 && (module.exports = {
|
|
4225
4986
|
AwarenessManager,
|
|
4987
|
+
BlobChunkChannel,
|
|
4226
4988
|
ChaosTransport,
|
|
4227
4989
|
ConnectionMonitor,
|
|
4228
4990
|
DEFAULT_RICHTEXT_DOC_CHANNEL_THRESHOLD,
|
|
@@ -4232,6 +4994,7 @@ function subscribeRemoteAwarenessStates(awareness, listener) {
|
|
|
4232
4994
|
InvalidScopeError,
|
|
4233
4995
|
JsonMessageSerializer,
|
|
4234
4996
|
KeyDerivationError,
|
|
4997
|
+
MemoryRejectedOperationStorage,
|
|
4235
4998
|
NegotiatedMessageSerializer,
|
|
4236
4999
|
OFFLINE_SYNC_STATUS,
|
|
4237
5000
|
OutboundQueue,
|
|
@@ -4247,11 +5010,13 @@ function subscribeRemoteAwarenessStates(awareness, listener) {
|
|
|
4247
5010
|
WebSocketTransport,
|
|
4248
5011
|
createDeltaCursorFromBatch,
|
|
4249
5012
|
createSyncStatusController,
|
|
5013
|
+
decodeBlobChunkBytes,
|
|
4250
5014
|
decodeDeltaCursor,
|
|
4251
5015
|
decodeYjsUpdate,
|
|
4252
5016
|
dedupeQuerySubsets,
|
|
4253
5017
|
deriveKey,
|
|
4254
5018
|
deriveVersionedKey,
|
|
5019
|
+
encodeBlobChunkBytes,
|
|
4255
5020
|
encodeDeltaCursor,
|
|
4256
5021
|
encodeYjsUpdate,
|
|
4257
5022
|
filterOperationsByScope,
|
|
@@ -4259,12 +5024,16 @@ function subscribeRemoteAwarenessStates(awareness, listener) {
|
|
|
4259
5024
|
getRemoteAwarenessStates,
|
|
4260
5025
|
isAcknowledgmentMessage,
|
|
4261
5026
|
isAwarenessUpdateMessage,
|
|
5027
|
+
isBlobChunkPushMessage,
|
|
5028
|
+
isBlobChunkRequestMessage,
|
|
5029
|
+
isBlobChunkResponseMessage,
|
|
4262
5030
|
isClientSchemaVersionSupported,
|
|
4263
5031
|
isEncryptedPayload,
|
|
4264
5032
|
isErrorMessage,
|
|
4265
5033
|
isHandshakeMessage,
|
|
4266
5034
|
isHandshakeResponseMessage,
|
|
4267
5035
|
isOperationBatchMessage,
|
|
5036
|
+
isOperationRejectedMessage,
|
|
4268
5037
|
isSchemaMismatchReject,
|
|
4269
5038
|
isSyncMessage,
|
|
4270
5039
|
isYjsDocUpdateMessage,
|