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/pause.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { PAUSE_FLAG_PAUSER_LEFT, PAUSE_REFUSE } from "./codec.js";
|
|
2
|
+
import { Emitter } from "./emitter.js";
|
|
3
|
+
import { OneLobbyError } from "./signaling.js";
|
|
4
|
+
/** The least time between two accepted pause requests from one peer. */
|
|
5
|
+
export const PAUSE_MIN_REQUEST_GAP_MS = 500;
|
|
6
|
+
const NONE = Object.freeze({ paused: false, byPeer: 0, pauserLeft: false });
|
|
7
|
+
function refusalName(code) {
|
|
8
|
+
return code === PAUSE_REFUSE.notInGame ? "not_in_game" : code === PAUSE_REFUSE.rateLimited ? "rate_limited" : code === PAUSE_REFUSE.notAllowed ? "not_allowed" : "unknown";
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Shared co-op pause (docs/recipes/pause.md): any player asks the host to pause or resume the match for everyone.
|
|
12
|
+
* The host validates (phase, rate, policy), keeps the state and broadcasts it over the reliable channel. OneLobby moves
|
|
13
|
+
* the state and, in lockstep, stops the host's frames; scaling time, menus and banners stay in the game. It is outside
|
|
14
|
+
* the sim: never put it in a lockstep command or hash.
|
|
15
|
+
*/
|
|
16
|
+
export class Pause extends Emitter {
|
|
17
|
+
hub;
|
|
18
|
+
deps;
|
|
19
|
+
/** Set by a blueprint (`session.pause`); the default is "anyone". */
|
|
20
|
+
mode = "anyone";
|
|
21
|
+
/**
|
|
22
|
+
* Host: replaces the default policy. Called with (requesting peer, true to pause / false to resume); return true to
|
|
23
|
+
* allow. Default: anyone pauses, the pauser or the host resumes (host only in "host" mode). Phase and rate run first.
|
|
24
|
+
*/
|
|
25
|
+
policy = null;
|
|
26
|
+
current = NONE;
|
|
27
|
+
lastAccepted = new Map();
|
|
28
|
+
constructor(hub, deps) {
|
|
29
|
+
super();
|
|
30
|
+
this.hub = hub;
|
|
31
|
+
this.deps = deps;
|
|
32
|
+
}
|
|
33
|
+
/** The current shared state (the same on every peer once the host's broadcast has arrived). */
|
|
34
|
+
get state() {
|
|
35
|
+
return this.current;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* True when the local peer may resume now under the default policy: the pauser or the host (in "host" mode, the
|
|
39
|
+
* host). A custom `policy` lives on the host, so on a joiner this is a hint.
|
|
40
|
+
*/
|
|
41
|
+
get canResume() {
|
|
42
|
+
return this.current.paused && this.defaultAllows(this.hub.localPeerId, false);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Ask to pause (true) or resume (false) the match for everyone. Throws `pause_disabled` when the blueprint says
|
|
46
|
+
* `session.pause` is "off" (the default; set it to "anyone" or "host" in onelobby.json). Works only while the lobby
|
|
47
|
+
* is in game (phase "in_game", after `start()`): before that the host refuses with `not_in_game`. The result arrives
|
|
48
|
+
* as `changed` or `refused`; asking for the state that already holds does nothing.
|
|
49
|
+
*/
|
|
50
|
+
request(paused) {
|
|
51
|
+
if (this.mode === "off")
|
|
52
|
+
throw new OneLobbyError("pause_disabled", "pause is off in onelobby.json (session.pause is \"off\"); set session.pause to \"anyone\" or \"host\" to turn it on");
|
|
53
|
+
if (this.hub.isHost)
|
|
54
|
+
this.hostHandle(this.hub.localPeerId, paused);
|
|
55
|
+
else
|
|
56
|
+
this.hub.sendToHost({ type: "pause_request", paused: paused ? 1 : 0 });
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Host only: unpause at once, skipping the policy and the rate limit. Call it before a restart or game over so the
|
|
60
|
+
* next round does not start frozen. Does nothing when not paused.
|
|
61
|
+
*/
|
|
62
|
+
clear() {
|
|
63
|
+
if (!this.hub.isHost)
|
|
64
|
+
throw new Error("only the host can clear the pause");
|
|
65
|
+
if (!this.current.paused)
|
|
66
|
+
return;
|
|
67
|
+
this.set({ paused: false, byPeer: this.hub.localPeerId, pauserLeft: false }, true);
|
|
68
|
+
}
|
|
69
|
+
// ---- internals (called by Lobby) ------------------------------------------------------------------------------
|
|
70
|
+
handle(from, msg) {
|
|
71
|
+
switch (msg.type) {
|
|
72
|
+
case "pause_request":
|
|
73
|
+
if (this.hub.isHost && this.hub.connectedPeers().includes(from))
|
|
74
|
+
this.hostHandle(from, msg.paused !== 0);
|
|
75
|
+
break;
|
|
76
|
+
case "pause_state":
|
|
77
|
+
if (!this.hub.isHost && from === this.hub.hostPeerId) {
|
|
78
|
+
this.set({ paused: msg.paused !== 0, byPeer: msg.byPeer, pauserLeft: (msg.flags & PAUSE_FLAG_PAUSER_LEFT) !== 0 }, false);
|
|
79
|
+
}
|
|
80
|
+
break;
|
|
81
|
+
case "pause_refuse":
|
|
82
|
+
if (!this.hub.isHost && from === this.hub.hostPeerId)
|
|
83
|
+
this.emit("refused", refusalName(msg.reason));
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** Host: a late joiner's link is up; it gets the state when there is one to tell. */
|
|
88
|
+
sendState(peerId) {
|
|
89
|
+
if (this.current.paused)
|
|
90
|
+
this.hub.sendTo(peerId, this.stateMessage(), 0);
|
|
91
|
+
}
|
|
92
|
+
/** A peer is gone: the host takes over the pause its leaver held. */
|
|
93
|
+
onPeerLeft(peerId) {
|
|
94
|
+
this.lastAccepted.delete(peerId);
|
|
95
|
+
const s = this.current;
|
|
96
|
+
if (this.hub.isHost && s.paused && !s.pauserLeft && s.byPeer === peerId) {
|
|
97
|
+
this.set({ paused: true, byPeer: this.hub.localPeerId, pauserLeft: true }, true);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/** This peer just became the host (migration): it keeps the state as is, and takes over a pause whose holder is gone. */
|
|
101
|
+
onBecameHost() {
|
|
102
|
+
this.deps.hold(this.current.paused);
|
|
103
|
+
const s = this.current;
|
|
104
|
+
if (s.paused && !s.pauserLeft && !this.deps.hasPeer(s.byPeer)) {
|
|
105
|
+
this.set({ paused: true, byPeer: this.hub.localPeerId, pauserLeft: true }, true);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/** The lobby went back to the lobby phase: nothing stays paused. */
|
|
109
|
+
onLobbyPhase() {
|
|
110
|
+
if (!this.current.paused)
|
|
111
|
+
return;
|
|
112
|
+
this.set({ paused: false, byPeer: this.hub.hostPeerId, pauserLeft: false }, this.hub.isHost);
|
|
113
|
+
}
|
|
114
|
+
// ---- host path --------------------------------------------------------------------------------------------------
|
|
115
|
+
defaultAllows(peer, wantPaused) {
|
|
116
|
+
if (this.mode === "host")
|
|
117
|
+
return peer === this.hub.hostPeerId;
|
|
118
|
+
return wantPaused || peer === this.current.byPeer || peer === this.hub.hostPeerId;
|
|
119
|
+
}
|
|
120
|
+
hostHandle(from, want) {
|
|
121
|
+
if (this.mode === "off")
|
|
122
|
+
return this.refuse(from, want, PAUSE_REFUSE.notAllowed);
|
|
123
|
+
if (this.deps.phase() !== "in_game")
|
|
124
|
+
return this.refuse(from, want, PAUSE_REFUSE.notInGame);
|
|
125
|
+
if (want === this.current.paused)
|
|
126
|
+
return;
|
|
127
|
+
const now = this.hub.clock.now();
|
|
128
|
+
const last = this.lastAccepted.get(from);
|
|
129
|
+
if (last !== undefined && now - last < PAUSE_MIN_REQUEST_GAP_MS)
|
|
130
|
+
return this.refuse(from, want, PAUSE_REFUSE.rateLimited);
|
|
131
|
+
const ok = this.policy ? this.policy(from, want) : this.defaultAllows(from, want);
|
|
132
|
+
if (!ok)
|
|
133
|
+
return this.refuse(from, want, PAUSE_REFUSE.notAllowed);
|
|
134
|
+
this.lastAccepted.set(from, now);
|
|
135
|
+
this.set({ paused: want, byPeer: from, pauserLeft: false }, true);
|
|
136
|
+
}
|
|
137
|
+
refuse(to, want, reason) {
|
|
138
|
+
if (to === this.hub.localPeerId)
|
|
139
|
+
this.emit("refused", refusalName(reason));
|
|
140
|
+
else
|
|
141
|
+
this.hub.sendTo(to, { type: "pause_refuse", paused: want ? 1 : 0, reason }, 0);
|
|
142
|
+
}
|
|
143
|
+
stateMessage() {
|
|
144
|
+
const s = this.current;
|
|
145
|
+
return { type: "pause_state", paused: s.paused ? 1 : 0, byPeer: s.byPeer, flags: s.pauserLeft ? PAUSE_FLAG_PAUSER_LEFT : 0 };
|
|
146
|
+
}
|
|
147
|
+
set(next, broadcast) {
|
|
148
|
+
const prev = this.current;
|
|
149
|
+
this.current = Object.freeze(next);
|
|
150
|
+
this.deps.hold(next.paused);
|
|
151
|
+
if (broadcast) {
|
|
152
|
+
const msg = this.stateMessage();
|
|
153
|
+
for (const p of this.hub.connectedPeers())
|
|
154
|
+
this.hub.sendTo(p, msg, 0);
|
|
155
|
+
}
|
|
156
|
+
if (prev.paused !== next.paused || prev.byPeer !== next.byPeer || prev.pauserLeft !== next.pauserLeft)
|
|
157
|
+
this.emit("changed", next);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
//# sourceMappingURL=pause.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pause.js","sourceRoot":"","sources":["../src/pause.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,YAAY,EAAgB,MAAM,YAAY,CAAC;AAChF,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAEvC,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAE/C,wEAAwE;AACxE,MAAM,CAAC,MAAM,wBAAwB,GAAG,GAAG,CAAC;AAkC5C,MAAM,IAAI,GAAe,MAAM,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAC;AAExF,SAAS,WAAW,CAAC,IAAY;IAC/B,OAAO,IAAI,KAAK,YAAY,CAAC,SAAS,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,KAAK,YAAY,CAAC,WAAW,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,IAAI,KAAK,YAAY,CAAC,UAAU,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,SAAS,CAAC;AAC7K,CAAC;AAED;;;;;GAKG;AACH,MAAM,OAAO,KAAM,SAAQ,OAAoB;IAYzB;IAAkB;IAXtC,qEAAqE;IACrE,IAAI,GAAc,QAAQ,CAAC;IAC3B;;;OAGG;IACH,MAAM,GAA0D,IAAI,CAAC;IAE7D,OAAO,GAAe,IAAI,CAAC;IAC3B,YAAY,GAAG,IAAI,GAAG,EAAkB,CAAC;IAEjD,YAAoB,GAAQ,EAAU,IAAe;QACnD,KAAK,EAAE,CAAC;QADU,QAAG,GAAH,GAAG,CAAK;QAAU,SAAI,GAAJ,IAAI,CAAW;IAErD,CAAC;IAED,+FAA+F;IAC/F,IAAI,KAAK;QACP,OAAO,IAAI,CAAC,OAAO,CAAC;IACtB,CAAC;IAED;;;OAGG;IACH,IAAI,SAAS;QACX,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,IAAI,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC;IAChF,CAAC;IAED;;;;;OAKG;IACH,OAAO,CAAC,MAAe;QACrB,IAAI,IAAI,CAAC,IAAI,KAAK,KAAK;YAAE,MAAM,IAAI,aAAa,CAAC,gBAAgB,EAAE,qHAAqH,CAAC,CAAC;QAC1L,IAAI,IAAI,CAAC,GAAG,CAAC,MAAM;YAAE,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;;YAC9D,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,IAAI,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IAC9E,CAAC;IAED;;;OAGG;IACH,KAAK;QACH,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM;YAAE,MAAM,IAAI,KAAK,CAAC,mCAAmC,CAAC,CAAC;QAC3E,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM;YAAE,OAAO;QACjC,IAAI,CAAC,GAAG,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,UAAU,EAAE,KAAK,EAAE,EAAE,IAAI,CAAC,CAAC;IACrF,CAAC;IAED,kHAAkH;IAElH,MAAM,CAAC,IAAY,EAAE,GAAY;QAC/B,QAAQ,GAAG,CAAC,IAAI,EAAE,CAAC;YACjB,KAAK,eAAe;gBAClB,IAAI,IAAI,CAAC,GAAG,CAAC,MAAM,IAAI,IAAI,CAAC,GAAG,CAAC,cAAc,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC;oBAAE,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,GAAG,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC;gBACzG,MAAM;YACR,KAAK,aAAa;gBAChB,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,IAAI,IAAI,KAAK,IAAI,CAAC,GAAG,CAAC,UAAU,EAAE,CAAC;oBACrD,IAAI,CAAC,GAAG,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,KAAK,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,EAAE,CAAC,GAAG,CAAC,KAAK,GAAG,sBAAsB,CAAC,KAAK,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;gBAC5H,CAAC;gBACD,MAAM;YACR,KAAK,cAAc;gBACjB,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,IAAI,IAAI,KAAK,IAAI,CAAC,GAAG,CAAC,UAAU;oBAAE,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;gBACpG,MAAM;QACV,CAAC;IACH,CAAC;IAED,qFAAqF;IACrF,SAAS,CAAC,MAAc;QACtB,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM;YAAE,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC,CAAC;IAC3E,CAAC;IAED,qEAAqE;IACrE,UAAU,CAAC,MAAc;QACvB,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QACjC,MAAM,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC;QACvB,IAAI,IAAI,CAAC,GAAG,CAAC,MAAM,IAAI,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC,UAAU,IAAI,CAAC,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;YACxE,IAAI,CAAC,GAAG,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,UAAU,EAAE,IAAI,EAAE,EAAE,IAAI,CAAC,CAAC;QACnF,CAAC;IACH,CAAC;IAED,yHAAyH;IACzH,YAAY;QACV,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QACpC,MAAM,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC;QACvB,IAAI,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC,UAAU,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC;YAC9D,IAAI,CAAC,GAAG,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,UAAU,EAAE,IAAI,EAAE,EAAE,IAAI,CAAC,CAAC;QACnF,CAAC;IACH,CAAC;IAED,oEAAoE;IACpE,YAAY;QACV,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM;YAAE,OAAO;QACjC,IAAI,CAAC,GAAG,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,UAAU,EAAE,UAAU,EAAE,KAAK,EAAE,EAAE,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IAC/F,CAAC;IAED,oHAAoH;IAE5G,aAAa,CAAC,IAAY,EAAE,UAAmB;QACrD,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM;YAAE,OAAO,IAAI,KAAK,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC;QAC9D,OAAO,UAAU,IAAI,IAAI,KAAK,IAAI,CAAC,OAAO,CAAC,MAAM,IAAI,IAAI,KAAK,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC;IACpF,CAAC;IAEO,UAAU,CAAC,IAAY,EAAE,IAAa;QAC5C,IAAI,IAAI,CAAC,IAAI,KAAK,KAAK;YAAE,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,YAAY,CAAC,UAAU,CAAC,CAAC;QACjF,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,KAAK,SAAS;YAAE,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,YAAY,CAAC,SAAS,CAAC,CAAC;QAC5F,IAAI,IAAI,KAAK,IAAI,CAAC,OAAO,CAAC,MAAM;YAAE,OAAO;QACzC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC;QACjC,MAAM,IAAI,GAAG,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACzC,IAAI,IAAI,KAAK,SAAS,IAAI,GAAG,GAAG,IAAI,GAAG,wBAAwB;YAAE,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,YAAY,CAAC,WAAW,CAAC,CAAC;QAC1H,MAAM,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QAClF,IAAI,CAAC,EAAE;YAAE,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,YAAY,CAAC,UAAU,CAAC,CAAC;QACjE,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;QACjC,IAAI,CAAC,GAAG,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,EAAE,IAAI,CAAC,CAAC;IACpE,CAAC;IAEO,MAAM,CAAC,EAAU,EAAE,IAAa,EAAE,MAAc;QACtD,IAAI,EAAE,KAAK,IAAI,CAAC,GAAG,CAAC,WAAW;YAAE,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC;;YACtE,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,cAAc,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC;IACtF,CAAC;IAEO,YAAY;QAClB,MAAM,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC;QACvB,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAC/H,CAAC;IAEO,GAAG,CAAC,IAAgB,EAAE,SAAkB;QAC9C,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC;QAC1B,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QACnC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC5B,IAAI,SAAS,EAAE,CAAC;YACd,MAAM,GAAG,GAAG,IAAI,CAAC,YAAY,EAAE,CAAC;YAChC,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,cAAc,EAAE;gBAAE,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC;QACxE,CAAC;QACD,IAAI,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,UAAU,KAAK,IAAI,CAAC,UAAU;YAAE,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;IACpI,CAAC;CACF","sourcesContent":["import { PAUSE_FLAG_PAUSER_LEFT, PAUSE_REFUSE, type Message } from \"./codec.js\";\nimport { Emitter } from \"./emitter.js\";\nimport type { Hub } from \"./hub.js\";\nimport { OneLobbyError } from \"./signaling.js\";\n\n/** The least time between two accepted pause requests from one peer. */\nexport const PAUSE_MIN_REQUEST_GAP_MS = 500;\n\n/**\n * The shared pause: `paused` is on while someone holds the match; `byPeer` is the peer that paused (while paused) or\n * that last resumed it; `pauserLeft` says the pauser left and the host now holds the pause (`byPeer` is then the host).\n */\nexport interface PauseState {\n readonly paused: boolean;\n readonly byPeer: number;\n readonly pauserLeft: boolean;\n}\n\n/** Why a request was refused: `not_in_game`, `rate_limited` or `not_allowed` (the `ErrorCodes` constants). */\nexport type PauseRefusal = \"not_in_game\" | \"rate_limited\" | \"not_allowed\" | \"unknown\";\n\nexport type PauseEvents = {\n /** The state changed (paused, resumed, pauser left, cleared). Fires on every peer. */\n changed: [PauseState];\n /** A request of this peer was refused; `code` is a `PauseRefusal`. */\n refused: [code: PauseRefusal];\n};\n\n/** `session.pause` of a blueprint: \"off\" makes `request` throw `pause_disabled`, \"host\" makes the default policy host-only. */\nexport type PauseMode = \"off\" | \"host\" | \"anyone\";\n\nexport interface PauseDeps {\n /** The lobby's phase now (\"lobby\" | \"in_game\"). */\n phase: () => string;\n /** Lockstep: stop (true) or resume (false) the host's frame emission. */\n hold: (held: boolean) => void;\n /** Whether `peerId` is still in the roster. */\n hasPeer: (peerId: number) => boolean;\n}\n\nconst NONE: PauseState = Object.freeze({ paused: false, byPeer: 0, pauserLeft: false });\n\nfunction refusalName(code: number): PauseRefusal {\n return code === PAUSE_REFUSE.notInGame ? \"not_in_game\" : code === PAUSE_REFUSE.rateLimited ? \"rate_limited\" : code === PAUSE_REFUSE.notAllowed ? \"not_allowed\" : \"unknown\";\n}\n\n/**\n * Shared co-op pause (docs/recipes/pause.md): any player asks the host to pause or resume the match for everyone.\n * The host validates (phase, rate, policy), keeps the state and broadcasts it over the reliable channel. OneLobby moves\n * the state and, in lockstep, stops the host's frames; scaling time, menus and banners stay in the game. It is outside\n * the sim: never put it in a lockstep command or hash.\n */\nexport class Pause extends Emitter<PauseEvents> {\n /** Set by a blueprint (`session.pause`); the default is \"anyone\". */\n mode: PauseMode = \"anyone\";\n /**\n * Host: replaces the default policy. Called with (requesting peer, true to pause / false to resume); return true to\n * allow. Default: anyone pauses, the pauser or the host resumes (host only in \"host\" mode). Phase and rate run first.\n */\n policy: ((peerId: number, paused: boolean) => boolean) | null = null;\n\n private current: PauseState = NONE;\n private lastAccepted = new Map<number, number>();\n\n constructor(private hub: Hub, private deps: PauseDeps) {\n super();\n }\n\n /** The current shared state (the same on every peer once the host's broadcast has arrived). */\n get state(): PauseState {\n return this.current;\n }\n\n /**\n * True when the local peer may resume now under the default policy: the pauser or the host (in \"host\" mode, the\n * host). A custom `policy` lives on the host, so on a joiner this is a hint.\n */\n get canResume(): boolean {\n return this.current.paused && this.defaultAllows(this.hub.localPeerId, false);\n }\n\n /**\n * Ask to pause (true) or resume (false) the match for everyone. Throws `pause_disabled` when the blueprint says\n * `session.pause` is \"off\" (the default; set it to \"anyone\" or \"host\" in onelobby.json). Works only while the lobby\n * is in game (phase \"in_game\", after `start()`): before that the host refuses with `not_in_game`. The result arrives\n * as `changed` or `refused`; asking for the state that already holds does nothing.\n */\n request(paused: boolean): void {\n if (this.mode === \"off\") throw new OneLobbyError(\"pause_disabled\", \"pause is off in onelobby.json (session.pause is \\\"off\\\"); set session.pause to \\\"anyone\\\" or \\\"host\\\" to turn it on\");\n if (this.hub.isHost) this.hostHandle(this.hub.localPeerId, paused);\n else this.hub.sendToHost({ type: \"pause_request\", paused: paused ? 1 : 0 });\n }\n\n /**\n * Host only: unpause at once, skipping the policy and the rate limit. Call it before a restart or game over so the\n * next round does not start frozen. Does nothing when not paused.\n */\n clear(): void {\n if (!this.hub.isHost) throw new Error(\"only the host can clear the pause\");\n if (!this.current.paused) return;\n this.set({ paused: false, byPeer: this.hub.localPeerId, pauserLeft: false }, true);\n }\n\n // ---- internals (called by Lobby) ------------------------------------------------------------------------------\n\n handle(from: number, msg: Message): void {\n switch (msg.type) {\n case \"pause_request\":\n if (this.hub.isHost && this.hub.connectedPeers().includes(from)) this.hostHandle(from, msg.paused !== 0);\n break;\n case \"pause_state\":\n if (!this.hub.isHost && from === this.hub.hostPeerId) {\n this.set({ paused: msg.paused !== 0, byPeer: msg.byPeer, pauserLeft: (msg.flags & PAUSE_FLAG_PAUSER_LEFT) !== 0 }, false);\n }\n break;\n case \"pause_refuse\":\n if (!this.hub.isHost && from === this.hub.hostPeerId) this.emit(\"refused\", refusalName(msg.reason));\n break;\n }\n }\n\n /** Host: a late joiner's link is up; it gets the state when there is one to tell. */\n sendState(peerId: number): void {\n if (this.current.paused) this.hub.sendTo(peerId, this.stateMessage(), 0);\n }\n\n /** A peer is gone: the host takes over the pause its leaver held. */\n onPeerLeft(peerId: number): void {\n this.lastAccepted.delete(peerId);\n const s = this.current;\n if (this.hub.isHost && s.paused && !s.pauserLeft && s.byPeer === peerId) {\n this.set({ paused: true, byPeer: this.hub.localPeerId, pauserLeft: true }, true);\n }\n }\n\n /** This peer just became the host (migration): it keeps the state as is, and takes over a pause whose holder is gone. */\n onBecameHost(): void {\n this.deps.hold(this.current.paused);\n const s = this.current;\n if (s.paused && !s.pauserLeft && !this.deps.hasPeer(s.byPeer)) {\n this.set({ paused: true, byPeer: this.hub.localPeerId, pauserLeft: true }, true);\n }\n }\n\n /** The lobby went back to the lobby phase: nothing stays paused. */\n onLobbyPhase(): void {\n if (!this.current.paused) return;\n this.set({ paused: false, byPeer: this.hub.hostPeerId, pauserLeft: false }, this.hub.isHost);\n }\n\n // ---- host path --------------------------------------------------------------------------------------------------\n\n private defaultAllows(peer: number, wantPaused: boolean): boolean {\n if (this.mode === \"host\") return peer === this.hub.hostPeerId;\n return wantPaused || peer === this.current.byPeer || peer === this.hub.hostPeerId;\n }\n\n private hostHandle(from: number, want: boolean): void {\n if (this.mode === \"off\") return this.refuse(from, want, PAUSE_REFUSE.notAllowed);\n if (this.deps.phase() !== \"in_game\") return this.refuse(from, want, PAUSE_REFUSE.notInGame);\n if (want === this.current.paused) return;\n const now = this.hub.clock.now();\n const last = this.lastAccepted.get(from);\n if (last !== undefined && now - last < PAUSE_MIN_REQUEST_GAP_MS) return this.refuse(from, want, PAUSE_REFUSE.rateLimited);\n const ok = this.policy ? this.policy(from, want) : this.defaultAllows(from, want);\n if (!ok) return this.refuse(from, want, PAUSE_REFUSE.notAllowed);\n this.lastAccepted.set(from, now);\n this.set({ paused: want, byPeer: from, pauserLeft: false }, true);\n }\n\n private refuse(to: number, want: boolean, reason: number): void {\n if (to === this.hub.localPeerId) this.emit(\"refused\", refusalName(reason));\n else this.hub.sendTo(to, { type: \"pause_refuse\", paused: want ? 1 : 0, reason }, 0);\n }\n\n private stateMessage(): Message {\n const s = this.current;\n return { type: \"pause_state\", paused: s.paused ? 1 : 0, byPeer: s.byPeer, flags: s.pauserLeft ? PAUSE_FLAG_PAUSER_LEFT : 0 };\n }\n\n private set(next: PauseState, broadcast: boolean): void {\n const prev = this.current;\n this.current = Object.freeze(next);\n this.deps.hold(next.paused);\n if (broadcast) {\n const msg = this.stateMessage();\n for (const p of this.hub.connectedPeers()) this.hub.sendTo(p, msg, 0);\n }\n if (prev.paused !== next.paused || prev.byPeer !== next.byPeer || prev.pauserLeft !== next.pauserLeft) this.emit(\"changed\", next);\n }\n}\n"]}
|
package/dist/signaling.d.ts
CHANGED
package/dist/signaling.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"signaling.js","sourceRoot":"","sources":["../src/signaling.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Clock } from \"./clock.js\";\nimport type { IceServer, SocketFactory, SocketLike } from \"./transport.js\";\n\n/** Signaling wire types (DESIGN s.7). Unknown fields are kept; unknown types are ignored by the Lobby. */\nexport interface PeerInfo {\n peerId: number;\n name: string;\n joinedAt?: number;\n}\n\nexport interface LobbyState {\n t: \"lobby\";\n code: string;\n gameId: string;\n netVersion: string;\n hostId: number;\n you: number;\n mode: \"lockstep\" | \"raw\";\n tickRate: number;\n lateJoin: boolean;\n phase: \"lobby\" | \"in_game\";\n meta: Record<string, string>;\n peers: PeerInfo[];\n resumeToken: string;\n name?: string;\n maxPlayers?: number;\n visibility?: string;\n relayOnly?: boolean;\n}\n\nexport interface LobbySummary {\n code: string;\n gameId: string;\n netVersion: string;\n name: string;\n hostName: string;\n players: number;\n maxPlayers: number;\n phase: string;\n meta: Record<string, string>;\n region?: string;\n createdAt?: number;\n}\n\nexport interface WelcomeMsg {\n t: \"welcome\";\n peerId?: number;\n ice: IceServer[];\n serverTime?: number;\n limits?: { maxMsg: number; maxPlayers: number };\n}\n\nexport interface ServerError {\n t: \"error\";\n code: string;\n msg?: string;\n data?: Record<string, unknown>;\n}\n\nexport type ServerMsg = { t: string; [k: string]: unknown };\n\nexport class OneLobbyError extends Error {\n constructor(public code: string, message?: string, public data?: Record<string, unknown>) {\n super(message ? `${code}: ${message}` : code);\n this.name = \"OneLobbyError\";\n }\n}\n\nexport const PROTO_ID = \"lk1\";\nconst PING_EVERY_MS = 10_000;\nconst SILENCE_MS = 30_000;\n\ninterface Waiter {\n types: string[];\n resolve: (m: ServerMsg) => void;\n reject: (e: Error) => void;\n}\n\n/** Thin JSON signaling client: one socket, request/response helpers, keep-alive pings. */\nexport class SignalingClient {\n private socket: SocketLike | null = null;\n private waiters: Waiter[] = [];\n private lastRx = 0;\n private cancelPing: (() => void) | null = null;\n private pingN = 0;\n closed = false;\n\n private handler: ((m: ServerMsg) => void) | null = null;\n // Messages with no waiter and no handler yet. The reply that resolves join/create can arrive in the same socket\n // read as the host's first offer; both are dispatched before the awaiting code builds its Lobby and sets a handler.\n private backlog: ServerMsg[] = [];\n onClose: (() => void) | null = null;\n\n get onMessage(): ((m: ServerMsg) => void) | null {\n return this.handler;\n }\n\n /** Setting a handler first delivers, in order, what arrived while there was none. */\n set onMessage(h: ((m: ServerMsg) => void) | null) {\n this.handler = h;\n while (h && this.handler === h && this.backlog.length) h(this.backlog.shift()!);\n }\n\n constructor(private url: string, private factory: SocketFactory, private clock: Clock) {}\n\n connect(): Promise<void> {\n return new Promise((resolve, reject) => {\n let s: SocketLike;\n try {\n s = this.factory(this.url);\n } catch (e) {\n reject(new OneLobbyError(\"connect_failed\", String(e)));\n return;\n }\n this.socket = s;\n s.onopen = () => {\n this.lastRx = this.clock.now();\n this.cancelPing = this.clock.setInterval(() => this.tick(), PING_EVERY_MS);\n resolve();\n };\n s.onerror = () => {\n if (!this.lastRx) reject(new OneLobbyError(\"connect_failed\", `cannot reach ${this.url}`));\n };\n s.onmessage = (ev) => {\n this.lastRx = this.clock.now();\n let m: ServerMsg;\n try {\n m = JSON.parse(String(ev.data)) as ServerMsg;\n } catch {\n return;\n }\n if (!m || typeof m.t !== \"string\") return;\n if (m.t === \"ping\") {\n this.send({ t: \"pong\", n: m.n });\n return;\n }\n if (m.t === \"pong\") return;\n const i = this.waiters.findIndex((w) => w.types.includes(m.t) || m.t === \"error\");\n if (i >= 0) {\n const [w] = this.waiters.splice(i, 1);\n if (m.t === \"error\") w.reject(errorFrom(m));\n else w.resolve(m);\n return;\n }\n if (this.handler) this.handler(m);\n else if (this.backlog.length < 256) this.backlog.push(m);\n };\n s.onclose = () => {\n if (!this.lastRx) reject(new OneLobbyError(\"connect_failed\", `cannot reach ${this.url}`));\n this.shutdown();\n };\n });\n }\n\n private tick() {\n if (this.clock.now() - this.lastRx > SILENCE_MS) {\n this.socket?.close(4000, \"silence\");\n this.shutdown();\n return;\n }\n this.send({ t: \"ping\", n: ++this.pingN });\n }\n\n private shutdown() {\n if (this.closed) return;\n this.closed = true;\n this.cancelPing?.();\n const err = new OneLobbyError(\"closed\", \"signaling connection closed\");\n for (const w of this.waiters.splice(0)) w.reject(err);\n this.onClose?.();\n }\n\n get isOpen(): boolean {\n return !this.closed && this.socket !== null && this.socket.readyState === 1;\n }\n\n send(m: Record<string, unknown>): void {\n if (!this.isOpen) return;\n this.socket!.send(JSON.stringify(m));\n }\n\n /** Wait for the next message of one of `types`, or reject on `error`/close. */\n expect<T extends { t: string } = ServerMsg>(...types: string[]): Promise<T> {\n if (this.closed) return Promise.reject(new OneLobbyError(\"closed\", \"signaling connection closed\"));\n return new Promise<T>((resolve, reject) => {\n this.waiters.push({ types, resolve: resolve as (m: ServerMsg) => void, reject });\n });\n }\n\n async request<T extends { t: string } = ServerMsg>(m: Record<string, unknown>, ...types: string[]): Promise<T> {\n const p = this.expect<T>(...types);\n this.send(m);\n return p;\n }\n\n close(): void {\n this.socket?.close(1000, \"bye\");\n this.shutdown();\n }\n}\n\nfunction errorFrom(m: ServerMsg): OneLobbyError {\n return new OneLobbyError(String(m.code ?? \"error\"), m.msg === undefined ? undefined : String(m.msg), m.data as Record<string, unknown> | undefined);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"signaling.js","sourceRoot":"","sources":["../src/signaling.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Clock } from \"./clock.js\";\nimport type { IceServer, SocketFactory, SocketLike } from \"./transport.js\";\n\n/** Signaling wire types (DESIGN s.7). Unknown fields are kept; unknown types are ignored by the Lobby. */\nexport interface PeerInfo {\n peerId: number;\n name: string;\n joinedAt?: number;\n}\n\nexport interface LobbyState {\n t: \"lobby\";\n code: string;\n gameId: string;\n netVersion: string;\n hostId: number;\n you: number;\n mode: \"lockstep\" | \"raw\";\n tickRate: number;\n lateJoin: boolean;\n phase: \"lobby\" | \"in_game\";\n meta: Record<string, string>;\n peers: PeerInfo[];\n resumeToken: string;\n name?: string;\n maxPlayers?: number;\n visibility?: string;\n relayOnly?: boolean;\n}\n\nexport interface LobbySummary {\n code: string;\n gameId: string;\n netVersion: string;\n name: string;\n hostName: string;\n players: number;\n maxPlayers: number;\n phase: string;\n meta: Record<string, string>;\n region?: string;\n createdAt?: number;\n}\n\nexport interface WelcomeMsg {\n t: \"welcome\";\n peerId?: number;\n ice: IceServer[];\n serverTime?: number;\n limits?: { maxMsg: number; maxPlayers: number };\n /** Optional messages the hub handles, e.g. \"set_name\"; missing on an older hub. */\n features?: string[];\n}\n\nexport interface ServerError {\n t: \"error\";\n code: string;\n msg?: string;\n data?: Record<string, unknown>;\n}\n\nexport type ServerMsg = { t: string; [k: string]: unknown };\n\nexport class OneLobbyError extends Error {\n constructor(public code: string, message?: string, public data?: Record<string, unknown>) {\n super(message ? `${code}: ${message}` : code);\n this.name = \"OneLobbyError\";\n }\n}\n\nexport const PROTO_ID = \"lk1\";\nconst PING_EVERY_MS = 10_000;\nconst SILENCE_MS = 30_000;\n\ninterface Waiter {\n types: string[];\n resolve: (m: ServerMsg) => void;\n reject: (e: Error) => void;\n}\n\n/** Thin JSON signaling client: one socket, request/response helpers, keep-alive pings. */\nexport class SignalingClient {\n private socket: SocketLike | null = null;\n private waiters: Waiter[] = [];\n private lastRx = 0;\n private cancelPing: (() => void) | null = null;\n private pingN = 0;\n closed = false;\n\n private handler: ((m: ServerMsg) => void) | null = null;\n // Messages with no waiter and no handler yet. The reply that resolves join/create can arrive in the same socket\n // read as the host's first offer; both are dispatched before the awaiting code builds its Lobby and sets a handler.\n private backlog: ServerMsg[] = [];\n onClose: (() => void) | null = null;\n\n get onMessage(): ((m: ServerMsg) => void) | null {\n return this.handler;\n }\n\n /** Setting a handler first delivers, in order, what arrived while there was none. */\n set onMessage(h: ((m: ServerMsg) => void) | null) {\n this.handler = h;\n while (h && this.handler === h && this.backlog.length) h(this.backlog.shift()!);\n }\n\n constructor(private url: string, private factory: SocketFactory, private clock: Clock) {}\n\n connect(): Promise<void> {\n return new Promise((resolve, reject) => {\n let s: SocketLike;\n try {\n s = this.factory(this.url);\n } catch (e) {\n reject(new OneLobbyError(\"connect_failed\", String(e)));\n return;\n }\n this.socket = s;\n s.onopen = () => {\n this.lastRx = this.clock.now();\n this.cancelPing = this.clock.setInterval(() => this.tick(), PING_EVERY_MS);\n resolve();\n };\n s.onerror = () => {\n if (!this.lastRx) reject(new OneLobbyError(\"connect_failed\", `cannot reach ${this.url}`));\n };\n s.onmessage = (ev) => {\n this.lastRx = this.clock.now();\n let m: ServerMsg;\n try {\n m = JSON.parse(String(ev.data)) as ServerMsg;\n } catch {\n return;\n }\n if (!m || typeof m.t !== \"string\") return;\n if (m.t === \"ping\") {\n this.send({ t: \"pong\", n: m.n });\n return;\n }\n if (m.t === \"pong\") return;\n const i = this.waiters.findIndex((w) => w.types.includes(m.t) || m.t === \"error\");\n if (i >= 0) {\n const [w] = this.waiters.splice(i, 1);\n if (m.t === \"error\") w.reject(errorFrom(m));\n else w.resolve(m);\n return;\n }\n if (this.handler) this.handler(m);\n else if (this.backlog.length < 256) this.backlog.push(m);\n };\n s.onclose = () => {\n if (!this.lastRx) reject(new OneLobbyError(\"connect_failed\", `cannot reach ${this.url}`));\n this.shutdown();\n };\n });\n }\n\n private tick() {\n if (this.clock.now() - this.lastRx > SILENCE_MS) {\n this.socket?.close(4000, \"silence\");\n this.shutdown();\n return;\n }\n this.send({ t: \"ping\", n: ++this.pingN });\n }\n\n private shutdown() {\n if (this.closed) return;\n this.closed = true;\n this.cancelPing?.();\n const err = new OneLobbyError(\"closed\", \"signaling connection closed\");\n for (const w of this.waiters.splice(0)) w.reject(err);\n this.onClose?.();\n }\n\n get isOpen(): boolean {\n return !this.closed && this.socket !== null && this.socket.readyState === 1;\n }\n\n send(m: Record<string, unknown>): void {\n if (!this.isOpen) return;\n this.socket!.send(JSON.stringify(m));\n }\n\n /** Wait for the next message of one of `types`, or reject on `error`/close. */\n expect<T extends { t: string } = ServerMsg>(...types: string[]): Promise<T> {\n if (this.closed) return Promise.reject(new OneLobbyError(\"closed\", \"signaling connection closed\"));\n return new Promise<T>((resolve, reject) => {\n this.waiters.push({ types, resolve: resolve as (m: ServerMsg) => void, reject });\n });\n }\n\n async request<T extends { t: string } = ServerMsg>(m: Record<string, unknown>, ...types: string[]): Promise<T> {\n const p = this.expect<T>(...types);\n this.send(m);\n return p;\n }\n\n close(): void {\n this.socket?.close(1000, \"bye\");\n this.shutdown();\n }\n}\n\nfunction errorFrom(m: ServerMsg): OneLobbyError {\n return new OneLobbyError(String(m.code ?? \"error\"), m.msg === undefined ? undefined : String(m.msg), m.data as Record<string, unknown> | undefined);\n}\n"]}
|
package/dist/socket.d.ts
CHANGED
|
@@ -3,8 +3,9 @@ import type { Lobby } from "./lobby.js";
|
|
|
3
3
|
* A WebSocket-shaped view of one peer link, so code written against `WebSocket` runs on a lobby unchanged:
|
|
4
4
|
* `lobby.asWebSocket()` on a joiner (the link to the host), `lobby.onConnection(conn => ...)` on the host (one per
|
|
5
5
|
* joiner). Not a real WebSocket: no URL, no subprotocols, no `addEventListener`; `bufferedAmount` is always 0.
|
|
6
|
+
* `D` is what `onmessage` gets: `Uint8Array` with `{ binary: true }`, `string` without.
|
|
6
7
|
*/
|
|
7
|
-
export interface LobbySocket {
|
|
8
|
+
export interface LobbySocket<D extends string | Uint8Array = string | Uint8Array> {
|
|
8
9
|
/** The peer at the other end (the host for a joiner's socket, the joiner for a host's connection). */
|
|
9
10
|
readonly peerId: number;
|
|
10
11
|
/** 0 CONNECTING (joiner, before `lobby.ready`), 1 OPEN, 3 CLOSED (there is no CLOSING). */
|
|
@@ -25,11 +26,11 @@ export interface LobbySocket {
|
|
|
25
26
|
onopen: ((ev: {
|
|
26
27
|
type: "open";
|
|
27
28
|
}) => void) | null;
|
|
28
|
-
/**
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
/**
|
|
30
|
+
* `data` is a string (UTF-8 decoded), or with `{ binary: true }` always a Uint8Array. It can be a view into a larger
|
|
31
|
+
* buffer (check `byteOffset`/`byteLength`): `data.slice()` before transferring its `.buffer` to a Worker.
|
|
32
|
+
*/
|
|
33
|
+
onmessage: MessageHandler<D> | null;
|
|
33
34
|
/** `reason` is the lobby's reason ("host_left", "timeout", "kicked", "disconnected", ...) unless `close()` or the host gave one. */
|
|
34
35
|
onclose: ((ev: {
|
|
35
36
|
type: "close";
|
|
@@ -42,6 +43,13 @@ export interface LobbySocket {
|
|
|
42
43
|
type: "error";
|
|
43
44
|
}) => void) | null;
|
|
44
45
|
}
|
|
46
|
+
/** Declared as a method so a `LobbySocket<Uint8Array>` still assigns to a plain `LobbySocket` (and back). */
|
|
47
|
+
type MessageHandler<D> = {
|
|
48
|
+
handle(ev: {
|
|
49
|
+
type: "message";
|
|
50
|
+
data: D;
|
|
51
|
+
}): void;
|
|
52
|
+
}["handle"];
|
|
45
53
|
export interface LobbySocketOptions {
|
|
46
54
|
/** Deliver `onmessage` data as bytes (Uint8Array) instead of UTF-8 text. Default false. */
|
|
47
55
|
binary?: boolean;
|
|
@@ -67,3 +75,4 @@ export declare function joinerSocket(lobby: Lobby, o?: LobbySocketOptions): Lobb
|
|
|
67
75
|
* that stops new connections (existing ones keep working).
|
|
68
76
|
*/
|
|
69
77
|
export declare function hostConnections(lobby: Lobby, handler: (conn: LobbySocket) => void, o?: LobbySocketOptions): () => void;
|
|
78
|
+
export {};
|
package/dist/socket.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"socket.js","sourceRoot":"","sources":["../src/socket.ts"],"names":[],"mappings":"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","sourcesContent":["import type { CloseInfo, Lobby, Peer } from \"./lobby.js\";\n\n/**\n * A WebSocket-shaped view of one peer link, so code written against `WebSocket` runs on a lobby unchanged:\n * `lobby.asWebSocket()` on a joiner (the link to the host), `lobby.onConnection(conn => ...)` on the host (one per\n * joiner). Not a real WebSocket: no URL, no subprotocols, no `addEventListener`; `bufferedAmount` is always 0.\n */\nexport interface LobbySocket {\n /** The peer at the other end (the host for a joiner's socket, the joiner for a host's connection). */\n readonly peerId: number;\n /** 0 CONNECTING (joiner, before `lobby.ready`), 1 OPEN, 3 CLOSED (there is no CLOSING). */\n readonly readyState: 0 | 1 | 3;\n readonly bufferedAmount: 0;\n /** A string is sent as UTF-8; bytes as they are. Ignored unless OPEN. */\n send(data: string | Uint8Array): void;\n /**\n * Closes this socket locally and fires `onclose` with `code` (default 1000). On a joiner it also leaves the lobby.\n * On the host it only stops this connection's events: to end the player's connection too (its socket gets your code\n * and reason), call `lobby.disconnect(conn.peerId, code, reason)`; `lobby.kick()` also bans it from the lobby.\n */\n close(code?: number, reason?: string): void;\n /**\n * Fires once, on a later task than the one that created the socket (like a real WebSocket). Assigned after it already\n * fired (the socket came out of an `await` chain)? It runs for the new handler once; `readyState === 1` also tells.\n */\n onopen: ((ev: { type: \"open\" }) => void) | null;\n /** `data` is a string (UTF-8 decoded), or the bytes with `{ binary: true }`. */\n onmessage: ((ev: { type: \"message\"; data: string | Uint8Array }) => void) | null;\n /** `reason` is the lobby's reason (\"host_left\", \"timeout\", \"kicked\", \"disconnected\", ...) unless `close()` or the host gave one. */\n onclose: ((ev: { type: \"close\"; code: number; reason: string; wasClean: boolean }) => void) | null;\n /** Never called (errors surface as `onclose` with code 1006); present so WebSocket code that assigns it works. */\n onerror: ((ev: { type: \"error\" }) => void) | null;\n}\n\nexport interface LobbySocketOptions {\n /** Deliver `onmessage` data as bytes (Uint8Array) instead of UTF-8 text. Default false. */\n binary?: boolean;\n /** Channel for `send` (default \"reliable\"). \"fast\" is unordered and may drop, like UDP. */\n channel?: \"reliable\" | \"fast\";\n}\n\n/**\n * Close codes: 1000 a normal leave, `close()` or the host's `disconnect()`, 1001 the other end went away (`host_left`,\n * the host closing, a player who left), 4000 kicked, 1006 anything abnormal (`timeout`, `ice_failed`, `server_lost`,\n * ...). A code and reason the host passed to `kick()` / `disconnect()` win over these.\n */\nexport function closeCodeFor(reason: string): number {\n if (reason === \"left\" || reason === \"disconnected\") return 1000;\n if (reason === \"host_left\" || reason === \"closed\") return 1001;\n if (reason === \"kicked\") return 4000;\n return 1006;\n}\n\n/** A later task, like a real WebSocket's events: setImmediate in Node, setTimeout(0) in browsers. */\nconst nextTask: (f: () => void) => void =\n (globalThis as { setImmediate?: (f: () => void) => void }).setImmediate ?? ((f) => void setTimeout(f, 0));\n\nconst enc = new TextEncoder();\nconst dec = new TextDecoder();\n\nclass PeerSocket implements LobbySocket {\n readyState: 0 | 1 | 3 = 0;\n readonly bufferedAmount = 0 as const;\n private openHandler: LobbySocket[\"onopen\"] = null;\n /** OPEN, but `onopen` has not run yet for any handler. */\n private openPending = false;\n onmessage: LobbySocket[\"onmessage\"] = null;\n onclose: LobbySocket[\"onclose\"] = null;\n onerror: LobbySocket[\"onerror\"] = null;\n /** Unsubscribes from the lobby; set by the factory. */\n detach: () => void = () => {};\n\n constructor(\n readonly peerId: number,\n private readonly out: (bytes: Uint8Array) => void,\n private readonly binary: boolean,\n private readonly onLocalClose: () => void,\n ) {}\n\n send(data: string | Uint8Array): void {\n if (this.readyState !== 1) return;\n this.out(typeof data === \"string\" ? enc.encode(data) : data);\n }\n\n close(code = 1000, reason = \"left\"): void {\n if (this.readyState === 3) return;\n this.closed(code, reason);\n this.onLocalClose();\n }\n\n get onopen(): LobbySocket[\"onopen\"] {\n return this.openHandler;\n }\n\n set onopen(h: LobbySocket[\"onopen\"]) {\n this.openHandler = h;\n // assigned after the open event went by with no handler: deliver it once, still on a later task\n if (h && this.openPending) nextTask(() => this.fireOpen());\n }\n\n /**\n * @internal OPEN now; `onopen` now, or on a later task (like a real WebSocket) so a handler assigned\n * after any number of awaits in the same task still runs.\n */\n open(later = false): void {\n if (this.readyState !== 0) return;\n this.readyState = 1;\n this.openPending = true;\n if (!later) this.fireOpen();\n else nextTask(() => this.fireOpen());\n }\n\n private fireOpen(): void {\n if (this.readyState !== 1 || !this.openPending || !this.openHandler) return;\n this.openPending = false;\n this.openHandler({ type: \"open\" });\n }\n\n /** @internal */\n deliver(bytes: Uint8Array): void {\n if (this.readyState === 1) this.onmessage?.({ type: \"message\", data: this.binary ? bytes : dec.decode(bytes) });\n }\n\n /** @internal */\n closed(code: number, reason: string): void {\n if (this.readyState === 3) return;\n this.readyState = 3;\n this.detach();\n this.onclose?.({ type: \"close\", code, reason, wasClean: code === 1000 || code === 1001 });\n }\n}\n\n/**\n * Joiner: the link to the host as a socket. OPEN once `lobby.ready` resolves (at once if it already has), with\n * `onopen` on a later task;\n * closed when the session ends, or with 1001 \"host_changed\" when a server-backed lobby migrates to a new host.\n */\nexport function joinerSocket(lobby: Lobby, o: LobbySocketOptions = {}): LobbySocket {\n if (lobby.isHost) throw new Error(\"asWebSocket() is for joiners; the host uses lobby.onConnection()\");\n const channel = o.channel ?? \"reliable\";\n const s = new PeerSocket(lobby.hostPeerId, (b) => lobby.send(lobby.hostPeerId, b, { channel }), !!o.binary, () => void lobby.leave());\n const offs = [\n lobby.on(\"message\", (from, bytes) => {\n if (from === lobby.hostPeerId) s.deliver(bytes);\n }),\n lobby.on(\"sessionEnded\", (reason, close?: CloseInfo) => s.closed(close?.code ?? closeCodeFor(reason), close?.reason ?? reason)),\n // The socket's far end is the host it opened to; after a migration that server is gone (direct lobbies end instead).\n lobby.on(\"hostChanged\", () => s.closed(1001, \"host_changed\")),\n ];\n s.detach = () => offs.forEach((f) => f());\n // Already connected (made after `await lobby.ready`): OPEN at once, so a send() right away goes out.\n if (lobby.connectedPeers.includes(lobby.hostPeerId)) s.open(true);\n else lobby.ready.then(() => s.open(true), () => {});\n return s;\n}\n\n/**\n * Host: calls `handler(conn)` once per joiner, as soon as its link is ready (and right away for joiners already\n * connected). A message from a joiner the handler has not seen yet creates its connection first, so nothing is lost.\n * `conn.onclose` fires when that peer leaves (code from closeCodeFor) or the host's session ends. Returns a function\n * that stops new connections (existing ones keep working).\n */\nexport function hostConnections(lobby: Lobby, handler: (conn: LobbySocket) => void, o: LobbySocketOptions = {}): () => void {\n if (!lobby.isHost) throw new Error(\"onConnection() is for the host; a joiner uses lobby.asWebSocket()\");\n const channel = o.channel ?? \"reliable\";\n const conns = new Map<number, PeerSocket>();\n let stopped = false;\n const ensure = (peerId: number): PeerSocket | undefined => {\n if (peerId === lobby.localPeerId) return undefined;\n let c = conns.get(peerId);\n if (c || stopped) return c;\n c = new PeerSocket(peerId, (b) => lobby.send(peerId, b, { channel }), !!o.binary, () => {});\n const conn = c;\n conn.detach = () => {\n if (conns.get(peerId) === conn) conns.delete(peerId);\n };\n conns.set(peerId, conn);\n conn.open();\n handler(conn);\n return conn;\n };\n const offs = [\n lobby.on(\"peerReady\", (p: Peer) => void ensure(p.peerId)),\n lobby.on(\"message\", (from, bytes) => ensure(from)?.deliver(bytes)),\n lobby.on(\"peerLeft\", (p: Peer, reason, close?: CloseInfo) =>\n conns.get(p.peerId)?.closed(close?.code ?? (reason === \"left\" ? 1001 : closeCodeFor(reason)), close?.reason ?? reason),\n ),\n lobby.on(\"sessionEnded\", (reason) => {\n for (const c of [...conns.values()]) c.closed(reason === \"left\" ? 1001 : closeCodeFor(reason), reason);\n offs.forEach((f) => f());\n }),\n ];\n for (const id of lobby.connectedPeers) ensure(id);\n return () => {\n stopped = true;\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"socket.js","sourceRoot":"","sources":["../src/socket.ts"],"names":[],"mappings":"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","sourcesContent":["import type { CloseInfo, Lobby, Peer } from \"./lobby.js\";\n\n/**\n * A WebSocket-shaped view of one peer link, so code written against `WebSocket` runs on a lobby unchanged:\n * `lobby.asWebSocket()` on a joiner (the link to the host), `lobby.onConnection(conn => ...)` on the host (one per\n * joiner). Not a real WebSocket: no URL, no subprotocols, no `addEventListener`; `bufferedAmount` is always 0.\n * `D` is what `onmessage` gets: `Uint8Array` with `{ binary: true }`, `string` without.\n */\nexport interface LobbySocket<D extends string | Uint8Array = string | Uint8Array> {\n /** The peer at the other end (the host for a joiner's socket, the joiner for a host's connection). */\n readonly peerId: number;\n /** 0 CONNECTING (joiner, before `lobby.ready`), 1 OPEN, 3 CLOSED (there is no CLOSING). */\n readonly readyState: 0 | 1 | 3;\n readonly bufferedAmount: 0;\n /** A string is sent as UTF-8; bytes as they are. Ignored unless OPEN. */\n send(data: string | Uint8Array): void;\n /**\n * Closes this socket locally and fires `onclose` with `code` (default 1000). On a joiner it also leaves the lobby.\n * On the host it only stops this connection's events: to end the player's connection too (its socket gets your code\n * and reason), call `lobby.disconnect(conn.peerId, code, reason)`; `lobby.kick()` also bans it from the lobby.\n */\n close(code?: number, reason?: string): void;\n /**\n * Fires once, on a later task than the one that created the socket (like a real WebSocket). Assigned after it already\n * fired (the socket came out of an `await` chain)? It runs for the new handler once; `readyState === 1` also tells.\n */\n onopen: ((ev: { type: \"open\" }) => void) | null;\n /**\n * `data` is a string (UTF-8 decoded), or with `{ binary: true }` always a Uint8Array. It can be a view into a larger\n * buffer (check `byteOffset`/`byteLength`): `data.slice()` before transferring its `.buffer` to a Worker.\n */\n onmessage: MessageHandler<D> | null;\n /** `reason` is the lobby's reason (\"host_left\", \"timeout\", \"kicked\", \"disconnected\", ...) unless `close()` or the host gave one. */\n onclose: ((ev: { type: \"close\"; code: number; reason: string; wasClean: boolean }) => void) | null;\n /** Never called (errors surface as `onclose` with code 1006); present so WebSocket code that assigns it works. */\n onerror: ((ev: { type: \"error\" }) => void) | null;\n}\n\n/** Declared as a method so a `LobbySocket<Uint8Array>` still assigns to a plain `LobbySocket` (and back). */\ntype MessageHandler<D> = { handle(ev: { type: \"message\"; data: D }): void }[\"handle\"];\n\nexport interface LobbySocketOptions {\n /** Deliver `onmessage` data as bytes (Uint8Array) instead of UTF-8 text. Default false. */\n binary?: boolean;\n /** Channel for `send` (default \"reliable\"). \"fast\" is unordered and may drop, like UDP. */\n channel?: \"reliable\" | \"fast\";\n}\n\n/**\n * Close codes: 1000 a normal leave, `close()` or the host's `disconnect()`, 1001 the other end went away (`host_left`,\n * the host closing, a player who left), 4000 kicked, 1006 anything abnormal (`timeout`, `ice_failed`, `server_lost`,\n * ...). A code and reason the host passed to `kick()` / `disconnect()` win over these.\n */\nexport function closeCodeFor(reason: string): number {\n if (reason === \"left\" || reason === \"disconnected\") return 1000;\n if (reason === \"host_left\" || reason === \"closed\") return 1001;\n if (reason === \"kicked\") return 4000;\n return 1006;\n}\n\n/** A later task, like a real WebSocket's events: setImmediate in Node, setTimeout(0) in browsers. */\nconst nextTask: (f: () => void) => void =\n (globalThis as { setImmediate?: (f: () => void) => void }).setImmediate ?? ((f) => void setTimeout(f, 0));\n\nconst enc = new TextEncoder();\nconst dec = new TextDecoder();\n\nclass PeerSocket implements LobbySocket {\n readyState: 0 | 1 | 3 = 0;\n readonly bufferedAmount = 0 as const;\n private openHandler: LobbySocket[\"onopen\"] = null;\n /** OPEN, but `onopen` has not run yet for any handler. */\n private openPending = false;\n onmessage: LobbySocket[\"onmessage\"] = null;\n onclose: LobbySocket[\"onclose\"] = null;\n onerror: LobbySocket[\"onerror\"] = null;\n /** Unsubscribes from the lobby; set by the factory. */\n detach: () => void = () => {};\n\n constructor(\n readonly peerId: number,\n private readonly out: (bytes: Uint8Array) => void,\n private readonly binary: boolean,\n private readonly onLocalClose: () => void,\n ) {}\n\n send(data: string | Uint8Array): void {\n if (this.readyState !== 1) return;\n this.out(typeof data === \"string\" ? enc.encode(data) : data);\n }\n\n close(code = 1000, reason = \"left\"): void {\n if (this.readyState === 3) return;\n this.closed(code, reason);\n this.onLocalClose();\n }\n\n get onopen(): LobbySocket[\"onopen\"] {\n return this.openHandler;\n }\n\n set onopen(h: LobbySocket[\"onopen\"]) {\n this.openHandler = h;\n // assigned after the open event went by with no handler: deliver it once, still on a later task\n if (h && this.openPending) nextTask(() => this.fireOpen());\n }\n\n /**\n * @internal OPEN now; `onopen` now, or on a later task (like a real WebSocket) so a handler assigned\n * after any number of awaits in the same task still runs.\n */\n open(later = false): void {\n if (this.readyState !== 0) return;\n this.readyState = 1;\n this.openPending = true;\n if (!later) this.fireOpen();\n else nextTask(() => this.fireOpen());\n }\n\n private fireOpen(): void {\n if (this.readyState !== 1 || !this.openPending || !this.openHandler) return;\n this.openPending = false;\n this.openHandler({ type: \"open\" });\n }\n\n /** @internal */\n deliver(bytes: Uint8Array): void {\n if (this.readyState === 1) this.onmessage?.({ type: \"message\", data: this.binary ? bytes : dec.decode(bytes) });\n }\n\n /** @internal */\n closed(code: number, reason: string): void {\n if (this.readyState === 3) return;\n this.readyState = 3;\n this.detach();\n this.onclose?.({ type: \"close\", code, reason, wasClean: code === 1000 || code === 1001 });\n }\n}\n\n/**\n * Joiner: the link to the host as a socket. OPEN once `lobby.ready` resolves (at once if it already has), with\n * `onopen` on a later task;\n * closed when the session ends, or with 1001 \"host_changed\" when a server-backed lobby migrates to a new host.\n */\nexport function joinerSocket(lobby: Lobby, o: LobbySocketOptions = {}): LobbySocket {\n if (lobby.isHost) throw new Error(\"asWebSocket() is for joiners; the host uses lobby.onConnection()\");\n const channel = o.channel ?? \"reliable\";\n const s = new PeerSocket(lobby.hostPeerId, (b) => lobby.send(lobby.hostPeerId, b, { channel }), !!o.binary, () => void lobby.leave());\n const offs = [\n lobby.on(\"message\", (from, bytes) => {\n if (from === lobby.hostPeerId) s.deliver(bytes);\n }),\n lobby.on(\"sessionEnded\", (reason, close?: CloseInfo) => s.closed(close?.code ?? closeCodeFor(reason), close?.reason ?? reason)),\n // The socket's far end is the host it opened to; after a migration that server is gone (direct lobbies end instead).\n lobby.on(\"hostChanged\", () => s.closed(1001, \"host_changed\")),\n ];\n s.detach = () => offs.forEach((f) => f());\n // Already connected (made after `await lobby.ready`): OPEN at once, so a send() right away goes out.\n if (lobby.connectedPeers.includes(lobby.hostPeerId)) s.open(true);\n else lobby.ready.then(() => s.open(true), () => {});\n return s;\n}\n\n/**\n * Host: calls `handler(conn)` once per joiner, as soon as its link is ready (and right away for joiners already\n * connected). A message from a joiner the handler has not seen yet creates its connection first, so nothing is lost.\n * `conn.onclose` fires when that peer leaves (code from closeCodeFor) or the host's session ends. Returns a function\n * that stops new connections (existing ones keep working).\n */\nexport function hostConnections(lobby: Lobby, handler: (conn: LobbySocket) => void, o: LobbySocketOptions = {}): () => void {\n if (!lobby.isHost) throw new Error(\"onConnection() is for the host; a joiner uses lobby.asWebSocket()\");\n const channel = o.channel ?? \"reliable\";\n const conns = new Map<number, PeerSocket>();\n let stopped = false;\n const ensure = (peerId: number): PeerSocket | undefined => {\n if (peerId === lobby.localPeerId) return undefined;\n let c = conns.get(peerId);\n if (c || stopped) return c;\n c = new PeerSocket(peerId, (b) => lobby.send(peerId, b, { channel }), !!o.binary, () => {});\n const conn = c;\n conn.detach = () => {\n if (conns.get(peerId) === conn) conns.delete(peerId);\n };\n conns.set(peerId, conn);\n conn.open();\n handler(conn);\n return conn;\n };\n const offs = [\n lobby.on(\"peerReady\", (p: Peer) => void ensure(p.peerId)),\n lobby.on(\"message\", (from, bytes) => ensure(from)?.deliver(bytes)),\n lobby.on(\"peerLeft\", (p: Peer, reason, close?: CloseInfo) =>\n conns.get(p.peerId)?.closed(close?.code ?? (reason === \"left\" ? 1001 : closeCodeFor(reason)), close?.reason ?? reason),\n ),\n lobby.on(\"sessionEnded\", (reason) => {\n for (const c of [...conns.values()]) c.closed(reason === \"left\" ? 1001 : closeCodeFor(reason), reason);\n offs.forEach((f) => f());\n }),\n ];\n for (const id of lobby.connectedPeers) ensure(id);\n return () => {\n stopped = true;\n };\n}\n"]}
|
package/dist/testing.d.ts
CHANGED
|
@@ -102,6 +102,8 @@ export declare class MemoryServer {
|
|
|
102
102
|
iceServers: {
|
|
103
103
|
urls: string[];
|
|
104
104
|
}[];
|
|
105
|
+
/** What `welcome.features` lists; `[]` plays an older hub that cannot rename. */
|
|
106
|
+
features: string[];
|
|
105
107
|
/** Log of every client message, for assertions. */
|
|
106
108
|
readonly received: Json[];
|
|
107
109
|
connect(): MemorySocket;
|
package/dist/testing.js
CHANGED
|
@@ -191,6 +191,8 @@ export class MemoryServer {
|
|
|
191
191
|
socketFactory = () => this.connect();
|
|
192
192
|
n = 0;
|
|
193
193
|
iceServers = [{ urls: ["stun:stun.example:3478"] }];
|
|
194
|
+
/** What `welcome.features` lists; `[]` plays an older hub that cannot rename. */
|
|
195
|
+
features = ["set_name"];
|
|
194
196
|
/** Log of every client message, for assertions. */
|
|
195
197
|
received = [];
|
|
196
198
|
connect() {
|
|
@@ -320,11 +322,11 @@ export class MemorySocket {
|
|
|
320
322
|
l.peers.set(rp.peerId, rp);
|
|
321
323
|
this.lobby = l;
|
|
322
324
|
this.peer = rp;
|
|
323
|
-
this.push({ t: "welcome", peerId: rp.peerId, ice: this.srv.iceServers, serverTime: Date.now(), limits: { maxMsg: 16384, maxPlayers: 64 } });
|
|
325
|
+
this.push({ t: "welcome", peerId: rp.peerId, ice: this.srv.iceServers, serverTime: Date.now(), limits: { maxMsg: 16384, maxPlayers: 64 }, features: this.srv.features });
|
|
324
326
|
this.push(this.lobbyMsg(l, rp));
|
|
325
327
|
return this.broadcast(l, { t: "peer_joined", peer: { peerId: rp.peerId, name: rp.name, joinedAt: rp.joinedAt } }, rp.peerId);
|
|
326
328
|
}
|
|
327
|
-
return this.push({ t: "welcome", ice: this.srv.iceServers, serverTime: Date.now(), limits: { maxMsg: 16384, maxPlayers: 64 } });
|
|
329
|
+
return this.push({ t: "welcome", ice: this.srv.iceServers, serverTime: Date.now(), limits: { maxMsg: 16384, maxPlayers: 64 }, features: this.srv.features });
|
|
328
330
|
}
|
|
329
331
|
if (!this.hello)
|
|
330
332
|
return this.err("bad_request", "hello first");
|
|
@@ -385,6 +387,20 @@ export class MemorySocket {
|
|
|
385
387
|
return this.err("bad_request", "no such peer");
|
|
386
388
|
return to.sock?.push({ t: "signal", from: this.peer.peerId, ...(m.sdp ? { sdp: m.sdp } : {}), ...(m.ice ? { ice: m.ice } : {}) });
|
|
387
389
|
}
|
|
390
|
+
case "set_name": {
|
|
391
|
+
const l = this.lobby;
|
|
392
|
+
if (!l || !this.peer)
|
|
393
|
+
return this.err("bad_request", "not in a lobby");
|
|
394
|
+
const name = typeof m.name === "string" ? m.name.trim().slice(0, 24) : "";
|
|
395
|
+
if (!name)
|
|
396
|
+
return this.err("bad_request", "name must be a non-empty string");
|
|
397
|
+
if (name === this.peer.name)
|
|
398
|
+
return;
|
|
399
|
+
this.peer.name = name;
|
|
400
|
+
for (const q of l.peers.values())
|
|
401
|
+
q.sock?.push(this.lobbyMsg(l, q));
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
388
404
|
case "meta":
|
|
389
405
|
case "phase": {
|
|
390
406
|
const l = this.lobby;
|