@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.
Files changed (2) hide show
  1. package/README.md +78 -75
  2. package/package.json +3 -3
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,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
- ## `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.
18
+ ```js
19
+ import { createPubSub } from "@dojo-ng/pubsub";
20
+ import { WSDispatcher } from "@dojo-ng/websocket";
23
21
 
24
- ```ts
25
- import { WSocket, SendState } from "@dojo-ng/websocket";
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
- 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();
29
+ server.open();
31
30
 
32
- const state = ws.send("hello"); // SendState.OKAY | NOT_READY | BUFFERING | SEND_ERROR
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
- 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.
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
- ## `WSDispatcher`
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
- ```ts
43
- import { WSDispatcher } from "@dojo-ng/websocket";
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
- 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 } } }],
50
- });
51
-
52
- dispatcher.open();
59
+ ## The wire format
53
60
 
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] });
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
- // 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" } });
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
- 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.
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
- ## The wire format
76
+ ## WSocket, the thinner layer
74
77
 
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.
78
+ Most apps want `WSDispatcher`. If you only want a thin wrapper, the package also exports `WSocket`.
82
79
 
83
- ## Playground
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
- 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.
85
+ ```js
86
+ import { WSocket, SendState } from "@dojo-ng/websocket";
90
87
 
91
- ```bash
92
- npm run playground
93
- # → http://localhost:8901/packages/websocket/playground/index.html
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
- See the comment at the top of `playground/index.html` for exactly what each button demonstrates.
96
+ ## Learn more
97
97
 
98
- ## Testing
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
- | 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` |
102
+ ## Development
104
103
 
105
- No real network in any test — `test/helpers/fake-websocket.mjs` is a synchronous fake shared
106
- across the `WSocket` and `WSDispatcher` suites.
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](../../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.2",
4
- "description": "Dojo NG websocket client",
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.2"
15
+ "@dojo-ng/pubsub": "^0.1.3"
16
16
  },
17
17
  "devDependencies": {
18
18
  "ajv": "^8.20.0",