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