@xnetjs/runtime 0.3.0 → 0.3.2
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.d.ts +1 -4
- package/dist/index.js +77 -43
- package/package.json +10 -10
package/dist/index.d.ts
CHANGED
|
@@ -605,10 +605,7 @@ declare class WebSocketSyncProvider {
|
|
|
605
605
|
readonly url: string;
|
|
606
606
|
readonly awareness: Awareness;
|
|
607
607
|
private ws;
|
|
608
|
-
private
|
|
609
|
-
private maxReconnectAttempts;
|
|
610
|
-
private reconnectAttempts;
|
|
611
|
-
private reconnectTimer;
|
|
608
|
+
private reconnectScheduler;
|
|
612
609
|
private destroyed;
|
|
613
610
|
private connected;
|
|
614
611
|
private synced;
|
package/dist/index.js
CHANGED
|
@@ -147,6 +147,43 @@ function createBlobSyncProvider(config) {
|
|
|
147
147
|
};
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
// src/sync/connection-manager.ts
|
|
151
|
+
import { capped, exponential, fixed, jittered, limitAttempts } from "@xnetjs/core";
|
|
152
|
+
|
|
153
|
+
// src/sync/reconnect-scheduler.ts
|
|
154
|
+
function createReconnectScheduler(options) {
|
|
155
|
+
let attempts = 0;
|
|
156
|
+
let timer = null;
|
|
157
|
+
return {
|
|
158
|
+
get attempts() {
|
|
159
|
+
return attempts;
|
|
160
|
+
},
|
|
161
|
+
get pending() {
|
|
162
|
+
return timer !== null;
|
|
163
|
+
},
|
|
164
|
+
schedule() {
|
|
165
|
+
if (timer) return false;
|
|
166
|
+
const delay = options.policy().delayFor(attempts + 1);
|
|
167
|
+
if (delay === null) return false;
|
|
168
|
+
attempts++;
|
|
169
|
+
timer = setTimeout(() => {
|
|
170
|
+
timer = null;
|
|
171
|
+
options.onRetry();
|
|
172
|
+
}, delay);
|
|
173
|
+
return true;
|
|
174
|
+
},
|
|
175
|
+
reset() {
|
|
176
|
+
attempts = 0;
|
|
177
|
+
},
|
|
178
|
+
cancel() {
|
|
179
|
+
if (timer) {
|
|
180
|
+
clearTimeout(timer);
|
|
181
|
+
timer = null;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
150
187
|
// src/sync/connection-manager.ts
|
|
151
188
|
function log(...args) {
|
|
152
189
|
if (typeof localStorage !== "undefined" && localStorage.getItem("xnet:sync:debug") === "true") {
|
|
@@ -156,8 +193,6 @@ function log(...args) {
|
|
|
156
193
|
function createConnectionManager(config) {
|
|
157
194
|
let ws = null;
|
|
158
195
|
let status = "disconnected";
|
|
159
|
-
let reconnectAttempts = 0;
|
|
160
|
-
let reconnectTimer = null;
|
|
161
196
|
let connectTimer = null;
|
|
162
197
|
let destroyed = false;
|
|
163
198
|
let connectInProgress = false;
|
|
@@ -168,6 +203,15 @@ function createConnectionManager(config) {
|
|
|
168
203
|
const maxReconnects = config.maxReconnects ?? Infinity;
|
|
169
204
|
const connectTimeout = config.connectTimeout ?? 1e4;
|
|
170
205
|
const initialConnectTimeout = config.initialConnectTimeout ?? (connectTimeout > 0 ? Math.min(connectTimeout, 6e3) : 0);
|
|
206
|
+
const reconnectPolicy = limitAttempts(
|
|
207
|
+
capped(exponential(reconnectDelay), maxReconnectDelay),
|
|
208
|
+
maxReconnects
|
|
209
|
+
);
|
|
210
|
+
const rateLimitPolicy = limitAttempts(jittered(fixed(rateLimitBackoffMs)), maxReconnects);
|
|
211
|
+
const reconnectScheduler = createReconnectScheduler({
|
|
212
|
+
policy: () => policyViolation ? rateLimitPolicy : reconnectPolicy,
|
|
213
|
+
onRetry: () => void doConnect()
|
|
214
|
+
});
|
|
171
215
|
function clearConnectTimer() {
|
|
172
216
|
if (connectTimer) {
|
|
173
217
|
clearTimeout(connectTimer);
|
|
@@ -197,7 +241,7 @@ function createConnectionManager(config) {
|
|
|
197
241
|
scheduleReconnect();
|
|
198
242
|
}
|
|
199
243
|
function armConnectTimeout() {
|
|
200
|
-
const timeout =
|
|
244
|
+
const timeout = reconnectScheduler.attempts === 0 ? initialConnectTimeout : connectTimeout;
|
|
201
245
|
if (timeout <= 0 || !Number.isFinite(timeout)) return;
|
|
202
246
|
connectTimer = setTimeout(onConnectTimeout, timeout);
|
|
203
247
|
}
|
|
@@ -317,7 +361,7 @@ function createConnectionManager(config) {
|
|
|
317
361
|
connectInProgress = false;
|
|
318
362
|
log("WebSocket connected");
|
|
319
363
|
setStatus("connected");
|
|
320
|
-
|
|
364
|
+
reconnectScheduler.reset();
|
|
321
365
|
policyViolation = false;
|
|
322
366
|
if (rooms.size > 0) {
|
|
323
367
|
const roomList = Array.from(rooms.keys());
|
|
@@ -350,20 +394,10 @@ function createConnectionManager(config) {
|
|
|
350
394
|
}
|
|
351
395
|
}
|
|
352
396
|
function scheduleReconnect() {
|
|
353
|
-
if (destroyed
|
|
354
|
-
if (
|
|
355
|
-
reconnectAttempts++;
|
|
356
|
-
let backoff;
|
|
357
|
-
if (policyViolation) {
|
|
397
|
+
if (destroyed) return;
|
|
398
|
+
if (reconnectScheduler.schedule()) {
|
|
358
399
|
policyViolation = false;
|
|
359
|
-
backoff = rateLimitBackoffMs + Math.floor(Math.random() * rateLimitBackoffMs * 0.5);
|
|
360
|
-
} else {
|
|
361
|
-
backoff = Math.min(reconnectDelay * 2 ** (reconnectAttempts - 1), maxReconnectDelay);
|
|
362
400
|
}
|
|
363
|
-
reconnectTimer = setTimeout(() => {
|
|
364
|
-
reconnectTimer = null;
|
|
365
|
-
void doConnect();
|
|
366
|
-
}, backoff);
|
|
367
401
|
}
|
|
368
402
|
return {
|
|
369
403
|
get status() {
|
|
@@ -379,10 +413,7 @@ function createConnectionManager(config) {
|
|
|
379
413
|
disconnect() {
|
|
380
414
|
destroyed = true;
|
|
381
415
|
clearConnectTimer();
|
|
382
|
-
|
|
383
|
-
clearTimeout(reconnectTimer);
|
|
384
|
-
reconnectTimer = null;
|
|
385
|
-
}
|
|
416
|
+
reconnectScheduler.cancel();
|
|
386
417
|
if (ws) {
|
|
387
418
|
if (rooms.size > 0) {
|
|
388
419
|
send({ type: "unsubscribe", topics: Array.from(rooms.keys()) });
|
|
@@ -2283,7 +2314,7 @@ function createSyncManager(config) {
|
|
|
2283
2314
|
log2("Connecting to signaling server...");
|
|
2284
2315
|
connection.connect();
|
|
2285
2316
|
nodeSyncProvider?.attach(connection);
|
|
2286
|
-
for (const
|
|
2317
|
+
for (const entry of shareRoomProviders.values()) entry.provider.attach(connection);
|
|
2287
2318
|
blobSync?.start();
|
|
2288
2319
|
const tracked = registry.getTracked();
|
|
2289
2320
|
log2("Joining rooms for", tracked.length, "tracked nodes");
|
|
@@ -2301,7 +2332,7 @@ function createSyncManager(config) {
|
|
|
2301
2332
|
});
|
|
2302
2333
|
blobSync?.stop();
|
|
2303
2334
|
nodeSyncProvider?.detach();
|
|
2304
|
-
for (const
|
|
2335
|
+
for (const entry of shareRoomProviders.values()) entry.provider.detach();
|
|
2305
2336
|
shareRoomProviders.clear();
|
|
2306
2337
|
for (const nodeId of Array.from(roomCleanups.keys())) {
|
|
2307
2338
|
leaveNodeRoom(nodeId);
|
|
@@ -2332,15 +2363,21 @@ function createSyncManager(config) {
|
|
|
2332
2363
|
leaveNodeRoom(nodeId);
|
|
2333
2364
|
},
|
|
2334
2365
|
subscribeShareRoom(room) {
|
|
2335
|
-
|
|
2366
|
+
const existing = shareRoomProviders.get(room);
|
|
2367
|
+
if (existing) {
|
|
2368
|
+
existing.refs += 1;
|
|
2369
|
+
return;
|
|
2370
|
+
}
|
|
2336
2371
|
const provider = new NodeStoreSyncProvider(config.nodeStore, room, true);
|
|
2337
|
-
shareRoomProviders.set(room, provider);
|
|
2372
|
+
shareRoomProviders.set(room, { provider, refs: 1 });
|
|
2338
2373
|
provider.attach(connection);
|
|
2339
2374
|
},
|
|
2340
2375
|
unsubscribeShareRoom(room) {
|
|
2341
|
-
const
|
|
2342
|
-
if (!
|
|
2343
|
-
|
|
2376
|
+
const entry = shareRoomProviders.get(room);
|
|
2377
|
+
if (!entry) return;
|
|
2378
|
+
entry.refs -= 1;
|
|
2379
|
+
if (entry.refs > 0) return;
|
|
2380
|
+
entry.provider.detach();
|
|
2344
2381
|
shareRoomProviders.delete(room);
|
|
2345
2382
|
},
|
|
2346
2383
|
async acquire(nodeId) {
|
|
@@ -2859,6 +2896,7 @@ async function runAdapterConformance(makeClient) {
|
|
|
2859
2896
|
}
|
|
2860
2897
|
|
|
2861
2898
|
// src/sync/WebSocketSyncProvider.ts
|
|
2899
|
+
import { fixed as fixed2, limitAttempts as limitAttempts2 } from "@xnetjs/core";
|
|
2862
2900
|
import { resolveSyncReplicationPolicy as resolveSyncReplicationPolicy2, signYjsUpdate as signYjsUpdate2, verifyYjsEnvelopeV1 as verifyYjsEnvelopeV12 } from "@xnetjs/sync";
|
|
2863
2901
|
import {
|
|
2864
2902
|
Awareness as Awareness2,
|
|
@@ -2924,10 +2962,7 @@ var WebSocketSyncProvider = class {
|
|
|
2924
2962
|
url;
|
|
2925
2963
|
awareness;
|
|
2926
2964
|
ws = null;
|
|
2927
|
-
|
|
2928
|
-
maxReconnectAttempts;
|
|
2929
|
-
reconnectAttempts = 0;
|
|
2930
|
-
reconnectTimer = null;
|
|
2965
|
+
reconnectScheduler;
|
|
2931
2966
|
destroyed = false;
|
|
2932
2967
|
connected = false;
|
|
2933
2968
|
synced = false;
|
|
@@ -2941,8 +2976,14 @@ var WebSocketSyncProvider = class {
|
|
|
2941
2976
|
this.room = options.room;
|
|
2942
2977
|
this.url = options.url;
|
|
2943
2978
|
this.options = options;
|
|
2944
|
-
|
|
2945
|
-
|
|
2979
|
+
const reconnectPolicy = limitAttempts2(
|
|
2980
|
+
fixed2(options.reconnectDelay ?? 2e3),
|
|
2981
|
+
options.maxReconnectAttempts ?? Infinity
|
|
2982
|
+
);
|
|
2983
|
+
this.reconnectScheduler = createReconnectScheduler({
|
|
2984
|
+
policy: () => reconnectPolicy,
|
|
2985
|
+
onRetry: () => this._connect()
|
|
2986
|
+
});
|
|
2946
2987
|
this.replicationPolicy = resolveSyncReplicationPolicy2(options.replication);
|
|
2947
2988
|
this.peerId = Math.random().toString(36).slice(2, 10);
|
|
2948
2989
|
this.awareness = new Awareness2(doc);
|
|
@@ -2982,10 +3023,7 @@ var WebSocketSyncProvider = class {
|
|
|
2982
3023
|
this.doc.off("update", this._onDocUpdate);
|
|
2983
3024
|
this.awareness.off("update", this._onAwarenessUpdate);
|
|
2984
3025
|
removeAwarenessStates2(this.awareness, [this.doc.clientID], this);
|
|
2985
|
-
|
|
2986
|
-
clearTimeout(this.reconnectTimer);
|
|
2987
|
-
this.reconnectTimer = null;
|
|
2988
|
-
}
|
|
3026
|
+
this.reconnectScheduler.cancel();
|
|
2989
3027
|
if (this.ws) {
|
|
2990
3028
|
this._send({ type: "unsubscribe", topics: [this.room] });
|
|
2991
3029
|
this.ws.close();
|
|
@@ -3002,7 +3040,7 @@ var WebSocketSyncProvider = class {
|
|
|
3002
3040
|
this.ws.onopen = () => {
|
|
3003
3041
|
log3(this, "WebSocket connected to", this.url);
|
|
3004
3042
|
this.connected = true;
|
|
3005
|
-
this.
|
|
3043
|
+
this.reconnectScheduler.reset();
|
|
3006
3044
|
this.emit("status", { connected: true });
|
|
3007
3045
|
log3(this, "Subscribing to room:", this.room);
|
|
3008
3046
|
this._send({ type: "subscribe", topics: [this.room] });
|
|
@@ -3049,11 +3087,7 @@ var WebSocketSyncProvider = class {
|
|
|
3049
3087
|
}
|
|
3050
3088
|
_scheduleReconnect() {
|
|
3051
3089
|
if (this.destroyed) return;
|
|
3052
|
-
|
|
3053
|
-
this.reconnectAttempts++;
|
|
3054
|
-
this.reconnectTimer = setTimeout(() => {
|
|
3055
|
-
this._connect();
|
|
3056
|
-
}, this.reconnectDelay);
|
|
3090
|
+
this.reconnectScheduler.schedule();
|
|
3057
3091
|
}
|
|
3058
3092
|
_send(msg) {
|
|
3059
3093
|
if (this.ws?.readyState === WebSocket.OPEN) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xnetjs/runtime",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Framework-agnostic xNet runtime: createXNetClient() and sync orchestration, usable from any framework, a CLI, a worker, or a Node service",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -28,15 +28,15 @@
|
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"y-protocols": "^1.0.6",
|
|
30
30
|
"yjs": "^13.6.24",
|
|
31
|
-
"@xnetjs/core": "0.
|
|
32
|
-
"@xnetjs/
|
|
33
|
-
"@xnetjs/
|
|
34
|
-
"@xnetjs/data-bridge": "0.
|
|
35
|
-
"@xnetjs/history": "0.
|
|
36
|
-
"@xnetjs/identity": "0.
|
|
37
|
-
"@xnetjs/plugins": "0.
|
|
38
|
-
"@xnetjs/storage": "0.
|
|
39
|
-
"@xnetjs/sync": "0.
|
|
31
|
+
"@xnetjs/core": "0.12.0",
|
|
32
|
+
"@xnetjs/crypto": "0.12.0",
|
|
33
|
+
"@xnetjs/data": "0.12.0",
|
|
34
|
+
"@xnetjs/data-bridge": "0.12.0",
|
|
35
|
+
"@xnetjs/history": "0.12.0",
|
|
36
|
+
"@xnetjs/identity": "0.12.0",
|
|
37
|
+
"@xnetjs/plugins": "0.12.0",
|
|
38
|
+
"@xnetjs/storage": "0.12.0",
|
|
39
|
+
"@xnetjs/sync": "0.12.0"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"jsdom": "^26.0.0",
|