@dojo-ng/websocket 0.1.2 → 0.2.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/README.md CHANGED
@@ -1,12 +1,11 @@
1
1
  # @dojo-ng/websocket
2
2
 
3
- A framework-agnostic WebSocket client: a raw-socket wrapper (`WSocket`) and a convenience
4
- dispatcher on top of it (`WSDispatcher`) with request/response correlation, an offline send
5
- queue, reconnect with backoff, and a heartbeat watchdog. The reasoning behind each non-obvious
6
- behavior below is in the header comments of `src/wsocket.ts` and `src/wsdispatcher.ts`.
3
+ A WebSocket client that reconnects, queues messages while offline, and matches each response to
4
+ its request. Events flow through a [`@dojo-ng/pubsub`](https://www.npmjs.com/package/@dojo-ng/pubsub)
5
+ bus in both directions.
7
6
 
8
- Part of the [Dojo NG framework](../../README.md) monorepo (the non-Lit runtime that complements
9
- `components`). BSD-3-Clause.
7
+ Part of the [Dojo NG framework](https://foss.heptapod.net/dojo-ng/framework) packages: plain ESM
8
+ and no Lit. BSD-3-Clause.
10
9
 
11
10
  ## Install
12
11
 
@@ -14,97 +13,125 @@ Part of the [Dojo NG framework](../../README.md) monorepo (the non-Lit runtime t
14
13
  npm install @dojo-ng/websocket
15
14
  ```
16
15
 
17
- ## `WSocket`
16
+ ## Usage
18
17
 
19
- Canonicalizes differences across `WebSocket` implementations: out-of-sync detection (a live
20
- re-check of `socket.readyState` on every send, since the browser's own state can move before its
21
- event fires), backpressure (`bufferedAmount > 0` returns `BUFFERING` instead of sending),
22
- listener teardown before close (the reconnect-storm fix), and `open(force)` semantics.
23
-
24
- ```ts
25
- import { WSocket, SendState } from "@dojo-ng/websocket";
18
+ ```js
19
+ import { createPubSub } from "@dojo-ng/pubsub";
20
+ import { WSDispatcher } from "@dojo-ng/websocket";
26
21
 
27
- const ws = new WSocket("wss://example.test");
28
- ws.addEventListener("open", () => console.log("open"));
29
- ws.addEventListener("message", (e) => console.log(e.detail.data));
30
- ws.open();
22
+ const bus = createPubSub();
23
+ const server = new WSDispatcher({
24
+ url: "wss://example.com/ws",
25
+ pubsub: bus, // share the app's bus; without this you get a private one
26
+ onConnect: () => [{ action: "event", id: 0, data: { event: "hello", params: { user: 7 } } }],
27
+ });
31
28
 
32
- const state = ws.send("hello"); // SendState.OKAY | NOT_READY | BUFFERING | SEND_ERROR
33
- ```
29
+ server.open();
34
30
 
35
- Most apps want `WSDispatcher`, below, rather than `WSocket` directly — it's the layer that
36
- carries the actual value (queueing, correlation, reconnect, heartbeat). `WSocket` is exported
37
- because it's a complete, independently useful primitive on its own, not because most callers need
38
- to reach for it.
31
+ // Request and response. Sent now if the socket is open, otherwise queued.
32
+ const user = await server.call({ method: "getUser", args: [7] });
39
33
 
40
- ## `WSDispatcher`
34
+ // Events from the server arrive on the bus under their own name.
35
+ bus.subscribe("chat:message", (msg) => console.log(msg), { replay: false });
41
36
 
42
- ```ts
43
- import { WSDispatcher } from "@dojo-ng/websocket";
37
+ // Publishing to the outbound topic sends an event to the server.
38
+ bus.publish("notify:event", { event: "chat:message", params: { text: "Hi" } });
39
+ ```
44
40
 
45
- const dispatcher = new WSDispatcher({
46
- url: "wss://example.test/ws",
47
- // Sent on every (re)connect, before the offline queue flushes. The app owns what identity
48
- // means here — this package has no opinion beyond "send these frames first."
49
- onConnect: () => [{ action: "event", id: 0, data: { event: "identify", params: { sessionId } } }],
41
+ ## What WSDispatcher handles
42
+
43
+ - `onConnect` returns messages to send first on every connect and reconnect, before the queue.
44
+ Use it to tell the server who the user is.
45
+ - Messages sent while the connection is down wait in a queue. A message leaves the queue only
46
+ after it is sent, so a failed send does not lose it.
47
+ - After a lost connection, the client waits longer before each new try (Fibonacci backoff). A
48
+ successful connect resets the delay. After `close()`, it does not reconnect.
49
+ - `reconnectBase` (seconds, default 15) is added to every reconnect delay. Lower it when the
50
+ server is on the same machine, where a disconnect is usually a quick restart. Keep it high for
51
+ a public site, so a server restart is not met by every tab at once.
52
+ - `url` can be a function (sync or async) instead of a string. It is called before every open,
53
+ the first connect and each reconnect, and what it returns is the URL for that attempt only.
54
+ Use it when the URL carries a single-use credential, such as a ticket in the query string
55
+ (a browser cannot set a header on a WebSocket). If the function throws or rejects, that counts
56
+ as a failed attempt and is retried through the backoff. To stop for good, call `close()` and
57
+ then throw. A string `url` works exactly as before.
58
+
59
+ ```js
60
+ const server = new WSDispatcher({
61
+ reconnectBase: 2,
62
+ url: async () => {
63
+ const res = await fetch("/api/ws-ticket", { method: "POST" });
64
+ if (res.status === 401) {
65
+ server.close(); // the credential was refused; do not retry
66
+ throw new Error("not authorized");
67
+ }
68
+ const { ticket } = await res.json();
69
+ return `wss://example.com/ws?ticket=${ticket}`;
70
+ },
50
71
  });
72
+ ```
73
+ - When the server sends `ping` messages, the client expects one at least every 40 seconds. If
74
+ none arrives, it closes and reconnects through the same backoff. A server that never sends a
75
+ ping turns this check off.
76
+ - `call()` has no timeout. If the server never answers, the promise never settles. Use
77
+ `Promise.race` with a timer if you need a limit.
78
+ - Publishing to `outboundTopic` sends an event to the server. The default topic is
79
+ `"notify:event"`.
80
+ - `simulate("disconnect")` and `simulate("connect")` let you test your reconnect handling without
81
+ a real outage.
51
82
 
52
- dispatcher.open();
83
+ ## The wire format
53
84
 
54
- // A correlated request/response, queued if the socket isn't open yet and flushed on connect.
55
- const result = await dispatcher.call({ method: "getUser", args: [42] });
85
+ Every message is one JSON object with `action`, `id`, and `data`. A `call` response sends back the
86
+ same `id`, and its `data` becomes the value of the promise. An `event` carries the topic name and
87
+ the payload.
56
88
 
57
- // Events flow through pub/sub in both directions — dispatcher.pubsub is a @dojo-ng/pubsub
58
- // instance (the caller's own, if one was passed via options.pubsub, otherwise a private one).
59
- // Inbound "event" frames publish under their own name; publishing to options.outboundTopic
60
- // (default "notify:event") sends one outbound. No new subscription concept invented.
61
- dispatcher.pubsub.subscribe("chat:message", (payload) => render(payload));
62
- dispatcher.pubsub.publish("notify:event", { event: "chat:message", params: { text: "hi" } });
89
+ ```json
90
+ { "action": "call", "id": 12, "data": { "method": "getUser", "args": [7] } }
91
+ { "action": "event", "id": 0, "data": { "event": "chat:message", "params": { "text": "Hi" } } }
92
+ { "action": "ping", "id": 0, "data": null }
63
93
  ```
64
94
 
65
- What it carries, and why each exists, is documented at length in the header comment of
66
- `src/wsdispatcher.ts` — request/response correlation via `Promise.withResolvers()`, an offline
67
- send queue that peeks and dequeues only on a confirmed send, Fibonacci-backoff reconnect (reset
68
- only on a successful open, suppressed entirely by an explicit `close()`), and a heartbeat
69
- watchdog that closes and reopens — through the same backoff path, not a bypass — if the server
70
- goes quiet for 40s. `dispatcher.simulate("disconnect" | "connect")` is a deliberate testing
71
- affordance; the playground (below) uses it.
95
+ The full rules are in
96
+ [the JSON Schema](https://foss.heptapod.net/dojo-ng/framework/-/blob/branch/default/packages/websocket/schema/envelope.schema.json).
97
+ The object form was chosen after measuring: with compression, a typical session is only about
98
+ 3% larger than with a positional array.
72
99
 
73
- ## The wire format
100
+ ## WSocket, the thinner layer
74
101
 
75
- `schema/envelope.schema.json` (JSON Schema draft-07) specifies the `{action, id, data}` frame
76
- `WSDispatcher` sends and receives, independently of the TypeScript `Envelope` type that binds it
77
- — the schema is the source of truth, the type is one binding of it. The object form was chosen
78
- over a positional-array form (`[action, id, data]`) by measurement, not argument: compressed (raw
79
- DEFLATE, context carried across a session — the realistic case), the object form is only 3.4%
80
- larger than positional over a representative session. `spike/measure-envelope.mjs` reproduces
81
- the measurement.
102
+ Most apps want `WSDispatcher`. If you only want a thin wrapper, the package also exports `WSocket`.
82
103
 
83
- ## Playground
104
+ - It smooths over differences between WebSocket implementations.
105
+ - `send()` returns a `SendState`: `OKAY`, `NOT_READY`, `BUFFERING`, or `SEND_ERROR`. It reports
106
+ `BUFFERING` instead of sending while the browser still has unsent data.
107
+ - It removes its listeners before closing, so a reconnect does not stack old handlers.
84
108
 
85
- A live connection against a local echo server, demonstrating queued sends while disconnected, a
86
- real reconnect with Fibonacci backoff, and the heartbeat watchdog — each one driven by a control
87
- a real client can't perform on itself (the server dropping the connection with no handshake;
88
- the server going silent without closing the socket), because `simulate("disconnect")` alone
89
- proves the opposite case: an explicit close schedules no reconnect at all.
109
+ ```js
110
+ import { WSocket, SendState } from "@dojo-ng/websocket";
90
111
 
91
- ```bash
92
- npm run playground
93
- # → http://localhost:8901/packages/websocket/playground/index.html
112
+ const ws = new WSocket("wss://example.com");
113
+ ws.addEventListener("open", () => console.log("open"));
114
+ ws.addEventListener("message", (e) => console.log(e.detail.data));
115
+ ws.open();
116
+
117
+ const state = ws.send("hello"); // SendState.OKAY, NOT_READY, BUFFERING, or SEND_ERROR
94
118
  ```
95
119
 
96
- See the comment at the top of `playground/index.html` for exactly what each button demonstrates.
120
+ ## Learn more
97
121
 
98
- ## Testing
122
+ The [framework runtime guide](https://dojo-ng.com/docs/framework/) shows the client together with
123
+ [`@dojo-ng/framework`](https://www.npmjs.com/package/@dojo-ng/framework) and
124
+ [`@dojo-ng/pubsub`](https://www.npmjs.com/package/@dojo-ng/pubsub).
99
125
 
100
- | Command | What it runs |
101
- |---|---|
102
- | `npm test` | `node --test` over `test/*.test.mjs` — `Queue`, `WSocket`, `WSDispatcher`, and the envelope schema, against fake sockets and (for the reconnect/heartbeat cases) `node:test`'s built-in fake timers |
103
- | `npm run typecheck` | `tsc --noEmit` |
126
+ ## Development
104
127
 
105
- No real network in any test — `test/helpers/fake-websocket.mjs` is a synchronous fake shared
106
- across the `WSocket` and `WSDispatcher` suites.
128
+ - `npm test` runs the unit tests with `node --test`, against a fake socket and fake timers. No
129
+ test uses the network.
130
+ - `npm run typecheck` runs `tsc --noEmit`.
131
+ - `npm run playground` starts a local echo server and a demo page at
132
+ `http://localhost:8901/packages/websocket/playground/index.html`. Its buttons show the offline
133
+ queue, a real reconnect with backoff, and the heartbeat check.
107
134
 
108
135
  ## License
109
136
 
110
- BSD-3-Clause. See [LICENSE](../../LICENSE).
137
+ BSD-3-Clause. See [LICENSE](https://foss.heptapod.net/dojo-ng/framework/-/blob/branch/default/LICENSE).
@@ -1,7 +1,28 @@
1
1
  import { type PubSub } from "@dojo-ng/pubsub";
2
2
  import { WSocket } from "./wsocket.js";
3
3
  export interface WSDispatcherOptions {
4
- url: string;
4
+ /**
5
+ * The socket URL, or a function returning it (sync or async). A function is called before every
6
+ * open, first connect and every reconnect alike, and its result is the URL for that attempt only
7
+ * — use it when the URL carries a single-use credential such as a ticket. A function that throws
8
+ * or rejects counts as a failed attempt and is retried through the reconnect backoff. To stop
9
+ * for good (say the credential was refused), call `close()` and then throw.
10
+ *
11
+ * ```ts
12
+ * url: async () => {
13
+ * const res = await fetch("/api/ws-ticket", { method: "POST" });
14
+ * if (res.status === 401) { dispatcher.close(); throw new Error("not authorized"); }
15
+ * return `wss://example.com/ws?ticket=${(await res.json()).ticket}`;
16
+ * }
17
+ * ```
18
+ */
19
+ url: string | (() => string | Promise<string>);
20
+ /**
21
+ * Seconds added to every reconnect delay, on top of the Fibonacci step. Default 15. Lower it for
22
+ * a client on the same machine as its server (a daemon restart); keep it high for a public site
23
+ * so a server restart is not met by every tab at once.
24
+ */
25
+ reconnectBase?: number;
5
26
  /** Frames to send first on every (re)connect, before the offline queue flushes. */
6
27
  onConnect?: () => unknown[];
7
28
  /** Shared with the rest of the app if given; otherwise a private instance is created. */
@@ -16,6 +37,7 @@ export interface WSDispatcherOptions {
16
37
  export declare class WSDispatcher {
17
38
  #private;
18
39
  constructor(options: WSDispatcherOptions);
40
+ /** The most recent URL opened; empty until a URL provider has resolved once. */
19
41
  get url(): string;
20
42
  /** The underlying `WSocket`, for connection-state inspection. */
21
43
  get socket(): WSocket;
@@ -7,8 +7,14 @@
7
7
  // - Offline send queue (`#drain`): peeks and sends, dequeuing only on a confirmed OKAY send, so a
8
8
  // failed send never drops the message. BUFFERING retries at 50ms; a confirmed send immediately
9
9
  // tries the next one.
10
- // - Reconnect with Fibonacci backoff (`#scheduleReopen`): `TIMEOUT_BASE` 15s plus the step table,
11
- // counter reset only on a successful open, suppressed entirely while `#closing` is true.
10
+ // - Reconnect with Fibonacci backoff (`#scheduleReopen`): `reconnectBase` (default 15s) plus the
11
+ // step table, counter reset only on a successful open, suppressed entirely while `#closing` is
12
+ // true.
13
+ // - URL provider: `url` may be a function, called before EVERY open (first connect and each
14
+ // reconnect) so a single-use ticket in the query string is fresh each time. A provider that
15
+ // throws or rejects is a failed attempt and goes through `#scheduleReopen`; one that wants to stop
16
+ // for good calls `close()` before throwing. `#closing` is rechecked after the provider resolves,
17
+ // so a `close()` that lands mid-await wins.
12
18
  // - Heartbeat watchdog (`#armHeartbeat`): each inbound "ping" frame arms a 40s timer. On expiry:
13
19
  // close, wait 2s, then go through the SAME backoff-aware reopen path as any other reconnect —
14
20
  // not a bypass. `TIMEOUT_STEP`'s reset-only-on-success rule means a heartbeat failure right after
@@ -42,11 +48,11 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
42
48
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
43
49
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
44
50
  };
45
- var _WSDispatcher_instances, _WSDispatcher_url, _WSDispatcher_ws, _WSDispatcher_pubsub, _WSDispatcher_onConnect, _WSDispatcher_msgQueue, _WSDispatcher_cbqueue, _WSDispatcher_closing, _WSDispatcher_reopenCount, _WSDispatcher_nextId, _WSDispatcher_pingTimer, _WSDispatcher_scheduleReopen, _WSDispatcher_flushQueue, _WSDispatcher_enqueue, _WSDispatcher_drain, _WSDispatcher_dispatch, _WSDispatcher_armHeartbeat;
51
+ var _WSDispatcher_instances, _WSDispatcher_urlSource, _WSDispatcher_reconnectBase, _WSDispatcher_ws, _WSDispatcher_pubsub, _WSDispatcher_onConnect, _WSDispatcher_msgQueue, _WSDispatcher_cbqueue, _WSDispatcher_closing, _WSDispatcher_resolvingUrl, _WSDispatcher_reopenCount, _WSDispatcher_nextId, _WSDispatcher_pingTimer, _WSDispatcher_openResolved, _WSDispatcher_scheduleReopen, _WSDispatcher_flushQueue, _WSDispatcher_enqueue, _WSDispatcher_drain, _WSDispatcher_dispatch, _WSDispatcher_armHeartbeat;
46
52
  import { createPubSub } from "@dojo-ng/pubsub";
47
53
  import { Queue } from "./queue.js";
48
54
  import { WSocket, SendState } from "./wsocket.js";
49
- const TIMEOUT_BASE = 15;
55
+ const DEFAULT_RECONNECT_BASE = 15;
50
56
  const TIMEOUT_STEP = [0, 0, 0, 0, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233];
51
57
  const PING_TIMEOUT_MS = 40000;
52
58
  const PING_RECOVERY_DELAY_MS = 2000;
@@ -57,20 +63,23 @@ const PING_RECOVERY_DELAY_MS = 2000;
57
63
  export class WSDispatcher {
58
64
  constructor(options) {
59
65
  _WSDispatcher_instances.add(this);
60
- _WSDispatcher_url.set(this, void 0);
66
+ _WSDispatcher_urlSource.set(this, void 0);
67
+ _WSDispatcher_reconnectBase.set(this, void 0);
61
68
  _WSDispatcher_ws.set(this, void 0);
62
69
  _WSDispatcher_pubsub.set(this, void 0);
63
70
  _WSDispatcher_onConnect.set(this, void 0);
64
71
  _WSDispatcher_msgQueue.set(this, new Queue());
65
72
  _WSDispatcher_cbqueue.set(this, new Map());
66
73
  _WSDispatcher_closing.set(this, false);
74
+ _WSDispatcher_resolvingUrl.set(this, false);
67
75
  _WSDispatcher_reopenCount.set(this, 0);
68
76
  _WSDispatcher_nextId.set(this, 1);
69
77
  _WSDispatcher_pingTimer.set(this, null);
70
- __classPrivateFieldSet(this, _WSDispatcher_url, options.url, "f");
78
+ __classPrivateFieldSet(this, _WSDispatcher_urlSource, options.url, "f");
79
+ __classPrivateFieldSet(this, _WSDispatcher_reconnectBase, options.reconnectBase ?? DEFAULT_RECONNECT_BASE, "f");
71
80
  __classPrivateFieldSet(this, _WSDispatcher_onConnect, options.onConnect, "f");
72
81
  __classPrivateFieldSet(this, _WSDispatcher_pubsub, options.pubsub ?? createPubSub(), "f");
73
- __classPrivateFieldSet(this, _WSDispatcher_ws, new WSocket(__classPrivateFieldGet(this, _WSDispatcher_url, "f")), "f");
82
+ __classPrivateFieldSet(this, _WSDispatcher_ws, new WSocket(typeof __classPrivateFieldGet(this, _WSDispatcher_urlSource, "f") === "string" ? __classPrivateFieldGet(this, _WSDispatcher_urlSource, "f") : ""), "f");
74
83
  __classPrivateFieldGet(this, _WSDispatcher_ws, "f").addEventListener("message", (event) => {
75
84
  __classPrivateFieldGet(this, _WSDispatcher_instances, "m", _WSDispatcher_dispatch).call(this, event.detail.data);
76
85
  });
@@ -84,8 +93,9 @@ export class WSDispatcher {
84
93
  const outboundTopic = options.outboundTopic ?? "notify:event";
85
94
  __classPrivateFieldGet(this, _WSDispatcher_pubsub, "f").subscribe(outboundTopic, (payload) => __classPrivateFieldGet(this, _WSDispatcher_instances, "m", _WSDispatcher_enqueue).call(this, { action: "event", id: 0, data: payload }), { replay: false });
86
95
  }
96
+ /** The most recent URL opened; empty until a URL provider has resolved once. */
87
97
  get url() {
88
- return __classPrivateFieldGet(this, _WSDispatcher_url, "f");
98
+ return __classPrivateFieldGet(this, _WSDispatcher_ws, "f").url;
89
99
  }
90
100
  /** The underlying `WSocket`, for connection-state inspection. */
91
101
  get socket() {
@@ -100,7 +110,14 @@ export class WSDispatcher {
100
110
  }
101
111
  open(force = false) {
102
112
  __classPrivateFieldSet(this, _WSDispatcher_closing, false, "f");
103
- __classPrivateFieldGet(this, _WSDispatcher_ws, "f").open(force);
113
+ if (typeof __classPrivateFieldGet(this, _WSDispatcher_urlSource, "f") === "string") {
114
+ __classPrivateFieldGet(this, _WSDispatcher_ws, "f").open(force);
115
+ return;
116
+ }
117
+ // A second open while the provider is pending would spend a second single-use URL.
118
+ if (__classPrivateFieldGet(this, _WSDispatcher_resolvingUrl, "f"))
119
+ return;
120
+ void __classPrivateFieldGet(this, _WSDispatcher_instances, "m", _WSDispatcher_openResolved).call(this, __classPrivateFieldGet(this, _WSDispatcher_urlSource, "f"), force);
104
121
  }
105
122
  close() {
106
123
  __classPrivateFieldSet(this, _WSDispatcher_closing, true, "f");
@@ -126,12 +143,28 @@ export class WSDispatcher {
126
143
  }
127
144
  }
128
145
  }
129
- _WSDispatcher_url = new WeakMap(), _WSDispatcher_ws = new WeakMap(), _WSDispatcher_pubsub = new WeakMap(), _WSDispatcher_onConnect = new WeakMap(), _WSDispatcher_msgQueue = new WeakMap(), _WSDispatcher_cbqueue = new WeakMap(), _WSDispatcher_closing = new WeakMap(), _WSDispatcher_reopenCount = new WeakMap(), _WSDispatcher_nextId = new WeakMap(), _WSDispatcher_pingTimer = new WeakMap(), _WSDispatcher_instances = new WeakSet(), _WSDispatcher_scheduleReopen = function _WSDispatcher_scheduleReopen() {
146
+ _WSDispatcher_urlSource = new WeakMap(), _WSDispatcher_reconnectBase = new WeakMap(), _WSDispatcher_ws = new WeakMap(), _WSDispatcher_pubsub = new WeakMap(), _WSDispatcher_onConnect = new WeakMap(), _WSDispatcher_msgQueue = new WeakMap(), _WSDispatcher_cbqueue = new WeakMap(), _WSDispatcher_closing = new WeakMap(), _WSDispatcher_resolvingUrl = new WeakMap(), _WSDispatcher_reopenCount = new WeakMap(), _WSDispatcher_nextId = new WeakMap(), _WSDispatcher_pingTimer = new WeakMap(), _WSDispatcher_instances = new WeakSet(), _WSDispatcher_openResolved = async function _WSDispatcher_openResolved(provider, force) {
147
+ __classPrivateFieldSet(this, _WSDispatcher_resolvingUrl, true, "f");
148
+ let url;
149
+ try {
150
+ url = await provider();
151
+ }
152
+ catch {
153
+ __classPrivateFieldSet(this, _WSDispatcher_resolvingUrl, false, "f");
154
+ __classPrivateFieldGet(this, _WSDispatcher_instances, "m", _WSDispatcher_scheduleReopen).call(this);
155
+ return;
156
+ }
157
+ __classPrivateFieldSet(this, _WSDispatcher_resolvingUrl, false, "f");
158
+ // A close() that landed while the provider was pending must win.
159
+ if (__classPrivateFieldGet(this, _WSDispatcher_closing, "f"))
160
+ return;
161
+ __classPrivateFieldGet(this, _WSDispatcher_ws, "f").open(force, url);
162
+ }, _WSDispatcher_scheduleReopen = function _WSDispatcher_scheduleReopen() {
130
163
  var _a;
131
164
  if (__classPrivateFieldGet(this, _WSDispatcher_closing, "f"))
132
165
  return;
133
166
  const step = Math.min(__classPrivateFieldGet(this, _WSDispatcher_reopenCount, "f"), TIMEOUT_STEP.length - 1);
134
- const timeoutSeconds = TIMEOUT_STEP[step] + TIMEOUT_BASE;
167
+ const timeoutSeconds = TIMEOUT_STEP[step] + __classPrivateFieldGet(this, _WSDispatcher_reconnectBase, "f");
135
168
  __classPrivateFieldSet(this, _WSDispatcher_reopenCount, (_a = __classPrivateFieldGet(this, _WSDispatcher_reopenCount, "f"), _a++, _a), "f");
136
169
  setTimeout(() => {
137
170
  this.open();
package/dist/wsocket.d.ts CHANGED
@@ -33,8 +33,11 @@ export declare class WSocket extends EventTarget {
33
33
  get socket(): WebSocket | null;
34
34
  /** Our own tracked state, not a live re-check of `socket.readyState` — see `send()` for why. */
35
35
  isOpen(): boolean;
36
- /** With no existing socket, or `force: true`, tears down any prior socket and opens a fresh one. */
37
- open(force?: boolean): void;
36
+ /**
37
+ * With no existing socket, or `force: true`, tears down any prior socket and opens a fresh one.
38
+ * A `url` replaces the stored one for this socket and onward, so `url` reports the latest.
39
+ */
40
+ open(force?: boolean, url?: string): void;
38
41
  close(code?: number, reason?: string): void;
39
42
  send(message: string | Blob | BufferSource): SendState;
40
43
  }
package/dist/wsocket.js CHANGED
@@ -73,8 +73,11 @@ export class WSocket extends EventTarget {
73
73
  isOpen() {
74
74
  return __classPrivateFieldGet(this, _WSocket_socket, "f") !== null && __classPrivateFieldGet(this, _WSocket_state, "f") === ReadyState.OPEN;
75
75
  }
76
- /** With no existing socket, or `force: true`, tears down any prior socket and opens a fresh one. */
77
- open(force = false) {
76
+ /**
77
+ * With no existing socket, or `force: true`, tears down any prior socket and opens a fresh one.
78
+ * A `url` replaces the stored one for this socket and onward, so `url` reports the latest.
79
+ */
80
+ open(force = false, url) {
78
81
  if (__classPrivateFieldGet(this, _WSocket_socket, "f")) {
79
82
  if (!force)
80
83
  return;
@@ -86,6 +89,8 @@ export class WSocket extends EventTarget {
86
89
  stale.close();
87
90
  __classPrivateFieldSet(this, _WSocket_socket, null, "f");
88
91
  }
92
+ if (url !== undefined)
93
+ __classPrivateFieldSet(this, _WSocket_url, url, "f");
89
94
  __classPrivateFieldSet(this, _WSocket_state, ReadyState.CONNECTING, "f");
90
95
  const socket = (__classPrivateFieldSet(this, _WSocket_socket, new WebSocket(__classPrivateFieldGet(this, _WSocket_url, "f")), "f"));
91
96
  // Pre-open error: straight to close, no `error` event — see the file header.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dojo-ng/websocket",
3
- "version": "0.1.2",
4
- "description": "Dojo NG websocket client",
3
+ "version": "0.2.0",
4
+ "description": "Dojo NG WebSocket client that reconnects, queues messages while offline, and matches responses to requests",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "module": "./dist/index.js",
@@ -12,7 +12,7 @@
12
12
  ],
13
13
  "license": "BSD-3-Clause",
14
14
  "dependencies": {
15
- "@dojo-ng/pubsub": "^0.1.2"
15
+ "@dojo-ng/pubsub": "^0.1.3"
16
16
  },
17
17
  "devDependencies": {
18
18
  "ajv": "^8.20.0",