onelobby 0.6.0 → 0.7.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.
Files changed (45) hide show
  1. package/README.md +2 -2
  2. package/dist/direct/hub.d.ts +2 -0
  3. package/dist/direct/hub.js +20 -1
  4. package/dist/direct/hub.js.map +1 -1
  5. package/dist/friendly.d.ts +1 -0
  6. package/dist/friendly.js +2 -0
  7. package/dist/friendly.js.map +1 -1
  8. package/dist/index.d.ts +0 -2
  9. package/dist/index.js +0 -2
  10. package/dist/index.js.map +1 -1
  11. package/dist/lobby.d.ts +29 -1
  12. package/dist/lobby.js +26 -6
  13. package/dist/lobby.js.map +1 -1
  14. package/dist/nostr/limits.d.ts +2 -0
  15. package/dist/nostr/limits.js +2 -0
  16. package/dist/nostr/limits.js.map +1 -1
  17. package/dist/nostr/relays.js +11 -1
  18. package/dist/nostr/relays.js.map +1 -1
  19. package/dist/onelobby.bundle.js +10 -10
  20. package/dist/onelobby.bundle.js.map +4 -4
  21. package/dist/onelobby.bundle.mjs +10 -10
  22. package/dist/onelobby.bundle.mjs.map +4 -4
  23. package/dist/onelobby.d.ts +1 -1
  24. package/dist/onelobby.global.js +79 -19
  25. package/dist/onelobby.global.js.map +3 -3
  26. package/dist/onelobby.global.min.js +10 -10
  27. package/dist/onelobby.global.min.js.map +4 -4
  28. package/dist/onelobby.js +10 -1
  29. package/dist/onelobby.js.map +1 -1
  30. package/dist/signaling.d.ts +2 -0
  31. package/dist/signaling.js.map +1 -1
  32. package/dist/socket.d.ts +15 -6
  33. package/dist/socket.js.map +1 -1
  34. package/dist/testing.d.ts +2 -0
  35. package/dist/testing.js +18 -2
  36. package/dist/testing.js.map +1 -1
  37. package/dist/transport.d.ts +2 -0
  38. package/dist/transport.js.map +1 -1
  39. package/dist/webrtc.d.ts +5 -0
  40. package/dist/webrtc.js +21 -2
  41. package/dist/webrtc.js.map +1 -1
  42. package/package.json +1 -1
  43. package/dist/deprecated.d.ts +0 -18
  44. package/dist/deprecated.js +0 -11
  45. package/dist/deprecated.js.map +0 -1
@@ -8,7 +8,7 @@ import { type RelayConnect } from "./nostr/relays.js";
8
8
  import { type Blueprint } from "./blueprint.js";
9
9
  import { type BrowserHostOptions } from "./direct/browser-host.js";
10
10
  export declare const LIB_NAME = "onelobby";
11
- export declare const LIB_VERSION = "0.6.0";
11
+ export declare const LIB_VERSION = "0.7.0";
12
12
  /** Where `rememberSeat` keeps the resume token: the subset of the Web Storage API it uses. */
13
13
  export interface SeatStorage {
14
14
  getItem(key: string): string | null;
@@ -1,4 +1,4 @@
1
- /*! onelobby 0.6.0 | MIT license */
1
+ /*! onelobby 0.7.0 | MIT license */
2
2
  "use strict";
3
3
  var OneLobby = (() => {
4
4
  var __defProp = Object.defineProperty;
@@ -29,8 +29,6 @@ var OneLobby = (() => {
29
29
  DEFAULT_NOSTR_STUN: () => DEFAULT_NOSTR_STUN,
30
30
  DEFAULT_READY_TIMEOUT_MS: () => DEFAULT_READY_TIMEOUT_MS,
31
31
  ErrorCodes: () => ErrorCodes,
32
- Keepalive: () => Keepalive,
33
- KeepaliveError: () => KeepaliveError,
34
32
  LIB_NAME: () => LIB_NAME,
35
33
  LIB_VERSION: () => LIB_VERSION,
36
34
  LOBBY_TAG: () => LOBBY_TAG,
@@ -2185,14 +2183,9 @@ var OneLobby = (() => {
2185
2183
  get connectedPeers() {
2186
2184
  return this.connectedPeerIds();
2187
2185
  }
2188
- /**
2189
- * Joiner: the link to the host as a WebSocket-shaped object (text by default), so WebSocket client code runs
2190
- * unchanged. It opens when `ready` resolves and closes when the session ends (`host_left` gives code 1001).
2191
- */
2192
2186
  asWebSocket(opts) {
2193
2187
  return joinerSocket(this, opts);
2194
2188
  }
2195
- /** Host: `handler(conn)` once per joiner (a WebSocket-shaped connection), the server-side mirror of asWebSocket(). */
2196
2189
  onConnection(handler, opts) {
2197
2190
  return hostConnections(this, handler, opts);
2198
2191
  }
@@ -2223,6 +2216,16 @@ var OneLobby = (() => {
2223
2216
  this.setPhase("in_game");
2224
2217
  if (this.mode === "lockstep") this.lockstep.hostStart();
2225
2218
  }
2219
+ /**
2220
+ * Changes your display name mid-session. The hub sanitises it (as at hello, at most 24 characters) and every member,
2221
+ * you included, gets `peerRenamed`. Names need not be unique. At most one rename per second; a refusal
2222
+ * (`bad_request` for an empty name, `rate_limited`) arrives on the kit's `error` event. Throws `unsupported` when
2223
+ * the hub is too old to rename.
2224
+ */
2225
+ async rename(name) {
2226
+ if (!(this.cfg.features ?? []).includes("set_name")) throw new OneLobbyError("unsupported", "this lobby's hub cannot rename players (update it)");
2227
+ this.signaling.send({ t: "set_name", name: String(name ?? "") });
2228
+ }
2226
2229
  async setMeta(meta) {
2227
2230
  if (!this.isHost) throw new Error("only the host can set metadata");
2228
2231
  this.meta = { ...meta };
@@ -2354,7 +2357,9 @@ var OneLobby = (() => {
2354
2357
  }, budget);
2355
2358
  }
2356
2359
  giveUpConnecting() {
2357
- const err = new OneLobbyError("ice_failed", `could not open a connection to the host${this.relayRetry ? " (direct or relayed)" : ""}`);
2360
+ const hostLink = this.links.get(this.hostPeerId);
2361
+ const diagnostics = hostLink ? hostLink.diagnostics?.() : this.lastHostLinkEnd ?? "no offer from the host ever arrived";
2362
+ const err = new OneLobbyError("ice_failed", `could not open a connection to the host${this.relayRetry ? " (direct or relayed)" : ""}`, diagnostics ? { diagnostics } : void 0);
2358
2363
  this.failReady(err);
2359
2364
  this.reportError(err);
2360
2365
  if (this.ended) return;
@@ -2403,8 +2408,11 @@ var OneLobby = (() => {
2403
2408
  this.emit("peerReady", this.roster.get(link.remoteId) ?? { peerId: link.remoteId, name: "" });
2404
2409
  this.flush(link.remoteId);
2405
2410
  }
2411
+ /** Why and how the joiner's last link to the host ended (shown when the join gives up). */
2412
+ lastHostLinkEnd = null;
2406
2413
  onLinkClosed(peerId, link, _reason) {
2407
2414
  if (this.links.get(peerId) !== link) return;
2415
+ if (!this.isHost && peerId === this.hostPeerId) this.lastHostLinkEnd = `link ended (${_reason}): ${link.diagnostics?.() ?? ""}`;
2408
2416
  this.links.delete(peerId);
2409
2417
  this.hiSentTo.delete(link);
2410
2418
  if (this.ended) return;
@@ -2671,7 +2679,12 @@ var OneLobby = (() => {
2671
2679
  this.resumeToken = st.resumeToken;
2672
2680
  this.seatSink?.(this.resumeToken);
2673
2681
  }
2682
+ const before = this.roster;
2674
2683
  this.roster = new Map(st.peers.map((p) => [p.peerId, seatName(p)]));
2684
+ for (const [id, p] of this.roster) {
2685
+ const old = before.get(id);
2686
+ if (old && old.name !== p.name) this.emit("peerRenamed", p, old.name);
2687
+ }
2675
2688
  if (st.hostId !== this.hostPeerId && !this.isHost) this.setHost(st.hostId);
2676
2689
  this.setPhase(st.phase);
2677
2690
  break;
@@ -3067,6 +3080,8 @@ registerProcessor('onelobby-capture', OneLobbyCapture);
3067
3080
  opened = 0;
3068
3081
  early = [];
3069
3082
  extra;
3083
+ born = Date.now();
3084
+ trace = [];
3070
3085
  /** `rtc`: the RTCPeerConnection class to use (default the global one; in Node, node-datachannel's polyfill). */
3071
3086
  constructor(opts, rtc = globalThis.RTCPeerConnection) {
3072
3087
  this.remoteId = opts.remoteId;
@@ -3082,16 +3097,28 @@ registerProcessor('onelobby-capture', OneLobbyCapture);
3082
3097
  }
3083
3098
  this.pc = new rtc(config);
3084
3099
  this.pc.onicecandidate = (e) => {
3085
- if (e.candidate) this.onSignal?.({ ice: e.candidate.toJSON() });
3100
+ if (e.candidate) {
3101
+ this.note(`out ice ${e.candidate.candidate}`);
3102
+ this.onSignal?.({ ice: e.candidate.toJSON() });
3103
+ }
3086
3104
  };
3087
3105
  this.pc.onconnectionstatechange = () => {
3088
3106
  const s = this.pc.connectionState;
3107
+ this.note(`connection ${s}`);
3089
3108
  if (s === "failed" || s === "closed" || s === "disconnected") {
3090
3109
  if (s === "disconnected") setTimeout(() => this.pc.connectionState === "disconnected" && this.fail("disconnected"), 4e3);
3091
3110
  else this.fail(s);
3092
3111
  }
3093
3112
  };
3094
3113
  }
3114
+ note(what) {
3115
+ if (this.trace.length < 40) this.trace.push(`+${Date.now() - this.born}ms ${what}`);
3116
+ }
3117
+ /** What a "could not connect" report adds: the peer connection's states and the signals seen so far. */
3118
+ diagnostics() {
3119
+ const pc = this.pc;
3120
+ return `connection=${pc.connectionState} ice=${pc.iceConnectionState} gathering=${pc.iceGatheringState} signaling=${pc.signalingState} haveRemote=${this.haveRemote} pendingIce=${this.pendingIce.length}; ${this.trace.join("; ")}`;
3121
+ }
3095
3122
  /**
3096
3123
  * The offerer creates the channels just before its offer, the answerer once the remote offer is set. Creating them
3097
3124
  * any earlier makes libdatachannel (node-datachannel) start an offer of its own on the answering side, which then
@@ -3110,7 +3137,7 @@ registerProcessor('onelobby-capture', OneLobbyCapture);
3110
3137
  void this.reportPath();
3111
3138
  }
3112
3139
  };
3113
- ch.onclose = () => this.fail(`channel ${name} closed`);
3140
+ if (name === "reliable") ch.onclose = () => this.fail(`channel ${name} closed`);
3114
3141
  ch.onmessage = (e) => {
3115
3142
  const d = e.data;
3116
3143
  const data = d instanceof ArrayBuffer ? new Uint8Array(d) : ArrayBuffer.isView(d) ? new Uint8Array(d.buffer, d.byteOffset, d.byteLength) : new Uint8Array(d);
@@ -3129,6 +3156,7 @@ registerProcessor('onelobby-capture', OneLobbyCapture);
3129
3156
  this.openChannels();
3130
3157
  const offer = await this.pc.createOffer();
3131
3158
  await this.pc.setLocalDescription(offer);
3159
+ this.note("out offer");
3132
3160
  this.onSignal?.({ sdp: { type: "offer", sdp: offer.sdp ?? "" } });
3133
3161
  } catch (e) {
3134
3162
  this.fail(`offer failed: ${String(e)}`);
@@ -3136,6 +3164,7 @@ registerProcessor('onelobby-capture', OneLobbyCapture);
3136
3164
  })();
3137
3165
  }
3138
3166
  async handleSignal(s) {
3167
+ this.note(`in ${s.sdp ? s.sdp.type : `ice ${s.ice?.candidate}`}`);
3139
3168
  try {
3140
3169
  if (s.sdp) {
3141
3170
  await this.pc.setRemoteDescription({ type: s.sdp.type, sdp: s.sdp.sdp });
@@ -3149,6 +3178,7 @@ registerProcessor('onelobby-capture', OneLobbyCapture);
3149
3178
  this.openChannels();
3150
3179
  const answer = await this.pc.createAnswer();
3151
3180
  await this.pc.setLocalDescription(answer);
3181
+ this.note("out answer");
3152
3182
  this.onSignal?.({ sdp: { type: "answer", sdp: answer.sdp ?? "" } });
3153
3183
  }
3154
3184
  }
@@ -3304,6 +3334,7 @@ registerProcessor('onelobby-capture', OneLobbyCapture);
3304
3334
  var JOINER_FIRST_RELAY_MS = 1e4;
3305
3335
  var RELAY_REST_WAIT_MS = 1500;
3306
3336
  var RELAY_REST_POLL_MS = 50;
3337
+ var DEAD_RELAY_DIALS = 4;
3307
3338
 
3308
3339
  // src/nostr/link.ts
3309
3340
  var utf8 = new TextEncoder();
@@ -3672,7 +3703,7 @@ registerProcessor('onelobby-capture', OneLobbyCapture);
3672
3703
  var RelayPool = class {
3673
3704
  constructor(o) {
3674
3705
  this.o = o;
3675
- this.relays = o.urls.map((url) => ({ url, ws: null, open: false, attempted: false, skipped: false, backoff: 1e3, cancelRetry: null }));
3706
+ this.relays = o.urls.map((url) => ({ url, ws: null, open: false, attempted: false, skipped: false, everOpen: false, fails: 0, backoff: 1e3, cancelRetry: null }));
3676
3707
  }
3677
3708
  o;
3678
3709
  relays;
@@ -3749,6 +3780,8 @@ registerProcessor('onelobby-capture', OneLobbyCapture);
3749
3780
  if (r.ws !== ws) return;
3750
3781
  r.open = true;
3751
3782
  r.attempted = true;
3783
+ r.everOpen = true;
3784
+ r.fails = 0;
3752
3785
  r.backoff = 1e3;
3753
3786
  const since = Math.floor(this.o.wall() / 1e3) - SINCE_SLACK_S;
3754
3787
  this.trySend(r, JSON.stringify(["REQ", this.sub, { kinds: [NOSTR_KIND], "#t": [this.o.topic], since }]));
@@ -3783,6 +3816,11 @@ registerProcessor('onelobby-capture', OneLobbyCapture);
3783
3816
  }
3784
3817
  retryLater(r) {
3785
3818
  if (this.closed || r.skipped || r.cancelRetry) return;
3819
+ if (!r.everOpen) r.fails++;
3820
+ if (!r.everOpen && r.fails >= DEAD_RELAY_DIALS && this.connected > 0) {
3821
+ this.skip(r);
3822
+ return;
3823
+ }
3786
3824
  const wait = r.backoff;
3787
3825
  r.backoff = Math.min(3e4, r.backoff * 2);
3788
3826
  r.cancelRetry = this.o.clock.setTimeout(() => {
@@ -4871,6 +4909,7 @@ ${name} ${g.read()}
4871
4909
  detachedAt = 0;
4872
4910
  token = "";
4873
4911
  ip = "";
4912
+ renames = null;
4874
4913
  };
4875
4914
  var HubLobby = class {
4876
4915
  code = "";
@@ -4900,7 +4939,7 @@ ${name} ${g.read()}
4900
4939
  return this.members.find((m) => m.peerId === peerId);
4901
4940
  }
4902
4941
  };
4903
- var KNOWN_TYPES = /* @__PURE__ */ new Set(["hello", "create", "join", "list", "leave", "signal", "meta", "phase", "kick", "approve", "ping", "host_changed", "path"]);
4942
+ var KNOWN_TYPES = /* @__PURE__ */ new Set(["hello", "create", "join", "list", "leave", "signal", "meta", "phase", "kick", "approve", "ping", "host_changed", "path", "set_name"]);
4904
4943
  var str2 = (o, k) => typeof o[k] === "string" ? o[k] : null;
4905
4944
  var int2 = (o, k) => {
4906
4945
  const v = o[k];
@@ -5102,6 +5141,8 @@ ${name} ${g.read()}
5102
5141
  return this.claimHost(s, msg);
5103
5142
  case "path":
5104
5143
  return this.pathReport(s, msg);
5144
+ case "set_name":
5145
+ return this.setName(s, msg);
5105
5146
  default:
5106
5147
  return;
5107
5148
  }
@@ -5148,7 +5189,8 @@ ${name} ${g.read()}
5148
5189
  t: "welcome",
5149
5190
  ice: this.iceList(s),
5150
5191
  serverTime: this.now(),
5151
- limits: { maxMsg: this.o.maxMessageBytes, maxPlayers: this.o.hardMaxPlayers }
5192
+ limits: { maxMsg: this.o.maxMessageBytes, maxPlayers: this.o.hardMaxPlayers },
5193
+ features: ["set_name"]
5152
5194
  };
5153
5195
  if (resumed) w.peerId = resumed.peerId;
5154
5196
  this.send(s, w);
@@ -5413,6 +5455,18 @@ ${name} ${g.read()}
5413
5455
  lobby.meta = meta.meta;
5414
5456
  this.broadcastLobby(lobby);
5415
5457
  }
5458
+ setName(s, m) {
5459
+ if (!s.lobby || !s.member) return this.error(s, "bad_request", "not in a lobby");
5460
+ const name = typeof m.name === "string" ? sanitize(m.name, NAME_MAX) : "";
5461
+ if (!name) return this.error(s, "bad_request", "name must be a non-empty string", field("name"));
5462
+ if (name === s.member.name) return;
5463
+ const now = this.now();
5464
+ const rate = s.member.renames ??= new RateWindow(this.scale, 1e3);
5465
+ if (!rate.tryHit(now)) return this.rateLimited(s, "set_name", rate.retryAfterMs(now), null);
5466
+ s.member.name = name;
5467
+ s.name = name;
5468
+ this.broadcastLobby(s.lobby);
5469
+ }
5416
5470
  hostPhase(s, m) {
5417
5471
  const lobby = this.requireHost(s);
5418
5472
  if (!lobby) return;
@@ -5742,7 +5796,7 @@ ${name} ${g.read()}
5742
5796
 
5743
5797
  // src/onelobby.ts
5744
5798
  var LIB_NAME = "onelobby";
5745
- var LIB_VERSION = "0.6.0";
5799
+ var LIB_VERSION = "0.7.0";
5746
5800
  var DEFAULT_READY_TIMEOUT_MS = 15e3;
5747
5801
  function hasMic() {
5748
5802
  return typeof navigator !== "undefined" && !!navigator.mediaDevices?.getUserMedia && typeof AudioWorkletNode !== "undefined";
@@ -5952,6 +6006,7 @@ ${name} ${g.read()}
5952
6006
  signaling: sig,
5953
6007
  state,
5954
6008
  ice: welcome.ice ?? [],
6009
+ features: welcome.features ?? [],
5955
6010
  links: direct?.links ?? this.linkFactory,
5956
6011
  direct: !!direct,
5957
6012
  clock: this.clock,
@@ -6180,6 +6235,13 @@ ${name} ${g.read()}
6180
6235
  return out;
6181
6236
  }
6182
6237
  function guardLobby(lobby, bp) {
6238
+ const rename = lobby.rename.bind(lobby);
6239
+ lobby.rename = async (name) => {
6240
+ if (bp.players.identity === "anonymous") throw new OneLobbyError("identity_anonymous", "this game uses anonymous players: no display names");
6241
+ if (String(name ?? "").trim().length > bp.players.maxNameLength)
6242
+ throw new BlueprintError("displayName", "length", `must be at most ${bp.players.maxNameLength} characters`);
6243
+ return rename(name);
6244
+ };
6183
6245
  lobby.voice.mode = bp.chat.voiceMode;
6184
6246
  if (!bp.chat.voice) {
6185
6247
  const voice = lobby.voice;
@@ -6225,6 +6287,7 @@ ${name} ${g.read()}
6225
6287
  DirectUnreachable: "direct_unreachable",
6226
6288
  BadAddress: "bad_address",
6227
6289
  PortInUse: "port_in_use",
6290
+ Unsupported: "unsupported",
6228
6291
  ConnectFailed: "connect_failed",
6229
6292
  Closed: "closed",
6230
6293
  ServerLost: "server_lost",
@@ -6265,6 +6328,7 @@ ${name} ${g.read()}
6265
6328
  ice_failed: { message: "Couldn't connect to the other players.", hint: "Some networks (school, work, public Wi-Fi) block game connections. Try another network or a phone hotspot." },
6266
6329
  no_server: { message: "This game has no lobby server address yet.", hint: "Enter one in the multiplayer settings, or host from your own computer with a join code." },
6267
6330
  timeout: { message: "The connection took too long.", hint: "Check your internet connection, then try again." },
6331
+ unsupported: { message: "The host's game is too old for that.", hint: "Ask the host to update the game." },
6268
6332
  voice_disabled: { message: "Voice chat is turned off in this game." },
6269
6333
  chat_disabled: { message: "Text chat is turned off in this game." },
6270
6334
  pause_disabled: { message: "Pausing is turned off in this game." },
@@ -6353,10 +6417,6 @@ ${name} ${g.read()}
6353
6417
  return v ? normalizeCode(v) : null;
6354
6418
  }
6355
6419
 
6356
- // src/deprecated.ts
6357
- var Keepalive = OneLobby;
6358
- var KeepaliveError = OneLobbyError;
6359
-
6360
6420
  // src/ui/controller.ts
6361
6421
  var enc4 = new TextEncoder();
6362
6422
  var dec3 = new TextDecoder();