react-websocket 3.3.0 → 3.6.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 CHANGED
@@ -349,13 +349,18 @@ function createClient(url, options = {}) {
349
349
  set({ lastMessage: message });
350
350
  emit("message", message, event);
351
351
  }
352
- function send(message, sendOptions) {
352
+ function send(message, sendOptions = {}) {
353
353
  const data = options.serialize ? options.serialize(message) : options.json ? JSON.stringify(message) : message;
354
+ const { key, priority, maxBuffered } = sendOptions;
354
355
  if (ws && ws.readyState === OPEN) {
356
+ if (maxBuffered !== void 0 && ws.bufferedAmount > maxBuffered) {
357
+ log("dropped message: send buffer above maxBuffered");
358
+ return false;
359
+ }
355
360
  ws.send(data);
356
361
  return true;
357
362
  }
358
- if (queueLimit === 0 || sendOptions?.volatile) {
363
+ if (queueLimit === 0 || sendOptions.volatile) {
359
364
  log("dropped message: socket is not open");
360
365
  return false;
361
366
  }
@@ -363,19 +368,23 @@ function createClient(url, options = {}) {
363
368
  log("dropped message: no WebSocket implementation available");
364
369
  return false;
365
370
  }
366
- const key = sendOptions?.key;
367
371
  if (key !== void 0) pending = pending.filter((entry) => {
368
372
  if (entry.key !== key) return true;
369
373
  dropped(entry);
370
374
  return false;
371
375
  });
372
- pending.push({
376
+ let head = pending.findIndex((entry) => !entry.priority);
377
+ if (head < 0) head = pending.length;
378
+ const entry = {
373
379
  data,
374
380
  key,
375
- onDrop: sendOptions?.onDrop
376
- });
381
+ priority,
382
+ onDrop: sendOptions.onDrop
383
+ };
384
+ if (priority) pending.splice(head++, 0, entry);
385
+ else pending.push(entry);
377
386
  if (pending.length > queueLimit) {
378
- dropped(pending.shift());
387
+ dropped(pending.splice(head < pending.length ? head : 0, 1)[0]);
379
388
  log(`dropped oldest queued message: queue limit (${queueLimit}) reached`);
380
389
  }
381
390
  return false;
@@ -651,6 +660,8 @@ function createWebSocketLatency(client, options) {
651
660
  //#endregion
652
661
  //#region src/requests.ts
653
662
  const lastIds = /* @__PURE__ */ new WeakMap();
663
+ const VOLATILE$2 = { volatile: true };
664
+ const noop = () => {};
654
665
  /**
655
666
  * Send requests and await their replies, matched by an id the library generates.
656
667
  *
@@ -671,8 +682,8 @@ const lastIds = /* @__PURE__ */ new WeakMap();
671
682
  */
672
683
  function createWebSocketRequests(client, options) {
673
684
  const waiting = /* @__PURE__ */ new Map();
674
- function rejectWhere(test, reason) {
675
- for (const entry of [...waiting.values()]) if (test(entry)) entry.reject(reason());
685
+ function endWhere(test, reason, cancel) {
686
+ for (const entry of [...waiting.values()]) if (test(entry)) entry.end(true, reason(), cancel);
676
687
  }
677
688
  const offs = [
678
689
  client.on("message", (message) => {
@@ -680,83 +691,137 @@ function createWebSocketRequests(client, options) {
680
691
  const entry = id == null ? void 0 : waiting.get(String(id));
681
692
  if (!entry) return;
682
693
  const error = options.error?.(message);
683
- if (error === void 0) entry.resolve(message);
684
- else entry.reject(error);
694
+ if (error === void 0) entry.receive(message);
695
+ else entry.end(true, error);
685
696
  }),
686
697
  client.on("open", () => {
687
698
  for (const entry of waiting.values()) entry.socket ??= client.socket;
688
699
  }),
689
700
  client.subscribe(() => {
690
701
  const { status } = client.getSnapshot();
691
- if (status === "closed" || status === "idle") rejectWhere(() => true, () => /* @__PURE__ */ new Error("WebSocket was closed before the request was answered"));
692
- else rejectWhere((entry) => entry.socket !== null && entry.socket !== client.socket, () => /* @__PURE__ */ new Error("WebSocket connection was lost before the request was answered"));
702
+ if (status === "closed" || status === "idle") endWhere(() => true, () => /* @__PURE__ */ new Error("WebSocket was closed before the request was answered"));
703
+ else endWhere((entry) => entry.socket !== null && entry.socket !== client.socket, () => /* @__PURE__ */ new Error("WebSocket connection was lost before the request was answered"));
693
704
  })
694
705
  ];
706
+ /**
707
+ * Send `message` as a new request. `next` gets each reply that isn't an error and returns
708
+ * whether it was the last; `done` runs once, when the request ends. Returns a function that
709
+ * ends it from this side and tells the server.
710
+ */
711
+ function start(message, requestOptions, next, done) {
712
+ const { signal, priority } = requestOptions;
713
+ if (signal?.aborted) {
714
+ done(true, signal.reason);
715
+ return noop;
716
+ }
717
+ const id = (lastIds.get(client) ?? 0) + 1;
718
+ lastIds.set(client, id);
719
+ const key = String(id);
720
+ const timeout = requestOptions.timeout ?? options.timeout ?? 1e4;
721
+ const release = client.acquire();
722
+ let timer;
723
+ const onAbort = () => entry.end(true, signal.reason, true);
724
+ function arm() {
725
+ clearTimeout(timer);
726
+ if (timeout > 0 && timeout < Infinity) timer = setTimeout(() => entry.end(true, new DOMException(`Request ${id} timed out after ${timeout}ms`, "TimeoutError"), true), timeout);
727
+ }
728
+ const entry = {
729
+ socket: null,
730
+ receive(reply) {
731
+ if (next(reply)) entry.end(false);
732
+ else arm();
733
+ },
734
+ end(failed, reason, cancel) {
735
+ if (!waiting.delete(key)) return;
736
+ clearTimeout(timer);
737
+ signal?.removeEventListener("abort", onAbort);
738
+ release();
739
+ done(failed, reason);
740
+ if (cancel && options.cancel) try {
741
+ client.send(options.cancel(id), entry.socket ? VOLATILE$2 : { priority });
742
+ } catch (error) {
743
+ queueMicrotask(() => {
744
+ throw error;
745
+ });
746
+ }
747
+ }
748
+ };
749
+ waiting.set(key, entry);
750
+ signal?.addEventListener("abort", onAbort, { once: true });
751
+ arm();
752
+ const { status } = client.getSnapshot();
753
+ if (status === "closed" || status === "idle") entry.end(true, /* @__PURE__ */ new Error(`WebSocket is ${status}: request not sent`));
754
+ else try {
755
+ if (client.send(options.send(message, id), {
756
+ ...requestOptions,
757
+ onDrop() {
758
+ entry.end(true, /* @__PURE__ */ new Error("Request was dropped from the send queue before it was sent"));
759
+ requestOptions.onDrop?.();
760
+ }
761
+ })) entry.socket = client.socket;
762
+ else if (client.socket?.readyState === 1) entry.end(true, /* @__PURE__ */ new Error("WebSocket send buffer is full: request dropped"));
763
+ else if (requestOptions.volatile) entry.end(true, /* @__PURE__ */ new Error("WebSocket is not open: request dropped"));
764
+ } catch (error) {
765
+ entry.end(true, error);
766
+ }
767
+ return () => entry.end(false, void 0, true);
768
+ }
695
769
  return {
696
770
  client,
697
771
  request(message, requestOptions = {}) {
698
- const { signal } = requestOptions;
699
- if (signal?.aborted) return Promise.reject(signal.reason);
700
- const id = (lastIds.get(client) ?? 0) + 1;
701
- lastIds.set(client, id);
702
- const key = String(id);
703
- const timeout = requestOptions.timeout ?? options.timeout ?? 1e4;
704
772
  return new Promise((resolve, reject) => {
705
- let timer;
706
- const release = client.acquire();
707
- const onAbort = () => entry.reject(signal.reason);
708
- function settle() {
709
- waiting.delete(key);
710
- clearTimeout(timer);
711
- signal?.removeEventListener("abort", onAbort);
712
- release();
713
- }
714
- const entry = {
715
- socket: null,
716
- resolve(reply) {
717
- settle();
718
- resolve(reply);
719
- },
720
- reject(reason) {
721
- settle();
722
- reject(reason);
723
- }
724
- };
725
- waiting.set(key, entry);
726
- signal?.addEventListener("abort", onAbort, { once: true });
727
- if (timeout > 0 && timeout < Infinity) timer = setTimeout(() => entry.reject(new DOMException(`Request ${id} timed out after ${timeout}ms`, "TimeoutError")), timeout);
728
- const { status } = client.getSnapshot();
729
- if (status === "closed" || status === "idle") {
730
- entry.reject(/* @__PURE__ */ new Error(`WebSocket is ${status}: request not sent`));
731
- return;
732
- }
733
- let sent;
773
+ start(message, requestOptions, (reply) => {
774
+ resolve(reply);
775
+ return true;
776
+ }, (failed, reason) => {
777
+ if (failed) reject(reason);
778
+ });
779
+ });
780
+ },
781
+ stream(message, requestOptions = {}) {
782
+ const last = options.done;
783
+ if (!last) throw new TypeError("stream() needs the `done` option");
784
+ const replies = [];
785
+ let ended;
786
+ let wake = noop;
787
+ const stop = start(message, requestOptions, (reply) => {
788
+ replies.push(reply);
789
+ wake();
790
+ return last(reply);
791
+ }, (failed, reason) => {
792
+ if (requestOptions.signal?.aborted) replies.length = 0;
793
+ ended = [failed, reason];
794
+ wake();
795
+ });
796
+ async function* read() {
734
797
  try {
735
- sent = client.send(options.send(message, id), {
736
- ...requestOptions,
737
- onDrop() {
738
- entry.reject(/* @__PURE__ */ new Error("Request was dropped from the send queue before it was sent"));
739
- requestOptions.onDrop?.();
740
- }
741
- });
742
- } catch (error) {
743
- entry.reject(error);
744
- return;
798
+ for (;;) if (replies.length) yield replies.shift();
799
+ else if (ended) {
800
+ if (ended[0]) throw ended[1];
801
+ return;
802
+ } else await new Promise((resolve) => wake = resolve);
803
+ } finally {
804
+ stop();
745
805
  }
746
- if (sent) entry.socket = client.socket;
747
- else if (requestOptions.volatile) entry.reject(/* @__PURE__ */ new Error("WebSocket is not open: request dropped"));
748
- });
806
+ }
807
+ const iterator = read();
808
+ const close = iterator.return;
809
+ iterator.return = (value) => {
810
+ stop();
811
+ return close.call(iterator, value);
812
+ };
813
+ return iterator;
749
814
  },
750
815
  pending: () => waiting.size,
751
816
  dispose() {
752
817
  offs.forEach((off) => off());
753
- rejectWhere(() => true, () => /* @__PURE__ */ new Error("Requests were disposed"));
818
+ endWhere(() => true, () => /* @__PURE__ */ new Error("Requests were disposed"), true);
754
819
  }
755
820
  };
756
821
  }
757
822
  //#endregion
758
823
  //#region src/responder.ts
759
- const VOLATILE = { volatile: true };
824
+ const VOLATILE$1 = { volatile: true };
760
825
  const abortError = (message) => new DOMException(message, "AbortError");
761
826
  /**
762
827
  * Answer requests the server sends: run a handler for tools that only exist in the browser
@@ -797,9 +862,9 @@ function createWebSocketResponder(client, options) {
797
862
  /** Send a reply, or an `error` reply if building or encoding it throws. */
798
863
  function reply(id, build) {
799
864
  try {
800
- return client.send(build(), VOLATILE);
865
+ return client.send(build(), VOLATILE$1);
801
866
  } catch (error) {
802
- return client.send(options.error(id, error), VOLATILE);
867
+ return client.send(options.error(id, error), VOLATILE$1);
803
868
  }
804
869
  }
805
870
  function receive(incoming) {
@@ -898,6 +963,123 @@ function createWebSocketResponder(client, options) {
898
963
  };
899
964
  }
900
965
  //#endregion
966
+ //#region src/sendrate.ts
967
+ const VOLATILE = { volatile: true };
968
+ /**
969
+ * Send high-rate state, such as aim, cursor or position, at a rate that adapts to the
970
+ * connection. Messages are coalesced per key between ticks. The rate drops by half when the
971
+ * send buffer fills up or the round trip grows, and climbs back one tick per second at a time
972
+ * when the link keeps up, so players on a poor connection degrade smoothly instead of building
973
+ * up lag.
974
+ *
975
+ * Ticks run only while the socket is open and something is waiting. Messages waiting during a
976
+ * reconnect go out after it; `close()` drops them. It doesn't open the connection by itself.
977
+ *
978
+ * @example
979
+ * ```ts
980
+ * export const stream = createWebSocketSendRate(game, { latency, maxRate: 30 });
981
+ *
982
+ * // On every pointer move: at most 30 a second, fewer on a congested link.
983
+ * stream.send({ type: 'aim', angle }, 'aim');
984
+ * ```
985
+ */
986
+ function createWebSocketSendRate(client, options = {}) {
987
+ const { maxBuffered = 1024, latency, maxRttIncrease = 100 } = options;
988
+ const maxRate = options.maxRate ?? 30;
989
+ const minRate = Math.min(options.minRate ?? 5, maxRate);
990
+ const listeners = /* @__PURE__ */ new Set();
991
+ const pending = /* @__PURE__ */ new Map();
992
+ let rate = maxRate;
993
+ let snapshot = {
994
+ rate: maxRate,
995
+ congested: false
996
+ };
997
+ let last = -Infinity;
998
+ let best = Infinity;
999
+ let slow = false;
1000
+ let measured;
1001
+ let timer;
1002
+ let disposed = false;
1003
+ function flush() {
1004
+ for (const [key, message] of pending) {
1005
+ if (!client.send(message, VOLATILE)) return;
1006
+ pending.delete(key);
1007
+ }
1008
+ }
1009
+ function publish(congested) {
1010
+ const rounded = Math.round(rate);
1011
+ if (rounded === snapshot.rate && congested === snapshot.congested) return;
1012
+ snapshot = {
1013
+ rate: rounded,
1014
+ congested
1015
+ };
1016
+ for (const listener of [...listeners]) listener();
1017
+ }
1018
+ function schedule() {
1019
+ if (disposed || timer !== void 0 || !pending.size || client.getSnapshot().status !== "open") return;
1020
+ timer = setTimeout(tick, Math.max(0, last + 1e3 / rate - performance.now()));
1021
+ }
1022
+ function stop() {
1023
+ clearTimeout(timer);
1024
+ timer = void 0;
1025
+ }
1026
+ function tick() {
1027
+ timer = void 0;
1028
+ last = performance.now();
1029
+ const measurement = latency?.getSnapshot();
1030
+ if (measurement !== measured) {
1031
+ measured = measurement;
1032
+ const rtt = measurement?.rtt;
1033
+ if (rtt !== void 0) {
1034
+ best = Math.min(best, rtt);
1035
+ slow = rtt - best > maxRttIncrease;
1036
+ if (slow) rate /= 2;
1037
+ }
1038
+ }
1039
+ const full = (client.socket?.bufferedAmount ?? 0) > maxBuffered;
1040
+ if (full) rate /= 2;
1041
+ else {
1042
+ flush();
1043
+ if (!slow) rate++;
1044
+ }
1045
+ rate = Math.min(maxRate, Math.max(minRate, rate));
1046
+ publish(full || slow);
1047
+ schedule();
1048
+ }
1049
+ const offs = [client.on("open", () => {
1050
+ best = Infinity;
1051
+ slow = false;
1052
+ measured = latency?.getSnapshot();
1053
+ schedule();
1054
+ }), client.subscribe(() => {
1055
+ const { status } = client.getSnapshot();
1056
+ if (status !== "open") stop();
1057
+ if (status === "closed" || status === "idle") pending.clear();
1058
+ })];
1059
+ return {
1060
+ client,
1061
+ send(message, key) {
1062
+ pending.delete(key);
1063
+ pending.set(key, message);
1064
+ schedule();
1065
+ },
1066
+ flush,
1067
+ getSnapshot: () => snapshot,
1068
+ subscribe(listener) {
1069
+ listeners.add(listener);
1070
+ return () => {
1071
+ listeners.delete(listener);
1072
+ };
1073
+ },
1074
+ dispose() {
1075
+ disposed = true;
1076
+ stop();
1077
+ pending.clear();
1078
+ offs.forEach((off) => off());
1079
+ }
1080
+ };
1081
+ }
1082
+ //#endregion
901
1083
  //#region src/state.ts
902
1084
  /**
903
1085
  * Keep state derived from a client's messages, like a reducer that runs on every message.
@@ -918,14 +1100,30 @@ function createWebSocketResponder(client, options) {
918
1100
  * ```
919
1101
  */
920
1102
  function createWebSocketState(client, options) {
921
- const { initialState, reduce } = options;
1103
+ const { initialState, reduce, batch } = options;
922
1104
  let state = initialState;
1105
+ let notified = state;
1106
+ let frame = false;
923
1107
  const listeners = /* @__PURE__ */ new Set();
924
- function setState(update) {
925
- const next = typeof update === "function" ? update(state) : update;
1108
+ function notify() {
1109
+ if (Object.is(state, notified)) return;
1110
+ notified = state;
1111
+ for (const listener of [...listeners]) listener();
1112
+ }
1113
+ function flush() {
1114
+ frame = false;
1115
+ notify();
1116
+ }
1117
+ function update(change, later) {
1118
+ const next = typeof change === "function" ? change(state) : change;
926
1119
  if (Object.is(next, state)) return;
927
1120
  state = next;
928
- for (const listener of [...listeners]) listener();
1121
+ if (!later) notify();
1122
+ else if (!frame) {
1123
+ frame = true;
1124
+ if (typeof requestAnimationFrame === "function") requestAnimationFrame(flush);
1125
+ else setTimeout(flush, 16);
1126
+ }
929
1127
  }
930
1128
  return {
931
1129
  client,
@@ -937,9 +1135,9 @@ function createWebSocketState(client, options) {
937
1135
  listeners.delete(listener);
938
1136
  };
939
1137
  },
940
- setState,
941
- reset: () => setState(initialState),
942
- dispose: client.on("message", (message) => setState((current) => reduce(current, message)))
1138
+ setState: (change) => update(change),
1139
+ reset: () => update(initialState),
1140
+ dispose: client.on("message", (message) => update((current) => reduce(current, message), batch === "frame"))
943
1141
  };
944
1142
  }
945
1143
  //#endregion
@@ -1031,6 +1229,7 @@ exports.createWebSocket = createWebSocket;
1031
1229
  exports.createWebSocketLatency = createWebSocketLatency;
1032
1230
  exports.createWebSocketRequests = createWebSocketRequests;
1033
1231
  exports.createWebSocketResponder = createWebSocketResponder;
1232
+ exports.createWebSocketSendRate = createWebSocketSendRate;
1034
1233
  exports.createWebSocketState = createWebSocketState;
1035
1234
  exports.createWebSocketTopics = createWebSocketTopics;
1036
1235