@korajs/sync 0.6.1 → 1.0.0-beta.1
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/LICENSE +21 -0
- package/dist/index.cjs +444 -34
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +154 -7
- package/dist/index.d.ts +154 -7
- package/dist/index.js +423 -19
- package/dist/index.js.map +1 -1
- package/dist/internal.d.cts +1 -1
- package/dist/internal.d.ts +1 -1
- package/dist/{transport-BrdBj7rH.d.cts → transport-CZLMdZ0k.d.cts} +70 -3
- package/dist/{transport-BrdBj7rH.d.ts → transport-CZLMdZ0k.d.ts} +70 -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,
|
|
@@ -55,11 +56,13 @@ __export(index_exports, {
|
|
|
55
56
|
WebSocketTransport: () => WebSocketTransport,
|
|
56
57
|
createDeltaCursorFromBatch: () => createDeltaCursorFromBatch,
|
|
57
58
|
createSyncStatusController: () => createSyncStatusController,
|
|
59
|
+
decodeBlobChunkBytes: () => decodeBlobChunkBytes,
|
|
58
60
|
decodeDeltaCursor: () => decodeDeltaCursor,
|
|
59
61
|
decodeYjsUpdate: () => decodeYjsUpdate,
|
|
60
62
|
dedupeQuerySubsets: () => dedupeQuerySubsets,
|
|
61
63
|
deriveKey: () => deriveKey,
|
|
62
64
|
deriveVersionedKey: () => deriveVersionedKey,
|
|
65
|
+
encodeBlobChunkBytes: () => encodeBlobChunkBytes,
|
|
63
66
|
encodeDeltaCursor: () => encodeDeltaCursor,
|
|
64
67
|
encodeYjsUpdate: () => encodeYjsUpdate,
|
|
65
68
|
filterOperationsByScope: () => filterOperationsByScope,
|
|
@@ -67,6 +70,9 @@ __export(index_exports, {
|
|
|
67
70
|
getRemoteAwarenessStates: () => getRemoteAwarenessStates,
|
|
68
71
|
isAcknowledgmentMessage: () => isAcknowledgmentMessage,
|
|
69
72
|
isAwarenessUpdateMessage: () => isAwarenessUpdateMessage,
|
|
73
|
+
isBlobChunkPushMessage: () => isBlobChunkPushMessage,
|
|
74
|
+
isBlobChunkRequestMessage: () => isBlobChunkRequestMessage,
|
|
75
|
+
isBlobChunkResponseMessage: () => isBlobChunkResponseMessage,
|
|
70
76
|
isClientSchemaVersionSupported: () => isClientSchemaVersionSupported,
|
|
71
77
|
isEncryptedPayload: () => isEncryptedPayload,
|
|
72
78
|
isErrorMessage: () => isErrorMessage,
|
|
@@ -101,6 +107,7 @@ var SYNC_STATUSES = [
|
|
|
101
107
|
"syncing",
|
|
102
108
|
"synced",
|
|
103
109
|
"offline",
|
|
110
|
+
"clock-error",
|
|
104
111
|
"error",
|
|
105
112
|
"schema-mismatch"
|
|
106
113
|
];
|
|
@@ -305,6 +312,12 @@ function isSyncMessage(value) {
|
|
|
305
312
|
return isAwarenessUpdateMessage(value);
|
|
306
313
|
case "yjs-doc-update":
|
|
307
314
|
return isYjsDocUpdateMessage(value);
|
|
315
|
+
case "blob-chunk-request":
|
|
316
|
+
return isBlobChunkRequestMessage(value);
|
|
317
|
+
case "blob-chunk-response":
|
|
318
|
+
return isBlobChunkResponseMessage(value);
|
|
319
|
+
case "blob-chunk-push":
|
|
320
|
+
return isBlobChunkPushMessage(value);
|
|
308
321
|
default:
|
|
309
322
|
return false;
|
|
310
323
|
}
|
|
@@ -344,6 +357,21 @@ function isYjsDocUpdateMessage(value) {
|
|
|
344
357
|
const msg = value;
|
|
345
358
|
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";
|
|
346
359
|
}
|
|
360
|
+
function isBlobChunkRequestMessage(value) {
|
|
361
|
+
if (typeof value !== "object" || value === null) return false;
|
|
362
|
+
const msg = value;
|
|
363
|
+
return msg.type === "blob-chunk-request" && typeof msg.messageId === "string" && typeof msg.requestId === "string" && typeof msg.hash === "string";
|
|
364
|
+
}
|
|
365
|
+
function isBlobChunkResponseMessage(value) {
|
|
366
|
+
if (typeof value !== "object" || value === null) return false;
|
|
367
|
+
const msg = value;
|
|
368
|
+
return msg.type === "blob-chunk-response" && typeof msg.messageId === "string" && typeof msg.requestId === "string" && (typeof msg.bytes === "string" || msg.bytes === null);
|
|
369
|
+
}
|
|
370
|
+
function isBlobChunkPushMessage(value) {
|
|
371
|
+
if (typeof value !== "object" || value === null) return false;
|
|
372
|
+
const msg = value;
|
|
373
|
+
return msg.type === "blob-chunk-push" && typeof msg.messageId === "string" && typeof msg.hash === "string" && typeof msg.bytes === "string";
|
|
374
|
+
}
|
|
347
375
|
|
|
348
376
|
// src/protocol/serializer.ts
|
|
349
377
|
var import_core2 = require("@korajs/core");
|
|
@@ -583,7 +611,9 @@ function toProtoEnvelope(message) {
|
|
|
583
611
|
schemaVersion: message.schemaVersion,
|
|
584
612
|
accepted: message.accepted,
|
|
585
613
|
rejectReason: message.rejectReason,
|
|
586
|
-
selectedWireFormat: message.selectedWireFormat
|
|
614
|
+
selectedWireFormat: message.selectedWireFormat,
|
|
615
|
+
serverTime: message.serverTime,
|
|
616
|
+
blobStorageEnabled: message.blobStorageEnabled
|
|
587
617
|
};
|
|
588
618
|
case "operation-batch":
|
|
589
619
|
return {
|
|
@@ -614,6 +644,28 @@ function toProtoEnvelope(message) {
|
|
|
614
644
|
type: message.type,
|
|
615
645
|
messageId: message.messageId
|
|
616
646
|
};
|
|
647
|
+
case "blob-chunk-request":
|
|
648
|
+
return {
|
|
649
|
+
type: message.type,
|
|
650
|
+
messageId: message.messageId,
|
|
651
|
+
requestId: message.requestId,
|
|
652
|
+
hash: message.hash
|
|
653
|
+
};
|
|
654
|
+
case "blob-chunk-response":
|
|
655
|
+
return {
|
|
656
|
+
type: message.type,
|
|
657
|
+
messageId: message.messageId,
|
|
658
|
+
requestId: message.requestId,
|
|
659
|
+
hasBytes: message.bytes !== null,
|
|
660
|
+
...message.bytes !== null ? { chunkBytes: message.bytes } : {}
|
|
661
|
+
};
|
|
662
|
+
case "blob-chunk-push":
|
|
663
|
+
return {
|
|
664
|
+
type: message.type,
|
|
665
|
+
messageId: message.messageId,
|
|
666
|
+
hash: message.hash,
|
|
667
|
+
chunkBytes: message.bytes
|
|
668
|
+
};
|
|
617
669
|
}
|
|
618
670
|
}
|
|
619
671
|
function fromProtoEnvelope(envelope) {
|
|
@@ -643,7 +695,11 @@ function fromProtoEnvelope(envelope) {
|
|
|
643
695
|
schemaVersion: envelope.schemaVersion ?? 0,
|
|
644
696
|
accepted: envelope.accepted ?? false,
|
|
645
697
|
rejectReason: envelope.rejectReason,
|
|
646
|
-
selectedWireFormat: envelope.selectedWireFormat === "json" || envelope.selectedWireFormat === "protobuf" ? envelope.selectedWireFormat : void 0
|
|
698
|
+
selectedWireFormat: envelope.selectedWireFormat === "json" || envelope.selectedWireFormat === "protobuf" ? envelope.selectedWireFormat : void 0,
|
|
699
|
+
// Preserve the server's wall-clock time so clock-skew detection and
|
|
700
|
+
// automatic timestamp rebase work over the protobuf wire, not just JSON.
|
|
701
|
+
...envelope.serverTime !== void 0 ? { serverTime: envelope.serverTime } : {},
|
|
702
|
+
...envelope.blobStorageEnabled !== void 0 ? { blobStorageEnabled: envelope.blobStorageEnabled } : {}
|
|
647
703
|
};
|
|
648
704
|
case "operation-batch":
|
|
649
705
|
return {
|
|
@@ -668,6 +724,27 @@ function fromProtoEnvelope(envelope) {
|
|
|
668
724
|
message: envelope.errorMessage ?? "Unknown error",
|
|
669
725
|
retriable: envelope.retriable ?? false
|
|
670
726
|
};
|
|
727
|
+
case "blob-chunk-request":
|
|
728
|
+
return {
|
|
729
|
+
type: "blob-chunk-request",
|
|
730
|
+
messageId: envelope.messageId,
|
|
731
|
+
requestId: envelope.requestId ?? "",
|
|
732
|
+
hash: envelope.hash ?? ""
|
|
733
|
+
};
|
|
734
|
+
case "blob-chunk-response":
|
|
735
|
+
return {
|
|
736
|
+
type: "blob-chunk-response",
|
|
737
|
+
messageId: envelope.messageId,
|
|
738
|
+
requestId: envelope.requestId ?? "",
|
|
739
|
+
bytes: envelope.hasBytes ? envelope.chunkBytes ?? "" : null
|
|
740
|
+
};
|
|
741
|
+
case "blob-chunk-push":
|
|
742
|
+
return {
|
|
743
|
+
type: "blob-chunk-push",
|
|
744
|
+
messageId: envelope.messageId,
|
|
745
|
+
hash: envelope.hash ?? "",
|
|
746
|
+
bytes: envelope.chunkBytes ?? ""
|
|
747
|
+
};
|
|
671
748
|
default:
|
|
672
749
|
throw new import_core2.SyncError("Failed to decode sync message: unknown protobuf type", {
|
|
673
750
|
type: envelope.type
|
|
@@ -812,6 +889,15 @@ function encodeEnvelope(envelope) {
|
|
|
812
889
|
writer.uint32(138).string(envelope.errorMessage);
|
|
813
890
|
}
|
|
814
891
|
if (envelope.retriable !== void 0) writer.uint32(144).bool(envelope.retriable);
|
|
892
|
+
if (envelope.serverTime !== void 0) writer.uint32(152).int64(envelope.serverTime);
|
|
893
|
+
if (envelope.requestId && envelope.requestId.length > 0)
|
|
894
|
+
writer.uint32(162).string(envelope.requestId);
|
|
895
|
+
if (envelope.hash && envelope.hash.length > 0) writer.uint32(170).string(envelope.hash);
|
|
896
|
+
if (envelope.chunkBytes && envelope.chunkBytes.length > 0)
|
|
897
|
+
writer.uint32(178).string(envelope.chunkBytes);
|
|
898
|
+
if (envelope.hasBytes !== void 0) writer.uint32(184).bool(envelope.hasBytes);
|
|
899
|
+
if (envelope.blobStorageEnabled !== void 0)
|
|
900
|
+
writer.uint32(200).bool(envelope.blobStorageEnabled);
|
|
815
901
|
return writer.finish();
|
|
816
902
|
}
|
|
817
903
|
function decodeEnvelope(bytes) {
|
|
@@ -880,6 +966,24 @@ function decodeEnvelope(bytes) {
|
|
|
880
966
|
case 18:
|
|
881
967
|
envelope.retriable = reader.bool();
|
|
882
968
|
break;
|
|
969
|
+
case 19:
|
|
970
|
+
envelope.serverTime = longToNumber(reader.int64());
|
|
971
|
+
break;
|
|
972
|
+
case 20:
|
|
973
|
+
envelope.requestId = reader.string();
|
|
974
|
+
break;
|
|
975
|
+
case 21:
|
|
976
|
+
envelope.hash = reader.string();
|
|
977
|
+
break;
|
|
978
|
+
case 22:
|
|
979
|
+
envelope.chunkBytes = reader.string();
|
|
980
|
+
break;
|
|
981
|
+
case 23:
|
|
982
|
+
envelope.hasBytes = reader.bool();
|
|
983
|
+
break;
|
|
984
|
+
case 25:
|
|
985
|
+
envelope.blobStorageEnabled = reader.bool();
|
|
986
|
+
break;
|
|
883
987
|
default:
|
|
884
988
|
reader.skipType(tag & 7);
|
|
885
989
|
}
|
|
@@ -1381,7 +1485,7 @@ var ChaosTransport = class {
|
|
|
1381
1485
|
random;
|
|
1382
1486
|
messageHandler = null;
|
|
1383
1487
|
reorderBuffer = [];
|
|
1384
|
-
|
|
1488
|
+
pending = [];
|
|
1385
1489
|
constructor(inner, config) {
|
|
1386
1490
|
this.inner = inner;
|
|
1387
1491
|
this.dropRate = config?.dropRate ?? 0;
|
|
@@ -1396,10 +1500,14 @@ var ChaosTransport = class {
|
|
|
1396
1500
|
}
|
|
1397
1501
|
async disconnect() {
|
|
1398
1502
|
this.flushReorderBuffer();
|
|
1399
|
-
|
|
1503
|
+
const flushed = this.pending;
|
|
1504
|
+
this.pending = [];
|
|
1505
|
+
for (const { timer } of flushed) {
|
|
1400
1506
|
clearTimeout(timer);
|
|
1401
1507
|
}
|
|
1402
|
-
|
|
1508
|
+
for (const { message } of flushed) {
|
|
1509
|
+
this.deliverIncoming(message);
|
|
1510
|
+
}
|
|
1403
1511
|
return this.inner.disconnect();
|
|
1404
1512
|
}
|
|
1405
1513
|
send(message) {
|
|
@@ -1436,9 +1544,10 @@ var ChaosTransport = class {
|
|
|
1436
1544
|
if (this.maxLatency > 0) {
|
|
1437
1545
|
const delay = Math.floor(this.random() * this.maxLatency);
|
|
1438
1546
|
const timer = setTimeout(() => {
|
|
1547
|
+
this.pending = this.pending.filter((p) => p.timer !== timer);
|
|
1439
1548
|
this.deliverIncoming(message);
|
|
1440
1549
|
}, delay);
|
|
1441
|
-
this.
|
|
1550
|
+
this.pending.push({ timer, message });
|
|
1442
1551
|
return;
|
|
1443
1552
|
}
|
|
1444
1553
|
this.deliverIncoming(message);
|
|
@@ -1466,7 +1575,7 @@ var ChaosTransport = class {
|
|
|
1466
1575
|
};
|
|
1467
1576
|
|
|
1468
1577
|
// src/engine/sync-engine.ts
|
|
1469
|
-
var
|
|
1578
|
+
var import_core7 = require("@korajs/core");
|
|
1470
1579
|
var import_internal2 = require("@korajs/core/internal");
|
|
1471
1580
|
|
|
1472
1581
|
// src/awareness/awareness-manager.ts
|
|
@@ -1675,6 +1784,95 @@ var AwarenessManager = class {
|
|
|
1675
1784
|
}
|
|
1676
1785
|
};
|
|
1677
1786
|
|
|
1787
|
+
// src/blob/blob-chunk-channel.ts
|
|
1788
|
+
var import_core5 = require("@korajs/core");
|
|
1789
|
+
function encodeBlobChunkBytes(bytes) {
|
|
1790
|
+
let binary = "";
|
|
1791
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
1792
|
+
binary += String.fromCharCode(bytes[i]);
|
|
1793
|
+
}
|
|
1794
|
+
return btoa(binary);
|
|
1795
|
+
}
|
|
1796
|
+
function decodeBlobChunkBytes(encoded) {
|
|
1797
|
+
const binary = atob(encoded);
|
|
1798
|
+
const out = new Uint8Array(binary.length);
|
|
1799
|
+
for (let i = 0; i < binary.length; i++) {
|
|
1800
|
+
out[i] = binary.charCodeAt(i);
|
|
1801
|
+
}
|
|
1802
|
+
return out;
|
|
1803
|
+
}
|
|
1804
|
+
var BlobChunkChannel = class {
|
|
1805
|
+
onSend;
|
|
1806
|
+
handlers = /* @__PURE__ */ new Set();
|
|
1807
|
+
constructor(options) {
|
|
1808
|
+
this.onSend = options?.onSend ?? null;
|
|
1809
|
+
}
|
|
1810
|
+
/**
|
|
1811
|
+
* Send a blob-chunk message to the peer. Response bytes are base64-encoded
|
|
1812
|
+
* onto the wire here; callers pass a `Uint8Array`.
|
|
1813
|
+
*/
|
|
1814
|
+
send(message) {
|
|
1815
|
+
if (!this.onSend) {
|
|
1816
|
+
return;
|
|
1817
|
+
}
|
|
1818
|
+
if (message.type === "blob-chunk-request") {
|
|
1819
|
+
this.onSend({
|
|
1820
|
+
type: "blob-chunk-request",
|
|
1821
|
+
messageId: (0, import_core5.generateUUIDv7)(),
|
|
1822
|
+
requestId: message.requestId,
|
|
1823
|
+
hash: message.hash
|
|
1824
|
+
});
|
|
1825
|
+
return;
|
|
1826
|
+
}
|
|
1827
|
+
if (message.type === "blob-chunk-push") {
|
|
1828
|
+
this.onSend({
|
|
1829
|
+
type: "blob-chunk-push",
|
|
1830
|
+
messageId: (0, import_core5.generateUUIDv7)(),
|
|
1831
|
+
hash: message.hash,
|
|
1832
|
+
bytes: encodeBlobChunkBytes(message.bytes)
|
|
1833
|
+
});
|
|
1834
|
+
return;
|
|
1835
|
+
}
|
|
1836
|
+
this.onSend({
|
|
1837
|
+
type: "blob-chunk-response",
|
|
1838
|
+
messageId: (0, import_core5.generateUUIDv7)(),
|
|
1839
|
+
requestId: message.requestId,
|
|
1840
|
+
bytes: message.bytes === null ? null : encodeBlobChunkBytes(message.bytes)
|
|
1841
|
+
});
|
|
1842
|
+
}
|
|
1843
|
+
/**
|
|
1844
|
+
* Register a handler for inbound blob-chunk messages. Multiple handlers are
|
|
1845
|
+
* allowed; each receives every delivered message.
|
|
1846
|
+
*/
|
|
1847
|
+
onMessage(handler) {
|
|
1848
|
+
this.handlers.add(handler);
|
|
1849
|
+
return () => {
|
|
1850
|
+
this.handlers.delete(handler);
|
|
1851
|
+
};
|
|
1852
|
+
}
|
|
1853
|
+
/**
|
|
1854
|
+
* Deliver an inbound wire message to registered handlers, decoding response
|
|
1855
|
+
* bytes from base64 back to a `Uint8Array` first.
|
|
1856
|
+
*/
|
|
1857
|
+
deliver(message) {
|
|
1858
|
+
if (this.handlers.size === 0) {
|
|
1859
|
+
return;
|
|
1860
|
+
}
|
|
1861
|
+
const decoded = message.type === "blob-chunk-request" ? { type: "blob-chunk-request", requestId: message.requestId, hash: message.hash } : message.type === "blob-chunk-push" ? {
|
|
1862
|
+
type: "blob-chunk-push",
|
|
1863
|
+
hash: message.hash,
|
|
1864
|
+
bytes: decodeBlobChunkBytes(message.bytes)
|
|
1865
|
+
} : {
|
|
1866
|
+
type: "blob-chunk-response",
|
|
1867
|
+
requestId: message.requestId,
|
|
1868
|
+
bytes: message.bytes === null ? null : decodeBlobChunkBytes(message.bytes)
|
|
1869
|
+
};
|
|
1870
|
+
for (const handler of this.handlers) {
|
|
1871
|
+
handler(decoded);
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
};
|
|
1875
|
+
|
|
1678
1876
|
// src/diagnostics/bandwidth-estimator.ts
|
|
1679
1877
|
var BandwidthEstimator = class {
|
|
1680
1878
|
maxSamples;
|
|
@@ -2118,7 +2316,7 @@ var SyncMetricsCollector = class {
|
|
|
2118
2316
|
};
|
|
2119
2317
|
|
|
2120
2318
|
// src/richtext/richtext-doc-channel.ts
|
|
2121
|
-
var
|
|
2319
|
+
var import_core6 = require("@korajs/core");
|
|
2122
2320
|
|
|
2123
2321
|
// src/richtext/doc-channel-wire.ts
|
|
2124
2322
|
function encodeYjsUpdate(update) {
|
|
@@ -2197,7 +2395,7 @@ var RichtextDocChannel = class {
|
|
|
2197
2395
|
}
|
|
2198
2396
|
this.onSend({
|
|
2199
2397
|
type: "yjs-doc-update",
|
|
2200
|
-
messageId: (0,
|
|
2398
|
+
messageId: (0, import_core6.generateUUIDv7)(),
|
|
2201
2399
|
collection,
|
|
2202
2400
|
recordId,
|
|
2203
2401
|
field,
|
|
@@ -2345,6 +2543,41 @@ var OutboundQueue = class {
|
|
|
2345
2543
|
peek(count) {
|
|
2346
2544
|
return this.queue.slice(0, count);
|
|
2347
2545
|
}
|
|
2546
|
+
/**
|
|
2547
|
+
* All queued operations (not counting in-flight), in causal order.
|
|
2548
|
+
*/
|
|
2549
|
+
getAll() {
|
|
2550
|
+
return [...this.queue];
|
|
2551
|
+
}
|
|
2552
|
+
/**
|
|
2553
|
+
* Atomically replace the entire queue with a new set of operations.
|
|
2554
|
+
*
|
|
2555
|
+
* Used after a timestamp rebase rewrote the queued operations under new
|
|
2556
|
+
* content-addressed ids: the old entries must vanish from memory AND from
|
|
2557
|
+
* persistent storage in one step, or a page refresh could resurrect
|
|
2558
|
+
* stale-stamped ops the server would reject. Resets the seen set to exactly
|
|
2559
|
+
* the new ids so the rewritten ops are not treated as duplicates.
|
|
2560
|
+
*/
|
|
2561
|
+
async replaceAll(ops) {
|
|
2562
|
+
const removeIds = this.queue.map((op) => op.id);
|
|
2563
|
+
for (const batch of this.inFlight.values()) {
|
|
2564
|
+
for (const op of batch) {
|
|
2565
|
+
removeIds.push(op.id);
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
this.inFlight.clear();
|
|
2569
|
+
this.queue = ops.length > 1 ? (0, import_internal.topologicalSort)([...ops]) : [...ops];
|
|
2570
|
+
this.seen.clear();
|
|
2571
|
+
for (const op of this.queue) {
|
|
2572
|
+
this.seen.add(op.id);
|
|
2573
|
+
}
|
|
2574
|
+
if (removeIds.length > 0) {
|
|
2575
|
+
await this.storage.dequeue(removeIds);
|
|
2576
|
+
}
|
|
2577
|
+
for (const op of this.queue) {
|
|
2578
|
+
await this.storage.enqueue(op);
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2348
2581
|
/**
|
|
2349
2582
|
* Whether initialize() has been called.
|
|
2350
2583
|
*/
|
|
@@ -2394,6 +2627,7 @@ var SyncEngine = class {
|
|
|
2394
2627
|
encryptor;
|
|
2395
2628
|
awarenessManager;
|
|
2396
2629
|
richtextDocChannel;
|
|
2630
|
+
blobChunkChannel;
|
|
2397
2631
|
metricsCollector;
|
|
2398
2632
|
syncState;
|
|
2399
2633
|
remoteVector = /* @__PURE__ */ new Map();
|
|
@@ -2406,6 +2640,9 @@ var SyncEngine = class {
|
|
|
2406
2640
|
currentBatch = null;
|
|
2407
2641
|
reconnecting = false;
|
|
2408
2642
|
schemaBlocked = false;
|
|
2643
|
+
clockBlocked = false;
|
|
2644
|
+
clockSkewMs = null;
|
|
2645
|
+
blobStorageEnabled = false;
|
|
2409
2646
|
// Track delta exchange state
|
|
2410
2647
|
deltaBatchesReceived = 0;
|
|
2411
2648
|
deltaReceiveComplete = false;
|
|
@@ -2454,6 +2691,14 @@ var SyncEngine = class {
|
|
|
2454
2691
|
this.transport.send(message);
|
|
2455
2692
|
}
|
|
2456
2693
|
});
|
|
2694
|
+
this.blobChunkChannel = new BlobChunkChannel({
|
|
2695
|
+
onSend: (message) => {
|
|
2696
|
+
if (this.state !== "streaming") {
|
|
2697
|
+
return;
|
|
2698
|
+
}
|
|
2699
|
+
this.transport.send(message);
|
|
2700
|
+
}
|
|
2701
|
+
});
|
|
2457
2702
|
this.awarenessManager.onSend((message) => {
|
|
2458
2703
|
if (this.state !== "streaming") return;
|
|
2459
2704
|
const wireMessage = {
|
|
@@ -2470,7 +2715,7 @@ var SyncEngine = class {
|
|
|
2470
2715
|
*/
|
|
2471
2716
|
async start() {
|
|
2472
2717
|
if (this.state !== "disconnected") {
|
|
2473
|
-
throw new
|
|
2718
|
+
throw new import_core7.SyncError("Cannot start sync engine: not in disconnected state", {
|
|
2474
2719
|
currentState: this.state
|
|
2475
2720
|
});
|
|
2476
2721
|
}
|
|
@@ -2487,7 +2732,7 @@ var SyncEngine = class {
|
|
|
2487
2732
|
this.transport.onClose((reason) => this.handleTransportClose(reason));
|
|
2488
2733
|
this.transport.onError((err) => this.handleTransportError(err));
|
|
2489
2734
|
if (this.schemaBlocked) {
|
|
2490
|
-
throw new
|
|
2735
|
+
throw new import_core7.SyncError(
|
|
2491
2736
|
"Sync is blocked due to schema version mismatch. Upgrade the app schema or align sync.schemaVersion with the server.",
|
|
2492
2737
|
{ code: "SCHEMA_MISMATCH_BLOCKED" }
|
|
2493
2738
|
);
|
|
@@ -2579,10 +2824,17 @@ var SyncEngine = class {
|
|
|
2579
2824
|
lastSyncedAt: this.lastSyncedAt,
|
|
2580
2825
|
lastSuccessfulPush: this.lastSuccessfulPush,
|
|
2581
2826
|
lastSuccessfulPull: this.lastSuccessfulPull,
|
|
2582
|
-
conflicts: this.conflictCount
|
|
2827
|
+
conflicts: this.conflictCount,
|
|
2828
|
+
clockSkewMs: this.clockSkewMs
|
|
2583
2829
|
};
|
|
2584
2830
|
switch (this.state) {
|
|
2585
2831
|
case "disconnected":
|
|
2832
|
+
if (this.clockBlocked) {
|
|
2833
|
+
return { ...base, status: "clock-error" };
|
|
2834
|
+
}
|
|
2835
|
+
if (this.schemaBlocked) {
|
|
2836
|
+
return { ...base, status: "schema-mismatch" };
|
|
2837
|
+
}
|
|
2586
2838
|
return { ...base, status: "offline" };
|
|
2587
2839
|
case "connecting":
|
|
2588
2840
|
case "handshaking":
|
|
@@ -2591,6 +2843,9 @@ var SyncEngine = class {
|
|
|
2591
2843
|
case "streaming":
|
|
2592
2844
|
return { ...base, status: pendingOperations > 0 ? "syncing" : "synced" };
|
|
2593
2845
|
case "error":
|
|
2846
|
+
if (this.clockBlocked) {
|
|
2847
|
+
return { ...base, status: "clock-error" };
|
|
2848
|
+
}
|
|
2594
2849
|
return { ...base, status: this.schemaBlocked ? "schema-mismatch" : "error" };
|
|
2595
2850
|
}
|
|
2596
2851
|
}
|
|
@@ -2601,6 +2856,27 @@ var SyncEngine = class {
|
|
|
2601
2856
|
isSchemaBlocked() {
|
|
2602
2857
|
return this.schemaBlocked;
|
|
2603
2858
|
}
|
|
2859
|
+
/**
|
|
2860
|
+
* True when sync is blocked because this device's clock is too far ahead.
|
|
2861
|
+
* Local writes continue to work and queue; only sync is paused.
|
|
2862
|
+
*/
|
|
2863
|
+
isClockBlocked() {
|
|
2864
|
+
return this.clockBlocked;
|
|
2865
|
+
}
|
|
2866
|
+
/** serverTime - localTime measured at the last handshake, or null before first connect. */
|
|
2867
|
+
getClockSkewMs() {
|
|
2868
|
+
return this.clockSkewMs;
|
|
2869
|
+
}
|
|
2870
|
+
/**
|
|
2871
|
+
* Clears the clock block after the user corrects the device clock.
|
|
2872
|
+
* Moves the engine back to `disconnected` so `start()` can run again.
|
|
2873
|
+
*/
|
|
2874
|
+
clearClockBlock() {
|
|
2875
|
+
this.clockBlocked = false;
|
|
2876
|
+
if (this.state === "error") {
|
|
2877
|
+
this.transitionTo("disconnected");
|
|
2878
|
+
}
|
|
2879
|
+
}
|
|
2604
2880
|
/**
|
|
2605
2881
|
* Clears schema-mismatch block after upgrading the local schema / sync config.
|
|
2606
2882
|
* Moves the engine back to `disconnected` so `start()` can run again.
|
|
@@ -2626,7 +2902,7 @@ var SyncEngine = class {
|
|
|
2626
2902
|
return this.getStatus().pendingOperations;
|
|
2627
2903
|
}
|
|
2628
2904
|
emitApplyFailure(op, result, overrides) {
|
|
2629
|
-
const reason = (0,
|
|
2905
|
+
const reason = (0, import_core7.defaultApplyFailureReason)(result, overrides);
|
|
2630
2906
|
this.emitter?.emit({
|
|
2631
2907
|
type: "sync:apply-failed",
|
|
2632
2908
|
operationId: op.id,
|
|
@@ -2736,6 +3012,31 @@ var SyncEngine = class {
|
|
|
2736
3012
|
getRichtextDocChannel() {
|
|
2737
3013
|
return this.richtextDocChannel;
|
|
2738
3014
|
}
|
|
3015
|
+
/**
|
|
3016
|
+
* Side channel for out-of-band blob chunk transfer over the sync connection.
|
|
3017
|
+
* The app layer bridges this to a `ChunkMessagePort` to pull/serve blob bytes.
|
|
3018
|
+
*/
|
|
3019
|
+
getBlobChunkChannel() {
|
|
3020
|
+
return this.blobChunkChannel;
|
|
3021
|
+
}
|
|
3022
|
+
/**
|
|
3023
|
+
* Whether the connected server persists blob bytes centrally (learned from the
|
|
3024
|
+
* handshake response). When true, the app uploads the bytes behind `blob`
|
|
3025
|
+
* fields so they stay available after the authoring device goes offline.
|
|
3026
|
+
*/
|
|
3027
|
+
isBlobStorageEnabled() {
|
|
3028
|
+
return this.blobStorageEnabled;
|
|
3029
|
+
}
|
|
3030
|
+
/**
|
|
3031
|
+
* Upload a blob chunk (or manifest) to the server for central persistence.
|
|
3032
|
+
* A no-op unless connected and the server advertised blob storage.
|
|
3033
|
+
*/
|
|
3034
|
+
uploadBlobChunk(hash, bytes) {
|
|
3035
|
+
if (this.state !== "streaming" || !this.blobStorageEnabled) {
|
|
3036
|
+
return;
|
|
3037
|
+
}
|
|
3038
|
+
this.blobChunkChannel.send({ type: "blob-chunk-push", hash, bytes });
|
|
3039
|
+
}
|
|
2739
3040
|
// --- Private methods ---
|
|
2740
3041
|
messageChain = Promise.resolve();
|
|
2741
3042
|
enqueueMessage(message) {
|
|
@@ -2744,7 +3045,7 @@ var SyncEngine = class {
|
|
|
2744
3045
|
async handleMessageAsync(message) {
|
|
2745
3046
|
switch (message.type) {
|
|
2746
3047
|
case "handshake-response":
|
|
2747
|
-
this.handleHandshakeResponse(message);
|
|
3048
|
+
await this.handleHandshakeResponse(message);
|
|
2748
3049
|
break;
|
|
2749
3050
|
case "operation-batch":
|
|
2750
3051
|
await this.handleOperationBatch(message);
|
|
@@ -2761,14 +3062,53 @@ var SyncEngine = class {
|
|
|
2761
3062
|
case "yjs-doc-update":
|
|
2762
3063
|
this.richtextDocChannel.deliver(message);
|
|
2763
3064
|
break;
|
|
3065
|
+
case "blob-chunk-request":
|
|
3066
|
+
case "blob-chunk-response":
|
|
3067
|
+
case "blob-chunk-push":
|
|
3068
|
+
this.blobChunkChannel.deliver(message);
|
|
3069
|
+
break;
|
|
2764
3070
|
}
|
|
2765
3071
|
}
|
|
2766
3072
|
handleMessageFailure(error) {
|
|
2767
3073
|
const reason = error instanceof Error ? error.message : "Message handling failed";
|
|
2768
3074
|
this.handleTransportClose(reason);
|
|
2769
3075
|
}
|
|
2770
|
-
|
|
3076
|
+
/**
|
|
3077
|
+
* Compares server wall-clock time from the handshake with local time.
|
|
3078
|
+
* Negative skew = this device's clock is fast (dangerous for LWW and rejected
|
|
3079
|
+
* by server ingest beyond 60s). Positive skew = slow (accepted but surfaced).
|
|
3080
|
+
* Zero developer work required: the result flows into sync status and events.
|
|
3081
|
+
*/
|
|
3082
|
+
evaluateClockSkew(serverTime) {
|
|
3083
|
+
const skewMs = serverTime - Date.now();
|
|
3084
|
+
this.clockSkewMs = skewMs;
|
|
3085
|
+
const FAST_BLOCK_MS = 6e4;
|
|
3086
|
+
const SLOW_WARN_MS = 10 * 6e4;
|
|
3087
|
+
let severity = "info";
|
|
3088
|
+
if (skewMs < -FAST_BLOCK_MS) {
|
|
3089
|
+
severity = "fast-blocked";
|
|
3090
|
+
} else if (skewMs > SLOW_WARN_MS) {
|
|
3091
|
+
severity = "slow-warning";
|
|
3092
|
+
}
|
|
3093
|
+
this.emitter?.emit({ type: "sync:clock-skew", skewMs, severity, source: "handshake" });
|
|
3094
|
+
if (severity === "fast-blocked") {
|
|
3095
|
+
this.clockBlocked = true;
|
|
3096
|
+
} else {
|
|
3097
|
+
this.clockBlocked = false;
|
|
3098
|
+
}
|
|
3099
|
+
}
|
|
3100
|
+
async handleHandshakeResponse(msg) {
|
|
2771
3101
|
if (this.state !== "handshaking") return;
|
|
3102
|
+
this.blobStorageEnabled = msg.blobStorageEnabled === true;
|
|
3103
|
+
if (typeof msg.serverTime === "number") {
|
|
3104
|
+
this.evaluateClockSkew(msg.serverTime);
|
|
3105
|
+
if (this.clockBlocked) {
|
|
3106
|
+
this.metricsCollector.updateStatus("error");
|
|
3107
|
+
this.transitionTo("error");
|
|
3108
|
+
void this.transport.disconnect();
|
|
3109
|
+
return;
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
2772
3112
|
if (!msg.accepted) {
|
|
2773
3113
|
const reason = msg.rejectReason ?? "Handshake rejected";
|
|
2774
3114
|
if (isSchemaMismatchReject(msg.rejectReason)) {
|
|
@@ -2815,8 +3155,51 @@ var SyncEngine = class {
|
|
|
2815
3155
|
this.deltaSentOpIds = [];
|
|
2816
3156
|
this.pendingDeltaBatchAcks.clear();
|
|
2817
3157
|
this.initialSyncTotalBatches = 0;
|
|
3158
|
+
if (typeof msg.serverTime === "number") {
|
|
3159
|
+
await this.maybeRebaseQueuedOperations(msg.serverTime);
|
|
3160
|
+
}
|
|
2818
3161
|
this.sendDelta();
|
|
2819
3162
|
}
|
|
3163
|
+
/**
|
|
3164
|
+
* Re-stamps queued (never-acknowledged) operations whose timestamps are far
|
|
3165
|
+
* enough in the future that the server would reject them, using the server's
|
|
3166
|
+
* own handshake time as the trusted "now". This is the automatic recovery
|
|
3167
|
+
* path after a user corrects a fast device clock: the queue drains
|
|
3168
|
+
* immediately instead of waiting for real time to catch up.
|
|
3169
|
+
*/
|
|
3170
|
+
async maybeRebaseQueuedOperations(serverTime) {
|
|
3171
|
+
const rebase = this.store.rebaseUnsyncedOperations?.bind(this.store);
|
|
3172
|
+
if (!rebase) return;
|
|
3173
|
+
const queued = this.outboundQueue.getAll();
|
|
3174
|
+
if (queued.length === 0) return;
|
|
3175
|
+
let maxQueuedWallTime = Number.NEGATIVE_INFINITY;
|
|
3176
|
+
for (const op of queued) {
|
|
3177
|
+
if (op.timestamp.wallTime > maxQueuedWallTime) {
|
|
3178
|
+
maxQueuedWallTime = op.timestamp.wallTime;
|
|
3179
|
+
}
|
|
3180
|
+
}
|
|
3181
|
+
const SERVER_FUTURE_TOLERANCE_MS = 6e4;
|
|
3182
|
+
if (maxQueuedWallTime <= serverTime + SERVER_FUTURE_TOLERANCE_MS) return;
|
|
3183
|
+
try {
|
|
3184
|
+
const result = await rebase(
|
|
3185
|
+
queued.map((op) => op.id),
|
|
3186
|
+
serverTime
|
|
3187
|
+
);
|
|
3188
|
+
await this.outboundQueue.replaceAll(result.operations);
|
|
3189
|
+
this.emitter?.emit({
|
|
3190
|
+
type: "sync:clock-rebase",
|
|
3191
|
+
rebasedCount: result.rebasedCount,
|
|
3192
|
+
maxSkewMs: maxQueuedWallTime - serverTime
|
|
3193
|
+
});
|
|
3194
|
+
} catch (error) {
|
|
3195
|
+
this.emitter?.emit({
|
|
3196
|
+
type: "store:persistence-error",
|
|
3197
|
+
dbName: "kora-oplog",
|
|
3198
|
+
message: error instanceof Error ? error.message : "Timestamp rebase failed",
|
|
3199
|
+
code: "CLOCK_REBASE_FAILED"
|
|
3200
|
+
});
|
|
3201
|
+
}
|
|
3202
|
+
}
|
|
2820
3203
|
async sendDelta() {
|
|
2821
3204
|
const localVector = this.store.getVersionVector();
|
|
2822
3205
|
const allMissingOps = await this.collectDelta(localVector, this.remoteVector);
|
|
@@ -2901,7 +3284,7 @@ var SyncEngine = class {
|
|
|
2901
3284
|
const targetSchemaVersion = this.config.schemaVersion ?? DEFAULT_SCHEMA_VERSION;
|
|
2902
3285
|
const transforms = this.config.operationTransforms ?? [];
|
|
2903
3286
|
for (const op of inScopeOps) {
|
|
2904
|
-
const transformed = transforms.length > 0 ? (0,
|
|
3287
|
+
const transformed = transforms.length > 0 ? (0, import_core7.applyOperationTransforms)(op, targetSchemaVersion, transforms) : op;
|
|
2905
3288
|
if (transformed === null) {
|
|
2906
3289
|
continue;
|
|
2907
3290
|
}
|
|
@@ -2911,20 +3294,20 @@ var SyncEngine = class {
|
|
|
2911
3294
|
this.emitApplyFailure(transformed, result);
|
|
2912
3295
|
}
|
|
2913
3296
|
} catch (error) {
|
|
2914
|
-
if (error instanceof
|
|
3297
|
+
if (error instanceof import_core7.ClockDriftError) {
|
|
2915
3298
|
this.emitApplyFailure(transformed, "rejected", {
|
|
2916
|
-
code:
|
|
3299
|
+
code: import_core7.APPLY_FAILURE_CODES.CLOCK_DRIFT,
|
|
2917
3300
|
message: error.message,
|
|
2918
3301
|
retriable: false
|
|
2919
3302
|
});
|
|
2920
3303
|
continue;
|
|
2921
3304
|
}
|
|
2922
3305
|
const message = error instanceof Error ? error.message : "Apply failed";
|
|
2923
|
-
const code = error instanceof
|
|
3306
|
+
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;
|
|
2924
3307
|
this.emitApplyFailure(transformed, "rejected", {
|
|
2925
3308
|
code,
|
|
2926
3309
|
message,
|
|
2927
|
-
retriable: code !==
|
|
3310
|
+
retriable: code !== import_core7.APPLY_FAILURE_CODES.REFERENTIAL_INTEGRITY
|
|
2928
3311
|
});
|
|
2929
3312
|
}
|
|
2930
3313
|
}
|
|
@@ -2996,6 +3379,17 @@ var SyncEngine = class {
|
|
|
2996
3379
|
if (msg.code === "AUTH_FAILED") {
|
|
2997
3380
|
this.emitter?.emit({ type: "sync:auth-failed", reason: msg.message });
|
|
2998
3381
|
}
|
|
3382
|
+
if (msg.code === "INVALID_TIMESTAMP") {
|
|
3383
|
+
this.clockBlocked = true;
|
|
3384
|
+
this.emitter?.emit({
|
|
3385
|
+
type: "sync:clock-skew",
|
|
3386
|
+
skewMs: this.clockSkewMs ?? Number.NaN,
|
|
3387
|
+
severity: "fast-blocked",
|
|
3388
|
+
source: "server-reject"
|
|
3389
|
+
});
|
|
3390
|
+
this.emitter?.emit({ type: "sync:disconnected", reason: msg.message });
|
|
3391
|
+
return;
|
|
3392
|
+
}
|
|
2999
3393
|
this.emitter?.emit({ type: "sync:disconnected", reason: msg.message });
|
|
3000
3394
|
this.transitionTo("disconnected");
|
|
3001
3395
|
}
|
|
@@ -3183,7 +3577,7 @@ var SyncEngine = class {
|
|
|
3183
3577
|
transitionTo(newState) {
|
|
3184
3578
|
const validTargets = VALID_TRANSITIONS[this.state];
|
|
3185
3579
|
if (!validTargets.includes(newState)) {
|
|
3186
|
-
throw new
|
|
3580
|
+
throw new import_core7.SyncError(`Invalid sync state transition: ${this.state} \u2192 ${newState}`, {
|
|
3187
3581
|
from: this.state,
|
|
3188
3582
|
to: newState
|
|
3189
3583
|
});
|
|
@@ -3465,18 +3859,18 @@ var ReconnectionManager = class {
|
|
|
3465
3859
|
};
|
|
3466
3860
|
|
|
3467
3861
|
// src/encryption/sync-encryptor.ts
|
|
3468
|
-
var
|
|
3862
|
+
var import_core9 = require("@korajs/core");
|
|
3469
3863
|
|
|
3470
3864
|
// src/encryption/key-derivation.ts
|
|
3471
|
-
var
|
|
3472
|
-
var KeyDerivationError = class extends
|
|
3865
|
+
var import_core8 = require("@korajs/core");
|
|
3866
|
+
var KeyDerivationError = class extends import_core8.SyncError {
|
|
3473
3867
|
constructor(message, context) {
|
|
3474
3868
|
super(message, { ...context, errorType: "KEY_DERIVATION_ERROR" });
|
|
3475
3869
|
this.name = "KeyDerivationError";
|
|
3476
3870
|
}
|
|
3477
3871
|
};
|
|
3478
3872
|
var SALT_LENGTH = 32;
|
|
3479
|
-
var
|
|
3873
|
+
var DEFAULT_PBKDF2_ITERATIONS = 6e5;
|
|
3480
3874
|
var DERIVED_KEY_LENGTH = 256;
|
|
3481
3875
|
function assertCryptoAvailable() {
|
|
3482
3876
|
if (typeof globalThis.crypto === "undefined" || typeof globalThis.crypto.subtle === "undefined") {
|
|
@@ -3493,13 +3887,19 @@ function generateSalt() {
|
|
|
3493
3887
|
}
|
|
3494
3888
|
return globalThis.crypto.getRandomValues(new Uint8Array(SALT_LENGTH));
|
|
3495
3889
|
}
|
|
3496
|
-
async function deriveKey(passphrase, salt) {
|
|
3890
|
+
async function deriveKey(passphrase, salt, iterations = DEFAULT_PBKDF2_ITERATIONS) {
|
|
3497
3891
|
assertCryptoAvailable();
|
|
3498
3892
|
if (passphrase.length === 0) {
|
|
3499
3893
|
throw new KeyDerivationError(
|
|
3500
3894
|
"Passphrase must not be empty. Provide a non-empty string for encryption key derivation."
|
|
3501
3895
|
);
|
|
3502
3896
|
}
|
|
3897
|
+
if (!Number.isInteger(iterations) || iterations < 1) {
|
|
3898
|
+
throw new KeyDerivationError(
|
|
3899
|
+
`PBKDF2 iteration count must be a positive integer, received: ${iterations}`,
|
|
3900
|
+
{ iterations }
|
|
3901
|
+
);
|
|
3902
|
+
}
|
|
3503
3903
|
const usedSalt = salt ?? generateSalt();
|
|
3504
3904
|
try {
|
|
3505
3905
|
const passphraseBytes = new TextEncoder().encode(passphrase);
|
|
@@ -3514,7 +3914,7 @@ async function deriveKey(passphrase, salt) {
|
|
|
3514
3914
|
{
|
|
3515
3915
|
name: "PBKDF2",
|
|
3516
3916
|
salt: usedSalt,
|
|
3517
|
-
iterations
|
|
3917
|
+
iterations,
|
|
3518
3918
|
hash: "SHA-256"
|
|
3519
3919
|
},
|
|
3520
3920
|
baseKey,
|
|
@@ -3534,24 +3934,24 @@ async function deriveKey(passphrase, salt) {
|
|
|
3534
3934
|
);
|
|
3535
3935
|
}
|
|
3536
3936
|
}
|
|
3537
|
-
async function deriveVersionedKey(passphrase, version, salt) {
|
|
3937
|
+
async function deriveVersionedKey(passphrase, version, salt, iterations = DEFAULT_PBKDF2_ITERATIONS) {
|
|
3538
3938
|
if (!Number.isInteger(version) || version < 1) {
|
|
3539
3939
|
throw new KeyDerivationError(`Key version must be a positive integer, received: ${version}`, {
|
|
3540
3940
|
version
|
|
3541
3941
|
});
|
|
3542
3942
|
}
|
|
3543
|
-
const { key, salt: usedSalt } = await deriveKey(passphrase, salt);
|
|
3943
|
+
const { key, salt: usedSalt } = await deriveKey(passphrase, salt, iterations);
|
|
3544
3944
|
return { version, key, salt: usedSalt };
|
|
3545
3945
|
}
|
|
3546
3946
|
|
|
3547
3947
|
// src/encryption/sync-encryptor.ts
|
|
3548
|
-
var EncryptionError = class extends
|
|
3948
|
+
var EncryptionError = class extends import_core9.SyncError {
|
|
3549
3949
|
constructor(message, context) {
|
|
3550
3950
|
super(message, { ...context, errorType: "ENCRYPTION_ERROR" });
|
|
3551
3951
|
this.name = "EncryptionError";
|
|
3552
3952
|
}
|
|
3553
3953
|
};
|
|
3554
|
-
var DecryptionError = class extends
|
|
3954
|
+
var DecryptionError = class extends import_core9.SyncError {
|
|
3555
3955
|
constructor(message, context) {
|
|
3556
3956
|
super(message, { ...context, errorType: "DECRYPTION_ERROR" });
|
|
3557
3957
|
this.name = "DecryptionError";
|
|
@@ -3580,11 +3980,13 @@ var SyncEncryptor = class _SyncEncryptor {
|
|
|
3580
3980
|
* @param config - Encryption configuration with passphrase
|
|
3581
3981
|
* @param salt - Optional salt for deterministic key derivation (mainly for testing).
|
|
3582
3982
|
* If omitted, a random salt is generated.
|
|
3983
|
+
* @param iterations - Optional PBKDF2 iteration count. Defaults to the
|
|
3984
|
+
* production-strength value. Lower it only in tests.
|
|
3583
3985
|
* @returns A configured SyncEncryptor instance
|
|
3584
3986
|
* @throws {EncryptionError} If configuration is invalid
|
|
3585
3987
|
* @throws {KeyDerivationError} If key derivation fails
|
|
3586
3988
|
*/
|
|
3587
|
-
static async create(config, salt) {
|
|
3989
|
+
static async create(config, salt, iterations) {
|
|
3588
3990
|
if (!config.enabled) {
|
|
3589
3991
|
throw new EncryptionError(
|
|
3590
3992
|
"Cannot create SyncEncryptor with encryption disabled. Set enabled: true in the encryption config."
|
|
@@ -3596,7 +3998,7 @@ var SyncEncryptor = class _SyncEncryptor {
|
|
|
3596
3998
|
"Encryption key/passphrase must not be empty. Provide a non-empty string or key provider function."
|
|
3597
3999
|
);
|
|
3598
4000
|
}
|
|
3599
|
-
const versionedKey = await deriveVersionedKey(passphrase, 1, salt);
|
|
4001
|
+
const versionedKey = await deriveVersionedKey(passphrase, 1, salt, iterations);
|
|
3600
4002
|
const keys = /* @__PURE__ */ new Map();
|
|
3601
4003
|
keys.set(1, versionedKey);
|
|
3602
4004
|
return new _SyncEncryptor(keys, 1);
|
|
@@ -3897,10 +4299,12 @@ var OFFLINE_SYNC_STATUS = Object.freeze({
|
|
|
3897
4299
|
lastSyncedAt: null,
|
|
3898
4300
|
lastSuccessfulPush: null,
|
|
3899
4301
|
lastSuccessfulPull: null,
|
|
3900
|
-
conflicts: 0
|
|
4302
|
+
conflicts: 0,
|
|
4303
|
+
clockSkewMs: null
|
|
3901
4304
|
});
|
|
3902
4305
|
var SYNC_STATUS_EVENT_TYPES = [
|
|
3903
4306
|
"sync:connected",
|
|
4307
|
+
"sync:clock-skew",
|
|
3904
4308
|
"sync:disconnected",
|
|
3905
4309
|
"sync:schema-mismatch",
|
|
3906
4310
|
"sync:auth-failed",
|
|
@@ -4044,6 +4448,7 @@ function subscribeRemoteAwarenessStates(awareness, listener) {
|
|
|
4044
4448
|
// Annotate the CommonJS export names for ESM import in node:
|
|
4045
4449
|
0 && (module.exports = {
|
|
4046
4450
|
AwarenessManager,
|
|
4451
|
+
BlobChunkChannel,
|
|
4047
4452
|
ChaosTransport,
|
|
4048
4453
|
ConnectionMonitor,
|
|
4049
4454
|
DEFAULT_RICHTEXT_DOC_CHANNEL_THRESHOLD,
|
|
@@ -4068,11 +4473,13 @@ function subscribeRemoteAwarenessStates(awareness, listener) {
|
|
|
4068
4473
|
WebSocketTransport,
|
|
4069
4474
|
createDeltaCursorFromBatch,
|
|
4070
4475
|
createSyncStatusController,
|
|
4476
|
+
decodeBlobChunkBytes,
|
|
4071
4477
|
decodeDeltaCursor,
|
|
4072
4478
|
decodeYjsUpdate,
|
|
4073
4479
|
dedupeQuerySubsets,
|
|
4074
4480
|
deriveKey,
|
|
4075
4481
|
deriveVersionedKey,
|
|
4482
|
+
encodeBlobChunkBytes,
|
|
4076
4483
|
encodeDeltaCursor,
|
|
4077
4484
|
encodeYjsUpdate,
|
|
4078
4485
|
filterOperationsByScope,
|
|
@@ -4080,6 +4487,9 @@ function subscribeRemoteAwarenessStates(awareness, listener) {
|
|
|
4080
4487
|
getRemoteAwarenessStates,
|
|
4081
4488
|
isAcknowledgmentMessage,
|
|
4082
4489
|
isAwarenessUpdateMessage,
|
|
4490
|
+
isBlobChunkPushMessage,
|
|
4491
|
+
isBlobChunkRequestMessage,
|
|
4492
|
+
isBlobChunkResponseMessage,
|
|
4083
4493
|
isClientSchemaVersionSupported,
|
|
4084
4494
|
isEncryptedPayload,
|
|
4085
4495
|
isErrorMessage,
|