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

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,11 +80,13 @@ __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,
85
87
  operationMatchesQuerySubsets: () => operationMatchesQuerySubsets,
86
88
  operationMatchesScope: () => operationMatchesScope,
89
+ querySubsetContains: () => querySubsetContains,
87
90
  richtextDocKey: () => richtextDocKey,
88
91
  sliceOperationsAfterCursor: () => sliceOperationsAfterCursor,
89
92
  subscribeRemoteAwarenessStates: () => subscribeRemoteAwarenessStates,
@@ -104,13 +107,29 @@ var SYNC_STATES = [
104
107
  ];
105
108
  var SYNC_STATUSES = [
106
109
  "connected",
110
+ "reconnecting",
107
111
  "syncing",
108
112
  "synced",
109
113
  "offline",
110
114
  "clock-error",
111
115
  "error",
112
- "schema-mismatch"
116
+ "schema-mismatch",
117
+ "auth-required"
113
118
  ];
119
+ var MemoryRejectedOperationStorage = class {
120
+ rejected = /* @__PURE__ */ new Map();
121
+ async record(rejected) {
122
+ this.rejected.set(rejected.operationId, rejected);
123
+ }
124
+ async list() {
125
+ return [...this.rejected.values()];
126
+ }
127
+ async remove(operationIds) {
128
+ for (const id of operationIds) {
129
+ this.rejected.delete(id);
130
+ }
131
+ }
132
+ };
114
133
  var ScopeViolationError = class extends import_core.KoraError {
115
134
  constructor(operationId, collection, scope, message) {
116
135
  super(
@@ -143,12 +162,19 @@ function operationMatchesScope(op, scopeMap, fullRecord) {
143
162
  const snapshot = buildSnapshot(op, fullRecord ?? void 0);
144
163
  if (!snapshot) return false;
145
164
  for (const [field, expected] of Object.entries(collectionScope)) {
146
- if (snapshot[field] !== expected) {
165
+ if (!matchesPredicate(snapshot[field], expected)) {
147
166
  return false;
148
167
  }
149
168
  }
150
169
  return true;
151
170
  }
171
+ function matchesPredicate(actual, expected) {
172
+ if (expected && typeof expected === "object" && !Array.isArray(expected) && "$in" in expected) {
173
+ const values = expected.$in;
174
+ return Array.isArray(values) && values.some((value) => Object.is(actual, value));
175
+ }
176
+ return Object.is(actual, expected);
177
+ }
152
178
  function filterOperationsByScope(operations, scopeMap) {
153
179
  if (!scopeMap) return operations;
154
180
  return operations.filter((op) => operationMatchesScope(op, scopeMap));
@@ -205,24 +231,77 @@ function operationMatchesQuerySubsets(op, subsets, fullRecord) {
205
231
  if (collectionSubsets.length === 0) {
206
232
  return true;
207
233
  }
234
+ if (collectionSubsets.some((subset) => Object.keys(subset.where).length === 0)) {
235
+ return true;
236
+ }
208
237
  const snapshot = buildSnapshot2(op, fullRecord);
209
238
  if (!snapshot) {
210
239
  return false;
211
240
  }
212
241
  return collectionSubsets.some((subset) => recordMatchesWhere(snapshot, subset.where));
213
242
  }
243
+ function stableValue(value) {
244
+ if (Array.isArray(value)) return value.map(stableValue);
245
+ if (value && typeof value === "object") {
246
+ return Object.fromEntries(
247
+ Object.entries(value).filter(([, entry]) => entry !== void 0).sort(([a], [b]) => a.localeCompare(b)).map(([key, entry]) => [key, stableValue(entry)])
248
+ );
249
+ }
250
+ return value;
251
+ }
252
+ function whereContains(broad, narrow) {
253
+ return Object.entries(broad).every(
254
+ ([field, expected]) => JSON.stringify(stableValue(narrow[field])) === JSON.stringify(stableValue(expected))
255
+ );
256
+ }
214
257
  function dedupeQuerySubsets(subsets) {
215
- const seen = /* @__PURE__ */ new Set();
258
+ const normalized = subsets.map((subset) => ({
259
+ collection: subset.collection,
260
+ where: stableValue(subset.where)
261
+ }));
216
262
  const result = [];
217
- for (const subset of subsets) {
218
- const key = `${subset.collection}:${JSON.stringify(subset.where)}`;
219
- if (seen.has(key)) {
263
+ for (const candidate of normalized) {
264
+ if (normalized.some(
265
+ (other) => other !== candidate && other.collection === candidate.collection && whereContains(other.where, candidate.where) && (Object.keys(other.where).length < Object.keys(candidate.where).length || JSON.stringify(other.where) === JSON.stringify(candidate.where))
266
+ )) {
267
+ const duplicateAlreadyKept = result.some(
268
+ (item) => item.collection === candidate.collection && JSON.stringify(item.where) === JSON.stringify(candidate.where)
269
+ );
270
+ if (duplicateAlreadyKept || normalized.some(
271
+ (other) => other !== candidate && other.collection === candidate.collection && Object.keys(other.where).length < Object.keys(candidate.where).length && whereContains(other.where, candidate.where)
272
+ ))
273
+ continue;
274
+ }
275
+ if (!result.some(
276
+ (item) => item.collection === candidate.collection && JSON.stringify(item.where) === JSON.stringify(candidate.where)
277
+ ))
278
+ result.push(candidate);
279
+ }
280
+ return result.sort(
281
+ (a, b) => `${a.collection}:${JSON.stringify(a.where)}`.localeCompare(
282
+ `${b.collection}:${JSON.stringify(b.where)}`
283
+ )
284
+ );
285
+ }
286
+ function querySubsetContains(broad, narrow) {
287
+ const collections = /* @__PURE__ */ new Set([
288
+ ...broad.map((subset) => subset.collection),
289
+ ...narrow.map((subset) => subset.collection)
290
+ ]);
291
+ for (const collection of collections) {
292
+ const broadForCollection = broad.filter((subset) => subset.collection === collection);
293
+ const narrowForCollection = narrow.filter((subset) => subset.collection === collection);
294
+ if (narrowForCollection.length === 0) {
295
+ if (broadForCollection.length > 0) return false;
220
296
  continue;
221
297
  }
222
- seen.add(key);
223
- result.push(subset);
298
+ if (broadForCollection.length === 0) continue;
299
+ if (!narrowForCollection.every(
300
+ (candidate) => broadForCollection.some((other) => whereContains(other.where, candidate.where))
301
+ ))
302
+ return false;
224
303
  }
225
- return result;
304
+ return true;
226
305
  }
227
306
 
228
307
  // src/delta/delta-cursor.ts
@@ -308,6 +387,8 @@ function isSyncMessage(value) {
308
387
  return isAcknowledgmentMessage(value);
309
388
  case "error":
310
389
  return isErrorMessage(value);
390
+ case "operation-rejected":
391
+ return isOperationRejectedMessage(value);
311
392
  case "awareness-update":
312
393
  return isAwarenessUpdateMessage(value);
313
394
  case "yjs-doc-update":
@@ -347,6 +428,11 @@ function isErrorMessage(value) {
347
428
  const msg = value;
348
429
  return msg.type === "error" && typeof msg.messageId === "string" && typeof msg.code === "string" && typeof msg.message === "string" && typeof msg.retriable === "boolean";
349
430
  }
431
+ function isOperationRejectedMessage(value) {
432
+ if (typeof value !== "object" || value === null) return false;
433
+ const msg = value;
434
+ 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";
435
+ }
350
436
  function isAwarenessUpdateMessage(value) {
351
437
  if (typeof value !== "object" || value === null) return false;
352
438
  const msg = value;
@@ -597,7 +683,8 @@ function toProtoEnvelope(message) {
597
683
  })),
598
684
  schemaVersion: message.schemaVersion,
599
685
  authToken: message.authToken,
600
- supportedWireFormats: message.supportedWireFormats
686
+ supportedWireFormats: message.supportedWireFormats,
687
+ ...message.lastDeliverySequence !== void 0 ? { lastDeliverySequence: message.lastDeliverySequence } : {}
601
688
  };
602
689
  case "handshake-response":
603
690
  return {
@@ -613,7 +700,8 @@ function toProtoEnvelope(message) {
613
700
  rejectReason: message.rejectReason,
614
701
  selectedWireFormat: message.selectedWireFormat,
615
702
  serverTime: message.serverTime,
616
- blobStorageEnabled: message.blobStorageEnabled
703
+ blobStorageEnabled: message.blobStorageEnabled,
704
+ ...message.serverMaxDeliverySequence !== void 0 ? { serverMaxDeliverySequence: message.serverMaxDeliverySequence } : {}
617
705
  };
618
706
  case "operation-batch":
619
707
  return {
@@ -621,14 +709,17 @@ function toProtoEnvelope(message) {
621
709
  messageId: message.messageId,
622
710
  operations: message.operations.map(serializeProtoOperation),
623
711
  isFinal: message.isFinal,
624
- batchIndex: message.batchIndex
712
+ batchIndex: message.batchIndex,
713
+ ...message.baseDeliverySequence !== void 0 ? { baseDeliverySequence: message.baseDeliverySequence } : {},
714
+ ...message.maxDeliverySequence !== void 0 ? { maxDeliverySequence: message.maxDeliverySequence } : {}
625
715
  };
626
716
  case "acknowledgment":
627
717
  return {
628
718
  type: message.type,
629
719
  messageId: message.messageId,
630
720
  acknowledgedMessageId: message.acknowledgedMessageId,
631
- lastSequenceNumber: message.lastSequenceNumber
721
+ lastSequenceNumber: message.lastSequenceNumber,
722
+ ...message.deliverySequence !== void 0 ? { deliverySequence: message.deliverySequence } : {}
632
723
  };
633
724
  case "error":
634
725
  return {
@@ -638,6 +729,17 @@ function toProtoEnvelope(message) {
638
729
  errorMessage: message.message,
639
730
  retriable: message.retriable
640
731
  };
732
+ case "operation-rejected":
733
+ return {
734
+ type: message.type,
735
+ messageId: message.messageId,
736
+ operationId: message.operationId,
737
+ collection: message.collection,
738
+ recordId: message.recordId,
739
+ errorCode: message.code,
740
+ errorMessage: message.message,
741
+ retriable: message.retriable
742
+ };
641
743
  case "awareness-update":
642
744
  case "yjs-doc-update":
643
745
  return {
@@ -682,7 +784,8 @@ function fromProtoEnvelope(envelope) {
682
784
  authToken: envelope.authToken,
683
785
  supportedWireFormats: envelope.supportedWireFormats?.filter(
684
786
  (format) => format === "json" || format === "protobuf"
685
- )
787
+ ),
788
+ ...envelope.lastDeliverySequence !== void 0 ? { lastDeliverySequence: envelope.lastDeliverySequence } : {}
686
789
  };
687
790
  case "handshake-response":
688
791
  return {
@@ -699,7 +802,8 @@ function fromProtoEnvelope(envelope) {
699
802
  // Preserve the server's wall-clock time so clock-skew detection and
700
803
  // automatic timestamp rebase work over the protobuf wire, not just JSON.
701
804
  ...envelope.serverTime !== void 0 ? { serverTime: envelope.serverTime } : {},
702
- ...envelope.blobStorageEnabled !== void 0 ? { blobStorageEnabled: envelope.blobStorageEnabled } : {}
805
+ ...envelope.blobStorageEnabled !== void 0 ? { blobStorageEnabled: envelope.blobStorageEnabled } : {},
806
+ ...envelope.serverMaxDeliverySequence !== void 0 ? { serverMaxDeliverySequence: envelope.serverMaxDeliverySequence } : {}
703
807
  };
704
808
  case "operation-batch":
705
809
  return {
@@ -707,14 +811,17 @@ function fromProtoEnvelope(envelope) {
707
811
  messageId: envelope.messageId,
708
812
  operations: (envelope.operations ?? []).map(deserializeProtoOperation),
709
813
  isFinal: envelope.isFinal ?? false,
710
- batchIndex: envelope.batchIndex ?? 0
814
+ batchIndex: envelope.batchIndex ?? 0,
815
+ ...envelope.baseDeliverySequence !== void 0 ? { baseDeliverySequence: envelope.baseDeliverySequence } : {},
816
+ ...envelope.maxDeliverySequence !== void 0 ? { maxDeliverySequence: envelope.maxDeliverySequence } : {}
711
817
  };
712
818
  case "acknowledgment":
713
819
  return {
714
820
  type: "acknowledgment",
715
821
  messageId: envelope.messageId,
716
822
  acknowledgedMessageId: envelope.acknowledgedMessageId ?? "",
717
- lastSequenceNumber: envelope.lastSequenceNumber ?? 0
823
+ lastSequenceNumber: envelope.lastSequenceNumber ?? 0,
824
+ ...envelope.deliverySequence !== void 0 ? { deliverySequence: envelope.deliverySequence } : {}
718
825
  };
719
826
  case "error":
720
827
  return {
@@ -724,6 +831,17 @@ function fromProtoEnvelope(envelope) {
724
831
  message: envelope.errorMessage ?? "Unknown error",
725
832
  retriable: envelope.retriable ?? false
726
833
  };
834
+ case "operation-rejected":
835
+ return {
836
+ type: "operation-rejected",
837
+ messageId: envelope.messageId,
838
+ operationId: envelope.operationId ?? "",
839
+ collection: envelope.collection ?? "",
840
+ recordId: envelope.recordId ?? "",
841
+ code: envelope.errorCode ?? "REJECTED",
842
+ message: envelope.errorMessage ?? "",
843
+ retriable: envelope.retriable ?? false
844
+ };
727
845
  case "blob-chunk-request":
728
846
  return {
729
847
  type: "blob-chunk-request",
@@ -898,6 +1016,21 @@ function encodeEnvelope(envelope) {
898
1016
  if (envelope.hasBytes !== void 0) writer.uint32(184).bool(envelope.hasBytes);
899
1017
  if (envelope.blobStorageEnabled !== void 0)
900
1018
  writer.uint32(200).bool(envelope.blobStorageEnabled);
1019
+ if (envelope.operationId && envelope.operationId.length > 0)
1020
+ writer.uint32(210).string(envelope.operationId);
1021
+ if (envelope.collection && envelope.collection.length > 0)
1022
+ writer.uint32(218).string(envelope.collection);
1023
+ if (envelope.recordId && envelope.recordId.length > 0)
1024
+ writer.uint32(226).string(envelope.recordId);
1025
+ if (envelope.lastDeliverySequence !== void 0)
1026
+ writer.uint32(232).int64(envelope.lastDeliverySequence);
1027
+ if (envelope.baseDeliverySequence !== void 0)
1028
+ writer.uint32(240).int64(envelope.baseDeliverySequence);
1029
+ if (envelope.maxDeliverySequence !== void 0)
1030
+ writer.uint32(248).int64(envelope.maxDeliverySequence);
1031
+ if (envelope.deliverySequence !== void 0) writer.uint32(256).int64(envelope.deliverySequence);
1032
+ if (envelope.serverMaxDeliverySequence !== void 0)
1033
+ writer.uint32(264).int64(envelope.serverMaxDeliverySequence);
901
1034
  return writer.finish();
902
1035
  }
903
1036
  function decodeEnvelope(bytes) {
@@ -984,6 +1117,30 @@ function decodeEnvelope(bytes) {
984
1117
  case 25:
985
1118
  envelope.blobStorageEnabled = reader.bool();
986
1119
  break;
1120
+ case 26:
1121
+ envelope.operationId = reader.string();
1122
+ break;
1123
+ case 27:
1124
+ envelope.collection = reader.string();
1125
+ break;
1126
+ case 28:
1127
+ envelope.recordId = reader.string();
1128
+ break;
1129
+ case 29:
1130
+ envelope.lastDeliverySequence = longToNumber(reader.int64());
1131
+ break;
1132
+ case 30:
1133
+ envelope.baseDeliverySequence = longToNumber(reader.int64());
1134
+ break;
1135
+ case 31:
1136
+ envelope.maxDeliverySequence = longToNumber(reader.int64());
1137
+ break;
1138
+ case 32:
1139
+ envelope.deliverySequence = longToNumber(reader.int64());
1140
+ break;
1141
+ case 33:
1142
+ envelope.serverMaxDeliverySequence = longToNumber(reader.int64());
1143
+ break;
987
1144
  default:
988
1145
  reader.skipType(tag & 7);
989
1146
  }
@@ -1483,6 +1640,7 @@ var ChaosTransport = class {
1483
1640
  reorderRate;
1484
1641
  maxLatency;
1485
1642
  random;
1643
+ dropPredicate;
1486
1644
  messageHandler = null;
1487
1645
  reorderBuffer = [];
1488
1646
  pending = [];
@@ -1493,6 +1651,7 @@ var ChaosTransport = class {
1493
1651
  this.reorderRate = config?.reorderRate ?? 0;
1494
1652
  this.maxLatency = config?.maxLatency ?? 0;
1495
1653
  this.random = config?.randomSource ?? Math.random;
1654
+ this.dropPredicate = config?.dropPredicate ?? null;
1496
1655
  }
1497
1656
  async connect(url, options) {
1498
1657
  this.inner.onMessage((msg) => this.handleIncoming(msg));
@@ -1511,6 +1670,9 @@ var ChaosTransport = class {
1511
1670
  return this.inner.disconnect();
1512
1671
  }
1513
1672
  send(message) {
1673
+ if (this.dropPredicate?.(message, "outgoing")) {
1674
+ return;
1675
+ }
1514
1676
  if (this.random() < this.dropRate) {
1515
1677
  return;
1516
1678
  }
@@ -1538,6 +1700,9 @@ var ChaosTransport = class {
1538
1700
  }
1539
1701
  handleIncoming(message) {
1540
1702
  if (!this.messageHandler) return;
1703
+ if (this.dropPredicate?.(message, "incoming")) {
1704
+ return;
1705
+ }
1541
1706
  if (this.random() < this.dropRate) {
1542
1707
  return;
1543
1708
  }
@@ -2502,6 +2667,32 @@ var OutboundQueue = class {
2502
2667
  const ids = ops.map((op) => op.id);
2503
2668
  await this.storage.dequeue(ids);
2504
2669
  }
2670
+ /**
2671
+ * Acknowledge only operations in an in-flight batch whose local sequence number is
2672
+ * covered by the server's ack. Any later operations return to the front of the
2673
+ * queue for retry. This is required when the server accepts a prefix but rejects a
2674
+ * later operation retriably (for example a stale-scope push after auth changed).
2675
+ */
2676
+ async acknowledgeThrough(batchId, lastSequenceNumber) {
2677
+ const ops = this.inFlight.get(batchId);
2678
+ if (!ops) return;
2679
+ this.inFlight.delete(batchId);
2680
+ const acknowledged = ops.filter((op) => op.sequenceNumber <= lastSequenceNumber);
2681
+ const retry = ops.filter((op) => op.sequenceNumber > lastSequenceNumber);
2682
+ if (retry.length > 0) {
2683
+ this.queue.unshift(...retry);
2684
+ if (this.queue.length > 1) {
2685
+ this.queue = (0, import_internal.topologicalSort)(this.queue);
2686
+ }
2687
+ }
2688
+ if (acknowledged.length > 0) {
2689
+ const ids = acknowledged.map((op) => op.id);
2690
+ for (const id of ids) {
2691
+ this.seen.delete(id);
2692
+ }
2693
+ await this.storage.dequeue(ids);
2694
+ }
2695
+ }
2505
2696
  /**
2506
2697
  * Return a failed batch to the front of the queue for retry.
2507
2698
  * Prepends the operations to maintain priority.
@@ -2584,6 +2775,37 @@ var OutboundQueue = class {
2584
2775
  get isInitialized() {
2585
2776
  return this.initialized;
2586
2777
  }
2778
+ /**
2779
+ * Remove a single operation the server permanently rejected — from the queue,
2780
+ * from any in-flight batch, and from durable storage — so a later batch ack or
2781
+ * a reconnect `returnBatch` can never resend or resurrect it. Returns the
2782
+ * removed operation so the caller can record it in a durable rejected store,
2783
+ * or null if it was not present (already acknowledged and removed).
2784
+ */
2785
+ async reject(opId) {
2786
+ let removed = null;
2787
+ for (const [batchId, ops] of this.inFlight) {
2788
+ const idx = ops.findIndex((op) => op.id === opId);
2789
+ if (idx !== -1) {
2790
+ removed = ops[idx] ?? null;
2791
+ ops.splice(idx, 1);
2792
+ if (ops.length === 0) {
2793
+ this.inFlight.delete(batchId);
2794
+ }
2795
+ break;
2796
+ }
2797
+ }
2798
+ if (!removed) {
2799
+ const idx = this.queue.findIndex((op) => op.id === opId);
2800
+ if (idx !== -1) {
2801
+ removed = this.queue[idx] ?? null;
2802
+ this.queue.splice(idx, 1);
2803
+ }
2804
+ }
2805
+ this.seen.delete(opId);
2806
+ await this.storage.dequeue([opId]);
2807
+ return removed;
2808
+ }
2587
2809
  /**
2588
2810
  * Remove operations by id from queue and persistent storage.
2589
2811
  * Used when ops were already sent during handshake delta exchange.
@@ -2602,6 +2824,7 @@ var OutboundQueue = class {
2602
2824
  // src/engine/sync-engine.ts
2603
2825
  var DEFAULT_BATCH_SIZE = 100;
2604
2826
  var DEFAULT_SCHEMA_VERSION = 1;
2827
+ var DEFAULT_OUTBOUND_ACK_TIMEOUT_MS = 3e4;
2605
2828
  var VALID_TRANSITIONS = {
2606
2829
  disconnected: ["connecting"],
2607
2830
  connecting: ["handshaking", "error", "disconnected"],
@@ -2615,6 +2838,17 @@ var nextQuerySubsetId = 0;
2615
2838
  function generateMessageId() {
2616
2839
  return `msg-${Date.now()}-${nextMessageId++}`;
2617
2840
  }
2841
+ var MAX_DELIVERY_VIEW_WATERMARKS = 64;
2842
+ function stableStringify(value) {
2843
+ if (value === null || typeof value !== "object") {
2844
+ return JSON.stringify(value) ?? "null";
2845
+ }
2846
+ if (Array.isArray(value)) {
2847
+ return `[${value.map(stableStringify).join(",")}]`;
2848
+ }
2849
+ 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)}`);
2850
+ return `{${entries.join(",")}}`;
2851
+ }
2618
2852
  var SyncEngine = class {
2619
2853
  state = "disconnected";
2620
2854
  transport;
@@ -2623,7 +2857,9 @@ var SyncEngine = class {
2623
2857
  serializer;
2624
2858
  emitter;
2625
2859
  outboundQueue;
2860
+ rejectedStorage;
2626
2861
  batchSize;
2862
+ outboundAckTimeoutMs;
2627
2863
  encryptor;
2628
2864
  awarenessManager;
2629
2865
  richtextDocChannel;
@@ -2638,11 +2874,21 @@ var SyncEngine = class {
2638
2874
  lastSuccessfulPull = null;
2639
2875
  conflictCount = 0;
2640
2876
  currentBatch = null;
2877
+ currentBatchRequiresPartialAck = false;
2641
2878
  reconnecting = false;
2642
2879
  schemaBlocked = false;
2643
2880
  clockBlocked = false;
2644
2881
  clockSkewMs = null;
2645
2882
  blobStorageEnabled = false;
2883
+ suspensionReason = null;
2884
+ authRejected = false;
2885
+ serverFrontier = null;
2886
+ hasInFlightDeliveryBatch = false;
2887
+ blockedFailure = null;
2888
+ outboundAckTimer = null;
2889
+ startPromise = null;
2890
+ stopPromise = null;
2891
+ reconnectPromise = null;
2646
2892
  // Track delta exchange state
2647
2893
  deltaBatchesReceived = 0;
2648
2894
  deltaReceiveComplete = false;
@@ -2659,10 +2905,32 @@ var SyncEngine = class {
2659
2905
  activeScope;
2660
2906
  /** Live query subsets registered from reactive subscriptions */
2661
2907
  querySubsets = /* @__PURE__ */ new Map();
2908
+ staticQuerySubsets = [];
2662
2909
  querySubsetReconnectTimer = null;
2663
2910
  /** Resume cursor for paginated initial sync (persisted across reconnects) */
2664
2911
  resumeDeltaCursor = null;
2665
2912
  initialSyncTotalBatches = 0;
2913
+ /**
2914
+ * The delivery watermark for the CURRENT sync view: the highest server delivery
2915
+ * sequence up to which every operation in the current view (scope + active query
2916
+ * subscriptions) has been contiguously applied. Advanced only through the server's
2917
+ * gap-free delivery stream (never through live relay), so it is a durable lower bound
2918
+ * on what this client holds for this view. Reported at handshake to resume the stream.
2919
+ *
2920
+ * The watermark is keyed by view: switching views (a scope or subscription change)
2921
+ * saves the current view's watermark and loads the target view's, so returning to a
2922
+ * previously-synced view resumes exactly where it left off instead of re-syncing. This
2923
+ * is correct because a view's watermark is a lower bound specific to that view's
2924
+ * filter; a widened view is simply a different (initially unsynced) view.
2925
+ */
2926
+ deliveryWatermark = 0;
2927
+ /**
2928
+ * In-memory cache of watermark per view signature, preloaded from persistence at start
2929
+ * so a view switch resolves synchronously (no race with the reconnect it triggers).
2930
+ */
2931
+ deliverySignatureWatermarks = /* @__PURE__ */ new Map();
2932
+ deliveryGapRepeatCount = 0;
2933
+ lastDeliveryGapKey = null;
2666
2934
  constructor(options) {
2667
2935
  this.transport = options.transport;
2668
2936
  this.store = options.store;
@@ -2670,11 +2938,13 @@ var SyncEngine = class {
2670
2938
  this.serializer = options.serializer ?? new NegotiatedMessageSerializer("json");
2671
2939
  this.emitter = options.emitter ?? null;
2672
2940
  this.batchSize = options.config.batchSize ?? DEFAULT_BATCH_SIZE;
2941
+ this.outboundAckTimeoutMs = options.config.outboundAckTimeoutMs ?? DEFAULT_OUTBOUND_ACK_TIMEOUT_MS;
2673
2942
  this.encryptor = options.encryptor ?? null;
2674
2943
  this.syncState = options.syncState ?? null;
2675
2944
  this.activeScope = options.config.scopeMap;
2676
2945
  const queueStorage = options.queueStorage ?? new MemoryQueueStorage();
2677
2946
  this.outboundQueue = new OutboundQueue(queueStorage);
2947
+ this.rejectedStorage = options.rejectedStorage ?? new MemoryRejectedOperationStorage();
2678
2948
  this.metricsCollector = new SyncMetricsCollector(options.metricsConfig);
2679
2949
  if (this.emitter) {
2680
2950
  this.metricsCollector.attachEmitter(this.emitter);
@@ -2714,17 +2984,53 @@ var SyncEngine = class {
2714
2984
  * Start the sync engine: connect → handshake → delta exchange → streaming.
2715
2985
  */
2716
2986
  async start() {
2987
+ if (this.state === "streaming") return;
2988
+ if (this.startPromise) return this.startPromise;
2989
+ this.startPromise = this.startInternal().finally(() => {
2990
+ this.startPromise = null;
2991
+ });
2992
+ return this.startPromise;
2993
+ }
2994
+ async startInternal() {
2995
+ if (this.stopPromise) {
2996
+ await this.stopPromise;
2997
+ }
2998
+ if (this.state === "streaming") return;
2717
2999
  if (this.state !== "disconnected") {
2718
- throw new import_core7.SyncError("Cannot start sync engine: not in disconnected state", {
2719
- currentState: this.state
3000
+ await this.stop();
3001
+ }
3002
+ if (this.authRejected) {
3003
+ this.emitter?.emit({
3004
+ type: "sync:suspended",
3005
+ reason: this.suspensionReason ?? "auth-rejected"
2720
3006
  });
3007
+ return;
2721
3008
  }
3009
+ if (this.config.authState) {
3010
+ const authState = await this.config.authState();
3011
+ if (authState.state === "loading" || authState.state === "signed-out") {
3012
+ this.suspensionReason = authState.state === "loading" ? "auth-loading" : "auth-required";
3013
+ this.emitter?.emit({ type: "sync:suspended", reason: this.suspensionReason });
3014
+ return;
3015
+ }
3016
+ }
3017
+ this.suspensionReason = null;
2722
3018
  await this.outboundQueue.initialize();
2723
3019
  if (this.syncState) {
2724
3020
  this.lastAckedServerVector = await this.syncState.loadLastAckedServerVector();
2725
3021
  if (this.syncState.loadDeltaCursor) {
2726
3022
  this.resumeDeltaCursor = await this.syncState.loadDeltaCursor();
2727
3023
  }
3024
+ if (this.syncState.loadAllDeliveryWatermarks) {
3025
+ const all = await this.syncState.loadAllDeliveryWatermarks();
3026
+ for (const [signature, watermark] of Object.entries(all)) {
3027
+ this.deliverySignatureWatermarks.set(signature, watermark);
3028
+ }
3029
+ } else if (this.syncState.loadDeliveryWatermark) {
3030
+ this.deliverySignatureWatermarks.set("", await this.syncState.loadDeliveryWatermark(""));
3031
+ }
3032
+ this.deliveryWatermark = this.deliverySignatureWatermarks.get(this.deliverySignature()) ?? 0;
3033
+ this.evictColdViewWatermarks();
2728
3034
  }
2729
3035
  await this.reconcileOutboundFromOpLog();
2730
3036
  await this.refreshPendingCount();
@@ -2740,7 +3046,12 @@ var SyncEngine = class {
2740
3046
  this.transitionTo("connecting");
2741
3047
  try {
2742
3048
  const authToken = this.config.auth ? (await this.config.auth()).token : void 0;
3049
+ if (this.state !== "connecting") return;
2743
3050
  await this.transport.connect(this.config.url, { authToken });
3051
+ if (this.state !== "connecting") {
3052
+ await this.transport.disconnect();
3053
+ return;
3054
+ }
2744
3055
  this.transitionTo("handshaking");
2745
3056
  const localVector = this.store.getVersionVector();
2746
3057
  const activeQuerySubsets = this.getActiveQuerySubsets();
@@ -2754,7 +3065,11 @@ var SyncEngine = class {
2754
3065
  supportedWireFormats: ["json", "protobuf"],
2755
3066
  ...this.config.scopeMap ? { syncScope: this.config.scopeMap } : {},
2756
3067
  ...activeQuerySubsets.length > 0 ? { syncQueries: activeQuerySubsets } : {},
2757
- ...this.resumeDeltaCursor ? { deltaCursor: encodeDeltaCursor(this.resumeDeltaCursor) } : {}
3068
+ ...this.resumeDeltaCursor ? { deltaCursor: encodeDeltaCursor(this.resumeDeltaCursor) } : {},
3069
+ // Resume the server's gap-free delivery stream from our watermark. A server
3070
+ // that understands it drives server->client sync from delivery sequences; an
3071
+ // older server ignores it and falls back to the version-vector delta.
3072
+ lastDeliverySequence: this.deliveryWatermark
2758
3073
  };
2759
3074
  this.transport.send(handshake);
2760
3075
  } catch (err) {
@@ -2766,11 +3081,20 @@ var SyncEngine = class {
2766
3081
  * Stop the sync engine. Disconnects the transport.
2767
3082
  */
2768
3083
  async stop() {
3084
+ if (this.stopPromise) return this.stopPromise;
3085
+ this.stopPromise = this.stopInternal().finally(() => {
3086
+ this.stopPromise = null;
3087
+ });
3088
+ return this.stopPromise;
3089
+ }
3090
+ async stopInternal() {
2769
3091
  if (this.state === "disconnected") return;
3092
+ this.clearOutboundAckTimer();
2770
3093
  this.awarenessManager.stopCleanupTimer();
2771
3094
  if (this.currentBatch) {
2772
3095
  this.outboundQueue.returnBatch(this.currentBatch.batchId);
2773
3096
  this.currentBatch = null;
3097
+ this.currentBatchRequiresPartialAck = false;
2774
3098
  }
2775
3099
  try {
2776
3100
  await this.transport.disconnect();
@@ -2791,7 +3115,7 @@ var SyncEngine = class {
2791
3115
  * because they should remain local-only and not be sent to the server.
2792
3116
  */
2793
3117
  async pushOperation(op) {
2794
- if (!this.operationAllowedForSync(op)) {
3118
+ if (!await this.operationAllowedForSync(op)) {
2795
3119
  return;
2796
3120
  }
2797
3121
  if (this.syncState) {
@@ -2820,13 +3144,30 @@ var SyncEngine = class {
2820
3144
  getStatus() {
2821
3145
  const pendingOperations = this.syncState ? this.cachedUnsyncedCount : this.outboundQueue.totalPending;
2822
3146
  const base = {
3147
+ phase: this.resolvePhase(),
3148
+ ...this.suspensionReason ? { reason: this.suspensionReason } : {},
3149
+ reconnecting: this.reconnecting,
2823
3150
  pendingOperations,
2824
3151
  lastSyncedAt: this.lastSyncedAt,
2825
3152
  lastSuccessfulPush: this.lastSuccessfulPush,
2826
3153
  lastSuccessfulPull: this.lastSuccessfulPull,
2827
3154
  conflicts: this.conflictCount,
2828
- clockSkewMs: this.clockSkewMs
3155
+ clockSkewMs: this.clockSkewMs,
3156
+ inFlightUploadOperations: this.currentBatch?.operations.length ?? 0,
3157
+ hasInFlightDeliveryBatch: this.hasInFlightDeliveryBatch,
3158
+ activeViewId: this.deliverySignature(),
3159
+ activeViewComplete: this.state === "streaming" && !this.hasInFlightDeliveryBatch && this.blockedFailure === null && (this.serverFrontier === null || this.deliveryWatermark >= this.serverFrontier),
3160
+ initialSync: {
3161
+ complete: this.deltaReceiveComplete && this.deltaSendComplete,
3162
+ receivedBatches: this.deltaBatchesReceived,
3163
+ totalBatches: this.initialSyncTotalBatches || null,
3164
+ progress: this.initialSyncTotalBatches > 0 ? Math.min(1, this.deltaBatchesReceived / this.initialSyncTotalBatches) : null
3165
+ },
3166
+ deliveryWatermark: this.deliveryWatermark,
3167
+ serverFrontier: this.serverFrontier,
3168
+ blockedFailure: this.blockedFailure
2829
3169
  };
3170
+ if (this.suspensionReason) return { ...base, status: "auth-required" };
2830
3171
  switch (this.state) {
2831
3172
  case "disconnected":
2832
3173
  if (this.clockBlocked) {
@@ -2839,7 +3180,7 @@ var SyncEngine = class {
2839
3180
  case "connecting":
2840
3181
  case "handshaking":
2841
3182
  case "syncing":
2842
- return { ...base, status: this.reconnecting ? "offline" : "syncing" };
3183
+ return { ...base, status: this.reconnecting ? "reconnecting" : "syncing" };
2843
3184
  case "streaming":
2844
3185
  return { ...base, status: pendingOperations > 0 ? "syncing" : "synced" };
2845
3186
  case "error":
@@ -2849,6 +3190,26 @@ var SyncEngine = class {
2849
3190
  return { ...base, status: this.schemaBlocked ? "schema-mismatch" : "error" };
2850
3191
  }
2851
3192
  }
3193
+ resolvePhase() {
3194
+ if (this.suspensionReason) return "suspended";
3195
+ if (this.blockedFailure || this.schemaBlocked || this.clockBlocked) return "blocked";
3196
+ if (this.hasInFlightDeliveryBatch) return "applying";
3197
+ if (this.currentBatch) return "uploading";
3198
+ switch (this.state) {
3199
+ case "disconnected":
3200
+ return "offline";
3201
+ case "connecting":
3202
+ return "connecting";
3203
+ case "handshaking":
3204
+ return "handshaking";
3205
+ case "syncing":
3206
+ return "receiving";
3207
+ case "streaming":
3208
+ return "streaming";
3209
+ case "error":
3210
+ return "blocked";
3211
+ }
3212
+ }
2852
3213
  /**
2853
3214
  * True when the server rejected the client's schema version at handshake.
2854
3215
  * Sync stays blocked until the app schema is upgraded or sync config changes.
@@ -2901,8 +3262,25 @@ var SyncEngine = class {
2901
3262
  await this.refreshPendingCount();
2902
3263
  return this.getStatus().pendingOperations;
2903
3264
  }
2904
- emitApplyFailure(op, result, overrides) {
3265
+ emitApplyFailure(op, result, overrides, blocking = false) {
2905
3266
  const reason = (0, import_core7.defaultApplyFailureReason)(result, overrides);
3267
+ if (blocking || reason.retriable) {
3268
+ const prior = this.blockedFailure;
3269
+ this.blockedFailure = {
3270
+ operationId: op.id,
3271
+ collection: op.collection,
3272
+ recordId: op.recordId,
3273
+ code: reason.code,
3274
+ message: reason.message,
3275
+ retriable: reason.retriable,
3276
+ firstSeenAt: prior?.operationId === op.id ? prior.firstSeenAt : Date.now(),
3277
+ retryCount: prior?.operationId === op.id ? prior.retryCount + 1 : 0
3278
+ };
3279
+ this.emitter?.emit({
3280
+ type: prior?.operationId === op.id ? "sync:apply-retrying" : "sync:apply-blocked",
3281
+ failure: this.blockedFailure
3282
+ });
3283
+ }
2906
3284
  this.emitter?.emit({
2907
3285
  type: "sync:apply-failed",
2908
3286
  operationId: op.id,
@@ -2919,11 +3297,36 @@ var SyncEngine = class {
2919
3297
  */
2920
3298
  async retryNow() {
2921
3299
  if (this.schemaBlocked) return;
3300
+ this.authRejected = false;
3301
+ this.suspensionReason = null;
2922
3302
  if (this.state === "disconnected" || this.state === "error") {
2923
3303
  this.reconnecting = false;
2924
3304
  await this.start();
2925
3305
  }
2926
3306
  }
3307
+ /** Wake a permanent auth suspension after the binding reports a new auth state. */
3308
+ notifyAuthChanged() {
3309
+ this.authRejected = false;
3310
+ this.suspensionReason = null;
3311
+ }
3312
+ /**
3313
+ * Atomically refresh the transport session. Overlapping callers share one
3314
+ * reconnect, in-flight outbound batches are returned to the queue by stop(), and the
3315
+ * next start handshakes with the latest auth token, sync scope, and query subsets.
3316
+ */
3317
+ async reconnect() {
3318
+ if (this.schemaBlocked) return;
3319
+ if (this.reconnectPromise) return this.reconnectPromise;
3320
+ this.reconnecting = false;
3321
+ this.reconnectPromise = this.reconnectInternal().finally(() => {
3322
+ this.reconnectPromise = null;
3323
+ });
3324
+ return this.reconnectPromise;
3325
+ }
3326
+ async reconnectInternal() {
3327
+ await this.stop();
3328
+ await this.start();
3329
+ }
2927
3330
  /**
2928
3331
  * Export a diagnostics snapshot for debugging and support tickets.
2929
3332
  * Contains connection state, timing info, and queue metrics.
@@ -2942,6 +3345,8 @@ var SyncEngine = class {
2942
3345
  pendingOperations: this.outboundQueue.totalPending,
2943
3346
  hasInFlightBatch: this.currentBatch !== null,
2944
3347
  reconnecting: this.reconnecting,
3348
+ deliveryWatermark: this.deliveryWatermark,
3349
+ deliveryGapRepeatCount: this.deliveryGapRepeatCount,
2945
3350
  timestamp: Date.now()
2946
3351
  };
2947
3352
  }
@@ -2970,8 +3375,84 @@ var SyncEngine = class {
2970
3375
  * @param scopeMap - New per-collection scope filters, or undefined to remove scope
2971
3376
  */
2972
3377
  updateScope(scopeMap) {
3378
+ const previousSignature = this.deliverySignature();
2973
3379
  this.activeScope = scopeMap;
2974
3380
  this.config.scopeMap = scopeMap;
3381
+ this.switchDeliveryView(previousSignature);
3382
+ }
3383
+ /**
3384
+ * A stable identifier for the current sync view: the auth/tenant scope plus the set of
3385
+ * active query subscriptions, canonicalized so the same view always produces the same
3386
+ * string. The delivery watermark is keyed by this, so each distinct view resumes from
3387
+ * its own last position.
3388
+ */
3389
+ deliverySignature() {
3390
+ const subsets = this.getActiveQuerySubsets();
3391
+ if (!this.activeScope && subsets.length === 0) {
3392
+ return "";
3393
+ }
3394
+ const normalizedSubsets = subsets.map((s) => `${s.collection}:${stableStringify(s.where)}`).sort();
3395
+ return stableStringify({ scope: this.activeScope ?? null, subsets: normalizedSubsets });
3396
+ }
3397
+ /**
3398
+ * Switch the active delivery watermark to the current view after a scope or subscription
3399
+ * change. The prior view's watermark is saved so returning to it resumes; the new view's
3400
+ * watermark is restored from the in-memory cache (0 for a never-synced view, which then
3401
+ * does a one-time resync under that view). No data is lost: a view's watermark is a lower
3402
+ * bound for that specific view's filter.
3403
+ */
3404
+ switchDeliveryView(previousSignature) {
3405
+ const nextSignature = this.deliverySignature();
3406
+ if (nextSignature === previousSignature) {
3407
+ return;
3408
+ }
3409
+ this.setViewWatermark(previousSignature, this.deliveryWatermark);
3410
+ void this.persistDeliveryWatermark(this.deliveryWatermark, previousSignature);
3411
+ this.deliveryWatermark = this.deliverySignatureWatermarks.get(nextSignature) ?? 0;
3412
+ }
3413
+ /**
3414
+ * Record a view's watermark in the in-memory cache as the most-recently-used entry (the
3415
+ * Map preserves insertion order, so re-inserting moves it to the tail), then evict cold
3416
+ * views past the retention cap. This is the single place the cache is written, so LRU
3417
+ * order and the size bound are always maintained together.
3418
+ */
3419
+ setViewWatermark(signature, watermark) {
3420
+ this.deliverySignatureWatermarks.delete(signature);
3421
+ this.deliverySignatureWatermarks.set(signature, watermark);
3422
+ this.evictColdViewWatermarks();
3423
+ }
3424
+ /**
3425
+ * Trim the per-view watermark cache to `MAX_DELIVERY_VIEW_WATERMARKS`, evicting the
3426
+ * least-recently-used views. The default view ('') and the live view are never evicted
3427
+ * (evicting the live view would force an immediate re-scan of what we are actively
3428
+ * syncing). Each eviction also removes the persisted row, bounding storage. Safe by
3429
+ * construction: an evicted view simply back-fills from 0 (deduplicated) when next seen.
3430
+ */
3431
+ evictColdViewWatermarks() {
3432
+ if (this.deliverySignatureWatermarks.size <= MAX_DELIVERY_VIEW_WATERMARKS) {
3433
+ return;
3434
+ }
3435
+ const liveSignature = this.deliverySignature();
3436
+ for (const signature of this.deliverySignatureWatermarks.keys()) {
3437
+ if (this.deliverySignatureWatermarks.size <= MAX_DELIVERY_VIEW_WATERMARKS) {
3438
+ break;
3439
+ }
3440
+ if (signature === "" || signature === liveSignature) {
3441
+ continue;
3442
+ }
3443
+ this.deliverySignatureWatermarks.delete(signature);
3444
+ void this.deleteDeliveryWatermark(signature);
3445
+ }
3446
+ }
3447
+ /**
3448
+ * Remove a view's persisted watermark row (best-effort; a persistence layer that omits
3449
+ * the delete simply keeps the row, which is harmless).
3450
+ */
3451
+ async deleteDeliveryWatermark(signature) {
3452
+ if (!this.syncState?.deleteDeliveryWatermark) {
3453
+ return;
3454
+ }
3455
+ await this.syncState.deleteDeliveryWatermark(signature);
2975
3456
  }
2976
3457
  /**
2977
3458
  * Get the currently active scope map. Returns undefined if no scope is configured.
@@ -2984,19 +3465,49 @@ var SyncEngine = class {
2984
3465
  * Takes effect on the next connection; reconnects when already connected.
2985
3466
  */
2986
3467
  registerQuerySubset(subset) {
3468
+ if ((this.config.querySubsets?.mode ?? "reactive") !== "reactive") return () => {
3469
+ };
2987
3470
  const id = `query-${nextQuerySubsetId++}`;
3471
+ const previousSignature = this.deliverySignature();
3472
+ const previousEffective = this.getActiveQuerySubsets();
2988
3473
  this.querySubsets.set(id, subset);
2989
- this.scheduleQuerySubsetReconnect();
3474
+ this.switchDeliveryViewWithInheritance(previousSignature, previousEffective);
3475
+ if (this.deliverySignature() !== previousSignature) this.scheduleQuerySubsetReconnect();
2990
3476
  return () => {
3477
+ const sigBeforeRemove = this.deliverySignature();
3478
+ const effectiveBeforeRemove = this.getActiveQuerySubsets();
2991
3479
  this.querySubsets.delete(id);
2992
- this.scheduleQuerySubsetReconnect();
3480
+ this.switchDeliveryViewWithInheritance(sigBeforeRemove, effectiveBeforeRemove);
3481
+ if (this.deliverySignature() !== sigBeforeRemove) this.scheduleQuerySubsetReconnect();
2993
3482
  };
2994
3483
  }
3484
+ switchDeliveryViewWithInheritance(previousSignature, previousSubsets) {
3485
+ const nextSubsets = this.getActiveQuerySubsets();
3486
+ const previousWatermark = this.deliveryWatermark;
3487
+ this.switchDeliveryView(previousSignature);
3488
+ if (this.deliveryWatermark === 0 && querySubsetContains(previousSubsets, nextSubsets)) {
3489
+ this.deliveryWatermark = previousWatermark;
3490
+ this.setViewWatermark(this.deliverySignature(), previousWatermark);
3491
+ void this.persistDeliveryWatermark(previousWatermark);
3492
+ }
3493
+ }
3494
+ /** Atomically replace the manifest used by static query-subset mode. */
3495
+ setQuerySubsets(subsets) {
3496
+ if ((this.config.querySubsets?.mode ?? "reactive") !== "static")
3497
+ throw new Error('setQuerySubsets() requires sync.querySubsets.mode = "static"');
3498
+ const previousSignature = this.deliverySignature();
3499
+ const previousEffective = this.getActiveQuerySubsets();
3500
+ this.staticQuerySubsets = dedupeQuerySubsets(subsets);
3501
+ this.switchDeliveryViewWithInheritance(previousSignature, previousEffective);
3502
+ if (this.deliverySignature() !== previousSignature) this.scheduleQuerySubsetReconnect();
3503
+ }
2995
3504
  /**
2996
3505
  * Returns deduplicated active query subsets from registered subscriptions.
2997
3506
  */
2998
3507
  getActiveQuerySubsets() {
2999
- return dedupeQuerySubsets([...this.querySubsets.values()]);
3508
+ const mode = this.config.querySubsets?.mode ?? "reactive";
3509
+ if (mode === "disabled") return [];
3510
+ return mode === "static" ? this.staticQuerySubsets : dedupeQuerySubsets([...this.querySubsets.values()]);
3000
3511
  }
3001
3512
  /**
3002
3513
  * Get the awareness manager for collaborative presence.
@@ -3051,11 +3562,14 @@ var SyncEngine = class {
3051
3562
  await this.handleOperationBatch(message);
3052
3563
  break;
3053
3564
  case "acknowledgment":
3054
- this.handleAcknowledgment(message);
3565
+ await this.handleAcknowledgment(message);
3055
3566
  break;
3056
3567
  case "error":
3057
3568
  this.handleError(message);
3058
3569
  break;
3570
+ case "operation-rejected":
3571
+ await this.handleOperationRejected(message);
3572
+ break;
3059
3573
  case "awareness-update":
3060
3574
  this.handleAwarenessUpdate(message);
3061
3575
  break;
@@ -3070,6 +3584,7 @@ var SyncEngine = class {
3070
3584
  }
3071
3585
  }
3072
3586
  handleMessageFailure(error) {
3587
+ this.hasInFlightDeliveryBatch = false;
3073
3588
  const reason = error instanceof Error ? error.message : "Message handling failed";
3074
3589
  this.handleTransportClose(reason);
3075
3590
  }
@@ -3138,6 +3653,12 @@ var SyncEngine = class {
3138
3653
  }
3139
3654
  this.remoteVector = wireToVersionVector(msg.versionVector);
3140
3655
  void this.persistLastAckedServerVector(this.remoteVector);
3656
+ if (typeof msg.serverMaxDeliverySequence === "number" && this.deliveryWatermark > msg.serverMaxDeliverySequence) {
3657
+ this.deliveryWatermark = 0;
3658
+ this.setViewWatermark(this.deliverySignature(), 0);
3659
+ await this.persistDeliveryWatermark(0);
3660
+ }
3661
+ this.serverFrontier = msg.serverMaxDeliverySequence ?? null;
3141
3662
  if (msg.selectedWireFormat) {
3142
3663
  this.setSerializerWireFormat(msg.selectedWireFormat);
3143
3664
  }
@@ -3203,7 +3724,7 @@ var SyncEngine = class {
3203
3724
  async sendDelta() {
3204
3725
  const localVector = this.store.getVersionVector();
3205
3726
  const allMissingOps = await this.collectDelta(localVector, this.remoteVector);
3206
- const missingOps = allMissingOps.filter((op) => this.operationAllowedForSync(op));
3727
+ const missingOps = await this.filterAllowedForSync(allMissingOps);
3207
3728
  this.deltaSentOpIds = missingOps.map((op) => op.id);
3208
3729
  if (missingOps.length === 0) {
3209
3730
  const messageId = generateMessageId();
@@ -3278,11 +3799,35 @@ var SyncEngine = class {
3278
3799
  return missing;
3279
3800
  }
3280
3801
  async handleOperationBatch(msg) {
3802
+ const isDeliveryBatch = msg.maxDeliverySequence !== void 0;
3803
+ if (isDeliveryBatch) {
3804
+ const base = msg.baseDeliverySequence ?? 0;
3805
+ if (base > this.deliveryWatermark) {
3806
+ const gapKey = `${this.deliveryWatermark}:${base}`;
3807
+ this.deliveryGapRepeatCount = this.lastDeliveryGapKey === gapKey ? this.deliveryGapRepeatCount + 1 : 1;
3808
+ this.lastDeliveryGapKey = gapKey;
3809
+ this.emitter?.emit({
3810
+ type: "sync:delivery-gap",
3811
+ expectedBase: this.deliveryWatermark,
3812
+ receivedBase: base,
3813
+ currentWatermark: this.deliveryWatermark,
3814
+ messageId: msg.messageId,
3815
+ repeatCount: this.deliveryGapRepeatCount
3816
+ });
3817
+ return;
3818
+ }
3819
+ if (base < this.deliveryWatermark) {
3820
+ this.sendDeliveryAck(msg.messageId, this.deliveryWatermark);
3821
+ return;
3822
+ }
3823
+ this.hasInFlightDeliveryBatch = true;
3824
+ }
3281
3825
  const deserialized = msg.operations.map((s) => this.serializer.decodeOperation(s));
3282
3826
  const operations = this.encryptor ? await this.encryptor.decryptBatch(deserialized) : deserialized;
3283
- const inScopeOps = operations.filter((op) => this.operationAllowedForSync(op));
3827
+ const inScopeOps = await this.filterAllowedForSync(operations);
3284
3828
  const targetSchemaVersion = this.config.schemaVersion ?? DEFAULT_SCHEMA_VERSION;
3285
3829
  const transforms = this.config.operationTransforms ?? [];
3830
+ let fullyApplied = true;
3286
3831
  for (const op of inScopeOps) {
3287
3832
  const transformed = transforms.length > 0 ? (0, import_core7.applyOperationTransforms)(op, targetSchemaVersion, transforms) : op;
3288
3833
  if (transformed === null) {
@@ -3294,21 +3839,24 @@ var SyncEngine = class {
3294
3839
  this.emitApplyFailure(transformed, result);
3295
3840
  }
3296
3841
  } catch (error) {
3842
+ fullyApplied = false;
3297
3843
  if (error instanceof import_core7.ClockDriftError) {
3298
- this.emitApplyFailure(transformed, "rejected", {
3299
- code: import_core7.APPLY_FAILURE_CODES.CLOCK_DRIFT,
3300
- message: error.message,
3301
- retriable: false
3302
- });
3844
+ this.emitApplyFailure(
3845
+ transformed,
3846
+ "rejected",
3847
+ {
3848
+ code: import_core7.APPLY_FAILURE_CODES.CLOCK_DRIFT,
3849
+ message: error.message,
3850
+ retriable: false
3851
+ },
3852
+ true
3853
+ );
3303
3854
  continue;
3304
3855
  }
3305
3856
  const message = error instanceof Error ? error.message : "Apply failed";
3306
3857
  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;
3307
- this.emitApplyFailure(transformed, "rejected", {
3308
- code,
3309
- message,
3310
- retriable: code !== import_core7.APPLY_FAILURE_CODES.REFERENTIAL_INTEGRITY
3311
- });
3858
+ const retriable = code !== import_core7.APPLY_FAILURE_CODES.REFERENTIAL_INTEGRITY;
3859
+ this.emitApplyFailure(transformed, "rejected", { code, message, retriable }, true);
3312
3860
  }
3313
3861
  }
3314
3862
  if (inScopeOps.length > 0) {
@@ -3319,18 +3867,35 @@ var SyncEngine = class {
3319
3867
  batchSize: inScopeOps.length
3320
3868
  });
3321
3869
  }
3322
- const lastOp = operations[operations.length - 1];
3323
- const ack = {
3324
- type: "acknowledgment",
3325
- messageId: generateMessageId(),
3326
- acknowledgedMessageId: msg.messageId,
3327
- lastSequenceNumber: lastOp ? lastOp.sequenceNumber : 0
3328
- };
3329
- this.transport.send(ack);
3330
- this.emitter?.emit({
3331
- type: "sync:acknowledged",
3332
- sequenceNumber: lastOp ? lastOp.sequenceNumber : 0
3333
- });
3870
+ const deliveryFullyApplied = isDeliveryBatch && fullyApplied;
3871
+ if (!isDeliveryBatch || deliveryFullyApplied) {
3872
+ const lastOp = operations[operations.length - 1];
3873
+ const ack = {
3874
+ type: "acknowledgment",
3875
+ messageId: generateMessageId(),
3876
+ acknowledgedMessageId: msg.messageId,
3877
+ lastSequenceNumber: lastOp ? lastOp.sequenceNumber : 0,
3878
+ ...deliveryFullyApplied && msg.maxDeliverySequence !== void 0 ? { deliverySequence: msg.maxDeliverySequence } : {}
3879
+ };
3880
+ this.transport.send(ack);
3881
+ this.emitter?.emit({
3882
+ type: "sync:acknowledged",
3883
+ sequenceNumber: lastOp ? lastOp.sequenceNumber : 0
3884
+ });
3885
+ }
3886
+ if (isDeliveryBatch && fullyApplied && msg.maxDeliverySequence !== void 0) {
3887
+ this.deliveryGapRepeatCount = 0;
3888
+ this.lastDeliveryGapKey = null;
3889
+ this.deliveryWatermark = msg.maxDeliverySequence;
3890
+ this.setViewWatermark(this.deliverySignature(), this.deliveryWatermark);
3891
+ await this.persistDeliveryWatermark(this.deliveryWatermark);
3892
+ if (this.blockedFailure && inScopeOps.some((op) => op.id === this.blockedFailure?.operationId)) {
3893
+ const recovered = this.blockedFailure;
3894
+ this.blockedFailure = null;
3895
+ this.emitter?.emit({ type: "sync:apply-recovered", failure: recovered });
3896
+ }
3897
+ }
3898
+ this.hasInFlightDeliveryBatch = false;
3334
3899
  if (this.state === "syncing") {
3335
3900
  this.deltaBatchesReceived++;
3336
3901
  const totalBatches = msg.totalBatches ?? this.initialSyncTotalBatches;
@@ -3338,15 +3903,17 @@ var SyncEngine = class {
3338
3903
  this.initialSyncTotalBatches = msg.totalBatches;
3339
3904
  }
3340
3905
  this.metricsCollector.updateInitialSyncProgress(this.deltaBatchesReceived, totalBatches);
3341
- const cursorFromBatch = msg.cursor !== void 0 ? decodeDeltaCursor(msg.cursor) : createDeltaCursorFromBatch(
3342
- inScopeOps.length > 0 ? inScopeOps : operations,
3343
- msg.batchIndex
3344
- );
3345
- if (cursorFromBatch) {
3346
- this.resumeDeltaCursor = cursorFromBatch;
3347
- await this.persistDeltaCursor(cursorFromBatch);
3906
+ if (!isDeliveryBatch) {
3907
+ const cursorFromBatch = msg.cursor !== void 0 ? decodeDeltaCursor(msg.cursor) : createDeltaCursorFromBatch(
3908
+ inScopeOps.length > 0 ? inScopeOps : operations,
3909
+ msg.batchIndex
3910
+ );
3911
+ if (cursorFromBatch) {
3912
+ this.resumeDeltaCursor = cursorFromBatch;
3913
+ await this.persistDeltaCursor(cursorFromBatch);
3914
+ }
3348
3915
  }
3349
- if (msg.isFinal) {
3916
+ if (msg.isFinal && (!isDeliveryBatch || fullyApplied)) {
3350
3917
  this.deltaReceiveComplete = true;
3351
3918
  this.resumeDeltaCursor = null;
3352
3919
  await this.persistDeltaCursor(null);
@@ -3355,29 +3922,57 @@ var SyncEngine = class {
3355
3922
  }
3356
3923
  }
3357
3924
  }
3358
- handleAcknowledgment(msg) {
3925
+ /** Acknowledge a delivery batch (used for duplicates) without re-applying it. */
3926
+ sendDeliveryAck(acknowledgedMessageId, deliverySequence) {
3927
+ const ack = {
3928
+ type: "acknowledgment",
3929
+ messageId: generateMessageId(),
3930
+ acknowledgedMessageId,
3931
+ lastSequenceNumber: 0,
3932
+ deliverySequence
3933
+ };
3934
+ this.transport.send(ack);
3935
+ }
3936
+ async handleAcknowledgment(msg) {
3937
+ this.clearOutboundAckTimer();
3359
3938
  if (this.state === "syncing" && this.config.strictHandshake) {
3360
3939
  this.pendingDeltaBatchAcks.delete(msg.acknowledgedMessageId);
3361
3940
  this.markDeltaSendCompleteIfReady();
3362
3941
  }
3363
3942
  if (this.currentBatch) {
3364
- this.outboundQueue.acknowledge(this.currentBatch.batchId);
3943
+ if (this.currentBatchRequiresPartialAck) {
3944
+ await this.outboundQueue.acknowledgeThrough(
3945
+ this.currentBatch.batchId,
3946
+ msg.lastSequenceNumber
3947
+ );
3948
+ } else {
3949
+ await this.outboundQueue.acknowledge(this.currentBatch.batchId);
3950
+ }
3365
3951
  this.currentBatch = null;
3952
+ this.currentBatchRequiresPartialAck = false;
3366
3953
  const now = Date.now();
3367
3954
  this.lastSyncedAt = now;
3368
3955
  this.lastSuccessfulPush = now;
3369
3956
  }
3370
- void this.advanceLastAckedForLocalNode(msg.lastSequenceNumber).then(
3371
- () => this.refreshPendingCount()
3372
- );
3957
+ await this.advanceLastAckedForLocalNode(msg.lastSequenceNumber);
3958
+ await this.refreshPendingCount();
3373
3959
  if (this.state === "streaming" && this.outboundQueue.hasOperations) {
3374
3960
  this.flushQueue();
3375
3961
  }
3376
3962
  }
3377
3963
  handleError(msg) {
3964
+ this.clearOutboundAckTimer();
3965
+ if (this.currentBatch) {
3966
+ this.outboundQueue.returnBatch(this.currentBatch.batchId);
3967
+ this.currentBatch = null;
3968
+ this.currentBatchRequiresPartialAck = false;
3969
+ }
3378
3970
  this.transitionTo("error");
3379
- if (msg.code === "AUTH_FAILED") {
3971
+ if (msg.code === "AUTH_FAILED" || msg.code === "DEVICE_REVOKED") {
3972
+ this.authRejected = true;
3973
+ this.suspensionReason = msg.code === "DEVICE_REVOKED" ? "device-revoked" : "auth-rejected";
3380
3974
  this.emitter?.emit({ type: "sync:auth-failed", reason: msg.message });
3975
+ this.emitter?.emit({ type: "sync:suspended", reason: this.suspensionReason });
3381
3976
  }
3382
3977
  if (msg.code === "INVALID_TIMESTAMP") {
3383
3978
  this.clockBlocked = true;
@@ -3393,6 +3988,64 @@ var SyncEngine = class {
3393
3988
  this.emitter?.emit({ type: "sync:disconnected", reason: msg.message });
3394
3989
  this.transitionTo("disconnected");
3395
3990
  }
3991
+ /**
3992
+ * Handle a per-operation rejection: divert the op out of the pending outbound
3993
+ * queue (so it is never retried or resurrected on reconnect), record it in the
3994
+ * durable rejected store (so it is kept and explainable), and emit an event so
3995
+ * the app can reconcile. Unlike {@link handleError}, this is a normal per-op
3996
+ * signal, so the connection stays up.
3997
+ */
3998
+ async handleOperationRejected(msg) {
3999
+ if (msg.retriable) {
4000
+ this.currentBatchRequiresPartialAck = true;
4001
+ await this.refreshPendingCount();
4002
+ this.emitter?.emit({
4003
+ type: "sync:operation-rejected",
4004
+ operationId: msg.operationId,
4005
+ collection: msg.collection,
4006
+ recordId: msg.recordId,
4007
+ code: msg.code,
4008
+ message: msg.message,
4009
+ retriable: true
4010
+ });
4011
+ return;
4012
+ }
4013
+ await this.outboundQueue.reject(msg.operationId);
4014
+ await this.rejectedStorage.record({
4015
+ operationId: msg.operationId,
4016
+ collection: msg.collection,
4017
+ recordId: msg.recordId,
4018
+ code: msg.code,
4019
+ message: msg.message,
4020
+ retriable: msg.retriable,
4021
+ rejectedAt: Date.now()
4022
+ });
4023
+ await this.refreshPendingCount();
4024
+ this.emitter?.emit({
4025
+ type: "sync:operation-rejected",
4026
+ operationId: msg.operationId,
4027
+ collection: msg.collection,
4028
+ recordId: msg.recordId,
4029
+ code: msg.code,
4030
+ message: msg.message,
4031
+ retriable: msg.retriable
4032
+ });
4033
+ }
4034
+ /**
4035
+ * Every operation the server rejected that has not yet been reconciled. The app
4036
+ * uses this (alongside the `sync:operation-rejected` event) to surface failed
4037
+ * submissions and decide whether to roll back or resubmit.
4038
+ */
4039
+ getRejectedOperations() {
4040
+ return this.rejectedStorage.list();
4041
+ }
4042
+ /**
4043
+ * Forget rejected operations by id once the app has reconciled them (rolled the
4044
+ * optimistic write back or resubmitted a corrected op).
4045
+ */
4046
+ clearRejectedOperations(operationIds) {
4047
+ return this.rejectedStorage.remove(operationIds);
4048
+ }
3396
4049
  async checkDeltaComplete() {
3397
4050
  if (!this.deltaSendComplete || !this.deltaReceiveComplete) {
3398
4051
  return;
@@ -3491,7 +4144,7 @@ var SyncEngine = class {
3491
4144
  return;
3492
4145
  }
3493
4146
  const ops = await this.store.getOperationRange(localNodeId, 1, localSeq);
3494
- const inScope = ops.filter((op) => this.operationAllowedForSync(op));
4147
+ const inScope = await this.filterAllowedForSync(ops);
3495
4148
  for (const op of inScope) {
3496
4149
  await this.outboundQueue.enqueue(op);
3497
4150
  }
@@ -3502,6 +4155,7 @@ var SyncEngine = class {
3502
4155
  const batch = this.outboundQueue.takeBatch(this.batchSize);
3503
4156
  if (!batch) return;
3504
4157
  this.currentBatch = batch;
4158
+ this.currentBatchRequiresPartialAck = false;
3505
4159
  if (this.encryptor) {
3506
4160
  this.encryptor.encryptBatch(batch.operations).then(
3507
4161
  (encrypted) => {
@@ -3514,6 +4168,7 @@ var SyncEngine = class {
3514
4168
  batchIndex: 0
3515
4169
  };
3516
4170
  this.transport.send(batchMsg);
4171
+ this.startOutboundAckTimer(batch.batchId);
3517
4172
  this.emitter?.emit({
3518
4173
  type: "sync:sent",
3519
4174
  operations: batch.operations,
@@ -3523,6 +4178,7 @@ var SyncEngine = class {
3523
4178
  (err) => {
3524
4179
  this.outboundQueue.returnBatch(batch.batchId);
3525
4180
  this.currentBatch = null;
4181
+ this.currentBatchRequiresPartialAck = false;
3526
4182
  this.emitter?.emit({
3527
4183
  type: "sync:disconnected",
3528
4184
  reason: err instanceof Error ? err.message : "Encryption failed"
@@ -3539,6 +4195,7 @@ var SyncEngine = class {
3539
4195
  batchIndex: 0
3540
4196
  };
3541
4197
  this.transport.send(batchMsg);
4198
+ this.startOutboundAckTimer(batch.batchId);
3542
4199
  this.emitter?.emit({
3543
4200
  type: "sync:sent",
3544
4201
  operations: batch.operations,
@@ -3546,6 +4203,34 @@ var SyncEngine = class {
3546
4203
  });
3547
4204
  }
3548
4205
  }
4206
+ startOutboundAckTimer(batchId) {
4207
+ this.clearOutboundAckTimer();
4208
+ if (this.outboundAckTimeoutMs <= 0) return;
4209
+ this.outboundAckTimer = setTimeout(() => {
4210
+ void this.handleOutboundAckTimeout(batchId);
4211
+ }, this.outboundAckTimeoutMs);
4212
+ }
4213
+ clearOutboundAckTimer() {
4214
+ if (!this.outboundAckTimer) return;
4215
+ clearTimeout(this.outboundAckTimer);
4216
+ this.outboundAckTimer = null;
4217
+ }
4218
+ async handleOutboundAckTimeout(batchId) {
4219
+ if (!this.currentBatch || this.currentBatch.batchId !== batchId) return;
4220
+ this.outboundQueue.returnBatch(batchId);
4221
+ this.currentBatch = null;
4222
+ this.currentBatchRequiresPartialAck = false;
4223
+ this.clearOutboundAckTimer();
4224
+ await this.refreshPendingCount();
4225
+ if (this.state === "disconnected") return;
4226
+ const reason = `Timed out waiting for acknowledgment of outbound batch ${batchId}`;
4227
+ try {
4228
+ await this.transport.disconnect();
4229
+ } finally {
4230
+ this.ensureDisconnected();
4231
+ this.emitter?.emit({ type: "sync:disconnected", reason });
4232
+ }
4233
+ }
3549
4234
  handleAwarenessUpdate(msg) {
3550
4235
  const awarenessMessage = {
3551
4236
  type: "awareness",
@@ -3555,9 +4240,11 @@ var SyncEngine = class {
3555
4240
  this.awarenessManager.handleRemoteMessage(awarenessMessage);
3556
4241
  }
3557
4242
  handleTransportClose(reason) {
4243
+ this.clearOutboundAckTimer();
3558
4244
  if (this.currentBatch) {
3559
4245
  this.outboundQueue.returnBatch(this.currentBatch.batchId);
3560
4246
  this.currentBatch = null;
4247
+ this.currentBatchRequiresPartialAck = false;
3561
4248
  }
3562
4249
  if (this.schemaBlocked) {
3563
4250
  return;
@@ -3589,11 +4276,40 @@ var SyncEngine = class {
3589
4276
  this.serializer.setWireFormat(format);
3590
4277
  }
3591
4278
  }
3592
- operationAllowedForSync(op) {
3593
- if (!operationMatchesScope(op, this.activeScope)) {
4279
+ async operationAllowedForSync(op) {
4280
+ if (this.matchesScopeAndSubsets(op)) {
4281
+ return true;
4282
+ }
4283
+ const fullRecord = await this.readRecordForBackfill(op);
4284
+ if (!fullRecord) {
3594
4285
  return false;
3595
4286
  }
3596
- return operationMatchesQuerySubsets(op, this.getActiveQuerySubsets());
4287
+ return this.matchesScopeAndSubsets(op, fullRecord);
4288
+ }
4289
+ matchesScopeAndSubsets(op, fullRecord) {
4290
+ if (!operationMatchesScope(op, this.activeScope, fullRecord)) {
4291
+ return false;
4292
+ }
4293
+ return operationMatchesQuerySubsets(op, this.getActiveQuerySubsets(), fullRecord);
4294
+ }
4295
+ async readRecordForBackfill(op) {
4296
+ if (!this.store.readRecordFields) {
4297
+ return null;
4298
+ }
4299
+ try {
4300
+ return await this.store.readRecordFields(op.collection, op.recordId);
4301
+ } catch {
4302
+ return null;
4303
+ }
4304
+ }
4305
+ async filterAllowedForSync(ops) {
4306
+ const allowed = [];
4307
+ for (const op of ops) {
4308
+ if (await this.operationAllowedForSync(op)) {
4309
+ allowed.push(op);
4310
+ }
4311
+ }
4312
+ return allowed;
3597
4313
  }
3598
4314
  async persistDeltaCursor(cursor) {
3599
4315
  if (!this.syncState?.saveDeltaCursor) {
@@ -3601,6 +4317,12 @@ var SyncEngine = class {
3601
4317
  }
3602
4318
  await this.syncState.saveDeltaCursor(cursor);
3603
4319
  }
4320
+ async persistDeliveryWatermark(watermark, signature = this.deliverySignature()) {
4321
+ if (!this.syncState?.saveDeliveryWatermark) {
4322
+ return;
4323
+ }
4324
+ await this.syncState.saveDeliveryWatermark(signature, watermark);
4325
+ }
3604
4326
  scheduleQuerySubsetReconnect() {
3605
4327
  if (this.querySubsetReconnectTimer) {
3606
4328
  clearTimeout(this.querySubsetReconnectTimer);
@@ -3613,8 +4335,7 @@ var SyncEngine = class {
3613
4335
  }, 500);
3614
4336
  }
3615
4337
  async reconnectForQuerySubsets() {
3616
- await this.stop();
3617
- await this.start();
4338
+ await this.reconnect();
3618
4339
  }
3619
4340
  };
3620
4341
  function awarenessStatesToWire(states) {
@@ -4295,12 +5016,22 @@ function fromBase642(str) {
4295
5016
  // src/reactivity/sync-status-controller.ts
4296
5017
  var OFFLINE_SYNC_STATUS = Object.freeze({
4297
5018
  status: "offline",
5019
+ phase: "offline",
5020
+ reconnecting: false,
4298
5021
  pendingOperations: 0,
4299
5022
  lastSyncedAt: null,
4300
5023
  lastSuccessfulPush: null,
4301
5024
  lastSuccessfulPull: null,
4302
5025
  conflicts: 0,
4303
- clockSkewMs: null
5026
+ clockSkewMs: null,
5027
+ inFlightUploadOperations: 0,
5028
+ hasInFlightDeliveryBatch: false,
5029
+ activeViewId: "",
5030
+ activeViewComplete: false,
5031
+ initialSync: { complete: false, receivedBatches: 0, totalBatches: null, progress: null },
5032
+ deliveryWatermark: 0,
5033
+ serverFrontier: null,
5034
+ blockedFailure: null
4304
5035
  });
4305
5036
  var SYNC_STATUS_EVENT_TYPES = [
4306
5037
  "sync:connected",
@@ -4312,6 +5043,11 @@ var SYNC_STATUS_EVENT_TYPES = [
4312
5043
  "sync:received",
4313
5044
  "sync:acknowledged",
4314
5045
  "sync:apply-failed",
5046
+ "sync:suspended",
5047
+ "sync:apply-blocked",
5048
+ "sync:apply-retrying",
5049
+ "sync:apply-recovered",
5050
+ "sync:delivery-gap",
4315
5051
  "sync:diagnostics",
4316
5052
  "sync:initial-sync-progress"
4317
5053
  ];
@@ -4458,6 +5194,7 @@ function subscribeRemoteAwarenessStates(awareness, listener) {
4458
5194
  InvalidScopeError,
4459
5195
  JsonMessageSerializer,
4460
5196
  KeyDerivationError,
5197
+ MemoryRejectedOperationStorage,
4461
5198
  NegotiatedMessageSerializer,
4462
5199
  OFFLINE_SYNC_STATUS,
4463
5200
  OutboundQueue,
@@ -4496,11 +5233,13 @@ function subscribeRemoteAwarenessStates(awareness, listener) {
4496
5233
  isHandshakeMessage,
4497
5234
  isHandshakeResponseMessage,
4498
5235
  isOperationBatchMessage,
5236
+ isOperationRejectedMessage,
4499
5237
  isSchemaMismatchReject,
4500
5238
  isSyncMessage,
4501
5239
  isYjsDocUpdateMessage,
4502
5240
  operationMatchesQuerySubsets,
4503
5241
  operationMatchesScope,
5242
+ querySubsetContains,
4504
5243
  richtextDocKey,
4505
5244
  sliceOperationsAfterCursor,
4506
5245
  subscribeRemoteAwarenessStates,