@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 +101 -74
- package/dist/wsdispatcher.d.ts +23 -1
- package/dist/wsdispatcher.js +44 -11
- package/dist/wsocket.d.ts +5 -2
- package/dist/wsocket.js +7 -2
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
# @dojo-ng/websocket
|
|
2
2
|
|
|
3
|
-
A
|
|
4
|
-
|
|
5
|
-
|
|
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](
|
|
9
|
-
|
|
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
|
-
##
|
|
16
|
+
## Usage
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
28
|
-
|
|
29
|
-
ws
|
|
30
|
-
|
|
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
|
-
|
|
33
|
-
```
|
|
29
|
+
server.open();
|
|
34
30
|
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
|
|
43
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
83
|
+
## The wire format
|
|
53
84
|
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
##
|
|
100
|
+
## WSocket, the thinner layer
|
|
74
101
|
|
|
75
|
-
`
|
|
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
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
120
|
+
## Learn more
|
|
97
121
|
|
|
98
|
-
|
|
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
|
-
|
|
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
|
-
|
|
106
|
-
|
|
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](
|
|
137
|
+
BSD-3-Clause. See [LICENSE](https://foss.heptapod.net/dojo-ng/framework/-/blob/branch/default/LICENSE).
|
package/dist/wsdispatcher.d.ts
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/dist/wsdispatcher.js
CHANGED
|
@@ -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`): `
|
|
11
|
-
// counter reset only on a successful open, suppressed entirely while `#closing` is
|
|
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,
|
|
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
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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] +
|
|
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
|
-
/**
|
|
37
|
-
|
|
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
|
-
/**
|
|
77
|
-
|
|
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.
|
|
4
|
-
"description": "Dojo NG
|
|
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.
|
|
15
|
+
"@dojo-ng/pubsub": "^0.1.3"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
18
|
"ajv": "^8.20.0",
|