@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 +1118 -243
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +361 -162
- package/dist/index.d.ts +361 -162
- package/dist/index.js +1097 -234
- package/dist/index.js.map +1 -1
- package/dist/internal.d.cts +1 -1
- package/dist/internal.d.ts +1 -1
- package/dist/{transport-B-BIguq9.d.cts → transport-BrdBj7rH.d.cts} +84 -4
- package/dist/{transport-B-BIguq9.d.ts → transport-BrdBj7rH.d.ts} +84 -4
- package/package.json +5 -4
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 = [
|
|
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
|
|
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/
|
|
1679
|
-
var
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
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
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
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
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
1828
|
-
if (
|
|
1829
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
if (
|
|
1842
|
-
|
|
1843
|
-
|
|
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
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
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
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
}
|
|
2185
|
+
current.delete(listener);
|
|
2186
|
+
if (current.size === 0) {
|
|
2187
|
+
this.listeners.delete(key);
|
|
2188
|
+
}
|
|
2189
|
+
};
|
|
1872
2190
|
}
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
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
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
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
|
|
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.
|
|
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 (!
|
|
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
|
-
|
|
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
|
|
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) =>
|
|
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
|
|
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.
|
|
2338
|
-
|
|
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
|
|
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) =>
|
|
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
|
-
|
|
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.
|
|
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
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
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
|
-
|
|
2441
|
-
|
|
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
|
|
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
|
|
3468
|
+
var import_core8 = require("@korajs/core");
|
|
2765
3469
|
|
|
2766
3470
|
// src/encryption/key-derivation.ts
|
|
2767
|
-
var
|
|
2768
|
-
var KeyDerivationError = class extends
|
|
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
|
|
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
|
|
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:
|
|
3091
|
-
ct:
|
|
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 =
|
|
3136
|
-
const ciphertext =
|
|
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
|
|
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
|
|
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
|
});
|