@marianmeres/ws 0.3.0 → 0.5.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/AGENTS.md +91 -16
- package/API.md +360 -90
- package/README.md +192 -71
- package/dist/client/outbox.d.ts +51 -11
- package/dist/client/outbox.js +56 -18
- package/dist/client/rooms.d.ts +5 -5
- package/dist/client/rooms.js +1 -1
- package/dist/client/ws-client.d.ts +142 -16
- package/dist/client/ws-client.js +170 -37
- package/dist/mod.d.ts +23 -4
- package/dist/mod.js +22 -3
- package/dist/protocol/constants.d.ts +35 -16
- package/dist/protocol/constants.js +40 -18
- package/dist/protocol/errors.d.ts +21 -2
- package/dist/protocol/errors.js +24 -3
- package/dist/protocol/frames.d.ts +68 -16
- package/dist/protocol/frames.js +5 -0
- package/package.json +2 -2
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The WebSocket client.
|
|
3
3
|
*
|
|
4
|
+
* Two ways to use it, freely mixed on one connection:
|
|
5
|
+
*
|
|
6
|
+
* - **Messages** (core): `send()` to the server, `on("message")` to receive.
|
|
7
|
+
* Needs nothing from the server beyond the core protocol.
|
|
8
|
+
* - **Rooms** (extension): `subscribe()` / `publish()` / `broadcast()` and
|
|
9
|
+
* presence, for servers that relay between clients.
|
|
10
|
+
*
|
|
4
11
|
* @module
|
|
5
12
|
*/
|
|
6
13
|
import { type Logger } from "@marianmeres/clog";
|
|
@@ -18,16 +25,26 @@ export type WSConnectionState = "idle" | "connecting" | "authenticating" | "open
|
|
|
18
25
|
export interface WSEvents {
|
|
19
26
|
/** Socket opened; authentication has not happened yet. */
|
|
20
27
|
open: void;
|
|
21
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* Authenticated and ready. `clientId` is `null` when the server assigned
|
|
30
|
+
* none — identity belongs to the rooms extension, and a core-only server
|
|
31
|
+
* may skip it.
|
|
32
|
+
*/
|
|
22
33
|
connected: {
|
|
23
|
-
clientId: string;
|
|
34
|
+
clientId: string | null;
|
|
24
35
|
namespace: string;
|
|
25
36
|
};
|
|
26
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* Every inbound message: direct messages from the server, and — with
|
|
39
|
+
* rooms — every room delivery regardless of room. `room` tells them apart.
|
|
40
|
+
*/
|
|
27
41
|
message: WSMessage;
|
|
28
42
|
/** Membership change in a room subscribed with presence enabled. */
|
|
29
43
|
presence: WSPresenceEvent;
|
|
30
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* Socket closed. `willReconnect` reflects the retry classification, and is
|
|
46
|
+
* `false` for the `4900` a local `disconnect()` emits.
|
|
47
|
+
*/
|
|
31
48
|
close: {
|
|
32
49
|
code: number;
|
|
33
50
|
reason: string;
|
|
@@ -59,6 +76,16 @@ export interface WSState {
|
|
|
59
76
|
/** Most recent error, retained until the next successful connect. */
|
|
60
77
|
lastError: Error | null;
|
|
61
78
|
}
|
|
79
|
+
/** Options for {@link WSClient.send}. */
|
|
80
|
+
export interface WSSendOptions {
|
|
81
|
+
/**
|
|
82
|
+
* Wait for the server's acknowledgement and resolve with its reply.
|
|
83
|
+
*
|
|
84
|
+
* Default `false`: resolve as soon as the frame is written to the socket,
|
|
85
|
+
* with no confirmation that the server ever received it.
|
|
86
|
+
*/
|
|
87
|
+
ack?: boolean;
|
|
88
|
+
}
|
|
62
89
|
/** Per-room subscription options. */
|
|
63
90
|
export interface SubscribeOptions {
|
|
64
91
|
/**
|
|
@@ -77,11 +104,14 @@ export interface WSClientOptions<TAuth = unknown> {
|
|
|
77
104
|
* a path resolved against `location` in the browser. Default `/ws`.
|
|
78
105
|
*/
|
|
79
106
|
url?: string | URL;
|
|
80
|
-
/**
|
|
107
|
+
/**
|
|
108
|
+
* Isolation boundary for rooms. Default `"default"`. Sent to the server only
|
|
109
|
+
* when set here — a server without rooms has no use for it.
|
|
110
|
+
*/
|
|
81
111
|
namespace?: string;
|
|
82
|
-
/** Preferred client id; the server may override it. */
|
|
112
|
+
/** Preferred client id; the server may override or ignore it. */
|
|
83
113
|
clientId?: string;
|
|
84
|
-
/** Rooms joined automatically on every (re)connect. */
|
|
114
|
+
/** Rooms joined automatically on every (re)connect (rooms extension). */
|
|
85
115
|
rooms?: string[];
|
|
86
116
|
/**
|
|
87
117
|
* Produces the auth payload. Called before *every* (re)connect, so
|
|
@@ -89,7 +119,7 @@ export interface WSClientOptions<TAuth = unknown> {
|
|
|
89
119
|
*/
|
|
90
120
|
auth?: () => TAuth | Promise<TAuth>;
|
|
91
121
|
/**
|
|
92
|
-
* Let the first `subscribe()`/`publish()` start the connection.
|
|
122
|
+
* Let the first `send()`/`subscribe()`/`publish()` start the connection.
|
|
93
123
|
* Default `true` — with an outbox and infinite retry, requiring an explicit
|
|
94
124
|
* `connect()` first is ceremony whose only product is an error for people
|
|
95
125
|
* who forgot.
|
|
@@ -124,9 +154,18 @@ export interface WSClientOptions<TAuth = unknown> {
|
|
|
124
154
|
decode?: WSDecoder;
|
|
125
155
|
}
|
|
126
156
|
/**
|
|
127
|
-
* A reconnecting WebSocket client
|
|
157
|
+
* A reconnecting WebSocket client: plain messages to and from the server, and
|
|
158
|
+
* optionally namespaces, rooms and presence on top.
|
|
128
159
|
*
|
|
129
|
-
* @example
|
|
160
|
+
* @example Messages — the server is the peer
|
|
161
|
+
* ```ts
|
|
162
|
+
* const ws = createWSClient({ url: "/ws", auth: () => session.token });
|
|
163
|
+
* ws.on("message", (msg) => console.log(msg.payload));
|
|
164
|
+
* await ws.send({ op: "typing" }); // fire-and-forget
|
|
165
|
+
* const doc = await ws.send({ op: "load", id: 42 }, { ack: true }); // reply
|
|
166
|
+
* ```
|
|
167
|
+
*
|
|
168
|
+
* @example Rooms — the server relays between clients
|
|
130
169
|
* ```ts
|
|
131
170
|
* const ws = createWSClient({ url: "/ws", namespace: "org-123" });
|
|
132
171
|
* const unsub = await ws.subscribe("chat", (msg) => console.log(msg.payload));
|
|
@@ -139,7 +178,7 @@ export declare class WSClient<TAuth = unknown> {
|
|
|
139
178
|
logger: Logger | null;
|
|
140
179
|
/**
|
|
141
180
|
* Nothing connects here — the socket opens on the first `connect()`,
|
|
142
|
-
* `subscribe()` or `publish()`.
|
|
181
|
+
* `send()`, `subscribe()` or `publish()`.
|
|
143
182
|
*
|
|
144
183
|
* @param options - see {@link WSClientOptions}; every field has a default
|
|
145
184
|
*/
|
|
@@ -158,9 +197,15 @@ export declare class WSClient<TAuth = unknown> {
|
|
|
158
197
|
get connected(): boolean;
|
|
159
198
|
/** Current lifecycle state. See {@link WSConnectionState}. */
|
|
160
199
|
get connectionState(): WSConnectionState;
|
|
161
|
-
/**
|
|
200
|
+
/**
|
|
201
|
+
* Server-assigned id, available once connected. Stays `null` when the
|
|
202
|
+
* server assigns none (a core-only server may not).
|
|
203
|
+
*/
|
|
162
204
|
get clientId(): string | null;
|
|
163
|
-
/**
|
|
205
|
+
/**
|
|
206
|
+
* Active namespace — the server's assignment wins over the request. Falls
|
|
207
|
+
* back to the requested one when the server assigns none.
|
|
208
|
+
*/
|
|
164
209
|
get namespace(): string;
|
|
165
210
|
/** Resolved endpoint. A copy — mutating it does not affect the client. */
|
|
166
211
|
get url(): URL;
|
|
@@ -229,6 +274,10 @@ export declare class WSClient<TAuth = unknown> {
|
|
|
229
274
|
* Resumable: handlers, room subscriptions and buffered sends all survive,
|
|
230
275
|
* so a later `connect()` picks up exactly where this left off. Use
|
|
231
276
|
* {@link dispose} for terminal teardown.
|
|
277
|
+
*
|
|
278
|
+
* Emits `close` with {@link CLOSE.CLIENT_GONE} and `willReconnect: false`
|
|
279
|
+
* when there was a socket to close; nothing when already idle, reconnecting
|
|
280
|
+
* or terminated.
|
|
232
281
|
*/
|
|
233
282
|
disconnect(): void;
|
|
234
283
|
/**
|
|
@@ -257,7 +306,8 @@ export declare class WSClient<TAuth = unknown> {
|
|
|
257
306
|
* @param options - pass `presence` to enable membership tracking
|
|
258
307
|
* @returns detaches this handler; also `Symbol.dispose`-compatible, and
|
|
259
308
|
* idempotent, so calling it twice is harmless
|
|
260
|
-
* @throws {WSRemoteError} when connected and the server refuses
|
|
309
|
+
* @throws {WSRemoteError} when connected and the server refuses — with code
|
|
310
|
+
* `unsupported` when it does not implement rooms at all
|
|
261
311
|
* @throws {WSDisposedError} when the client was disposed
|
|
262
312
|
*
|
|
263
313
|
* @example
|
|
@@ -293,6 +343,73 @@ export declare class WSClient<TAuth = unknown> {
|
|
|
293
343
|
* @returns a copy of the members; empty when the room has no presence
|
|
294
344
|
*/
|
|
295
345
|
members(room: string): string[];
|
|
346
|
+
/**
|
|
347
|
+
* Sends a message to the server — the room-free way to talk to it, and all
|
|
348
|
+
* a core-only server has to understand.
|
|
349
|
+
*
|
|
350
|
+
* Fire-and-forget: resolves as soon as the frame is written to the socket.
|
|
351
|
+
* While disconnected it is buffered and resolves when flushed after the
|
|
352
|
+
* next connect — bounded by `sendTimeout`, never indefinitely. There is no
|
|
353
|
+
* delivery confirmation: a frame written into a connection that turns out
|
|
354
|
+
* to be dead is lost, exactly as with a plain `WebSocket`. Pass
|
|
355
|
+
* `{ ack: true }` when that matters.
|
|
356
|
+
*
|
|
357
|
+
* Safe to call without awaiting: the returned promise is marked handled,
|
|
358
|
+
* so a failure nobody awaits is not an unhandled rejection. Await it to
|
|
359
|
+
* learn about one.
|
|
360
|
+
*
|
|
361
|
+
* @param payload - opaque application data; never inspected or mutated
|
|
362
|
+
* @param options - see {@link WSSendOptions}
|
|
363
|
+
* @returns resolves once the frame is written to the socket
|
|
364
|
+
* @throws {WSTimeoutError} still buffered when `sendTimeout` elapsed
|
|
365
|
+
* @throws {WSOutboxDropError} evicted from a full outbox
|
|
366
|
+
* @throws {WSNotConnectedError} sent while offline with `outboxMaxSize: 0`
|
|
367
|
+
* @throws {WSTerminatedError} sent after a terminal close
|
|
368
|
+
*
|
|
369
|
+
* @example
|
|
370
|
+
* ```ts
|
|
371
|
+
* ws.send({ op: "cursor", x: 10, y: 20 });
|
|
372
|
+
* ```
|
|
373
|
+
*/
|
|
374
|
+
send<T = unknown>(payload: T, options?: WSSendOptions & {
|
|
375
|
+
ack?: false;
|
|
376
|
+
}): Promise<void>;
|
|
377
|
+
/**
|
|
378
|
+
* Sends a message to the server and waits for its acknowledgement.
|
|
379
|
+
*
|
|
380
|
+
* Resolves with the reply the server put in the ack — which makes this a
|
|
381
|
+
* request/response call — or with `undefined` when it sent a bare ack.
|
|
382
|
+
* Buffered while disconnected like any other send, and one `sendTimeout`
|
|
383
|
+
* spans queue, flight and ack.
|
|
384
|
+
*
|
|
385
|
+
* @param payload - opaque application data; never inspected or mutated
|
|
386
|
+
* @param options - `{ ack: true }`
|
|
387
|
+
* @returns the server's reply, `undefined` when there was none
|
|
388
|
+
* @throws {WSRemoteError} the server rejected it with a `nack` — code
|
|
389
|
+
* `unsupported` when it accepts no messages at all
|
|
390
|
+
* @throws {WSTimeoutError} `sendTimeout` elapsed with no acknowledgement
|
|
391
|
+
* @throws {WSConnectionLostError} the socket closed before the ack arrived;
|
|
392
|
+
* the message was not resent
|
|
393
|
+
* @throws {WSOutboxDropError} evicted from a full outbox
|
|
394
|
+
* @throws {WSNotConnectedError} sent while offline with `outboxMaxSize: 0`
|
|
395
|
+
* @throws {WSTerminatedError} sent after a terminal close
|
|
396
|
+
*
|
|
397
|
+
* @example
|
|
398
|
+
* ```ts
|
|
399
|
+
* const doc = await ws.send<Doc>({ op: "load", id: 42 }, { ack: true });
|
|
400
|
+
* ```
|
|
401
|
+
*/
|
|
402
|
+
send<R = unknown, T = unknown>(payload: T, options: WSSendOptions & {
|
|
403
|
+
ack: true;
|
|
404
|
+
}): Promise<R>;
|
|
405
|
+
/**
|
|
406
|
+
* Either of the above, decided at runtime by `options.ack`.
|
|
407
|
+
*
|
|
408
|
+
* @param payload - opaque application data; never inspected or mutated
|
|
409
|
+
* @param options - see {@link WSSendOptions}
|
|
410
|
+
* @returns `undefined` without an ack, the server's reply with one
|
|
411
|
+
*/
|
|
412
|
+
send<T = unknown>(payload: T, options?: WSSendOptions): Promise<unknown>;
|
|
296
413
|
/**
|
|
297
414
|
* Publishes to a room within this client's namespace.
|
|
298
415
|
*
|
|
@@ -309,6 +426,8 @@ export declare class WSClient<TAuth = unknown> {
|
|
|
309
426
|
* @throws {WSTimeoutError} `sendTimeout` elapsed with no acknowledgement
|
|
310
427
|
* @throws {WSOutboxDropError} evicted from a full outbox
|
|
311
428
|
* @throws {WSNotConnectedError} sent while offline with `outboxMaxSize: 0`
|
|
429
|
+
* @throws {WSTerminatedError} sent after a terminal close, which only an
|
|
430
|
+
* explicit `connect()` recovers from — rejected at once, not buffered
|
|
312
431
|
* @throws {WSRemoteError} the server rejected it with a `nack`
|
|
313
432
|
*/
|
|
314
433
|
publish<T = unknown>(room: string, payload: T, namespace?: string): Promise<WSPublishResult>;
|
|
@@ -336,14 +455,21 @@ export declare class WSClient<TAuth = unknown> {
|
|
|
336
455
|
* @param options - see {@link WSClientOptions}
|
|
337
456
|
* @returns a client that has not connected yet
|
|
338
457
|
*
|
|
339
|
-
* @example
|
|
458
|
+
* @example Messages
|
|
340
459
|
* ```ts
|
|
341
460
|
* const ws = createWSClient({
|
|
342
461
|
* url: "wss://example.com/ws",
|
|
343
|
-
* namespace: "org-123",
|
|
344
462
|
* auth: () => session.token, // re-read on every reconnect
|
|
345
463
|
* });
|
|
346
464
|
*
|
|
465
|
+
* ws.on("message", (msg) => console.log(msg.payload));
|
|
466
|
+
* const reply = await ws.send({ op: "ping" }, { ack: true });
|
|
467
|
+
* ```
|
|
468
|
+
*
|
|
469
|
+
* @example Rooms
|
|
470
|
+
* ```ts
|
|
471
|
+
* const ws = createWSClient({ url: "wss://example.com/ws", namespace: "org-123" });
|
|
472
|
+
*
|
|
347
473
|
* await ws.subscribe("chat", (msg) => console.log(msg.from, msg.payload));
|
|
348
474
|
* await ws.publish("chat", { text: "hello" });
|
|
349
475
|
* ```
|