@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 CHANGED
@@ -605,10 +605,7 @@ declare class WebSocketSyncProvider {
605
605
  readonly url: string;
606
606
  readonly awareness: Awareness;
607
607
  private ws;
608
- private reconnectDelay;
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 = reconnectAttempts === 0 ? initialConnectTimeout : connectTimeout;
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
- reconnectAttempts = 0;
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 || reconnectAttempts >= maxReconnects) return;
354
- if (reconnectTimer) return;
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
- if (reconnectTimer) {
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 provider of shareRoomProviders.values()) provider.attach(connection);
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 provider of shareRoomProviders.values()) provider.detach();
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
- if (shareRoomProviders.has(room)) return;
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 provider = shareRoomProviders.get(room);
2342
- if (!provider) return;
2343
- provider.detach();
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
- reconnectDelay;
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
- this.reconnectDelay = options.reconnectDelay ?? 2e3;
2945
- this.maxReconnectAttempts = options.maxReconnectAttempts ?? Infinity;
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
- if (this.reconnectTimer) {
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.reconnectAttempts = 0;
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
- if (this.reconnectAttempts >= this.maxReconnectAttempts) return;
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.0",
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.11.0",
32
- "@xnetjs/data": "0.11.0",
33
- "@xnetjs/crypto": "0.11.0",
34
- "@xnetjs/data-bridge": "0.11.0",
35
- "@xnetjs/history": "0.11.0",
36
- "@xnetjs/identity": "0.11.0",
37
- "@xnetjs/plugins": "0.11.0",
38
- "@xnetjs/storage": "0.11.0",
39
- "@xnetjs/sync": "0.11.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",