@korajs/sync 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1659 @@
1
+ import {
2
+ MemoryQueueStorage
3
+ } from "./chunk-TU345YUC.js";
4
+
5
+ // src/types.ts
6
+ var SYNC_STATES = [
7
+ "disconnected",
8
+ "connecting",
9
+ "handshaking",
10
+ "syncing",
11
+ "streaming",
12
+ "error"
13
+ ];
14
+ var SYNC_STATUSES = ["connected", "syncing", "synced", "offline", "error"];
15
+
16
+ // src/protocol/messages.ts
17
+ function isSyncMessage(value) {
18
+ if (typeof value !== "object" || value === null) return false;
19
+ const msg = value;
20
+ if (typeof msg.type !== "string" || typeof msg.messageId !== "string") return false;
21
+ switch (msg.type) {
22
+ case "handshake":
23
+ return isHandshakeMessage(value);
24
+ case "handshake-response":
25
+ return isHandshakeResponseMessage(value);
26
+ case "operation-batch":
27
+ return isOperationBatchMessage(value);
28
+ case "acknowledgment":
29
+ return isAcknowledgmentMessage(value);
30
+ case "error":
31
+ return isErrorMessage(value);
32
+ default:
33
+ return false;
34
+ }
35
+ }
36
+ function isHandshakeMessage(value) {
37
+ if (typeof value !== "object" || value === null) return false;
38
+ const msg = value;
39
+ return msg.type === "handshake" && typeof msg.messageId === "string" && typeof msg.nodeId === "string" && typeof msg.versionVector === "object" && msg.versionVector !== null && !Array.isArray(msg.versionVector) && typeof msg.schemaVersion === "number";
40
+ }
41
+ function isHandshakeResponseMessage(value) {
42
+ if (typeof value !== "object" || value === null) return false;
43
+ const msg = value;
44
+ return msg.type === "handshake-response" && typeof msg.messageId === "string" && typeof msg.nodeId === "string" && typeof msg.versionVector === "object" && msg.versionVector !== null && !Array.isArray(msg.versionVector) && typeof msg.schemaVersion === "number" && typeof msg.accepted === "boolean";
45
+ }
46
+ function isOperationBatchMessage(value) {
47
+ if (typeof value !== "object" || value === null) return false;
48
+ const msg = value;
49
+ return msg.type === "operation-batch" && typeof msg.messageId === "string" && Array.isArray(msg.operations) && typeof msg.isFinal === "boolean" && typeof msg.batchIndex === "number";
50
+ }
51
+ function isAcknowledgmentMessage(value) {
52
+ if (typeof value !== "object" || value === null) return false;
53
+ const msg = value;
54
+ return msg.type === "acknowledgment" && typeof msg.messageId === "string" && typeof msg.acknowledgedMessageId === "string" && typeof msg.lastSequenceNumber === "number";
55
+ }
56
+ function isErrorMessage(value) {
57
+ if (typeof value !== "object" || value === null) return false;
58
+ const msg = value;
59
+ return msg.type === "error" && typeof msg.messageId === "string" && typeof msg.code === "string" && typeof msg.message === "string" && typeof msg.retriable === "boolean";
60
+ }
61
+
62
+ // src/protocol/serializer.ts
63
+ import { SyncError } from "@korajs/core";
64
+ import { Reader, Writer } from "protobufjs/minimal";
65
+ function versionVectorToWire(vector) {
66
+ const wire = {};
67
+ for (const [nodeId, seq] of vector) {
68
+ wire[nodeId] = seq;
69
+ }
70
+ return wire;
71
+ }
72
+ function wireToVersionVector(wire) {
73
+ return new Map(Object.entries(wire));
74
+ }
75
+ var JsonMessageSerializer = class {
76
+ encode(message) {
77
+ return JSON.stringify(message);
78
+ }
79
+ decode(data) {
80
+ const text = decodeTextPayload(data);
81
+ let parsed;
82
+ try {
83
+ parsed = JSON.parse(text);
84
+ } catch {
85
+ throw new SyncError("Failed to decode sync message: invalid JSON", {
86
+ dataLength: text.length
87
+ });
88
+ }
89
+ if (!isSyncMessage(parsed)) {
90
+ throw new SyncError("Failed to decode sync message: invalid message structure", {
91
+ receivedType: typeof parsed === "object" && parsed !== null ? parsed.type : typeof parsed
92
+ });
93
+ }
94
+ return parsed;
95
+ }
96
+ encodeOperation(op) {
97
+ return {
98
+ id: op.id,
99
+ nodeId: op.nodeId,
100
+ type: op.type,
101
+ collection: op.collection,
102
+ recordId: op.recordId,
103
+ data: op.data,
104
+ previousData: op.previousData,
105
+ timestamp: {
106
+ wallTime: op.timestamp.wallTime,
107
+ logical: op.timestamp.logical,
108
+ nodeId: op.timestamp.nodeId
109
+ },
110
+ sequenceNumber: op.sequenceNumber,
111
+ causalDeps: [...op.causalDeps],
112
+ schemaVersion: op.schemaVersion
113
+ };
114
+ }
115
+ decodeOperation(serialized) {
116
+ return {
117
+ id: serialized.id,
118
+ nodeId: serialized.nodeId,
119
+ type: serialized.type,
120
+ collection: serialized.collection,
121
+ recordId: serialized.recordId,
122
+ data: serialized.data,
123
+ previousData: serialized.previousData,
124
+ timestamp: {
125
+ wallTime: serialized.timestamp.wallTime,
126
+ logical: serialized.timestamp.logical,
127
+ nodeId: serialized.timestamp.nodeId
128
+ },
129
+ sequenceNumber: serialized.sequenceNumber,
130
+ causalDeps: [...serialized.causalDeps],
131
+ schemaVersion: serialized.schemaVersion
132
+ };
133
+ }
134
+ };
135
+ var ProtobufMessageSerializer = class {
136
+ encode(message) {
137
+ const envelope = toProtoEnvelope(message);
138
+ return encodeEnvelope(envelope);
139
+ }
140
+ decode(data) {
141
+ const bytes = toBytes(data);
142
+ const envelope = decodeEnvelope(bytes);
143
+ return fromProtoEnvelope(envelope);
144
+ }
145
+ encodeOperation(op) {
146
+ return new JsonMessageSerializer().encodeOperation(op);
147
+ }
148
+ decodeOperation(serialized) {
149
+ return new JsonMessageSerializer().decodeOperation(serialized);
150
+ }
151
+ };
152
+ var NegotiatedMessageSerializer = class {
153
+ json = new JsonMessageSerializer();
154
+ protobuf = new ProtobufMessageSerializer();
155
+ wireFormat;
156
+ constructor(initialWireFormat = "json") {
157
+ this.wireFormat = initialWireFormat;
158
+ }
159
+ encode(message) {
160
+ if (this.wireFormat === "protobuf") {
161
+ return this.protobuf.encode(message);
162
+ }
163
+ return this.json.encode(message);
164
+ }
165
+ decode(data) {
166
+ if (typeof data === "string") {
167
+ return this.json.decode(data);
168
+ }
169
+ try {
170
+ return this.protobuf.decode(data);
171
+ } catch {
172
+ return this.json.decode(data);
173
+ }
174
+ }
175
+ encodeOperation(op) {
176
+ return this.json.encodeOperation(op);
177
+ }
178
+ decodeOperation(serialized) {
179
+ return this.json.decodeOperation(serialized);
180
+ }
181
+ setWireFormat(format) {
182
+ this.wireFormat = format;
183
+ }
184
+ getWireFormat() {
185
+ return this.wireFormat;
186
+ }
187
+ };
188
+ function toProtoEnvelope(message) {
189
+ switch (message.type) {
190
+ case "handshake":
191
+ return {
192
+ type: message.type,
193
+ messageId: message.messageId,
194
+ nodeId: message.nodeId,
195
+ versionVector: Object.entries(message.versionVector).map(([key, value]) => ({ key, value })),
196
+ schemaVersion: message.schemaVersion,
197
+ authToken: message.authToken,
198
+ supportedWireFormats: message.supportedWireFormats
199
+ };
200
+ case "handshake-response":
201
+ return {
202
+ type: message.type,
203
+ messageId: message.messageId,
204
+ nodeId: message.nodeId,
205
+ versionVector: Object.entries(message.versionVector).map(([key, value]) => ({ key, value })),
206
+ schemaVersion: message.schemaVersion,
207
+ accepted: message.accepted,
208
+ rejectReason: message.rejectReason,
209
+ selectedWireFormat: message.selectedWireFormat
210
+ };
211
+ case "operation-batch":
212
+ return {
213
+ type: message.type,
214
+ messageId: message.messageId,
215
+ operations: message.operations.map(serializeProtoOperation),
216
+ isFinal: message.isFinal,
217
+ batchIndex: message.batchIndex
218
+ };
219
+ case "acknowledgment":
220
+ return {
221
+ type: message.type,
222
+ messageId: message.messageId,
223
+ acknowledgedMessageId: message.acknowledgedMessageId,
224
+ lastSequenceNumber: message.lastSequenceNumber
225
+ };
226
+ case "error":
227
+ return {
228
+ type: message.type,
229
+ messageId: message.messageId,
230
+ errorCode: message.code,
231
+ errorMessage: message.message,
232
+ retriable: message.retriable
233
+ };
234
+ }
235
+ }
236
+ function fromProtoEnvelope(envelope) {
237
+ switch (envelope.type) {
238
+ case "handshake":
239
+ return {
240
+ type: "handshake",
241
+ messageId: envelope.messageId,
242
+ nodeId: envelope.nodeId ?? "",
243
+ versionVector: Object.fromEntries((envelope.versionVector ?? []).map((entry) => [entry.key, entry.value])),
244
+ schemaVersion: envelope.schemaVersion ?? 0,
245
+ authToken: envelope.authToken,
246
+ supportedWireFormats: envelope.supportedWireFormats?.filter(
247
+ (format) => format === "json" || format === "protobuf"
248
+ )
249
+ };
250
+ case "handshake-response":
251
+ return {
252
+ type: "handshake-response",
253
+ messageId: envelope.messageId,
254
+ nodeId: envelope.nodeId ?? "",
255
+ versionVector: Object.fromEntries((envelope.versionVector ?? []).map((entry) => [entry.key, entry.value])),
256
+ schemaVersion: envelope.schemaVersion ?? 0,
257
+ accepted: envelope.accepted ?? false,
258
+ rejectReason: envelope.rejectReason,
259
+ selectedWireFormat: envelope.selectedWireFormat === "json" || envelope.selectedWireFormat === "protobuf" ? envelope.selectedWireFormat : void 0
260
+ };
261
+ case "operation-batch":
262
+ return {
263
+ type: "operation-batch",
264
+ messageId: envelope.messageId,
265
+ operations: (envelope.operations ?? []).map(deserializeProtoOperation),
266
+ isFinal: envelope.isFinal ?? false,
267
+ batchIndex: envelope.batchIndex ?? 0
268
+ };
269
+ case "acknowledgment":
270
+ return {
271
+ type: "acknowledgment",
272
+ messageId: envelope.messageId,
273
+ acknowledgedMessageId: envelope.acknowledgedMessageId ?? "",
274
+ lastSequenceNumber: envelope.lastSequenceNumber ?? 0
275
+ };
276
+ case "error":
277
+ return {
278
+ type: "error",
279
+ messageId: envelope.messageId,
280
+ code: envelope.errorCode ?? "UNKNOWN",
281
+ message: envelope.errorMessage ?? "Unknown error",
282
+ retriable: envelope.retriable ?? false
283
+ };
284
+ default:
285
+ throw new SyncError("Failed to decode sync message: unknown protobuf type", {
286
+ type: envelope.type
287
+ });
288
+ }
289
+ }
290
+ function serializeProtoOperation(operation) {
291
+ return {
292
+ id: operation.id,
293
+ nodeId: operation.nodeId,
294
+ type: operation.type,
295
+ collection: operation.collection,
296
+ recordId: operation.recordId,
297
+ dataJson: operation.data === null ? "" : JSON.stringify(operation.data),
298
+ previousDataJson: operation.previousData === null ? "" : JSON.stringify(operation.previousData),
299
+ timestamp: {
300
+ wallTime: operation.timestamp.wallTime,
301
+ logical: operation.timestamp.logical,
302
+ nodeId: operation.timestamp.nodeId
303
+ },
304
+ sequenceNumber: operation.sequenceNumber,
305
+ causalDeps: [...operation.causalDeps],
306
+ schemaVersion: operation.schemaVersion,
307
+ hasData: operation.data !== null,
308
+ hasPreviousData: operation.previousData !== null
309
+ };
310
+ }
311
+ function deserializeProtoOperation(operation) {
312
+ return {
313
+ id: operation.id,
314
+ nodeId: operation.nodeId,
315
+ type: operation.type,
316
+ collection: operation.collection,
317
+ recordId: operation.recordId,
318
+ data: operation.hasData ? JSON.parse(operation.dataJson) : null,
319
+ previousData: operation.hasPreviousData ? JSON.parse(operation.previousDataJson) : null,
320
+ timestamp: {
321
+ wallTime: operation.timestamp.wallTime,
322
+ logical: operation.timestamp.logical,
323
+ nodeId: operation.timestamp.nodeId
324
+ },
325
+ sequenceNumber: operation.sequenceNumber,
326
+ causalDeps: [...operation.causalDeps],
327
+ schemaVersion: operation.schemaVersion
328
+ };
329
+ }
330
+ function decodeTextPayload(data) {
331
+ if (typeof data === "string") return data;
332
+ return new TextDecoder().decode(toBytes(data));
333
+ }
334
+ function toBytes(data) {
335
+ if (typeof data === "string") {
336
+ return new TextEncoder().encode(data);
337
+ }
338
+ if (data instanceof Uint8Array) {
339
+ return data;
340
+ }
341
+ if (data instanceof ArrayBuffer) {
342
+ return new Uint8Array(data);
343
+ }
344
+ throw new SyncError("Unsupported sync payload type", { receivedType: typeof data });
345
+ }
346
+ function encodeEnvelope(envelope) {
347
+ const writer = Writer.create();
348
+ if (envelope.type.length > 0) writer.uint32(10).string(envelope.type);
349
+ if (envelope.messageId.length > 0) writer.uint32(18).string(envelope.messageId);
350
+ if (envelope.nodeId && envelope.nodeId.length > 0) writer.uint32(26).string(envelope.nodeId);
351
+ for (const entry of envelope.versionVector ?? []) {
352
+ writer.uint32(34).fork();
353
+ writer.uint32(10).string(entry.key);
354
+ writer.uint32(16).int64(entry.value);
355
+ writer.ldelim();
356
+ }
357
+ if (envelope.schemaVersion !== void 0) writer.uint32(40).int32(envelope.schemaVersion);
358
+ if (envelope.authToken && envelope.authToken.length > 0) writer.uint32(50).string(envelope.authToken);
359
+ for (const format of envelope.supportedWireFormats ?? []) {
360
+ writer.uint32(58).string(format);
361
+ }
362
+ if (envelope.accepted !== void 0) writer.uint32(64).bool(envelope.accepted);
363
+ if (envelope.rejectReason && envelope.rejectReason.length > 0) writer.uint32(74).string(envelope.rejectReason);
364
+ if (envelope.selectedWireFormat && envelope.selectedWireFormat.length > 0) {
365
+ writer.uint32(82).string(envelope.selectedWireFormat);
366
+ }
367
+ for (const operation of envelope.operations ?? []) {
368
+ writer.uint32(90).fork();
369
+ encodeProtoOperation(writer, operation);
370
+ writer.ldelim();
371
+ }
372
+ if (envelope.isFinal !== void 0) writer.uint32(96).bool(envelope.isFinal);
373
+ if (envelope.batchIndex !== void 0) writer.uint32(104).uint32(envelope.batchIndex);
374
+ if (envelope.acknowledgedMessageId && envelope.acknowledgedMessageId.length > 0) {
375
+ writer.uint32(114).string(envelope.acknowledgedMessageId);
376
+ }
377
+ if (envelope.lastSequenceNumber !== void 0) writer.uint32(120).int64(envelope.lastSequenceNumber);
378
+ if (envelope.errorCode && envelope.errorCode.length > 0) writer.uint32(130).string(envelope.errorCode);
379
+ if (envelope.errorMessage && envelope.errorMessage.length > 0) {
380
+ writer.uint32(138).string(envelope.errorMessage);
381
+ }
382
+ if (envelope.retriable !== void 0) writer.uint32(144).bool(envelope.retriable);
383
+ return writer.finish();
384
+ }
385
+ function decodeEnvelope(bytes) {
386
+ const reader = Reader.create(bytes);
387
+ const envelope = { type: "error", messageId: "" };
388
+ while (reader.pos < reader.len) {
389
+ const tag = reader.uint32();
390
+ switch (tag >>> 3) {
391
+ case 1:
392
+ envelope.type = reader.string();
393
+ break;
394
+ case 2:
395
+ envelope.messageId = reader.string();
396
+ break;
397
+ case 3:
398
+ envelope.nodeId = reader.string();
399
+ break;
400
+ case 4:
401
+ envelope.versionVector = [...envelope.versionVector ?? [], decodeVectorEntry(reader, reader.uint32())];
402
+ break;
403
+ case 5:
404
+ envelope.schemaVersion = reader.int32();
405
+ break;
406
+ case 6:
407
+ envelope.authToken = reader.string();
408
+ break;
409
+ case 7:
410
+ envelope.supportedWireFormats = [...envelope.supportedWireFormats ?? [], reader.string()];
411
+ break;
412
+ case 8:
413
+ envelope.accepted = reader.bool();
414
+ break;
415
+ case 9:
416
+ envelope.rejectReason = reader.string();
417
+ break;
418
+ case 10:
419
+ envelope.selectedWireFormat = reader.string();
420
+ break;
421
+ case 11:
422
+ envelope.operations = [...envelope.operations ?? [], decodeProtoOperation(reader, reader.uint32())];
423
+ break;
424
+ case 12:
425
+ envelope.isFinal = reader.bool();
426
+ break;
427
+ case 13:
428
+ envelope.batchIndex = reader.uint32();
429
+ break;
430
+ case 14:
431
+ envelope.acknowledgedMessageId = reader.string();
432
+ break;
433
+ case 15:
434
+ envelope.lastSequenceNumber = longToNumber(reader.int64());
435
+ break;
436
+ case 16:
437
+ envelope.errorCode = reader.string();
438
+ break;
439
+ case 17:
440
+ envelope.errorMessage = reader.string();
441
+ break;
442
+ case 18:
443
+ envelope.retriable = reader.bool();
444
+ break;
445
+ default:
446
+ reader.skipType(tag & 7);
447
+ }
448
+ }
449
+ return envelope;
450
+ }
451
+ function encodeProtoOperation(writer, operation) {
452
+ if (operation.id.length > 0) writer.uint32(10).string(operation.id);
453
+ if (operation.nodeId.length > 0) writer.uint32(18).string(operation.nodeId);
454
+ if (operation.type.length > 0) writer.uint32(26).string(operation.type);
455
+ if (operation.collection.length > 0) writer.uint32(34).string(operation.collection);
456
+ if (operation.recordId.length > 0) writer.uint32(42).string(operation.recordId);
457
+ if (operation.dataJson.length > 0) writer.uint32(50).string(operation.dataJson);
458
+ if (operation.previousDataJson.length > 0) writer.uint32(58).string(operation.previousDataJson);
459
+ writer.uint32(66).fork();
460
+ writer.uint32(8).int64(operation.timestamp.wallTime);
461
+ writer.uint32(16).uint32(operation.timestamp.logical);
462
+ writer.uint32(26).string(operation.timestamp.nodeId);
463
+ writer.ldelim();
464
+ writer.uint32(72).int64(operation.sequenceNumber);
465
+ for (const dep of operation.causalDeps) {
466
+ writer.uint32(82).string(dep);
467
+ }
468
+ writer.uint32(88).int32(operation.schemaVersion);
469
+ writer.uint32(96).bool(operation.hasData);
470
+ writer.uint32(104).bool(operation.hasPreviousData);
471
+ }
472
+ function decodeProtoOperation(reader, length) {
473
+ const end = reader.pos + length;
474
+ const operation = {
475
+ id: "",
476
+ nodeId: "",
477
+ type: "insert",
478
+ collection: "",
479
+ recordId: "",
480
+ dataJson: "",
481
+ previousDataJson: "",
482
+ timestamp: { wallTime: 0, logical: 0, nodeId: "" },
483
+ sequenceNumber: 0,
484
+ causalDeps: [],
485
+ schemaVersion: 0,
486
+ hasData: false,
487
+ hasPreviousData: false
488
+ };
489
+ while (reader.pos < end) {
490
+ const tag = reader.uint32();
491
+ switch (tag >>> 3) {
492
+ case 1:
493
+ operation.id = reader.string();
494
+ break;
495
+ case 2:
496
+ operation.nodeId = reader.string();
497
+ break;
498
+ case 3:
499
+ operation.type = reader.string();
500
+ break;
501
+ case 4:
502
+ operation.collection = reader.string();
503
+ break;
504
+ case 5:
505
+ operation.recordId = reader.string();
506
+ break;
507
+ case 6:
508
+ operation.dataJson = reader.string();
509
+ break;
510
+ case 7:
511
+ operation.previousDataJson = reader.string();
512
+ break;
513
+ case 8: {
514
+ const timestampEnd = reader.pos + reader.uint32();
515
+ while (reader.pos < timestampEnd) {
516
+ const timestampTag = reader.uint32();
517
+ switch (timestampTag >>> 3) {
518
+ case 1:
519
+ operation.timestamp.wallTime = longToNumber(reader.int64());
520
+ break;
521
+ case 2:
522
+ operation.timestamp.logical = reader.uint32();
523
+ break;
524
+ case 3:
525
+ operation.timestamp.nodeId = reader.string();
526
+ break;
527
+ default:
528
+ reader.skipType(timestampTag & 7);
529
+ }
530
+ }
531
+ break;
532
+ }
533
+ case 9:
534
+ operation.sequenceNumber = longToNumber(reader.int64());
535
+ break;
536
+ case 10:
537
+ operation.causalDeps.push(reader.string());
538
+ break;
539
+ case 11:
540
+ operation.schemaVersion = reader.int32();
541
+ break;
542
+ case 12:
543
+ operation.hasData = reader.bool();
544
+ break;
545
+ case 13:
546
+ operation.hasPreviousData = reader.bool();
547
+ break;
548
+ default:
549
+ reader.skipType(tag & 7);
550
+ }
551
+ }
552
+ return operation;
553
+ }
554
+ function decodeVectorEntry(reader, length) {
555
+ const end = reader.pos + length;
556
+ const entry = { key: "", value: 0 };
557
+ while (reader.pos < end) {
558
+ const tag = reader.uint32();
559
+ switch (tag >>> 3) {
560
+ case 1:
561
+ entry.key = reader.string();
562
+ break;
563
+ case 2:
564
+ entry.value = longToNumber(reader.int64());
565
+ break;
566
+ default:
567
+ reader.skipType(tag & 7);
568
+ }
569
+ }
570
+ return entry;
571
+ }
572
+ function longToNumber(value) {
573
+ if (typeof value === "number") return value;
574
+ if (typeof value === "string") return Number.parseInt(value, 10);
575
+ if (typeof value === "object" && value !== null && "toNumber" in value && typeof value.toNumber === "function") {
576
+ return value.toNumber();
577
+ }
578
+ throw new SyncError("Failed to decode int64 value", {
579
+ receivedType: typeof value
580
+ });
581
+ }
582
+
583
+ // src/transport/websocket-transport.ts
584
+ import { SyncError as SyncError2 } from "@korajs/core";
585
+ var WS_OPEN = 1;
586
+ var WebSocketTransport = class {
587
+ ws = null;
588
+ messageHandler = null;
589
+ closeHandler = null;
590
+ errorHandler = null;
591
+ serializer;
592
+ WebSocketImpl;
593
+ constructor(options) {
594
+ this.serializer = options?.serializer ?? new JsonMessageSerializer();
595
+ if (options?.WebSocketImpl) {
596
+ this.WebSocketImpl = options.WebSocketImpl;
597
+ } else if (typeof globalThis.WebSocket !== "undefined") {
598
+ this.WebSocketImpl = globalThis.WebSocket;
599
+ } else {
600
+ this.WebSocketImpl = null;
601
+ }
602
+ }
603
+ async connect(url, options) {
604
+ if (!this.WebSocketImpl) {
605
+ throw new SyncError2("WebSocket is not available in this environment", {
606
+ hint: "Provide a WebSocketImpl option or use a polyfill"
607
+ });
608
+ }
609
+ return new Promise((resolve, reject) => {
610
+ try {
611
+ const connectUrl = options?.authToken ? `${url}${url.includes("?") ? "&" : "?"}token=${encodeURIComponent(options.authToken)}` : url;
612
+ const ws = new this.WebSocketImpl(connectUrl);
613
+ this.ws = ws;
614
+ ws.onopen = () => {
615
+ resolve();
616
+ };
617
+ ws.onmessage = (event) => {
618
+ try {
619
+ if (typeof event.data !== "string" && !(event.data instanceof Uint8Array) && !(event.data instanceof ArrayBuffer)) {
620
+ return;
621
+ }
622
+ const message = this.serializer.decode(event.data);
623
+ this.messageHandler?.(message);
624
+ } catch {
625
+ this.errorHandler?.(new SyncError2("Failed to decode incoming message"));
626
+ }
627
+ };
628
+ ws.onclose = (event) => {
629
+ this.ws = null;
630
+ this.closeHandler?.(event.reason || `WebSocket closed with code ${event.code}`);
631
+ };
632
+ ws.onerror = (event) => {
633
+ const err = new SyncError2("WebSocket error", {
634
+ url
635
+ });
636
+ this.errorHandler?.(err);
637
+ if (!this.isConnected()) {
638
+ this.ws = null;
639
+ reject(err);
640
+ }
641
+ };
642
+ } catch (err) {
643
+ reject(
644
+ err instanceof SyncError2 ? err : new SyncError2("Failed to create WebSocket", {
645
+ url,
646
+ error: String(err)
647
+ })
648
+ );
649
+ }
650
+ });
651
+ }
652
+ async disconnect() {
653
+ if (this.ws) {
654
+ this.ws.onclose = null;
655
+ this.ws.close(1e3, "Client disconnecting");
656
+ this.ws = null;
657
+ }
658
+ }
659
+ send(message) {
660
+ if (!this.ws || this.ws.readyState !== WS_OPEN) {
661
+ throw new SyncError2("Cannot send message: WebSocket is not connected", {
662
+ messageType: message.type
663
+ });
664
+ }
665
+ const encoded = this.serializer.encode(message);
666
+ this.ws.send(encoded);
667
+ }
668
+ onMessage(handler) {
669
+ this.messageHandler = handler;
670
+ }
671
+ onClose(handler) {
672
+ this.closeHandler = handler;
673
+ }
674
+ onError(handler) {
675
+ this.errorHandler = handler;
676
+ }
677
+ isConnected() {
678
+ return this.ws !== null && this.ws.readyState === WS_OPEN;
679
+ }
680
+ };
681
+
682
+ // src/transport/http-long-polling-transport.ts
683
+ import { SyncError as SyncError3 } from "@korajs/core";
684
+ var DEFAULT_RETRY_DELAY_MS = 250;
685
+ var HttpLongPollingTransport = class {
686
+ serializer;
687
+ fetchImpl;
688
+ retryDelayMs;
689
+ preferWebSocket;
690
+ webSocketFactory;
691
+ messageHandler = null;
692
+ closeHandler = null;
693
+ errorHandler = null;
694
+ connected = false;
695
+ polling = false;
696
+ url = null;
697
+ authToken;
698
+ pollAbort = null;
699
+ upgradedTransport = null;
700
+ constructor(options) {
701
+ this.serializer = options?.serializer ?? new NegotiatedMessageSerializer("json");
702
+ this.fetchImpl = options?.fetchImpl ?? fetch;
703
+ this.retryDelayMs = options?.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS;
704
+ this.preferWebSocket = options?.preferWebSocket ?? true;
705
+ this.webSocketFactory = options?.webSocketFactory ?? (() => new WebSocketTransport({ serializer: this.serializer }));
706
+ }
707
+ async connect(url, options) {
708
+ if (this.connected) {
709
+ throw new SyncError3("HTTP long-poll transport already connected", { url });
710
+ }
711
+ this.url = normalizeHttpUrl(url);
712
+ this.authToken = options?.authToken;
713
+ if (this.preferWebSocket) {
714
+ const upgraded = await this.tryUpgradeToWebSocket(url, options);
715
+ if (upgraded) {
716
+ this.upgradedTransport = upgraded;
717
+ this.connected = true;
718
+ return;
719
+ }
720
+ }
721
+ this.connected = true;
722
+ this.polling = true;
723
+ this.pollAbort = new AbortController();
724
+ void this.runPollLoop();
725
+ }
726
+ async disconnect() {
727
+ if (this.upgradedTransport) {
728
+ await this.upgradedTransport.disconnect();
729
+ this.upgradedTransport = null;
730
+ }
731
+ this.connected = false;
732
+ this.polling = false;
733
+ this.pollAbort?.abort();
734
+ this.pollAbort = null;
735
+ this.url = null;
736
+ }
737
+ send(message) {
738
+ if (!this.connected) {
739
+ throw new SyncError3("Cannot send message: HTTP long-poll transport is not connected", {
740
+ messageType: message.type
741
+ });
742
+ }
743
+ if (this.upgradedTransport) {
744
+ this.upgradedTransport.send(message);
745
+ return;
746
+ }
747
+ void this.postMessage(message);
748
+ }
749
+ onMessage(handler) {
750
+ this.messageHandler = handler;
751
+ if (this.upgradedTransport) {
752
+ this.upgradedTransport.onMessage(handler);
753
+ }
754
+ }
755
+ onClose(handler) {
756
+ this.closeHandler = handler;
757
+ if (this.upgradedTransport) {
758
+ this.upgradedTransport.onClose(handler);
759
+ }
760
+ }
761
+ onError(handler) {
762
+ this.errorHandler = handler;
763
+ if (this.upgradedTransport) {
764
+ this.upgradedTransport.onError(handler);
765
+ }
766
+ }
767
+ isConnected() {
768
+ if (this.upgradedTransport) {
769
+ return this.upgradedTransport.isConnected();
770
+ }
771
+ return this.connected;
772
+ }
773
+ async tryUpgradeToWebSocket(url, options) {
774
+ const wsTransport = this.webSocketFactory();
775
+ if (this.messageHandler) wsTransport.onMessage(this.messageHandler);
776
+ if (this.closeHandler) wsTransport.onClose(this.closeHandler);
777
+ if (this.errorHandler) wsTransport.onError(this.errorHandler);
778
+ try {
779
+ await wsTransport.connect(normalizeWebSocketUrl(url), options);
780
+ return wsTransport;
781
+ } catch {
782
+ return null;
783
+ }
784
+ }
785
+ async postMessage(message) {
786
+ if (!this.url) return;
787
+ const encoded = this.serializer.encode(message);
788
+ const headers = new Headers();
789
+ headers.set("accept", "application/json, application/x-protobuf");
790
+ if (this.authToken) {
791
+ headers.set("authorization", `Bearer ${this.authToken}`);
792
+ }
793
+ const isBinary = encoded instanceof Uint8Array;
794
+ headers.set("content-type", isBinary ? "application/x-protobuf" : "application/json");
795
+ try {
796
+ const requestBody = isBinary ? toArrayBuffer(encoded) : encoded;
797
+ const response = await this.fetchImpl(this.url, {
798
+ method: "POST",
799
+ headers,
800
+ body: requestBody
801
+ });
802
+ if (!response.ok) {
803
+ throw new SyncError3("HTTP transport send failed", {
804
+ status: response.status,
805
+ messageType: message.type
806
+ });
807
+ }
808
+ } catch (error) {
809
+ this.errorHandler?.(error instanceof Error ? error : new Error(String(error)));
810
+ }
811
+ }
812
+ async runPollLoop() {
813
+ while (this.polling && this.connected && this.url) {
814
+ try {
815
+ const response = await this.fetchImpl(this.url, {
816
+ method: "GET",
817
+ headers: this.makePollHeaders(),
818
+ signal: this.pollAbort?.signal
819
+ });
820
+ if (response.status === 204) {
821
+ await sleep(this.retryDelayMs);
822
+ continue;
823
+ }
824
+ if (!response.ok) {
825
+ throw new SyncError3("HTTP long-poll request failed", {
826
+ status: response.status
827
+ });
828
+ }
829
+ const payload = await readResponsePayload(response);
830
+ if (payload === null) {
831
+ continue;
832
+ }
833
+ const message = this.serializer.decode(payload);
834
+ this.messageHandler?.(message);
835
+ } catch (error) {
836
+ if (!this.connected || !this.polling) {
837
+ break;
838
+ }
839
+ if (isAbortError(error)) {
840
+ break;
841
+ }
842
+ this.errorHandler?.(error instanceof Error ? error : new Error(String(error)));
843
+ await sleep(this.retryDelayMs);
844
+ }
845
+ }
846
+ if (!this.connected) {
847
+ this.closeHandler?.("http long-polling disconnected");
848
+ }
849
+ }
850
+ makePollHeaders() {
851
+ const headers = new Headers();
852
+ headers.set("accept", "application/json, application/x-protobuf");
853
+ if (this.authToken) {
854
+ headers.set("authorization", `Bearer ${this.authToken}`);
855
+ }
856
+ return headers;
857
+ }
858
+ };
859
+ function normalizeHttpUrl(url) {
860
+ if (url.startsWith("http://") || url.startsWith("https://")) {
861
+ return url;
862
+ }
863
+ if (url.startsWith("ws://")) {
864
+ return `http://${url.slice("ws://".length)}`;
865
+ }
866
+ if (url.startsWith("wss://")) {
867
+ return `https://${url.slice("wss://".length)}`;
868
+ }
869
+ return url;
870
+ }
871
+ function normalizeWebSocketUrl(url) {
872
+ if (url.startsWith("ws://") || url.startsWith("wss://")) {
873
+ return url;
874
+ }
875
+ if (url.startsWith("http://")) {
876
+ return `ws://${url.slice("http://".length)}`;
877
+ }
878
+ if (url.startsWith("https://")) {
879
+ return `wss://${url.slice("https://".length)}`;
880
+ }
881
+ return url;
882
+ }
883
+ async function readResponsePayload(response) {
884
+ const contentType = response.headers.get("content-type") ?? "";
885
+ if (contentType.includes("application/x-protobuf")) {
886
+ const buffer = await response.arrayBuffer();
887
+ if (buffer.byteLength === 0) return null;
888
+ return new Uint8Array(buffer);
889
+ }
890
+ const text = await response.text();
891
+ if (text.length === 0) return null;
892
+ return text;
893
+ }
894
+ function sleep(ms) {
895
+ return new Promise((resolve) => setTimeout(resolve, ms));
896
+ }
897
+ function isAbortError(error) {
898
+ return typeof error === "object" && error !== null && "name" in error && error.name === "AbortError";
899
+ }
900
+ function toArrayBuffer(data) {
901
+ const copied = new Uint8Array(data.byteLength);
902
+ copied.set(data);
903
+ return copied.buffer;
904
+ }
905
+
906
+ // src/transport/chaos-transport.ts
907
+ var ChaosTransport = class {
908
+ inner;
909
+ dropRate;
910
+ duplicateRate;
911
+ reorderRate;
912
+ maxLatency;
913
+ random;
914
+ messageHandler = null;
915
+ reorderBuffer = [];
916
+ timers = [];
917
+ constructor(inner, config) {
918
+ this.inner = inner;
919
+ this.dropRate = config?.dropRate ?? 0;
920
+ this.duplicateRate = config?.duplicateRate ?? 0;
921
+ this.reorderRate = config?.reorderRate ?? 0;
922
+ this.maxLatency = config?.maxLatency ?? 0;
923
+ this.random = config?.randomSource ?? Math.random;
924
+ }
925
+ async connect(url, options) {
926
+ this.inner.onMessage((msg) => this.handleIncoming(msg));
927
+ return this.inner.connect(url, options);
928
+ }
929
+ async disconnect() {
930
+ this.flushReorderBuffer();
931
+ for (const timer of this.timers) {
932
+ clearTimeout(timer);
933
+ }
934
+ this.timers = [];
935
+ return this.inner.disconnect();
936
+ }
937
+ send(message) {
938
+ if (this.random() < this.dropRate) {
939
+ return;
940
+ }
941
+ if (this.random() < this.reorderRate) {
942
+ this.reorderBuffer.push(message);
943
+ return;
944
+ }
945
+ this.flushReorderBuffer();
946
+ this.inner.send(message);
947
+ if (this.random() < this.duplicateRate) {
948
+ this.inner.send(message);
949
+ }
950
+ }
951
+ onMessage(handler) {
952
+ this.messageHandler = handler;
953
+ }
954
+ onClose(handler) {
955
+ this.inner.onClose(handler);
956
+ }
957
+ onError(handler) {
958
+ this.inner.onError(handler);
959
+ }
960
+ isConnected() {
961
+ return this.inner.isConnected();
962
+ }
963
+ handleIncoming(message) {
964
+ if (!this.messageHandler) return;
965
+ if (this.random() < this.dropRate) {
966
+ return;
967
+ }
968
+ if (this.maxLatency > 0) {
969
+ const delay = Math.floor(this.random() * this.maxLatency);
970
+ const timer = setTimeout(() => {
971
+ this.deliverIncoming(message);
972
+ }, delay);
973
+ this.timers.push(timer);
974
+ return;
975
+ }
976
+ this.deliverIncoming(message);
977
+ }
978
+ deliverIncoming(message) {
979
+ if (!this.messageHandler) return;
980
+ this.messageHandler(message);
981
+ if (this.random() < this.duplicateRate) {
982
+ this.messageHandler(message);
983
+ }
984
+ }
985
+ flushReorderBuffer() {
986
+ const buffer = [...this.reorderBuffer];
987
+ this.reorderBuffer = [];
988
+ for (let i = buffer.length - 1; i > 0; i--) {
989
+ const j = Math.floor(this.random() * (i + 1));
990
+ const temp = buffer[i];
991
+ buffer[i] = buffer[j];
992
+ buffer[j] = temp;
993
+ }
994
+ for (const msg of buffer) {
995
+ this.inner.send(msg);
996
+ }
997
+ }
998
+ };
999
+
1000
+ // src/engine/sync-engine.ts
1001
+ import { SyncError as SyncError4 } from "@korajs/core";
1002
+ import { topologicalSort as topologicalSort2 } from "@korajs/core/internal";
1003
+
1004
+ // src/engine/outbound-queue.ts
1005
+ import { topologicalSort } from "@korajs/core/internal";
1006
+ var OutboundQueue = class {
1007
+ constructor(storage) {
1008
+ this.storage = storage;
1009
+ }
1010
+ storage;
1011
+ queue = [];
1012
+ seen = /* @__PURE__ */ new Set();
1013
+ inFlight = /* @__PURE__ */ new Map();
1014
+ nextBatchId = 0;
1015
+ initialized = false;
1016
+ /**
1017
+ * Load persisted operations from storage.
1018
+ * Must be called before using the queue.
1019
+ */
1020
+ async initialize() {
1021
+ const stored = await this.storage.load();
1022
+ for (const op of stored) {
1023
+ if (!this.seen.has(op.id)) {
1024
+ this.seen.add(op.id);
1025
+ this.queue.push(op);
1026
+ }
1027
+ }
1028
+ if (this.queue.length > 1) {
1029
+ this.queue = topologicalSort(this.queue);
1030
+ }
1031
+ this.initialized = true;
1032
+ }
1033
+ /**
1034
+ * Add an operation to the outbound queue.
1035
+ * Deduplicates by operation ID. Persists to storage.
1036
+ */
1037
+ async enqueue(op) {
1038
+ if (this.seen.has(op.id)) return;
1039
+ this.seen.add(op.id);
1040
+ this.queue.push(op);
1041
+ await this.storage.enqueue(op);
1042
+ if (this.queue.length > 1) {
1043
+ this.queue = topologicalSort(this.queue);
1044
+ }
1045
+ }
1046
+ /**
1047
+ * Take a batch of operations from the front of the queue.
1048
+ * Moves them to in-flight status. Returns null if queue is empty.
1049
+ *
1050
+ * @param batchSize - Maximum number of operations in the batch
1051
+ */
1052
+ takeBatch(batchSize) {
1053
+ if (this.queue.length === 0) return null;
1054
+ const ops = this.queue.splice(0, batchSize);
1055
+ const batchId = `batch-${this.nextBatchId++}`;
1056
+ this.inFlight.set(batchId, ops);
1057
+ return { batchId, operations: ops };
1058
+ }
1059
+ /**
1060
+ * Acknowledge a batch, removing its operations permanently.
1061
+ */
1062
+ async acknowledge(batchId) {
1063
+ const ops = this.inFlight.get(batchId);
1064
+ if (!ops) return;
1065
+ this.inFlight.delete(batchId);
1066
+ const ids = ops.map((op) => op.id);
1067
+ await this.storage.dequeue(ids);
1068
+ }
1069
+ /**
1070
+ * Return a failed batch to the front of the queue for retry.
1071
+ * Prepends the operations to maintain priority.
1072
+ */
1073
+ returnBatch(batchId) {
1074
+ const ops = this.inFlight.get(batchId);
1075
+ if (!ops) return;
1076
+ this.inFlight.delete(batchId);
1077
+ this.queue.unshift(...ops);
1078
+ if (this.queue.length > 1) {
1079
+ this.queue = topologicalSort(this.queue);
1080
+ }
1081
+ }
1082
+ /**
1083
+ * Number of operations waiting in the queue (not counting in-flight).
1084
+ */
1085
+ get size() {
1086
+ return this.queue.length;
1087
+ }
1088
+ /**
1089
+ * Total operations including in-flight.
1090
+ */
1091
+ get totalPending() {
1092
+ let inFlightCount = 0;
1093
+ for (const ops of this.inFlight.values()) {
1094
+ inFlightCount += ops.length;
1095
+ }
1096
+ return this.queue.length + inFlightCount;
1097
+ }
1098
+ /**
1099
+ * Whether the queue has any operations to send.
1100
+ */
1101
+ get hasOperations() {
1102
+ return this.queue.length > 0;
1103
+ }
1104
+ /**
1105
+ * Peek at the first `count` operations without removing them.
1106
+ */
1107
+ peek(count) {
1108
+ return this.queue.slice(0, count);
1109
+ }
1110
+ /**
1111
+ * Whether initialize() has been called.
1112
+ */
1113
+ get isInitialized() {
1114
+ return this.initialized;
1115
+ }
1116
+ };
1117
+
1118
+ // src/engine/sync-engine.ts
1119
+ var DEFAULT_BATCH_SIZE = 100;
1120
+ var DEFAULT_SCHEMA_VERSION = 1;
1121
+ var VALID_TRANSITIONS = {
1122
+ disconnected: ["connecting"],
1123
+ connecting: ["handshaking", "error", "disconnected"],
1124
+ handshaking: ["syncing", "error", "disconnected"],
1125
+ syncing: ["streaming", "error", "disconnected"],
1126
+ streaming: ["disconnected", "error"],
1127
+ error: ["disconnected"]
1128
+ };
1129
+ var nextMessageId = 0;
1130
+ function generateMessageId() {
1131
+ return `msg-${Date.now()}-${nextMessageId++}`;
1132
+ }
1133
+ var SyncEngine = class {
1134
+ state = "disconnected";
1135
+ transport;
1136
+ store;
1137
+ config;
1138
+ serializer;
1139
+ emitter;
1140
+ outboundQueue;
1141
+ batchSize;
1142
+ remoteVector = /* @__PURE__ */ new Map();
1143
+ lastSyncedAt = null;
1144
+ currentBatch = null;
1145
+ // Track delta exchange state
1146
+ deltaBatchesReceived = 0;
1147
+ deltaReceiveComplete = false;
1148
+ deltaSendComplete = false;
1149
+ constructor(options) {
1150
+ this.transport = options.transport;
1151
+ this.store = options.store;
1152
+ this.config = options.config;
1153
+ this.serializer = options.serializer ?? new NegotiatedMessageSerializer("json");
1154
+ this.emitter = options.emitter ?? null;
1155
+ this.batchSize = options.config.batchSize ?? DEFAULT_BATCH_SIZE;
1156
+ const queueStorage = options.queueStorage ?? new MemoryQueueStorage();
1157
+ this.outboundQueue = new OutboundQueue(queueStorage);
1158
+ }
1159
+ /**
1160
+ * Start the sync engine: connect → handshake → delta exchange → streaming.
1161
+ */
1162
+ async start() {
1163
+ if (this.state !== "disconnected") {
1164
+ throw new SyncError4("Cannot start sync engine: not in disconnected state", {
1165
+ currentState: this.state
1166
+ });
1167
+ }
1168
+ await this.outboundQueue.initialize();
1169
+ this.transport.onMessage((msg) => this.handleMessage(msg));
1170
+ this.transport.onClose((reason) => this.handleTransportClose(reason));
1171
+ this.transport.onError((err) => this.handleTransportError(err));
1172
+ this.transitionTo("connecting");
1173
+ try {
1174
+ const authToken = this.config.auth ? (await this.config.auth()).token : void 0;
1175
+ await this.transport.connect(this.config.url, { authToken });
1176
+ this.transitionTo("handshaking");
1177
+ const localVector = this.store.getVersionVector();
1178
+ const handshake = {
1179
+ type: "handshake",
1180
+ messageId: generateMessageId(),
1181
+ nodeId: this.store.getNodeId(),
1182
+ versionVector: versionVectorToWire(localVector),
1183
+ schemaVersion: this.config.schemaVersion ?? DEFAULT_SCHEMA_VERSION,
1184
+ authToken,
1185
+ supportedWireFormats: ["json", "protobuf"]
1186
+ };
1187
+ this.transport.send(handshake);
1188
+ } catch (err) {
1189
+ this.transitionTo("error");
1190
+ this.transitionTo("disconnected");
1191
+ throw err;
1192
+ }
1193
+ }
1194
+ /**
1195
+ * Stop the sync engine. Disconnects the transport.
1196
+ */
1197
+ async stop() {
1198
+ if (this.state === "disconnected") return;
1199
+ if (this.currentBatch) {
1200
+ this.outboundQueue.returnBatch(this.currentBatch.batchId);
1201
+ this.currentBatch = null;
1202
+ }
1203
+ try {
1204
+ await this.transport.disconnect();
1205
+ } finally {
1206
+ this.ensureDisconnected();
1207
+ }
1208
+ }
1209
+ ensureDisconnected() {
1210
+ if (this.state !== "disconnected") {
1211
+ this.transitionTo("disconnected");
1212
+ }
1213
+ }
1214
+ /**
1215
+ * Push a local operation to the outbound queue.
1216
+ * If streaming, flushes immediately.
1217
+ */
1218
+ async pushOperation(op) {
1219
+ await this.outboundQueue.enqueue(op);
1220
+ if (this.state === "streaming") {
1221
+ this.flushQueue();
1222
+ }
1223
+ }
1224
+ /**
1225
+ * Get the current developer-facing sync status.
1226
+ */
1227
+ getStatus() {
1228
+ const pendingOperations = this.outboundQueue.totalPending;
1229
+ switch (this.state) {
1230
+ case "disconnected":
1231
+ return { status: "offline", pendingOperations, lastSyncedAt: this.lastSyncedAt };
1232
+ case "connecting":
1233
+ case "handshaking":
1234
+ case "syncing":
1235
+ return { status: "syncing", pendingOperations, lastSyncedAt: this.lastSyncedAt };
1236
+ case "streaming":
1237
+ return {
1238
+ status: pendingOperations > 0 ? "syncing" : "synced",
1239
+ pendingOperations,
1240
+ lastSyncedAt: this.lastSyncedAt
1241
+ };
1242
+ case "error":
1243
+ return { status: "error", pendingOperations, lastSyncedAt: this.lastSyncedAt };
1244
+ }
1245
+ }
1246
+ /**
1247
+ * Get the current internal state (for testing).
1248
+ */
1249
+ getState() {
1250
+ return this.state;
1251
+ }
1252
+ /**
1253
+ * Get the outbound queue (for testing).
1254
+ */
1255
+ getOutboundQueue() {
1256
+ return this.outboundQueue;
1257
+ }
1258
+ // --- Private methods ---
1259
+ handleMessage(message) {
1260
+ switch (message.type) {
1261
+ case "handshake-response":
1262
+ this.handleHandshakeResponse(message);
1263
+ break;
1264
+ case "operation-batch":
1265
+ this.handleOperationBatch(message);
1266
+ break;
1267
+ case "acknowledgment":
1268
+ this.handleAcknowledgment(message);
1269
+ break;
1270
+ case "error":
1271
+ this.handleError(message);
1272
+ break;
1273
+ }
1274
+ }
1275
+ handleHandshakeResponse(msg) {
1276
+ if (this.state !== "handshaking") return;
1277
+ if (!msg.accepted) {
1278
+ this.transitionTo("error");
1279
+ this.emitter?.emit({
1280
+ type: "sync:disconnected",
1281
+ reason: msg.rejectReason ?? "Handshake rejected"
1282
+ });
1283
+ this.transitionTo("disconnected");
1284
+ return;
1285
+ }
1286
+ this.remoteVector = wireToVersionVector(msg.versionVector);
1287
+ if (msg.selectedWireFormat) {
1288
+ this.setSerializerWireFormat(msg.selectedWireFormat);
1289
+ }
1290
+ this.emitter?.emit({ type: "sync:connected", nodeId: this.store.getNodeId() });
1291
+ this.transitionTo("syncing");
1292
+ this.deltaBatchesReceived = 0;
1293
+ this.deltaReceiveComplete = false;
1294
+ this.deltaSendComplete = false;
1295
+ this.sendDelta();
1296
+ }
1297
+ async sendDelta() {
1298
+ const localVector = this.store.getVersionVector();
1299
+ const missingOps = await this.collectDelta(localVector, this.remoteVector);
1300
+ if (missingOps.length === 0) {
1301
+ const emptyBatch = {
1302
+ type: "operation-batch",
1303
+ messageId: generateMessageId(),
1304
+ operations: [],
1305
+ isFinal: true,
1306
+ batchIndex: 0
1307
+ };
1308
+ this.transport.send(emptyBatch);
1309
+ this.deltaSendComplete = true;
1310
+ this.checkDeltaComplete();
1311
+ return;
1312
+ }
1313
+ const sorted = topologicalSort2(missingOps);
1314
+ const totalBatches = Math.ceil(sorted.length / this.batchSize);
1315
+ for (let i = 0; i < totalBatches; i++) {
1316
+ const start = i * this.batchSize;
1317
+ const batchOps = sorted.slice(start, start + this.batchSize);
1318
+ const serializedOps = batchOps.map((op) => this.serializer.encodeOperation(op));
1319
+ const batchMsg = {
1320
+ type: "operation-batch",
1321
+ messageId: generateMessageId(),
1322
+ operations: serializedOps,
1323
+ isFinal: i === totalBatches - 1,
1324
+ batchIndex: i
1325
+ };
1326
+ this.transport.send(batchMsg);
1327
+ this.emitter?.emit({
1328
+ type: "sync:sent",
1329
+ operations: batchOps,
1330
+ batchSize: batchOps.length
1331
+ });
1332
+ }
1333
+ this.deltaSendComplete = true;
1334
+ this.checkDeltaComplete();
1335
+ }
1336
+ async collectDelta(localVector, remoteVector) {
1337
+ const missing = [];
1338
+ for (const [nodeId, localSeq] of localVector) {
1339
+ const remoteSeq = remoteVector.get(nodeId) ?? 0;
1340
+ if (localSeq > remoteSeq) {
1341
+ const ops = await this.store.getOperationRange(nodeId, remoteSeq + 1, localSeq);
1342
+ missing.push(...ops);
1343
+ }
1344
+ }
1345
+ return missing;
1346
+ }
1347
+ async handleOperationBatch(msg) {
1348
+ const operations = msg.operations.map((s) => this.serializer.decodeOperation(s));
1349
+ for (const op of operations) {
1350
+ await this.store.applyRemoteOperation(op);
1351
+ }
1352
+ if (operations.length > 0) {
1353
+ this.emitter?.emit({
1354
+ type: "sync:received",
1355
+ operations,
1356
+ batchSize: operations.length
1357
+ });
1358
+ }
1359
+ const lastOp = operations[operations.length - 1];
1360
+ const ack = {
1361
+ type: "acknowledgment",
1362
+ messageId: generateMessageId(),
1363
+ acknowledgedMessageId: msg.messageId,
1364
+ lastSequenceNumber: lastOp ? lastOp.sequenceNumber : 0
1365
+ };
1366
+ this.transport.send(ack);
1367
+ this.emitter?.emit({
1368
+ type: "sync:acknowledged",
1369
+ sequenceNumber: lastOp ? lastOp.sequenceNumber : 0
1370
+ });
1371
+ if (this.state === "syncing") {
1372
+ this.deltaBatchesReceived++;
1373
+ if (msg.isFinal) {
1374
+ this.deltaReceiveComplete = true;
1375
+ this.checkDeltaComplete();
1376
+ }
1377
+ }
1378
+ }
1379
+ handleAcknowledgment(msg) {
1380
+ if (this.currentBatch) {
1381
+ this.outboundQueue.acknowledge(this.currentBatch.batchId);
1382
+ this.currentBatch = null;
1383
+ this.lastSyncedAt = Date.now();
1384
+ }
1385
+ if (this.state === "streaming" && this.outboundQueue.hasOperations) {
1386
+ this.flushQueue();
1387
+ }
1388
+ }
1389
+ handleError(msg) {
1390
+ this.transitionTo("error");
1391
+ this.emitter?.emit({ type: "sync:disconnected", reason: msg.message });
1392
+ this.transitionTo("disconnected");
1393
+ }
1394
+ checkDeltaComplete() {
1395
+ if (this.deltaSendComplete && this.deltaReceiveComplete) {
1396
+ this.lastSyncedAt = Date.now();
1397
+ this.transitionTo("streaming");
1398
+ if (this.outboundQueue.hasOperations) {
1399
+ this.flushQueue();
1400
+ }
1401
+ }
1402
+ }
1403
+ flushQueue() {
1404
+ if (this.currentBatch) return;
1405
+ if (!this.outboundQueue.hasOperations) return;
1406
+ const batch = this.outboundQueue.takeBatch(this.batchSize);
1407
+ if (!batch) return;
1408
+ this.currentBatch = batch;
1409
+ const serializedOps = batch.operations.map((op) => this.serializer.encodeOperation(op));
1410
+ const batchMsg = {
1411
+ type: "operation-batch",
1412
+ messageId: generateMessageId(),
1413
+ operations: serializedOps,
1414
+ isFinal: true,
1415
+ batchIndex: 0
1416
+ };
1417
+ this.transport.send(batchMsg);
1418
+ this.emitter?.emit({
1419
+ type: "sync:sent",
1420
+ operations: batch.operations,
1421
+ batchSize: batch.operations.length
1422
+ });
1423
+ }
1424
+ handleTransportClose(reason) {
1425
+ if (this.currentBatch) {
1426
+ this.outboundQueue.returnBatch(this.currentBatch.batchId);
1427
+ this.currentBatch = null;
1428
+ }
1429
+ if (this.state !== "disconnected") {
1430
+ this.emitter?.emit({ type: "sync:disconnected", reason });
1431
+ this.transitionTo("disconnected");
1432
+ }
1433
+ }
1434
+ handleTransportError(err) {
1435
+ if (this.state !== "disconnected") {
1436
+ this.transitionTo("error");
1437
+ this.emitter?.emit({ type: "sync:disconnected", reason: err.message });
1438
+ this.transitionTo("disconnected");
1439
+ }
1440
+ }
1441
+ transitionTo(newState) {
1442
+ const validTargets = VALID_TRANSITIONS[this.state];
1443
+ if (!validTargets.includes(newState)) {
1444
+ throw new SyncError4(`Invalid sync state transition: ${this.state} \u2192 ${newState}`, {
1445
+ from: this.state,
1446
+ to: newState
1447
+ });
1448
+ }
1449
+ this.state = newState;
1450
+ }
1451
+ setSerializerWireFormat(format) {
1452
+ if (typeof this.serializer.setWireFormat === "function") {
1453
+ this.serializer.setWireFormat(format);
1454
+ }
1455
+ }
1456
+ };
1457
+
1458
+ // src/engine/connection-monitor.ts
1459
+ var ConnectionMonitor = class {
1460
+ windowSize;
1461
+ staleThreshold;
1462
+ timeSource;
1463
+ latencies = [];
1464
+ missedAcks = 0;
1465
+ lastActivityTime;
1466
+ constructor(config) {
1467
+ this.windowSize = config?.windowSize ?? 20;
1468
+ this.staleThreshold = config?.staleThreshold ?? 3e4;
1469
+ this.timeSource = config?.timeSource ?? { now: () => Date.now() };
1470
+ this.lastActivityTime = this.timeSource.now();
1471
+ }
1472
+ /**
1473
+ * Record a round-trip time sample.
1474
+ */
1475
+ recordLatency(ms) {
1476
+ this.latencies.push(ms);
1477
+ if (this.latencies.length > this.windowSize) {
1478
+ this.latencies.shift();
1479
+ }
1480
+ this.lastActivityTime = this.timeSource.now();
1481
+ this.missedAcks = 0;
1482
+ }
1483
+ /**
1484
+ * Record a missed acknowledgment (message sent but no response received).
1485
+ */
1486
+ recordMissedAck() {
1487
+ this.missedAcks++;
1488
+ }
1489
+ /**
1490
+ * Record any activity (message sent or received).
1491
+ */
1492
+ recordActivity() {
1493
+ this.lastActivityTime = this.timeSource.now();
1494
+ }
1495
+ /**
1496
+ * Assess current connection quality based on collected metrics.
1497
+ *
1498
+ * Quality thresholds (average RTT):
1499
+ * - excellent: < 100ms, 0 missed acks
1500
+ * - good: < 300ms, ≤ 1 missed ack
1501
+ * - fair: < 1000ms, ≤ 3 missed acks
1502
+ * - poor: < 5000ms or > 3 missed acks
1503
+ * - offline: no activity for staleThreshold ms
1504
+ */
1505
+ getQuality() {
1506
+ const elapsed = this.timeSource.now() - this.lastActivityTime;
1507
+ if (elapsed > this.staleThreshold) return "offline";
1508
+ if (this.latencies.length === 0) {
1509
+ return this.missedAcks > 3 ? "poor" : "good";
1510
+ }
1511
+ const avgLatency = this.latencies.reduce((sum, l) => sum + l, 0) / this.latencies.length;
1512
+ if (this.missedAcks > 3) return "poor";
1513
+ if (avgLatency < 100 && this.missedAcks === 0) return "excellent";
1514
+ if (avgLatency < 300 && this.missedAcks <= 1) return "good";
1515
+ if (avgLatency < 1e3 && this.missedAcks <= 3) return "fair";
1516
+ return "poor";
1517
+ }
1518
+ /**
1519
+ * Reset all metrics. Call on disconnect.
1520
+ */
1521
+ reset() {
1522
+ this.latencies = [];
1523
+ this.missedAcks = 0;
1524
+ this.lastActivityTime = this.timeSource.now();
1525
+ }
1526
+ /**
1527
+ * Get the current average latency in ms. Returns null if no samples.
1528
+ */
1529
+ getAverageLatency() {
1530
+ if (this.latencies.length === 0) return null;
1531
+ return this.latencies.reduce((sum, l) => sum + l, 0) / this.latencies.length;
1532
+ }
1533
+ /**
1534
+ * Get the number of missed acks.
1535
+ */
1536
+ getMissedAcks() {
1537
+ return this.missedAcks;
1538
+ }
1539
+ };
1540
+
1541
+ // src/engine/reconnection-manager.ts
1542
+ var ReconnectionManager = class {
1543
+ initialDelay;
1544
+ maxDelay;
1545
+ multiplier;
1546
+ maxAttempts;
1547
+ jitter;
1548
+ random;
1549
+ attempt = 0;
1550
+ timer = null;
1551
+ stopped = false;
1552
+ waitResolve = null;
1553
+ constructor(config) {
1554
+ this.initialDelay = config?.initialDelay ?? 1e3;
1555
+ this.maxDelay = config?.maxDelay ?? 3e4;
1556
+ this.multiplier = config?.multiplier ?? 2;
1557
+ this.maxAttempts = config?.maxAttempts ?? 0;
1558
+ this.jitter = config?.jitter ?? 0.25;
1559
+ this.random = config?.randomSource ?? Math.random;
1560
+ }
1561
+ /**
1562
+ * Start reconnection attempts. Calls `onReconnect` with exponential backoff.
1563
+ *
1564
+ * @param onReconnect - Called on each attempt. Return `true` if reconnection succeeded.
1565
+ * @returns Promise that resolves when reconnection succeeds or maxAttempts reached.
1566
+ */
1567
+ async start(onReconnect) {
1568
+ this.stopped = false;
1569
+ this.attempt = 0;
1570
+ while (!this.stopped) {
1571
+ if (this.maxAttempts > 0 && this.attempt >= this.maxAttempts) {
1572
+ return false;
1573
+ }
1574
+ const delay = this.getNextDelay();
1575
+ this.attempt++;
1576
+ await this.wait(delay);
1577
+ if (this.stopped) return false;
1578
+ try {
1579
+ const success = await onReconnect();
1580
+ if (success) {
1581
+ this.reset();
1582
+ return true;
1583
+ }
1584
+ } catch {
1585
+ }
1586
+ }
1587
+ return false;
1588
+ }
1589
+ /**
1590
+ * Stop any pending reconnection attempt.
1591
+ */
1592
+ stop() {
1593
+ this.stopped = true;
1594
+ if (this.timer !== null) {
1595
+ clearTimeout(this.timer);
1596
+ this.timer = null;
1597
+ }
1598
+ if (this.waitResolve) {
1599
+ this.waitResolve();
1600
+ this.waitResolve = null;
1601
+ }
1602
+ }
1603
+ /**
1604
+ * Reset the attempt counter. Call after a successful manual reconnection.
1605
+ */
1606
+ reset() {
1607
+ this.attempt = 0;
1608
+ this.stopped = false;
1609
+ }
1610
+ /**
1611
+ * Compute the next delay for the current attempt.
1612
+ * Exposed for testing purposes.
1613
+ */
1614
+ getNextDelay() {
1615
+ const baseDelay = Math.min(this.initialDelay * this.multiplier ** this.attempt, this.maxDelay);
1616
+ const jitterRange = baseDelay * this.jitter;
1617
+ const jitterOffset = (this.random() - 0.5) * 2 * jitterRange;
1618
+ return Math.max(0, Math.round(baseDelay + jitterOffset));
1619
+ }
1620
+ /**
1621
+ * Current attempt number (for testing).
1622
+ */
1623
+ getAttemptCount() {
1624
+ return this.attempt;
1625
+ }
1626
+ wait(ms) {
1627
+ return new Promise((resolve) => {
1628
+ this.waitResolve = resolve;
1629
+ this.timer = setTimeout(() => {
1630
+ this.timer = null;
1631
+ this.waitResolve = null;
1632
+ resolve();
1633
+ }, ms);
1634
+ });
1635
+ }
1636
+ };
1637
+ export {
1638
+ ChaosTransport,
1639
+ ConnectionMonitor,
1640
+ HttpLongPollingTransport,
1641
+ JsonMessageSerializer,
1642
+ NegotiatedMessageSerializer,
1643
+ OutboundQueue,
1644
+ ProtobufMessageSerializer,
1645
+ ReconnectionManager,
1646
+ SYNC_STATES,
1647
+ SYNC_STATUSES,
1648
+ SyncEngine,
1649
+ WebSocketTransport,
1650
+ isAcknowledgmentMessage,
1651
+ isErrorMessage,
1652
+ isHandshakeMessage,
1653
+ isHandshakeResponseMessage,
1654
+ isOperationBatchMessage,
1655
+ isSyncMessage,
1656
+ versionVectorToWire,
1657
+ wireToVersionVector
1658
+ };
1659
+ //# sourceMappingURL=index.js.map