@korajs/sync 1.0.0-beta.0 → 1.0.0-beta.2
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 +392 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +119 -3
- package/dist/index.d.ts +119 -3
- package/dist/index.js +369 -4
- 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-BGr-_eH8.d.cts} +138 -2
- package/dist/{transport-BMesMB6s.d.ts → transport-BGr-_eH8.d.ts} +138 -2
- 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,
|
|
@@ -105,6 +113,20 @@ var SYNC_STATUSES = [
|
|
|
105
113
|
"error",
|
|
106
114
|
"schema-mismatch"
|
|
107
115
|
];
|
|
116
|
+
var MemoryRejectedOperationStorage = class {
|
|
117
|
+
rejected = /* @__PURE__ */ new Map();
|
|
118
|
+
async record(rejected) {
|
|
119
|
+
this.rejected.set(rejected.operationId, rejected);
|
|
120
|
+
}
|
|
121
|
+
async list() {
|
|
122
|
+
return [...this.rejected.values()];
|
|
123
|
+
}
|
|
124
|
+
async remove(operationIds) {
|
|
125
|
+
for (const id of operationIds) {
|
|
126
|
+
this.rejected.delete(id);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
};
|
|
108
130
|
var ScopeViolationError = class extends import_core.KoraError {
|
|
109
131
|
constructor(operationId, collection, scope, message) {
|
|
110
132
|
super(
|
|
@@ -302,10 +324,18 @@ function isSyncMessage(value) {
|
|
|
302
324
|
return isAcknowledgmentMessage(value);
|
|
303
325
|
case "error":
|
|
304
326
|
return isErrorMessage(value);
|
|
327
|
+
case "operation-rejected":
|
|
328
|
+
return isOperationRejectedMessage(value);
|
|
305
329
|
case "awareness-update":
|
|
306
330
|
return isAwarenessUpdateMessage(value);
|
|
307
331
|
case "yjs-doc-update":
|
|
308
332
|
return isYjsDocUpdateMessage(value);
|
|
333
|
+
case "blob-chunk-request":
|
|
334
|
+
return isBlobChunkRequestMessage(value);
|
|
335
|
+
case "blob-chunk-response":
|
|
336
|
+
return isBlobChunkResponseMessage(value);
|
|
337
|
+
case "blob-chunk-push":
|
|
338
|
+
return isBlobChunkPushMessage(value);
|
|
309
339
|
default:
|
|
310
340
|
return false;
|
|
311
341
|
}
|
|
@@ -335,6 +365,11 @@ function isErrorMessage(value) {
|
|
|
335
365
|
const msg = value;
|
|
336
366
|
return msg.type === "error" && typeof msg.messageId === "string" && typeof msg.code === "string" && typeof msg.message === "string" && typeof msg.retriable === "boolean";
|
|
337
367
|
}
|
|
368
|
+
function isOperationRejectedMessage(value) {
|
|
369
|
+
if (typeof value !== "object" || value === null) return false;
|
|
370
|
+
const msg = value;
|
|
371
|
+
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";
|
|
372
|
+
}
|
|
338
373
|
function isAwarenessUpdateMessage(value) {
|
|
339
374
|
if (typeof value !== "object" || value === null) return false;
|
|
340
375
|
const msg = value;
|
|
@@ -345,6 +380,21 @@ function isYjsDocUpdateMessage(value) {
|
|
|
345
380
|
const msg = value;
|
|
346
381
|
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
382
|
}
|
|
383
|
+
function isBlobChunkRequestMessage(value) {
|
|
384
|
+
if (typeof value !== "object" || value === null) return false;
|
|
385
|
+
const msg = value;
|
|
386
|
+
return msg.type === "blob-chunk-request" && typeof msg.messageId === "string" && typeof msg.requestId === "string" && typeof msg.hash === "string";
|
|
387
|
+
}
|
|
388
|
+
function isBlobChunkResponseMessage(value) {
|
|
389
|
+
if (typeof value !== "object" || value === null) return false;
|
|
390
|
+
const msg = value;
|
|
391
|
+
return msg.type === "blob-chunk-response" && typeof msg.messageId === "string" && typeof msg.requestId === "string" && (typeof msg.bytes === "string" || msg.bytes === null);
|
|
392
|
+
}
|
|
393
|
+
function isBlobChunkPushMessage(value) {
|
|
394
|
+
if (typeof value !== "object" || value === null) return false;
|
|
395
|
+
const msg = value;
|
|
396
|
+
return msg.type === "blob-chunk-push" && typeof msg.messageId === "string" && typeof msg.hash === "string" && typeof msg.bytes === "string";
|
|
397
|
+
}
|
|
348
398
|
|
|
349
399
|
// src/protocol/serializer.ts
|
|
350
400
|
var import_core2 = require("@korajs/core");
|
|
@@ -585,7 +635,8 @@ function toProtoEnvelope(message) {
|
|
|
585
635
|
accepted: message.accepted,
|
|
586
636
|
rejectReason: message.rejectReason,
|
|
587
637
|
selectedWireFormat: message.selectedWireFormat,
|
|
588
|
-
serverTime: message.serverTime
|
|
638
|
+
serverTime: message.serverTime,
|
|
639
|
+
blobStorageEnabled: message.blobStorageEnabled
|
|
589
640
|
};
|
|
590
641
|
case "operation-batch":
|
|
591
642
|
return {
|
|
@@ -610,12 +661,45 @@ function toProtoEnvelope(message) {
|
|
|
610
661
|
errorMessage: message.message,
|
|
611
662
|
retriable: message.retriable
|
|
612
663
|
};
|
|
664
|
+
case "operation-rejected":
|
|
665
|
+
return {
|
|
666
|
+
type: message.type,
|
|
667
|
+
messageId: message.messageId,
|
|
668
|
+
operationId: message.operationId,
|
|
669
|
+
collection: message.collection,
|
|
670
|
+
recordId: message.recordId,
|
|
671
|
+
errorCode: message.code,
|
|
672
|
+
errorMessage: message.message,
|
|
673
|
+
retriable: message.retriable
|
|
674
|
+
};
|
|
613
675
|
case "awareness-update":
|
|
614
676
|
case "yjs-doc-update":
|
|
615
677
|
return {
|
|
616
678
|
type: message.type,
|
|
617
679
|
messageId: message.messageId
|
|
618
680
|
};
|
|
681
|
+
case "blob-chunk-request":
|
|
682
|
+
return {
|
|
683
|
+
type: message.type,
|
|
684
|
+
messageId: message.messageId,
|
|
685
|
+
requestId: message.requestId,
|
|
686
|
+
hash: message.hash
|
|
687
|
+
};
|
|
688
|
+
case "blob-chunk-response":
|
|
689
|
+
return {
|
|
690
|
+
type: message.type,
|
|
691
|
+
messageId: message.messageId,
|
|
692
|
+
requestId: message.requestId,
|
|
693
|
+
hasBytes: message.bytes !== null,
|
|
694
|
+
...message.bytes !== null ? { chunkBytes: message.bytes } : {}
|
|
695
|
+
};
|
|
696
|
+
case "blob-chunk-push":
|
|
697
|
+
return {
|
|
698
|
+
type: message.type,
|
|
699
|
+
messageId: message.messageId,
|
|
700
|
+
hash: message.hash,
|
|
701
|
+
chunkBytes: message.bytes
|
|
702
|
+
};
|
|
619
703
|
}
|
|
620
704
|
}
|
|
621
705
|
function fromProtoEnvelope(envelope) {
|
|
@@ -648,7 +732,8 @@ function fromProtoEnvelope(envelope) {
|
|
|
648
732
|
selectedWireFormat: envelope.selectedWireFormat === "json" || envelope.selectedWireFormat === "protobuf" ? envelope.selectedWireFormat : void 0,
|
|
649
733
|
// Preserve the server's wall-clock time so clock-skew detection and
|
|
650
734
|
// automatic timestamp rebase work over the protobuf wire, not just JSON.
|
|
651
|
-
...envelope.serverTime !== void 0 ? { serverTime: envelope.serverTime } : {}
|
|
735
|
+
...envelope.serverTime !== void 0 ? { serverTime: envelope.serverTime } : {},
|
|
736
|
+
...envelope.blobStorageEnabled !== void 0 ? { blobStorageEnabled: envelope.blobStorageEnabled } : {}
|
|
652
737
|
};
|
|
653
738
|
case "operation-batch":
|
|
654
739
|
return {
|
|
@@ -673,6 +758,38 @@ function fromProtoEnvelope(envelope) {
|
|
|
673
758
|
message: envelope.errorMessage ?? "Unknown error",
|
|
674
759
|
retriable: envelope.retriable ?? false
|
|
675
760
|
};
|
|
761
|
+
case "operation-rejected":
|
|
762
|
+
return {
|
|
763
|
+
type: "operation-rejected",
|
|
764
|
+
messageId: envelope.messageId,
|
|
765
|
+
operationId: envelope.operationId ?? "",
|
|
766
|
+
collection: envelope.collection ?? "",
|
|
767
|
+
recordId: envelope.recordId ?? "",
|
|
768
|
+
code: envelope.errorCode ?? "REJECTED",
|
|
769
|
+
message: envelope.errorMessage ?? "",
|
|
770
|
+
retriable: envelope.retriable ?? false
|
|
771
|
+
};
|
|
772
|
+
case "blob-chunk-request":
|
|
773
|
+
return {
|
|
774
|
+
type: "blob-chunk-request",
|
|
775
|
+
messageId: envelope.messageId,
|
|
776
|
+
requestId: envelope.requestId ?? "",
|
|
777
|
+
hash: envelope.hash ?? ""
|
|
778
|
+
};
|
|
779
|
+
case "blob-chunk-response":
|
|
780
|
+
return {
|
|
781
|
+
type: "blob-chunk-response",
|
|
782
|
+
messageId: envelope.messageId,
|
|
783
|
+
requestId: envelope.requestId ?? "",
|
|
784
|
+
bytes: envelope.hasBytes ? envelope.chunkBytes ?? "" : null
|
|
785
|
+
};
|
|
786
|
+
case "blob-chunk-push":
|
|
787
|
+
return {
|
|
788
|
+
type: "blob-chunk-push",
|
|
789
|
+
messageId: envelope.messageId,
|
|
790
|
+
hash: envelope.hash ?? "",
|
|
791
|
+
bytes: envelope.chunkBytes ?? ""
|
|
792
|
+
};
|
|
676
793
|
default:
|
|
677
794
|
throw new import_core2.SyncError("Failed to decode sync message: unknown protobuf type", {
|
|
678
795
|
type: envelope.type
|
|
@@ -818,6 +935,20 @@ function encodeEnvelope(envelope) {
|
|
|
818
935
|
}
|
|
819
936
|
if (envelope.retriable !== void 0) writer.uint32(144).bool(envelope.retriable);
|
|
820
937
|
if (envelope.serverTime !== void 0) writer.uint32(152).int64(envelope.serverTime);
|
|
938
|
+
if (envelope.requestId && envelope.requestId.length > 0)
|
|
939
|
+
writer.uint32(162).string(envelope.requestId);
|
|
940
|
+
if (envelope.hash && envelope.hash.length > 0) writer.uint32(170).string(envelope.hash);
|
|
941
|
+
if (envelope.chunkBytes && envelope.chunkBytes.length > 0)
|
|
942
|
+
writer.uint32(178).string(envelope.chunkBytes);
|
|
943
|
+
if (envelope.hasBytes !== void 0) writer.uint32(184).bool(envelope.hasBytes);
|
|
944
|
+
if (envelope.blobStorageEnabled !== void 0)
|
|
945
|
+
writer.uint32(200).bool(envelope.blobStorageEnabled);
|
|
946
|
+
if (envelope.operationId && envelope.operationId.length > 0)
|
|
947
|
+
writer.uint32(210).string(envelope.operationId);
|
|
948
|
+
if (envelope.collection && envelope.collection.length > 0)
|
|
949
|
+
writer.uint32(218).string(envelope.collection);
|
|
950
|
+
if (envelope.recordId && envelope.recordId.length > 0)
|
|
951
|
+
writer.uint32(226).string(envelope.recordId);
|
|
821
952
|
return writer.finish();
|
|
822
953
|
}
|
|
823
954
|
function decodeEnvelope(bytes) {
|
|
@@ -889,6 +1020,30 @@ function decodeEnvelope(bytes) {
|
|
|
889
1020
|
case 19:
|
|
890
1021
|
envelope.serverTime = longToNumber(reader.int64());
|
|
891
1022
|
break;
|
|
1023
|
+
case 20:
|
|
1024
|
+
envelope.requestId = reader.string();
|
|
1025
|
+
break;
|
|
1026
|
+
case 21:
|
|
1027
|
+
envelope.hash = reader.string();
|
|
1028
|
+
break;
|
|
1029
|
+
case 22:
|
|
1030
|
+
envelope.chunkBytes = reader.string();
|
|
1031
|
+
break;
|
|
1032
|
+
case 23:
|
|
1033
|
+
envelope.hasBytes = reader.bool();
|
|
1034
|
+
break;
|
|
1035
|
+
case 25:
|
|
1036
|
+
envelope.blobStorageEnabled = reader.bool();
|
|
1037
|
+
break;
|
|
1038
|
+
case 26:
|
|
1039
|
+
envelope.operationId = reader.string();
|
|
1040
|
+
break;
|
|
1041
|
+
case 27:
|
|
1042
|
+
envelope.collection = reader.string();
|
|
1043
|
+
break;
|
|
1044
|
+
case 28:
|
|
1045
|
+
envelope.recordId = reader.string();
|
|
1046
|
+
break;
|
|
892
1047
|
default:
|
|
893
1048
|
reader.skipType(tag & 7);
|
|
894
1049
|
}
|
|
@@ -1480,7 +1635,7 @@ var ChaosTransport = class {
|
|
|
1480
1635
|
};
|
|
1481
1636
|
|
|
1482
1637
|
// src/engine/sync-engine.ts
|
|
1483
|
-
var
|
|
1638
|
+
var import_core7 = require("@korajs/core");
|
|
1484
1639
|
var import_internal2 = require("@korajs/core/internal");
|
|
1485
1640
|
|
|
1486
1641
|
// src/awareness/awareness-manager.ts
|
|
@@ -1689,6 +1844,95 @@ var AwarenessManager = class {
|
|
|
1689
1844
|
}
|
|
1690
1845
|
};
|
|
1691
1846
|
|
|
1847
|
+
// src/blob/blob-chunk-channel.ts
|
|
1848
|
+
var import_core5 = require("@korajs/core");
|
|
1849
|
+
function encodeBlobChunkBytes(bytes) {
|
|
1850
|
+
let binary = "";
|
|
1851
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
1852
|
+
binary += String.fromCharCode(bytes[i]);
|
|
1853
|
+
}
|
|
1854
|
+
return btoa(binary);
|
|
1855
|
+
}
|
|
1856
|
+
function decodeBlobChunkBytes(encoded) {
|
|
1857
|
+
const binary = atob(encoded);
|
|
1858
|
+
const out = new Uint8Array(binary.length);
|
|
1859
|
+
for (let i = 0; i < binary.length; i++) {
|
|
1860
|
+
out[i] = binary.charCodeAt(i);
|
|
1861
|
+
}
|
|
1862
|
+
return out;
|
|
1863
|
+
}
|
|
1864
|
+
var BlobChunkChannel = class {
|
|
1865
|
+
onSend;
|
|
1866
|
+
handlers = /* @__PURE__ */ new Set();
|
|
1867
|
+
constructor(options) {
|
|
1868
|
+
this.onSend = options?.onSend ?? null;
|
|
1869
|
+
}
|
|
1870
|
+
/**
|
|
1871
|
+
* Send a blob-chunk message to the peer. Response bytes are base64-encoded
|
|
1872
|
+
* onto the wire here; callers pass a `Uint8Array`.
|
|
1873
|
+
*/
|
|
1874
|
+
send(message) {
|
|
1875
|
+
if (!this.onSend) {
|
|
1876
|
+
return;
|
|
1877
|
+
}
|
|
1878
|
+
if (message.type === "blob-chunk-request") {
|
|
1879
|
+
this.onSend({
|
|
1880
|
+
type: "blob-chunk-request",
|
|
1881
|
+
messageId: (0, import_core5.generateUUIDv7)(),
|
|
1882
|
+
requestId: message.requestId,
|
|
1883
|
+
hash: message.hash
|
|
1884
|
+
});
|
|
1885
|
+
return;
|
|
1886
|
+
}
|
|
1887
|
+
if (message.type === "blob-chunk-push") {
|
|
1888
|
+
this.onSend({
|
|
1889
|
+
type: "blob-chunk-push",
|
|
1890
|
+
messageId: (0, import_core5.generateUUIDv7)(),
|
|
1891
|
+
hash: message.hash,
|
|
1892
|
+
bytes: encodeBlobChunkBytes(message.bytes)
|
|
1893
|
+
});
|
|
1894
|
+
return;
|
|
1895
|
+
}
|
|
1896
|
+
this.onSend({
|
|
1897
|
+
type: "blob-chunk-response",
|
|
1898
|
+
messageId: (0, import_core5.generateUUIDv7)(),
|
|
1899
|
+
requestId: message.requestId,
|
|
1900
|
+
bytes: message.bytes === null ? null : encodeBlobChunkBytes(message.bytes)
|
|
1901
|
+
});
|
|
1902
|
+
}
|
|
1903
|
+
/**
|
|
1904
|
+
* Register a handler for inbound blob-chunk messages. Multiple handlers are
|
|
1905
|
+
* allowed; each receives every delivered message.
|
|
1906
|
+
*/
|
|
1907
|
+
onMessage(handler) {
|
|
1908
|
+
this.handlers.add(handler);
|
|
1909
|
+
return () => {
|
|
1910
|
+
this.handlers.delete(handler);
|
|
1911
|
+
};
|
|
1912
|
+
}
|
|
1913
|
+
/**
|
|
1914
|
+
* Deliver an inbound wire message to registered handlers, decoding response
|
|
1915
|
+
* bytes from base64 back to a `Uint8Array` first.
|
|
1916
|
+
*/
|
|
1917
|
+
deliver(message) {
|
|
1918
|
+
if (this.handlers.size === 0) {
|
|
1919
|
+
return;
|
|
1920
|
+
}
|
|
1921
|
+
const decoded = message.type === "blob-chunk-request" ? { type: "blob-chunk-request", requestId: message.requestId, hash: message.hash } : message.type === "blob-chunk-push" ? {
|
|
1922
|
+
type: "blob-chunk-push",
|
|
1923
|
+
hash: message.hash,
|
|
1924
|
+
bytes: decodeBlobChunkBytes(message.bytes)
|
|
1925
|
+
} : {
|
|
1926
|
+
type: "blob-chunk-response",
|
|
1927
|
+
requestId: message.requestId,
|
|
1928
|
+
bytes: message.bytes === null ? null : decodeBlobChunkBytes(message.bytes)
|
|
1929
|
+
};
|
|
1930
|
+
for (const handler of this.handlers) {
|
|
1931
|
+
handler(decoded);
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
};
|
|
1935
|
+
|
|
1692
1936
|
// src/diagnostics/bandwidth-estimator.ts
|
|
1693
1937
|
var BandwidthEstimator = class {
|
|
1694
1938
|
maxSamples;
|
|
@@ -2132,7 +2376,7 @@ var SyncMetricsCollector = class {
|
|
|
2132
2376
|
};
|
|
2133
2377
|
|
|
2134
2378
|
// src/richtext/richtext-doc-channel.ts
|
|
2135
|
-
var
|
|
2379
|
+
var import_core6 = require("@korajs/core");
|
|
2136
2380
|
|
|
2137
2381
|
// src/richtext/doc-channel-wire.ts
|
|
2138
2382
|
function encodeYjsUpdate(update) {
|
|
@@ -2211,7 +2455,7 @@ var RichtextDocChannel = class {
|
|
|
2211
2455
|
}
|
|
2212
2456
|
this.onSend({
|
|
2213
2457
|
type: "yjs-doc-update",
|
|
2214
|
-
messageId: (0,
|
|
2458
|
+
messageId: (0, import_core6.generateUUIDv7)(),
|
|
2215
2459
|
collection,
|
|
2216
2460
|
recordId,
|
|
2217
2461
|
field,
|
|
@@ -2400,6 +2644,37 @@ var OutboundQueue = class {
|
|
|
2400
2644
|
get isInitialized() {
|
|
2401
2645
|
return this.initialized;
|
|
2402
2646
|
}
|
|
2647
|
+
/**
|
|
2648
|
+
* Remove a single operation the server permanently rejected — from the queue,
|
|
2649
|
+
* from any in-flight batch, and from durable storage — so a later batch ack or
|
|
2650
|
+
* a reconnect `returnBatch` can never resend or resurrect it. Returns the
|
|
2651
|
+
* removed operation so the caller can record it in a durable rejected store,
|
|
2652
|
+
* or null if it was not present (already acknowledged and removed).
|
|
2653
|
+
*/
|
|
2654
|
+
async reject(opId) {
|
|
2655
|
+
let removed = null;
|
|
2656
|
+
for (const [batchId, ops] of this.inFlight) {
|
|
2657
|
+
const idx = ops.findIndex((op) => op.id === opId);
|
|
2658
|
+
if (idx !== -1) {
|
|
2659
|
+
removed = ops[idx] ?? null;
|
|
2660
|
+
ops.splice(idx, 1);
|
|
2661
|
+
if (ops.length === 0) {
|
|
2662
|
+
this.inFlight.delete(batchId);
|
|
2663
|
+
}
|
|
2664
|
+
break;
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
if (!removed) {
|
|
2668
|
+
const idx = this.queue.findIndex((op) => op.id === opId);
|
|
2669
|
+
if (idx !== -1) {
|
|
2670
|
+
removed = this.queue[idx] ?? null;
|
|
2671
|
+
this.queue.splice(idx, 1);
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
this.seen.delete(opId);
|
|
2675
|
+
await this.storage.dequeue([opId]);
|
|
2676
|
+
return removed;
|
|
2677
|
+
}
|
|
2403
2678
|
/**
|
|
2404
2679
|
* Remove operations by id from queue and persistent storage.
|
|
2405
2680
|
* Used when ops were already sent during handshake delta exchange.
|
|
@@ -2439,10 +2714,12 @@ var SyncEngine = class {
|
|
|
2439
2714
|
serializer;
|
|
2440
2715
|
emitter;
|
|
2441
2716
|
outboundQueue;
|
|
2717
|
+
rejectedStorage;
|
|
2442
2718
|
batchSize;
|
|
2443
2719
|
encryptor;
|
|
2444
2720
|
awarenessManager;
|
|
2445
2721
|
richtextDocChannel;
|
|
2722
|
+
blobChunkChannel;
|
|
2446
2723
|
metricsCollector;
|
|
2447
2724
|
syncState;
|
|
2448
2725
|
remoteVector = /* @__PURE__ */ new Map();
|
|
@@ -2457,6 +2734,7 @@ var SyncEngine = class {
|
|
|
2457
2734
|
schemaBlocked = false;
|
|
2458
2735
|
clockBlocked = false;
|
|
2459
2736
|
clockSkewMs = null;
|
|
2737
|
+
blobStorageEnabled = false;
|
|
2460
2738
|
// Track delta exchange state
|
|
2461
2739
|
deltaBatchesReceived = 0;
|
|
2462
2740
|
deltaReceiveComplete = false;
|
|
@@ -2489,6 +2767,7 @@ var SyncEngine = class {
|
|
|
2489
2767
|
this.activeScope = options.config.scopeMap;
|
|
2490
2768
|
const queueStorage = options.queueStorage ?? new MemoryQueueStorage();
|
|
2491
2769
|
this.outboundQueue = new OutboundQueue(queueStorage);
|
|
2770
|
+
this.rejectedStorage = options.rejectedStorage ?? new MemoryRejectedOperationStorage();
|
|
2492
2771
|
this.metricsCollector = new SyncMetricsCollector(options.metricsConfig);
|
|
2493
2772
|
if (this.emitter) {
|
|
2494
2773
|
this.metricsCollector.attachEmitter(this.emitter);
|
|
@@ -2505,6 +2784,14 @@ var SyncEngine = class {
|
|
|
2505
2784
|
this.transport.send(message);
|
|
2506
2785
|
}
|
|
2507
2786
|
});
|
|
2787
|
+
this.blobChunkChannel = new BlobChunkChannel({
|
|
2788
|
+
onSend: (message) => {
|
|
2789
|
+
if (this.state !== "streaming") {
|
|
2790
|
+
return;
|
|
2791
|
+
}
|
|
2792
|
+
this.transport.send(message);
|
|
2793
|
+
}
|
|
2794
|
+
});
|
|
2508
2795
|
this.awarenessManager.onSend((message) => {
|
|
2509
2796
|
if (this.state !== "streaming") return;
|
|
2510
2797
|
const wireMessage = {
|
|
@@ -2521,7 +2808,7 @@ var SyncEngine = class {
|
|
|
2521
2808
|
*/
|
|
2522
2809
|
async start() {
|
|
2523
2810
|
if (this.state !== "disconnected") {
|
|
2524
|
-
throw new
|
|
2811
|
+
throw new import_core7.SyncError("Cannot start sync engine: not in disconnected state", {
|
|
2525
2812
|
currentState: this.state
|
|
2526
2813
|
});
|
|
2527
2814
|
}
|
|
@@ -2538,7 +2825,7 @@ var SyncEngine = class {
|
|
|
2538
2825
|
this.transport.onClose((reason) => this.handleTransportClose(reason));
|
|
2539
2826
|
this.transport.onError((err) => this.handleTransportError(err));
|
|
2540
2827
|
if (this.schemaBlocked) {
|
|
2541
|
-
throw new
|
|
2828
|
+
throw new import_core7.SyncError(
|
|
2542
2829
|
"Sync is blocked due to schema version mismatch. Upgrade the app schema or align sync.schemaVersion with the server.",
|
|
2543
2830
|
{ code: "SCHEMA_MISMATCH_BLOCKED" }
|
|
2544
2831
|
);
|
|
@@ -2708,7 +2995,7 @@ var SyncEngine = class {
|
|
|
2708
2995
|
return this.getStatus().pendingOperations;
|
|
2709
2996
|
}
|
|
2710
2997
|
emitApplyFailure(op, result, overrides) {
|
|
2711
|
-
const reason = (0,
|
|
2998
|
+
const reason = (0, import_core7.defaultApplyFailureReason)(result, overrides);
|
|
2712
2999
|
this.emitter?.emit({
|
|
2713
3000
|
type: "sync:apply-failed",
|
|
2714
3001
|
operationId: op.id,
|
|
@@ -2818,6 +3105,31 @@ var SyncEngine = class {
|
|
|
2818
3105
|
getRichtextDocChannel() {
|
|
2819
3106
|
return this.richtextDocChannel;
|
|
2820
3107
|
}
|
|
3108
|
+
/**
|
|
3109
|
+
* Side channel for out-of-band blob chunk transfer over the sync connection.
|
|
3110
|
+
* The app layer bridges this to a `ChunkMessagePort` to pull/serve blob bytes.
|
|
3111
|
+
*/
|
|
3112
|
+
getBlobChunkChannel() {
|
|
3113
|
+
return this.blobChunkChannel;
|
|
3114
|
+
}
|
|
3115
|
+
/**
|
|
3116
|
+
* Whether the connected server persists blob bytes centrally (learned from the
|
|
3117
|
+
* handshake response). When true, the app uploads the bytes behind `blob`
|
|
3118
|
+
* fields so they stay available after the authoring device goes offline.
|
|
3119
|
+
*/
|
|
3120
|
+
isBlobStorageEnabled() {
|
|
3121
|
+
return this.blobStorageEnabled;
|
|
3122
|
+
}
|
|
3123
|
+
/**
|
|
3124
|
+
* Upload a blob chunk (or manifest) to the server for central persistence.
|
|
3125
|
+
* A no-op unless connected and the server advertised blob storage.
|
|
3126
|
+
*/
|
|
3127
|
+
uploadBlobChunk(hash, bytes) {
|
|
3128
|
+
if (this.state !== "streaming" || !this.blobStorageEnabled) {
|
|
3129
|
+
return;
|
|
3130
|
+
}
|
|
3131
|
+
this.blobChunkChannel.send({ type: "blob-chunk-push", hash, bytes });
|
|
3132
|
+
}
|
|
2821
3133
|
// --- Private methods ---
|
|
2822
3134
|
messageChain = Promise.resolve();
|
|
2823
3135
|
enqueueMessage(message) {
|
|
@@ -2837,12 +3149,20 @@ var SyncEngine = class {
|
|
|
2837
3149
|
case "error":
|
|
2838
3150
|
this.handleError(message);
|
|
2839
3151
|
break;
|
|
3152
|
+
case "operation-rejected":
|
|
3153
|
+
await this.handleOperationRejected(message);
|
|
3154
|
+
break;
|
|
2840
3155
|
case "awareness-update":
|
|
2841
3156
|
this.handleAwarenessUpdate(message);
|
|
2842
3157
|
break;
|
|
2843
3158
|
case "yjs-doc-update":
|
|
2844
3159
|
this.richtextDocChannel.deliver(message);
|
|
2845
3160
|
break;
|
|
3161
|
+
case "blob-chunk-request":
|
|
3162
|
+
case "blob-chunk-response":
|
|
3163
|
+
case "blob-chunk-push":
|
|
3164
|
+
this.blobChunkChannel.deliver(message);
|
|
3165
|
+
break;
|
|
2846
3166
|
}
|
|
2847
3167
|
}
|
|
2848
3168
|
handleMessageFailure(error) {
|
|
@@ -2875,6 +3195,7 @@ var SyncEngine = class {
|
|
|
2875
3195
|
}
|
|
2876
3196
|
async handleHandshakeResponse(msg) {
|
|
2877
3197
|
if (this.state !== "handshaking") return;
|
|
3198
|
+
this.blobStorageEnabled = msg.blobStorageEnabled === true;
|
|
2878
3199
|
if (typeof msg.serverTime === "number") {
|
|
2879
3200
|
this.evaluateClockSkew(msg.serverTime);
|
|
2880
3201
|
if (this.clockBlocked) {
|
|
@@ -3059,7 +3380,7 @@ var SyncEngine = class {
|
|
|
3059
3380
|
const targetSchemaVersion = this.config.schemaVersion ?? DEFAULT_SCHEMA_VERSION;
|
|
3060
3381
|
const transforms = this.config.operationTransforms ?? [];
|
|
3061
3382
|
for (const op of inScopeOps) {
|
|
3062
|
-
const transformed = transforms.length > 0 ? (0,
|
|
3383
|
+
const transformed = transforms.length > 0 ? (0, import_core7.applyOperationTransforms)(op, targetSchemaVersion, transforms) : op;
|
|
3063
3384
|
if (transformed === null) {
|
|
3064
3385
|
continue;
|
|
3065
3386
|
}
|
|
@@ -3069,20 +3390,20 @@ var SyncEngine = class {
|
|
|
3069
3390
|
this.emitApplyFailure(transformed, result);
|
|
3070
3391
|
}
|
|
3071
3392
|
} catch (error) {
|
|
3072
|
-
if (error instanceof
|
|
3393
|
+
if (error instanceof import_core7.ClockDriftError) {
|
|
3073
3394
|
this.emitApplyFailure(transformed, "rejected", {
|
|
3074
|
-
code:
|
|
3395
|
+
code: import_core7.APPLY_FAILURE_CODES.CLOCK_DRIFT,
|
|
3075
3396
|
message: error.message,
|
|
3076
3397
|
retriable: false
|
|
3077
3398
|
});
|
|
3078
3399
|
continue;
|
|
3079
3400
|
}
|
|
3080
3401
|
const message = error instanceof Error ? error.message : "Apply failed";
|
|
3081
|
-
const code = error instanceof
|
|
3402
|
+
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;
|
|
3082
3403
|
this.emitApplyFailure(transformed, "rejected", {
|
|
3083
3404
|
code,
|
|
3084
3405
|
message,
|
|
3085
|
-
retriable: code !==
|
|
3406
|
+
retriable: code !== import_core7.APPLY_FAILURE_CODES.REFERENTIAL_INTEGRITY
|
|
3086
3407
|
});
|
|
3087
3408
|
}
|
|
3088
3409
|
}
|
|
@@ -3168,6 +3489,50 @@ var SyncEngine = class {
|
|
|
3168
3489
|
this.emitter?.emit({ type: "sync:disconnected", reason: msg.message });
|
|
3169
3490
|
this.transitionTo("disconnected");
|
|
3170
3491
|
}
|
|
3492
|
+
/**
|
|
3493
|
+
* Handle a per-operation rejection: divert the op out of the pending outbound
|
|
3494
|
+
* queue (so it is never retried or resurrected on reconnect), record it in the
|
|
3495
|
+
* durable rejected store (so it is kept and explainable), and emit an event so
|
|
3496
|
+
* the app can reconcile. Unlike {@link handleError}, this is a normal per-op
|
|
3497
|
+
* signal, so the connection stays up.
|
|
3498
|
+
*/
|
|
3499
|
+
async handleOperationRejected(msg) {
|
|
3500
|
+
await this.outboundQueue.reject(msg.operationId);
|
|
3501
|
+
await this.rejectedStorage.record({
|
|
3502
|
+
operationId: msg.operationId,
|
|
3503
|
+
collection: msg.collection,
|
|
3504
|
+
recordId: msg.recordId,
|
|
3505
|
+
code: msg.code,
|
|
3506
|
+
message: msg.message,
|
|
3507
|
+
retriable: msg.retriable,
|
|
3508
|
+
rejectedAt: Date.now()
|
|
3509
|
+
});
|
|
3510
|
+
await this.refreshPendingCount();
|
|
3511
|
+
this.emitter?.emit({
|
|
3512
|
+
type: "sync:operation-rejected",
|
|
3513
|
+
operationId: msg.operationId,
|
|
3514
|
+
collection: msg.collection,
|
|
3515
|
+
recordId: msg.recordId,
|
|
3516
|
+
code: msg.code,
|
|
3517
|
+
message: msg.message,
|
|
3518
|
+
retriable: msg.retriable
|
|
3519
|
+
});
|
|
3520
|
+
}
|
|
3521
|
+
/**
|
|
3522
|
+
* Every operation the server rejected that has not yet been reconciled. The app
|
|
3523
|
+
* uses this (alongside the `sync:operation-rejected` event) to surface failed
|
|
3524
|
+
* submissions and decide whether to roll back or resubmit.
|
|
3525
|
+
*/
|
|
3526
|
+
getRejectedOperations() {
|
|
3527
|
+
return this.rejectedStorage.list();
|
|
3528
|
+
}
|
|
3529
|
+
/**
|
|
3530
|
+
* Forget rejected operations by id once the app has reconciled them (rolled the
|
|
3531
|
+
* optimistic write back or resubmitted a corrected op).
|
|
3532
|
+
*/
|
|
3533
|
+
clearRejectedOperations(operationIds) {
|
|
3534
|
+
return this.rejectedStorage.remove(operationIds);
|
|
3535
|
+
}
|
|
3171
3536
|
async checkDeltaComplete() {
|
|
3172
3537
|
if (!this.deltaSendComplete || !this.deltaReceiveComplete) {
|
|
3173
3538
|
return;
|
|
@@ -3352,7 +3717,7 @@ var SyncEngine = class {
|
|
|
3352
3717
|
transitionTo(newState) {
|
|
3353
3718
|
const validTargets = VALID_TRANSITIONS[this.state];
|
|
3354
3719
|
if (!validTargets.includes(newState)) {
|
|
3355
|
-
throw new
|
|
3720
|
+
throw new import_core7.SyncError(`Invalid sync state transition: ${this.state} \u2192 ${newState}`, {
|
|
3356
3721
|
from: this.state,
|
|
3357
3722
|
to: newState
|
|
3358
3723
|
});
|
|
@@ -3634,11 +3999,11 @@ var ReconnectionManager = class {
|
|
|
3634
3999
|
};
|
|
3635
4000
|
|
|
3636
4001
|
// src/encryption/sync-encryptor.ts
|
|
3637
|
-
var
|
|
4002
|
+
var import_core9 = require("@korajs/core");
|
|
3638
4003
|
|
|
3639
4004
|
// src/encryption/key-derivation.ts
|
|
3640
|
-
var
|
|
3641
|
-
var KeyDerivationError = class extends
|
|
4005
|
+
var import_core8 = require("@korajs/core");
|
|
4006
|
+
var KeyDerivationError = class extends import_core8.SyncError {
|
|
3642
4007
|
constructor(message, context) {
|
|
3643
4008
|
super(message, { ...context, errorType: "KEY_DERIVATION_ERROR" });
|
|
3644
4009
|
this.name = "KeyDerivationError";
|
|
@@ -3720,13 +4085,13 @@ async function deriveVersionedKey(passphrase, version, salt, iterations = DEFAUL
|
|
|
3720
4085
|
}
|
|
3721
4086
|
|
|
3722
4087
|
// src/encryption/sync-encryptor.ts
|
|
3723
|
-
var EncryptionError = class extends
|
|
4088
|
+
var EncryptionError = class extends import_core9.SyncError {
|
|
3724
4089
|
constructor(message, context) {
|
|
3725
4090
|
super(message, { ...context, errorType: "ENCRYPTION_ERROR" });
|
|
3726
4091
|
this.name = "EncryptionError";
|
|
3727
4092
|
}
|
|
3728
4093
|
};
|
|
3729
|
-
var DecryptionError = class extends
|
|
4094
|
+
var DecryptionError = class extends import_core9.SyncError {
|
|
3730
4095
|
constructor(message, context) {
|
|
3731
4096
|
super(message, { ...context, errorType: "DECRYPTION_ERROR" });
|
|
3732
4097
|
this.name = "DecryptionError";
|
|
@@ -4223,6 +4588,7 @@ function subscribeRemoteAwarenessStates(awareness, listener) {
|
|
|
4223
4588
|
// Annotate the CommonJS export names for ESM import in node:
|
|
4224
4589
|
0 && (module.exports = {
|
|
4225
4590
|
AwarenessManager,
|
|
4591
|
+
BlobChunkChannel,
|
|
4226
4592
|
ChaosTransport,
|
|
4227
4593
|
ConnectionMonitor,
|
|
4228
4594
|
DEFAULT_RICHTEXT_DOC_CHANNEL_THRESHOLD,
|
|
@@ -4232,6 +4598,7 @@ function subscribeRemoteAwarenessStates(awareness, listener) {
|
|
|
4232
4598
|
InvalidScopeError,
|
|
4233
4599
|
JsonMessageSerializer,
|
|
4234
4600
|
KeyDerivationError,
|
|
4601
|
+
MemoryRejectedOperationStorage,
|
|
4235
4602
|
NegotiatedMessageSerializer,
|
|
4236
4603
|
OFFLINE_SYNC_STATUS,
|
|
4237
4604
|
OutboundQueue,
|
|
@@ -4247,11 +4614,13 @@ function subscribeRemoteAwarenessStates(awareness, listener) {
|
|
|
4247
4614
|
WebSocketTransport,
|
|
4248
4615
|
createDeltaCursorFromBatch,
|
|
4249
4616
|
createSyncStatusController,
|
|
4617
|
+
decodeBlobChunkBytes,
|
|
4250
4618
|
decodeDeltaCursor,
|
|
4251
4619
|
decodeYjsUpdate,
|
|
4252
4620
|
dedupeQuerySubsets,
|
|
4253
4621
|
deriveKey,
|
|
4254
4622
|
deriveVersionedKey,
|
|
4623
|
+
encodeBlobChunkBytes,
|
|
4255
4624
|
encodeDeltaCursor,
|
|
4256
4625
|
encodeYjsUpdate,
|
|
4257
4626
|
filterOperationsByScope,
|
|
@@ -4259,12 +4628,16 @@ function subscribeRemoteAwarenessStates(awareness, listener) {
|
|
|
4259
4628
|
getRemoteAwarenessStates,
|
|
4260
4629
|
isAcknowledgmentMessage,
|
|
4261
4630
|
isAwarenessUpdateMessage,
|
|
4631
|
+
isBlobChunkPushMessage,
|
|
4632
|
+
isBlobChunkRequestMessage,
|
|
4633
|
+
isBlobChunkResponseMessage,
|
|
4262
4634
|
isClientSchemaVersionSupported,
|
|
4263
4635
|
isEncryptedPayload,
|
|
4264
4636
|
isErrorMessage,
|
|
4265
4637
|
isHandshakeMessage,
|
|
4266
4638
|
isHandshakeResponseMessage,
|
|
4267
4639
|
isOperationBatchMessage,
|
|
4640
|
+
isOperationRejectedMessage,
|
|
4268
4641
|
isSchemaMismatchReject,
|
|
4269
4642
|
isSyncMessage,
|
|
4270
4643
|
isYjsDocUpdateMessage,
|