@korajs/sync 0.4.0 → 0.5.0

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
@@ -33,6 +33,7 @@ __export(index_exports, {
33
33
  AwarenessManager: () => AwarenessManager,
34
34
  ChaosTransport: () => ChaosTransport,
35
35
  ConnectionMonitor: () => ConnectionMonitor,
36
+ DEFAULT_RICHTEXT_DOC_CHANNEL_THRESHOLD: () => DEFAULT_RICHTEXT_DOC_CHANNEL_THRESHOLD,
36
37
  DecryptionError: () => DecryptionError,
37
38
  EncryptionError: () => EncryptionError,
38
39
  HttpLongPollingTransport: () => HttpLongPollingTransport,
@@ -43,25 +44,39 @@ __export(index_exports, {
43
44
  OutboundQueue: () => OutboundQueue,
44
45
  ProtobufMessageSerializer: () => ProtobufMessageSerializer,
45
46
  ReconnectionManager: () => ReconnectionManager,
47
+ RichtextDocChannel: () => RichtextDocChannel,
48
+ SCHEMA_MISMATCH_PREFIX: () => SCHEMA_MISMATCH_PREFIX,
46
49
  SYNC_STATES: () => SYNC_STATES,
47
50
  SYNC_STATUSES: () => SYNC_STATUSES,
48
51
  ScopeViolationError: () => ScopeViolationError,
49
52
  SyncEncryptor: () => SyncEncryptor,
50
53
  SyncEngine: () => SyncEngine,
51
54
  WebSocketTransport: () => WebSocketTransport,
55
+ createDeltaCursorFromBatch: () => createDeltaCursorFromBatch,
56
+ decodeDeltaCursor: () => decodeDeltaCursor,
57
+ decodeYjsUpdate: () => decodeYjsUpdate,
58
+ dedupeQuerySubsets: () => dedupeQuerySubsets,
52
59
  deriveKey: () => deriveKey,
53
60
  deriveVersionedKey: () => deriveVersionedKey,
61
+ encodeDeltaCursor: () => encodeDeltaCursor,
62
+ encodeYjsUpdate: () => encodeYjsUpdate,
54
63
  filterOperationsByScope: () => filterOperationsByScope,
55
64
  generateSalt: () => generateSalt,
56
65
  isAcknowledgmentMessage: () => isAcknowledgmentMessage,
57
66
  isAwarenessUpdateMessage: () => isAwarenessUpdateMessage,
67
+ isClientSchemaVersionSupported: () => isClientSchemaVersionSupported,
58
68
  isEncryptedPayload: () => isEncryptedPayload,
59
69
  isErrorMessage: () => isErrorMessage,
60
70
  isHandshakeMessage: () => isHandshakeMessage,
61
71
  isHandshakeResponseMessage: () => isHandshakeResponseMessage,
62
72
  isOperationBatchMessage: () => isOperationBatchMessage,
73
+ isSchemaMismatchReject: () => isSchemaMismatchReject,
63
74
  isSyncMessage: () => isSyncMessage,
75
+ isYjsDocUpdateMessage: () => isYjsDocUpdateMessage,
76
+ operationMatchesQuerySubsets: () => operationMatchesQuerySubsets,
64
77
  operationMatchesScope: () => operationMatchesScope,
78
+ richtextDocKey: () => richtextDocKey,
79
+ sliceOperationsAfterCursor: () => sliceOperationsAfterCursor,
65
80
  versionVectorToWire: () => versionVectorToWire,
66
81
  wireToVersionVector: () => wireToVersionVector
67
82
  });
@@ -77,7 +92,14 @@ var SYNC_STATES = [
77
92
  "streaming",
78
93
  "error"
79
94
  ];
80
- var SYNC_STATUSES = ["connected", "syncing", "synced", "offline", "error"];
95
+ var SYNC_STATUSES = [
96
+ "connected",
97
+ "syncing",
98
+ "synced",
99
+ "offline",
100
+ "error",
101
+ "schema-mismatch"
102
+ ];
81
103
  var ScopeViolationError = class extends import_core.KoraError {
82
104
  constructor(operationId, collection, scope, message) {
83
105
  super(
@@ -102,12 +124,12 @@ var InvalidScopeError = class extends import_core.KoraError {
102
124
  };
103
125
 
104
126
  // src/scopes/scope-filter.ts
105
- function operationMatchesScope(op, scopeMap) {
127
+ function operationMatchesScope(op, scopeMap, fullRecord) {
106
128
  if (!scopeMap) return true;
107
129
  const collectionScope = scopeMap[op.collection];
108
130
  if (!collectionScope) return false;
109
131
  if (Object.keys(collectionScope).length === 0) return true;
110
- const snapshot = buildSnapshot(op);
132
+ const snapshot = buildSnapshot(op, fullRecord ?? void 0);
111
133
  if (!snapshot) return false;
112
134
  for (const [field, expected] of Object.entries(collectionScope)) {
113
135
  if (snapshot[field] !== expected) {
@@ -120,11 +142,12 @@ function filterOperationsByScope(operations, scopeMap) {
120
142
  if (!scopeMap) return operations;
121
143
  return operations.filter((op) => operationMatchesScope(op, scopeMap));
122
144
  }
123
- function buildSnapshot(op) {
145
+ function buildSnapshot(op, fullRecord) {
124
146
  const previous = asRecord(op.previousData);
125
147
  const next = asRecord(op.data);
126
- if (!previous && !next) return null;
148
+ if (!previous && !next && !fullRecord) return null;
127
149
  return {
150
+ ...fullRecord ?? {},
128
151
  ...previous ?? {},
129
152
  ...next ?? {}
130
153
  };
@@ -136,6 +159,128 @@ function asRecord(value) {
136
159
  return value;
137
160
  }
138
161
 
162
+ // src/scopes/query-subset.ts
163
+ function asRecord2(value) {
164
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
165
+ return null;
166
+ }
167
+ return value;
168
+ }
169
+ function buildSnapshot2(op, fullRecord) {
170
+ const previous = asRecord2(op.previousData);
171
+ const next = asRecord2(op.data);
172
+ if (!previous && !next && !fullRecord) {
173
+ return null;
174
+ }
175
+ return {
176
+ ...fullRecord ?? {},
177
+ ...previous ?? {},
178
+ ...next ?? {}
179
+ };
180
+ }
181
+ function recordMatchesWhere(snapshot, where) {
182
+ for (const [field, expected] of Object.entries(where)) {
183
+ if (snapshot[field] !== expected) {
184
+ return false;
185
+ }
186
+ }
187
+ return true;
188
+ }
189
+ function operationMatchesQuerySubsets(op, subsets, fullRecord) {
190
+ if (!subsets || subsets.length === 0) {
191
+ return true;
192
+ }
193
+ const collectionSubsets = subsets.filter((subset) => subset.collection === op.collection);
194
+ if (collectionSubsets.length === 0) {
195
+ return true;
196
+ }
197
+ const snapshot = buildSnapshot2(op, fullRecord);
198
+ if (!snapshot) {
199
+ return false;
200
+ }
201
+ return collectionSubsets.some((subset) => recordMatchesWhere(snapshot, subset.where));
202
+ }
203
+ function dedupeQuerySubsets(subsets) {
204
+ const seen = /* @__PURE__ */ new Set();
205
+ const result = [];
206
+ for (const subset of subsets) {
207
+ const key = `${subset.collection}:${JSON.stringify(subset.where)}`;
208
+ if (seen.has(key)) {
209
+ continue;
210
+ }
211
+ seen.add(key);
212
+ result.push(subset);
213
+ }
214
+ return result;
215
+ }
216
+
217
+ // src/delta/delta-cursor.ts
218
+ function bytesToBase64Url(bytes) {
219
+ let binary = "";
220
+ for (const byte of bytes) {
221
+ binary += String.fromCharCode(byte);
222
+ }
223
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
224
+ }
225
+ function base64UrlToBytes(value) {
226
+ const base64 = value.replace(/-/g, "+").replace(/_/g, "/");
227
+ const padded = base64 + "=".repeat((4 - base64.length % 4) % 4);
228
+ const binary = atob(padded);
229
+ const bytes = new Uint8Array(binary.length);
230
+ for (let i = 0; i < binary.length; i++) {
231
+ bytes[i] = binary.charCodeAt(i);
232
+ }
233
+ return bytes;
234
+ }
235
+ function encodeDeltaCursor(cursor) {
236
+ const json = JSON.stringify(cursor);
237
+ return bytesToBase64Url(new TextEncoder().encode(json));
238
+ }
239
+ function decodeDeltaCursor(value) {
240
+ if (!value) {
241
+ return null;
242
+ }
243
+ try {
244
+ const json = new TextDecoder().decode(base64UrlToBytes(value));
245
+ const parsed = JSON.parse(json);
246
+ if (typeof parsed !== "object" || parsed === null || typeof parsed.lastOperationId !== "string" || typeof parsed.batchIndex !== "number") {
247
+ return null;
248
+ }
249
+ return parsed;
250
+ } catch {
251
+ return null;
252
+ }
253
+ }
254
+ function sliceOperationsAfterCursor(operations, cursor) {
255
+ if (!cursor) {
256
+ return operations;
257
+ }
258
+ const index = operations.findIndex((op) => op.id === cursor.lastOperationId);
259
+ if (index === -1) {
260
+ return operations;
261
+ }
262
+ return operations.slice(index + 1);
263
+ }
264
+ function createDeltaCursorFromBatch(operations, batchIndex) {
265
+ const last = operations[operations.length - 1];
266
+ if (!last) {
267
+ return null;
268
+ }
269
+ return {
270
+ lastOperationId: last.id,
271
+ batchIndex
272
+ };
273
+ }
274
+
275
+ // src/protocol/schema-version.ts
276
+ var SCHEMA_MISMATCH_PREFIX = "SCHEMA_MISMATCH";
277
+ function isSchemaMismatchReject(rejectReason) {
278
+ return rejectReason?.startsWith(SCHEMA_MISMATCH_PREFIX) === true;
279
+ }
280
+ function isClientSchemaVersionSupported(clientVersion, supported) {
281
+ return clientVersion >= supported.min && clientVersion <= supported.max;
282
+ }
283
+
139
284
  // src/protocol/messages.ts
140
285
  function isSyncMessage(value) {
141
286
  if (typeof value !== "object" || value === null) return false;
@@ -154,6 +299,8 @@ function isSyncMessage(value) {
154
299
  return isErrorMessage(value);
155
300
  case "awareness-update":
156
301
  return isAwarenessUpdateMessage(value);
302
+ case "yjs-doc-update":
303
+ return isYjsDocUpdateMessage(value);
157
304
  default:
158
305
  return false;
159
306
  }
@@ -188,6 +335,11 @@ function isAwarenessUpdateMessage(value) {
188
335
  const msg = value;
189
336
  return msg.type === "awareness-update" && typeof msg.messageId === "string" && typeof msg.clientId === "number" && typeof msg.states === "object" && msg.states !== null && !Array.isArray(msg.states);
190
337
  }
338
+ function isYjsDocUpdateMessage(value) {
339
+ if (typeof value !== "object" || value === null) return false;
340
+ const msg = value;
341
+ return msg.type === "yjs-doc-update" && typeof msg.messageId === "string" && typeof msg.collection === "string" && typeof msg.recordId === "string" && typeof msg.field === "string" && typeof msg.update === "string";
342
+ }
191
343
 
192
344
  // src/protocol/serializer.ts
193
345
  var import_core2 = require("@korajs/core");
@@ -203,6 +355,84 @@ function versionVectorToWire(vector) {
203
355
  function wireToVersionVector(wire) {
204
356
  return new Map(Object.entries(wire));
205
357
  }
358
+ var WIRE_BYTES_KEY = "__kora_bytes__";
359
+ function toBase64(bytes) {
360
+ let binary = "";
361
+ for (const byte of bytes) {
362
+ binary += String.fromCharCode(byte);
363
+ }
364
+ return btoa(binary);
365
+ }
366
+ function fromBase64(value) {
367
+ const binary = atob(value);
368
+ const bytes = new Uint8Array(binary.length);
369
+ for (let index = 0; index < binary.length; index++) {
370
+ bytes[index] = binary.charCodeAt(index);
371
+ }
372
+ return bytes;
373
+ }
374
+ function serializeWireRecord(record) {
375
+ if (!record) {
376
+ return null;
377
+ }
378
+ const out = {};
379
+ for (const [key, value] of Object.entries(record)) {
380
+ out[key] = serializeWireValue(value);
381
+ }
382
+ return out;
383
+ }
384
+ function serializeWireValue(value) {
385
+ if (value instanceof Uint8Array) {
386
+ return { [WIRE_BYTES_KEY]: toBase64(value) };
387
+ }
388
+ if (value instanceof ArrayBuffer) {
389
+ return { [WIRE_BYTES_KEY]: toBase64(new Uint8Array(value)) };
390
+ }
391
+ return value;
392
+ }
393
+ function deserializeWireRecord(record) {
394
+ if (!record) {
395
+ return null;
396
+ }
397
+ const out = {};
398
+ for (const [key, value] of Object.entries(record)) {
399
+ out[key] = deserializeWireValue(value);
400
+ }
401
+ return out;
402
+ }
403
+ function deserializeWireValue(value) {
404
+ if (typeof value === "object" && value !== null && WIRE_BYTES_KEY in value) {
405
+ const encoded = value[WIRE_BYTES_KEY];
406
+ if (typeof encoded === "string") {
407
+ return fromBase64(encoded);
408
+ }
409
+ }
410
+ if (isLegacyIndexedByteRecord(value)) {
411
+ return legacyIndexedByteRecordToUint8Array(value);
412
+ }
413
+ return value;
414
+ }
415
+ function isLegacyIndexedByteRecord(value) {
416
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
417
+ return false;
418
+ }
419
+ const entries = Object.entries(value);
420
+ if (entries.length === 0) {
421
+ return false;
422
+ }
423
+ return entries.every(([key, entryValue]) => {
424
+ const index = Number(key);
425
+ return Number.isInteger(index) && index >= 0 && typeof entryValue === "number";
426
+ });
427
+ }
428
+ function legacyIndexedByteRecordToUint8Array(value) {
429
+ const maxIndex = Math.max(...Object.keys(value).map((key) => Number(key)));
430
+ const bytes = new Uint8Array(maxIndex + 1);
431
+ for (const [key, entryValue] of Object.entries(value)) {
432
+ bytes[Number(key)] = entryValue;
433
+ }
434
+ return bytes;
435
+ }
206
436
  var JsonMessageSerializer = class {
207
437
  encode(message) {
208
438
  return JSON.stringify(message);
@@ -231,8 +461,8 @@ var JsonMessageSerializer = class {
231
461
  type: op.type,
232
462
  collection: op.collection,
233
463
  recordId: op.recordId,
234
- data: op.data,
235
- previousData: op.previousData,
464
+ data: serializeWireRecord(op.data),
465
+ previousData: serializeWireRecord(op.previousData),
236
466
  timestamp: {
237
467
  wallTime: op.timestamp.wallTime,
238
468
  logical: op.timestamp.logical,
@@ -253,8 +483,8 @@ var JsonMessageSerializer = class {
253
483
  type: serialized.type,
254
484
  collection: serialized.collection,
255
485
  recordId: serialized.recordId,
256
- data: serialized.data,
257
- previousData: serialized.previousData,
486
+ data: deserializeWireRecord(serialized.data),
487
+ previousData: deserializeWireRecord(serialized.previousData),
258
488
  timestamp: {
259
489
  wallTime: serialized.timestamp.wallTime,
260
490
  logical: serialized.timestamp.logical,
@@ -375,6 +605,7 @@ function toProtoEnvelope(message) {
375
605
  retriable: message.retriable
376
606
  };
377
607
  case "awareness-update":
608
+ case "yjs-doc-update":
378
609
  return {
379
610
  type: message.type,
380
611
  messageId: message.messageId
@@ -1231,9 +1462,215 @@ var ChaosTransport = class {
1231
1462
  };
1232
1463
 
1233
1464
  // src/engine/sync-engine.ts
1234
- var import_core5 = require("@korajs/core");
1465
+ var import_core6 = require("@korajs/core");
1235
1466
  var import_internal2 = require("@korajs/core/internal");
1236
1467
 
1468
+ // src/awareness/awareness-manager.ts
1469
+ var DEFAULT_TIMEOUT_MS = 3e4;
1470
+ var nextLocalClientId = 1;
1471
+ var AwarenessManager = class {
1472
+ /** Unique client ID for this instance */
1473
+ clientId;
1474
+ localState = null;
1475
+ remoteStates = /* @__PURE__ */ new Map();
1476
+ listeners = /* @__PURE__ */ new Set();
1477
+ emitter;
1478
+ timeoutMs;
1479
+ // Track when we last received an update from each remote client.
1480
+ // Used for timeout-based cleanup as a safety net in case the server
1481
+ // does not send an explicit removal on disconnect.
1482
+ lastUpdated = /* @__PURE__ */ new Map();
1483
+ cleanupTimer = null;
1484
+ sendHandler = null;
1485
+ destroyed = false;
1486
+ constructor(options) {
1487
+ this.clientId = options?.clientId ?? nextLocalClientId++;
1488
+ this.emitter = options?.emitter ?? null;
1489
+ this.timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
1490
+ }
1491
+ /**
1492
+ * Set the local user's awareness state and broadcast to peers.
1493
+ *
1494
+ * @param state - The awareness state to share. Pass null to clear presence.
1495
+ */
1496
+ setLocalState(state) {
1497
+ if (this.destroyed) return;
1498
+ this.localState = state;
1499
+ const message = {
1500
+ type: "awareness",
1501
+ clientId: this.clientId,
1502
+ states: { [this.clientId]: state }
1503
+ };
1504
+ this.sendHandler?.(message);
1505
+ this.emitAwarenessEvent();
1506
+ }
1507
+ /**
1508
+ * Get the local awareness state.
1509
+ */
1510
+ getLocalState() {
1511
+ return this.localState;
1512
+ }
1513
+ /**
1514
+ * Get all known awareness states (local + remote).
1515
+ * Returns a new Map on each call.
1516
+ */
1517
+ getStates() {
1518
+ const result = /* @__PURE__ */ new Map();
1519
+ if (this.localState) {
1520
+ result.set(this.clientId, this.localState);
1521
+ }
1522
+ for (const [id, state] of this.remoteStates) {
1523
+ result.set(id, state);
1524
+ }
1525
+ return result;
1526
+ }
1527
+ /**
1528
+ * Handle an incoming awareness message from the transport.
1529
+ * This processes remote state updates and notifies listeners.
1530
+ */
1531
+ handleRemoteMessage(message) {
1532
+ if (this.destroyed) return;
1533
+ const added = [];
1534
+ const updated = [];
1535
+ const removed = [];
1536
+ const now = Date.now();
1537
+ for (const [clientIdStr, state] of Object.entries(message.states)) {
1538
+ const clientId = Number(clientIdStr);
1539
+ if (clientId === this.clientId) continue;
1540
+ if (state === null) {
1541
+ if (this.remoteStates.has(clientId)) {
1542
+ this.remoteStates.delete(clientId);
1543
+ this.lastUpdated.delete(clientId);
1544
+ removed.push(clientId);
1545
+ }
1546
+ } else {
1547
+ if (this.remoteStates.has(clientId)) {
1548
+ this.remoteStates.set(clientId, state);
1549
+ this.lastUpdated.set(clientId, now);
1550
+ updated.push(clientId);
1551
+ } else {
1552
+ this.remoteStates.set(clientId, state);
1553
+ this.lastUpdated.set(clientId, now);
1554
+ added.push(clientId);
1555
+ }
1556
+ }
1557
+ }
1558
+ if (added.length > 0 || updated.length > 0 || removed.length > 0) {
1559
+ const change = { added, updated, removed };
1560
+ this.notifyListeners(change);
1561
+ this.emitAwarenessEvent();
1562
+ }
1563
+ }
1564
+ /**
1565
+ * Remove a specific remote client's awareness state.
1566
+ * Called when the server notifies that a client has disconnected.
1567
+ */
1568
+ removeClient(clientId) {
1569
+ if (this.destroyed) return;
1570
+ if (!this.remoteStates.has(clientId)) return;
1571
+ this.remoteStates.delete(clientId);
1572
+ this.lastUpdated.delete(clientId);
1573
+ const change = {
1574
+ added: [],
1575
+ updated: [],
1576
+ removed: [clientId]
1577
+ };
1578
+ this.notifyListeners(change);
1579
+ this.emitAwarenessEvent();
1580
+ }
1581
+ /**
1582
+ * Register a handler for sending awareness messages through the transport.
1583
+ * The sync engine calls this to wire outgoing awareness messages to the transport.
1584
+ */
1585
+ onSend(handler) {
1586
+ this.sendHandler = handler;
1587
+ }
1588
+ /**
1589
+ * Register a listener for awareness state changes.
1590
+ * Returns an unsubscribe function.
1591
+ */
1592
+ on(_event, listener) {
1593
+ this.listeners.add(listener);
1594
+ return () => {
1595
+ this.listeners.delete(listener);
1596
+ };
1597
+ }
1598
+ /**
1599
+ * Remove a specific change listener.
1600
+ */
1601
+ off(_event, listener) {
1602
+ this.listeners.delete(listener);
1603
+ }
1604
+ /**
1605
+ * Start the cleanup timer that removes stale remote states.
1606
+ * Called when the sync engine transitions to streaming state.
1607
+ */
1608
+ startCleanupTimer() {
1609
+ if (this.cleanupTimer) return;
1610
+ this.cleanupTimer = setInterval(() => {
1611
+ this.cleanupStaleStates();
1612
+ }, this.timeoutMs);
1613
+ }
1614
+ /**
1615
+ * Stop the cleanup timer.
1616
+ */
1617
+ stopCleanupTimer() {
1618
+ if (this.cleanupTimer) {
1619
+ clearInterval(this.cleanupTimer);
1620
+ this.cleanupTimer = null;
1621
+ }
1622
+ }
1623
+ /**
1624
+ * Clean up all resources. After calling destroy(), the manager
1625
+ * will no longer send or receive awareness updates.
1626
+ * Broadcasts removal of local state before shutting down.
1627
+ */
1628
+ destroy() {
1629
+ if (this.destroyed) return;
1630
+ this.destroyed = true;
1631
+ if (this.localState) {
1632
+ const message = {
1633
+ type: "awareness",
1634
+ clientId: this.clientId,
1635
+ states: { [this.clientId]: null }
1636
+ };
1637
+ this.sendHandler?.(message);
1638
+ }
1639
+ this.localState = null;
1640
+ this.remoteStates.clear();
1641
+ this.lastUpdated.clear();
1642
+ this.listeners.clear();
1643
+ this.sendHandler = null;
1644
+ this.stopCleanupTimer();
1645
+ }
1646
+ // --- Private ---
1647
+ cleanupStaleStates() {
1648
+ const now = Date.now();
1649
+ const removed = [];
1650
+ for (const [clientId, lastTime] of this.lastUpdated) {
1651
+ if (now - lastTime > this.timeoutMs) {
1652
+ this.remoteStates.delete(clientId);
1653
+ this.lastUpdated.delete(clientId);
1654
+ removed.push(clientId);
1655
+ }
1656
+ }
1657
+ if (removed.length > 0) {
1658
+ const change = { added: [], updated: [], removed };
1659
+ this.notifyListeners(change);
1660
+ this.emitAwarenessEvent();
1661
+ }
1662
+ }
1663
+ notifyListeners(change) {
1664
+ for (const listener of this.listeners) {
1665
+ listener(change);
1666
+ }
1667
+ }
1668
+ emitAwarenessEvent() {
1669
+ const states = this.getStates();
1670
+ this.emitter?.emit({ type: "awareness:updated", states });
1671
+ }
1672
+ };
1673
+
1237
1674
  // src/diagnostics/bandwidth-estimator.ts
1238
1675
  var BandwidthEstimator = class {
1239
1676
  maxSamples;
@@ -1275,6 +1712,7 @@ var BandwidthEstimator = class {
1275
1712
  const decayFactor = 0.9;
1276
1713
  for (let i = this.samples.length - 1; i >= 0; i--) {
1277
1714
  const sample = this.samples[i];
1715
+ if (!sample) continue;
1278
1716
  const bytesPerSec = sample.bytes / sample.durationMs * 1e3;
1279
1717
  const age = this.samples.length - 1 - i;
1280
1718
  const weight = decayFactor ** age;
@@ -1340,7 +1778,7 @@ var SlidingWindowPercentile = class {
1340
1778
  const sorted = this.samples.slice(0, this.count).sort((a, b) => a - b);
1341
1779
  const rank = Math.ceil(p / 100 * sorted.length) - 1;
1342
1780
  const index = Math.max(0, Math.min(rank, sorted.length - 1));
1343
- return sorted[index];
1781
+ return sorted[index] ?? 0;
1344
1782
  }
1345
1783
  /**
1346
1784
  * Get the most recently added sample, or 0 if no samples exist.
@@ -1348,7 +1786,7 @@ var SlidingWindowPercentile = class {
1348
1786
  latest() {
1349
1787
  if (this.count === 0) return 0;
1350
1788
  const lastIndex = (this.writeIndex - 1 + this.maxSize) % this.maxSize;
1351
- return this.samples[lastIndex];
1789
+ return this.samples[lastIndex] ?? 0;
1352
1790
  }
1353
1791
  /**
1354
1792
  * Get the number of samples currently in the window.
@@ -1675,210 +2113,107 @@ var SyncMetricsCollector = class {
1675
2113
  }
1676
2114
  };
1677
2115
 
1678
- // src/awareness/awareness-manager.ts
1679
- var DEFAULT_TIMEOUT_MS = 3e4;
1680
- var nextLocalClientId = 1;
1681
- var AwarenessManager = class {
1682
- /** Unique client ID for this instance */
1683
- clientId;
1684
- localState = null;
1685
- remoteStates = /* @__PURE__ */ new Map();
1686
- listeners = /* @__PURE__ */ new Set();
1687
- emitter;
1688
- timeoutMs;
1689
- // Track when we last received an update from each remote client.
1690
- // Used for timeout-based cleanup as a safety net in case the server
1691
- // does not send an explicit removal on disconnect.
1692
- lastUpdated = /* @__PURE__ */ new Map();
1693
- cleanupTimer = null;
1694
- sendHandler = null;
1695
- destroyed = false;
1696
- constructor(options) {
1697
- this.clientId = options?.clientId ?? nextLocalClientId++;
1698
- this.emitter = options?.emitter ?? null;
1699
- this.timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
1700
- }
1701
- /**
1702
- * Set the local user's awareness state and broadcast to peers.
1703
- *
1704
- * @param state - The awareness state to share. Pass null to clear presence.
1705
- */
1706
- setLocalState(state) {
1707
- if (this.destroyed) return;
1708
- this.localState = state;
1709
- const message = {
1710
- type: "awareness",
1711
- clientId: this.clientId,
1712
- states: { [this.clientId]: state }
1713
- };
1714
- this.sendHandler?.(message);
1715
- this.emitAwarenessEvent();
1716
- }
1717
- /**
1718
- * Get the local awareness state.
1719
- */
1720
- getLocalState() {
1721
- return this.localState;
1722
- }
1723
- /**
1724
- * Get all known awareness states (local + remote).
1725
- * Returns a new Map on each call.
1726
- */
1727
- getStates() {
1728
- const result = /* @__PURE__ */ new Map();
1729
- if (this.localState) {
1730
- result.set(this.clientId, this.localState);
1731
- }
1732
- for (const [id, state] of this.remoteStates) {
1733
- result.set(id, state);
1734
- }
1735
- return result;
1736
- }
1737
- /**
1738
- * Handle an incoming awareness message from the transport.
1739
- * This processes remote state updates and notifies listeners.
1740
- */
1741
- handleRemoteMessage(message) {
1742
- if (this.destroyed) return;
1743
- const added = [];
1744
- const updated = [];
1745
- const removed = [];
1746
- const now = Date.now();
1747
- for (const [clientIdStr, state] of Object.entries(message.states)) {
1748
- const clientId = Number(clientIdStr);
1749
- if (clientId === this.clientId) continue;
1750
- if (state === null) {
1751
- if (this.remoteStates.has(clientId)) {
1752
- this.remoteStates.delete(clientId);
1753
- this.lastUpdated.delete(clientId);
1754
- removed.push(clientId);
1755
- }
1756
- } else {
1757
- if (this.remoteStates.has(clientId)) {
1758
- this.remoteStates.set(clientId, state);
1759
- this.lastUpdated.set(clientId, now);
1760
- updated.push(clientId);
1761
- } else {
1762
- this.remoteStates.set(clientId, state);
1763
- this.lastUpdated.set(clientId, now);
1764
- added.push(clientId);
1765
- }
1766
- }
1767
- }
1768
- if (added.length > 0 || updated.length > 0 || removed.length > 0) {
1769
- const change = { added, updated, removed };
1770
- this.notifyListeners(change);
1771
- this.emitAwarenessEvent();
1772
- }
2116
+ // src/richtext/richtext-doc-channel.ts
2117
+ var import_core5 = require("@korajs/core");
2118
+
2119
+ // src/richtext/doc-channel-wire.ts
2120
+ function encodeYjsUpdate(update) {
2121
+ let binary = "";
2122
+ for (let i = 0; i < update.length; i++) {
2123
+ binary += String.fromCharCode(update[i]);
1773
2124
  }
1774
- /**
1775
- * Remove a specific remote client's awareness state.
1776
- * Called when the server notifies that a client has disconnected.
1777
- */
1778
- removeClient(clientId) {
1779
- if (this.destroyed) return;
1780
- if (!this.remoteStates.has(clientId)) return;
1781
- this.remoteStates.delete(clientId);
1782
- this.lastUpdated.delete(clientId);
1783
- const change = {
1784
- added: [],
1785
- updated: [],
1786
- removed: [clientId]
1787
- };
1788
- this.notifyListeners(change);
1789
- this.emitAwarenessEvent();
2125
+ return btoa(binary);
2126
+ }
2127
+ function decodeYjsUpdate(encoded) {
2128
+ const binary = atob(encoded);
2129
+ const bytes = new Uint8Array(binary.length);
2130
+ for (let i = 0; i < binary.length; i++) {
2131
+ bytes[i] = binary.charCodeAt(i);
1790
2132
  }
1791
- /**
1792
- * Register a handler for sending awareness messages through the transport.
1793
- * The sync engine calls this to wire outgoing awareness messages to the transport.
1794
- */
1795
- onSend(handler) {
1796
- this.sendHandler = handler;
2133
+ return bytes;
2134
+ }
2135
+ function richtextDocKey(collection, recordId, field) {
2136
+ return `${collection}:${recordId}:${field}`;
2137
+ }
2138
+
2139
+ // src/richtext/richtext-doc-channel.ts
2140
+ var DEFAULT_RICHTEXT_DOC_CHANNEL_THRESHOLD = 4096;
2141
+ var RichtextDocChannel = class {
2142
+ threshold;
2143
+ onSend;
2144
+ listeners = /* @__PURE__ */ new Map();
2145
+ constructor(options) {
2146
+ this.threshold = options?.largeDocThreshold ?? DEFAULT_RICHTEXT_DOC_CHANNEL_THRESHOLD;
2147
+ this.onSend = options?.onSend ?? null;
1797
2148
  }
1798
2149
  /**
1799
- * Register a listener for awareness state changes.
1800
- * Returns an unsubscribe function.
2150
+ * Whether the doc channel should be used for a field.
2151
+ * @param preference - `true` forces on, `false` forces off, `undefined` uses size threshold
1801
2152
  */
1802
- on(_event, listener) {
1803
- this.listeners.add(listener);
1804
- return () => {
1805
- this.listeners.delete(listener);
1806
- };
2153
+ shouldUseChannel(snapshotByteLength, preference) {
2154
+ if (preference === false) {
2155
+ return false;
2156
+ }
2157
+ if (preference === true) {
2158
+ return true;
2159
+ }
2160
+ return snapshotByteLength >= this.threshold;
1807
2161
  }
1808
- /**
1809
- * Remove a specific change listener.
1810
- */
1811
- off(_event, listener) {
1812
- this.listeners.delete(listener);
2162
+ getThreshold() {
2163
+ return this.threshold;
1813
2164
  }
1814
2165
  /**
1815
- * Start the cleanup timer that removes stale remote states.
1816
- * Called when the sync engine transitions to streaming state.
2166
+ * Subscribe to incremental updates for a document key.
1817
2167
  */
1818
- startCleanupTimer() {
1819
- if (this.cleanupTimer) return;
1820
- this.cleanupTimer = setInterval(() => {
1821
- this.cleanupStaleStates();
1822
- }, this.timeoutMs);
2168
+ subscribe(collection, recordId, field, listener) {
2169
+ const key = richtextDocKey(collection, recordId, field);
2170
+ let set = this.listeners.get(key);
2171
+ if (!set) {
2172
+ set = /* @__PURE__ */ new Set();
2173
+ this.listeners.set(key, set);
2174
+ }
2175
+ set.add(listener);
2176
+ return () => {
2177
+ const current = this.listeners.get(key);
2178
+ if (!current) {
2179
+ return;
2180
+ }
2181
+ current.delete(listener);
2182
+ if (current.size === 0) {
2183
+ this.listeners.delete(key);
2184
+ }
2185
+ };
1823
2186
  }
1824
2187
  /**
1825
- * Stop the cleanup timer.
2188
+ * Publish a local incremental update to peers.
1826
2189
  */
1827
- stopCleanupTimer() {
1828
- if (this.cleanupTimer) {
1829
- clearInterval(this.cleanupTimer);
1830
- this.cleanupTimer = null;
2190
+ send(collection, recordId, field, update) {
2191
+ if (!this.onSend || update.length === 0) {
2192
+ return;
1831
2193
  }
2194
+ this.onSend({
2195
+ type: "yjs-doc-update",
2196
+ messageId: (0, import_core5.generateUUIDv7)(),
2197
+ collection,
2198
+ recordId,
2199
+ field,
2200
+ update: encodeYjsUpdate(update)
2201
+ });
1832
2202
  }
1833
2203
  /**
1834
- * Clean up all resources. After calling destroy(), the manager
1835
- * will no longer send or receive awareness updates.
1836
- * Broadcasts removal of local state before shutting down.
2204
+ * Deliver a remote incremental update to local subscribers.
1837
2205
  */
1838
- destroy() {
1839
- if (this.destroyed) return;
1840
- this.destroyed = true;
1841
- if (this.localState) {
1842
- const message = {
1843
- type: "awareness",
1844
- clientId: this.clientId,
1845
- states: { [this.clientId]: null }
1846
- };
1847
- this.sendHandler?.(message);
1848
- }
1849
- this.localState = null;
1850
- this.remoteStates.clear();
1851
- this.lastUpdated.clear();
1852
- this.listeners.clear();
1853
- this.sendHandler = null;
1854
- this.stopCleanupTimer();
1855
- }
1856
- // --- Private ---
1857
- cleanupStaleStates() {
1858
- const now = Date.now();
1859
- const removed = [];
1860
- for (const [clientId, lastTime] of this.lastUpdated) {
1861
- if (now - lastTime > this.timeoutMs) {
1862
- this.remoteStates.delete(clientId);
1863
- this.lastUpdated.delete(clientId);
1864
- removed.push(clientId);
1865
- }
1866
- }
1867
- if (removed.length > 0) {
1868
- const change = { added: [], updated: [], removed };
1869
- this.notifyListeners(change);
1870
- this.emitAwarenessEvent();
2206
+ deliver(message) {
2207
+ const key = richtextDocKey(message.collection, message.recordId, message.field);
2208
+ const set = this.listeners.get(key);
2209
+ if (!set || set.size === 0) {
2210
+ return;
1871
2211
  }
1872
- }
1873
- notifyListeners(change) {
1874
- for (const listener of this.listeners) {
1875
- listener(change);
2212
+ const update = decodeYjsUpdate(message.update);
2213
+ for (const listener of set) {
2214
+ listener(update);
1876
2215
  }
1877
2216
  }
1878
- emitAwarenessEvent() {
1879
- const states = this.getStates();
1880
- this.emitter?.emit({ type: "awareness:updated", states });
1881
- }
1882
2217
  };
1883
2218
 
1884
2219
  // src/engine/memory-queue-storage.ts
@@ -2012,6 +2347,19 @@ var OutboundQueue = class {
2012
2347
  get isInitialized() {
2013
2348
  return this.initialized;
2014
2349
  }
2350
+ /**
2351
+ * Remove operations by id from queue and persistent storage.
2352
+ * Used when ops were already sent during handshake delta exchange.
2353
+ */
2354
+ async removeByIds(ids) {
2355
+ if (ids.length === 0) return;
2356
+ const idSet = new Set(ids);
2357
+ this.queue = this.queue.filter((op) => !idSet.has(op.id));
2358
+ for (const id of ids) {
2359
+ this.seen.delete(id);
2360
+ }
2361
+ await this.storage.dequeue(ids);
2362
+ }
2015
2363
  };
2016
2364
 
2017
2365
  // src/engine/sync-engine.ts
@@ -2026,6 +2374,7 @@ var VALID_TRANSITIONS = {
2026
2374
  error: ["disconnected"]
2027
2375
  };
2028
2376
  var nextMessageId = 0;
2377
+ var nextQuerySubsetId = 0;
2029
2378
  function generateMessageId() {
2030
2379
  return `msg-${Date.now()}-${nextMessageId++}`;
2031
2380
  }
@@ -2040,24 +2389,39 @@ var SyncEngine = class {
2040
2389
  batchSize;
2041
2390
  encryptor;
2042
2391
  awarenessManager;
2392
+ richtextDocChannel;
2043
2393
  metricsCollector;
2394
+ syncState;
2044
2395
  remoteVector = /* @__PURE__ */ new Map();
2396
+ lastAckedServerVector = /* @__PURE__ */ new Map();
2397
+ cachedUnsyncedCount = 0;
2045
2398
  lastSyncedAt = null;
2046
2399
  lastSuccessfulPush = null;
2047
2400
  lastSuccessfulPull = null;
2048
2401
  conflictCount = 0;
2049
2402
  currentBatch = null;
2050
2403
  reconnecting = false;
2404
+ schemaBlocked = false;
2051
2405
  // Track delta exchange state
2052
2406
  deltaBatchesReceived = 0;
2053
2407
  deltaReceiveComplete = false;
2054
2408
  deltaSendComplete = false;
2409
+ /** Op ids sent during handshake delta — removed from outbound queue to avoid duplicate send */
2410
+ deltaSentOpIds = [];
2411
+ /** Outbound delta batch message IDs awaiting ACK when strictHandshake is enabled */
2412
+ pendingDeltaBatchAcks = /* @__PURE__ */ new Set();
2055
2413
  /**
2056
2414
  * The effective scope for this sync session.
2057
2415
  * Starts as the configured scopeMap. After handshake, may be replaced
2058
2416
  * with the server-accepted scope (server is authoritative).
2059
2417
  */
2060
2418
  activeScope;
2419
+ /** Live query subsets registered from reactive subscriptions */
2420
+ querySubsets = /* @__PURE__ */ new Map();
2421
+ querySubsetReconnectTimer = null;
2422
+ /** Resume cursor for paginated initial sync (persisted across reconnects) */
2423
+ resumeDeltaCursor = null;
2424
+ initialSyncTotalBatches = 0;
2061
2425
  constructor(options) {
2062
2426
  this.transport = options.transport;
2063
2427
  this.store = options.store;
@@ -2066,6 +2430,7 @@ var SyncEngine = class {
2066
2430
  this.emitter = options.emitter ?? null;
2067
2431
  this.batchSize = options.config.batchSize ?? DEFAULT_BATCH_SIZE;
2068
2432
  this.encryptor = options.encryptor ?? null;
2433
+ this.syncState = options.syncState ?? null;
2069
2434
  this.activeScope = options.config.scopeMap;
2070
2435
  const queueStorage = options.queueStorage ?? new MemoryQueueStorage();
2071
2436
  this.outboundQueue = new OutboundQueue(queueStorage);
@@ -2076,6 +2441,15 @@ var SyncEngine = class {
2076
2441
  this.awarenessManager = new AwarenessManager({
2077
2442
  emitter: this.emitter ?? void 0
2078
2443
  });
2444
+ this.richtextDocChannel = new RichtextDocChannel({
2445
+ largeDocThreshold: options.config.richtextDocChannelThreshold,
2446
+ onSend: (message) => {
2447
+ if (this.state !== "streaming") {
2448
+ return;
2449
+ }
2450
+ this.transport.send(message);
2451
+ }
2452
+ });
2079
2453
  this.awarenessManager.onSend((message) => {
2080
2454
  if (this.state !== "streaming") return;
2081
2455
  const wireMessage = {
@@ -2092,20 +2466,35 @@ var SyncEngine = class {
2092
2466
  */
2093
2467
  async start() {
2094
2468
  if (this.state !== "disconnected") {
2095
- throw new import_core5.SyncError("Cannot start sync engine: not in disconnected state", {
2469
+ throw new import_core6.SyncError("Cannot start sync engine: not in disconnected state", {
2096
2470
  currentState: this.state
2097
2471
  });
2098
2472
  }
2099
2473
  await this.outboundQueue.initialize();
2100
- this.transport.onMessage((msg) => this.handleMessage(msg));
2474
+ if (this.syncState) {
2475
+ this.lastAckedServerVector = await this.syncState.loadLastAckedServerVector();
2476
+ if (this.syncState.loadDeltaCursor) {
2477
+ this.resumeDeltaCursor = await this.syncState.loadDeltaCursor();
2478
+ }
2479
+ }
2480
+ await this.reconcileOutboundFromOpLog();
2481
+ await this.refreshPendingCount();
2482
+ this.transport.onMessage((msg) => this.enqueueMessage(msg));
2101
2483
  this.transport.onClose((reason) => this.handleTransportClose(reason));
2102
2484
  this.transport.onError((err) => this.handleTransportError(err));
2485
+ if (this.schemaBlocked) {
2486
+ throw new import_core6.SyncError(
2487
+ "Sync is blocked due to schema version mismatch. Upgrade the app schema or align sync.schemaVersion with the server.",
2488
+ { code: "SCHEMA_MISMATCH_BLOCKED" }
2489
+ );
2490
+ }
2103
2491
  this.transitionTo("connecting");
2104
2492
  try {
2105
2493
  const authToken = this.config.auth ? (await this.config.auth()).token : void 0;
2106
2494
  await this.transport.connect(this.config.url, { authToken });
2107
2495
  this.transitionTo("handshaking");
2108
2496
  const localVector = this.store.getVersionVector();
2497
+ const activeQuerySubsets = this.getActiveQuerySubsets();
2109
2498
  const handshake = {
2110
2499
  type: "handshake",
2111
2500
  messageId: generateMessageId(),
@@ -2114,7 +2503,9 @@ var SyncEngine = class {
2114
2503
  schemaVersion: this.config.schemaVersion ?? DEFAULT_SCHEMA_VERSION,
2115
2504
  authToken,
2116
2505
  supportedWireFormats: ["json", "protobuf"],
2117
- ...this.config.scopeMap ? { syncScope: this.config.scopeMap } : {}
2506
+ ...this.config.scopeMap ? { syncScope: this.config.scopeMap } : {},
2507
+ ...activeQuerySubsets.length > 0 ? { syncQueries: activeQuerySubsets } : {},
2508
+ ...this.resumeDeltaCursor ? { deltaCursor: encodeDeltaCursor(this.resumeDeltaCursor) } : {}
2118
2509
  };
2119
2510
  this.transport.send(handshake);
2120
2511
  } catch (err) {
@@ -2151,10 +2542,16 @@ var SyncEngine = class {
2151
2542
  * because they should remain local-only and not be sent to the server.
2152
2543
  */
2153
2544
  async pushOperation(op) {
2154
- if (!operationMatchesScope(op, this.activeScope)) {
2545
+ if (!this.operationAllowedForSync(op)) {
2155
2546
  return;
2156
2547
  }
2548
+ if (this.syncState) {
2549
+ this.cachedUnsyncedCount++;
2550
+ }
2157
2551
  await this.outboundQueue.enqueue(op);
2552
+ if (this.syncState) {
2553
+ await this.refreshPendingCount();
2554
+ }
2158
2555
  if (this.state === "streaming") {
2159
2556
  this.flushQueue();
2160
2557
  }
@@ -2172,7 +2569,7 @@ var SyncEngine = class {
2172
2569
  * Get the current developer-facing sync status.
2173
2570
  */
2174
2571
  getStatus() {
2175
- const pendingOperations = this.outboundQueue.totalPending;
2572
+ const pendingOperations = this.syncState ? this.cachedUnsyncedCount : this.outboundQueue.totalPending;
2176
2573
  const base = {
2177
2574
  pendingOperations,
2178
2575
  lastSyncedAt: this.lastSyncedAt,
@@ -2190,7 +2587,24 @@ var SyncEngine = class {
2190
2587
  case "streaming":
2191
2588
  return { ...base, status: pendingOperations > 0 ? "syncing" : "synced" };
2192
2589
  case "error":
2193
- return { ...base, status: "error" };
2590
+ return { ...base, status: this.schemaBlocked ? "schema-mismatch" : "error" };
2591
+ }
2592
+ }
2593
+ /**
2594
+ * True when the server rejected the client's schema version at handshake.
2595
+ * Sync stays blocked until the app schema is upgraded or sync config changes.
2596
+ */
2597
+ isSchemaBlocked() {
2598
+ return this.schemaBlocked;
2599
+ }
2600
+ /**
2601
+ * Clears schema-mismatch block after upgrading the local schema / sync config.
2602
+ * Moves the engine back to `disconnected` so `start()` can run again.
2603
+ */
2604
+ clearSchemaBlock() {
2605
+ this.schemaBlocked = false;
2606
+ if (this.state === "error") {
2607
+ this.transitionTo("disconnected");
2194
2608
  }
2195
2609
  }
2196
2610
  /**
@@ -2200,11 +2614,31 @@ var SyncEngine = class {
2200
2614
  recordConflict() {
2201
2615
  this.conflictCount++;
2202
2616
  }
2617
+ /**
2618
+ * Count of local operations not yet covered by the last acked server vector (op-log source of truth).
2619
+ */
2620
+ async getUnsyncedOperationCount() {
2621
+ await this.refreshPendingCount();
2622
+ return this.getStatus().pendingOperations;
2623
+ }
2624
+ emitApplyFailure(op, result, overrides) {
2625
+ const reason = (0, import_core6.defaultApplyFailureReason)(result, overrides);
2626
+ this.emitter?.emit({
2627
+ type: "sync:apply-failed",
2628
+ operationId: op.id,
2629
+ collection: op.collection,
2630
+ recordId: op.recordId,
2631
+ code: reason.code,
2632
+ message: reason.message,
2633
+ retriable: reason.retriable
2634
+ });
2635
+ }
2203
2636
  /**
2204
2637
  * Force an immediate reconnection attempt. If the engine is disconnected
2205
2638
  * or in error state, restarts the sync. If already connected, no-op.
2206
2639
  */
2207
2640
  async retryNow() {
2641
+ if (this.schemaBlocked) return;
2208
2642
  if (this.state === "disconnected" || this.state === "error") {
2209
2643
  this.reconnecting = false;
2210
2644
  await this.start();
@@ -2265,6 +2699,25 @@ var SyncEngine = class {
2265
2699
  getActiveScope() {
2266
2700
  return this.activeScope;
2267
2701
  }
2702
+ /**
2703
+ * Register a live query subset that narrows synced data for a collection.
2704
+ * Takes effect on the next connection; reconnects when already connected.
2705
+ */
2706
+ registerQuerySubset(subset) {
2707
+ const id = `query-${nextQuerySubsetId++}`;
2708
+ this.querySubsets.set(id, subset);
2709
+ this.scheduleQuerySubsetReconnect();
2710
+ return () => {
2711
+ this.querySubsets.delete(id);
2712
+ this.scheduleQuerySubsetReconnect();
2713
+ };
2714
+ }
2715
+ /**
2716
+ * Returns deduplicated active query subsets from registered subscriptions.
2717
+ */
2718
+ getActiveQuerySubsets() {
2719
+ return dedupeQuerySubsets([...this.querySubsets.values()]);
2720
+ }
2268
2721
  /**
2269
2722
  * Get the awareness manager for collaborative presence.
2270
2723
  * Use this to set local presence, observe remote collaborators,
@@ -2273,14 +2726,24 @@ var SyncEngine = class {
2273
2726
  getAwarenessManager() {
2274
2727
  return this.awarenessManager;
2275
2728
  }
2729
+ /**
2730
+ * Optional side channel for incremental Yjs updates on large richtext fields.
2731
+ */
2732
+ getRichtextDocChannel() {
2733
+ return this.richtextDocChannel;
2734
+ }
2276
2735
  // --- Private methods ---
2277
- handleMessage(message) {
2736
+ messageChain = Promise.resolve();
2737
+ enqueueMessage(message) {
2738
+ this.messageChain = this.messageChain.then(() => this.handleMessageAsync(message)).catch((error) => this.handleMessageFailure(error));
2739
+ }
2740
+ async handleMessageAsync(message) {
2278
2741
  switch (message.type) {
2279
2742
  case "handshake-response":
2280
2743
  this.handleHandshakeResponse(message);
2281
2744
  break;
2282
2745
  case "operation-batch":
2283
- this.handleOperationBatch(message);
2746
+ await this.handleOperationBatch(message);
2284
2747
  break;
2285
2748
  case "acknowledgment":
2286
2749
  this.handleAcknowledgment(message);
@@ -2291,20 +2754,46 @@ var SyncEngine = class {
2291
2754
  case "awareness-update":
2292
2755
  this.handleAwarenessUpdate(message);
2293
2756
  break;
2757
+ case "yjs-doc-update":
2758
+ this.richtextDocChannel.deliver(message);
2759
+ break;
2294
2760
  }
2295
2761
  }
2762
+ handleMessageFailure(error) {
2763
+ const reason = error instanceof Error ? error.message : "Message handling failed";
2764
+ this.handleTransportClose(reason);
2765
+ }
2296
2766
  handleHandshakeResponse(msg) {
2297
2767
  if (this.state !== "handshaking") return;
2298
2768
  if (!msg.accepted) {
2769
+ const reason = msg.rejectReason ?? "Handshake rejected";
2770
+ if (isSchemaMismatchReject(msg.rejectReason)) {
2771
+ this.schemaBlocked = true;
2772
+ const supportedMin = msg.supportedSchemaMin ?? msg.schemaVersion;
2773
+ const supportedMax = msg.supportedSchemaMax ?? msg.schemaVersion;
2774
+ this.emitter?.emit({
2775
+ type: "sync:schema-mismatch",
2776
+ clientSchemaVersion: this.config.schemaVersion ?? DEFAULT_SCHEMA_VERSION,
2777
+ serverSchemaVersion: msg.schemaVersion,
2778
+ supportedMin,
2779
+ supportedMax,
2780
+ reason
2781
+ });
2782
+ this.metricsCollector.updateStatus("error");
2783
+ this.transitionTo("error");
2784
+ void this.transport.disconnect();
2785
+ return;
2786
+ }
2299
2787
  this.transitionTo("error");
2300
2788
  this.emitter?.emit({
2301
2789
  type: "sync:disconnected",
2302
- reason: msg.rejectReason ?? "Handshake rejected"
2790
+ reason
2303
2791
  });
2304
2792
  this.transitionTo("disconnected");
2305
2793
  return;
2306
2794
  }
2307
2795
  this.remoteVector = wireToVersionVector(msg.versionVector);
2796
+ void this.persistLastAckedServerVector(this.remoteVector);
2308
2797
  if (msg.selectedWireFormat) {
2309
2798
  this.setSerializerWireFormat(msg.selectedWireFormat);
2310
2799
  }
@@ -2319,38 +2808,57 @@ var SyncEngine = class {
2319
2808
  this.deltaBatchesReceived = 0;
2320
2809
  this.deltaReceiveComplete = false;
2321
2810
  this.deltaSendComplete = false;
2811
+ this.deltaSentOpIds = [];
2812
+ this.pendingDeltaBatchAcks.clear();
2813
+ this.initialSyncTotalBatches = 0;
2322
2814
  this.sendDelta();
2323
2815
  }
2324
2816
  async sendDelta() {
2325
2817
  const localVector = this.store.getVersionVector();
2326
2818
  const allMissingOps = await this.collectDelta(localVector, this.remoteVector);
2327
- const missingOps = allMissingOps.filter((op) => operationMatchesScope(op, this.activeScope));
2819
+ const missingOps = allMissingOps.filter((op) => this.operationAllowedForSync(op));
2820
+ this.deltaSentOpIds = missingOps.map((op) => op.id);
2328
2821
  if (missingOps.length === 0) {
2822
+ const messageId = generateMessageId();
2823
+ if (this.config.strictHandshake) {
2824
+ this.pendingDeltaBatchAcks.add(messageId);
2825
+ }
2329
2826
  const emptyBatch = {
2330
2827
  type: "operation-batch",
2331
- messageId: generateMessageId(),
2828
+ messageId,
2332
2829
  operations: [],
2333
2830
  isFinal: true,
2334
- batchIndex: 0
2831
+ batchIndex: 0,
2832
+ totalBatches: 1
2335
2833
  };
2336
2834
  this.transport.send(emptyBatch);
2337
- this.deltaSendComplete = true;
2338
- this.checkDeltaComplete();
2835
+ if (!this.config.strictHandshake) {
2836
+ this.deltaSendComplete = true;
2837
+ void this.checkDeltaComplete();
2838
+ }
2339
2839
  return;
2340
2840
  }
2341
2841
  const sorted = (0, import_internal2.topologicalSort)(missingOps);
2342
2842
  const totalBatches = Math.ceil(sorted.length / this.batchSize);
2843
+ this.initialSyncTotalBatches = Math.max(this.initialSyncTotalBatches, totalBatches);
2343
2844
  for (let i = 0; i < totalBatches; i++) {
2344
2845
  const start = i * this.batchSize;
2345
2846
  const batchOps = sorted.slice(start, start + this.batchSize);
2847
+ const batchCursor = createDeltaCursorFromBatch(batchOps, i);
2346
2848
  const opsToSerialize = this.encryptor ? await this.encryptor.encryptBatch(batchOps) : batchOps;
2347
2849
  const serializedOps = opsToSerialize.map((op) => this.serializer.encodeOperation(op));
2850
+ const messageId = generateMessageId();
2851
+ if (this.config.strictHandshake) {
2852
+ this.pendingDeltaBatchAcks.add(messageId);
2853
+ }
2348
2854
  const batchMsg = {
2349
2855
  type: "operation-batch",
2350
- messageId: generateMessageId(),
2856
+ messageId,
2351
2857
  operations: serializedOps,
2352
2858
  isFinal: i === totalBatches - 1,
2353
- batchIndex: i
2859
+ batchIndex: i,
2860
+ totalBatches,
2861
+ ...batchCursor ? { cursor: encodeDeltaCursor(batchCursor) } : {}
2354
2862
  };
2355
2863
  this.transport.send(batchMsg);
2356
2864
  this.emitter?.emit({
@@ -2359,8 +2867,17 @@ var SyncEngine = class {
2359
2867
  batchSize: batchOps.length
2360
2868
  });
2361
2869
  }
2870
+ if (!this.config.strictHandshake) {
2871
+ this.deltaSendComplete = true;
2872
+ void this.checkDeltaComplete();
2873
+ }
2874
+ }
2875
+ markDeltaSendCompleteIfReady() {
2876
+ if (this.config.strictHandshake && this.pendingDeltaBatchAcks.size > 0) {
2877
+ return;
2878
+ }
2362
2879
  this.deltaSendComplete = true;
2363
- this.checkDeltaComplete();
2880
+ void this.checkDeltaComplete();
2364
2881
  }
2365
2882
  async collectDelta(localVector, remoteVector) {
2366
2883
  const missing = [];
@@ -2376,9 +2893,36 @@ var SyncEngine = class {
2376
2893
  async handleOperationBatch(msg) {
2377
2894
  const deserialized = msg.operations.map((s) => this.serializer.decodeOperation(s));
2378
2895
  const operations = this.encryptor ? await this.encryptor.decryptBatch(deserialized) : deserialized;
2379
- const inScopeOps = operations.filter((op) => operationMatchesScope(op, this.activeScope));
2896
+ const inScopeOps = operations.filter((op) => this.operationAllowedForSync(op));
2897
+ const targetSchemaVersion = this.config.schemaVersion ?? DEFAULT_SCHEMA_VERSION;
2898
+ const transforms = this.config.operationTransforms ?? [];
2380
2899
  for (const op of inScopeOps) {
2381
- await this.store.applyRemoteOperation(op);
2900
+ const transformed = transforms.length > 0 ? (0, import_core6.applyOperationTransforms)(op, targetSchemaVersion, transforms) : op;
2901
+ if (transformed === null) {
2902
+ continue;
2903
+ }
2904
+ try {
2905
+ const result = await this.store.applyRemoteOperation(transformed);
2906
+ if (result === "skipped" || result === "rejected" || result === "deferred") {
2907
+ this.emitApplyFailure(transformed, result);
2908
+ }
2909
+ } catch (error) {
2910
+ if (error instanceof import_core6.ClockDriftError) {
2911
+ this.emitApplyFailure(transformed, "rejected", {
2912
+ code: import_core6.APPLY_FAILURE_CODES.CLOCK_DRIFT,
2913
+ message: error.message,
2914
+ retriable: false
2915
+ });
2916
+ continue;
2917
+ }
2918
+ const message = error instanceof Error ? error.message : "Apply failed";
2919
+ const code = error instanceof import_core6.SyncError ? error.code : error instanceof import_core6.KoraError ? error.code : error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : import_core6.APPLY_FAILURE_CODES.APPLY_FAILED;
2920
+ this.emitApplyFailure(transformed, "rejected", {
2921
+ code,
2922
+ message,
2923
+ retriable: code !== import_core6.APPLY_FAILURE_CODES.REFERENTIAL_INTEGRITY
2924
+ });
2925
+ }
2382
2926
  }
2383
2927
  if (inScopeOps.length > 0) {
2384
2928
  this.lastSuccessfulPull = Date.now();
@@ -2402,13 +2946,33 @@ var SyncEngine = class {
2402
2946
  });
2403
2947
  if (this.state === "syncing") {
2404
2948
  this.deltaBatchesReceived++;
2949
+ const totalBatches = msg.totalBatches ?? this.initialSyncTotalBatches;
2950
+ if (msg.totalBatches !== void 0) {
2951
+ this.initialSyncTotalBatches = msg.totalBatches;
2952
+ }
2953
+ this.metricsCollector.updateInitialSyncProgress(this.deltaBatchesReceived, totalBatches);
2954
+ const cursorFromBatch = msg.cursor !== void 0 ? decodeDeltaCursor(msg.cursor) : createDeltaCursorFromBatch(
2955
+ inScopeOps.length > 0 ? inScopeOps : operations,
2956
+ msg.batchIndex
2957
+ );
2958
+ if (cursorFromBatch) {
2959
+ this.resumeDeltaCursor = cursorFromBatch;
2960
+ await this.persistDeltaCursor(cursorFromBatch);
2961
+ }
2405
2962
  if (msg.isFinal) {
2406
2963
  this.deltaReceiveComplete = true;
2407
- this.checkDeltaComplete();
2964
+ this.resumeDeltaCursor = null;
2965
+ await this.persistDeltaCursor(null);
2966
+ this.metricsCollector.recordSyncCompleted();
2967
+ void this.checkDeltaComplete();
2408
2968
  }
2409
2969
  }
2410
2970
  }
2411
2971
  handleAcknowledgment(msg) {
2972
+ if (this.state === "syncing" && this.config.strictHandshake) {
2973
+ this.pendingDeltaBatchAcks.delete(msg.acknowledgedMessageId);
2974
+ this.markDeltaSendCompleteIfReady();
2975
+ }
2412
2976
  if (this.currentBatch) {
2413
2977
  this.outboundQueue.acknowledge(this.currentBatch.batchId);
2414
2978
  this.currentBatch = null;
@@ -2416,6 +2980,9 @@ var SyncEngine = class {
2416
2980
  this.lastSyncedAt = now;
2417
2981
  this.lastSuccessfulPush = now;
2418
2982
  }
2983
+ void this.advanceLastAckedForLocalNode(msg.lastSequenceNumber).then(
2984
+ () => this.refreshPendingCount()
2985
+ );
2419
2986
  if (this.state === "streaming" && this.outboundQueue.hasOperations) {
2420
2987
  this.flushQueue();
2421
2988
  }
@@ -2428,18 +2995,107 @@ var SyncEngine = class {
2428
2995
  this.emitter?.emit({ type: "sync:disconnected", reason: msg.message });
2429
2996
  this.transitionTo("disconnected");
2430
2997
  }
2431
- checkDeltaComplete() {
2432
- if (this.deltaSendComplete && this.deltaReceiveComplete) {
2433
- this.lastSyncedAt = Date.now();
2434
- this.transitionTo("streaming");
2435
- this.awarenessManager.startCleanupTimer();
2436
- const localState = this.awarenessManager.getLocalState();
2437
- if (localState) {
2438
- this.awarenessManager.setLocalState(localState);
2998
+ async checkDeltaComplete() {
2999
+ if (!this.deltaSendComplete || !this.deltaReceiveComplete) {
3000
+ return;
3001
+ }
3002
+ if (this.state !== "syncing") {
3003
+ return;
3004
+ }
3005
+ this.lastSyncedAt = Date.now();
3006
+ this.transitionTo("streaming");
3007
+ this.awarenessManager.startCleanupTimer();
3008
+ const localState = this.awarenessManager.getLocalState();
3009
+ if (localState) {
3010
+ this.awarenessManager.setLocalState(localState);
3011
+ }
3012
+ if (this.deltaSentOpIds.length > 0) {
3013
+ await this.outboundQueue.removeByIds(this.deltaSentOpIds);
3014
+ this.deltaSentOpIds = [];
3015
+ }
3016
+ if (this.syncState) {
3017
+ const localNodeId = this.store.getNodeId();
3018
+ const localSeq = this.store.getVersionVector().get(localNodeId) ?? 0;
3019
+ if (localSeq > 0) {
3020
+ await this.advanceLastAckedForLocalNode(localSeq);
2439
3021
  }
2440
- if (this.outboundQueue.hasOperations) {
2441
- this.flushQueue();
3022
+ }
3023
+ if (this.outboundQueue.hasOperations) {
3024
+ this.flushQueue();
3025
+ }
3026
+ await this.refreshPendingCount();
3027
+ }
3028
+ /**
3029
+ * Effective server vector: persisted last-ack merged with live handshake remote vector.
3030
+ */
3031
+ getEffectiveServerVector() {
3032
+ if (!this.syncState) {
3033
+ return this.remoteVector;
3034
+ }
3035
+ return this.syncState.mergeServerVectors(this.lastAckedServerVector, this.remoteVector);
3036
+ }
3037
+ async persistLastAckedServerVector(vector) {
3038
+ if (!this.syncState) {
3039
+ return;
3040
+ }
3041
+ this.lastAckedServerVector = this.syncState.mergeServerVectors(
3042
+ this.lastAckedServerVector,
3043
+ vector
3044
+ );
3045
+ await this.syncState.saveLastAckedServerVector(this.lastAckedServerVector);
3046
+ }
3047
+ async advanceLastAckedForLocalNode(lastSequenceNumber) {
3048
+ if (!this.syncState || lastSequenceNumber <= 0) {
3049
+ return;
3050
+ }
3051
+ const nodeId = this.store.getNodeId();
3052
+ const merged = new Map(this.lastAckedServerVector);
3053
+ merged.set(nodeId, Math.max(merged.get(nodeId) ?? 0, lastSequenceNumber));
3054
+ this.lastAckedServerVector = merged;
3055
+ await this.syncState.saveLastAckedServerVector(merged);
3056
+ }
3057
+ /**
3058
+ * Refresh cached unsynced count from op log vs effective server vector.
3059
+ */
3060
+ async refreshPendingCount() {
3061
+ if (!this.syncState) {
3062
+ this.cachedUnsyncedCount = this.outboundQueue.totalPending;
3063
+ return;
3064
+ }
3065
+ this.cachedUnsyncedCount = await this.syncState.countUnsyncedOperations(
3066
+ this.getEffectiveServerVector()
3067
+ );
3068
+ }
3069
+ /**
3070
+ * Returns operations the server has not yet acknowledged (op-log source of truth).
3071
+ */
3072
+ async getPendingSyncOperations() {
3073
+ if (!this.syncState) {
3074
+ return this.outboundQueue.peek(Number.MAX_SAFE_INTEGER);
3075
+ }
3076
+ return this.syncState.getUnsyncedOperations(this.getEffectiveServerVector());
3077
+ }
3078
+ /**
3079
+ * Hydrate the outbound queue from unsynced ops in the op log (deduped by operation id).
3080
+ */
3081
+ async reconcileOutboundFromOpLog() {
3082
+ if (this.syncState) {
3083
+ const unsynced = await this.syncState.getUnsyncedOperations(this.getEffectiveServerVector());
3084
+ for (const op of unsynced) {
3085
+ await this.outboundQueue.enqueue(op);
2442
3086
  }
3087
+ return;
3088
+ }
3089
+ const localNodeId = this.store.getNodeId();
3090
+ const localVector = this.store.getVersionVector();
3091
+ const localSeq = localVector.get(localNodeId) ?? 0;
3092
+ if (localSeq === 0) {
3093
+ return;
3094
+ }
3095
+ const ops = await this.store.getOperationRange(localNodeId, 1, localSeq);
3096
+ const inScope = ops.filter((op) => this.operationAllowedForSync(op));
3097
+ for (const op of inScope) {
3098
+ await this.outboundQueue.enqueue(op);
2443
3099
  }
2444
3100
  }
2445
3101
  flushQueue() {
@@ -2505,6 +3161,9 @@ var SyncEngine = class {
2505
3161
  this.outboundQueue.returnBatch(this.currentBatch.batchId);
2506
3162
  this.currentBatch = null;
2507
3163
  }
3164
+ if (this.schemaBlocked) {
3165
+ return;
3166
+ }
2508
3167
  if (this.state !== "disconnected") {
2509
3168
  this.emitter?.emit({ type: "sync:disconnected", reason });
2510
3169
  this.transitionTo("disconnected");
@@ -2520,7 +3179,7 @@ var SyncEngine = class {
2520
3179
  transitionTo(newState) {
2521
3180
  const validTargets = VALID_TRANSITIONS[this.state];
2522
3181
  if (!validTargets.includes(newState)) {
2523
- throw new import_core5.SyncError(`Invalid sync state transition: ${this.state} \u2192 ${newState}`, {
3182
+ throw new import_core6.SyncError(`Invalid sync state transition: ${this.state} \u2192 ${newState}`, {
2524
3183
  from: this.state,
2525
3184
  to: newState
2526
3185
  });
@@ -2532,6 +3191,33 @@ var SyncEngine = class {
2532
3191
  this.serializer.setWireFormat(format);
2533
3192
  }
2534
3193
  }
3194
+ operationAllowedForSync(op) {
3195
+ if (!operationMatchesScope(op, this.activeScope)) {
3196
+ return false;
3197
+ }
3198
+ return operationMatchesQuerySubsets(op, this.getActiveQuerySubsets());
3199
+ }
3200
+ async persistDeltaCursor(cursor) {
3201
+ if (!this.syncState?.saveDeltaCursor) {
3202
+ return;
3203
+ }
3204
+ await this.syncState.saveDeltaCursor(cursor);
3205
+ }
3206
+ scheduleQuerySubsetReconnect() {
3207
+ if (this.querySubsetReconnectTimer) {
3208
+ clearTimeout(this.querySubsetReconnectTimer);
3209
+ }
3210
+ this.querySubsetReconnectTimer = setTimeout(() => {
3211
+ this.querySubsetReconnectTimer = null;
3212
+ if (this.state === "streaming" || this.state === "syncing" || this.state === "handshaking") {
3213
+ void this.reconnectForQuerySubsets();
3214
+ }
3215
+ }, 500);
3216
+ }
3217
+ async reconnectForQuerySubsets() {
3218
+ await this.stop();
3219
+ await this.start();
3220
+ }
2535
3221
  };
2536
3222
  function awarenessStatesToWire(states) {
2537
3223
  const wire = {};
@@ -2705,6 +3391,20 @@ var ReconnectionManager = class {
2705
3391
  this.running = false;
2706
3392
  }
2707
3393
  }
3394
+ /**
3395
+ * Cancel the current backoff wait and proceed to the next reconnect attempt immediately.
3396
+ * No-op if not waiting.
3397
+ */
3398
+ wake() {
3399
+ if (this.timer !== null) {
3400
+ clearTimeout(this.timer);
3401
+ this.timer = null;
3402
+ }
3403
+ if (this.waitResolve) {
3404
+ this.waitResolve();
3405
+ this.waitResolve = null;
3406
+ }
3407
+ }
2708
3408
  /**
2709
3409
  * Stop any pending reconnection attempt.
2710
3410
  */
@@ -2761,11 +3461,11 @@ var ReconnectionManager = class {
2761
3461
  };
2762
3462
 
2763
3463
  // src/encryption/sync-encryptor.ts
2764
- var import_core7 = require("@korajs/core");
3464
+ var import_core8 = require("@korajs/core");
2765
3465
 
2766
3466
  // src/encryption/key-derivation.ts
2767
- var import_core6 = require("@korajs/core");
2768
- var KeyDerivationError = class extends import_core6.SyncError {
3467
+ var import_core7 = require("@korajs/core");
3468
+ var KeyDerivationError = class extends import_core7.SyncError {
2769
3469
  constructor(message, context) {
2770
3470
  super(message, { ...context, errorType: "KEY_DERIVATION_ERROR" });
2771
3471
  this.name = "KeyDerivationError";
@@ -2841,13 +3541,13 @@ async function deriveVersionedKey(passphrase, version, salt) {
2841
3541
  }
2842
3542
 
2843
3543
  // src/encryption/sync-encryptor.ts
2844
- var EncryptionError = class extends import_core7.SyncError {
3544
+ var EncryptionError = class extends import_core8.SyncError {
2845
3545
  constructor(message, context) {
2846
3546
  super(message, { ...context, errorType: "ENCRYPTION_ERROR" });
2847
3547
  this.name = "EncryptionError";
2848
3548
  }
2849
3549
  };
2850
- var DecryptionError = class extends import_core7.SyncError {
3550
+ var DecryptionError = class extends import_core8.SyncError {
2851
3551
  constructor(message, context) {
2852
3552
  super(message, { ...context, errorType: "DECRYPTION_ERROR" });
2853
3553
  this.name = "DecryptionError";
@@ -3087,8 +3787,8 @@ var SyncEncryptor = class _SyncEncryptor {
3087
3787
  );
3088
3788
  const payload = {
3089
3789
  v: this.currentVersion,
3090
- iv: toBase64(iv),
3091
- ct: toBase64(new Uint8Array(ciphertextBuffer)),
3790
+ iv: toBase642(iv),
3791
+ ct: toBase642(new Uint8Array(ciphertextBuffer)),
3092
3792
  alg: "aes-256-gcm"
3093
3793
  };
3094
3794
  return {
@@ -3132,8 +3832,8 @@ var SyncEncryptor = class _SyncEncryptor {
3132
3832
  );
3133
3833
  }
3134
3834
  try {
3135
- const iv = fromBase64(payload.iv);
3136
- const ciphertext = fromBase64(payload.ct);
3835
+ const iv = fromBase642(payload.iv);
3836
+ const ciphertext = fromBase642(payload.ct);
3137
3837
  const plaintextBuffer = await globalThis.crypto.subtle.decrypt(
3138
3838
  { name: "AES-GCM", iv },
3139
3839
  key.key,
@@ -3170,14 +3870,14 @@ function isEncryptedPayload(field) {
3170
3870
  }
3171
3871
  return field[ENCRYPTED_MARKER] === true && typeof field.v === "number" && typeof field.iv === "string" && typeof field.ct === "string" && typeof field.alg === "string";
3172
3872
  }
3173
- function toBase64(bytes) {
3873
+ function toBase642(bytes) {
3174
3874
  let binary = "";
3175
3875
  for (let i = 0; i < bytes.length; i++) {
3176
3876
  binary += String.fromCharCode(bytes[i]);
3177
3877
  }
3178
3878
  return btoa(binary);
3179
3879
  }
3180
- function fromBase64(str) {
3880
+ function fromBase642(str) {
3181
3881
  const binary = atob(str);
3182
3882
  const bytes = new Uint8Array(binary.length);
3183
3883
  for (let i = 0; i < binary.length; i++) {
@@ -3190,6 +3890,7 @@ function fromBase64(str) {
3190
3890
  AwarenessManager,
3191
3891
  ChaosTransport,
3192
3892
  ConnectionMonitor,
3893
+ DEFAULT_RICHTEXT_DOC_CHANNEL_THRESHOLD,
3193
3894
  DecryptionError,
3194
3895
  EncryptionError,
3195
3896
  HttpLongPollingTransport,
@@ -3200,25 +3901,39 @@ function fromBase64(str) {
3200
3901
  OutboundQueue,
3201
3902
  ProtobufMessageSerializer,
3202
3903
  ReconnectionManager,
3904
+ RichtextDocChannel,
3905
+ SCHEMA_MISMATCH_PREFIX,
3203
3906
  SYNC_STATES,
3204
3907
  SYNC_STATUSES,
3205
3908
  ScopeViolationError,
3206
3909
  SyncEncryptor,
3207
3910
  SyncEngine,
3208
3911
  WebSocketTransport,
3912
+ createDeltaCursorFromBatch,
3913
+ decodeDeltaCursor,
3914
+ decodeYjsUpdate,
3915
+ dedupeQuerySubsets,
3209
3916
  deriveKey,
3210
3917
  deriveVersionedKey,
3918
+ encodeDeltaCursor,
3919
+ encodeYjsUpdate,
3211
3920
  filterOperationsByScope,
3212
3921
  generateSalt,
3213
3922
  isAcknowledgmentMessage,
3214
3923
  isAwarenessUpdateMessage,
3924
+ isClientSchemaVersionSupported,
3215
3925
  isEncryptedPayload,
3216
3926
  isErrorMessage,
3217
3927
  isHandshakeMessage,
3218
3928
  isHandshakeResponseMessage,
3219
3929
  isOperationBatchMessage,
3930
+ isSchemaMismatchReject,
3220
3931
  isSyncMessage,
3932
+ isYjsDocUpdateMessage,
3933
+ operationMatchesQuerySubsets,
3221
3934
  operationMatchesScope,
3935
+ richtextDocKey,
3936
+ sliceOperationsAfterCursor,
3222
3937
  versionVectorToWire,
3223
3938
  wireToVersionVector
3224
3939
  });