onelobby 0.5.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.
- package/README.md +2 -2
- package/blueprint.schema.json +8 -1
- package/dist/blueprint.d.ts +4 -0
- package/dist/blueprint.js +5 -0
- package/dist/blueprint.js.map +1 -1
- package/dist/codec.d.ts +23 -0
- package/dist/codec.js +28 -0
- package/dist/codec.js.map +1 -1
- package/dist/direct/browser-host.d.ts +46 -0
- package/dist/direct/browser-host.js +93 -0
- package/dist/direct/browser-host.js.map +1 -0
- package/dist/direct/hub-host.d.ts +34 -23
- package/dist/direct/hub-host.js +75 -59
- package/dist/direct/hub-host.js.map +1 -1
- package/dist/direct/hub-local.d.ts +22 -0
- package/dist/direct/hub-local.js +46 -0
- package/dist/direct/hub-local.js.map +1 -0
- package/dist/direct/hub.d.ts +39 -10
- package/dist/direct/hub.js +134 -24
- package/dist/direct/hub.js.map +1 -1
- package/dist/direct/index.d.ts +36 -3
- package/dist/direct/index.js +146 -20
- package/dist/direct/index.js.map +1 -1
- package/dist/direct/metrics.d.ts +12 -0
- package/dist/direct/metrics.js +45 -0
- package/dist/direct/metrics.js.map +1 -0
- package/dist/direct/portmap.d.ts +9 -2
- package/dist/direct/portmap.js +7 -2
- package/dist/direct/portmap.js.map +1 -1
- package/dist/direct/sha1.d.ts +16 -0
- package/dist/direct/sha1.js +111 -0
- package/dist/direct/sha1.js.map +1 -0
- package/dist/direct-address.d.ts +2 -2
- package/dist/direct-address.js.map +1 -1
- package/dist/friendly.d.ts +24 -0
- package/dist/friendly.js +30 -0
- package/dist/friendly.js.map +1 -1
- package/dist/index.d.ts +8 -4
- package/dist/index.js +6 -4
- package/dist/index.js.map +1 -1
- package/dist/internals.d.ts +9 -0
- package/dist/internals.js +9 -0
- package/dist/internals.js.map +1 -1
- package/dist/join-code-kind.d.ts +10 -0
- package/dist/join-code-kind.js +10 -0
- package/dist/join-code-kind.js.map +1 -0
- package/dist/joincode.d.ts +10 -1
- package/dist/joincode.js +23 -3
- package/dist/joincode.js.map +1 -1
- package/dist/lobby.d.ts +94 -5
- package/dist/lobby.js +177 -18
- package/dist/lobby.js.map +1 -1
- package/dist/lockstep.d.ts +8 -0
- package/dist/lockstep.js +23 -2
- package/dist/lockstep.js.map +1 -1
- package/dist/nostr/crypto.d.ts +20 -0
- package/dist/nostr/crypto.js +78 -0
- package/dist/nostr/crypto.js.map +1 -0
- package/dist/nostr/host.d.ts +66 -0
- package/dist/nostr/host.js +196 -0
- package/dist/nostr/host.js.map +1 -0
- package/dist/nostr/limits.d.ts +33 -0
- package/dist/nostr/limits.js +38 -0
- package/dist/nostr/limits.js.map +1 -0
- package/dist/nostr/link.d.ts +70 -0
- package/dist/nostr/link.js +180 -0
- package/dist/nostr/link.js.map +1 -0
- package/dist/nostr/relays.d.ts +79 -0
- package/dist/nostr/relays.js +325 -0
- package/dist/nostr/relays.js.map +1 -0
- package/dist/nostr/room.d.ts +43 -0
- package/dist/nostr/room.js +104 -0
- package/dist/nostr/room.js.map +1 -0
- package/dist/nostr/secp256k1.d.ts +10 -0
- package/dist/nostr/secp256k1.js +171 -0
- package/dist/nostr/secp256k1.js.map +1 -0
- package/dist/nostr/socket.d.ts +48 -0
- package/dist/nostr/socket.js +141 -0
- package/dist/nostr/socket.js.map +1 -0
- package/dist/onelobby.bundle.js +146 -0
- package/dist/onelobby.bundle.js.map +7 -0
- package/dist/onelobby.bundle.mjs +11 -5
- package/dist/onelobby.bundle.mjs.map +4 -4
- package/dist/onelobby.d.ts +31 -3
- package/dist/onelobby.global.js +2673 -60
- package/dist/onelobby.global.js.map +4 -4
- package/dist/onelobby.global.min.js +11 -5
- package/dist/onelobby.global.min.js.map +4 -4
- package/dist/onelobby.js +77 -14
- package/dist/onelobby.js.map +1 -1
- package/dist/pause.d.ts +85 -0
- package/dist/pause.js +160 -0
- package/dist/pause.js.map +1 -0
- package/dist/signaling.d.ts +2 -0
- package/dist/signaling.js.map +1 -1
- package/dist/socket.d.ts +15 -6
- package/dist/socket.js.map +1 -1
- package/dist/testing.d.ts +2 -0
- package/dist/testing.js +18 -2
- package/dist/testing.js.map +1 -1
- package/dist/transport.d.ts +5 -1
- package/dist/transport.js.map +1 -1
- package/dist/webrtc.d.ts +6 -0
- package/dist/webrtc.js +27 -3
- package/dist/webrtc.js.map +1 -1
- package/package.json +1 -1
- package/dist/deprecated.d.ts +0 -18
- package/dist/deprecated.js +0 -11
- package/dist/deprecated.js.map +0 -1
package/dist/lobby.js
CHANGED
|
@@ -4,6 +4,7 @@ import { Reassembler, splitChunks } from "./chunking.js";
|
|
|
4
4
|
import { Emitter } from "./emitter.js";
|
|
5
5
|
import { Lockstep } from "./lockstep.js";
|
|
6
6
|
import { claimWinsWrapped, pickNewHost } from "./migration.js";
|
|
7
|
+
import { Pause } from "./pause.js";
|
|
7
8
|
import { OneLobbyError } from "./signaling.js";
|
|
8
9
|
import { Voice } from "./voice.js";
|
|
9
10
|
import { hostConnections, joinerSocket } from "./socket.js";
|
|
@@ -11,6 +12,8 @@ import { hostConnections, joinerSocket } from "./socket.js";
|
|
|
11
12
|
export const MIN_PEER_TIMEOUT_MS = 6000;
|
|
12
13
|
/** Host: how long a kicked / disconnected peer's link stays open for the hub's notice to reach it first. */
|
|
13
14
|
export const KICK_LINGER_MS = 3000;
|
|
15
|
+
/** How long an offer held during a possible migration stays answerable. */
|
|
16
|
+
const HELD_OFFER_MS = 20_000;
|
|
14
17
|
/** The name shown for a peer that chose none: "Player N", N = its seat (peer id, join order). */
|
|
15
18
|
export function seatName(p) {
|
|
16
19
|
const n = typeof p.name === "string" ? p.name.trim() : "";
|
|
@@ -31,31 +34,62 @@ export class Lobby extends Emitter {
|
|
|
31
34
|
tickRate;
|
|
32
35
|
lockstep;
|
|
33
36
|
chat;
|
|
37
|
+
/** The shared co-op pause (docs/recipes/pause.md). */
|
|
38
|
+
pause;
|
|
34
39
|
voice;
|
|
35
40
|
localPeerId;
|
|
36
41
|
hostPeerId;
|
|
37
42
|
phase;
|
|
38
43
|
meta;
|
|
44
|
+
/** The lobby's name as the server keeps it (host()'s `name`, sanitised); "" if the server sends none. */
|
|
45
|
+
name;
|
|
39
46
|
resumeToken;
|
|
40
47
|
/** Last measured round trip to the host in ms (joiners), or 0. */
|
|
41
48
|
rtt = 0;
|
|
42
49
|
/**
|
|
43
|
-
* Direct-IP host only (onelobby/direct): the code friends paste to join, e.g. "6B01-RGFE-P2B" (docs/direct-ip.md
|
|
50
|
+
* Direct-IP host only (onelobby/direct): the code friends paste to join, e.g. "6B01-RGFE-P2B" (docs/design/direct-ip.md
|
|
44
51
|
* s.2a). Show it with joinCodeKind. Empty when the machine has no IPv4 address, and on joiners.
|
|
45
52
|
*/
|
|
46
53
|
joinCode = "";
|
|
47
|
-
/**
|
|
54
|
+
/**
|
|
55
|
+
* "nostr": a version 1 code that works through public Nostr relays (spec/nostr-signaling.md; the default for a direct
|
|
56
|
+
* host). "internet": the router forwarded the port and the code carries its public address; "same_network": the LAN
|
|
57
|
+
* address. "none": no code.
|
|
58
|
+
*/
|
|
48
59
|
joinCodeKind = "none";
|
|
60
|
+
/**
|
|
61
|
+
* Direct-IP host only: the IP code (address and port, version 0), with `directCodeKind` "internet" or "same_network".
|
|
62
|
+
* Equal to `joinCode` when Nostr is off; otherwise the code to hand out for LAN play or when the router port is open.
|
|
63
|
+
*/
|
|
64
|
+
directCode = "";
|
|
65
|
+
directCodeKind = "none";
|
|
49
66
|
/** Direct-IP host only: this machine's addresses with the port, for an "Advanced" fold-out. */
|
|
50
67
|
directAddresses = [];
|
|
51
68
|
/** Direct-IP host only: how the router port-open went ("UPnP", "no UPnP gateway answered; ...", "off"). */
|
|
52
69
|
portMapping = "";
|
|
70
|
+
/** Direct-IP host only: the one startup line (bound port, join code, port mapping); see `formatHostLog` in onelobby/direct. */
|
|
71
|
+
directHostLog = "";
|
|
72
|
+
/** Browser host only (`kit.hostNostr`): true while the host tab is hidden (see the `hostHiddenChanged` event). */
|
|
73
|
+
hostHidden = false;
|
|
74
|
+
/** @internal kit.hostNostr: the tab's visibility changed. */
|
|
75
|
+
setHostHidden(hidden) {
|
|
76
|
+
if (this.hostHidden === hidden)
|
|
77
|
+
return;
|
|
78
|
+
this.hostHidden = hidden;
|
|
79
|
+
this.emit("hostHiddenChanged", hidden);
|
|
80
|
+
}
|
|
81
|
+
/** @internal onelobby/direct: fires `joinCodeChanged` with the current code. */
|
|
82
|
+
notifyJoinCodeChanged() {
|
|
83
|
+
this.emit("joinCodeChanged", this.joinCode, this.joinCodeKind);
|
|
84
|
+
}
|
|
53
85
|
signaling;
|
|
54
86
|
cfg;
|
|
55
87
|
roster = new Map();
|
|
56
88
|
links = new Map();
|
|
57
89
|
/** ICE from a peer that has no link yet: its candidates can overtake its offer (node-datachannel). */
|
|
58
90
|
earlyIce = new Map();
|
|
91
|
+
/** Offers from roster members that arrived while the host link was open (see offerVerdict), by sender. */
|
|
92
|
+
heldOffers = new Map();
|
|
59
93
|
connected = new Set(); // host: peers that said hi
|
|
60
94
|
hiSentTo = new Set();
|
|
61
95
|
epoch = 0;
|
|
@@ -77,6 +111,8 @@ export class Lobby extends Emitter {
|
|
|
77
111
|
lastHeard = new Map();
|
|
78
112
|
/** Host: links of kicked / disconnected peers, kept open briefly (removePeer `linger`). */
|
|
79
113
|
lingering = new Set();
|
|
114
|
+
/** Host: joiners that joined but are not ready yet, with the cancel of their readyTimeoutMs timer (peerFailed). */
|
|
115
|
+
notReady = new Map();
|
|
80
116
|
/** Host: per-peer fixed window for rateLimit. */
|
|
81
117
|
rate = new Map();
|
|
82
118
|
/**
|
|
@@ -107,6 +143,7 @@ export class Lobby extends Emitter {
|
|
|
107
143
|
this.hostPeerId = st.hostId;
|
|
108
144
|
this.phase = st.phase;
|
|
109
145
|
this.meta = { ...st.meta };
|
|
146
|
+
this.name = st.name ?? "";
|
|
110
147
|
this.resumeToken = st.resumeToken;
|
|
111
148
|
for (const p of st.peers)
|
|
112
149
|
this.roster.set(p.peerId, seatName(p));
|
|
@@ -131,6 +168,11 @@ export class Lobby extends Emitter {
|
|
|
131
168
|
this.lockstep = new Lockstep(this.hub, { hashEveryTicks: cfg.hashEveryTicks });
|
|
132
169
|
this.chat = new Chat(this.hub, cfg.wall, (id) => this.voice.isMutedPeer(id));
|
|
133
170
|
this.voice = new Voice(this.hub, cfg.voice);
|
|
171
|
+
this.pause = new Pause(this.hub, {
|
|
172
|
+
phase: () => this.phase,
|
|
173
|
+
hold: (held) => this.lockstep.hold(held),
|
|
174
|
+
hasPeer: (id) => this.roster.has(id),
|
|
175
|
+
});
|
|
134
176
|
this.lockstep.on("started", () => this.setPhase("in_game"));
|
|
135
177
|
this.lockstep.on("error", (e) => this.reportError(e));
|
|
136
178
|
this.signaling.onMessage = (m) => this.onSignaling(m);
|
|
@@ -162,14 +204,9 @@ export class Lobby extends Emitter {
|
|
|
162
204
|
get connectedPeers() {
|
|
163
205
|
return this.connectedPeerIds();
|
|
164
206
|
}
|
|
165
|
-
/**
|
|
166
|
-
* Joiner: the link to the host as a WebSocket-shaped object (text by default), so WebSocket client code runs
|
|
167
|
-
* unchanged. It opens when `ready` resolves and closes when the session ends (`host_left` gives code 1001).
|
|
168
|
-
*/
|
|
169
207
|
asWebSocket(opts) {
|
|
170
208
|
return joinerSocket(this, opts);
|
|
171
209
|
}
|
|
172
|
-
/** Host: `handler(conn)` once per joiner (a WebSocket-shaped connection), the server-side mirror of asWebSocket(). */
|
|
173
210
|
onConnection(handler, opts) {
|
|
174
211
|
return hostConnections(this, handler, opts);
|
|
175
212
|
}
|
|
@@ -204,6 +241,17 @@ export class Lobby extends Emitter {
|
|
|
204
241
|
if (this.mode === "lockstep")
|
|
205
242
|
this.lockstep.hostStart();
|
|
206
243
|
}
|
|
244
|
+
/**
|
|
245
|
+
* Changes your display name mid-session. The hub sanitises it (as at hello, at most 24 characters) and every member,
|
|
246
|
+
* you included, gets `peerRenamed`. Names need not be unique. At most one rename per second; a refusal
|
|
247
|
+
* (`bad_request` for an empty name, `rate_limited`) arrives on the kit's `error` event. Throws `unsupported` when
|
|
248
|
+
* the hub is too old to rename.
|
|
249
|
+
*/
|
|
250
|
+
async rename(name) {
|
|
251
|
+
if (!(this.cfg.features ?? []).includes("set_name"))
|
|
252
|
+
throw new OneLobbyError("unsupported", "this lobby's hub cannot rename players (update it)");
|
|
253
|
+
this.signaling.send({ t: "set_name", name: String(name ?? "") });
|
|
254
|
+
}
|
|
207
255
|
async setMeta(meta) {
|
|
208
256
|
if (!this.isHost)
|
|
209
257
|
throw new Error("only the host can set metadata");
|
|
@@ -368,7 +416,11 @@ export class Lobby extends Emitter {
|
|
|
368
416
|
}, budget);
|
|
369
417
|
}
|
|
370
418
|
giveUpConnecting() {
|
|
371
|
-
|
|
419
|
+
// what the host link saw (states, signals, timings), so a test log of a failed connect says where it stalled
|
|
420
|
+
const hostLink = this.links.get(this.hostPeerId);
|
|
421
|
+
const diagnostics = hostLink ? hostLink.diagnostics?.() : (this.lastHostLinkEnd ?? "no offer from the host ever arrived");
|
|
422
|
+
// the trace holds ICE candidates (local addresses): keep it in data, out of the message a game may show
|
|
423
|
+
const err = new OneLobbyError("ice_failed", `could not open a connection to the host${this.relayRetry ? " (direct or relayed)" : ""}`, diagnostics ? { diagnostics } : undefined);
|
|
372
424
|
this.failReady(err);
|
|
373
425
|
this.reportError(err);
|
|
374
426
|
if (this.ended)
|
|
@@ -377,6 +429,8 @@ export class Lobby extends Emitter {
|
|
|
377
429
|
this.teardown("ice_failed");
|
|
378
430
|
}
|
|
379
431
|
// ---- links -----------------------------------------------------------------------------------------------------
|
|
432
|
+
/** @internal A direct host's hub learns where each joiner's link comes from (Nostr kick bans the address too). */
|
|
433
|
+
linkAddressSink = null;
|
|
380
434
|
makeLink(peerId, relayOnly = this.cfg.relayOnly) {
|
|
381
435
|
const old = this.links.get(peerId);
|
|
382
436
|
if (old) {
|
|
@@ -391,6 +445,7 @@ export class Lobby extends Emitter {
|
|
|
391
445
|
link.onClose = (reason) => this.onLinkClosed(peerId, link, reason);
|
|
392
446
|
link.onMessage = (ch, bytes) => this.onData(peerId, ch, bytes);
|
|
393
447
|
link.onPath = (path) => this.signaling.send({ t: "path", path });
|
|
448
|
+
link.onRemoteAddress = (ip) => this.linkAddressSink?.(peerId, ip);
|
|
394
449
|
return link;
|
|
395
450
|
}
|
|
396
451
|
/** Host side: offer to a joiner. */
|
|
@@ -420,9 +475,13 @@ export class Lobby extends Emitter {
|
|
|
420
475
|
this.emit("peerReady", this.roster.get(link.remoteId) ?? { peerId: link.remoteId, name: "" });
|
|
421
476
|
this.flush(link.remoteId);
|
|
422
477
|
}
|
|
478
|
+
/** Why and how the joiner's last link to the host ended (shown when the join gives up). */
|
|
479
|
+
lastHostLinkEnd = null;
|
|
423
480
|
onLinkClosed(peerId, link, _reason) {
|
|
424
481
|
if (this.links.get(peerId) !== link)
|
|
425
482
|
return; // replaced
|
|
483
|
+
if (!this.isHost && peerId === this.hostPeerId)
|
|
484
|
+
this.lastHostLinkEnd = `link ended (${_reason}): ${link.diagnostics?.() ?? ""}`;
|
|
426
485
|
this.links.delete(peerId);
|
|
427
486
|
this.hiSentTo.delete(link);
|
|
428
487
|
if (this.ended)
|
|
@@ -454,6 +513,10 @@ export class Lobby extends Emitter {
|
|
|
454
513
|
}
|
|
455
514
|
if (!d)
|
|
456
515
|
return;
|
|
516
|
+
// The topology is a star: a joiner hears only its host. Anything else over some other link (a peer that offered to
|
|
517
|
+
// us, or a member that dialled us) is not host traffic and must not reach raw / lockstep / pause / chat handling.
|
|
518
|
+
if (!this.isHost && from !== this.hostPeerId && d.msg.type !== "host_claim")
|
|
519
|
+
return;
|
|
457
520
|
if (!reassembled)
|
|
458
521
|
this.lastHeard.set(from, this.cfg.clock.now());
|
|
459
522
|
const { msg, route } = d;
|
|
@@ -505,6 +568,11 @@ export class Lobby extends Emitter {
|
|
|
505
568
|
case "chat_reject":
|
|
506
569
|
this.chat.handle(from, route, msg);
|
|
507
570
|
break;
|
|
571
|
+
case "pause_request":
|
|
572
|
+
case "pause_state":
|
|
573
|
+
case "pause_refuse":
|
|
574
|
+
this.pause.handle(from, msg);
|
|
575
|
+
break;
|
|
508
576
|
case "voice":
|
|
509
577
|
if (channel === "voice")
|
|
510
578
|
this.voice.handle(from, route, msg); // a voice frame on another channel is ignored
|
|
@@ -525,6 +593,8 @@ export class Lobby extends Emitter {
|
|
|
525
593
|
}
|
|
526
594
|
onHi(from, lastTick) {
|
|
527
595
|
this.connected.add(from);
|
|
596
|
+
this.notReady.get(from)?.();
|
|
597
|
+
this.notReady.delete(from);
|
|
528
598
|
this.sendTo(from, {
|
|
529
599
|
type: "welcome_dc",
|
|
530
600
|
// the tick the snapshot will carry (the game's applied tick), so welcome_dc.tick == snapshot.tick even when
|
|
@@ -535,6 +605,7 @@ export class Lobby extends Emitter {
|
|
|
535
605
|
hostId: this.localPeerId,
|
|
536
606
|
}, 0);
|
|
537
607
|
this.chat.sendHistory(from);
|
|
608
|
+
this.pause.sendState(from);
|
|
538
609
|
if (this.mode === "lockstep")
|
|
539
610
|
this.lockstep.onPeerHi(from, lastTick);
|
|
540
611
|
this.emit("peerReady", this.roster.get(from) ?? { peerId: from, name: "" });
|
|
@@ -583,6 +654,12 @@ export class Lobby extends Emitter {
|
|
|
583
654
|
return;
|
|
584
655
|
this.hostPeerId = hostId;
|
|
585
656
|
this.emit("hostChanged", hostId);
|
|
657
|
+
const held = this.heldOffers.get(hostId);
|
|
658
|
+
if (held) {
|
|
659
|
+
this.heldOffers.delete(hostId);
|
|
660
|
+
if (this.cfg.clock.now() - held.at <= HELD_OFFER_MS)
|
|
661
|
+
void this.acceptOffer(hostId, held.payload);
|
|
662
|
+
}
|
|
586
663
|
}
|
|
587
664
|
/** The host is gone (link closed, no frames for 3 s, or the server said so): run the s.9 rule. */
|
|
588
665
|
handleHostLost(reason) {
|
|
@@ -610,6 +687,16 @@ export class Lobby extends Emitter {
|
|
|
610
687
|
if (candidate === this.localPeerId)
|
|
611
688
|
this.becomeHost();
|
|
612
689
|
// else: wait for the candidate's host_claim (or the server's host_changed); it offers to us and we answer with hi
|
|
690
|
+
this.replayHeldOffers();
|
|
691
|
+
}
|
|
692
|
+
/** Offers that came from roster members while the host link was still open: now the host is lost, answer them. */
|
|
693
|
+
replayHeldOffers() {
|
|
694
|
+
const now = this.cfg.clock.now();
|
|
695
|
+
for (const [from, held] of [...this.heldOffers]) {
|
|
696
|
+
this.heldOffers.delete(from);
|
|
697
|
+
if (now - held.at <= HELD_OFFER_MS && this.roster.has(from))
|
|
698
|
+
void this.acceptOffer(from, held.payload);
|
|
699
|
+
}
|
|
613
700
|
}
|
|
614
701
|
becomeHost() {
|
|
615
702
|
this.epoch = (this.epoch + 1) & 0xffff;
|
|
@@ -619,6 +706,7 @@ export class Lobby extends Emitter {
|
|
|
619
706
|
this.hiSentTo.clear();
|
|
620
707
|
this.lastHeard.clear(); // joiners spoke to the old host; their clocks start when they say hi to us
|
|
621
708
|
this.lockstep.becomeHost();
|
|
709
|
+
this.pause.onBecameHost();
|
|
622
710
|
this.emit("hostChanged", this.localPeerId);
|
|
623
711
|
if (this.serverUp)
|
|
624
712
|
this.signaling.send({ t: "host_changed", hostId: this.localPeerId, epoch: this.epoch });
|
|
@@ -628,6 +716,8 @@ export class Lobby extends Emitter {
|
|
|
628
716
|
}
|
|
629
717
|
}
|
|
630
718
|
onHostClaim(from, epoch, hostId) {
|
|
719
|
+
if (hostId !== from)
|
|
720
|
+
return; // a claim is a peer claiming itself; nobody claims for someone else
|
|
631
721
|
const mine = { epoch: this.epoch, hostId: this.hostPeerId };
|
|
632
722
|
const theirs = { epoch, hostId };
|
|
633
723
|
if (hostId === this.hostPeerId && epoch === this.epoch && !this.isHost) {
|
|
@@ -667,8 +757,10 @@ export class Lobby extends Emitter {
|
|
|
667
757
|
const peer = seatName(m.peer);
|
|
668
758
|
this.roster.set(peer.peerId, peer);
|
|
669
759
|
this.emit("peerJoined", peer);
|
|
670
|
-
if (this.isHost)
|
|
760
|
+
if (this.isHost) {
|
|
761
|
+
this.watchReady(peer.peerId);
|
|
671
762
|
this.offerTo(peer.peerId);
|
|
763
|
+
}
|
|
672
764
|
break;
|
|
673
765
|
}
|
|
674
766
|
case "peer_left":
|
|
@@ -702,11 +794,19 @@ export class Lobby extends Emitter {
|
|
|
702
794
|
case "lobby": {
|
|
703
795
|
const st = m;
|
|
704
796
|
this.meta = { ...st.meta };
|
|
797
|
+
if (st.name)
|
|
798
|
+
this.name = st.name;
|
|
705
799
|
if (st.resumeToken && st.resumeToken !== this.resumeToken) {
|
|
706
800
|
this.resumeToken = st.resumeToken;
|
|
707
801
|
this.seatSink?.(this.resumeToken);
|
|
708
802
|
}
|
|
803
|
+
const before = this.roster;
|
|
709
804
|
this.roster = new Map(st.peers.map((p) => [p.peerId, seatName(p)]));
|
|
805
|
+
for (const [id, p] of this.roster) {
|
|
806
|
+
const old = before.get(id);
|
|
807
|
+
if (old && old.name !== p.name)
|
|
808
|
+
this.emit("peerRenamed", p, old.name);
|
|
809
|
+
}
|
|
710
810
|
if (st.hostId !== this.hostPeerId && !this.isHost)
|
|
711
811
|
this.setHost(st.hostId);
|
|
712
812
|
this.setPhase(st.phase);
|
|
@@ -745,20 +845,21 @@ export class Lobby extends Emitter {
|
|
|
745
845
|
if (s.ice)
|
|
746
846
|
payload.ice = s.ice;
|
|
747
847
|
if (s.sdp?.type === "offer") {
|
|
748
|
-
// We are the answerer
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
await done;
|
|
848
|
+
// We are the answerer. An offer comes from the (new) host, so a peer may dial us only as that rule allows.
|
|
849
|
+
const verdict = this.offerVerdict(from);
|
|
850
|
+
if (verdict === "hold") {
|
|
851
|
+
if (this.heldOffers.size < 64 || this.heldOffers.has(from))
|
|
852
|
+
this.heldOffers.set(from, { payload, at: this.cfg.clock.now() });
|
|
853
|
+
}
|
|
854
|
+
else if (verdict === "accept")
|
|
855
|
+
await this.acceptOffer(from, payload);
|
|
757
856
|
return;
|
|
758
857
|
}
|
|
759
858
|
if (!link) {
|
|
760
859
|
if (!payload.ice || this.ended || !this.roster.has(from))
|
|
761
860
|
return;
|
|
861
|
+
if (this.cfg.direct && from !== this.hostPeerId)
|
|
862
|
+
return;
|
|
762
863
|
const q = this.earlyIce.get(from) ?? [];
|
|
763
864
|
if (q.length < 64)
|
|
764
865
|
q.push(payload);
|
|
@@ -767,6 +868,35 @@ export class Lobby extends Emitter {
|
|
|
767
868
|
}
|
|
768
869
|
await link.handleSignal(payload);
|
|
769
870
|
}
|
|
871
|
+
/**
|
|
872
|
+
* Whether an SDP offer from `from` may be answered. The host (or a joiner's host) always may. Direct lobbies have no
|
|
873
|
+
* migration, so nobody else. In a server lobby a roster member may dial us when we are the host (a claim race decides
|
|
874
|
+
* who wins) or when our own link to the host is gone (the migration window); while that link is open the offer is
|
|
875
|
+
* held, to be answered if the host turns out to be lost a moment later.
|
|
876
|
+
*/
|
|
877
|
+
offerVerdict(from) {
|
|
878
|
+
if (this.ended || from === this.localPeerId)
|
|
879
|
+
return "drop";
|
|
880
|
+
if (!this.isHost && from === this.hostPeerId)
|
|
881
|
+
return "accept";
|
|
882
|
+
if (this.cfg.direct || !this.roster.has(from))
|
|
883
|
+
return "drop";
|
|
884
|
+
if (this.isHost)
|
|
885
|
+
return "accept";
|
|
886
|
+
if (!this.isReady)
|
|
887
|
+
return "drop"; // still joining: only the host's own offer is expected
|
|
888
|
+
return this.links.get(this.hostPeerId)?.isOpen ? "hold" : "accept";
|
|
889
|
+
}
|
|
890
|
+
async acceptOffer(from, payload) {
|
|
891
|
+
const link = this.makeLink(from);
|
|
892
|
+
const early = this.earlyIce.get(from) ?? [];
|
|
893
|
+
this.earlyIce.delete(from);
|
|
894
|
+
// the link holds candidates until the offer is set, so hand them over right behind it
|
|
895
|
+
const done = link.handleSignal(payload);
|
|
896
|
+
for (const e of early)
|
|
897
|
+
void link.handleSignal(e);
|
|
898
|
+
await done;
|
|
899
|
+
}
|
|
770
900
|
/**
|
|
771
901
|
* `linger` (host kick / disconnect): stop using the peer's link at once but leave it open until that peer closes it
|
|
772
902
|
* or KICK_LINGER_MS pass. The hub tells the peer over its WebSocket; closing the data link first would make it look
|
|
@@ -781,6 +911,13 @@ export class Lobby extends Emitter {
|
|
|
781
911
|
if (peer)
|
|
782
912
|
this.roster.delete(peerId);
|
|
783
913
|
this.earlyIce.delete(peerId);
|
|
914
|
+
const cancelWatch = this.notReady.get(peerId);
|
|
915
|
+
if (cancelWatch) {
|
|
916
|
+
cancelWatch();
|
|
917
|
+
this.notReady.delete(peerId);
|
|
918
|
+
if (peer && reason !== "kicked" && reason !== "disconnected")
|
|
919
|
+
this.emit("peerFailed", peer, reason);
|
|
920
|
+
}
|
|
784
921
|
const link = this.links.get(peerId);
|
|
785
922
|
if (link && peerId !== this.hostPeerId) {
|
|
786
923
|
link.onMessage = null;
|
|
@@ -810,6 +947,7 @@ export class Lobby extends Emitter {
|
|
|
810
947
|
this.lockstep.onPeerLeft(peerId);
|
|
811
948
|
this.chat.onPeerLeft(peerId);
|
|
812
949
|
this.voice.onPeerLeft(peerId);
|
|
950
|
+
this.pause.onPeerLeft(peerId);
|
|
813
951
|
if (peer)
|
|
814
952
|
this.emit("peerLeft", peer, reason, close);
|
|
815
953
|
if (peerId === this.hostPeerId && !this.isHost) {
|
|
@@ -827,6 +965,8 @@ export class Lobby extends Emitter {
|
|
|
827
965
|
if (this.phase === p)
|
|
828
966
|
return;
|
|
829
967
|
this.phase = p;
|
|
968
|
+
if (p === "lobby")
|
|
969
|
+
this.pause.onLobbyPhase();
|
|
830
970
|
if (p === "in_game" && !this.lockstep.started)
|
|
831
971
|
this.lockstep.markStarted();
|
|
832
972
|
this.emit("phase", p);
|
|
@@ -882,11 +1022,29 @@ export class Lobby extends Emitter {
|
|
|
882
1022
|
return;
|
|
883
1023
|
this.sendTo(this.hostPeerId, { type: "ping", n: ++this.pingN, t: this.cfg.clock.now() >>> 0 }, 0, "fast");
|
|
884
1024
|
}
|
|
1025
|
+
/** Host: report a joiner that is not ready `readyTimeoutMs` after joining as peerFailed("timeout"), once. */
|
|
1026
|
+
watchReady(peerId) {
|
|
1027
|
+
const ms = this.cfg.readyTimeoutMs ?? 0;
|
|
1028
|
+
let cancel = () => { };
|
|
1029
|
+
if (ms > 0) {
|
|
1030
|
+
cancel = this.cfg.clock.setTimeout(() => {
|
|
1031
|
+
if (!this.notReady.delete(peerId))
|
|
1032
|
+
return;
|
|
1033
|
+
const peer = this.roster.get(peerId);
|
|
1034
|
+
if (peer)
|
|
1035
|
+
this.emit("peerFailed", peer, "timeout");
|
|
1036
|
+
}, ms);
|
|
1037
|
+
}
|
|
1038
|
+
this.notReady.set(peerId, cancel);
|
|
1039
|
+
}
|
|
885
1040
|
teardown(reason, close) {
|
|
886
1041
|
if (this.ended)
|
|
887
1042
|
return;
|
|
888
1043
|
this.ended = true;
|
|
889
1044
|
this.cancelReadyTimer?.();
|
|
1045
|
+
for (const cancel of this.notReady.values())
|
|
1046
|
+
cancel();
|
|
1047
|
+
this.notReady.clear();
|
|
890
1048
|
if (!this.isReady) {
|
|
891
1049
|
const code = reason === "ice_failed" || reason === "kicked" ? reason : "closed";
|
|
892
1050
|
this.failReady(new OneLobbyError(code, `the lobby ended before connecting (${reason})`));
|
|
@@ -902,6 +1060,7 @@ export class Lobby extends Emitter {
|
|
|
902
1060
|
}
|
|
903
1061
|
this.links.clear();
|
|
904
1062
|
this.earlyIce.clear();
|
|
1063
|
+
this.heldOffers.clear();
|
|
905
1064
|
this.lingering.clear();
|
|
906
1065
|
this.signaling.onClose = null;
|
|
907
1066
|
this.signaling.onMessage = null;
|