@korajs/sync 1.0.0-beta.1 → 1.0.0-beta.4

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
@@ -41,6 +41,7 @@ __export(index_exports, {
41
41
  InvalidScopeError: () => InvalidScopeError,
42
42
  JsonMessageSerializer: () => JsonMessageSerializer,
43
43
  KeyDerivationError: () => KeyDerivationError,
44
+ MemoryRejectedOperationStorage: () => MemoryRejectedOperationStorage,
44
45
  NegotiatedMessageSerializer: () => NegotiatedMessageSerializer,
45
46
  OFFLINE_SYNC_STATUS: () => OFFLINE_SYNC_STATUS,
46
47
  OutboundQueue: () => OutboundQueue,
@@ -79,6 +80,7 @@ __export(index_exports, {
79
80
  isHandshakeMessage: () => isHandshakeMessage,
80
81
  isHandshakeResponseMessage: () => isHandshakeResponseMessage,
81
82
  isOperationBatchMessage: () => isOperationBatchMessage,
83
+ isOperationRejectedMessage: () => isOperationRejectedMessage,
82
84
  isSchemaMismatchReject: () => isSchemaMismatchReject,
83
85
  isSyncMessage: () => isSyncMessage,
84
86
  isYjsDocUpdateMessage: () => isYjsDocUpdateMessage,
@@ -111,6 +113,20 @@ var SYNC_STATUSES = [
111
113
  "error",
112
114
  "schema-mismatch"
113
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
+ };
114
130
  var ScopeViolationError = class extends import_core.KoraError {
115
131
  constructor(operationId, collection, scope, message) {
116
132
  super(
@@ -308,6 +324,8 @@ function isSyncMessage(value) {
308
324
  return isAcknowledgmentMessage(value);
309
325
  case "error":
310
326
  return isErrorMessage(value);
327
+ case "operation-rejected":
328
+ return isOperationRejectedMessage(value);
311
329
  case "awareness-update":
312
330
  return isAwarenessUpdateMessage(value);
313
331
  case "yjs-doc-update":
@@ -347,6 +365,11 @@ function isErrorMessage(value) {
347
365
  const msg = value;
348
366
  return msg.type === "error" && typeof msg.messageId === "string" && typeof msg.code === "string" && typeof msg.message === "string" && typeof msg.retriable === "boolean";
349
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
+ }
350
373
  function isAwarenessUpdateMessage(value) {
351
374
  if (typeof value !== "object" || value === null) return false;
352
375
  const msg = value;
@@ -638,6 +661,17 @@ function toProtoEnvelope(message) {
638
661
  errorMessage: message.message,
639
662
  retriable: message.retriable
640
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
+ };
641
675
  case "awareness-update":
642
676
  case "yjs-doc-update":
643
677
  return {
@@ -724,6 +758,17 @@ function fromProtoEnvelope(envelope) {
724
758
  message: envelope.errorMessage ?? "Unknown error",
725
759
  retriable: envelope.retriable ?? false
726
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
+ };
727
772
  case "blob-chunk-request":
728
773
  return {
729
774
  type: "blob-chunk-request",
@@ -898,6 +943,12 @@ function encodeEnvelope(envelope) {
898
943
  if (envelope.hasBytes !== void 0) writer.uint32(184).bool(envelope.hasBytes);
899
944
  if (envelope.blobStorageEnabled !== void 0)
900
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);
901
952
  return writer.finish();
902
953
  }
903
954
  function decodeEnvelope(bytes) {
@@ -984,6 +1035,15 @@ function decodeEnvelope(bytes) {
984
1035
  case 25:
985
1036
  envelope.blobStorageEnabled = reader.bool();
986
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;
987
1047
  default:
988
1048
  reader.skipType(tag & 7);
989
1049
  }
@@ -2584,6 +2644,37 @@ var OutboundQueue = class {
2584
2644
  get isInitialized() {
2585
2645
  return this.initialized;
2586
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
+ }
2587
2678
  /**
2588
2679
  * Remove operations by id from queue and persistent storage.
2589
2680
  * Used when ops were already sent during handshake delta exchange.
@@ -2623,6 +2714,7 @@ var SyncEngine = class {
2623
2714
  serializer;
2624
2715
  emitter;
2625
2716
  outboundQueue;
2717
+ rejectedStorage;
2626
2718
  batchSize;
2627
2719
  encryptor;
2628
2720
  awarenessManager;
@@ -2675,6 +2767,7 @@ var SyncEngine = class {
2675
2767
  this.activeScope = options.config.scopeMap;
2676
2768
  const queueStorage = options.queueStorage ?? new MemoryQueueStorage();
2677
2769
  this.outboundQueue = new OutboundQueue(queueStorage);
2770
+ this.rejectedStorage = options.rejectedStorage ?? new MemoryRejectedOperationStorage();
2678
2771
  this.metricsCollector = new SyncMetricsCollector(options.metricsConfig);
2679
2772
  if (this.emitter) {
2680
2773
  this.metricsCollector.attachEmitter(this.emitter);
@@ -3056,6 +3149,9 @@ var SyncEngine = class {
3056
3149
  case "error":
3057
3150
  this.handleError(message);
3058
3151
  break;
3152
+ case "operation-rejected":
3153
+ await this.handleOperationRejected(message);
3154
+ break;
3059
3155
  case "awareness-update":
3060
3156
  this.handleAwarenessUpdate(message);
3061
3157
  break;
@@ -3393,6 +3489,50 @@ var SyncEngine = class {
3393
3489
  this.emitter?.emit({ type: "sync:disconnected", reason: msg.message });
3394
3490
  this.transitionTo("disconnected");
3395
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
+ }
3396
3536
  async checkDeltaComplete() {
3397
3537
  if (!this.deltaSendComplete || !this.deltaReceiveComplete) {
3398
3538
  return;
@@ -4458,6 +4598,7 @@ function subscribeRemoteAwarenessStates(awareness, listener) {
4458
4598
  InvalidScopeError,
4459
4599
  JsonMessageSerializer,
4460
4600
  KeyDerivationError,
4601
+ MemoryRejectedOperationStorage,
4461
4602
  NegotiatedMessageSerializer,
4462
4603
  OFFLINE_SYNC_STATUS,
4463
4604
  OutboundQueue,
@@ -4496,6 +4637,7 @@ function subscribeRemoteAwarenessStates(awareness, listener) {
4496
4637
  isHandshakeMessage,
4497
4638
  isHandshakeResponseMessage,
4498
4639
  isOperationBatchMessage,
4640
+ isOperationRejectedMessage,
4499
4641
  isSchemaMismatchReject,
4500
4642
  isSyncMessage,
4501
4643
  isYjsDocUpdateMessage,