@marianmeres/ws 0.4.1 → 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.
@@ -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 declare const PROTOCOL_VERSION = 1;
20
+ export declare const PROTOCOL_VERSION = 2;
16
21
  /** Namespace used when the client does not specify one. */
17
22
  export declare const DEFAULT_NAMESPACE = "default";
18
23
  /**
@@ -21,34 +26,43 @@ export declare 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 declare const FRAME: {
26
36
  /** Handshake. Always the first frame; carries the auth payload. */
27
37
  readonly AUTH: "auth";
28
- /** Join one or more rooms, optionally with presence. */
29
- readonly SUB: "sub";
30
- /** Leave one or more rooms. */
31
- readonly UNSUB: "unsub";
32
- /** Publish into a room within the connection's own namespace. */
33
- readonly PUB: "pub";
34
- /** Publish into a room across every namespace. Gated server-side. */
35
- readonly BROADCAST: "broadcast";
36
38
  /** Liveness probe. Answered with `pong`. */
37
39
  readonly PING: "ping";
38
- /** Handshake accepted; carries the assigned id, namespace and version. */
40
+ /** Handshake accepted; carries the protocol version (and, with rooms, identity). */
39
41
  readonly HELLO: "hello";
40
- /** Positive acknowledgement of a correlated request. */
42
+ /** Positive acknowledgement of a frame that carried an `id`; may carry a reply. */
41
43
  readonly ACK: "ack";
42
44
  /** Negative acknowledgement; carries a `WSErrorInfo`. */
43
45
  readonly NACK: "nack";
44
- /** A message delivered to a subscribed room. */
45
- readonly MSG: "msg";
46
- /** A membership change in a presence-enabled room. */
47
- readonly PRESENCE: "presence";
48
46
  /** Reply to `ping`. */
49
47
  readonly PONG: "pong";
50
48
  /** Uncorrelated error — not tied to any request id. */
51
49
  readonly ERROR: "error";
50
+ /**
51
+ * A message. Client -> server it is addressed to the server itself;
52
+ * server -> client it is either a direct message (core) or a room delivery
53
+ * (rooms extension, recognisable by its `room` field).
54
+ */
55
+ readonly MSG: "msg";
56
+ /** Join one or more rooms, optionally with presence. */
57
+ readonly SUB: "sub";
58
+ /** Leave one or more rooms. */
59
+ readonly UNSUB: "unsub";
60
+ /** Publish into a room within the connection's own namespace. */
61
+ readonly PUB: "pub";
62
+ /** Publish into a room across every namespace. Gated server-side. */
63
+ readonly BROADCAST: "broadcast";
64
+ /** A membership change in a presence-enabled room. */
65
+ readonly PRESENCE: "presence";
52
66
  };
53
67
  /**
54
68
  * WebSocket close codes.
@@ -99,10 +113,15 @@ export declare const ERROR_CODE: {
99
113
  readonly BAD_REQUEST: "bad_request";
100
114
  /** Frame rate cap exceeded. */
101
115
  readonly RATE_LIMITED: "rate_limited";
116
+ /**
117
+ * A frame type this server does not implement — typically a rooms frame
118
+ * sent to a core-only server, or a `msg` to a server with no handler.
119
+ */
120
+ readonly UNSUPPORTED: "unsupported";
102
121
  /** Unexpected server-side failure. */
103
122
  readonly INTERNAL: "internal";
104
123
  };
105
- /** Presence event kinds. */
124
+ /** Presence event kinds (rooms extension). */
106
125
  export declare const PRESENCE: {
107
126
  /** Full membership snapshot, sent on every (re)subscribe. */
108
127
  readonly SYNC: "sync";
@@ -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 = 1;
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 assigned id, namespace and version. */
41
+ // core, server -> client
42
+ /** Handshake accepted; carries the protocol version (and, with rooms, identity). */
41
43
  HELLO: "hello",
42
- /** Positive acknowledgement of a correlated request. */
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
- /** `sendTimeout` elapsed while queued, in flight, or awaiting an ack. */
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.
@@ -80,7 +85,12 @@ export declare class WSOutboxDropError extends WSError {
80
85
  export declare class WSConnectionLostError extends WSError {
81
86
  constructor();
82
87
  }
83
- /** The server rejected the operation with a `nack`. */
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
+ */
84
94
  export declare class WSRemoteError extends WSError {
85
95
  /** Machine-readable code from the server — see `ERROR_CODE`. */
86
96
  readonly code: string;
@@ -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
- /** `sendTimeout` elapsed while queued, in flight, or awaiting an ack. */
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(`No acknowledgement within ${ms}ms`);
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. */
@@ -92,7 +97,12 @@ export class WSConnectionLostError extends WSError {
92
97
  super("The connection closed before the server acknowledged the frame; it was not resent");
93
98
  }
94
99
  }
95
- /** The server rejected the operation with a `nack`. */
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
+ */
96
106
  export class WSRemoteError extends WSError {
97
107
  /** Machine-readable code from the server — see `ERROR_CODE`. */
98
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
- id: string;
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**.
@@ -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.4.1",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "main": "dist/mod.js",
6
6
  "types": "dist/mod.d.ts",