react-websocket 2.2.0 → 3.1.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/dist/core.cjs ADDED
@@ -0,0 +1,594 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/core.ts
21
+ var core_exports = {};
22
+ __export(core_exports, {
23
+ MessageError: () => MessageError,
24
+ createWebSocket: () => createWebSocket,
25
+ createWebSocketState: () => createWebSocketState,
26
+ createWebSocketTopics: () => createWebSocketTopics
27
+ });
28
+ module.exports = __toCommonJS(core_exports);
29
+
30
+ // src/types.ts
31
+ var MessageError = class extends Error {
32
+ /** The raw `event.data` that failed. */
33
+ data;
34
+ /** Schema validation issues, when the message failed `schema`. */
35
+ issues;
36
+ constructor(message, data, options) {
37
+ super(message, options?.cause === void 0 ? void 0 : { cause: options.cause });
38
+ this.name = "MessageError";
39
+ this.data = data;
40
+ this.issues = options?.issues;
41
+ }
42
+ };
43
+
44
+ // src/client.ts
45
+ var OPEN = 1;
46
+ var RECONNECT_DEFAULTS = {
47
+ maxAttempts: Infinity,
48
+ minDelay: 1e3,
49
+ maxDelay: 3e4,
50
+ factor: 2,
51
+ jitter: 0.5
52
+ };
53
+ function createDeferred() {
54
+ let resolve;
55
+ let reject;
56
+ const promise = new Promise((res, rej) => {
57
+ resolve = res;
58
+ reject = rej;
59
+ });
60
+ promise.status = "pending";
61
+ promise.catch(() => {
62
+ });
63
+ const deferred = {
64
+ promise,
65
+ settled: false,
66
+ resolve() {
67
+ if (deferred.settled) return;
68
+ deferred.settled = true;
69
+ promise.status = "fulfilled";
70
+ promise.value = void 0;
71
+ resolve();
72
+ },
73
+ reject(reason) {
74
+ if (deferred.settled) return;
75
+ deferred.settled = true;
76
+ promise.status = "rejected";
77
+ promise.reason = reason;
78
+ reject(reason);
79
+ }
80
+ };
81
+ return deferred;
82
+ }
83
+ function rethrowAsync(error) {
84
+ queueMicrotask(() => {
85
+ throw error;
86
+ });
87
+ }
88
+ function isThenable(value) {
89
+ return typeof value?.then === "function";
90
+ }
91
+ var ON_TEARDOWN = /* @__PURE__ */ Symbol.for("react-websocket.onTeardown");
92
+ function createClient(url, options = {}) {
93
+ const onTeardown = options[ON_TEARDOWN];
94
+ const log = options.logger ?? (() => {
95
+ });
96
+ const retry = options.reconnect === false ? null : {
97
+ ...RECONNECT_DEFAULTS,
98
+ ...typeof options.reconnect === "object" ? options.reconnect : {}
99
+ };
100
+ const queueLimit = options.queue === false ? 0 : typeof options.queue === "object" ? Math.max(0, options.queue.limit ?? Infinity) : Infinity;
101
+ let snapshot = { status: "idle", lastMessage: void 0, attempt: 0 };
102
+ const storeListeners = /* @__PURE__ */ new Set();
103
+ const listeners = {
104
+ open: /* @__PURE__ */ new Set(),
105
+ message: /* @__PURE__ */ new Set(),
106
+ close: /* @__PURE__ */ new Set(),
107
+ error: /* @__PURE__ */ new Set()
108
+ };
109
+ let ws = null;
110
+ let wanted = false;
111
+ let refs = 0;
112
+ let pinned = false;
113
+ let pending = [];
114
+ let ready = null;
115
+ let opened = false;
116
+ let unavailable = null;
117
+ let startScheduled = false;
118
+ let inbox = null;
119
+ let generation = 0;
120
+ let windowBound = false;
121
+ let retryTimer;
122
+ let teardownTimer;
123
+ let heartbeatTimer;
124
+ let pongTimer;
125
+ function set(patch) {
126
+ snapshot = { ...snapshot, ...patch };
127
+ for (const listener of [...storeListeners]) listener();
128
+ }
129
+ function emit(type, ...args) {
130
+ for (const listener of [...listeners[type]]) {
131
+ try {
132
+ listener(...args);
133
+ } catch (error) {
134
+ rethrowAsync(error);
135
+ }
136
+ }
137
+ }
138
+ function getImpl() {
139
+ if (options.WebSocket) return options.WebSocket;
140
+ return typeof window === "undefined" ? void 0 : globalThis.WebSocket;
141
+ }
142
+ function rejectReady(reason) {
143
+ if (!ready || ready.settled) ready = createDeferred();
144
+ ready.reject(reason);
145
+ }
146
+ function retryNow() {
147
+ if (retryTimer !== void 0) {
148
+ log("network or visibility change, retrying now");
149
+ open();
150
+ }
151
+ }
152
+ function onVisibilityChange() {
153
+ if (document.visibilityState === "visible") retryNow();
154
+ }
155
+ function bindWindow() {
156
+ if (windowBound || typeof window === "undefined") return;
157
+ windowBound = true;
158
+ window.addEventListener("online", retryNow);
159
+ document.addEventListener("visibilitychange", onVisibilityChange);
160
+ }
161
+ function unbindWindow() {
162
+ if (!windowBound) return;
163
+ windowBound = false;
164
+ window.removeEventListener("online", retryNow);
165
+ document.removeEventListener("visibilitychange", onVisibilityChange);
166
+ }
167
+ function startHeartbeat(socket) {
168
+ const heartbeat = options.heartbeat;
169
+ if (!heartbeat) return;
170
+ heartbeatTimer = setInterval(() => {
171
+ socket.send(heartbeat.message ?? "ping");
172
+ if (heartbeat.timeout && pongTimer === void 0) {
173
+ pongTimer = setTimeout(() => {
174
+ log("heartbeat timeout");
175
+ detach(socket, false);
176
+ socket.close(4e3, "heartbeat timeout");
177
+ handleClose({ code: 4e3, reason: "heartbeat timeout", wasClean: false });
178
+ }, heartbeat.timeout);
179
+ }
180
+ }, heartbeat.interval);
181
+ }
182
+ function stopHeartbeat() {
183
+ clearInterval(heartbeatTimer);
184
+ clearTimeout(pongTimer);
185
+ heartbeatTimer = pongTimer = void 0;
186
+ }
187
+ function detach(socket, reportClose) {
188
+ if (ws === socket) ws = null;
189
+ socket.onopen = socket.onmessage = socket.onerror = null;
190
+ socket.onclose = reportClose ? (event) => emit("close", { code: event.code, reason: event.reason, wasClean: event.wasClean }) : null;
191
+ }
192
+ function backoff(attempt) {
193
+ const { minDelay, maxDelay, factor, jitter } = retry;
194
+ const base = Math.min(maxDelay, minDelay * factor ** (attempt - 1));
195
+ return Math.round(base * (1 - jitter * Math.random()));
196
+ }
197
+ function open() {
198
+ clearTimeout(retryTimer);
199
+ retryTimer = void 0;
200
+ const Impl = getImpl();
201
+ if (!Impl) return;
202
+ const id = ++generation;
203
+ set({ status: snapshot.attempt === 0 ? "connecting" : "reconnecting" });
204
+ let target;
205
+ try {
206
+ target = typeof url === "string" ? url : url();
207
+ } catch (error) {
208
+ urlFailed(error);
209
+ return;
210
+ }
211
+ if (typeof target === "string") {
212
+ start(Impl, target);
213
+ return;
214
+ }
215
+ Promise.resolve(target).then(
216
+ (resolved) => {
217
+ if (id === generation && wanted) start(Impl, resolved);
218
+ },
219
+ (error) => {
220
+ if (id === generation && wanted) urlFailed(error);
221
+ }
222
+ );
223
+ }
224
+ function urlFailed(error) {
225
+ const reason = error instanceof Error ? error : new Error(String(error));
226
+ log("url() failed", reason);
227
+ emit("error", reason);
228
+ retryOrFail({ code: 1006, reason: "url() failed", wasClean: false }, reason);
229
+ }
230
+ function start(Impl, target) {
231
+ let socket;
232
+ try {
233
+ socket = new Impl(target, options.protocols);
234
+ } catch (error) {
235
+ emit("error", error);
236
+ fail(error);
237
+ return;
238
+ }
239
+ if (options.binaryType) socket.binaryType = options.binaryType;
240
+ ws = socket;
241
+ log("connecting", target);
242
+ socket.onopen = (event) => {
243
+ log("open");
244
+ opened = true;
245
+ set({ status: "open", attempt: 0 });
246
+ startHeartbeat(socket);
247
+ ready?.resolve();
248
+ emit("open", event);
249
+ if (ws !== socket) return;
250
+ const queued = pending;
251
+ pending = [];
252
+ for (const data of queued) socket.send(data);
253
+ };
254
+ socket.onmessage = (event) => receive(event, socket);
255
+ socket.onerror = (event) => emit("error", event);
256
+ socket.onclose = (event) => {
257
+ detach(socket, false);
258
+ handleClose({ code: event.code, reason: event.reason, wasClean: event.wasClean });
259
+ };
260
+ }
261
+ function handleClose(info) {
262
+ stopHeartbeat();
263
+ log("closed", info);
264
+ retryOrFail(info);
265
+ emit("close", info);
266
+ }
267
+ function retryOrFail(info, error) {
268
+ if (wanted && retry && snapshot.attempt < retry.maxAttempts && (retry.shouldReconnect?.(info) ?? true)) {
269
+ const attempt = snapshot.attempt + 1;
270
+ const delay = backoff(attempt);
271
+ set({ status: "reconnecting", attempt });
272
+ log(`reconnecting in ${delay}ms (attempt ${attempt})`);
273
+ retryTimer = setTimeout(open, delay);
274
+ } else {
275
+ fail(
276
+ error ?? new Error(`WebSocket closed (code ${info.code}${info.reason ? `: ${info.reason}` : ""})`)
277
+ );
278
+ }
279
+ }
280
+ function fail(reason) {
281
+ wanted = false;
282
+ opened = false;
283
+ unbindWindow();
284
+ pending = [];
285
+ set({ status: "closed" });
286
+ rejectReady(reason);
287
+ }
288
+ function receive(event, socket) {
289
+ clearTimeout(pongTimer);
290
+ pongTimer = void 0;
291
+ if (options.heartbeat?.pong !== void 0 && event.data === options.heartbeat.pong) return;
292
+ const run = () => decode(event, socket);
293
+ if (inbox) {
294
+ const next = inbox.then(run).then(() => {
295
+ if (inbox === next) inbox = null;
296
+ });
297
+ inbox = next;
298
+ return;
299
+ }
300
+ const result = run();
301
+ if (result) {
302
+ const next = result.then(() => {
303
+ if (inbox === next) inbox = null;
304
+ });
305
+ inbox = next;
306
+ }
307
+ }
308
+ function decode(event, socket) {
309
+ let value;
310
+ try {
311
+ value = options.parse ? options.parse(event.data) : options.json ? JSON.parse(event.data) : event.data;
312
+ } catch (cause) {
313
+ emit("error", new MessageError("Failed to parse message", event.data, { cause }));
314
+ return;
315
+ }
316
+ const schema = options.schema;
317
+ if (!schema) return deliver(value, event, socket);
318
+ let result;
319
+ try {
320
+ result = schema["~standard"].validate(value);
321
+ } catch (cause) {
322
+ emit("error", new MessageError("Schema validation threw", event.data, { cause }));
323
+ return;
324
+ }
325
+ if (isThenable(result)) {
326
+ return Promise.resolve(result).then(
327
+ (r) => validated(r, event, socket),
328
+ (cause) => emit("error", new MessageError("Schema validation threw", event.data, { cause }))
329
+ );
330
+ }
331
+ validated(result, event, socket);
332
+ }
333
+ function validated(result, event, socket) {
334
+ if (result.issues) {
335
+ emit(
336
+ "error",
337
+ new MessageError("Message failed schema validation", event.data, { issues: result.issues })
338
+ );
339
+ } else {
340
+ deliver(result.value, event, socket);
341
+ }
342
+ }
343
+ function deliver(message, event, socket) {
344
+ if (ws !== socket) return;
345
+ set({ lastMessage: message });
346
+ emit("message", message, event);
347
+ }
348
+ function send(message) {
349
+ const data = options.serialize ? options.serialize(message) : options.json ? JSON.stringify(message) : message;
350
+ if (ws && ws.readyState === OPEN) {
351
+ ws.send(data);
352
+ return true;
353
+ }
354
+ if (queueLimit === 0) {
355
+ log("dropped message: socket is not open");
356
+ return false;
357
+ }
358
+ if (!getImpl()) {
359
+ log("dropped message: no WebSocket implementation available");
360
+ return false;
361
+ }
362
+ pending.push(data);
363
+ if (pending.length > queueLimit) {
364
+ pending.shift();
365
+ log(`dropped oldest queued message: queue limit (${queueLimit}) reached`);
366
+ }
367
+ return false;
368
+ }
369
+ function begin() {
370
+ wanted = true;
371
+ if (snapshot.status === "idle" || snapshot.status === "closed") {
372
+ opened = false;
373
+ if (ready?.settled) ready = null;
374
+ }
375
+ snapshot = { ...snapshot, attempt: 0 };
376
+ bindWindow();
377
+ open();
378
+ }
379
+ function ensureConnecting() {
380
+ if (wanted || !getImpl()) return;
381
+ begin();
382
+ }
383
+ function connect() {
384
+ pinned = true;
385
+ ensureConnecting();
386
+ }
387
+ function stop(code, reason) {
388
+ wanted = false;
389
+ generation++;
390
+ clearTimeout(retryTimer);
391
+ retryTimer = void 0;
392
+ stopHeartbeat();
393
+ unbindWindow();
394
+ pending = [];
395
+ if (ws) {
396
+ const socket = ws;
397
+ detach(socket, true);
398
+ socket.close(code, reason);
399
+ }
400
+ }
401
+ function close(code = 1e3, reason) {
402
+ pinned = false;
403
+ opened = false;
404
+ stop(code, reason);
405
+ set({ status: "closed", attempt: 0 });
406
+ rejectReady(new Error("WebSocket was closed"));
407
+ }
408
+ function reconnect() {
409
+ if (!getImpl()) return;
410
+ clearTimeout(retryTimer);
411
+ retryTimer = void 0;
412
+ stopHeartbeat();
413
+ if (ws) {
414
+ const socket = ws;
415
+ detach(socket, true);
416
+ socket.close(1e3, "reconnect");
417
+ }
418
+ begin();
419
+ }
420
+ function teardown() {
421
+ if (refs > 0 || pinned) return;
422
+ if (ready && !ready.settled) return;
423
+ stop();
424
+ ready = null;
425
+ opened = false;
426
+ set({ status: "idle", attempt: 0 });
427
+ onTeardown?.();
428
+ }
429
+ function acquire() {
430
+ refs++;
431
+ clearTimeout(teardownTimer);
432
+ if (snapshot.status === "idle") ensureConnecting();
433
+ let released = false;
434
+ return () => {
435
+ if (released) return;
436
+ released = true;
437
+ if (--refs > 0) return;
438
+ clearTimeout(teardownTimer);
439
+ teardownTimer = setTimeout(teardown, 0);
440
+ };
441
+ }
442
+ return {
443
+ url,
444
+ get ready() {
445
+ if (!getImpl()) {
446
+ if (!unavailable) {
447
+ unavailable = createDeferred();
448
+ unavailable.reject(
449
+ new Error("react-websocket: no WebSocket available here (server render?)")
450
+ );
451
+ }
452
+ return unavailable.promise;
453
+ }
454
+ if (snapshot.status === "idle" && !startScheduled) {
455
+ startScheduled = true;
456
+ queueMicrotask(() => {
457
+ startScheduled = false;
458
+ if (snapshot.status === "idle") ensureConnecting();
459
+ });
460
+ }
461
+ if (!ready) {
462
+ ready = createDeferred();
463
+ if (opened) ready.resolve();
464
+ }
465
+ return ready.promise;
466
+ },
467
+ get socket() {
468
+ return ws;
469
+ },
470
+ subscribe(listener) {
471
+ storeListeners.add(listener);
472
+ return () => {
473
+ storeListeners.delete(listener);
474
+ };
475
+ },
476
+ getSnapshot: () => snapshot,
477
+ on(type, listener) {
478
+ const entry = ((...args) => listener(...args));
479
+ listeners[type].add(entry);
480
+ return () => {
481
+ listeners[type].delete(entry);
482
+ };
483
+ },
484
+ send,
485
+ connect,
486
+ close,
487
+ reconnect,
488
+ acquire
489
+ };
490
+ }
491
+ function createWebSocket(url, options) {
492
+ return createClient(url, options);
493
+ }
494
+
495
+ // src/state.ts
496
+ function createWebSocketState(client, options) {
497
+ const { initialState, reduce } = options;
498
+ let state = initialState;
499
+ const listeners = /* @__PURE__ */ new Set();
500
+ function setState(update) {
501
+ const next = typeof update === "function" ? update(state) : update;
502
+ if (Object.is(next, state)) return;
503
+ state = next;
504
+ for (const listener of [...listeners]) listener();
505
+ }
506
+ const off = client.on("message", (message) => setState((current) => reduce(current, message)));
507
+ return {
508
+ client,
509
+ initialState,
510
+ getState: () => state,
511
+ subscribe(listener) {
512
+ listeners.add(listener);
513
+ return () => {
514
+ listeners.delete(listener);
515
+ };
516
+ },
517
+ setState,
518
+ reset: () => setState(initialState),
519
+ dispose: off
520
+ };
521
+ }
522
+
523
+ // src/topics.ts
524
+ function createWebSocketTopics(client, options) {
525
+ const counts = /* @__PURE__ */ new Map();
526
+ const pendingRemoval = /* @__PURE__ */ new Map();
527
+ let sentOn = null;
528
+ let sent = /* @__PURE__ */ new Set();
529
+ function isOpen() {
530
+ return client.getSnapshot().status === "open" && client.socket !== null;
531
+ }
532
+ function subscribe(topic) {
533
+ if (!isOpen()) return;
534
+ if (client.socket !== sentOn) {
535
+ sentOn = client.socket;
536
+ sent = /* @__PURE__ */ new Set();
537
+ }
538
+ if (sent.has(topic)) return;
539
+ sent.add(topic);
540
+ client.send(options.subscribe(topic));
541
+ }
542
+ function unsubscribe(topic) {
543
+ if (!isOpen() || client.socket !== sentOn || !sent.delete(topic)) return;
544
+ if (options.unsubscribe) client.send(options.unsubscribe(topic));
545
+ }
546
+ const off = client.on("open", () => {
547
+ sentOn = client.socket;
548
+ sent = /* @__PURE__ */ new Set();
549
+ for (const topic of counts.keys()) subscribe(topic);
550
+ });
551
+ return {
552
+ client,
553
+ acquire(topic) {
554
+ const releaseConnection = client.acquire();
555
+ clearTimeout(pendingRemoval.get(topic));
556
+ pendingRemoval.delete(topic);
557
+ counts.set(topic, (counts.get(topic) ?? 0) + 1);
558
+ subscribe(topic);
559
+ let released = false;
560
+ return () => {
561
+ if (released) return;
562
+ released = true;
563
+ const remaining = (counts.get(topic) ?? 1) - 1;
564
+ if (remaining > 0) {
565
+ counts.set(topic, remaining);
566
+ } else {
567
+ counts.delete(topic);
568
+ pendingRemoval.set(
569
+ topic,
570
+ setTimeout(() => {
571
+ pendingRemoval.delete(topic);
572
+ if (!counts.has(topic)) unsubscribe(topic);
573
+ }, 0)
574
+ );
575
+ }
576
+ releaseConnection();
577
+ };
578
+ },
579
+ active: () => [...counts.keys()],
580
+ dispose() {
581
+ off();
582
+ for (const timer of pendingRemoval.values()) clearTimeout(timer);
583
+ pendingRemoval.clear();
584
+ }
585
+ };
586
+ }
587
+ // Annotate the CommonJS export names for ESM import in node:
588
+ 0 && (module.exports = {
589
+ MessageError,
590
+ createWebSocket,
591
+ createWebSocketState,
592
+ createWebSocketTopics
593
+ });
594
+ //# sourceMappingURL=core.cjs.map