onelobby 0.2.1 → 0.3.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.
@@ -5,7 +5,7 @@ import { type LobbySummary } from "./signaling.js";
5
5
  import { type LinkFactory, type SocketFactory } from "./transport.js";
6
6
  import type { VoiceCodec, FrameSource, AudioSink } from "./voice-types.js";
7
7
  export declare const LIB_NAME = "onelobby";
8
- export declare const LIB_VERSION = "0.2.1";
8
+ export declare const LIB_VERSION = "0.3.0";
9
9
  /** Where `rememberSeat` keeps the resume token: the subset of the Web Storage API it uses. */
10
10
  export interface SeatStorage {
11
11
  getItem(key: string): string | null;
@@ -1,4 +1,4 @@
1
- /*! onelobby 0.2.1 | MIT license */
1
+ /*! onelobby 0.3.0 | MIT license */
2
2
  "use strict";
3
3
  var OneLobby = (() => {
4
4
  var __defProp = Object.defineProperty;
@@ -1146,8 +1146,19 @@ var OneLobby = (() => {
1146
1146
  cancelPing = null;
1147
1147
  pingN = 0;
1148
1148
  closed = false;
1149
- onMessage = null;
1149
+ handler = null;
1150
+ // Messages with no waiter and no handler yet. The reply that resolves join/create can arrive in the same socket
1151
+ // read as the host's first offer; both are dispatched before the awaiting code builds its Lobby and sets a handler.
1152
+ backlog = [];
1150
1153
  onClose = null;
1154
+ get onMessage() {
1155
+ return this.handler;
1156
+ }
1157
+ /** Setting a handler first delivers, in order, what arrived while there was none. */
1158
+ set onMessage(h) {
1159
+ this.handler = h;
1160
+ while (h && this.handler === h && this.backlog.length) h(this.backlog.shift());
1161
+ }
1151
1162
  connect() {
1152
1163
  return new Promise((resolve, reject) => {
1153
1164
  let s;
@@ -1187,7 +1198,8 @@ var OneLobby = (() => {
1187
1198
  else w.resolve(m);
1188
1199
  return;
1189
1200
  }
1190
- this.onMessage?.(m);
1201
+ if (this.handler) this.handler(m);
1202
+ else if (this.backlog.length < 256) this.backlog.push(m);
1191
1203
  };
1192
1204
  s.onclose = () => {
1193
1205
  if (!this.lastRx) reject(new OneLobbyError("connect_failed", `cannot reach ${this.url}`));
@@ -1798,6 +1810,8 @@ var OneLobby = (() => {
1798
1810
  cfg;
1799
1811
  roster = /* @__PURE__ */ new Map();
1800
1812
  links = /* @__PURE__ */ new Map();
1813
+ /** ICE from a peer that has no link yet: its candidates can overtake its offer (node-datachannel). */
1814
+ earlyIce = /* @__PURE__ */ new Map();
1801
1815
  connected = /* @__PURE__ */ new Set();
1802
1816
  // host: peers that said hi
1803
1817
  hiSentTo = /* @__PURE__ */ new Set();
@@ -2393,13 +2407,25 @@ var OneLobby = (() => {
2393
2407
  }
2394
2408
  async onSignal(from, s) {
2395
2409
  let link = this.links.get(from);
2396
- if (s.sdp?.type === "offer") {
2397
- link = this.makeLink(from);
2398
- }
2399
- if (!link) return;
2400
2410
  const payload = {};
2401
2411
  if (s.sdp) payload.sdp = s.sdp;
2402
2412
  if (s.ice) payload.ice = s.ice;
2413
+ if (s.sdp?.type === "offer") {
2414
+ link = this.makeLink(from);
2415
+ const early = this.earlyIce.get(from) ?? [];
2416
+ this.earlyIce.delete(from);
2417
+ const done = link.handleSignal(payload);
2418
+ for (const e of early) void link.handleSignal(e);
2419
+ await done;
2420
+ return;
2421
+ }
2422
+ if (!link) {
2423
+ if (!payload.ice || this.ended || !this.roster.has(from)) return;
2424
+ const q = this.earlyIce.get(from) ?? [];
2425
+ if (q.length < 64) q.push(payload);
2426
+ this.earlyIce.set(from, q);
2427
+ return;
2428
+ }
2403
2429
  await link.handleSignal(payload);
2404
2430
  }
2405
2431
  /**
@@ -2414,6 +2440,7 @@ var OneLobby = (() => {
2414
2440
  return;
2415
2441
  }
2416
2442
  if (peer) this.roster.delete(peerId);
2443
+ this.earlyIce.delete(peerId);
2417
2444
  const link = this.links.get(peerId);
2418
2445
  if (link && peerId !== this.hostPeerId) {
2419
2446
  link.onMessage = null;
@@ -2518,6 +2545,7 @@ var OneLobby = (() => {
2518
2545
  l.close();
2519
2546
  }
2520
2547
  this.links.clear();
2548
+ this.earlyIce.clear();
2521
2549
  this.lingering.clear();
2522
2550
  this.signaling.onClose = null;
2523
2551
  this.signaling.onMessage = null;
@@ -2691,8 +2719,14 @@ registerProcessor('onelobby-capture', OneLobbyCapture);
2691
2719
  pc;
2692
2720
  channels = {};
2693
2721
  pendingIce = [];
2722
+ /**
2723
+ * Set once setRemoteDescription resolves. Not `pc.remoteDescription`: node-datachannel's polyfill returns an object
2724
+ * there before any is set, and its addIceCandidate throws until one is.
2725
+ */
2726
+ haveRemote = false;
2694
2727
  closed = false;
2695
2728
  opened = 0;
2729
+ early = [];
2696
2730
  extra;
2697
2731
  /** `rtc`: the RTCPeerConnection class to use (default the global one; in Node, node-datachannel's polyfill). */
2698
2732
  constructor(opts, rtc = globalThis.RTCPeerConnection) {
@@ -2718,6 +2752,14 @@ registerProcessor('onelobby-capture', OneLobbyCapture);
2718
2752
  else this.fail(s);
2719
2753
  }
2720
2754
  };
2755
+ }
2756
+ /**
2757
+ * The offerer creates the channels just before its offer, the answerer once the remote offer is set. Creating them
2758
+ * any earlier makes libdatachannel (node-datachannel) start an offer of its own on the answering side, which then
2759
+ * collides with the real one (both ICE agents controlling, the link never opens).
2760
+ */
2761
+ openChannels() {
2762
+ if (this.channels.reliable) return;
2721
2763
  for (const name of ["reliable", "fast", "voice"]) {
2722
2764
  const ch = this.pc.createDataChannel(name, CHANNEL_INIT[name]);
2723
2765
  ch.binaryType = "arraybuffer";
@@ -2725,6 +2767,7 @@ registerProcessor('onelobby-capture', OneLobbyCapture);
2725
2767
  if (++this.opened === 3 && !this.closed) {
2726
2768
  this.isOpen = true;
2727
2769
  this.onOpen?.();
2770
+ for (const [n, data] of this.early.splice(0)) if (!this.closed) this.onMessage?.(n, data);
2728
2771
  void this.reportPath();
2729
2772
  }
2730
2773
  };
@@ -2732,6 +2775,10 @@ registerProcessor('onelobby-capture', OneLobbyCapture);
2732
2775
  ch.onmessage = (e) => {
2733
2776
  const d = e.data;
2734
2777
  const data = d instanceof ArrayBuffer ? new Uint8Array(d) : ArrayBuffer.isView(d) ? new Uint8Array(d.buffer, d.byteOffset, d.byteLength) : new Uint8Array(d);
2778
+ if (!this.isOpen) {
2779
+ if (this.early.length < 1e3) this.early.push([name, data]);
2780
+ return;
2781
+ }
2735
2782
  this.onMessage?.(name, data);
2736
2783
  };
2737
2784
  this.channels[name] = ch;
@@ -2740,6 +2787,7 @@ registerProcessor('onelobby-capture', OneLobbyCapture);
2740
2787
  startOffer() {
2741
2788
  void (async () => {
2742
2789
  try {
2790
+ this.openChannels();
2743
2791
  const offer = await this.pc.createOffer();
2744
2792
  await this.pc.setLocalDescription(offer);
2745
2793
  this.onSignal?.({ sdp: { type: "offer", sdp: offer.sdp ?? "" } });
@@ -2752,19 +2800,21 @@ registerProcessor('onelobby-capture', OneLobbyCapture);
2752
2800
  try {
2753
2801
  if (s.sdp) {
2754
2802
  await this.pc.setRemoteDescription({ type: s.sdp.type, sdp: s.sdp.sdp });
2803
+ this.haveRemote = true;
2755
2804
  for (const c of this.pendingIce.splice(0)) await this.pc.addIceCandidate(c);
2756
2805
  if (this.extra.length) {
2757
2806
  const mid = /^a=mid:(\S+)/m.exec(s.sdp.sdp)?.[1] ?? "0";
2758
2807
  for (const candidate of this.extra.splice(0)) await this.pc.addIceCandidate({ candidate, sdpMid: mid, sdpMLineIndex: 0 });
2759
2808
  }
2760
2809
  if (s.sdp.type === "offer") {
2810
+ this.openChannels();
2761
2811
  const answer = await this.pc.createAnswer();
2762
2812
  await this.pc.setLocalDescription(answer);
2763
2813
  this.onSignal?.({ sdp: { type: "answer", sdp: answer.sdp ?? "" } });
2764
2814
  }
2765
2815
  }
2766
2816
  if (s.ice) {
2767
- if (this.pc.remoteDescription) await this.pc.addIceCandidate(s.ice);
2817
+ if (this.haveRemote) await this.pc.addIceCandidate(s.ice);
2768
2818
  else this.pendingIce.push(s.ice);
2769
2819
  }
2770
2820
  } catch (e) {
@@ -2930,7 +2980,7 @@ registerProcessor('onelobby-capture', OneLobbyCapture);
2930
2980
 
2931
2981
  // src/onelobby.ts
2932
2982
  var LIB_NAME = "onelobby";
2933
- var LIB_VERSION = "0.2.1";
2983
+ var LIB_VERSION = "0.3.0";
2934
2984
  var DEFAULT_READY_TIMEOUT_MS = 15e3;
2935
2985
  function hasMic() {
2936
2986
  return typeof navigator !== "undefined" && !!navigator.mediaDevices?.getUserMedia && typeof AudioWorkletNode !== "undefined";