@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.js
CHANGED
|
@@ -21,6 +21,20 @@ var SYNC_STATUSES = [
|
|
|
21
21
|
"error",
|
|
22
22
|
"schema-mismatch"
|
|
23
23
|
];
|
|
24
|
+
var MemoryRejectedOperationStorage = class {
|
|
25
|
+
rejected = /* @__PURE__ */ new Map();
|
|
26
|
+
async record(rejected) {
|
|
27
|
+
this.rejected.set(rejected.operationId, rejected);
|
|
28
|
+
}
|
|
29
|
+
async list() {
|
|
30
|
+
return [...this.rejected.values()];
|
|
31
|
+
}
|
|
32
|
+
async remove(operationIds) {
|
|
33
|
+
for (const id of operationIds) {
|
|
34
|
+
this.rejected.delete(id);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
};
|
|
24
38
|
var ScopeViolationError = class extends KoraError {
|
|
25
39
|
constructor(operationId, collection, scope, message) {
|
|
26
40
|
super(
|
|
@@ -218,10 +232,18 @@ function isSyncMessage(value) {
|
|
|
218
232
|
return isAcknowledgmentMessage(value);
|
|
219
233
|
case "error":
|
|
220
234
|
return isErrorMessage(value);
|
|
235
|
+
case "operation-rejected":
|
|
236
|
+
return isOperationRejectedMessage(value);
|
|
221
237
|
case "awareness-update":
|
|
222
238
|
return isAwarenessUpdateMessage(value);
|
|
223
239
|
case "yjs-doc-update":
|
|
224
240
|
return isYjsDocUpdateMessage(value);
|
|
241
|
+
case "blob-chunk-request":
|
|
242
|
+
return isBlobChunkRequestMessage(value);
|
|
243
|
+
case "blob-chunk-response":
|
|
244
|
+
return isBlobChunkResponseMessage(value);
|
|
245
|
+
case "blob-chunk-push":
|
|
246
|
+
return isBlobChunkPushMessage(value);
|
|
225
247
|
default:
|
|
226
248
|
return false;
|
|
227
249
|
}
|
|
@@ -251,6 +273,11 @@ function isErrorMessage(value) {
|
|
|
251
273
|
const msg = value;
|
|
252
274
|
return msg.type === "error" && typeof msg.messageId === "string" && typeof msg.code === "string" && typeof msg.message === "string" && typeof msg.retriable === "boolean";
|
|
253
275
|
}
|
|
276
|
+
function isOperationRejectedMessage(value) {
|
|
277
|
+
if (typeof value !== "object" || value === null) return false;
|
|
278
|
+
const msg = value;
|
|
279
|
+
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";
|
|
280
|
+
}
|
|
254
281
|
function isAwarenessUpdateMessage(value) {
|
|
255
282
|
if (typeof value !== "object" || value === null) return false;
|
|
256
283
|
const msg = value;
|
|
@@ -261,6 +288,21 @@ function isYjsDocUpdateMessage(value) {
|
|
|
261
288
|
const msg = value;
|
|
262
289
|
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";
|
|
263
290
|
}
|
|
291
|
+
function isBlobChunkRequestMessage(value) {
|
|
292
|
+
if (typeof value !== "object" || value === null) return false;
|
|
293
|
+
const msg = value;
|
|
294
|
+
return msg.type === "blob-chunk-request" && typeof msg.messageId === "string" && typeof msg.requestId === "string" && typeof msg.hash === "string";
|
|
295
|
+
}
|
|
296
|
+
function isBlobChunkResponseMessage(value) {
|
|
297
|
+
if (typeof value !== "object" || value === null) return false;
|
|
298
|
+
const msg = value;
|
|
299
|
+
return msg.type === "blob-chunk-response" && typeof msg.messageId === "string" && typeof msg.requestId === "string" && (typeof msg.bytes === "string" || msg.bytes === null);
|
|
300
|
+
}
|
|
301
|
+
function isBlobChunkPushMessage(value) {
|
|
302
|
+
if (typeof value !== "object" || value === null) return false;
|
|
303
|
+
const msg = value;
|
|
304
|
+
return msg.type === "blob-chunk-push" && typeof msg.messageId === "string" && typeof msg.hash === "string" && typeof msg.bytes === "string";
|
|
305
|
+
}
|
|
264
306
|
|
|
265
307
|
// src/protocol/serializer.ts
|
|
266
308
|
import { SyncError } from "@korajs/core";
|
|
@@ -501,7 +543,8 @@ function toProtoEnvelope(message) {
|
|
|
501
543
|
accepted: message.accepted,
|
|
502
544
|
rejectReason: message.rejectReason,
|
|
503
545
|
selectedWireFormat: message.selectedWireFormat,
|
|
504
|
-
serverTime: message.serverTime
|
|
546
|
+
serverTime: message.serverTime,
|
|
547
|
+
blobStorageEnabled: message.blobStorageEnabled
|
|
505
548
|
};
|
|
506
549
|
case "operation-batch":
|
|
507
550
|
return {
|
|
@@ -526,12 +569,45 @@ function toProtoEnvelope(message) {
|
|
|
526
569
|
errorMessage: message.message,
|
|
527
570
|
retriable: message.retriable
|
|
528
571
|
};
|
|
572
|
+
case "operation-rejected":
|
|
573
|
+
return {
|
|
574
|
+
type: message.type,
|
|
575
|
+
messageId: message.messageId,
|
|
576
|
+
operationId: message.operationId,
|
|
577
|
+
collection: message.collection,
|
|
578
|
+
recordId: message.recordId,
|
|
579
|
+
errorCode: message.code,
|
|
580
|
+
errorMessage: message.message,
|
|
581
|
+
retriable: message.retriable
|
|
582
|
+
};
|
|
529
583
|
case "awareness-update":
|
|
530
584
|
case "yjs-doc-update":
|
|
531
585
|
return {
|
|
532
586
|
type: message.type,
|
|
533
587
|
messageId: message.messageId
|
|
534
588
|
};
|
|
589
|
+
case "blob-chunk-request":
|
|
590
|
+
return {
|
|
591
|
+
type: message.type,
|
|
592
|
+
messageId: message.messageId,
|
|
593
|
+
requestId: message.requestId,
|
|
594
|
+
hash: message.hash
|
|
595
|
+
};
|
|
596
|
+
case "blob-chunk-response":
|
|
597
|
+
return {
|
|
598
|
+
type: message.type,
|
|
599
|
+
messageId: message.messageId,
|
|
600
|
+
requestId: message.requestId,
|
|
601
|
+
hasBytes: message.bytes !== null,
|
|
602
|
+
...message.bytes !== null ? { chunkBytes: message.bytes } : {}
|
|
603
|
+
};
|
|
604
|
+
case "blob-chunk-push":
|
|
605
|
+
return {
|
|
606
|
+
type: message.type,
|
|
607
|
+
messageId: message.messageId,
|
|
608
|
+
hash: message.hash,
|
|
609
|
+
chunkBytes: message.bytes
|
|
610
|
+
};
|
|
535
611
|
}
|
|
536
612
|
}
|
|
537
613
|
function fromProtoEnvelope(envelope) {
|
|
@@ -564,7 +640,8 @@ function fromProtoEnvelope(envelope) {
|
|
|
564
640
|
selectedWireFormat: envelope.selectedWireFormat === "json" || envelope.selectedWireFormat === "protobuf" ? envelope.selectedWireFormat : void 0,
|
|
565
641
|
// Preserve the server's wall-clock time so clock-skew detection and
|
|
566
642
|
// automatic timestamp rebase work over the protobuf wire, not just JSON.
|
|
567
|
-
...envelope.serverTime !== void 0 ? { serverTime: envelope.serverTime } : {}
|
|
643
|
+
...envelope.serverTime !== void 0 ? { serverTime: envelope.serverTime } : {},
|
|
644
|
+
...envelope.blobStorageEnabled !== void 0 ? { blobStorageEnabled: envelope.blobStorageEnabled } : {}
|
|
568
645
|
};
|
|
569
646
|
case "operation-batch":
|
|
570
647
|
return {
|
|
@@ -589,6 +666,38 @@ function fromProtoEnvelope(envelope) {
|
|
|
589
666
|
message: envelope.errorMessage ?? "Unknown error",
|
|
590
667
|
retriable: envelope.retriable ?? false
|
|
591
668
|
};
|
|
669
|
+
case "operation-rejected":
|
|
670
|
+
return {
|
|
671
|
+
type: "operation-rejected",
|
|
672
|
+
messageId: envelope.messageId,
|
|
673
|
+
operationId: envelope.operationId ?? "",
|
|
674
|
+
collection: envelope.collection ?? "",
|
|
675
|
+
recordId: envelope.recordId ?? "",
|
|
676
|
+
code: envelope.errorCode ?? "REJECTED",
|
|
677
|
+
message: envelope.errorMessage ?? "",
|
|
678
|
+
retriable: envelope.retriable ?? false
|
|
679
|
+
};
|
|
680
|
+
case "blob-chunk-request":
|
|
681
|
+
return {
|
|
682
|
+
type: "blob-chunk-request",
|
|
683
|
+
messageId: envelope.messageId,
|
|
684
|
+
requestId: envelope.requestId ?? "",
|
|
685
|
+
hash: envelope.hash ?? ""
|
|
686
|
+
};
|
|
687
|
+
case "blob-chunk-response":
|
|
688
|
+
return {
|
|
689
|
+
type: "blob-chunk-response",
|
|
690
|
+
messageId: envelope.messageId,
|
|
691
|
+
requestId: envelope.requestId ?? "",
|
|
692
|
+
bytes: envelope.hasBytes ? envelope.chunkBytes ?? "" : null
|
|
693
|
+
};
|
|
694
|
+
case "blob-chunk-push":
|
|
695
|
+
return {
|
|
696
|
+
type: "blob-chunk-push",
|
|
697
|
+
messageId: envelope.messageId,
|
|
698
|
+
hash: envelope.hash ?? "",
|
|
699
|
+
bytes: envelope.chunkBytes ?? ""
|
|
700
|
+
};
|
|
592
701
|
default:
|
|
593
702
|
throw new SyncError("Failed to decode sync message: unknown protobuf type", {
|
|
594
703
|
type: envelope.type
|
|
@@ -734,6 +843,20 @@ function encodeEnvelope(envelope) {
|
|
|
734
843
|
}
|
|
735
844
|
if (envelope.retriable !== void 0) writer.uint32(144).bool(envelope.retriable);
|
|
736
845
|
if (envelope.serverTime !== void 0) writer.uint32(152).int64(envelope.serverTime);
|
|
846
|
+
if (envelope.requestId && envelope.requestId.length > 0)
|
|
847
|
+
writer.uint32(162).string(envelope.requestId);
|
|
848
|
+
if (envelope.hash && envelope.hash.length > 0) writer.uint32(170).string(envelope.hash);
|
|
849
|
+
if (envelope.chunkBytes && envelope.chunkBytes.length > 0)
|
|
850
|
+
writer.uint32(178).string(envelope.chunkBytes);
|
|
851
|
+
if (envelope.hasBytes !== void 0) writer.uint32(184).bool(envelope.hasBytes);
|
|
852
|
+
if (envelope.blobStorageEnabled !== void 0)
|
|
853
|
+
writer.uint32(200).bool(envelope.blobStorageEnabled);
|
|
854
|
+
if (envelope.operationId && envelope.operationId.length > 0)
|
|
855
|
+
writer.uint32(210).string(envelope.operationId);
|
|
856
|
+
if (envelope.collection && envelope.collection.length > 0)
|
|
857
|
+
writer.uint32(218).string(envelope.collection);
|
|
858
|
+
if (envelope.recordId && envelope.recordId.length > 0)
|
|
859
|
+
writer.uint32(226).string(envelope.recordId);
|
|
737
860
|
return writer.finish();
|
|
738
861
|
}
|
|
739
862
|
function decodeEnvelope(bytes) {
|
|
@@ -805,6 +928,30 @@ function decodeEnvelope(bytes) {
|
|
|
805
928
|
case 19:
|
|
806
929
|
envelope.serverTime = longToNumber(reader.int64());
|
|
807
930
|
break;
|
|
931
|
+
case 20:
|
|
932
|
+
envelope.requestId = reader.string();
|
|
933
|
+
break;
|
|
934
|
+
case 21:
|
|
935
|
+
envelope.hash = reader.string();
|
|
936
|
+
break;
|
|
937
|
+
case 22:
|
|
938
|
+
envelope.chunkBytes = reader.string();
|
|
939
|
+
break;
|
|
940
|
+
case 23:
|
|
941
|
+
envelope.hasBytes = reader.bool();
|
|
942
|
+
break;
|
|
943
|
+
case 25:
|
|
944
|
+
envelope.blobStorageEnabled = reader.bool();
|
|
945
|
+
break;
|
|
946
|
+
case 26:
|
|
947
|
+
envelope.operationId = reader.string();
|
|
948
|
+
break;
|
|
949
|
+
case 27:
|
|
950
|
+
envelope.collection = reader.string();
|
|
951
|
+
break;
|
|
952
|
+
case 28:
|
|
953
|
+
envelope.recordId = reader.string();
|
|
954
|
+
break;
|
|
808
955
|
default:
|
|
809
956
|
reader.skipType(tag & 7);
|
|
810
957
|
}
|
|
@@ -1612,6 +1759,95 @@ var AwarenessManager = class {
|
|
|
1612
1759
|
}
|
|
1613
1760
|
};
|
|
1614
1761
|
|
|
1762
|
+
// src/blob/blob-chunk-channel.ts
|
|
1763
|
+
import { generateUUIDv7 } from "@korajs/core";
|
|
1764
|
+
function encodeBlobChunkBytes(bytes) {
|
|
1765
|
+
let binary = "";
|
|
1766
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
1767
|
+
binary += String.fromCharCode(bytes[i]);
|
|
1768
|
+
}
|
|
1769
|
+
return btoa(binary);
|
|
1770
|
+
}
|
|
1771
|
+
function decodeBlobChunkBytes(encoded) {
|
|
1772
|
+
const binary = atob(encoded);
|
|
1773
|
+
const out = new Uint8Array(binary.length);
|
|
1774
|
+
for (let i = 0; i < binary.length; i++) {
|
|
1775
|
+
out[i] = binary.charCodeAt(i);
|
|
1776
|
+
}
|
|
1777
|
+
return out;
|
|
1778
|
+
}
|
|
1779
|
+
var BlobChunkChannel = class {
|
|
1780
|
+
onSend;
|
|
1781
|
+
handlers = /* @__PURE__ */ new Set();
|
|
1782
|
+
constructor(options) {
|
|
1783
|
+
this.onSend = options?.onSend ?? null;
|
|
1784
|
+
}
|
|
1785
|
+
/**
|
|
1786
|
+
* Send a blob-chunk message to the peer. Response bytes are base64-encoded
|
|
1787
|
+
* onto the wire here; callers pass a `Uint8Array`.
|
|
1788
|
+
*/
|
|
1789
|
+
send(message) {
|
|
1790
|
+
if (!this.onSend) {
|
|
1791
|
+
return;
|
|
1792
|
+
}
|
|
1793
|
+
if (message.type === "blob-chunk-request") {
|
|
1794
|
+
this.onSend({
|
|
1795
|
+
type: "blob-chunk-request",
|
|
1796
|
+
messageId: generateUUIDv7(),
|
|
1797
|
+
requestId: message.requestId,
|
|
1798
|
+
hash: message.hash
|
|
1799
|
+
});
|
|
1800
|
+
return;
|
|
1801
|
+
}
|
|
1802
|
+
if (message.type === "blob-chunk-push") {
|
|
1803
|
+
this.onSend({
|
|
1804
|
+
type: "blob-chunk-push",
|
|
1805
|
+
messageId: generateUUIDv7(),
|
|
1806
|
+
hash: message.hash,
|
|
1807
|
+
bytes: encodeBlobChunkBytes(message.bytes)
|
|
1808
|
+
});
|
|
1809
|
+
return;
|
|
1810
|
+
}
|
|
1811
|
+
this.onSend({
|
|
1812
|
+
type: "blob-chunk-response",
|
|
1813
|
+
messageId: generateUUIDv7(),
|
|
1814
|
+
requestId: message.requestId,
|
|
1815
|
+
bytes: message.bytes === null ? null : encodeBlobChunkBytes(message.bytes)
|
|
1816
|
+
});
|
|
1817
|
+
}
|
|
1818
|
+
/**
|
|
1819
|
+
* Register a handler for inbound blob-chunk messages. Multiple handlers are
|
|
1820
|
+
* allowed; each receives every delivered message.
|
|
1821
|
+
*/
|
|
1822
|
+
onMessage(handler) {
|
|
1823
|
+
this.handlers.add(handler);
|
|
1824
|
+
return () => {
|
|
1825
|
+
this.handlers.delete(handler);
|
|
1826
|
+
};
|
|
1827
|
+
}
|
|
1828
|
+
/**
|
|
1829
|
+
* Deliver an inbound wire message to registered handlers, decoding response
|
|
1830
|
+
* bytes from base64 back to a `Uint8Array` first.
|
|
1831
|
+
*/
|
|
1832
|
+
deliver(message) {
|
|
1833
|
+
if (this.handlers.size === 0) {
|
|
1834
|
+
return;
|
|
1835
|
+
}
|
|
1836
|
+
const decoded = message.type === "blob-chunk-request" ? { type: "blob-chunk-request", requestId: message.requestId, hash: message.hash } : message.type === "blob-chunk-push" ? {
|
|
1837
|
+
type: "blob-chunk-push",
|
|
1838
|
+
hash: message.hash,
|
|
1839
|
+
bytes: decodeBlobChunkBytes(message.bytes)
|
|
1840
|
+
} : {
|
|
1841
|
+
type: "blob-chunk-response",
|
|
1842
|
+
requestId: message.requestId,
|
|
1843
|
+
bytes: message.bytes === null ? null : decodeBlobChunkBytes(message.bytes)
|
|
1844
|
+
};
|
|
1845
|
+
for (const handler of this.handlers) {
|
|
1846
|
+
handler(decoded);
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
};
|
|
1850
|
+
|
|
1615
1851
|
// src/diagnostics/bandwidth-estimator.ts
|
|
1616
1852
|
var BandwidthEstimator = class {
|
|
1617
1853
|
maxSamples;
|
|
@@ -2055,7 +2291,7 @@ var SyncMetricsCollector = class {
|
|
|
2055
2291
|
};
|
|
2056
2292
|
|
|
2057
2293
|
// src/richtext/richtext-doc-channel.ts
|
|
2058
|
-
import { generateUUIDv7 } from "@korajs/core";
|
|
2294
|
+
import { generateUUIDv7 as generateUUIDv72 } from "@korajs/core";
|
|
2059
2295
|
|
|
2060
2296
|
// src/richtext/doc-channel-wire.ts
|
|
2061
2297
|
function encodeYjsUpdate(update) {
|
|
@@ -2134,7 +2370,7 @@ var RichtextDocChannel = class {
|
|
|
2134
2370
|
}
|
|
2135
2371
|
this.onSend({
|
|
2136
2372
|
type: "yjs-doc-update",
|
|
2137
|
-
messageId:
|
|
2373
|
+
messageId: generateUUIDv72(),
|
|
2138
2374
|
collection,
|
|
2139
2375
|
recordId,
|
|
2140
2376
|
field,
|
|
@@ -2304,6 +2540,37 @@ var OutboundQueue = class {
|
|
|
2304
2540
|
get isInitialized() {
|
|
2305
2541
|
return this.initialized;
|
|
2306
2542
|
}
|
|
2543
|
+
/**
|
|
2544
|
+
* Remove a single operation the server permanently rejected — from the queue,
|
|
2545
|
+
* from any in-flight batch, and from durable storage — so a later batch ack or
|
|
2546
|
+
* a reconnect `returnBatch` can never resend or resurrect it. Returns the
|
|
2547
|
+
* removed operation so the caller can record it in a durable rejected store,
|
|
2548
|
+
* or null if it was not present (already acknowledged and removed).
|
|
2549
|
+
*/
|
|
2550
|
+
async reject(opId) {
|
|
2551
|
+
let removed = null;
|
|
2552
|
+
for (const [batchId, ops] of this.inFlight) {
|
|
2553
|
+
const idx = ops.findIndex((op) => op.id === opId);
|
|
2554
|
+
if (idx !== -1) {
|
|
2555
|
+
removed = ops[idx] ?? null;
|
|
2556
|
+
ops.splice(idx, 1);
|
|
2557
|
+
if (ops.length === 0) {
|
|
2558
|
+
this.inFlight.delete(batchId);
|
|
2559
|
+
}
|
|
2560
|
+
break;
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
if (!removed) {
|
|
2564
|
+
const idx = this.queue.findIndex((op) => op.id === opId);
|
|
2565
|
+
if (idx !== -1) {
|
|
2566
|
+
removed = this.queue[idx] ?? null;
|
|
2567
|
+
this.queue.splice(idx, 1);
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
this.seen.delete(opId);
|
|
2571
|
+
await this.storage.dequeue([opId]);
|
|
2572
|
+
return removed;
|
|
2573
|
+
}
|
|
2307
2574
|
/**
|
|
2308
2575
|
* Remove operations by id from queue and persistent storage.
|
|
2309
2576
|
* Used when ops were already sent during handshake delta exchange.
|
|
@@ -2343,10 +2610,12 @@ var SyncEngine = class {
|
|
|
2343
2610
|
serializer;
|
|
2344
2611
|
emitter;
|
|
2345
2612
|
outboundQueue;
|
|
2613
|
+
rejectedStorage;
|
|
2346
2614
|
batchSize;
|
|
2347
2615
|
encryptor;
|
|
2348
2616
|
awarenessManager;
|
|
2349
2617
|
richtextDocChannel;
|
|
2618
|
+
blobChunkChannel;
|
|
2350
2619
|
metricsCollector;
|
|
2351
2620
|
syncState;
|
|
2352
2621
|
remoteVector = /* @__PURE__ */ new Map();
|
|
@@ -2361,6 +2630,7 @@ var SyncEngine = class {
|
|
|
2361
2630
|
schemaBlocked = false;
|
|
2362
2631
|
clockBlocked = false;
|
|
2363
2632
|
clockSkewMs = null;
|
|
2633
|
+
blobStorageEnabled = false;
|
|
2364
2634
|
// Track delta exchange state
|
|
2365
2635
|
deltaBatchesReceived = 0;
|
|
2366
2636
|
deltaReceiveComplete = false;
|
|
@@ -2393,6 +2663,7 @@ var SyncEngine = class {
|
|
|
2393
2663
|
this.activeScope = options.config.scopeMap;
|
|
2394
2664
|
const queueStorage = options.queueStorage ?? new MemoryQueueStorage();
|
|
2395
2665
|
this.outboundQueue = new OutboundQueue(queueStorage);
|
|
2666
|
+
this.rejectedStorage = options.rejectedStorage ?? new MemoryRejectedOperationStorage();
|
|
2396
2667
|
this.metricsCollector = new SyncMetricsCollector(options.metricsConfig);
|
|
2397
2668
|
if (this.emitter) {
|
|
2398
2669
|
this.metricsCollector.attachEmitter(this.emitter);
|
|
@@ -2409,6 +2680,14 @@ var SyncEngine = class {
|
|
|
2409
2680
|
this.transport.send(message);
|
|
2410
2681
|
}
|
|
2411
2682
|
});
|
|
2683
|
+
this.blobChunkChannel = new BlobChunkChannel({
|
|
2684
|
+
onSend: (message) => {
|
|
2685
|
+
if (this.state !== "streaming") {
|
|
2686
|
+
return;
|
|
2687
|
+
}
|
|
2688
|
+
this.transport.send(message);
|
|
2689
|
+
}
|
|
2690
|
+
});
|
|
2412
2691
|
this.awarenessManager.onSend((message) => {
|
|
2413
2692
|
if (this.state !== "streaming") return;
|
|
2414
2693
|
const wireMessage = {
|
|
@@ -2722,6 +3001,31 @@ var SyncEngine = class {
|
|
|
2722
3001
|
getRichtextDocChannel() {
|
|
2723
3002
|
return this.richtextDocChannel;
|
|
2724
3003
|
}
|
|
3004
|
+
/**
|
|
3005
|
+
* Side channel for out-of-band blob chunk transfer over the sync connection.
|
|
3006
|
+
* The app layer bridges this to a `ChunkMessagePort` to pull/serve blob bytes.
|
|
3007
|
+
*/
|
|
3008
|
+
getBlobChunkChannel() {
|
|
3009
|
+
return this.blobChunkChannel;
|
|
3010
|
+
}
|
|
3011
|
+
/**
|
|
3012
|
+
* Whether the connected server persists blob bytes centrally (learned from the
|
|
3013
|
+
* handshake response). When true, the app uploads the bytes behind `blob`
|
|
3014
|
+
* fields so they stay available after the authoring device goes offline.
|
|
3015
|
+
*/
|
|
3016
|
+
isBlobStorageEnabled() {
|
|
3017
|
+
return this.blobStorageEnabled;
|
|
3018
|
+
}
|
|
3019
|
+
/**
|
|
3020
|
+
* Upload a blob chunk (or manifest) to the server for central persistence.
|
|
3021
|
+
* A no-op unless connected and the server advertised blob storage.
|
|
3022
|
+
*/
|
|
3023
|
+
uploadBlobChunk(hash, bytes) {
|
|
3024
|
+
if (this.state !== "streaming" || !this.blobStorageEnabled) {
|
|
3025
|
+
return;
|
|
3026
|
+
}
|
|
3027
|
+
this.blobChunkChannel.send({ type: "blob-chunk-push", hash, bytes });
|
|
3028
|
+
}
|
|
2725
3029
|
// --- Private methods ---
|
|
2726
3030
|
messageChain = Promise.resolve();
|
|
2727
3031
|
enqueueMessage(message) {
|
|
@@ -2741,12 +3045,20 @@ var SyncEngine = class {
|
|
|
2741
3045
|
case "error":
|
|
2742
3046
|
this.handleError(message);
|
|
2743
3047
|
break;
|
|
3048
|
+
case "operation-rejected":
|
|
3049
|
+
await this.handleOperationRejected(message);
|
|
3050
|
+
break;
|
|
2744
3051
|
case "awareness-update":
|
|
2745
3052
|
this.handleAwarenessUpdate(message);
|
|
2746
3053
|
break;
|
|
2747
3054
|
case "yjs-doc-update":
|
|
2748
3055
|
this.richtextDocChannel.deliver(message);
|
|
2749
3056
|
break;
|
|
3057
|
+
case "blob-chunk-request":
|
|
3058
|
+
case "blob-chunk-response":
|
|
3059
|
+
case "blob-chunk-push":
|
|
3060
|
+
this.blobChunkChannel.deliver(message);
|
|
3061
|
+
break;
|
|
2750
3062
|
}
|
|
2751
3063
|
}
|
|
2752
3064
|
handleMessageFailure(error) {
|
|
@@ -2779,6 +3091,7 @@ var SyncEngine = class {
|
|
|
2779
3091
|
}
|
|
2780
3092
|
async handleHandshakeResponse(msg) {
|
|
2781
3093
|
if (this.state !== "handshaking") return;
|
|
3094
|
+
this.blobStorageEnabled = msg.blobStorageEnabled === true;
|
|
2782
3095
|
if (typeof msg.serverTime === "number") {
|
|
2783
3096
|
this.evaluateClockSkew(msg.serverTime);
|
|
2784
3097
|
if (this.clockBlocked) {
|
|
@@ -3072,6 +3385,50 @@ var SyncEngine = class {
|
|
|
3072
3385
|
this.emitter?.emit({ type: "sync:disconnected", reason: msg.message });
|
|
3073
3386
|
this.transitionTo("disconnected");
|
|
3074
3387
|
}
|
|
3388
|
+
/**
|
|
3389
|
+
* Handle a per-operation rejection: divert the op out of the pending outbound
|
|
3390
|
+
* queue (so it is never retried or resurrected on reconnect), record it in the
|
|
3391
|
+
* durable rejected store (so it is kept and explainable), and emit an event so
|
|
3392
|
+
* the app can reconcile. Unlike {@link handleError}, this is a normal per-op
|
|
3393
|
+
* signal, so the connection stays up.
|
|
3394
|
+
*/
|
|
3395
|
+
async handleOperationRejected(msg) {
|
|
3396
|
+
await this.outboundQueue.reject(msg.operationId);
|
|
3397
|
+
await this.rejectedStorage.record({
|
|
3398
|
+
operationId: msg.operationId,
|
|
3399
|
+
collection: msg.collection,
|
|
3400
|
+
recordId: msg.recordId,
|
|
3401
|
+
code: msg.code,
|
|
3402
|
+
message: msg.message,
|
|
3403
|
+
retriable: msg.retriable,
|
|
3404
|
+
rejectedAt: Date.now()
|
|
3405
|
+
});
|
|
3406
|
+
await this.refreshPendingCount();
|
|
3407
|
+
this.emitter?.emit({
|
|
3408
|
+
type: "sync:operation-rejected",
|
|
3409
|
+
operationId: msg.operationId,
|
|
3410
|
+
collection: msg.collection,
|
|
3411
|
+
recordId: msg.recordId,
|
|
3412
|
+
code: msg.code,
|
|
3413
|
+
message: msg.message,
|
|
3414
|
+
retriable: msg.retriable
|
|
3415
|
+
});
|
|
3416
|
+
}
|
|
3417
|
+
/**
|
|
3418
|
+
* Every operation the server rejected that has not yet been reconciled. The app
|
|
3419
|
+
* uses this (alongside the `sync:operation-rejected` event) to surface failed
|
|
3420
|
+
* submissions and decide whether to roll back or resubmit.
|
|
3421
|
+
*/
|
|
3422
|
+
getRejectedOperations() {
|
|
3423
|
+
return this.rejectedStorage.list();
|
|
3424
|
+
}
|
|
3425
|
+
/**
|
|
3426
|
+
* Forget rejected operations by id once the app has reconciled them (rolled the
|
|
3427
|
+
* optimistic write back or resubmitted a corrected op).
|
|
3428
|
+
*/
|
|
3429
|
+
clearRejectedOperations(operationIds) {
|
|
3430
|
+
return this.rejectedStorage.remove(operationIds);
|
|
3431
|
+
}
|
|
3075
3432
|
async checkDeltaComplete() {
|
|
3076
3433
|
if (!this.deltaSendComplete || !this.deltaReceiveComplete) {
|
|
3077
3434
|
return;
|
|
@@ -4126,6 +4483,7 @@ function subscribeRemoteAwarenessStates(awareness, listener) {
|
|
|
4126
4483
|
}
|
|
4127
4484
|
export {
|
|
4128
4485
|
AwarenessManager,
|
|
4486
|
+
BlobChunkChannel,
|
|
4129
4487
|
ChaosTransport,
|
|
4130
4488
|
ConnectionMonitor,
|
|
4131
4489
|
DEFAULT_RICHTEXT_DOC_CHANNEL_THRESHOLD,
|
|
@@ -4135,6 +4493,7 @@ export {
|
|
|
4135
4493
|
InvalidScopeError,
|
|
4136
4494
|
JsonMessageSerializer,
|
|
4137
4495
|
KeyDerivationError,
|
|
4496
|
+
MemoryRejectedOperationStorage,
|
|
4138
4497
|
NegotiatedMessageSerializer,
|
|
4139
4498
|
OFFLINE_SYNC_STATUS,
|
|
4140
4499
|
OutboundQueue,
|
|
@@ -4150,11 +4509,13 @@ export {
|
|
|
4150
4509
|
WebSocketTransport,
|
|
4151
4510
|
createDeltaCursorFromBatch,
|
|
4152
4511
|
createSyncStatusController,
|
|
4512
|
+
decodeBlobChunkBytes,
|
|
4153
4513
|
decodeDeltaCursor,
|
|
4154
4514
|
decodeYjsUpdate,
|
|
4155
4515
|
dedupeQuerySubsets,
|
|
4156
4516
|
deriveKey,
|
|
4157
4517
|
deriveVersionedKey,
|
|
4518
|
+
encodeBlobChunkBytes,
|
|
4158
4519
|
encodeDeltaCursor,
|
|
4159
4520
|
encodeYjsUpdate,
|
|
4160
4521
|
filterOperationsByScope,
|
|
@@ -4162,12 +4523,16 @@ export {
|
|
|
4162
4523
|
getRemoteAwarenessStates,
|
|
4163
4524
|
isAcknowledgmentMessage,
|
|
4164
4525
|
isAwarenessUpdateMessage,
|
|
4526
|
+
isBlobChunkPushMessage,
|
|
4527
|
+
isBlobChunkRequestMessage,
|
|
4528
|
+
isBlobChunkResponseMessage,
|
|
4165
4529
|
isClientSchemaVersionSupported,
|
|
4166
4530
|
isEncryptedPayload,
|
|
4167
4531
|
isErrorMessage,
|
|
4168
4532
|
isHandshakeMessage,
|
|
4169
4533
|
isHandshakeResponseMessage,
|
|
4170
4534
|
isOperationBatchMessage,
|
|
4535
|
+
isOperationRejectedMessage,
|
|
4171
4536
|
isSchemaMismatchReject,
|
|
4172
4537
|
isSyncMessage,
|
|
4173
4538
|
isYjsDocUpdateMessage,
|