@korajs/sync 0.1.3 → 0.1.5
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 +80 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +80 -25
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.d.cts
CHANGED
|
@@ -121,6 +121,8 @@ interface WebSocketTransportOptions {
|
|
|
121
121
|
serializer?: MessageSerializer;
|
|
122
122
|
/** Injectable WebSocket constructor for testing. Defaults to globalThis.WebSocket. */
|
|
123
123
|
WebSocketImpl?: WebSocketConstructor;
|
|
124
|
+
/** Connection timeout in ms. Defaults to 10000 (10s). */
|
|
125
|
+
connectTimeout?: number;
|
|
124
126
|
}
|
|
125
127
|
/**
|
|
126
128
|
* WebSocket-based sync transport implementation.
|
|
@@ -132,6 +134,7 @@ declare class WebSocketTransport implements SyncTransport {
|
|
|
132
134
|
private errorHandler;
|
|
133
135
|
private readonly serializer;
|
|
134
136
|
private readonly WebSocketImpl;
|
|
137
|
+
private readonly connectTimeout;
|
|
135
138
|
constructor(options?: WebSocketTransportOptions);
|
|
136
139
|
connect(url: string, options?: TransportOptions): Promise<void>;
|
|
137
140
|
disconnect(): Promise<void>;
|
|
@@ -334,6 +337,7 @@ declare class SyncEngine {
|
|
|
334
337
|
private remoteVector;
|
|
335
338
|
private lastSyncedAt;
|
|
336
339
|
private currentBatch;
|
|
340
|
+
private reconnecting;
|
|
337
341
|
private deltaBatchesReceived;
|
|
338
342
|
private deltaReceiveComplete;
|
|
339
343
|
private deltaSendComplete;
|
|
@@ -352,6 +356,13 @@ declare class SyncEngine {
|
|
|
352
356
|
* If streaming, flushes immediately.
|
|
353
357
|
*/
|
|
354
358
|
pushOperation(op: Operation): Promise<void>;
|
|
359
|
+
/**
|
|
360
|
+
* Mark the engine as being in a reconnection loop. When reconnecting,
|
|
361
|
+
* `getStatus()` returns 'offline' instead of 'syncing' for intermediate
|
|
362
|
+
* states (connecting, handshaking, syncing), since the user is effectively
|
|
363
|
+
* disconnected until reconnection succeeds.
|
|
364
|
+
*/
|
|
365
|
+
setReconnecting(value: boolean): void;
|
|
355
366
|
/**
|
|
356
367
|
* Get the current developer-facing sync status.
|
|
357
368
|
*/
|
|
@@ -473,6 +484,7 @@ declare class ReconnectionManager {
|
|
|
473
484
|
private attempt;
|
|
474
485
|
private timer;
|
|
475
486
|
private stopped;
|
|
487
|
+
private running;
|
|
476
488
|
private waitResolve;
|
|
477
489
|
constructor(config?: ReconnectionConfig);
|
|
478
490
|
/**
|
|
@@ -495,6 +507,10 @@ declare class ReconnectionManager {
|
|
|
495
507
|
* Exposed for testing purposes.
|
|
496
508
|
*/
|
|
497
509
|
getNextDelay(): number;
|
|
510
|
+
/**
|
|
511
|
+
* Whether the reconnection loop is currently running.
|
|
512
|
+
*/
|
|
513
|
+
isRunning(): boolean;
|
|
498
514
|
/**
|
|
499
515
|
* Current attempt number (for testing).
|
|
500
516
|
*/
|
package/dist/index.d.ts
CHANGED
|
@@ -121,6 +121,8 @@ interface WebSocketTransportOptions {
|
|
|
121
121
|
serializer?: MessageSerializer;
|
|
122
122
|
/** Injectable WebSocket constructor for testing. Defaults to globalThis.WebSocket. */
|
|
123
123
|
WebSocketImpl?: WebSocketConstructor;
|
|
124
|
+
/** Connection timeout in ms. Defaults to 10000 (10s). */
|
|
125
|
+
connectTimeout?: number;
|
|
124
126
|
}
|
|
125
127
|
/**
|
|
126
128
|
* WebSocket-based sync transport implementation.
|
|
@@ -132,6 +134,7 @@ declare class WebSocketTransport implements SyncTransport {
|
|
|
132
134
|
private errorHandler;
|
|
133
135
|
private readonly serializer;
|
|
134
136
|
private readonly WebSocketImpl;
|
|
137
|
+
private readonly connectTimeout;
|
|
135
138
|
constructor(options?: WebSocketTransportOptions);
|
|
136
139
|
connect(url: string, options?: TransportOptions): Promise<void>;
|
|
137
140
|
disconnect(): Promise<void>;
|
|
@@ -334,6 +337,7 @@ declare class SyncEngine {
|
|
|
334
337
|
private remoteVector;
|
|
335
338
|
private lastSyncedAt;
|
|
336
339
|
private currentBatch;
|
|
340
|
+
private reconnecting;
|
|
337
341
|
private deltaBatchesReceived;
|
|
338
342
|
private deltaReceiveComplete;
|
|
339
343
|
private deltaSendComplete;
|
|
@@ -352,6 +356,13 @@ declare class SyncEngine {
|
|
|
352
356
|
* If streaming, flushes immediately.
|
|
353
357
|
*/
|
|
354
358
|
pushOperation(op: Operation): Promise<void>;
|
|
359
|
+
/**
|
|
360
|
+
* Mark the engine as being in a reconnection loop. When reconnecting,
|
|
361
|
+
* `getStatus()` returns 'offline' instead of 'syncing' for intermediate
|
|
362
|
+
* states (connecting, handshaking, syncing), since the user is effectively
|
|
363
|
+
* disconnected until reconnection succeeds.
|
|
364
|
+
*/
|
|
365
|
+
setReconnecting(value: boolean): void;
|
|
355
366
|
/**
|
|
356
367
|
* Get the current developer-facing sync status.
|
|
357
368
|
*/
|
|
@@ -473,6 +484,7 @@ declare class ReconnectionManager {
|
|
|
473
484
|
private attempt;
|
|
474
485
|
private timer;
|
|
475
486
|
private stopped;
|
|
487
|
+
private running;
|
|
476
488
|
private waitResolve;
|
|
477
489
|
constructor(config?: ReconnectionConfig);
|
|
478
490
|
/**
|
|
@@ -495,6 +507,10 @@ declare class ReconnectionManager {
|
|
|
495
507
|
* Exposed for testing purposes.
|
|
496
508
|
*/
|
|
497
509
|
getNextDelay(): number;
|
|
510
|
+
/**
|
|
511
|
+
* Whether the reconnection loop is currently running.
|
|
512
|
+
*/
|
|
513
|
+
isRunning(): boolean;
|
|
498
514
|
/**
|
|
499
515
|
* Current attempt number (for testing).
|
|
500
516
|
*/
|
package/dist/index.js
CHANGED
|
@@ -591,8 +591,10 @@ var WebSocketTransport = class {
|
|
|
591
591
|
errorHandler = null;
|
|
592
592
|
serializer;
|
|
593
593
|
WebSocketImpl;
|
|
594
|
+
connectTimeout;
|
|
594
595
|
constructor(options) {
|
|
595
596
|
this.serializer = options?.serializer ?? new JsonMessageSerializer();
|
|
597
|
+
this.connectTimeout = options?.connectTimeout ?? 1e4;
|
|
596
598
|
if (options?.WebSocketImpl) {
|
|
597
599
|
this.WebSocketImpl = options.WebSocketImpl;
|
|
598
600
|
} else if (typeof globalThis.WebSocket !== "undefined") {
|
|
@@ -608,12 +610,38 @@ var WebSocketTransport = class {
|
|
|
608
610
|
});
|
|
609
611
|
}
|
|
610
612
|
return new Promise((resolve, reject) => {
|
|
613
|
+
let settled = false;
|
|
614
|
+
const settle = (fn) => {
|
|
615
|
+
if (settled) return;
|
|
616
|
+
settled = true;
|
|
617
|
+
clearTimeout(timer);
|
|
618
|
+
fn();
|
|
619
|
+
};
|
|
620
|
+
const timer = setTimeout(() => {
|
|
621
|
+
settle(() => {
|
|
622
|
+
const err = new SyncError2("WebSocket connection timed out", {
|
|
623
|
+
url,
|
|
624
|
+
timeout: this.connectTimeout
|
|
625
|
+
});
|
|
626
|
+
if (this.ws) {
|
|
627
|
+
try {
|
|
628
|
+
this.ws.onclose = null;
|
|
629
|
+
this.ws.onerror = null;
|
|
630
|
+
this.ws.close();
|
|
631
|
+
} catch {
|
|
632
|
+
}
|
|
633
|
+
this.ws = null;
|
|
634
|
+
}
|
|
635
|
+
this.errorHandler?.(err);
|
|
636
|
+
reject(err);
|
|
637
|
+
});
|
|
638
|
+
}, this.connectTimeout);
|
|
611
639
|
try {
|
|
612
640
|
const connectUrl = options?.authToken ? `${url}${url.includes("?") ? "&" : "?"}token=${encodeURIComponent(options.authToken)}` : url;
|
|
613
641
|
const ws = new this.WebSocketImpl(connectUrl);
|
|
614
642
|
this.ws = ws;
|
|
615
643
|
ws.onopen = () => {
|
|
616
|
-
resolve();
|
|
644
|
+
settle(() => resolve());
|
|
617
645
|
};
|
|
618
646
|
ws.onmessage = (event) => {
|
|
619
647
|
try {
|
|
@@ -637,15 +665,17 @@ var WebSocketTransport = class {
|
|
|
637
665
|
this.errorHandler?.(err);
|
|
638
666
|
if (!this.isConnected()) {
|
|
639
667
|
this.ws = null;
|
|
640
|
-
reject(err);
|
|
668
|
+
settle(() => reject(err));
|
|
641
669
|
}
|
|
642
670
|
};
|
|
643
671
|
} catch (err) {
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
672
|
+
settle(
|
|
673
|
+
() => reject(
|
|
674
|
+
err instanceof SyncError2 ? err : new SyncError2("Failed to create WebSocket", {
|
|
675
|
+
url,
|
|
676
|
+
error: String(err)
|
|
677
|
+
})
|
|
678
|
+
)
|
|
649
679
|
);
|
|
650
680
|
}
|
|
651
681
|
});
|
|
@@ -1143,6 +1173,7 @@ var SyncEngine = class {
|
|
|
1143
1173
|
remoteVector = /* @__PURE__ */ new Map();
|
|
1144
1174
|
lastSyncedAt = null;
|
|
1145
1175
|
currentBatch = null;
|
|
1176
|
+
reconnecting = false;
|
|
1146
1177
|
// Track delta exchange state
|
|
1147
1178
|
deltaBatchesReceived = 0;
|
|
1148
1179
|
deltaReceiveComplete = false;
|
|
@@ -1187,8 +1218,7 @@ var SyncEngine = class {
|
|
|
1187
1218
|
};
|
|
1188
1219
|
this.transport.send(handshake);
|
|
1189
1220
|
} catch (err) {
|
|
1190
|
-
this.
|
|
1191
|
-
this.transitionTo("disconnected");
|
|
1221
|
+
this.ensureDisconnected();
|
|
1192
1222
|
throw err;
|
|
1193
1223
|
}
|
|
1194
1224
|
}
|
|
@@ -1222,6 +1252,15 @@ var SyncEngine = class {
|
|
|
1222
1252
|
this.flushQueue();
|
|
1223
1253
|
}
|
|
1224
1254
|
}
|
|
1255
|
+
/**
|
|
1256
|
+
* Mark the engine as being in a reconnection loop. When reconnecting,
|
|
1257
|
+
* `getStatus()` returns 'offline' instead of 'syncing' for intermediate
|
|
1258
|
+
* states (connecting, handshaking, syncing), since the user is effectively
|
|
1259
|
+
* disconnected until reconnection succeeds.
|
|
1260
|
+
*/
|
|
1261
|
+
setReconnecting(value) {
|
|
1262
|
+
this.reconnecting = value;
|
|
1263
|
+
}
|
|
1225
1264
|
/**
|
|
1226
1265
|
* Get the current developer-facing sync status.
|
|
1227
1266
|
*/
|
|
@@ -1233,7 +1272,11 @@ var SyncEngine = class {
|
|
|
1233
1272
|
case "connecting":
|
|
1234
1273
|
case "handshaking":
|
|
1235
1274
|
case "syncing":
|
|
1236
|
-
return {
|
|
1275
|
+
return {
|
|
1276
|
+
status: this.reconnecting ? "offline" : "syncing",
|
|
1277
|
+
pendingOperations,
|
|
1278
|
+
lastSyncedAt: this.lastSyncedAt
|
|
1279
|
+
};
|
|
1237
1280
|
case "streaming":
|
|
1238
1281
|
return {
|
|
1239
1282
|
status: pendingOperations > 0 ? "syncing" : "synced",
|
|
@@ -1550,6 +1593,7 @@ var ReconnectionManager = class {
|
|
|
1550
1593
|
attempt = 0;
|
|
1551
1594
|
timer = null;
|
|
1552
1595
|
stopped = false;
|
|
1596
|
+
running = false;
|
|
1553
1597
|
waitResolve = null;
|
|
1554
1598
|
constructor(config) {
|
|
1555
1599
|
this.initialDelay = config?.initialDelay ?? 1e3;
|
|
@@ -1567,25 +1611,30 @@ var ReconnectionManager = class {
|
|
|
1567
1611
|
*/
|
|
1568
1612
|
async start(onReconnect) {
|
|
1569
1613
|
this.stopped = false;
|
|
1614
|
+
this.running = true;
|
|
1570
1615
|
this.attempt = 0;
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1616
|
+
try {
|
|
1617
|
+
while (!this.stopped) {
|
|
1618
|
+
if (this.maxAttempts > 0 && this.attempt >= this.maxAttempts) {
|
|
1619
|
+
return false;
|
|
1620
|
+
}
|
|
1621
|
+
const delay = this.getNextDelay();
|
|
1622
|
+
this.attempt++;
|
|
1623
|
+
await this.wait(delay);
|
|
1624
|
+
if (this.stopped) return false;
|
|
1625
|
+
try {
|
|
1626
|
+
const success = await onReconnect();
|
|
1627
|
+
if (success) {
|
|
1628
|
+
this.reset();
|
|
1629
|
+
return true;
|
|
1630
|
+
}
|
|
1631
|
+
} catch {
|
|
1584
1632
|
}
|
|
1585
|
-
} catch {
|
|
1586
1633
|
}
|
|
1634
|
+
return false;
|
|
1635
|
+
} finally {
|
|
1636
|
+
this.running = false;
|
|
1587
1637
|
}
|
|
1588
|
-
return false;
|
|
1589
1638
|
}
|
|
1590
1639
|
/**
|
|
1591
1640
|
* Stop any pending reconnection attempt.
|
|
@@ -1618,6 +1667,12 @@ var ReconnectionManager = class {
|
|
|
1618
1667
|
const jitterOffset = (this.random() - 0.5) * 2 * jitterRange;
|
|
1619
1668
|
return Math.max(0, Math.round(baseDelay + jitterOffset));
|
|
1620
1669
|
}
|
|
1670
|
+
/**
|
|
1671
|
+
* Whether the reconnection loop is currently running.
|
|
1672
|
+
*/
|
|
1673
|
+
isRunning() {
|
|
1674
|
+
return this.running;
|
|
1675
|
+
}
|
|
1621
1676
|
/**
|
|
1622
1677
|
* Current attempt number (for testing).
|
|
1623
1678
|
*/
|