@korajs/sync 0.4.0 → 0.6.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.js CHANGED
@@ -12,7 +12,14 @@ var SYNC_STATES = [
12
12
  "streaming",
13
13
  "error"
14
14
  ];
15
- var SYNC_STATUSES = ["connected", "syncing", "synced", "offline", "error"];
15
+ var SYNC_STATUSES = [
16
+ "connected",
17
+ "syncing",
18
+ "synced",
19
+ "offline",
20
+ "error",
21
+ "schema-mismatch"
22
+ ];
16
23
  var ScopeViolationError = class extends KoraError {
17
24
  constructor(operationId, collection, scope, message) {
18
25
  super(
@@ -37,12 +44,12 @@ var InvalidScopeError = class extends KoraError {
37
44
  };
38
45
 
39
46
  // src/scopes/scope-filter.ts
40
- function operationMatchesScope(op, scopeMap) {
47
+ function operationMatchesScope(op, scopeMap, fullRecord) {
41
48
  if (!scopeMap) return true;
42
49
  const collectionScope = scopeMap[op.collection];
43
50
  if (!collectionScope) return false;
44
51
  if (Object.keys(collectionScope).length === 0) return true;
45
- const snapshot = buildSnapshot(op);
52
+ const snapshot = buildSnapshot(op, fullRecord ?? void 0);
46
53
  if (!snapshot) return false;
47
54
  for (const [field, expected] of Object.entries(collectionScope)) {
48
55
  if (snapshot[field] !== expected) {
@@ -55,11 +62,12 @@ function filterOperationsByScope(operations, scopeMap) {
55
62
  if (!scopeMap) return operations;
56
63
  return operations.filter((op) => operationMatchesScope(op, scopeMap));
57
64
  }
58
- function buildSnapshot(op) {
65
+ function buildSnapshot(op, fullRecord) {
59
66
  const previous = asRecord(op.previousData);
60
67
  const next = asRecord(op.data);
61
- if (!previous && !next) return null;
68
+ if (!previous && !next && !fullRecord) return null;
62
69
  return {
70
+ ...fullRecord ?? {},
63
71
  ...previous ?? {},
64
72
  ...next ?? {}
65
73
  };
@@ -71,6 +79,128 @@ function asRecord(value) {
71
79
  return value;
72
80
  }
73
81
 
82
+ // src/scopes/query-subset.ts
83
+ function asRecord2(value) {
84
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
85
+ return null;
86
+ }
87
+ return value;
88
+ }
89
+ function buildSnapshot2(op, fullRecord) {
90
+ const previous = asRecord2(op.previousData);
91
+ const next = asRecord2(op.data);
92
+ if (!previous && !next && !fullRecord) {
93
+ return null;
94
+ }
95
+ return {
96
+ ...fullRecord ?? {},
97
+ ...previous ?? {},
98
+ ...next ?? {}
99
+ };
100
+ }
101
+ function recordMatchesWhere(snapshot, where) {
102
+ for (const [field, expected] of Object.entries(where)) {
103
+ if (snapshot[field] !== expected) {
104
+ return false;
105
+ }
106
+ }
107
+ return true;
108
+ }
109
+ function operationMatchesQuerySubsets(op, subsets, fullRecord) {
110
+ if (!subsets || subsets.length === 0) {
111
+ return true;
112
+ }
113
+ const collectionSubsets = subsets.filter((subset) => subset.collection === op.collection);
114
+ if (collectionSubsets.length === 0) {
115
+ return true;
116
+ }
117
+ const snapshot = buildSnapshot2(op, fullRecord);
118
+ if (!snapshot) {
119
+ return false;
120
+ }
121
+ return collectionSubsets.some((subset) => recordMatchesWhere(snapshot, subset.where));
122
+ }
123
+ function dedupeQuerySubsets(subsets) {
124
+ const seen = /* @__PURE__ */ new Set();
125
+ const result = [];
126
+ for (const subset of subsets) {
127
+ const key = `${subset.collection}:${JSON.stringify(subset.where)}`;
128
+ if (seen.has(key)) {
129
+ continue;
130
+ }
131
+ seen.add(key);
132
+ result.push(subset);
133
+ }
134
+ return result;
135
+ }
136
+
137
+ // src/delta/delta-cursor.ts
138
+ function bytesToBase64Url(bytes) {
139
+ let binary = "";
140
+ for (const byte of bytes) {
141
+ binary += String.fromCharCode(byte);
142
+ }
143
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
144
+ }
145
+ function base64UrlToBytes(value) {
146
+ const base64 = value.replace(/-/g, "+").replace(/_/g, "/");
147
+ const padded = base64 + "=".repeat((4 - base64.length % 4) % 4);
148
+ const binary = atob(padded);
149
+ const bytes = new Uint8Array(binary.length);
150
+ for (let i = 0; i < binary.length; i++) {
151
+ bytes[i] = binary.charCodeAt(i);
152
+ }
153
+ return bytes;
154
+ }
155
+ function encodeDeltaCursor(cursor) {
156
+ const json = JSON.stringify(cursor);
157
+ return bytesToBase64Url(new TextEncoder().encode(json));
158
+ }
159
+ function decodeDeltaCursor(value) {
160
+ if (!value) {
161
+ return null;
162
+ }
163
+ try {
164
+ const json = new TextDecoder().decode(base64UrlToBytes(value));
165
+ const parsed = JSON.parse(json);
166
+ if (typeof parsed !== "object" || parsed === null || typeof parsed.lastOperationId !== "string" || typeof parsed.batchIndex !== "number") {
167
+ return null;
168
+ }
169
+ return parsed;
170
+ } catch {
171
+ return null;
172
+ }
173
+ }
174
+ function sliceOperationsAfterCursor(operations, cursor) {
175
+ if (!cursor) {
176
+ return operations;
177
+ }
178
+ const index = operations.findIndex((op) => op.id === cursor.lastOperationId);
179
+ if (index === -1) {
180
+ return operations;
181
+ }
182
+ return operations.slice(index + 1);
183
+ }
184
+ function createDeltaCursorFromBatch(operations, batchIndex) {
185
+ const last = operations[operations.length - 1];
186
+ if (!last) {
187
+ return null;
188
+ }
189
+ return {
190
+ lastOperationId: last.id,
191
+ batchIndex
192
+ };
193
+ }
194
+
195
+ // src/protocol/schema-version.ts
196
+ var SCHEMA_MISMATCH_PREFIX = "SCHEMA_MISMATCH";
197
+ function isSchemaMismatchReject(rejectReason) {
198
+ return rejectReason?.startsWith(SCHEMA_MISMATCH_PREFIX) === true;
199
+ }
200
+ function isClientSchemaVersionSupported(clientVersion, supported) {
201
+ return clientVersion >= supported.min && clientVersion <= supported.max;
202
+ }
203
+
74
204
  // src/protocol/messages.ts
75
205
  function isSyncMessage(value) {
76
206
  if (typeof value !== "object" || value === null) return false;
@@ -89,6 +219,8 @@ function isSyncMessage(value) {
89
219
  return isErrorMessage(value);
90
220
  case "awareness-update":
91
221
  return isAwarenessUpdateMessage(value);
222
+ case "yjs-doc-update":
223
+ return isYjsDocUpdateMessage(value);
92
224
  default:
93
225
  return false;
94
226
  }
@@ -123,6 +255,11 @@ function isAwarenessUpdateMessage(value) {
123
255
  const msg = value;
124
256
  return msg.type === "awareness-update" && typeof msg.messageId === "string" && typeof msg.clientId === "number" && typeof msg.states === "object" && msg.states !== null && !Array.isArray(msg.states);
125
257
  }
258
+ function isYjsDocUpdateMessage(value) {
259
+ if (typeof value !== "object" || value === null) return false;
260
+ const msg = value;
261
+ 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";
262
+ }
126
263
 
127
264
  // src/protocol/serializer.ts
128
265
  import { SyncError } from "@korajs/core";
@@ -138,6 +275,84 @@ function versionVectorToWire(vector) {
138
275
  function wireToVersionVector(wire) {
139
276
  return new Map(Object.entries(wire));
140
277
  }
278
+ var WIRE_BYTES_KEY = "__kora_bytes__";
279
+ function toBase64(bytes) {
280
+ let binary = "";
281
+ for (const byte of bytes) {
282
+ binary += String.fromCharCode(byte);
283
+ }
284
+ return btoa(binary);
285
+ }
286
+ function fromBase64(value) {
287
+ const binary = atob(value);
288
+ const bytes = new Uint8Array(binary.length);
289
+ for (let index = 0; index < binary.length; index++) {
290
+ bytes[index] = binary.charCodeAt(index);
291
+ }
292
+ return bytes;
293
+ }
294
+ function serializeWireRecord(record) {
295
+ if (!record) {
296
+ return null;
297
+ }
298
+ const out = {};
299
+ for (const [key, value] of Object.entries(record)) {
300
+ out[key] = serializeWireValue(value);
301
+ }
302
+ return out;
303
+ }
304
+ function serializeWireValue(value) {
305
+ if (value instanceof Uint8Array) {
306
+ return { [WIRE_BYTES_KEY]: toBase64(value) };
307
+ }
308
+ if (value instanceof ArrayBuffer) {
309
+ return { [WIRE_BYTES_KEY]: toBase64(new Uint8Array(value)) };
310
+ }
311
+ return value;
312
+ }
313
+ function deserializeWireRecord(record) {
314
+ if (!record) {
315
+ return null;
316
+ }
317
+ const out = {};
318
+ for (const [key, value] of Object.entries(record)) {
319
+ out[key] = deserializeWireValue(value);
320
+ }
321
+ return out;
322
+ }
323
+ function deserializeWireValue(value) {
324
+ if (typeof value === "object" && value !== null && WIRE_BYTES_KEY in value) {
325
+ const encoded = value[WIRE_BYTES_KEY];
326
+ if (typeof encoded === "string") {
327
+ return fromBase64(encoded);
328
+ }
329
+ }
330
+ if (isLegacyIndexedByteRecord(value)) {
331
+ return legacyIndexedByteRecordToUint8Array(value);
332
+ }
333
+ return value;
334
+ }
335
+ function isLegacyIndexedByteRecord(value) {
336
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
337
+ return false;
338
+ }
339
+ const entries = Object.entries(value);
340
+ if (entries.length === 0) {
341
+ return false;
342
+ }
343
+ return entries.every(([key, entryValue]) => {
344
+ const index = Number(key);
345
+ return Number.isInteger(index) && index >= 0 && typeof entryValue === "number";
346
+ });
347
+ }
348
+ function legacyIndexedByteRecordToUint8Array(value) {
349
+ const maxIndex = Math.max(...Object.keys(value).map((key) => Number(key)));
350
+ const bytes = new Uint8Array(maxIndex + 1);
351
+ for (const [key, entryValue] of Object.entries(value)) {
352
+ bytes[Number(key)] = entryValue;
353
+ }
354
+ return bytes;
355
+ }
141
356
  var JsonMessageSerializer = class {
142
357
  encode(message) {
143
358
  return JSON.stringify(message);
@@ -166,8 +381,8 @@ var JsonMessageSerializer = class {
166
381
  type: op.type,
167
382
  collection: op.collection,
168
383
  recordId: op.recordId,
169
- data: op.data,
170
- previousData: op.previousData,
384
+ data: serializeWireRecord(op.data),
385
+ previousData: serializeWireRecord(op.previousData),
171
386
  timestamp: {
172
387
  wallTime: op.timestamp.wallTime,
173
388
  logical: op.timestamp.logical,
@@ -188,8 +403,8 @@ var JsonMessageSerializer = class {
188
403
  type: serialized.type,
189
404
  collection: serialized.collection,
190
405
  recordId: serialized.recordId,
191
- data: serialized.data,
192
- previousData: serialized.previousData,
406
+ data: deserializeWireRecord(serialized.data),
407
+ previousData: deserializeWireRecord(serialized.previousData),
193
408
  timestamp: {
194
409
  wallTime: serialized.timestamp.wallTime,
195
410
  logical: serialized.timestamp.logical,
@@ -310,6 +525,7 @@ function toProtoEnvelope(message) {
310
525
  retriable: message.retriable
311
526
  };
312
527
  case "awareness-update":
528
+ case "yjs-doc-update":
313
529
  return {
314
530
  type: message.type,
315
531
  messageId: message.messageId
@@ -1166,9 +1382,222 @@ var ChaosTransport = class {
1166
1382
  };
1167
1383
 
1168
1384
  // src/engine/sync-engine.ts
1169
- import { SyncError as SyncError4 } from "@korajs/core";
1385
+ import {
1386
+ APPLY_FAILURE_CODES,
1387
+ ClockDriftError,
1388
+ KoraError as KoraError2,
1389
+ SyncError as SyncError4,
1390
+ applyOperationTransforms,
1391
+ defaultApplyFailureReason
1392
+ } from "@korajs/core";
1170
1393
  import { topologicalSort as topologicalSort2 } from "@korajs/core/internal";
1171
1394
 
1395
+ // src/awareness/awareness-manager.ts
1396
+ var DEFAULT_TIMEOUT_MS = 3e4;
1397
+ var nextLocalClientId = 1;
1398
+ var AwarenessManager = class {
1399
+ /** Unique client ID for this instance */
1400
+ clientId;
1401
+ localState = null;
1402
+ remoteStates = /* @__PURE__ */ new Map();
1403
+ listeners = /* @__PURE__ */ new Set();
1404
+ emitter;
1405
+ timeoutMs;
1406
+ // Track when we last received an update from each remote client.
1407
+ // Used for timeout-based cleanup as a safety net in case the server
1408
+ // does not send an explicit removal on disconnect.
1409
+ lastUpdated = /* @__PURE__ */ new Map();
1410
+ cleanupTimer = null;
1411
+ sendHandler = null;
1412
+ destroyed = false;
1413
+ constructor(options) {
1414
+ this.clientId = options?.clientId ?? nextLocalClientId++;
1415
+ this.emitter = options?.emitter ?? null;
1416
+ this.timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
1417
+ }
1418
+ /**
1419
+ * Set the local user's awareness state and broadcast to peers.
1420
+ *
1421
+ * @param state - The awareness state to share. Pass null to clear presence.
1422
+ */
1423
+ setLocalState(state) {
1424
+ if (this.destroyed) return;
1425
+ this.localState = state;
1426
+ const message = {
1427
+ type: "awareness",
1428
+ clientId: this.clientId,
1429
+ states: { [this.clientId]: state }
1430
+ };
1431
+ this.sendHandler?.(message);
1432
+ this.emitAwarenessEvent();
1433
+ }
1434
+ /**
1435
+ * Get the local awareness state.
1436
+ */
1437
+ getLocalState() {
1438
+ return this.localState;
1439
+ }
1440
+ /**
1441
+ * Get all known awareness states (local + remote).
1442
+ * Returns a new Map on each call.
1443
+ */
1444
+ getStates() {
1445
+ const result = /* @__PURE__ */ new Map();
1446
+ if (this.localState) {
1447
+ result.set(this.clientId, this.localState);
1448
+ }
1449
+ for (const [id, state] of this.remoteStates) {
1450
+ result.set(id, state);
1451
+ }
1452
+ return result;
1453
+ }
1454
+ /**
1455
+ * Handle an incoming awareness message from the transport.
1456
+ * This processes remote state updates and notifies listeners.
1457
+ */
1458
+ handleRemoteMessage(message) {
1459
+ if (this.destroyed) return;
1460
+ const added = [];
1461
+ const updated = [];
1462
+ const removed = [];
1463
+ const now = Date.now();
1464
+ for (const [clientIdStr, state] of Object.entries(message.states)) {
1465
+ const clientId = Number(clientIdStr);
1466
+ if (clientId === this.clientId) continue;
1467
+ if (state === null) {
1468
+ if (this.remoteStates.has(clientId)) {
1469
+ this.remoteStates.delete(clientId);
1470
+ this.lastUpdated.delete(clientId);
1471
+ removed.push(clientId);
1472
+ }
1473
+ } else {
1474
+ if (this.remoteStates.has(clientId)) {
1475
+ this.remoteStates.set(clientId, state);
1476
+ this.lastUpdated.set(clientId, now);
1477
+ updated.push(clientId);
1478
+ } else {
1479
+ this.remoteStates.set(clientId, state);
1480
+ this.lastUpdated.set(clientId, now);
1481
+ added.push(clientId);
1482
+ }
1483
+ }
1484
+ }
1485
+ if (added.length > 0 || updated.length > 0 || removed.length > 0) {
1486
+ const change = { added, updated, removed };
1487
+ this.notifyListeners(change);
1488
+ this.emitAwarenessEvent();
1489
+ }
1490
+ }
1491
+ /**
1492
+ * Remove a specific remote client's awareness state.
1493
+ * Called when the server notifies that a client has disconnected.
1494
+ */
1495
+ removeClient(clientId) {
1496
+ if (this.destroyed) return;
1497
+ if (!this.remoteStates.has(clientId)) return;
1498
+ this.remoteStates.delete(clientId);
1499
+ this.lastUpdated.delete(clientId);
1500
+ const change = {
1501
+ added: [],
1502
+ updated: [],
1503
+ removed: [clientId]
1504
+ };
1505
+ this.notifyListeners(change);
1506
+ this.emitAwarenessEvent();
1507
+ }
1508
+ /**
1509
+ * Register a handler for sending awareness messages through the transport.
1510
+ * The sync engine calls this to wire outgoing awareness messages to the transport.
1511
+ */
1512
+ onSend(handler) {
1513
+ this.sendHandler = handler;
1514
+ }
1515
+ /**
1516
+ * Register a listener for awareness state changes.
1517
+ * Returns an unsubscribe function.
1518
+ */
1519
+ on(_event, listener) {
1520
+ this.listeners.add(listener);
1521
+ return () => {
1522
+ this.listeners.delete(listener);
1523
+ };
1524
+ }
1525
+ /**
1526
+ * Remove a specific change listener.
1527
+ */
1528
+ off(_event, listener) {
1529
+ this.listeners.delete(listener);
1530
+ }
1531
+ /**
1532
+ * Start the cleanup timer that removes stale remote states.
1533
+ * Called when the sync engine transitions to streaming state.
1534
+ */
1535
+ startCleanupTimer() {
1536
+ if (this.cleanupTimer) return;
1537
+ this.cleanupTimer = setInterval(() => {
1538
+ this.cleanupStaleStates();
1539
+ }, this.timeoutMs);
1540
+ }
1541
+ /**
1542
+ * Stop the cleanup timer.
1543
+ */
1544
+ stopCleanupTimer() {
1545
+ if (this.cleanupTimer) {
1546
+ clearInterval(this.cleanupTimer);
1547
+ this.cleanupTimer = null;
1548
+ }
1549
+ }
1550
+ /**
1551
+ * Clean up all resources. After calling destroy(), the manager
1552
+ * will no longer send or receive awareness updates.
1553
+ * Broadcasts removal of local state before shutting down.
1554
+ */
1555
+ destroy() {
1556
+ if (this.destroyed) return;
1557
+ this.destroyed = true;
1558
+ if (this.localState) {
1559
+ const message = {
1560
+ type: "awareness",
1561
+ clientId: this.clientId,
1562
+ states: { [this.clientId]: null }
1563
+ };
1564
+ this.sendHandler?.(message);
1565
+ }
1566
+ this.localState = null;
1567
+ this.remoteStates.clear();
1568
+ this.lastUpdated.clear();
1569
+ this.listeners.clear();
1570
+ this.sendHandler = null;
1571
+ this.stopCleanupTimer();
1572
+ }
1573
+ // --- Private ---
1574
+ cleanupStaleStates() {
1575
+ const now = Date.now();
1576
+ const removed = [];
1577
+ for (const [clientId, lastTime] of this.lastUpdated) {
1578
+ if (now - lastTime > this.timeoutMs) {
1579
+ this.remoteStates.delete(clientId);
1580
+ this.lastUpdated.delete(clientId);
1581
+ removed.push(clientId);
1582
+ }
1583
+ }
1584
+ if (removed.length > 0) {
1585
+ const change = { added: [], updated: [], removed };
1586
+ this.notifyListeners(change);
1587
+ this.emitAwarenessEvent();
1588
+ }
1589
+ }
1590
+ notifyListeners(change) {
1591
+ for (const listener of this.listeners) {
1592
+ listener(change);
1593
+ }
1594
+ }
1595
+ emitAwarenessEvent() {
1596
+ const states = this.getStates();
1597
+ this.emitter?.emit({ type: "awareness:updated", states });
1598
+ }
1599
+ };
1600
+
1172
1601
  // src/diagnostics/bandwidth-estimator.ts
1173
1602
  var BandwidthEstimator = class {
1174
1603
  maxSamples;
@@ -1210,6 +1639,7 @@ var BandwidthEstimator = class {
1210
1639
  const decayFactor = 0.9;
1211
1640
  for (let i = this.samples.length - 1; i >= 0; i--) {
1212
1641
  const sample = this.samples[i];
1642
+ if (!sample) continue;
1213
1643
  const bytesPerSec = sample.bytes / sample.durationMs * 1e3;
1214
1644
  const age = this.samples.length - 1 - i;
1215
1645
  const weight = decayFactor ** age;
@@ -1275,7 +1705,7 @@ var SlidingWindowPercentile = class {
1275
1705
  const sorted = this.samples.slice(0, this.count).sort((a, b) => a - b);
1276
1706
  const rank = Math.ceil(p / 100 * sorted.length) - 1;
1277
1707
  const index = Math.max(0, Math.min(rank, sorted.length - 1));
1278
- return sorted[index];
1708
+ return sorted[index] ?? 0;
1279
1709
  }
1280
1710
  /**
1281
1711
  * Get the most recently added sample, or 0 if no samples exist.
@@ -1283,7 +1713,7 @@ var SlidingWindowPercentile = class {
1283
1713
  latest() {
1284
1714
  if (this.count === 0) return 0;
1285
1715
  const lastIndex = (this.writeIndex - 1 + this.maxSize) % this.maxSize;
1286
- return this.samples[lastIndex];
1716
+ return this.samples[lastIndex] ?? 0;
1287
1717
  }
1288
1718
  /**
1289
1719
  * Get the number of samples currently in the window.
@@ -1610,210 +2040,107 @@ var SyncMetricsCollector = class {
1610
2040
  }
1611
2041
  };
1612
2042
 
1613
- // src/awareness/awareness-manager.ts
1614
- var DEFAULT_TIMEOUT_MS = 3e4;
1615
- var nextLocalClientId = 1;
1616
- var AwarenessManager = class {
1617
- /** Unique client ID for this instance */
1618
- clientId;
1619
- localState = null;
1620
- remoteStates = /* @__PURE__ */ new Map();
1621
- listeners = /* @__PURE__ */ new Set();
1622
- emitter;
1623
- timeoutMs;
1624
- // Track when we last received an update from each remote client.
1625
- // Used for timeout-based cleanup as a safety net in case the server
1626
- // does not send an explicit removal on disconnect.
1627
- lastUpdated = /* @__PURE__ */ new Map();
1628
- cleanupTimer = null;
1629
- sendHandler = null;
1630
- destroyed = false;
1631
- constructor(options) {
1632
- this.clientId = options?.clientId ?? nextLocalClientId++;
1633
- this.emitter = options?.emitter ?? null;
1634
- this.timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
1635
- }
1636
- /**
1637
- * Set the local user's awareness state and broadcast to peers.
1638
- *
1639
- * @param state - The awareness state to share. Pass null to clear presence.
1640
- */
1641
- setLocalState(state) {
1642
- if (this.destroyed) return;
1643
- this.localState = state;
1644
- const message = {
1645
- type: "awareness",
1646
- clientId: this.clientId,
1647
- states: { [this.clientId]: state }
1648
- };
1649
- this.sendHandler?.(message);
1650
- this.emitAwarenessEvent();
1651
- }
1652
- /**
1653
- * Get the local awareness state.
1654
- */
1655
- getLocalState() {
1656
- return this.localState;
1657
- }
1658
- /**
1659
- * Get all known awareness states (local + remote).
1660
- * Returns a new Map on each call.
1661
- */
1662
- getStates() {
1663
- const result = /* @__PURE__ */ new Map();
1664
- if (this.localState) {
1665
- result.set(this.clientId, this.localState);
1666
- }
1667
- for (const [id, state] of this.remoteStates) {
1668
- result.set(id, state);
1669
- }
1670
- return result;
1671
- }
1672
- /**
1673
- * Handle an incoming awareness message from the transport.
1674
- * This processes remote state updates and notifies listeners.
1675
- */
1676
- handleRemoteMessage(message) {
1677
- if (this.destroyed) return;
1678
- const added = [];
1679
- const updated = [];
1680
- const removed = [];
1681
- const now = Date.now();
1682
- for (const [clientIdStr, state] of Object.entries(message.states)) {
1683
- const clientId = Number(clientIdStr);
1684
- if (clientId === this.clientId) continue;
1685
- if (state === null) {
1686
- if (this.remoteStates.has(clientId)) {
1687
- this.remoteStates.delete(clientId);
1688
- this.lastUpdated.delete(clientId);
1689
- removed.push(clientId);
1690
- }
1691
- } else {
1692
- if (this.remoteStates.has(clientId)) {
1693
- this.remoteStates.set(clientId, state);
1694
- this.lastUpdated.set(clientId, now);
1695
- updated.push(clientId);
1696
- } else {
1697
- this.remoteStates.set(clientId, state);
1698
- this.lastUpdated.set(clientId, now);
1699
- added.push(clientId);
1700
- }
1701
- }
1702
- }
1703
- if (added.length > 0 || updated.length > 0 || removed.length > 0) {
1704
- const change = { added, updated, removed };
1705
- this.notifyListeners(change);
1706
- this.emitAwarenessEvent();
1707
- }
1708
- }
1709
- /**
1710
- * Remove a specific remote client's awareness state.
1711
- * Called when the server notifies that a client has disconnected.
1712
- */
1713
- removeClient(clientId) {
1714
- if (this.destroyed) return;
1715
- if (!this.remoteStates.has(clientId)) return;
1716
- this.remoteStates.delete(clientId);
1717
- this.lastUpdated.delete(clientId);
1718
- const change = {
1719
- added: [],
1720
- updated: [],
1721
- removed: [clientId]
1722
- };
1723
- this.notifyListeners(change);
1724
- this.emitAwarenessEvent();
1725
- }
1726
- /**
1727
- * Register a handler for sending awareness messages through the transport.
1728
- * The sync engine calls this to wire outgoing awareness messages to the transport.
1729
- */
1730
- onSend(handler) {
1731
- this.sendHandler = handler;
2043
+ // src/richtext/richtext-doc-channel.ts
2044
+ import { generateUUIDv7 } from "@korajs/core";
2045
+
2046
+ // src/richtext/doc-channel-wire.ts
2047
+ function encodeYjsUpdate(update) {
2048
+ let binary = "";
2049
+ for (let i = 0; i < update.length; i++) {
2050
+ binary += String.fromCharCode(update[i]);
1732
2051
  }
1733
- /**
1734
- * Register a listener for awareness state changes.
1735
- * Returns an unsubscribe function.
1736
- */
1737
- on(_event, listener) {
1738
- this.listeners.add(listener);
1739
- return () => {
1740
- this.listeners.delete(listener);
1741
- };
2052
+ return btoa(binary);
2053
+ }
2054
+ function decodeYjsUpdate(encoded) {
2055
+ const binary = atob(encoded);
2056
+ const bytes = new Uint8Array(binary.length);
2057
+ for (let i = 0; i < binary.length; i++) {
2058
+ bytes[i] = binary.charCodeAt(i);
1742
2059
  }
1743
- /**
1744
- * Remove a specific change listener.
1745
- */
1746
- off(_event, listener) {
1747
- this.listeners.delete(listener);
2060
+ return bytes;
2061
+ }
2062
+ function richtextDocKey(collection, recordId, field) {
2063
+ return `${collection}:${recordId}:${field}`;
2064
+ }
2065
+
2066
+ // src/richtext/richtext-doc-channel.ts
2067
+ var DEFAULT_RICHTEXT_DOC_CHANNEL_THRESHOLD = 4096;
2068
+ var RichtextDocChannel = class {
2069
+ threshold;
2070
+ onSend;
2071
+ listeners = /* @__PURE__ */ new Map();
2072
+ constructor(options) {
2073
+ this.threshold = options?.largeDocThreshold ?? DEFAULT_RICHTEXT_DOC_CHANNEL_THRESHOLD;
2074
+ this.onSend = options?.onSend ?? null;
1748
2075
  }
1749
2076
  /**
1750
- * Start the cleanup timer that removes stale remote states.
1751
- * Called when the sync engine transitions to streaming state.
2077
+ * Whether the doc channel should be used for a field.
2078
+ * @param preference - `true` forces on, `false` forces off, `undefined` uses size threshold
1752
2079
  */
1753
- startCleanupTimer() {
1754
- if (this.cleanupTimer) return;
1755
- this.cleanupTimer = setInterval(() => {
1756
- this.cleanupStaleStates();
1757
- }, this.timeoutMs);
2080
+ shouldUseChannel(snapshotByteLength, preference) {
2081
+ if (preference === false) {
2082
+ return false;
2083
+ }
2084
+ if (preference === true) {
2085
+ return true;
2086
+ }
2087
+ return snapshotByteLength >= this.threshold;
2088
+ }
2089
+ getThreshold() {
2090
+ return this.threshold;
1758
2091
  }
1759
2092
  /**
1760
- * Stop the cleanup timer.
2093
+ * Subscribe to incremental updates for a document key.
1761
2094
  */
1762
- stopCleanupTimer() {
1763
- if (this.cleanupTimer) {
1764
- clearInterval(this.cleanupTimer);
1765
- this.cleanupTimer = null;
2095
+ subscribe(collection, recordId, field, listener) {
2096
+ const key = richtextDocKey(collection, recordId, field);
2097
+ let set = this.listeners.get(key);
2098
+ if (!set) {
2099
+ set = /* @__PURE__ */ new Set();
2100
+ this.listeners.set(key, set);
1766
2101
  }
2102
+ set.add(listener);
2103
+ return () => {
2104
+ const current = this.listeners.get(key);
2105
+ if (!current) {
2106
+ return;
2107
+ }
2108
+ current.delete(listener);
2109
+ if (current.size === 0) {
2110
+ this.listeners.delete(key);
2111
+ }
2112
+ };
1767
2113
  }
1768
2114
  /**
1769
- * Clean up all resources. After calling destroy(), the manager
1770
- * will no longer send or receive awareness updates.
1771
- * Broadcasts removal of local state before shutting down.
2115
+ * Publish a local incremental update to peers.
1772
2116
  */
1773
- destroy() {
1774
- if (this.destroyed) return;
1775
- this.destroyed = true;
1776
- if (this.localState) {
1777
- const message = {
1778
- type: "awareness",
1779
- clientId: this.clientId,
1780
- states: { [this.clientId]: null }
1781
- };
1782
- this.sendHandler?.(message);
2117
+ send(collection, recordId, field, update) {
2118
+ if (!this.onSend || update.length === 0) {
2119
+ return;
1783
2120
  }
1784
- this.localState = null;
1785
- this.remoteStates.clear();
1786
- this.lastUpdated.clear();
1787
- this.listeners.clear();
1788
- this.sendHandler = null;
1789
- this.stopCleanupTimer();
2121
+ this.onSend({
2122
+ type: "yjs-doc-update",
2123
+ messageId: generateUUIDv7(),
2124
+ collection,
2125
+ recordId,
2126
+ field,
2127
+ update: encodeYjsUpdate(update)
2128
+ });
1790
2129
  }
1791
- // --- Private ---
1792
- cleanupStaleStates() {
1793
- const now = Date.now();
1794
- const removed = [];
1795
- for (const [clientId, lastTime] of this.lastUpdated) {
1796
- if (now - lastTime > this.timeoutMs) {
1797
- this.remoteStates.delete(clientId);
1798
- this.lastUpdated.delete(clientId);
1799
- removed.push(clientId);
1800
- }
1801
- }
1802
- if (removed.length > 0) {
1803
- const change = { added: [], updated: [], removed };
1804
- this.notifyListeners(change);
1805
- this.emitAwarenessEvent();
2130
+ /**
2131
+ * Deliver a remote incremental update to local subscribers.
2132
+ */
2133
+ deliver(message) {
2134
+ const key = richtextDocKey(message.collection, message.recordId, message.field);
2135
+ const set = this.listeners.get(key);
2136
+ if (!set || set.size === 0) {
2137
+ return;
1806
2138
  }
1807
- }
1808
- notifyListeners(change) {
1809
- for (const listener of this.listeners) {
1810
- listener(change);
2139
+ const update = decodeYjsUpdate(message.update);
2140
+ for (const listener of set) {
2141
+ listener(update);
1811
2142
  }
1812
2143
  }
1813
- emitAwarenessEvent() {
1814
- const states = this.getStates();
1815
- this.emitter?.emit({ type: "awareness:updated", states });
1816
- }
1817
2144
  };
1818
2145
 
1819
2146
  // src/engine/outbound-queue.ts
@@ -1928,6 +2255,19 @@ var OutboundQueue = class {
1928
2255
  get isInitialized() {
1929
2256
  return this.initialized;
1930
2257
  }
2258
+ /**
2259
+ * Remove operations by id from queue and persistent storage.
2260
+ * Used when ops were already sent during handshake delta exchange.
2261
+ */
2262
+ async removeByIds(ids) {
2263
+ if (ids.length === 0) return;
2264
+ const idSet = new Set(ids);
2265
+ this.queue = this.queue.filter((op) => !idSet.has(op.id));
2266
+ for (const id of ids) {
2267
+ this.seen.delete(id);
2268
+ }
2269
+ await this.storage.dequeue(ids);
2270
+ }
1931
2271
  };
1932
2272
 
1933
2273
  // src/engine/sync-engine.ts
@@ -1942,6 +2282,7 @@ var VALID_TRANSITIONS = {
1942
2282
  error: ["disconnected"]
1943
2283
  };
1944
2284
  var nextMessageId = 0;
2285
+ var nextQuerySubsetId = 0;
1945
2286
  function generateMessageId() {
1946
2287
  return `msg-${Date.now()}-${nextMessageId++}`;
1947
2288
  }
@@ -1956,24 +2297,39 @@ var SyncEngine = class {
1956
2297
  batchSize;
1957
2298
  encryptor;
1958
2299
  awarenessManager;
2300
+ richtextDocChannel;
1959
2301
  metricsCollector;
2302
+ syncState;
1960
2303
  remoteVector = /* @__PURE__ */ new Map();
2304
+ lastAckedServerVector = /* @__PURE__ */ new Map();
2305
+ cachedUnsyncedCount = 0;
1961
2306
  lastSyncedAt = null;
1962
2307
  lastSuccessfulPush = null;
1963
2308
  lastSuccessfulPull = null;
1964
2309
  conflictCount = 0;
1965
2310
  currentBatch = null;
1966
2311
  reconnecting = false;
2312
+ schemaBlocked = false;
1967
2313
  // Track delta exchange state
1968
2314
  deltaBatchesReceived = 0;
1969
2315
  deltaReceiveComplete = false;
1970
2316
  deltaSendComplete = false;
2317
+ /** Op ids sent during handshake delta — removed from outbound queue to avoid duplicate send */
2318
+ deltaSentOpIds = [];
2319
+ /** Outbound delta batch message IDs awaiting ACK when strictHandshake is enabled */
2320
+ pendingDeltaBatchAcks = /* @__PURE__ */ new Set();
1971
2321
  /**
1972
2322
  * The effective scope for this sync session.
1973
2323
  * Starts as the configured scopeMap. After handshake, may be replaced
1974
2324
  * with the server-accepted scope (server is authoritative).
1975
2325
  */
1976
2326
  activeScope;
2327
+ /** Live query subsets registered from reactive subscriptions */
2328
+ querySubsets = /* @__PURE__ */ new Map();
2329
+ querySubsetReconnectTimer = null;
2330
+ /** Resume cursor for paginated initial sync (persisted across reconnects) */
2331
+ resumeDeltaCursor = null;
2332
+ initialSyncTotalBatches = 0;
1977
2333
  constructor(options) {
1978
2334
  this.transport = options.transport;
1979
2335
  this.store = options.store;
@@ -1982,6 +2338,7 @@ var SyncEngine = class {
1982
2338
  this.emitter = options.emitter ?? null;
1983
2339
  this.batchSize = options.config.batchSize ?? DEFAULT_BATCH_SIZE;
1984
2340
  this.encryptor = options.encryptor ?? null;
2341
+ this.syncState = options.syncState ?? null;
1985
2342
  this.activeScope = options.config.scopeMap;
1986
2343
  const queueStorage = options.queueStorage ?? new MemoryQueueStorage();
1987
2344
  this.outboundQueue = new OutboundQueue(queueStorage);
@@ -1992,6 +2349,15 @@ var SyncEngine = class {
1992
2349
  this.awarenessManager = new AwarenessManager({
1993
2350
  emitter: this.emitter ?? void 0
1994
2351
  });
2352
+ this.richtextDocChannel = new RichtextDocChannel({
2353
+ largeDocThreshold: options.config.richtextDocChannelThreshold,
2354
+ onSend: (message) => {
2355
+ if (this.state !== "streaming") {
2356
+ return;
2357
+ }
2358
+ this.transport.send(message);
2359
+ }
2360
+ });
1995
2361
  this.awarenessManager.onSend((message) => {
1996
2362
  if (this.state !== "streaming") return;
1997
2363
  const wireMessage = {
@@ -2013,15 +2379,30 @@ var SyncEngine = class {
2013
2379
  });
2014
2380
  }
2015
2381
  await this.outboundQueue.initialize();
2016
- this.transport.onMessage((msg) => this.handleMessage(msg));
2382
+ if (this.syncState) {
2383
+ this.lastAckedServerVector = await this.syncState.loadLastAckedServerVector();
2384
+ if (this.syncState.loadDeltaCursor) {
2385
+ this.resumeDeltaCursor = await this.syncState.loadDeltaCursor();
2386
+ }
2387
+ }
2388
+ await this.reconcileOutboundFromOpLog();
2389
+ await this.refreshPendingCount();
2390
+ this.transport.onMessage((msg) => this.enqueueMessage(msg));
2017
2391
  this.transport.onClose((reason) => this.handleTransportClose(reason));
2018
2392
  this.transport.onError((err) => this.handleTransportError(err));
2393
+ if (this.schemaBlocked) {
2394
+ throw new SyncError4(
2395
+ "Sync is blocked due to schema version mismatch. Upgrade the app schema or align sync.schemaVersion with the server.",
2396
+ { code: "SCHEMA_MISMATCH_BLOCKED" }
2397
+ );
2398
+ }
2019
2399
  this.transitionTo("connecting");
2020
2400
  try {
2021
2401
  const authToken = this.config.auth ? (await this.config.auth()).token : void 0;
2022
2402
  await this.transport.connect(this.config.url, { authToken });
2023
2403
  this.transitionTo("handshaking");
2024
2404
  const localVector = this.store.getVersionVector();
2405
+ const activeQuerySubsets = this.getActiveQuerySubsets();
2025
2406
  const handshake = {
2026
2407
  type: "handshake",
2027
2408
  messageId: generateMessageId(),
@@ -2030,7 +2411,9 @@ var SyncEngine = class {
2030
2411
  schemaVersion: this.config.schemaVersion ?? DEFAULT_SCHEMA_VERSION,
2031
2412
  authToken,
2032
2413
  supportedWireFormats: ["json", "protobuf"],
2033
- ...this.config.scopeMap ? { syncScope: this.config.scopeMap } : {}
2414
+ ...this.config.scopeMap ? { syncScope: this.config.scopeMap } : {},
2415
+ ...activeQuerySubsets.length > 0 ? { syncQueries: activeQuerySubsets } : {},
2416
+ ...this.resumeDeltaCursor ? { deltaCursor: encodeDeltaCursor(this.resumeDeltaCursor) } : {}
2034
2417
  };
2035
2418
  this.transport.send(handshake);
2036
2419
  } catch (err) {
@@ -2067,10 +2450,16 @@ var SyncEngine = class {
2067
2450
  * because they should remain local-only and not be sent to the server.
2068
2451
  */
2069
2452
  async pushOperation(op) {
2070
- if (!operationMatchesScope(op, this.activeScope)) {
2453
+ if (!this.operationAllowedForSync(op)) {
2071
2454
  return;
2072
2455
  }
2456
+ if (this.syncState) {
2457
+ this.cachedUnsyncedCount++;
2458
+ }
2073
2459
  await this.outboundQueue.enqueue(op);
2460
+ if (this.syncState) {
2461
+ await this.refreshPendingCount();
2462
+ }
2074
2463
  if (this.state === "streaming") {
2075
2464
  this.flushQueue();
2076
2465
  }
@@ -2088,7 +2477,7 @@ var SyncEngine = class {
2088
2477
  * Get the current developer-facing sync status.
2089
2478
  */
2090
2479
  getStatus() {
2091
- const pendingOperations = this.outboundQueue.totalPending;
2480
+ const pendingOperations = this.syncState ? this.cachedUnsyncedCount : this.outboundQueue.totalPending;
2092
2481
  const base = {
2093
2482
  pendingOperations,
2094
2483
  lastSyncedAt: this.lastSyncedAt,
@@ -2106,7 +2495,24 @@ var SyncEngine = class {
2106
2495
  case "streaming":
2107
2496
  return { ...base, status: pendingOperations > 0 ? "syncing" : "synced" };
2108
2497
  case "error":
2109
- return { ...base, status: "error" };
2498
+ return { ...base, status: this.schemaBlocked ? "schema-mismatch" : "error" };
2499
+ }
2500
+ }
2501
+ /**
2502
+ * True when the server rejected the client's schema version at handshake.
2503
+ * Sync stays blocked until the app schema is upgraded or sync config changes.
2504
+ */
2505
+ isSchemaBlocked() {
2506
+ return this.schemaBlocked;
2507
+ }
2508
+ /**
2509
+ * Clears schema-mismatch block after upgrading the local schema / sync config.
2510
+ * Moves the engine back to `disconnected` so `start()` can run again.
2511
+ */
2512
+ clearSchemaBlock() {
2513
+ this.schemaBlocked = false;
2514
+ if (this.state === "error") {
2515
+ this.transitionTo("disconnected");
2110
2516
  }
2111
2517
  }
2112
2518
  /**
@@ -2116,11 +2522,31 @@ var SyncEngine = class {
2116
2522
  recordConflict() {
2117
2523
  this.conflictCount++;
2118
2524
  }
2525
+ /**
2526
+ * Count of local operations not yet covered by the last acked server vector (op-log source of truth).
2527
+ */
2528
+ async getUnsyncedOperationCount() {
2529
+ await this.refreshPendingCount();
2530
+ return this.getStatus().pendingOperations;
2531
+ }
2532
+ emitApplyFailure(op, result, overrides) {
2533
+ const reason = defaultApplyFailureReason(result, overrides);
2534
+ this.emitter?.emit({
2535
+ type: "sync:apply-failed",
2536
+ operationId: op.id,
2537
+ collection: op.collection,
2538
+ recordId: op.recordId,
2539
+ code: reason.code,
2540
+ message: reason.message,
2541
+ retriable: reason.retriable
2542
+ });
2543
+ }
2119
2544
  /**
2120
2545
  * Force an immediate reconnection attempt. If the engine is disconnected
2121
2546
  * or in error state, restarts the sync. If already connected, no-op.
2122
2547
  */
2123
2548
  async retryNow() {
2549
+ if (this.schemaBlocked) return;
2124
2550
  if (this.state === "disconnected" || this.state === "error") {
2125
2551
  this.reconnecting = false;
2126
2552
  await this.start();
@@ -2181,6 +2607,25 @@ var SyncEngine = class {
2181
2607
  getActiveScope() {
2182
2608
  return this.activeScope;
2183
2609
  }
2610
+ /**
2611
+ * Register a live query subset that narrows synced data for a collection.
2612
+ * Takes effect on the next connection; reconnects when already connected.
2613
+ */
2614
+ registerQuerySubset(subset) {
2615
+ const id = `query-${nextQuerySubsetId++}`;
2616
+ this.querySubsets.set(id, subset);
2617
+ this.scheduleQuerySubsetReconnect();
2618
+ return () => {
2619
+ this.querySubsets.delete(id);
2620
+ this.scheduleQuerySubsetReconnect();
2621
+ };
2622
+ }
2623
+ /**
2624
+ * Returns deduplicated active query subsets from registered subscriptions.
2625
+ */
2626
+ getActiveQuerySubsets() {
2627
+ return dedupeQuerySubsets([...this.querySubsets.values()]);
2628
+ }
2184
2629
  /**
2185
2630
  * Get the awareness manager for collaborative presence.
2186
2631
  * Use this to set local presence, observe remote collaborators,
@@ -2189,14 +2634,24 @@ var SyncEngine = class {
2189
2634
  getAwarenessManager() {
2190
2635
  return this.awarenessManager;
2191
2636
  }
2637
+ /**
2638
+ * Optional side channel for incremental Yjs updates on large richtext fields.
2639
+ */
2640
+ getRichtextDocChannel() {
2641
+ return this.richtextDocChannel;
2642
+ }
2192
2643
  // --- Private methods ---
2193
- handleMessage(message) {
2644
+ messageChain = Promise.resolve();
2645
+ enqueueMessage(message) {
2646
+ this.messageChain = this.messageChain.then(() => this.handleMessageAsync(message)).catch((error) => this.handleMessageFailure(error));
2647
+ }
2648
+ async handleMessageAsync(message) {
2194
2649
  switch (message.type) {
2195
2650
  case "handshake-response":
2196
2651
  this.handleHandshakeResponse(message);
2197
2652
  break;
2198
2653
  case "operation-batch":
2199
- this.handleOperationBatch(message);
2654
+ await this.handleOperationBatch(message);
2200
2655
  break;
2201
2656
  case "acknowledgment":
2202
2657
  this.handleAcknowledgment(message);
@@ -2207,20 +2662,46 @@ var SyncEngine = class {
2207
2662
  case "awareness-update":
2208
2663
  this.handleAwarenessUpdate(message);
2209
2664
  break;
2665
+ case "yjs-doc-update":
2666
+ this.richtextDocChannel.deliver(message);
2667
+ break;
2210
2668
  }
2211
2669
  }
2670
+ handleMessageFailure(error) {
2671
+ const reason = error instanceof Error ? error.message : "Message handling failed";
2672
+ this.handleTransportClose(reason);
2673
+ }
2212
2674
  handleHandshakeResponse(msg) {
2213
2675
  if (this.state !== "handshaking") return;
2214
2676
  if (!msg.accepted) {
2677
+ const reason = msg.rejectReason ?? "Handshake rejected";
2678
+ if (isSchemaMismatchReject(msg.rejectReason)) {
2679
+ this.schemaBlocked = true;
2680
+ const supportedMin = msg.supportedSchemaMin ?? msg.schemaVersion;
2681
+ const supportedMax = msg.supportedSchemaMax ?? msg.schemaVersion;
2682
+ this.emitter?.emit({
2683
+ type: "sync:schema-mismatch",
2684
+ clientSchemaVersion: this.config.schemaVersion ?? DEFAULT_SCHEMA_VERSION,
2685
+ serverSchemaVersion: msg.schemaVersion,
2686
+ supportedMin,
2687
+ supportedMax,
2688
+ reason
2689
+ });
2690
+ this.metricsCollector.updateStatus("error");
2691
+ this.transitionTo("error");
2692
+ void this.transport.disconnect();
2693
+ return;
2694
+ }
2215
2695
  this.transitionTo("error");
2216
2696
  this.emitter?.emit({
2217
2697
  type: "sync:disconnected",
2218
- reason: msg.rejectReason ?? "Handshake rejected"
2698
+ reason
2219
2699
  });
2220
2700
  this.transitionTo("disconnected");
2221
2701
  return;
2222
2702
  }
2223
2703
  this.remoteVector = wireToVersionVector(msg.versionVector);
2704
+ void this.persistLastAckedServerVector(this.remoteVector);
2224
2705
  if (msg.selectedWireFormat) {
2225
2706
  this.setSerializerWireFormat(msg.selectedWireFormat);
2226
2707
  }
@@ -2235,38 +2716,57 @@ var SyncEngine = class {
2235
2716
  this.deltaBatchesReceived = 0;
2236
2717
  this.deltaReceiveComplete = false;
2237
2718
  this.deltaSendComplete = false;
2719
+ this.deltaSentOpIds = [];
2720
+ this.pendingDeltaBatchAcks.clear();
2721
+ this.initialSyncTotalBatches = 0;
2238
2722
  this.sendDelta();
2239
2723
  }
2240
2724
  async sendDelta() {
2241
2725
  const localVector = this.store.getVersionVector();
2242
2726
  const allMissingOps = await this.collectDelta(localVector, this.remoteVector);
2243
- const missingOps = allMissingOps.filter((op) => operationMatchesScope(op, this.activeScope));
2727
+ const missingOps = allMissingOps.filter((op) => this.operationAllowedForSync(op));
2728
+ this.deltaSentOpIds = missingOps.map((op) => op.id);
2244
2729
  if (missingOps.length === 0) {
2730
+ const messageId = generateMessageId();
2731
+ if (this.config.strictHandshake) {
2732
+ this.pendingDeltaBatchAcks.add(messageId);
2733
+ }
2245
2734
  const emptyBatch = {
2246
2735
  type: "operation-batch",
2247
- messageId: generateMessageId(),
2736
+ messageId,
2248
2737
  operations: [],
2249
2738
  isFinal: true,
2250
- batchIndex: 0
2739
+ batchIndex: 0,
2740
+ totalBatches: 1
2251
2741
  };
2252
2742
  this.transport.send(emptyBatch);
2253
- this.deltaSendComplete = true;
2254
- this.checkDeltaComplete();
2743
+ if (!this.config.strictHandshake) {
2744
+ this.deltaSendComplete = true;
2745
+ void this.checkDeltaComplete();
2746
+ }
2255
2747
  return;
2256
2748
  }
2257
2749
  const sorted = topologicalSort2(missingOps);
2258
2750
  const totalBatches = Math.ceil(sorted.length / this.batchSize);
2751
+ this.initialSyncTotalBatches = Math.max(this.initialSyncTotalBatches, totalBatches);
2259
2752
  for (let i = 0; i < totalBatches; i++) {
2260
2753
  const start = i * this.batchSize;
2261
2754
  const batchOps = sorted.slice(start, start + this.batchSize);
2755
+ const batchCursor = createDeltaCursorFromBatch(batchOps, i);
2262
2756
  const opsToSerialize = this.encryptor ? await this.encryptor.encryptBatch(batchOps) : batchOps;
2263
2757
  const serializedOps = opsToSerialize.map((op) => this.serializer.encodeOperation(op));
2758
+ const messageId = generateMessageId();
2759
+ if (this.config.strictHandshake) {
2760
+ this.pendingDeltaBatchAcks.add(messageId);
2761
+ }
2264
2762
  const batchMsg = {
2265
2763
  type: "operation-batch",
2266
- messageId: generateMessageId(),
2764
+ messageId,
2267
2765
  operations: serializedOps,
2268
2766
  isFinal: i === totalBatches - 1,
2269
- batchIndex: i
2767
+ batchIndex: i,
2768
+ totalBatches,
2769
+ ...batchCursor ? { cursor: encodeDeltaCursor(batchCursor) } : {}
2270
2770
  };
2271
2771
  this.transport.send(batchMsg);
2272
2772
  this.emitter?.emit({
@@ -2275,8 +2775,17 @@ var SyncEngine = class {
2275
2775
  batchSize: batchOps.length
2276
2776
  });
2277
2777
  }
2778
+ if (!this.config.strictHandshake) {
2779
+ this.deltaSendComplete = true;
2780
+ void this.checkDeltaComplete();
2781
+ }
2782
+ }
2783
+ markDeltaSendCompleteIfReady() {
2784
+ if (this.config.strictHandshake && this.pendingDeltaBatchAcks.size > 0) {
2785
+ return;
2786
+ }
2278
2787
  this.deltaSendComplete = true;
2279
- this.checkDeltaComplete();
2788
+ void this.checkDeltaComplete();
2280
2789
  }
2281
2790
  async collectDelta(localVector, remoteVector) {
2282
2791
  const missing = [];
@@ -2292,9 +2801,36 @@ var SyncEngine = class {
2292
2801
  async handleOperationBatch(msg) {
2293
2802
  const deserialized = msg.operations.map((s) => this.serializer.decodeOperation(s));
2294
2803
  const operations = this.encryptor ? await this.encryptor.decryptBatch(deserialized) : deserialized;
2295
- const inScopeOps = operations.filter((op) => operationMatchesScope(op, this.activeScope));
2804
+ const inScopeOps = operations.filter((op) => this.operationAllowedForSync(op));
2805
+ const targetSchemaVersion = this.config.schemaVersion ?? DEFAULT_SCHEMA_VERSION;
2806
+ const transforms = this.config.operationTransforms ?? [];
2296
2807
  for (const op of inScopeOps) {
2297
- await this.store.applyRemoteOperation(op);
2808
+ const transformed = transforms.length > 0 ? applyOperationTransforms(op, targetSchemaVersion, transforms) : op;
2809
+ if (transformed === null) {
2810
+ continue;
2811
+ }
2812
+ try {
2813
+ const result = await this.store.applyRemoteOperation(transformed);
2814
+ if (result === "skipped" || result === "rejected" || result === "deferred") {
2815
+ this.emitApplyFailure(transformed, result);
2816
+ }
2817
+ } catch (error) {
2818
+ if (error instanceof ClockDriftError) {
2819
+ this.emitApplyFailure(transformed, "rejected", {
2820
+ code: APPLY_FAILURE_CODES.CLOCK_DRIFT,
2821
+ message: error.message,
2822
+ retriable: false
2823
+ });
2824
+ continue;
2825
+ }
2826
+ const message = error instanceof Error ? error.message : "Apply failed";
2827
+ const code = error instanceof SyncError4 ? error.code : error instanceof KoraError2 ? error.code : error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : APPLY_FAILURE_CODES.APPLY_FAILED;
2828
+ this.emitApplyFailure(transformed, "rejected", {
2829
+ code,
2830
+ message,
2831
+ retriable: code !== APPLY_FAILURE_CODES.REFERENTIAL_INTEGRITY
2832
+ });
2833
+ }
2298
2834
  }
2299
2835
  if (inScopeOps.length > 0) {
2300
2836
  this.lastSuccessfulPull = Date.now();
@@ -2318,13 +2854,33 @@ var SyncEngine = class {
2318
2854
  });
2319
2855
  if (this.state === "syncing") {
2320
2856
  this.deltaBatchesReceived++;
2857
+ const totalBatches = msg.totalBatches ?? this.initialSyncTotalBatches;
2858
+ if (msg.totalBatches !== void 0) {
2859
+ this.initialSyncTotalBatches = msg.totalBatches;
2860
+ }
2861
+ this.metricsCollector.updateInitialSyncProgress(this.deltaBatchesReceived, totalBatches);
2862
+ const cursorFromBatch = msg.cursor !== void 0 ? decodeDeltaCursor(msg.cursor) : createDeltaCursorFromBatch(
2863
+ inScopeOps.length > 0 ? inScopeOps : operations,
2864
+ msg.batchIndex
2865
+ );
2866
+ if (cursorFromBatch) {
2867
+ this.resumeDeltaCursor = cursorFromBatch;
2868
+ await this.persistDeltaCursor(cursorFromBatch);
2869
+ }
2321
2870
  if (msg.isFinal) {
2322
2871
  this.deltaReceiveComplete = true;
2323
- this.checkDeltaComplete();
2872
+ this.resumeDeltaCursor = null;
2873
+ await this.persistDeltaCursor(null);
2874
+ this.metricsCollector.recordSyncCompleted();
2875
+ void this.checkDeltaComplete();
2324
2876
  }
2325
2877
  }
2326
2878
  }
2327
2879
  handleAcknowledgment(msg) {
2880
+ if (this.state === "syncing" && this.config.strictHandshake) {
2881
+ this.pendingDeltaBatchAcks.delete(msg.acknowledgedMessageId);
2882
+ this.markDeltaSendCompleteIfReady();
2883
+ }
2328
2884
  if (this.currentBatch) {
2329
2885
  this.outboundQueue.acknowledge(this.currentBatch.batchId);
2330
2886
  this.currentBatch = null;
@@ -2332,6 +2888,9 @@ var SyncEngine = class {
2332
2888
  this.lastSyncedAt = now;
2333
2889
  this.lastSuccessfulPush = now;
2334
2890
  }
2891
+ void this.advanceLastAckedForLocalNode(msg.lastSequenceNumber).then(
2892
+ () => this.refreshPendingCount()
2893
+ );
2335
2894
  if (this.state === "streaming" && this.outboundQueue.hasOperations) {
2336
2895
  this.flushQueue();
2337
2896
  }
@@ -2344,18 +2903,107 @@ var SyncEngine = class {
2344
2903
  this.emitter?.emit({ type: "sync:disconnected", reason: msg.message });
2345
2904
  this.transitionTo("disconnected");
2346
2905
  }
2347
- checkDeltaComplete() {
2348
- if (this.deltaSendComplete && this.deltaReceiveComplete) {
2349
- this.lastSyncedAt = Date.now();
2350
- this.transitionTo("streaming");
2351
- this.awarenessManager.startCleanupTimer();
2352
- const localState = this.awarenessManager.getLocalState();
2353
- if (localState) {
2354
- this.awarenessManager.setLocalState(localState);
2906
+ async checkDeltaComplete() {
2907
+ if (!this.deltaSendComplete || !this.deltaReceiveComplete) {
2908
+ return;
2909
+ }
2910
+ if (this.state !== "syncing") {
2911
+ return;
2912
+ }
2913
+ this.lastSyncedAt = Date.now();
2914
+ this.transitionTo("streaming");
2915
+ this.awarenessManager.startCleanupTimer();
2916
+ const localState = this.awarenessManager.getLocalState();
2917
+ if (localState) {
2918
+ this.awarenessManager.setLocalState(localState);
2919
+ }
2920
+ if (this.deltaSentOpIds.length > 0) {
2921
+ await this.outboundQueue.removeByIds(this.deltaSentOpIds);
2922
+ this.deltaSentOpIds = [];
2923
+ }
2924
+ if (this.syncState) {
2925
+ const localNodeId = this.store.getNodeId();
2926
+ const localSeq = this.store.getVersionVector().get(localNodeId) ?? 0;
2927
+ if (localSeq > 0) {
2928
+ await this.advanceLastAckedForLocalNode(localSeq);
2355
2929
  }
2356
- if (this.outboundQueue.hasOperations) {
2357
- this.flushQueue();
2930
+ }
2931
+ if (this.outboundQueue.hasOperations) {
2932
+ this.flushQueue();
2933
+ }
2934
+ await this.refreshPendingCount();
2935
+ }
2936
+ /**
2937
+ * Effective server vector: persisted last-ack merged with live handshake remote vector.
2938
+ */
2939
+ getEffectiveServerVector() {
2940
+ if (!this.syncState) {
2941
+ return this.remoteVector;
2942
+ }
2943
+ return this.syncState.mergeServerVectors(this.lastAckedServerVector, this.remoteVector);
2944
+ }
2945
+ async persistLastAckedServerVector(vector) {
2946
+ if (!this.syncState) {
2947
+ return;
2948
+ }
2949
+ this.lastAckedServerVector = this.syncState.mergeServerVectors(
2950
+ this.lastAckedServerVector,
2951
+ vector
2952
+ );
2953
+ await this.syncState.saveLastAckedServerVector(this.lastAckedServerVector);
2954
+ }
2955
+ async advanceLastAckedForLocalNode(lastSequenceNumber) {
2956
+ if (!this.syncState || lastSequenceNumber <= 0) {
2957
+ return;
2958
+ }
2959
+ const nodeId = this.store.getNodeId();
2960
+ const merged = new Map(this.lastAckedServerVector);
2961
+ merged.set(nodeId, Math.max(merged.get(nodeId) ?? 0, lastSequenceNumber));
2962
+ this.lastAckedServerVector = merged;
2963
+ await this.syncState.saveLastAckedServerVector(merged);
2964
+ }
2965
+ /**
2966
+ * Refresh cached unsynced count from op log vs effective server vector.
2967
+ */
2968
+ async refreshPendingCount() {
2969
+ if (!this.syncState) {
2970
+ this.cachedUnsyncedCount = this.outboundQueue.totalPending;
2971
+ return;
2972
+ }
2973
+ this.cachedUnsyncedCount = await this.syncState.countUnsyncedOperations(
2974
+ this.getEffectiveServerVector()
2975
+ );
2976
+ }
2977
+ /**
2978
+ * Returns operations the server has not yet acknowledged (op-log source of truth).
2979
+ */
2980
+ async getPendingSyncOperations() {
2981
+ if (!this.syncState) {
2982
+ return this.outboundQueue.peek(Number.MAX_SAFE_INTEGER);
2983
+ }
2984
+ return this.syncState.getUnsyncedOperations(this.getEffectiveServerVector());
2985
+ }
2986
+ /**
2987
+ * Hydrate the outbound queue from unsynced ops in the op log (deduped by operation id).
2988
+ */
2989
+ async reconcileOutboundFromOpLog() {
2990
+ if (this.syncState) {
2991
+ const unsynced = await this.syncState.getUnsyncedOperations(this.getEffectiveServerVector());
2992
+ for (const op of unsynced) {
2993
+ await this.outboundQueue.enqueue(op);
2358
2994
  }
2995
+ return;
2996
+ }
2997
+ const localNodeId = this.store.getNodeId();
2998
+ const localVector = this.store.getVersionVector();
2999
+ const localSeq = localVector.get(localNodeId) ?? 0;
3000
+ if (localSeq === 0) {
3001
+ return;
3002
+ }
3003
+ const ops = await this.store.getOperationRange(localNodeId, 1, localSeq);
3004
+ const inScope = ops.filter((op) => this.operationAllowedForSync(op));
3005
+ for (const op of inScope) {
3006
+ await this.outboundQueue.enqueue(op);
2359
3007
  }
2360
3008
  }
2361
3009
  flushQueue() {
@@ -2421,6 +3069,9 @@ var SyncEngine = class {
2421
3069
  this.outboundQueue.returnBatch(this.currentBatch.batchId);
2422
3070
  this.currentBatch = null;
2423
3071
  }
3072
+ if (this.schemaBlocked) {
3073
+ return;
3074
+ }
2424
3075
  if (this.state !== "disconnected") {
2425
3076
  this.emitter?.emit({ type: "sync:disconnected", reason });
2426
3077
  this.transitionTo("disconnected");
@@ -2448,6 +3099,33 @@ var SyncEngine = class {
2448
3099
  this.serializer.setWireFormat(format);
2449
3100
  }
2450
3101
  }
3102
+ operationAllowedForSync(op) {
3103
+ if (!operationMatchesScope(op, this.activeScope)) {
3104
+ return false;
3105
+ }
3106
+ return operationMatchesQuerySubsets(op, this.getActiveQuerySubsets());
3107
+ }
3108
+ async persistDeltaCursor(cursor) {
3109
+ if (!this.syncState?.saveDeltaCursor) {
3110
+ return;
3111
+ }
3112
+ await this.syncState.saveDeltaCursor(cursor);
3113
+ }
3114
+ scheduleQuerySubsetReconnect() {
3115
+ if (this.querySubsetReconnectTimer) {
3116
+ clearTimeout(this.querySubsetReconnectTimer);
3117
+ }
3118
+ this.querySubsetReconnectTimer = setTimeout(() => {
3119
+ this.querySubsetReconnectTimer = null;
3120
+ if (this.state === "streaming" || this.state === "syncing" || this.state === "handshaking") {
3121
+ void this.reconnectForQuerySubsets();
3122
+ }
3123
+ }, 500);
3124
+ }
3125
+ async reconnectForQuerySubsets() {
3126
+ await this.stop();
3127
+ await this.start();
3128
+ }
2451
3129
  };
2452
3130
  function awarenessStatesToWire(states) {
2453
3131
  const wire = {};
@@ -2621,6 +3299,20 @@ var ReconnectionManager = class {
2621
3299
  this.running = false;
2622
3300
  }
2623
3301
  }
3302
+ /**
3303
+ * Cancel the current backoff wait and proceed to the next reconnect attempt immediately.
3304
+ * No-op if not waiting.
3305
+ */
3306
+ wake() {
3307
+ if (this.timer !== null) {
3308
+ clearTimeout(this.timer);
3309
+ this.timer = null;
3310
+ }
3311
+ if (this.waitResolve) {
3312
+ this.waitResolve();
3313
+ this.waitResolve = null;
3314
+ }
3315
+ }
2624
3316
  /**
2625
3317
  * Stop any pending reconnection attempt.
2626
3318
  */
@@ -3003,8 +3695,8 @@ var SyncEncryptor = class _SyncEncryptor {
3003
3695
  );
3004
3696
  const payload = {
3005
3697
  v: this.currentVersion,
3006
- iv: toBase64(iv),
3007
- ct: toBase64(new Uint8Array(ciphertextBuffer)),
3698
+ iv: toBase642(iv),
3699
+ ct: toBase642(new Uint8Array(ciphertextBuffer)),
3008
3700
  alg: "aes-256-gcm"
3009
3701
  };
3010
3702
  return {
@@ -3048,8 +3740,8 @@ var SyncEncryptor = class _SyncEncryptor {
3048
3740
  );
3049
3741
  }
3050
3742
  try {
3051
- const iv = fromBase64(payload.iv);
3052
- const ciphertext = fromBase64(payload.ct);
3743
+ const iv = fromBase642(payload.iv);
3744
+ const ciphertext = fromBase642(payload.ct);
3053
3745
  const plaintextBuffer = await globalThis.crypto.subtle.decrypt(
3054
3746
  { name: "AES-GCM", iv },
3055
3747
  key.key,
@@ -3086,14 +3778,14 @@ function isEncryptedPayload(field) {
3086
3778
  }
3087
3779
  return field[ENCRYPTED_MARKER] === true && typeof field.v === "number" && typeof field.iv === "string" && typeof field.ct === "string" && typeof field.alg === "string";
3088
3780
  }
3089
- function toBase64(bytes) {
3781
+ function toBase642(bytes) {
3090
3782
  let binary = "";
3091
3783
  for (let i = 0; i < bytes.length; i++) {
3092
3784
  binary += String.fromCharCode(bytes[i]);
3093
3785
  }
3094
3786
  return btoa(binary);
3095
3787
  }
3096
- function fromBase64(str) {
3788
+ function fromBase642(str) {
3097
3789
  const binary = atob(str);
3098
3790
  const bytes = new Uint8Array(binary.length);
3099
3791
  for (let i = 0; i < binary.length; i++) {
@@ -3101,10 +3793,163 @@ function fromBase64(str) {
3101
3793
  }
3102
3794
  return bytes;
3103
3795
  }
3796
+
3797
+ // src/reactivity/sync-status-controller.ts
3798
+ var OFFLINE_SYNC_STATUS = Object.freeze({
3799
+ status: "offline",
3800
+ pendingOperations: 0,
3801
+ lastSyncedAt: null,
3802
+ lastSuccessfulPush: null,
3803
+ lastSuccessfulPull: null,
3804
+ conflicts: 0
3805
+ });
3806
+ var SYNC_STATUS_EVENT_TYPES = [
3807
+ "sync:connected",
3808
+ "sync:disconnected",
3809
+ "sync:schema-mismatch",
3810
+ "sync:auth-failed",
3811
+ "sync:sent",
3812
+ "sync:received",
3813
+ "sync:acknowledged",
3814
+ "sync:apply-failed",
3815
+ "sync:diagnostics",
3816
+ "sync:initial-sync-progress"
3817
+ ];
3818
+ function createSyncStatusController(options) {
3819
+ const useLiveSnapshot = options.subscribeSyncStatus === null && options.events === null;
3820
+ let snapshot = OFFLINE_SYNC_STATUS;
3821
+ let serialized = JSON.stringify(OFFLINE_SYNC_STATUS);
3822
+ const listeners = /* @__PURE__ */ new Set();
3823
+ let cleanup = null;
3824
+ const resolveEngine = () => {
3825
+ return options.getSyncEngine?.() ?? options.syncEngine ?? null;
3826
+ };
3827
+ const notify = () => {
3828
+ for (const listener of listeners) {
3829
+ listener();
3830
+ }
3831
+ };
3832
+ const setSnapshot = (next) => {
3833
+ const nextSerialized = JSON.stringify(next);
3834
+ if (nextSerialized === serialized) {
3835
+ return;
3836
+ }
3837
+ serialized = nextSerialized;
3838
+ snapshot = next;
3839
+ notify();
3840
+ };
3841
+ const refresh = () => {
3842
+ const engine = resolveEngine();
3843
+ const next = engine ? engine.getStatus() : OFFLINE_SYNC_STATUS;
3844
+ setSnapshot(next);
3845
+ };
3846
+ const attach = () => {
3847
+ cleanup?.();
3848
+ if (options.subscribeSyncStatus) {
3849
+ cleanup = options.subscribeSyncStatus(setSnapshot);
3850
+ return;
3851
+ }
3852
+ if (!resolveEngine()) {
3853
+ setSnapshot(OFFLINE_SYNC_STATUS);
3854
+ cleanup = () => {
3855
+ };
3856
+ return;
3857
+ }
3858
+ if (options.events) {
3859
+ const unsubs = SYNC_STATUS_EVENT_TYPES.map((type) => options.events?.on(type, refresh));
3860
+ refresh();
3861
+ cleanup = () => {
3862
+ for (const unsub of unsubs) {
3863
+ unsub?.();
3864
+ }
3865
+ };
3866
+ return;
3867
+ }
3868
+ refresh();
3869
+ cleanup = () => {
3870
+ };
3871
+ };
3872
+ attach();
3873
+ return {
3874
+ getSnapshot() {
3875
+ if (useLiveSnapshot) {
3876
+ const engine = resolveEngine();
3877
+ return engine ? engine.getStatus() : OFFLINE_SYNC_STATUS;
3878
+ }
3879
+ return snapshot;
3880
+ },
3881
+ subscribe(listener) {
3882
+ listeners.add(listener);
3883
+ listener();
3884
+ return () => {
3885
+ listeners.delete(listener);
3886
+ };
3887
+ },
3888
+ refresh,
3889
+ destroy() {
3890
+ cleanup?.();
3891
+ cleanup = null;
3892
+ listeners.clear();
3893
+ }
3894
+ };
3895
+ }
3896
+
3897
+ // src/reactivity/collaborators-snapshot.ts
3898
+ var EMPTY_COLLABORATORS = [];
3899
+ function awarenessStatesEqual(left, right) {
3900
+ if (left.length !== right.length) {
3901
+ return false;
3902
+ }
3903
+ for (let index = 0; index < left.length; index++) {
3904
+ const a = left[index];
3905
+ const b = right[index];
3906
+ if (!a || !b) {
3907
+ return false;
3908
+ }
3909
+ if (a.user.name !== b.user.name || a.user.color !== b.user.color) {
3910
+ return false;
3911
+ }
3912
+ const aCursor = a.cursor;
3913
+ const bCursor = b.cursor;
3914
+ if (aCursor === void 0 && bCursor === void 0) {
3915
+ continue;
3916
+ }
3917
+ if (!aCursor || !bCursor) {
3918
+ return false;
3919
+ }
3920
+ if (aCursor.collection !== bCursor.collection || aCursor.recordId !== bCursor.recordId || aCursor.field !== bCursor.field || aCursor.anchor !== bCursor.anchor || aCursor.head !== bCursor.head) {
3921
+ return false;
3922
+ }
3923
+ }
3924
+ return true;
3925
+ }
3926
+ function getRemoteAwarenessStates(awareness) {
3927
+ const localClientId = awareness.clientId;
3928
+ const remoteStates = [];
3929
+ for (const [clientId, state] of awareness.getStates()) {
3930
+ if (clientId === localClientId) continue;
3931
+ remoteStates.push(state);
3932
+ }
3933
+ return remoteStates;
3934
+ }
3935
+ function subscribeRemoteAwarenessStates(awareness, listener) {
3936
+ let snapshot = EMPTY_COLLABORATORS;
3937
+ const emitIfChanged = () => {
3938
+ const next = getRemoteAwarenessStates(awareness);
3939
+ if (awarenessStatesEqual(snapshot, next)) {
3940
+ return;
3941
+ }
3942
+ snapshot = next;
3943
+ listener(snapshot);
3944
+ };
3945
+ emitIfChanged();
3946
+ return awareness.on("change", emitIfChanged);
3947
+ }
3104
3948
  export {
3105
3949
  AwarenessManager,
3106
3950
  ChaosTransport,
3107
3951
  ConnectionMonitor,
3952
+ DEFAULT_RICHTEXT_DOC_CHANNEL_THRESHOLD,
3108
3953
  DecryptionError,
3109
3954
  EncryptionError,
3110
3955
  HttpLongPollingTransport,
@@ -3112,28 +3957,46 @@ export {
3112
3957
  JsonMessageSerializer,
3113
3958
  KeyDerivationError,
3114
3959
  NegotiatedMessageSerializer,
3960
+ OFFLINE_SYNC_STATUS,
3115
3961
  OutboundQueue,
3116
3962
  ProtobufMessageSerializer,
3117
3963
  ReconnectionManager,
3964
+ RichtextDocChannel,
3965
+ SCHEMA_MISMATCH_PREFIX,
3118
3966
  SYNC_STATES,
3119
3967
  SYNC_STATUSES,
3120
3968
  ScopeViolationError,
3121
3969
  SyncEncryptor,
3122
3970
  SyncEngine,
3123
3971
  WebSocketTransport,
3972
+ createDeltaCursorFromBatch,
3973
+ createSyncStatusController,
3974
+ decodeDeltaCursor,
3975
+ decodeYjsUpdate,
3976
+ dedupeQuerySubsets,
3124
3977
  deriveKey,
3125
3978
  deriveVersionedKey,
3979
+ encodeDeltaCursor,
3980
+ encodeYjsUpdate,
3126
3981
  filterOperationsByScope,
3127
3982
  generateSalt,
3983
+ getRemoteAwarenessStates,
3128
3984
  isAcknowledgmentMessage,
3129
3985
  isAwarenessUpdateMessage,
3986
+ isClientSchemaVersionSupported,
3130
3987
  isEncryptedPayload,
3131
3988
  isErrorMessage,
3132
3989
  isHandshakeMessage,
3133
3990
  isHandshakeResponseMessage,
3134
3991
  isOperationBatchMessage,
3992
+ isSchemaMismatchReject,
3135
3993
  isSyncMessage,
3994
+ isYjsDocUpdateMessage,
3995
+ operationMatchesQuerySubsets,
3136
3996
  operationMatchesScope,
3997
+ richtextDocKey,
3998
+ sliceOperationsAfterCursor,
3999
+ subscribeRemoteAwarenessStates,
3137
4000
  versionVectorToWire,
3138
4001
  wireToVersionVector
3139
4002
  };