@korajs/sync 1.0.0-beta.1 → 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.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { S as SyncScopeMap, D as DeltaCursor, a as SyncMessage, b as SerializedOperation, W as WireFormat, c as SyncTransport, T as TransportOptions, d as TransportMessageHandler, e as TransportCloseHandler, f as TransportErrorHandler, B as BlobChunkRequestMessage, g as BlobChunkResponseMessage, h as BlobChunkPushMessage, i as SyncEncryptionConfig, V as VersionedKey, Y as YjsDocUpdateMessage, Q as QueueStorage, j as SyncState, k as SyncStatusInfo, l as SyncConfig, m as SyncStatePersistence, n as SyncQuerySubset } from './transport-CZLMdZ0k.cjs';
2
- export { A as AcknowledgmentMessage, o as AwarenessStateWire, p as AwarenessUpdateMessage, E as EncryptedPayload, q as ErrorMessage, H as HandshakeMessage, r as HandshakeResponseMessage, I as InvalidScopeError, O as OperationBatchMessage, s as SYNC_STATES, t as SYNC_STATUSES, u as ScopeViolationError, v as SyncEncryptionAlgorithm, w as SyncScopeContext, x as SyncStatus, y as dedupeQuerySubsets, z as isAcknowledgmentMessage, C as isAwarenessUpdateMessage, F as isBlobChunkPushMessage, G as isBlobChunkRequestMessage, J as isBlobChunkResponseMessage, K as isErrorMessage, L as isHandshakeMessage, M as isHandshakeResponseMessage, N as isOperationBatchMessage, P as isSyncMessage, R as isYjsDocUpdateMessage, U as operationMatchesQuerySubsets } from './transport-CZLMdZ0k.cjs';
1
+ import { S as SyncScopeMap, D as DeltaCursor, a as SyncMessage, b as SerializedOperation, W as WireFormat, c as SyncTransport, T as TransportOptions, d as TransportMessageHandler, e as TransportCloseHandler, f as TransportErrorHandler, B as BlobChunkRequestMessage, g as BlobChunkResponseMessage, h as BlobChunkPushMessage, i as SyncEncryptionConfig, V as VersionedKey, Y as YjsDocUpdateMessage, Q as QueueStorage, j as SyncState, k as SyncStatusInfo, l as SyncConfig, R as RejectedOperationStorage, m as SyncStatePersistence, n as SyncQuerySubset, o as RejectedOperation } from './transport-BGr-_eH8.cjs';
2
+ export { A as AcknowledgmentMessage, p as AwarenessStateWire, q as AwarenessUpdateMessage, E as EncryptedPayload, r as ErrorMessage, H as HandshakeMessage, s as HandshakeResponseMessage, I as InvalidScopeError, M as MemoryRejectedOperationStorage, O as OperationBatchMessage, t as OperationRejectedMessage, u as SYNC_STATES, v as SYNC_STATUSES, w as ScopeViolationError, x as SyncEncryptionAlgorithm, y as SyncScopeContext, z as SyncStatus, C as dedupeQuerySubsets, F as isAcknowledgmentMessage, G as isAwarenessUpdateMessage, J as isBlobChunkPushMessage, K as isBlobChunkRequestMessage, L as isBlobChunkResponseMessage, N as isErrorMessage, P as isHandshakeMessage, U as isHandshakeResponseMessage, X as isOperationBatchMessage, Z as isOperationRejectedMessage, _ as isSyncMessage, $ as isYjsDocUpdateMessage, a0 as operationMatchesQuerySubsets } from './transport-BGr-_eH8.cjs';
3
3
  import * as _korajs_core from '@korajs/core';
4
4
  import { Operation, VersionVector, ApplyResult, KoraEventEmitter, TimeSource, SyncError, ConnectionQuality } from '@korajs/core';
5
5
  export { ApplyFailureReason, ApplyResult } from '@korajs/core';
@@ -844,6 +844,14 @@ declare class OutboundQueue {
844
844
  * Whether initialize() has been called.
845
845
  */
846
846
  get isInitialized(): boolean;
847
+ /**
848
+ * Remove a single operation the server permanently rejected — from the queue,
849
+ * from any in-flight batch, and from durable storage — so a later batch ack or
850
+ * a reconnect `returnBatch` can never resend or resurrect it. Returns the
851
+ * removed operation so the caller can record it in a durable rejected store,
852
+ * or null if it was not present (already acknowledged and removed).
853
+ */
854
+ reject(opId: string): Promise<Operation | null>;
847
855
  /**
848
856
  * Remove operations by id from queue and persistent storage.
849
857
  * Used when ops were already sent during handshake delta exchange.
@@ -867,6 +875,11 @@ interface SyncEngineOptions {
867
875
  emitter?: KoraEventEmitter;
868
876
  /** Queue storage for persistent outbound queue. Defaults to in-memory. */
869
877
  queueStorage?: QueueStorage;
878
+ /**
879
+ * Durable storage for operations the server rejected. Defaults to in-memory.
880
+ * Provide a store-backed implementation so rejections survive a page refresh.
881
+ */
882
+ rejectedStorage?: RejectedOperationStorage;
870
883
  /**
871
884
  * Optional encryptor for end-to-end encryption.
872
885
  * When provided, `data` and `previousData` fields of operations are encrypted
@@ -911,6 +924,7 @@ declare class SyncEngine {
911
924
  private readonly serializer;
912
925
  private readonly emitter;
913
926
  private readonly outboundQueue;
927
+ private readonly rejectedStorage;
914
928
  private readonly batchSize;
915
929
  private readonly encryptor;
916
930
  private readonly awarenessManager;
@@ -1107,6 +1121,25 @@ declare class SyncEngine {
1107
1121
  private handleOperationBatch;
1108
1122
  private handleAcknowledgment;
1109
1123
  private handleError;
1124
+ /**
1125
+ * Handle a per-operation rejection: divert the op out of the pending outbound
1126
+ * queue (so it is never retried or resurrected on reconnect), record it in the
1127
+ * durable rejected store (so it is kept and explainable), and emit an event so
1128
+ * the app can reconcile. Unlike {@link handleError}, this is a normal per-op
1129
+ * signal, so the connection stays up.
1130
+ */
1131
+ private handleOperationRejected;
1132
+ /**
1133
+ * Every operation the server rejected that has not yet been reconciled. The app
1134
+ * uses this (alongside the `sync:operation-rejected` event) to surface failed
1135
+ * submissions and decide whether to roll back or resubmit.
1136
+ */
1137
+ getRejectedOperations(): Promise<RejectedOperation[]>;
1138
+ /**
1139
+ * Forget rejected operations by id once the app has reconciled them (rolled the
1140
+ * optimistic write back or resubmitted a corrected op).
1141
+ */
1142
+ clearRejectedOperations(operationIds: string[]): Promise<void>;
1110
1143
  private checkDeltaComplete;
1111
1144
  /**
1112
1145
  * Effective server vector: persisted last-ack merged with live handshake remote vector.
@@ -1375,4 +1408,4 @@ declare function getRemoteAwarenessStates(awareness: AwarenessManager): Awarenes
1375
1408
  */
1376
1409
  declare function subscribeRemoteAwarenessStates(awareness: AwarenessManager, listener: (states: AwarenessState[]) => void): () => void;
1377
1410
 
1378
- export { type AwarenessChange, type AwarenessCursor, AwarenessManager, type AwarenessMessage, type AwarenessState, type AwarenessUser, BlobChunkChannel, type BlobChunkChannelMessage, type BlobChunkChannelOptions, BlobChunkPushMessage, BlobChunkRequestMessage, BlobChunkResponseMessage, type ChaosConfig, ChaosTransport, ConnectionMonitor, type ConnectionMonitorConfig, type CursorInfo, DEFAULT_RICHTEXT_DOC_CHANNEL_THRESHOLD, DecryptionError, DeltaCursor, EncryptionError, HttpLongPollingTransport, type HttpLongPollingTransportOptions, JsonMessageSerializer, KeyDerivationError, type MessageSerializer, NegotiatedMessageSerializer, OFFLINE_SYNC_STATUS, type OutboundBatch, OutboundQueue, ProtobufMessageSerializer, QueueStorage, type ReconnectionConfig, ReconnectionManager, RichtextDocChannel, type RichtextDocChannelOptions, type RichtextDocListener, SCHEMA_MISMATCH_PREFIX, SerializedOperation, SyncConfig, type SyncDiagnostics, SyncEncryptionConfig, SyncEncryptor, SyncEngine, type SyncEngineOptions, SyncMessage, SyncQuerySubset, SyncScopeMap, SyncState, SyncStatePersistence, type SyncStatusController, type SyncStatusControllerOptions, SyncStatusInfo, type SyncStore, SyncTransport, TransportCloseHandler, TransportErrorHandler, TransportMessageHandler, TransportOptions, VersionedKey, type WebSocketConstructor, type WebSocketLike, WebSocketTransport, type WebSocketTransportOptions, WireFormat, YjsDocUpdateMessage, createDeltaCursorFromBatch, createSyncStatusController, decodeBlobChunkBytes, decodeDeltaCursor, decodeYjsUpdate, deriveKey, deriveVersionedKey, encodeBlobChunkBytes, encodeDeltaCursor, encodeYjsUpdate, filterOperationsByScope, generateSalt, getRemoteAwarenessStates, isClientSchemaVersionSupported, isEncryptedPayload, isSchemaMismatchReject, operationMatchesScope, richtextDocKey, sliceOperationsAfterCursor, subscribeRemoteAwarenessStates, versionVectorToWire, wireToVersionVector };
1411
+ export { type AwarenessChange, type AwarenessCursor, AwarenessManager, type AwarenessMessage, type AwarenessState, type AwarenessUser, BlobChunkChannel, type BlobChunkChannelMessage, type BlobChunkChannelOptions, BlobChunkPushMessage, BlobChunkRequestMessage, BlobChunkResponseMessage, type ChaosConfig, ChaosTransport, ConnectionMonitor, type ConnectionMonitorConfig, type CursorInfo, DEFAULT_RICHTEXT_DOC_CHANNEL_THRESHOLD, DecryptionError, DeltaCursor, EncryptionError, HttpLongPollingTransport, type HttpLongPollingTransportOptions, JsonMessageSerializer, KeyDerivationError, type MessageSerializer, NegotiatedMessageSerializer, OFFLINE_SYNC_STATUS, type OutboundBatch, OutboundQueue, ProtobufMessageSerializer, QueueStorage, type ReconnectionConfig, ReconnectionManager, RejectedOperation, RejectedOperationStorage, RichtextDocChannel, type RichtextDocChannelOptions, type RichtextDocListener, SCHEMA_MISMATCH_PREFIX, SerializedOperation, SyncConfig, type SyncDiagnostics, SyncEncryptionConfig, SyncEncryptor, SyncEngine, type SyncEngineOptions, SyncMessage, SyncQuerySubset, SyncScopeMap, SyncState, SyncStatePersistence, type SyncStatusController, type SyncStatusControllerOptions, SyncStatusInfo, type SyncStore, SyncTransport, TransportCloseHandler, TransportErrorHandler, TransportMessageHandler, TransportOptions, VersionedKey, type WebSocketConstructor, type WebSocketLike, WebSocketTransport, type WebSocketTransportOptions, WireFormat, YjsDocUpdateMessage, createDeltaCursorFromBatch, createSyncStatusController, decodeBlobChunkBytes, decodeDeltaCursor, decodeYjsUpdate, deriveKey, deriveVersionedKey, encodeBlobChunkBytes, encodeDeltaCursor, encodeYjsUpdate, filterOperationsByScope, generateSalt, getRemoteAwarenessStates, isClientSchemaVersionSupported, isEncryptedPayload, isSchemaMismatchReject, operationMatchesScope, richtextDocKey, sliceOperationsAfterCursor, subscribeRemoteAwarenessStates, versionVectorToWire, wireToVersionVector };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { S as SyncScopeMap, D as DeltaCursor, a as SyncMessage, b as SerializedOperation, W as WireFormat, c as SyncTransport, T as TransportOptions, d as TransportMessageHandler, e as TransportCloseHandler, f as TransportErrorHandler, B as BlobChunkRequestMessage, g as BlobChunkResponseMessage, h as BlobChunkPushMessage, i as SyncEncryptionConfig, V as VersionedKey, Y as YjsDocUpdateMessage, Q as QueueStorage, j as SyncState, k as SyncStatusInfo, l as SyncConfig, m as SyncStatePersistence, n as SyncQuerySubset } from './transport-CZLMdZ0k.js';
2
- export { A as AcknowledgmentMessage, o as AwarenessStateWire, p as AwarenessUpdateMessage, E as EncryptedPayload, q as ErrorMessage, H as HandshakeMessage, r as HandshakeResponseMessage, I as InvalidScopeError, O as OperationBatchMessage, s as SYNC_STATES, t as SYNC_STATUSES, u as ScopeViolationError, v as SyncEncryptionAlgorithm, w as SyncScopeContext, x as SyncStatus, y as dedupeQuerySubsets, z as isAcknowledgmentMessage, C as isAwarenessUpdateMessage, F as isBlobChunkPushMessage, G as isBlobChunkRequestMessage, J as isBlobChunkResponseMessage, K as isErrorMessage, L as isHandshakeMessage, M as isHandshakeResponseMessage, N as isOperationBatchMessage, P as isSyncMessage, R as isYjsDocUpdateMessage, U as operationMatchesQuerySubsets } from './transport-CZLMdZ0k.js';
1
+ import { S as SyncScopeMap, D as DeltaCursor, a as SyncMessage, b as SerializedOperation, W as WireFormat, c as SyncTransport, T as TransportOptions, d as TransportMessageHandler, e as TransportCloseHandler, f as TransportErrorHandler, B as BlobChunkRequestMessage, g as BlobChunkResponseMessage, h as BlobChunkPushMessage, i as SyncEncryptionConfig, V as VersionedKey, Y as YjsDocUpdateMessage, Q as QueueStorage, j as SyncState, k as SyncStatusInfo, l as SyncConfig, R as RejectedOperationStorage, m as SyncStatePersistence, n as SyncQuerySubset, o as RejectedOperation } from './transport-BGr-_eH8.js';
2
+ export { A as AcknowledgmentMessage, p as AwarenessStateWire, q as AwarenessUpdateMessage, E as EncryptedPayload, r as ErrorMessage, H as HandshakeMessage, s as HandshakeResponseMessage, I as InvalidScopeError, M as MemoryRejectedOperationStorage, O as OperationBatchMessage, t as OperationRejectedMessage, u as SYNC_STATES, v as SYNC_STATUSES, w as ScopeViolationError, x as SyncEncryptionAlgorithm, y as SyncScopeContext, z as SyncStatus, C as dedupeQuerySubsets, F as isAcknowledgmentMessage, G as isAwarenessUpdateMessage, J as isBlobChunkPushMessage, K as isBlobChunkRequestMessage, L as isBlobChunkResponseMessage, N as isErrorMessage, P as isHandshakeMessage, U as isHandshakeResponseMessage, X as isOperationBatchMessage, Z as isOperationRejectedMessage, _ as isSyncMessage, $ as isYjsDocUpdateMessage, a0 as operationMatchesQuerySubsets } from './transport-BGr-_eH8.js';
3
3
  import * as _korajs_core from '@korajs/core';
4
4
  import { Operation, VersionVector, ApplyResult, KoraEventEmitter, TimeSource, SyncError, ConnectionQuality } from '@korajs/core';
5
5
  export { ApplyFailureReason, ApplyResult } from '@korajs/core';
@@ -844,6 +844,14 @@ declare class OutboundQueue {
844
844
  * Whether initialize() has been called.
845
845
  */
846
846
  get isInitialized(): boolean;
847
+ /**
848
+ * Remove a single operation the server permanently rejected — from the queue,
849
+ * from any in-flight batch, and from durable storage — so a later batch ack or
850
+ * a reconnect `returnBatch` can never resend or resurrect it. Returns the
851
+ * removed operation so the caller can record it in a durable rejected store,
852
+ * or null if it was not present (already acknowledged and removed).
853
+ */
854
+ reject(opId: string): Promise<Operation | null>;
847
855
  /**
848
856
  * Remove operations by id from queue and persistent storage.
849
857
  * Used when ops were already sent during handshake delta exchange.
@@ -867,6 +875,11 @@ interface SyncEngineOptions {
867
875
  emitter?: KoraEventEmitter;
868
876
  /** Queue storage for persistent outbound queue. Defaults to in-memory. */
869
877
  queueStorage?: QueueStorage;
878
+ /**
879
+ * Durable storage for operations the server rejected. Defaults to in-memory.
880
+ * Provide a store-backed implementation so rejections survive a page refresh.
881
+ */
882
+ rejectedStorage?: RejectedOperationStorage;
870
883
  /**
871
884
  * Optional encryptor for end-to-end encryption.
872
885
  * When provided, `data` and `previousData` fields of operations are encrypted
@@ -911,6 +924,7 @@ declare class SyncEngine {
911
924
  private readonly serializer;
912
925
  private readonly emitter;
913
926
  private readonly outboundQueue;
927
+ private readonly rejectedStorage;
914
928
  private readonly batchSize;
915
929
  private readonly encryptor;
916
930
  private readonly awarenessManager;
@@ -1107,6 +1121,25 @@ declare class SyncEngine {
1107
1121
  private handleOperationBatch;
1108
1122
  private handleAcknowledgment;
1109
1123
  private handleError;
1124
+ /**
1125
+ * Handle a per-operation rejection: divert the op out of the pending outbound
1126
+ * queue (so it is never retried or resurrected on reconnect), record it in the
1127
+ * durable rejected store (so it is kept and explainable), and emit an event so
1128
+ * the app can reconcile. Unlike {@link handleError}, this is a normal per-op
1129
+ * signal, so the connection stays up.
1130
+ */
1131
+ private handleOperationRejected;
1132
+ /**
1133
+ * Every operation the server rejected that has not yet been reconciled. The app
1134
+ * uses this (alongside the `sync:operation-rejected` event) to surface failed
1135
+ * submissions and decide whether to roll back or resubmit.
1136
+ */
1137
+ getRejectedOperations(): Promise<RejectedOperation[]>;
1138
+ /**
1139
+ * Forget rejected operations by id once the app has reconciled them (rolled the
1140
+ * optimistic write back or resubmitted a corrected op).
1141
+ */
1142
+ clearRejectedOperations(operationIds: string[]): Promise<void>;
1110
1143
  private checkDeltaComplete;
1111
1144
  /**
1112
1145
  * Effective server vector: persisted last-ack merged with live handshake remote vector.
@@ -1375,4 +1408,4 @@ declare function getRemoteAwarenessStates(awareness: AwarenessManager): Awarenes
1375
1408
  */
1376
1409
  declare function subscribeRemoteAwarenessStates(awareness: AwarenessManager, listener: (states: AwarenessState[]) => void): () => void;
1377
1410
 
1378
- export { type AwarenessChange, type AwarenessCursor, AwarenessManager, type AwarenessMessage, type AwarenessState, type AwarenessUser, BlobChunkChannel, type BlobChunkChannelMessage, type BlobChunkChannelOptions, BlobChunkPushMessage, BlobChunkRequestMessage, BlobChunkResponseMessage, type ChaosConfig, ChaosTransport, ConnectionMonitor, type ConnectionMonitorConfig, type CursorInfo, DEFAULT_RICHTEXT_DOC_CHANNEL_THRESHOLD, DecryptionError, DeltaCursor, EncryptionError, HttpLongPollingTransport, type HttpLongPollingTransportOptions, JsonMessageSerializer, KeyDerivationError, type MessageSerializer, NegotiatedMessageSerializer, OFFLINE_SYNC_STATUS, type OutboundBatch, OutboundQueue, ProtobufMessageSerializer, QueueStorage, type ReconnectionConfig, ReconnectionManager, RichtextDocChannel, type RichtextDocChannelOptions, type RichtextDocListener, SCHEMA_MISMATCH_PREFIX, SerializedOperation, SyncConfig, type SyncDiagnostics, SyncEncryptionConfig, SyncEncryptor, SyncEngine, type SyncEngineOptions, SyncMessage, SyncQuerySubset, SyncScopeMap, SyncState, SyncStatePersistence, type SyncStatusController, type SyncStatusControllerOptions, SyncStatusInfo, type SyncStore, SyncTransport, TransportCloseHandler, TransportErrorHandler, TransportMessageHandler, TransportOptions, VersionedKey, type WebSocketConstructor, type WebSocketLike, WebSocketTransport, type WebSocketTransportOptions, WireFormat, YjsDocUpdateMessage, createDeltaCursorFromBatch, createSyncStatusController, decodeBlobChunkBytes, decodeDeltaCursor, decodeYjsUpdate, deriveKey, deriveVersionedKey, encodeBlobChunkBytes, encodeDeltaCursor, encodeYjsUpdate, filterOperationsByScope, generateSalt, getRemoteAwarenessStates, isClientSchemaVersionSupported, isEncryptedPayload, isSchemaMismatchReject, operationMatchesScope, richtextDocKey, sliceOperationsAfterCursor, subscribeRemoteAwarenessStates, versionVectorToWire, wireToVersionVector };
1411
+ export { type AwarenessChange, type AwarenessCursor, AwarenessManager, type AwarenessMessage, type AwarenessState, type AwarenessUser, BlobChunkChannel, type BlobChunkChannelMessage, type BlobChunkChannelOptions, BlobChunkPushMessage, BlobChunkRequestMessage, BlobChunkResponseMessage, type ChaosConfig, ChaosTransport, ConnectionMonitor, type ConnectionMonitorConfig, type CursorInfo, DEFAULT_RICHTEXT_DOC_CHANNEL_THRESHOLD, DecryptionError, DeltaCursor, EncryptionError, HttpLongPollingTransport, type HttpLongPollingTransportOptions, JsonMessageSerializer, KeyDerivationError, type MessageSerializer, NegotiatedMessageSerializer, OFFLINE_SYNC_STATUS, type OutboundBatch, OutboundQueue, ProtobufMessageSerializer, QueueStorage, type ReconnectionConfig, ReconnectionManager, RejectedOperation, RejectedOperationStorage, RichtextDocChannel, type RichtextDocChannelOptions, type RichtextDocListener, SCHEMA_MISMATCH_PREFIX, SerializedOperation, SyncConfig, type SyncDiagnostics, SyncEncryptionConfig, SyncEncryptor, SyncEngine, type SyncEngineOptions, SyncMessage, SyncQuerySubset, SyncScopeMap, SyncState, SyncStatePersistence, type SyncStatusController, type SyncStatusControllerOptions, SyncStatusInfo, type SyncStore, SyncTransport, TransportCloseHandler, TransportErrorHandler, TransportMessageHandler, TransportOptions, VersionedKey, type WebSocketConstructor, type WebSocketLike, WebSocketTransport, type WebSocketTransportOptions, WireFormat, YjsDocUpdateMessage, createDeltaCursorFromBatch, createSyncStatusController, decodeBlobChunkBytes, decodeDeltaCursor, decodeYjsUpdate, deriveKey, deriveVersionedKey, encodeBlobChunkBytes, encodeDeltaCursor, encodeYjsUpdate, filterOperationsByScope, generateSalt, getRemoteAwarenessStates, isClientSchemaVersionSupported, isEncryptedPayload, isSchemaMismatchReject, operationMatchesScope, richtextDocKey, sliceOperationsAfterCursor, subscribeRemoteAwarenessStates, versionVectorToWire, wireToVersionVector };
package/dist/index.js CHANGED
@@ -21,6 +21,20 @@ var SYNC_STATUSES = [
21
21
  "error",
22
22
  "schema-mismatch"
23
23
  ];
24
+ var MemoryRejectedOperationStorage = class {
25
+ rejected = /* @__PURE__ */ new Map();
26
+ async record(rejected) {
27
+ this.rejected.set(rejected.operationId, rejected);
28
+ }
29
+ async list() {
30
+ return [...this.rejected.values()];
31
+ }
32
+ async remove(operationIds) {
33
+ for (const id of operationIds) {
34
+ this.rejected.delete(id);
35
+ }
36
+ }
37
+ };
24
38
  var ScopeViolationError = class extends KoraError {
25
39
  constructor(operationId, collection, scope, message) {
26
40
  super(
@@ -218,6 +232,8 @@ function isSyncMessage(value) {
218
232
  return isAcknowledgmentMessage(value);
219
233
  case "error":
220
234
  return isErrorMessage(value);
235
+ case "operation-rejected":
236
+ return isOperationRejectedMessage(value);
221
237
  case "awareness-update":
222
238
  return isAwarenessUpdateMessage(value);
223
239
  case "yjs-doc-update":
@@ -257,6 +273,11 @@ function isErrorMessage(value) {
257
273
  const msg = value;
258
274
  return msg.type === "error" && typeof msg.messageId === "string" && typeof msg.code === "string" && typeof msg.message === "string" && typeof msg.retriable === "boolean";
259
275
  }
276
+ function isOperationRejectedMessage(value) {
277
+ if (typeof value !== "object" || value === null) return false;
278
+ const msg = value;
279
+ return msg.type === "operation-rejected" && typeof msg.messageId === "string" && typeof msg.operationId === "string" && typeof msg.collection === "string" && typeof msg.recordId === "string" && typeof msg.code === "string" && typeof msg.message === "string" && typeof msg.retriable === "boolean";
280
+ }
260
281
  function isAwarenessUpdateMessage(value) {
261
282
  if (typeof value !== "object" || value === null) return false;
262
283
  const msg = value;
@@ -548,6 +569,17 @@ function toProtoEnvelope(message) {
548
569
  errorMessage: message.message,
549
570
  retriable: message.retriable
550
571
  };
572
+ case "operation-rejected":
573
+ return {
574
+ type: message.type,
575
+ messageId: message.messageId,
576
+ operationId: message.operationId,
577
+ collection: message.collection,
578
+ recordId: message.recordId,
579
+ errorCode: message.code,
580
+ errorMessage: message.message,
581
+ retriable: message.retriable
582
+ };
551
583
  case "awareness-update":
552
584
  case "yjs-doc-update":
553
585
  return {
@@ -634,6 +666,17 @@ function fromProtoEnvelope(envelope) {
634
666
  message: envelope.errorMessage ?? "Unknown error",
635
667
  retriable: envelope.retriable ?? false
636
668
  };
669
+ case "operation-rejected":
670
+ return {
671
+ type: "operation-rejected",
672
+ messageId: envelope.messageId,
673
+ operationId: envelope.operationId ?? "",
674
+ collection: envelope.collection ?? "",
675
+ recordId: envelope.recordId ?? "",
676
+ code: envelope.errorCode ?? "REJECTED",
677
+ message: envelope.errorMessage ?? "",
678
+ retriable: envelope.retriable ?? false
679
+ };
637
680
  case "blob-chunk-request":
638
681
  return {
639
682
  type: "blob-chunk-request",
@@ -808,6 +851,12 @@ function encodeEnvelope(envelope) {
808
851
  if (envelope.hasBytes !== void 0) writer.uint32(184).bool(envelope.hasBytes);
809
852
  if (envelope.blobStorageEnabled !== void 0)
810
853
  writer.uint32(200).bool(envelope.blobStorageEnabled);
854
+ if (envelope.operationId && envelope.operationId.length > 0)
855
+ writer.uint32(210).string(envelope.operationId);
856
+ if (envelope.collection && envelope.collection.length > 0)
857
+ writer.uint32(218).string(envelope.collection);
858
+ if (envelope.recordId && envelope.recordId.length > 0)
859
+ writer.uint32(226).string(envelope.recordId);
811
860
  return writer.finish();
812
861
  }
813
862
  function decodeEnvelope(bytes) {
@@ -894,6 +943,15 @@ function decodeEnvelope(bytes) {
894
943
  case 25:
895
944
  envelope.blobStorageEnabled = reader.bool();
896
945
  break;
946
+ case 26:
947
+ envelope.operationId = reader.string();
948
+ break;
949
+ case 27:
950
+ envelope.collection = reader.string();
951
+ break;
952
+ case 28:
953
+ envelope.recordId = reader.string();
954
+ break;
897
955
  default:
898
956
  reader.skipType(tag & 7);
899
957
  }
@@ -2482,6 +2540,37 @@ var OutboundQueue = class {
2482
2540
  get isInitialized() {
2483
2541
  return this.initialized;
2484
2542
  }
2543
+ /**
2544
+ * Remove a single operation the server permanently rejected — from the queue,
2545
+ * from any in-flight batch, and from durable storage — so a later batch ack or
2546
+ * a reconnect `returnBatch` can never resend or resurrect it. Returns the
2547
+ * removed operation so the caller can record it in a durable rejected store,
2548
+ * or null if it was not present (already acknowledged and removed).
2549
+ */
2550
+ async reject(opId) {
2551
+ let removed = null;
2552
+ for (const [batchId, ops] of this.inFlight) {
2553
+ const idx = ops.findIndex((op) => op.id === opId);
2554
+ if (idx !== -1) {
2555
+ removed = ops[idx] ?? null;
2556
+ ops.splice(idx, 1);
2557
+ if (ops.length === 0) {
2558
+ this.inFlight.delete(batchId);
2559
+ }
2560
+ break;
2561
+ }
2562
+ }
2563
+ if (!removed) {
2564
+ const idx = this.queue.findIndex((op) => op.id === opId);
2565
+ if (idx !== -1) {
2566
+ removed = this.queue[idx] ?? null;
2567
+ this.queue.splice(idx, 1);
2568
+ }
2569
+ }
2570
+ this.seen.delete(opId);
2571
+ await this.storage.dequeue([opId]);
2572
+ return removed;
2573
+ }
2485
2574
  /**
2486
2575
  * Remove operations by id from queue and persistent storage.
2487
2576
  * Used when ops were already sent during handshake delta exchange.
@@ -2521,6 +2610,7 @@ var SyncEngine = class {
2521
2610
  serializer;
2522
2611
  emitter;
2523
2612
  outboundQueue;
2613
+ rejectedStorage;
2524
2614
  batchSize;
2525
2615
  encryptor;
2526
2616
  awarenessManager;
@@ -2573,6 +2663,7 @@ var SyncEngine = class {
2573
2663
  this.activeScope = options.config.scopeMap;
2574
2664
  const queueStorage = options.queueStorage ?? new MemoryQueueStorage();
2575
2665
  this.outboundQueue = new OutboundQueue(queueStorage);
2666
+ this.rejectedStorage = options.rejectedStorage ?? new MemoryRejectedOperationStorage();
2576
2667
  this.metricsCollector = new SyncMetricsCollector(options.metricsConfig);
2577
2668
  if (this.emitter) {
2578
2669
  this.metricsCollector.attachEmitter(this.emitter);
@@ -2954,6 +3045,9 @@ var SyncEngine = class {
2954
3045
  case "error":
2955
3046
  this.handleError(message);
2956
3047
  break;
3048
+ case "operation-rejected":
3049
+ await this.handleOperationRejected(message);
3050
+ break;
2957
3051
  case "awareness-update":
2958
3052
  this.handleAwarenessUpdate(message);
2959
3053
  break;
@@ -3291,6 +3385,50 @@ var SyncEngine = class {
3291
3385
  this.emitter?.emit({ type: "sync:disconnected", reason: msg.message });
3292
3386
  this.transitionTo("disconnected");
3293
3387
  }
3388
+ /**
3389
+ * Handle a per-operation rejection: divert the op out of the pending outbound
3390
+ * queue (so it is never retried or resurrected on reconnect), record it in the
3391
+ * durable rejected store (so it is kept and explainable), and emit an event so
3392
+ * the app can reconcile. Unlike {@link handleError}, this is a normal per-op
3393
+ * signal, so the connection stays up.
3394
+ */
3395
+ async handleOperationRejected(msg) {
3396
+ await this.outboundQueue.reject(msg.operationId);
3397
+ await this.rejectedStorage.record({
3398
+ operationId: msg.operationId,
3399
+ collection: msg.collection,
3400
+ recordId: msg.recordId,
3401
+ code: msg.code,
3402
+ message: msg.message,
3403
+ retriable: msg.retriable,
3404
+ rejectedAt: Date.now()
3405
+ });
3406
+ await this.refreshPendingCount();
3407
+ this.emitter?.emit({
3408
+ type: "sync:operation-rejected",
3409
+ operationId: msg.operationId,
3410
+ collection: msg.collection,
3411
+ recordId: msg.recordId,
3412
+ code: msg.code,
3413
+ message: msg.message,
3414
+ retriable: msg.retriable
3415
+ });
3416
+ }
3417
+ /**
3418
+ * Every operation the server rejected that has not yet been reconciled. The app
3419
+ * uses this (alongside the `sync:operation-rejected` event) to surface failed
3420
+ * submissions and decide whether to roll back or resubmit.
3421
+ */
3422
+ getRejectedOperations() {
3423
+ return this.rejectedStorage.list();
3424
+ }
3425
+ /**
3426
+ * Forget rejected operations by id once the app has reconciled them (rolled the
3427
+ * optimistic write back or resubmitted a corrected op).
3428
+ */
3429
+ clearRejectedOperations(operationIds) {
3430
+ return this.rejectedStorage.remove(operationIds);
3431
+ }
3294
3432
  async checkDeltaComplete() {
3295
3433
  if (!this.deltaSendComplete || !this.deltaReceiveComplete) {
3296
3434
  return;
@@ -4355,6 +4493,7 @@ export {
4355
4493
  InvalidScopeError,
4356
4494
  JsonMessageSerializer,
4357
4495
  KeyDerivationError,
4496
+ MemoryRejectedOperationStorage,
4358
4497
  NegotiatedMessageSerializer,
4359
4498
  OFFLINE_SYNC_STATUS,
4360
4499
  OutboundQueue,
@@ -4393,6 +4532,7 @@ export {
4393
4532
  isHandshakeMessage,
4394
4533
  isHandshakeResponseMessage,
4395
4534
  isOperationBatchMessage,
4535
+ isOperationRejectedMessage,
4396
4536
  isSchemaMismatchReject,
4397
4537
  isSyncMessage,
4398
4538
  isYjsDocUpdateMessage,