@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
|
@@ -11,8 +11,13 @@
|
|
|
11
11
|
*
|
|
12
12
|
* It costs nothing today and it is the only thing that makes a breaking
|
|
13
13
|
* protocol change survivable later.
|
|
14
|
+
*
|
|
15
|
+
* Version 2 split the protocol into a required **core** (handshake, messages
|
|
16
|
+
* in both directions, heartbeat) and an optional **rooms extension**
|
|
17
|
+
* (subscriptions, publishing, presence, broadcast), so a server that only
|
|
18
|
+
* needs a data channel implements the core and nothing else.
|
|
14
19
|
*/
|
|
15
|
-
export const PROTOCOL_VERSION =
|
|
20
|
+
export const PROTOCOL_VERSION = 2;
|
|
16
21
|
/** Namespace used when the client does not specify one. */
|
|
17
22
|
export const DEFAULT_NAMESPACE = "default";
|
|
18
23
|
/**
|
|
@@ -21,36 +26,48 @@ export const DEFAULT_NAMESPACE = "default";
|
|
|
21
26
|
* Note these are *protocol* types and have nothing to do with whatever the
|
|
22
27
|
* application puts inside `payload` — the payload is opaque and is never
|
|
23
28
|
* inspected nor mutated by this library.
|
|
29
|
+
*
|
|
30
|
+
* Every server implements the **core** frames. The **rooms extension**
|
|
31
|
+
* frames are sent only when the application uses rooms (`subscribe()`,
|
|
32
|
+
* `publish()`, `broadcast()`, presence), so a server that does not support
|
|
33
|
+
* them never sees one.
|
|
24
34
|
*/
|
|
25
35
|
export const FRAME = {
|
|
26
|
-
// client -> server
|
|
36
|
+
// core, client -> server
|
|
27
37
|
/** Handshake. Always the first frame; carries the auth payload. */
|
|
28
38
|
AUTH: "auth",
|
|
29
|
-
/** Join one or more rooms, optionally with presence. */
|
|
30
|
-
SUB: "sub",
|
|
31
|
-
/** Leave one or more rooms. */
|
|
32
|
-
UNSUB: "unsub",
|
|
33
|
-
/** Publish into a room within the connection's own namespace. */
|
|
34
|
-
PUB: "pub",
|
|
35
|
-
/** Publish into a room across every namespace. Gated server-side. */
|
|
36
|
-
BROADCAST: "broadcast",
|
|
37
39
|
/** Liveness probe. Answered with `pong`. */
|
|
38
40
|
PING: "ping",
|
|
39
|
-
// server -> client
|
|
40
|
-
/** Handshake accepted; carries the
|
|
41
|
+
// core, server -> client
|
|
42
|
+
/** Handshake accepted; carries the protocol version (and, with rooms, identity). */
|
|
41
43
|
HELLO: "hello",
|
|
42
|
-
/** Positive acknowledgement of a
|
|
44
|
+
/** Positive acknowledgement of a frame that carried an `id`; may carry a reply. */
|
|
43
45
|
ACK: "ack",
|
|
44
46
|
/** Negative acknowledgement; carries a `WSErrorInfo`. */
|
|
45
47
|
NACK: "nack",
|
|
46
|
-
/** A message delivered to a subscribed room. */
|
|
47
|
-
MSG: "msg",
|
|
48
|
-
/** A membership change in a presence-enabled room. */
|
|
49
|
-
PRESENCE: "presence",
|
|
50
48
|
/** Reply to `ping`. */
|
|
51
49
|
PONG: "pong",
|
|
52
50
|
/** Uncorrelated error — not tied to any request id. */
|
|
53
51
|
ERROR: "error",
|
|
52
|
+
// core, both directions
|
|
53
|
+
/**
|
|
54
|
+
* A message. Client -> server it is addressed to the server itself;
|
|
55
|
+
* server -> client it is either a direct message (core) or a room delivery
|
|
56
|
+
* (rooms extension, recognisable by its `room` field).
|
|
57
|
+
*/
|
|
58
|
+
MSG: "msg",
|
|
59
|
+
// rooms extension, client -> server
|
|
60
|
+
/** Join one or more rooms, optionally with presence. */
|
|
61
|
+
SUB: "sub",
|
|
62
|
+
/** Leave one or more rooms. */
|
|
63
|
+
UNSUB: "unsub",
|
|
64
|
+
/** Publish into a room within the connection's own namespace. */
|
|
65
|
+
PUB: "pub",
|
|
66
|
+
/** Publish into a room across every namespace. Gated server-side. */
|
|
67
|
+
BROADCAST: "broadcast",
|
|
68
|
+
// rooms extension, server -> client
|
|
69
|
+
/** A membership change in a presence-enabled room. */
|
|
70
|
+
PRESENCE: "presence",
|
|
54
71
|
};
|
|
55
72
|
/**
|
|
56
73
|
* WebSocket close codes.
|
|
@@ -104,10 +121,15 @@ export const ERROR_CODE = {
|
|
|
104
121
|
BAD_REQUEST: "bad_request",
|
|
105
122
|
/** Frame rate cap exceeded. */
|
|
106
123
|
RATE_LIMITED: "rate_limited",
|
|
124
|
+
/**
|
|
125
|
+
* A frame type this server does not implement — typically a rooms frame
|
|
126
|
+
* sent to a core-only server, or a `msg` to a server with no handler.
|
|
127
|
+
*/
|
|
128
|
+
UNSUPPORTED: "unsupported",
|
|
107
129
|
/** Unexpected server-side failure. */
|
|
108
130
|
INTERNAL: "internal",
|
|
109
131
|
};
|
|
110
|
-
/** Presence event kinds. */
|
|
132
|
+
/** Presence event kinds (rooms extension). */
|
|
111
133
|
export const PRESENCE = {
|
|
112
134
|
/** Full membership snapshot, sent on every (re)subscribe. */
|
|
113
135
|
SYNC: "sync",
|
|
@@ -58,7 +58,12 @@ export declare class WSConnectTimeoutError extends WSError {
|
|
|
58
58
|
*/
|
|
59
59
|
constructor(ms: number);
|
|
60
60
|
}
|
|
61
|
-
/**
|
|
61
|
+
/**
|
|
62
|
+
* `sendTimeout` elapsed while queued, in flight, or awaiting an ack.
|
|
63
|
+
*
|
|
64
|
+
* A send that awaits no ack — `send()` without `{ ack: true }` — can only hit
|
|
65
|
+
* this while still queued, i.e. when no connection came up in time.
|
|
66
|
+
*/
|
|
62
67
|
export declare class WSTimeoutError extends WSError {
|
|
63
68
|
/**
|
|
64
69
|
* Reports the deadline that was exceeded.
|
|
@@ -71,7 +76,21 @@ export declare class WSTimeoutError extends WSError {
|
|
|
71
76
|
export declare class WSOutboxDropError extends WSError {
|
|
72
77
|
constructor();
|
|
73
78
|
}
|
|
74
|
-
/**
|
|
79
|
+
/**
|
|
80
|
+
* The socket closed while the frame was in flight.
|
|
81
|
+
*
|
|
82
|
+
* Distinct from {@link WSTimeoutError} on purpose: this one means "retry when
|
|
83
|
+
* connected", that one means "the server is not answering".
|
|
84
|
+
*/
|
|
85
|
+
export declare class WSConnectionLostError extends WSError {
|
|
86
|
+
constructor();
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The server rejected the operation with a `nack`.
|
|
90
|
+
*
|
|
91
|
+
* Also the way a server-side `onMessage` hook rejects a message on purpose:
|
|
92
|
+
* throw one, and its `code` and `message` travel back in the `nack` unchanged.
|
|
93
|
+
*/
|
|
75
94
|
export declare class WSRemoteError extends WSError {
|
|
76
95
|
/** Machine-readable code from the server — see `ERROR_CODE`. */
|
|
77
96
|
readonly code: string;
|
package/dist/protocol/errors.js
CHANGED
|
@@ -64,7 +64,12 @@ export class WSConnectTimeoutError extends WSError {
|
|
|
64
64
|
super(`Not connected within ${ms}ms (still retrying in background)`);
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
|
-
/**
|
|
67
|
+
/**
|
|
68
|
+
* `sendTimeout` elapsed while queued, in flight, or awaiting an ack.
|
|
69
|
+
*
|
|
70
|
+
* A send that awaits no ack — `send()` without `{ ack: true }` — can only hit
|
|
71
|
+
* this while still queued, i.e. when no connection came up in time.
|
|
72
|
+
*/
|
|
68
73
|
export class WSTimeoutError extends WSError {
|
|
69
74
|
/**
|
|
70
75
|
* Reports the deadline that was exceeded.
|
|
@@ -72,7 +77,7 @@ export class WSTimeoutError extends WSError {
|
|
|
72
77
|
* @param ms - the elapsed `sendTimeout`
|
|
73
78
|
*/
|
|
74
79
|
constructor(ms) {
|
|
75
|
-
super(`
|
|
80
|
+
super(`Send not completed within ${ms}ms`);
|
|
76
81
|
}
|
|
77
82
|
}
|
|
78
83
|
/** The frame was evicted from a full outbox before it could be sent. */
|
|
@@ -81,7 +86,23 @@ export class WSOutboxDropError extends WSError {
|
|
|
81
86
|
super("Dropped from outbox (capacity reached while disconnected)");
|
|
82
87
|
}
|
|
83
88
|
}
|
|
84
|
-
/**
|
|
89
|
+
/**
|
|
90
|
+
* The socket closed while the frame was in flight.
|
|
91
|
+
*
|
|
92
|
+
* Distinct from {@link WSTimeoutError} on purpose: this one means "retry when
|
|
93
|
+
* connected", that one means "the server is not answering".
|
|
94
|
+
*/
|
|
95
|
+
export class WSConnectionLostError extends WSError {
|
|
96
|
+
constructor() {
|
|
97
|
+
super("The connection closed before the server acknowledged the frame; it was not resent");
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* The server rejected the operation with a `nack`.
|
|
102
|
+
*
|
|
103
|
+
* Also the way a server-side `onMessage` hook rejects a message on purpose:
|
|
104
|
+
* throw one, and its `code` and `message` travel back in the `nack` unchanged.
|
|
105
|
+
*/
|
|
85
106
|
export class WSRemoteError extends WSError {
|
|
86
107
|
/** Machine-readable code from the server — see `ERROR_CODE`. */
|
|
87
108
|
code;
|
|
@@ -7,6 +7,11 @@
|
|
|
7
7
|
* application. Your payload may contain its own `type` field and nothing will
|
|
8
8
|
* collide.
|
|
9
9
|
*
|
|
10
|
+
* The frames come in two layers. The **core** — `auth`/`hello`, `msg` in both
|
|
11
|
+
* directions, `ack`/`nack`, `ping`/`pong`, `error` — is all a server must
|
|
12
|
+
* implement. The **rooms extension** — `sub`, `unsub`, `pub`, `broadcast`,
|
|
13
|
+
* `presence`, and the routing fields on a delivered `msg` — is opt-in.
|
|
14
|
+
*
|
|
10
15
|
* @module
|
|
11
16
|
*/
|
|
12
17
|
import type { FRAME, PRESENCE } from "./constants.js";
|
|
@@ -18,23 +23,45 @@ export interface WSErrorInfo {
|
|
|
18
23
|
message: string;
|
|
19
24
|
}
|
|
20
25
|
/**
|
|
21
|
-
* A message as delivered to application code
|
|
26
|
+
* A message as delivered to application code — the `message` event fires
|
|
27
|
+
* with one for every inbound `msg` frame.
|
|
22
28
|
*
|
|
23
29
|
* A `msg` frame minus its `type` field *is* a `WSMessage` — there is no
|
|
24
30
|
* translation layer and no divergence between wire names and API names.
|
|
31
|
+
*
|
|
32
|
+
* Only `payload` is guaranteed. A direct message from the server (core) has
|
|
33
|
+
* nothing else; a message delivered through a room (rooms extension) carries
|
|
34
|
+
* every routing field — see {@link WSRoomMessage}. `room` tells them apart.
|
|
25
35
|
*/
|
|
26
36
|
export interface WSMessage<T = unknown> {
|
|
37
|
+
/** Opaque application payload. */
|
|
38
|
+
payload: T;
|
|
39
|
+
/** Room the message was published to. Absent on a direct message. */
|
|
40
|
+
room?: string;
|
|
41
|
+
/** Namespace the message belongs to. Absent on a direct message. */
|
|
42
|
+
namespace?: string;
|
|
43
|
+
/**
|
|
44
|
+
* Originating client id, `null` when injected server-side. Absent on a
|
|
45
|
+
* direct message, which always comes from the server.
|
|
46
|
+
*/
|
|
47
|
+
from?: string | null;
|
|
48
|
+
/** Server-assigned epoch milliseconds. Absent on a direct message. */
|
|
49
|
+
timestamp?: number;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* A message delivered through a room (rooms extension). This is what room
|
|
53
|
+
* handlers receive: every routing field is present.
|
|
54
|
+
*/
|
|
55
|
+
export interface WSRoomMessage<T = unknown> extends WSMessage<T> {
|
|
27
56
|
/** Room the message was published to. */
|
|
28
57
|
room: string;
|
|
29
|
-
/** Namespace the message belongs to. */
|
|
58
|
+
/** Namespace the message belongs to — always the receiver's own. */
|
|
30
59
|
namespace: string;
|
|
31
60
|
/**
|
|
32
61
|
* Originating client id, or `null` when the message was injected
|
|
33
62
|
* server-side (via the service API or the HTTP routes).
|
|
34
63
|
*/
|
|
35
64
|
from: string | null;
|
|
36
|
-
/** Opaque application payload. */
|
|
37
|
-
payload: T;
|
|
38
65
|
/** Server-assigned epoch milliseconds. */
|
|
39
66
|
timestamp: number;
|
|
40
67
|
}
|
|
@@ -42,7 +69,7 @@ export interface WSMessage<T = unknown> {
|
|
|
42
69
|
export type PresenceEventType = (typeof PRESENCE)[keyof typeof PRESENCE];
|
|
43
70
|
/**
|
|
44
71
|
* A membership change in a room the client subscribed to with presence
|
|
45
|
-
* enabled.
|
|
72
|
+
* enabled (rooms extension).
|
|
46
73
|
*
|
|
47
74
|
* `sync` carries the full snapshot and is emitted on every (re)subscribe —
|
|
48
75
|
* crucially including after a reconnect, when membership may have changed
|
|
@@ -62,7 +89,7 @@ export interface WSPresenceEvent {
|
|
|
62
89
|
/** Server-assigned epoch milliseconds. */
|
|
63
90
|
timestamp: number;
|
|
64
91
|
}
|
|
65
|
-
/** A single room subscription request. */
|
|
92
|
+
/** A single room subscription request (rooms extension). */
|
|
66
93
|
export interface SubRequest {
|
|
67
94
|
/** Room name to join. */
|
|
68
95
|
room: string;
|
|
@@ -72,12 +99,24 @@ export interface SubRequest {
|
|
|
72
99
|
/** Frames sent by the client. */
|
|
73
100
|
export type ClientFrame = {
|
|
74
101
|
type: typeof FRAME.AUTH;
|
|
75
|
-
|
|
102
|
+
/**
|
|
103
|
+
* Not sent since protocol 2 and never acknowledged. Kept optional so a
|
|
104
|
+
* server keeps accepting a protocol-1 client that still sends it.
|
|
105
|
+
*/
|
|
106
|
+
id?: string;
|
|
76
107
|
protocol: number;
|
|
77
108
|
payload: unknown;
|
|
78
109
|
/** Preferred client id — the server may override it. */
|
|
79
110
|
clientId?: string;
|
|
111
|
+
/** Requested namespace — sent only when the application chose one. */
|
|
80
112
|
namespace?: string;
|
|
113
|
+
} | {
|
|
114
|
+
type: typeof FRAME.MSG;
|
|
115
|
+
/** Present only when the sender awaits an `ack`/`nack`. */
|
|
116
|
+
id?: string;
|
|
117
|
+
payload: unknown;
|
|
118
|
+
} | {
|
|
119
|
+
type: typeof FRAME.PING;
|
|
81
120
|
} | {
|
|
82
121
|
type: typeof FRAME.SUB;
|
|
83
122
|
id: string;
|
|
@@ -97,36 +136,39 @@ export type ClientFrame = {
|
|
|
97
136
|
id: string;
|
|
98
137
|
room: string;
|
|
99
138
|
payload: unknown;
|
|
100
|
-
} | {
|
|
101
|
-
type: typeof FRAME.PING;
|
|
102
139
|
};
|
|
103
140
|
/** Frames sent by the server. */
|
|
104
141
|
export type ServerFrame = {
|
|
105
142
|
type: typeof FRAME.HELLO;
|
|
106
|
-
clientId: string;
|
|
107
|
-
namespace: string;
|
|
108
143
|
protocol: number;
|
|
144
|
+
/** Assigned client id. Optional in the core; always sent with rooms. */
|
|
145
|
+
clientId?: string;
|
|
146
|
+
/** Assigned namespace. Optional in the core; always sent with rooms. */
|
|
147
|
+
namespace?: string;
|
|
109
148
|
} | {
|
|
110
149
|
type: typeof FRAME.ACK;
|
|
111
150
|
id: string;
|
|
151
|
+
/** Delivery count, on `pub`/`broadcast` acks only. */
|
|
112
152
|
recipients?: number;
|
|
153
|
+
/** The server's reply, on `msg` acks only. Any JSON value. */
|
|
154
|
+
payload?: unknown;
|
|
113
155
|
} | {
|
|
114
156
|
type: typeof FRAME.NACK;
|
|
115
157
|
id: string;
|
|
116
158
|
error: WSErrorInfo;
|
|
117
159
|
} | ({
|
|
118
160
|
type: typeof FRAME.MSG;
|
|
119
|
-
} & WSMessage) |
|
|
120
|
-
type: typeof FRAME.PRESENCE;
|
|
121
|
-
} & WSPresenceEvent) | {
|
|
161
|
+
} & WSMessage) | {
|
|
122
162
|
type: typeof FRAME.PONG;
|
|
123
163
|
} | {
|
|
124
164
|
type: typeof FRAME.ERROR;
|
|
125
165
|
error: WSErrorInfo;
|
|
126
|
-
}
|
|
166
|
+
} | ({
|
|
167
|
+
type: typeof FRAME.PRESENCE;
|
|
168
|
+
} & WSPresenceEvent);
|
|
127
169
|
/** Any frame, in either direction. */
|
|
128
170
|
export type WSFrame = ClientFrame | ServerFrame;
|
|
129
|
-
/** Result of a successful `publish()` / `broadcast()
|
|
171
|
+
/** Result of a successful `publish()` / `broadcast()` (rooms extension). */
|
|
130
172
|
export interface WSPublishResult {
|
|
131
173
|
/**
|
|
132
174
|
* Sockets the message was handed to **on the receiving server instance**.
|
|
@@ -136,6 +178,16 @@ export interface WSPublishResult {
|
|
|
136
178
|
*/
|
|
137
179
|
recipients: number;
|
|
138
180
|
}
|
|
181
|
+
/**
|
|
182
|
+
* What the client proposed in its `auth` frame. Hints, not facts — they are
|
|
183
|
+
* whatever the socket sent, so validate them before honouring them.
|
|
184
|
+
*/
|
|
185
|
+
export interface WSRequestedIdentity {
|
|
186
|
+
/** The claimed client id, when the frame carried a usable one. */
|
|
187
|
+
clientId?: string;
|
|
188
|
+
/** The requested namespace, or the default when the frame carried none. */
|
|
189
|
+
namespace: string;
|
|
190
|
+
}
|
|
139
191
|
/** Outcome of the server's `verify()` hook. */
|
|
140
192
|
export interface AuthResult {
|
|
141
193
|
/** Assign a specific client id. Defaults to a generated one. */
|
package/dist/protocol/frames.js
CHANGED
|
@@ -7,6 +7,11 @@
|
|
|
7
7
|
* application. Your payload may contain its own `type` field and nothing will
|
|
8
8
|
* collide.
|
|
9
9
|
*
|
|
10
|
+
* The frames come in two layers. The **core** — `auth`/`hello`, `msg` in both
|
|
11
|
+
* directions, `ack`/`nack`, `ping`/`pong`, `error` — is all a server must
|
|
12
|
+
* implement. The **rooms extension** — `sub`, `unsub`, `pub`, `broadcast`,
|
|
13
|
+
* `presence`, and the routing fields on a delivered `msg` — is opt-in.
|
|
14
|
+
*
|
|
10
15
|
* @module
|
|
11
16
|
*/
|
|
12
17
|
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marianmeres/ws",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/mod.js",
|
|
6
6
|
"types": "dist/mod.d.ts",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"@marianmeres/clog": "^3.21.0",
|
|
28
28
|
"@marianmeres/pubsub": "^3.0.0",
|
|
29
29
|
"@marianmeres/ticker": "^1.17.1",
|
|
30
|
-
"@marianmeres/uid": "^1.
|
|
30
|
+
"@marianmeres/uid": "^1.2.0"
|
|
31
31
|
},
|
|
32
32
|
"repository": {
|
|
33
33
|
"type": "git",
|