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

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