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