@korajs/sync 1.0.0-beta.0 → 1.0.0-beta.10

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