@marianmeres/ws 0.2.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 +161 -0
- package/API.md +672 -0
- package/LICENSE +21 -0
- package/README.md +211 -0
- package/dist/client/backoff.d.ts +25 -0
- package/dist/client/backoff.js +31 -0
- package/dist/client/heartbeat.d.ts +38 -0
- package/dist/client/heartbeat.js +78 -0
- package/dist/client/outbox.d.ts +65 -0
- package/dist/client/outbox.js +155 -0
- package/dist/client/rooms.d.ts +62 -0
- package/dist/client/rooms.js +120 -0
- package/dist/client/ws-client.d.ts +351 -0
- package/dist/client/ws-client.js +892 -0
- package/dist/mod.d.ts +30 -0
- package/dist/mod.js +29 -0
- package/dist/protocol/constants.d.ts +113 -0
- package/dist/protocol/constants.js +118 -0
- package/dist/protocol/errors.d.ts +92 -0
- package/dist/protocol/errors.js +109 -0
- package/dist/protocol/frames.d.ts +151 -0
- package/dist/protocol/frames.js +12 -0
- package/dist/protocol/mod.d.ts +10 -0
- package/dist/protocol/mod.js +10 -0
- package/dist/protocol.d.ts +9 -0
- package/dist/protocol.js +9 -0
- package/package.json +39 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Refcounted room registry.
|
|
3
|
+
*
|
|
4
|
+
* N handlers on one room produce exactly one wire subscription; the `unsub`
|
|
5
|
+
* frame goes out when the last handler detaches. This is what lets
|
|
6
|
+
* `subscribe()` hand back a plain unsubscriber that is safe to call from
|
|
7
|
+
* component teardown without any coordination between call sites.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
/** Tracks rooms, their handlers, and their known membership. */
|
|
12
|
+
export class RoomRegistry {
|
|
13
|
+
#rooms = new Map();
|
|
14
|
+
/** Rooms currently held, in insertion order. */
|
|
15
|
+
get rooms() {
|
|
16
|
+
return [...this.#rooms.keys()];
|
|
17
|
+
}
|
|
18
|
+
has(room) {
|
|
19
|
+
return this.#rooms.has(room);
|
|
20
|
+
}
|
|
21
|
+
wantsPresence(room) {
|
|
22
|
+
const entry = this.#rooms.get(room);
|
|
23
|
+
return !!entry && entry.presenceHandlers.size > 0;
|
|
24
|
+
}
|
|
25
|
+
members(room) {
|
|
26
|
+
return [...(this.#rooms.get(room)?.members ?? [])];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Attaches handlers, creating the room entry if needed.
|
|
30
|
+
*
|
|
31
|
+
* Presence is enabled by *providing a presence handler* rather than by a
|
|
32
|
+
* separate boolean — one way to express the intent instead of two that can
|
|
33
|
+
* disagree.
|
|
34
|
+
*/
|
|
35
|
+
add(room, handler, presenceHandler) {
|
|
36
|
+
let entry = this.#rooms.get(room);
|
|
37
|
+
const created = !entry;
|
|
38
|
+
const hadPresence = !!entry && entry.presenceHandlers.size > 0;
|
|
39
|
+
if (!entry) {
|
|
40
|
+
entry = { handlers: new Set(), presenceHandlers: new Set(), members: [] };
|
|
41
|
+
this.#rooms.set(room, entry);
|
|
42
|
+
}
|
|
43
|
+
entry.handlers.add(handler);
|
|
44
|
+
if (presenceHandler)
|
|
45
|
+
entry.presenceHandlers.add(presenceHandler);
|
|
46
|
+
return {
|
|
47
|
+
created,
|
|
48
|
+
presenceUpgraded: !created && !hadPresence && !!presenceHandler,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Detaches handlers.
|
|
53
|
+
*
|
|
54
|
+
* @returns `true` when the room became empty and should be unsubscribed.
|
|
55
|
+
*/
|
|
56
|
+
remove(room, handler, presenceHandler) {
|
|
57
|
+
const entry = this.#rooms.get(room);
|
|
58
|
+
if (!entry)
|
|
59
|
+
return false;
|
|
60
|
+
entry.handlers.delete(handler);
|
|
61
|
+
if (presenceHandler)
|
|
62
|
+
entry.presenceHandlers.delete(presenceHandler);
|
|
63
|
+
if (entry.handlers.size === 0 && entry.presenceHandlers.size === 0) {
|
|
64
|
+
this.#rooms.delete(room);
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
/** Force-removes a room and every handler attached to it. */
|
|
70
|
+
removeRoom(room) {
|
|
71
|
+
return this.#rooms.delete(room);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The full subscription set, for re-subscribing after a reconnect in a
|
|
75
|
+
* single batched frame rather than one round-trip per room.
|
|
76
|
+
*/
|
|
77
|
+
subRequests() {
|
|
78
|
+
return [...this.#rooms.entries()].map(([room, entry]) => ({
|
|
79
|
+
room,
|
|
80
|
+
...(entry.presenceHandlers.size > 0 ? { presence: true } : {}),
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Delivers to every handler for the room.
|
|
85
|
+
*
|
|
86
|
+
* Handlers are snapshotted first, so a handler that unsubscribes (or
|
|
87
|
+
* subscribes) during delivery cannot corrupt the in-flight iteration.
|
|
88
|
+
*/
|
|
89
|
+
deliver(room, msg, onError) {
|
|
90
|
+
const entry = this.#rooms.get(room);
|
|
91
|
+
if (!entry)
|
|
92
|
+
return;
|
|
93
|
+
for (const handler of [...entry.handlers]) {
|
|
94
|
+
try {
|
|
95
|
+
handler(msg);
|
|
96
|
+
}
|
|
97
|
+
catch (e) {
|
|
98
|
+
onError(e);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/** Updates cached membership, then delivers to presence handlers. */
|
|
103
|
+
deliverPresence(room, event, onError) {
|
|
104
|
+
const entry = this.#rooms.get(room);
|
|
105
|
+
if (!entry)
|
|
106
|
+
return;
|
|
107
|
+
entry.members = event.members;
|
|
108
|
+
for (const handler of [...entry.presenceHandlers]) {
|
|
109
|
+
try {
|
|
110
|
+
handler(event);
|
|
111
|
+
}
|
|
112
|
+
catch (e) {
|
|
113
|
+
onError(e);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
clear() {
|
|
118
|
+
this.#rooms.clear();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The WebSocket client.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import { type Logger } from "@marianmeres/clog";
|
|
7
|
+
import { type Subscriber, type Unsubscriber } from "@marianmeres/pubsub";
|
|
8
|
+
import type { ClientFrame, WSDecoder, WSEncoder, WSMessage, WSPresenceEvent, WSPublishResult } from "../protocol/frames.js";
|
|
9
|
+
import { type MessageHandler, type PresenceHandler } from "./rooms.js";
|
|
10
|
+
/**
|
|
11
|
+
* Connection lifecycle states.
|
|
12
|
+
*
|
|
13
|
+
* A plain union rather than a state-machine dependency — the transition table
|
|
14
|
+
* below is the whole of the machinery, and it is small enough to read.
|
|
15
|
+
*/
|
|
16
|
+
export type WSConnectionState = "idle" | "connecting" | "authenticating" | "open" | "reconnecting" | "terminated" | "disposed";
|
|
17
|
+
/** Events emitted by {@link WSClient}. */
|
|
18
|
+
export interface WSEvents {
|
|
19
|
+
/** Socket opened; authentication has not happened yet. */
|
|
20
|
+
open: void;
|
|
21
|
+
/** Authenticated and ready. */
|
|
22
|
+
connected: {
|
|
23
|
+
clientId: string;
|
|
24
|
+
namespace: string;
|
|
25
|
+
};
|
|
26
|
+
/** Firehose — every message, regardless of room. */
|
|
27
|
+
message: WSMessage;
|
|
28
|
+
/** Membership change in a room subscribed with presence enabled. */
|
|
29
|
+
presence: WSPresenceEvent;
|
|
30
|
+
/** Socket closed. `willReconnect` reflects the retry classification. */
|
|
31
|
+
close: {
|
|
32
|
+
code: number;
|
|
33
|
+
reason: string;
|
|
34
|
+
willReconnect: boolean;
|
|
35
|
+
};
|
|
36
|
+
/** A retry is scheduled; `delay` is the jittered backoff in ms. */
|
|
37
|
+
reconnecting: {
|
|
38
|
+
attempt: number;
|
|
39
|
+
delay: number;
|
|
40
|
+
};
|
|
41
|
+
/** Gave up — the only non-retrying exit. */
|
|
42
|
+
terminated: {
|
|
43
|
+
code: number;
|
|
44
|
+
reason: string;
|
|
45
|
+
};
|
|
46
|
+
/** Something failed but the client carries on: decode, a throwing handler, … */
|
|
47
|
+
error: Error;
|
|
48
|
+
}
|
|
49
|
+
/** Reactive connection state, delivered through the Svelte store contract. */
|
|
50
|
+
export interface WSState {
|
|
51
|
+
/** Current lifecycle state. */
|
|
52
|
+
state: WSConnectionState;
|
|
53
|
+
/** `true` only in the `open` state — authenticated and usable. */
|
|
54
|
+
connected: boolean;
|
|
55
|
+
/** `true` while connecting, authenticating or waiting out a backoff. */
|
|
56
|
+
connecting: boolean;
|
|
57
|
+
/** Consecutive failed connection attempts; resets to 0 on success. */
|
|
58
|
+
attempt: number;
|
|
59
|
+
/** Most recent error, retained until the next successful connect. */
|
|
60
|
+
lastError: Error | null;
|
|
61
|
+
}
|
|
62
|
+
/** Per-room subscription options. */
|
|
63
|
+
export interface SubscribeOptions {
|
|
64
|
+
/**
|
|
65
|
+
* Enables presence tracking for this room.
|
|
66
|
+
*
|
|
67
|
+
* Presence is opt-in per room rather than global: a 10k-subscriber
|
|
68
|
+
* notification room does not want 10k join events every time the fleet
|
|
69
|
+
* reconnects.
|
|
70
|
+
*/
|
|
71
|
+
presence?: PresenceHandler;
|
|
72
|
+
}
|
|
73
|
+
/** Configuration for {@link WSClient}. */
|
|
74
|
+
export interface WSClientOptions<TAuth = unknown> {
|
|
75
|
+
/**
|
|
76
|
+
* Endpoint. `ws://`/`wss://`, or `http(s)://` (upgraded automatically), or
|
|
77
|
+
* a path resolved against `location` in the browser. Default `/ws`.
|
|
78
|
+
*/
|
|
79
|
+
url?: string | URL;
|
|
80
|
+
/** Isolation boundary. Default `"default"`. */
|
|
81
|
+
namespace?: string;
|
|
82
|
+
/** Preferred client id; the server may override it. */
|
|
83
|
+
clientId?: string;
|
|
84
|
+
/** Rooms joined automatically on every (re)connect. */
|
|
85
|
+
rooms?: string[];
|
|
86
|
+
/**
|
|
87
|
+
* Produces the auth payload. Called before *every* (re)connect, so
|
|
88
|
+
* returning a fresh token here is all that token refresh requires.
|
|
89
|
+
*/
|
|
90
|
+
auth?: () => TAuth | Promise<TAuth>;
|
|
91
|
+
/**
|
|
92
|
+
* Let the first `subscribe()`/`publish()` start the connection.
|
|
93
|
+
* Default `true` — with an outbox and infinite retry, requiring an explicit
|
|
94
|
+
* `connect()` first is ceremony whose only product is an error for people
|
|
95
|
+
* who forgot.
|
|
96
|
+
*/
|
|
97
|
+
autoConnect?: boolean;
|
|
98
|
+
/** `null` disables logging. */
|
|
99
|
+
logger?: Logger | null;
|
|
100
|
+
/** Initial reconnect delay in ms. Default 500. */
|
|
101
|
+
reconnectDelay?: number;
|
|
102
|
+
/** Reconnect delay ceiling in ms. Default 30_000. */
|
|
103
|
+
reconnectDelayMax?: number;
|
|
104
|
+
/** Close codes after which retrying stops. Default `[4001, 4003]`. */
|
|
105
|
+
terminalCloseCodes?: number[];
|
|
106
|
+
/** Ping cadence in ms. `0` disables. Default 25_000. */
|
|
107
|
+
pingInterval?: number;
|
|
108
|
+
/**
|
|
109
|
+
* Liveness deadline in ms — bounds both the pong reply and the initial
|
|
110
|
+
* auth handshake. Default 10_000.
|
|
111
|
+
*/
|
|
112
|
+
pongTimeout?: number;
|
|
113
|
+
/** Bound for the *first* `connect()` await. `0` waits indefinitely. */
|
|
114
|
+
connectTimeout?: number;
|
|
115
|
+
/** Per-send deadline covering queue + flight + ack. Default 30_000. */
|
|
116
|
+
sendTimeout?: number;
|
|
117
|
+
/** Frames buffered while disconnected. `0` disables buffering. Default 100. */
|
|
118
|
+
outboxMaxSize?: number;
|
|
119
|
+
/** Called with frames evicted from a full outbox. */
|
|
120
|
+
onOutboxDrop?: (frames: ClientFrame[]) => void;
|
|
121
|
+
/** Custom wire encoder. Must match the server's. */
|
|
122
|
+
encode?: WSEncoder;
|
|
123
|
+
/** Custom wire decoder. Must match the server's. */
|
|
124
|
+
decode?: WSDecoder;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* A reconnecting WebSocket client with namespaces, rooms and presence.
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* ```ts
|
|
131
|
+
* const ws = createWSClient({ url: "/ws", namespace: "org-123" });
|
|
132
|
+
* const unsub = await ws.subscribe("chat", (msg) => console.log(msg.payload));
|
|
133
|
+
* await ws.publish("chat", { text: "hi" });
|
|
134
|
+
* ```
|
|
135
|
+
*/
|
|
136
|
+
export declare class WSClient<TAuth = unknown> {
|
|
137
|
+
#private;
|
|
138
|
+
/** Logger. Assignable — set to `null` to silence. */
|
|
139
|
+
logger: Logger | null;
|
|
140
|
+
/**
|
|
141
|
+
* Nothing connects here — the socket opens on the first `connect()`,
|
|
142
|
+
* `subscribe()` or `publish()`.
|
|
143
|
+
*
|
|
144
|
+
* @param options - see {@link WSClientOptions}; every field has a default
|
|
145
|
+
*/
|
|
146
|
+
constructor(options?: WSClientOptions<TAuth>);
|
|
147
|
+
/**
|
|
148
|
+
* Normalizes an endpoint: relative paths resolve against `location`, and
|
|
149
|
+
* `http(s)` is upgraded to `ws(s)`.
|
|
150
|
+
*
|
|
151
|
+
* @param input - absolute url, or a path when running in a browser
|
|
152
|
+
* @returns the normalized `ws(s)://` url
|
|
153
|
+
* @throws {WSError} when the input cannot be resolved — outside a browser
|
|
154
|
+
* there is no `location` to resolve a relative path against
|
|
155
|
+
*/
|
|
156
|
+
static resolveUrl(input: string | URL): URL;
|
|
157
|
+
/** `true` only when authenticated and usable — not merely socket-open. */
|
|
158
|
+
get connected(): boolean;
|
|
159
|
+
/** Current lifecycle state. See {@link WSConnectionState}. */
|
|
160
|
+
get connectionState(): WSConnectionState;
|
|
161
|
+
/** Server-assigned id, available once connected. */
|
|
162
|
+
get clientId(): string | null;
|
|
163
|
+
/** Active namespace — the server's assignment wins over the request. */
|
|
164
|
+
get namespace(): string;
|
|
165
|
+
/** Resolved endpoint. A copy — mutating it does not affect the client. */
|
|
166
|
+
get url(): URL;
|
|
167
|
+
/**
|
|
168
|
+
* The underlying socket, or `null` while disconnected.
|
|
169
|
+
*
|
|
170
|
+
* Escape hatch for inspection. Sending on it directly bypasses the outbox
|
|
171
|
+
* and the ack correlation, so don't.
|
|
172
|
+
*/
|
|
173
|
+
get socket(): WebSocket | null;
|
|
174
|
+
/** Rooms currently subscribed. */
|
|
175
|
+
get rooms(): string[];
|
|
176
|
+
/**
|
|
177
|
+
* Reactive state, Svelte-store compatible: the callback fires immediately
|
|
178
|
+
* with the current value and again on every change.
|
|
179
|
+
*/
|
|
180
|
+
get state(): {
|
|
181
|
+
subscribe: (cb: Subscriber<WSState>) => Unsubscriber;
|
|
182
|
+
};
|
|
183
|
+
/**
|
|
184
|
+
* Debug snapshot: url, state, identity, rooms and outbox counters.
|
|
185
|
+
*
|
|
186
|
+
* For logging and troubleshooting — the shape is not part of the stable API.
|
|
187
|
+
*/
|
|
188
|
+
dump(): Record<string, unknown>;
|
|
189
|
+
/**
|
|
190
|
+
* Subscribes to a lifecycle event. See {@link WSEvents}.
|
|
191
|
+
*
|
|
192
|
+
* @param event - event name
|
|
193
|
+
* @param cb - handler; a throw here is caught and reported as `error`
|
|
194
|
+
* @returns detaches the handler; also `Symbol.dispose`-compatible
|
|
195
|
+
*/
|
|
196
|
+
on<K extends keyof WSEvents>(event: K, cb: (data: WSEvents[K]) => void): Unsubscriber;
|
|
197
|
+
/**
|
|
198
|
+
* Like {@link on}, but detaches after the first emission.
|
|
199
|
+
*
|
|
200
|
+
* @param event - event name
|
|
201
|
+
* @param cb - handler
|
|
202
|
+
* @returns detaches the handler early, if it has not fired yet
|
|
203
|
+
*/
|
|
204
|
+
once<K extends keyof WSEvents>(event: K, cb: (data: WSEvents[K]) => void): Unsubscriber;
|
|
205
|
+
/**
|
|
206
|
+
* Starts the connection and resolves once it is established.
|
|
207
|
+
*
|
|
208
|
+
* Idempotent: concurrent calls share one promise, and it resolves
|
|
209
|
+
* immediately when already connected.
|
|
210
|
+
*
|
|
211
|
+
* Rejects **only** where retrying cannot help:
|
|
212
|
+
* - {@link WSTerminatedError} — terminal close code (bad credentials, etc.)
|
|
213
|
+
* - {@link WSConnectTimeoutError} — `connectTimeout` elapsed; note the
|
|
214
|
+
* client keeps retrying in the background, so this bounds *your await*,
|
|
215
|
+
* not the connection attempt
|
|
216
|
+
*
|
|
217
|
+
* Ordinary network failure never rejects; that is what the infinite retry
|
|
218
|
+
* is for.
|
|
219
|
+
*
|
|
220
|
+
* Calling this is optional when `autoConnect` is on — it is a readiness
|
|
221
|
+
* gate, not a prerequisite.
|
|
222
|
+
*
|
|
223
|
+
* @returns resolves once authenticated
|
|
224
|
+
*/
|
|
225
|
+
connect(): Promise<void>;
|
|
226
|
+
/**
|
|
227
|
+
* Stops retrying and closes the socket.
|
|
228
|
+
*
|
|
229
|
+
* Resumable: handlers, room subscriptions and buffered sends all survive,
|
|
230
|
+
* so a later `connect()` picks up exactly where this left off. Use
|
|
231
|
+
* {@link dispose} for terminal teardown.
|
|
232
|
+
*/
|
|
233
|
+
disconnect(): void;
|
|
234
|
+
/**
|
|
235
|
+
* Terminal teardown: disconnects, then drops every handler, room, timer and
|
|
236
|
+
* pending promise. The instance is unusable afterwards.
|
|
237
|
+
*/
|
|
238
|
+
dispose(): void;
|
|
239
|
+
/**
|
|
240
|
+
* Subscribes to a room and attaches a handler.
|
|
241
|
+
*
|
|
242
|
+
* The handler is attached **synchronously**, before any frame goes out, so
|
|
243
|
+
* nothing arriving between the request and its acknowledgement is lost.
|
|
244
|
+
*
|
|
245
|
+
* Rooms are refcounted: N handlers produce one wire subscription, and the
|
|
246
|
+
* returned unsubscriber detaches this handler — sending `unsub` only when
|
|
247
|
+
* it was the last one.
|
|
248
|
+
*
|
|
249
|
+
* Resolution: when connected, this awaits the server's acknowledgement, so
|
|
250
|
+
* a rejected subscription surfaces as a rejection here. When not connected
|
|
251
|
+
* it resolves as soon as the room is registered — the subscription is then
|
|
252
|
+
* guaranteed to be established by the re-subscribe step on the next
|
|
253
|
+
* connect, and a failure there surfaces as an `error` event.
|
|
254
|
+
*
|
|
255
|
+
* @param room - room name, scoped to this client's namespace
|
|
256
|
+
* @param handler - receives every message published to the room
|
|
257
|
+
* @param options - pass `presence` to enable membership tracking
|
|
258
|
+
* @returns detaches this handler; also `Symbol.dispose`-compatible, and
|
|
259
|
+
* idempotent, so calling it twice is harmless
|
|
260
|
+
* @throws {WSRemoteError} when connected and the server refuses
|
|
261
|
+
* @throws {WSDisposedError} when the client was disposed
|
|
262
|
+
*
|
|
263
|
+
* @example
|
|
264
|
+
* ```ts
|
|
265
|
+
* const unsub = await ws.subscribe("chat", (msg) => render(msg.payload), {
|
|
266
|
+
* presence: (e) => setMembers(e.members),
|
|
267
|
+
* });
|
|
268
|
+
* ```
|
|
269
|
+
*/
|
|
270
|
+
subscribe<T = unknown>(room: string, handler: MessageHandler<T>, options?: SubscribeOptions): Promise<Unsubscriber>;
|
|
271
|
+
/**
|
|
272
|
+
* Removes every handler for a room and unsubscribes it.
|
|
273
|
+
*
|
|
274
|
+
* The blunt counterpart to the refcounted unsubscriber returned by
|
|
275
|
+
* {@link subscribe} — this drops other call sites' handlers too.
|
|
276
|
+
*
|
|
277
|
+
* @param room - room name; unknown rooms are a no-op
|
|
278
|
+
*/
|
|
279
|
+
unsubscribe(room: string): Promise<void>;
|
|
280
|
+
/**
|
|
281
|
+
* Whether the room is held locally.
|
|
282
|
+
*
|
|
283
|
+
* Reflects local intent, not server state: a room registered while offline
|
|
284
|
+
* reads `true` before the wire subscription exists.
|
|
285
|
+
*
|
|
286
|
+
* @param room - room name
|
|
287
|
+
*/
|
|
288
|
+
isSubscribed(room: string): boolean;
|
|
289
|
+
/**
|
|
290
|
+
* Last known membership of a presence-enabled room.
|
|
291
|
+
*
|
|
292
|
+
* @param room - room name
|
|
293
|
+
* @returns a copy of the members; empty when the room has no presence
|
|
294
|
+
*/
|
|
295
|
+
members(room: string): string[];
|
|
296
|
+
/**
|
|
297
|
+
* Publishes to a room within this client's namespace.
|
|
298
|
+
*
|
|
299
|
+
* Resolves with the recipient count once the server acknowledges. While
|
|
300
|
+
* disconnected the frame is buffered and the promise stays pending until it
|
|
301
|
+
* flushes — bounded by `sendTimeout`, never indefinitely.
|
|
302
|
+
*
|
|
303
|
+
* @param room - target room
|
|
304
|
+
* @param payload - opaque application data; never inspected or mutated
|
|
305
|
+
* @param namespace - must equal this client's namespace; the server rejects
|
|
306
|
+
* anything else, so this is only useful for asserting the expected one
|
|
307
|
+
* @returns the recipient count reported by the receiving server instance —
|
|
308
|
+
* best-effort telemetry, not a delivery guarantee
|
|
309
|
+
* @throws {WSTimeoutError} `sendTimeout` elapsed with no acknowledgement
|
|
310
|
+
* @throws {WSOutboxDropError} evicted from a full outbox
|
|
311
|
+
* @throws {WSNotConnectedError} sent while offline with `outboxMaxSize: 0`
|
|
312
|
+
* @throws {WSRemoteError} the server rejected it with a `nack`
|
|
313
|
+
*/
|
|
314
|
+
publish<T = unknown>(room: string, payload: T, namespace?: string): Promise<WSPublishResult>;
|
|
315
|
+
/**
|
|
316
|
+
* Publishes to a room across **all** namespaces.
|
|
317
|
+
*
|
|
318
|
+
* This crosses the isolation boundary, which is why it is its own method
|
|
319
|
+
* rather than a flag on {@link publish} — the server gates it separately
|
|
320
|
+
* via `allowBroadcast`, and it denies by default.
|
|
321
|
+
*
|
|
322
|
+
* @param room - target room, in every namespace at once
|
|
323
|
+
* @param payload - opaque application data
|
|
324
|
+
* @returns the recipient count across all namespaces on the receiving
|
|
325
|
+
* server instance
|
|
326
|
+
* @throws {WSRemoteError} with code `forbidden` when `allowBroadcast` denies
|
|
327
|
+
*/
|
|
328
|
+
broadcast<T = unknown>(room: string, payload: T): Promise<WSPublishResult>;
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Creates a {@link WSClient}.
|
|
332
|
+
*
|
|
333
|
+
* Both this and the class are exported, following the `PubSub` /
|
|
334
|
+
* `createPubSub` precedent in `@marianmeres/pubsub`.
|
|
335
|
+
*
|
|
336
|
+
* @param options - see {@link WSClientOptions}
|
|
337
|
+
* @returns a client that has not connected yet
|
|
338
|
+
*
|
|
339
|
+
* @example
|
|
340
|
+
* ```ts
|
|
341
|
+
* const ws = createWSClient({
|
|
342
|
+
* url: "wss://example.com/ws",
|
|
343
|
+
* namespace: "org-123",
|
|
344
|
+
* auth: () => session.token, // re-read on every reconnect
|
|
345
|
+
* });
|
|
346
|
+
*
|
|
347
|
+
* await ws.subscribe("chat", (msg) => console.log(msg.from, msg.payload));
|
|
348
|
+
* await ws.publish("chat", { text: "hello" });
|
|
349
|
+
* ```
|
|
350
|
+
*/
|
|
351
|
+
export declare function createWSClient<TAuth = unknown>(options?: WSClientOptions<TAuth>): WSClient<TAuth>;
|