@dojo-ng/websocket 0.1.2 → 0.1.3
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 +78 -75
- 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,101 @@ 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.
|
|
18
|
+
```js
|
|
19
|
+
import { createPubSub } from "@dojo-ng/pubsub";
|
|
20
|
+
import { WSDispatcher } from "@dojo-ng/websocket";
|
|
23
21
|
|
|
24
|
-
|
|
25
|
-
|
|
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
|
+
});
|
|
26
28
|
|
|
27
|
-
|
|
28
|
-
ws.addEventListener("open", () => console.log("open"));
|
|
29
|
-
ws.addEventListener("message", (e) => console.log(e.detail.data));
|
|
30
|
-
ws.open();
|
|
29
|
+
server.open();
|
|
31
30
|
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
// Request and response. Sent now if the socket is open, otherwise queued.
|
|
32
|
+
const user = await server.call({ method: "getUser", args: [7] });
|
|
34
33
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
because it's a complete, independently useful primitive on its own, not because most callers need
|
|
38
|
-
to reach for it.
|
|
34
|
+
// Events from the server arrive on the bus under their own name.
|
|
35
|
+
bus.subscribe("chat:message", (msg) => console.log(msg), { replay: false });
|
|
39
36
|
|
|
40
|
-
|
|
37
|
+
// Publishing to the outbound topic sends an event to the server.
|
|
38
|
+
bus.publish("notify:event", { event: "chat:message", params: { text: "Hi" } });
|
|
39
|
+
```
|
|
41
40
|
|
|
42
|
-
|
|
43
|
-
|
|
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
|
+
- When the server sends `ping` messages, the client expects one at least every 40 seconds. If
|
|
50
|
+
none arrives, it closes and reconnects through the same backoff. A server that never sends a
|
|
51
|
+
ping turns this check off.
|
|
52
|
+
- `call()` has no timeout. If the server never answers, the promise never settles. Use
|
|
53
|
+
`Promise.race` with a timer if you need a limit.
|
|
54
|
+
- Publishing to `outboundTopic` sends an event to the server. The default topic is
|
|
55
|
+
`"notify:event"`.
|
|
56
|
+
- `simulate("disconnect")` and `simulate("connect")` let you test your reconnect handling without
|
|
57
|
+
a real outage.
|
|
44
58
|
|
|
45
|
-
|
|
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 } } }],
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
dispatcher.open();
|
|
59
|
+
## The wire format
|
|
53
60
|
|
|
54
|
-
|
|
55
|
-
|
|
61
|
+
Every message is one JSON object with `action`, `id`, and `data`. A `call` response sends back the
|
|
62
|
+
same `id`, and its `data` becomes the value of the promise. An `event` carries the topic name and
|
|
63
|
+
the payload.
|
|
56
64
|
|
|
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" } });
|
|
65
|
+
```json
|
|
66
|
+
{ "action": "call", "id": 12, "data": { "method": "getUser", "args": [7] } }
|
|
67
|
+
{ "action": "event", "id": 0, "data": { "event": "chat:message", "params": { "text": "Hi" } } }
|
|
68
|
+
{ "action": "ping", "id": 0, "data": null }
|
|
63
69
|
```
|
|
64
70
|
|
|
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.
|
|
71
|
+
The full rules are in
|
|
72
|
+
[the JSON Schema](https://foss.heptapod.net/dojo-ng/framework/-/blob/branch/default/packages/websocket/schema/envelope.schema.json).
|
|
73
|
+
The object form was chosen after measuring: with compression, a typical session is only about
|
|
74
|
+
3% larger than with a positional array.
|
|
72
75
|
|
|
73
|
-
##
|
|
76
|
+
## WSocket, the thinner layer
|
|
74
77
|
|
|
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.
|
|
78
|
+
Most apps want `WSDispatcher`. If you only want a thin wrapper, the package also exports `WSocket`.
|
|
82
79
|
|
|
83
|
-
|
|
80
|
+
- It smooths over differences between WebSocket implementations.
|
|
81
|
+
- `send()` returns a `SendState`: `OKAY`, `NOT_READY`, `BUFFERING`, or `SEND_ERROR`. It reports
|
|
82
|
+
`BUFFERING` instead of sending while the browser still has unsent data.
|
|
83
|
+
- It removes its listeners before closing, so a reconnect does not stack old handlers.
|
|
84
84
|
|
|
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.
|
|
85
|
+
```js
|
|
86
|
+
import { WSocket, SendState } from "@dojo-ng/websocket";
|
|
90
87
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
88
|
+
const ws = new WSocket("wss://example.com");
|
|
89
|
+
ws.addEventListener("open", () => console.log("open"));
|
|
90
|
+
ws.addEventListener("message", (e) => console.log(e.detail.data));
|
|
91
|
+
ws.open();
|
|
92
|
+
|
|
93
|
+
const state = ws.send("hello"); // SendState.OKAY, NOT_READY, BUFFERING, or SEND_ERROR
|
|
94
94
|
```
|
|
95
95
|
|
|
96
|
-
|
|
96
|
+
## Learn more
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
The [framework runtime guide](https://dojo-ng.com/docs/framework/) shows the client together with
|
|
99
|
+
[`@dojo-ng/framework`](https://www.npmjs.com/package/@dojo-ng/framework) and
|
|
100
|
+
[`@dojo-ng/pubsub`](https://www.npmjs.com/package/@dojo-ng/pubsub).
|
|
99
101
|
|
|
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` |
|
|
102
|
+
## Development
|
|
104
103
|
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
- `npm test` runs the unit tests with `node --test`, against a fake socket and fake timers. No
|
|
105
|
+
test uses the network.
|
|
106
|
+
- `npm run typecheck` runs `tsc --noEmit`.
|
|
107
|
+
- `npm run playground` starts a local echo server and a demo page at
|
|
108
|
+
`http://localhost:8901/packages/websocket/playground/index.html`. Its buttons show the offline
|
|
109
|
+
queue, a real reconnect with backoff, and the heartbeat check.
|
|
107
110
|
|
|
108
111
|
## License
|
|
109
112
|
|
|
110
|
-
BSD-3-Clause. See [LICENSE](
|
|
113
|
+
BSD-3-Clause. See [LICENSE](https://foss.heptapod.net/dojo-ng/framework/-/blob/branch/default/LICENSE).
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dojo-ng/websocket",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Dojo NG
|
|
3
|
+
"version": "0.1.3",
|
|
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",
|