onelobby 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +77 -2
- package/dist/chat.d.ts +60 -0
- package/dist/chat.js +163 -0
- package/dist/chat.js.map +1 -0
- package/dist/chunking.d.ts +17 -0
- package/dist/chunking.js +59 -0
- package/dist/chunking.js.map +1 -0
- package/dist/clock.d.ts +8 -0
- package/dist/clock.js +39 -0
- package/dist/clock.js.map +1 -0
- package/dist/codec.d.ts +141 -0
- package/dist/codec.js +308 -0
- package/dist/codec.js.map +1 -0
- package/dist/crockford.d.ts +14 -0
- package/dist/crockford.js +55 -0
- package/dist/crockford.js.map +1 -0
- package/dist/deflate.d.ts +4 -0
- package/dist/deflate.js +32 -0
- package/dist/deflate.js.map +1 -0
- package/dist/deprecated.d.ts +18 -0
- package/dist/deprecated.js +11 -0
- package/dist/deprecated.js.map +1 -0
- package/dist/direct/hub-host.d.ts +82 -0
- package/dist/direct/hub-host.js +393 -0
- package/dist/direct/hub-host.js.map +1 -0
- package/dist/direct/hub.d.ts +178 -0
- package/dist/direct/hub.js +862 -0
- package/dist/direct/hub.js.map +1 -0
- package/dist/direct/index.d.ts +55 -0
- package/dist/direct/index.js +71 -0
- package/dist/direct/index.js.map +1 -0
- package/dist/direct/portmap.d.ts +84 -0
- package/dist/direct/portmap.js +465 -0
- package/dist/direct/portmap.js.map +1 -0
- package/dist/direct-address.d.ts +20 -0
- package/dist/direct-address.js +61 -0
- package/dist/direct-address.js.map +1 -0
- package/dist/emitter.d.ts +9 -0
- package/dist/emitter.js +36 -0
- package/dist/emitter.js.map +1 -0
- package/dist/friendly.d.ts +20 -0
- package/dist/friendly.js +120 -0
- package/dist/friendly.js.map +1 -0
- package/dist/global.d.ts +8 -0
- package/dist/global.js +9 -0
- package/dist/global.js.map +1 -0
- package/dist/gridpaint.d.ts +27 -0
- package/dist/gridpaint.js +106 -0
- package/dist/gridpaint.js.map +1 -0
- package/dist/hub.d.ts +25 -0
- package/dist/hub.js +2 -0
- package/dist/hub.js.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/internals.d.ts +28 -0
- package/dist/internals.js +24 -0
- package/dist/internals.js.map +1 -0
- package/dist/joincode.d.ts +21 -0
- package/dist/joincode.js +84 -0
- package/dist/joincode.js.map +1 -0
- package/dist/lobby.d.ts +170 -0
- package/dist/lobby.js +778 -0
- package/dist/lobby.js.map +1 -0
- package/dist/lockstep.d.ts +132 -0
- package/dist/lockstep.js +471 -0
- package/dist/lockstep.js.map +1 -0
- package/dist/migration.d.ts +17 -0
- package/dist/migration.js +34 -0
- package/dist/migration.js.map +1 -0
- package/dist/onelobby.bundle.mjs +140 -0
- package/dist/onelobby.bundle.mjs.map +7 -0
- package/dist/onelobby.d.ts +142 -0
- package/dist/onelobby.global.js +3730 -0
- package/dist/onelobby.global.js.map +7 -0
- package/dist/onelobby.global.min.js +140 -0
- package/dist/onelobby.global.min.js.map +7 -0
- package/dist/onelobby.js +334 -0
- package/dist/onelobby.js.map +1 -0
- package/dist/signaling.d.ts +94 -0
- package/dist/signaling.js +134 -0
- package/dist/signaling.js.map +1 -0
- package/dist/testing.d.ts +135 -0
- package/dist/testing.js +438 -0
- package/dist/testing.js.map +1 -0
- package/dist/transport.d.ts +80 -0
- package/dist/transport.js +7 -0
- package/dist/transport.js.map +1 -0
- package/dist/ui/controller.d.ts +137 -0
- package/dist/ui/controller.js +337 -0
- package/dist/ui/controller.js.map +1 -0
- package/dist/ui/element.d.ts +22 -0
- package/dist/ui/element.js +284 -0
- package/dist/ui/element.js.map +1 -0
- package/dist/ui/index.d.ts +5 -0
- package/dist/ui/index.js +9 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/server-url.d.ts +9 -0
- package/dist/ui/server-url.js +39 -0
- package/dist/ui/server-url.js.map +1 -0
- package/dist/ui/styles.d.ts +2 -0
- package/dist/ui/styles.js +70 -0
- package/dist/ui/styles.js.map +1 -0
- package/dist/voice-browser.d.ts +19 -0
- package/dist/voice-browser.js +151 -0
- package/dist/voice-browser.js.map +1 -0
- package/dist/voice-core.d.ts +98 -0
- package/dist/voice-core.js +233 -0
- package/dist/voice-core.js.map +1 -0
- package/dist/voice-types.d.ts +35 -0
- package/dist/voice-types.js +3 -0
- package/dist/voice-types.js.map +1 -0
- package/dist/voice.d.ts +68 -0
- package/dist/voice.js +223 -0
- package/dist/voice.js.map +1 -0
- package/dist/webrtc.d.ts +29 -0
- package/dist/webrtc.js +169 -0
- package/dist/webrtc.js.map +1 -0
- package/package.json +93 -4
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test doubles: a manual clock, an in-memory lobby server speaking the lk1 protocol (spec/signaling.md, the parts the
|
|
3
|
+
* client uses) and an in-memory peer network. Exported as `onelobby/testing`.
|
|
4
|
+
*/
|
|
5
|
+
import type { Clock } from "./clock.js";
|
|
6
|
+
import type { ChannelName, LinkFactory, LinkOptions, PeerLink, SignalPayload, SocketFactory, SocketLike } from "./transport.js";
|
|
7
|
+
/** Deterministic clock: time only moves through `advance()`. Timers fire in (time, creation) order. */
|
|
8
|
+
export declare class ManualClock implements Clock {
|
|
9
|
+
private t;
|
|
10
|
+
private seq;
|
|
11
|
+
private timers;
|
|
12
|
+
now(): number;
|
|
13
|
+
setInterval(fn: () => void, ms: number): () => void;
|
|
14
|
+
setTimeout(fn: () => void, ms: number): () => void;
|
|
15
|
+
/** Advance time, firing due timers one at a time (microtasks flush between them when awaited via `run`). */
|
|
16
|
+
advance(ms: number): void;
|
|
17
|
+
/** Advance in `step` ms slices, letting promises and queued microtasks settle between slices. */
|
|
18
|
+
run(ms: number, step?: number): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
/** Let all pending microtasks (and chained promise continuations) run. */
|
|
21
|
+
export declare function settle(rounds?: number): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* Pairs `PeerLink`s through the signaling path like WebRTC would: the offerer's SDP string names it, the answerer
|
|
24
|
+
* resolves that name, and both open one microtask later. Messages are delivered FIFO, one microtask later.
|
|
25
|
+
*/
|
|
26
|
+
export declare class MemoryNetwork {
|
|
27
|
+
private offers;
|
|
28
|
+
/** Every link ever created (for assertions and fault injection). */
|
|
29
|
+
readonly links: MemoryLink[];
|
|
30
|
+
/** Set to drop all traffic in one direction pair for fault injection: key `${from}->${to}` is not tracked; use link.mute(). */
|
|
31
|
+
readonly factory: LinkFactory;
|
|
32
|
+
/** @internal */
|
|
33
|
+
register(offer: string, link: MemoryLink): void;
|
|
34
|
+
/** @internal */
|
|
35
|
+
lookup(offer: string): MemoryLink | undefined;
|
|
36
|
+
/** Kill every link whose far end is this identity-tagged factory owner (set `tag` via MemoryLink.owner). */
|
|
37
|
+
killOwner(owner: string): void;
|
|
38
|
+
/** Per-owner factory so tests can tell whose link is whose and cut one peer off. */
|
|
39
|
+
factoryFor(owner: string): LinkFactory;
|
|
40
|
+
}
|
|
41
|
+
export declare class MemoryLink implements PeerLink {
|
|
42
|
+
private net;
|
|
43
|
+
readonly remoteId: number;
|
|
44
|
+
isOpen: boolean;
|
|
45
|
+
owner: string;
|
|
46
|
+
onSignal: PeerLink["onSignal"];
|
|
47
|
+
onOpen: PeerLink["onOpen"];
|
|
48
|
+
onClose: PeerLink["onClose"];
|
|
49
|
+
onMessage: PeerLink["onMessage"];
|
|
50
|
+
onPath: PeerLink["onPath"];
|
|
51
|
+
/** Bytes sent per channel, for assertions. */
|
|
52
|
+
readonly sent: Record<ChannelName, number>;
|
|
53
|
+
/** While true, outgoing messages vanish (a frozen or partitioned peer). */
|
|
54
|
+
blackhole: boolean;
|
|
55
|
+
private peer;
|
|
56
|
+
private closed;
|
|
57
|
+
private id;
|
|
58
|
+
constructor(net: MemoryNetwork, opts: LinkOptions);
|
|
59
|
+
startOffer(): void;
|
|
60
|
+
handleSignal(s: SignalPayload): void;
|
|
61
|
+
/** @internal */
|
|
62
|
+
accept(answerer: MemoryLink): void;
|
|
63
|
+
private open;
|
|
64
|
+
send(channel: ChannelName, bytes: Uint8Array): void;
|
|
65
|
+
close(): void;
|
|
66
|
+
private closeFromRemote;
|
|
67
|
+
}
|
|
68
|
+
type Json = Record<string, unknown>;
|
|
69
|
+
interface MPeer {
|
|
70
|
+
peerId: number;
|
|
71
|
+
name: string;
|
|
72
|
+
joinedAt: number;
|
|
73
|
+
sock: MemorySocket | null;
|
|
74
|
+
token: string;
|
|
75
|
+
}
|
|
76
|
+
interface MLobby {
|
|
77
|
+
code: string;
|
|
78
|
+
gameId: string;
|
|
79
|
+
netVersion: string;
|
|
80
|
+
name: string;
|
|
81
|
+
hostId: number;
|
|
82
|
+
epoch: number;
|
|
83
|
+
mode: string;
|
|
84
|
+
tickRate: number;
|
|
85
|
+
lateJoin: boolean;
|
|
86
|
+
phase: string;
|
|
87
|
+
meta: Record<string, string>;
|
|
88
|
+
visibility: string;
|
|
89
|
+
maxPlayers: number;
|
|
90
|
+
peers: Map<number, MPeer>;
|
|
91
|
+
next: number;
|
|
92
|
+
kicked: Set<number>;
|
|
93
|
+
reserved: Map<string, MPeer>;
|
|
94
|
+
}
|
|
95
|
+
/** Minimal onelobby-server (lk1) for client tests. Not a conformance target: stream A owns the real server. */
|
|
96
|
+
export declare class MemoryServer {
|
|
97
|
+
readonly lobbies: Map<string, MLobby>;
|
|
98
|
+
/** Every socket ever opened (to drop one from a test). */
|
|
99
|
+
readonly sockets: MemorySocket[];
|
|
100
|
+
readonly socketFactory: SocketFactory;
|
|
101
|
+
private n;
|
|
102
|
+
iceServers: {
|
|
103
|
+
urls: string[];
|
|
104
|
+
}[];
|
|
105
|
+
/** Log of every client message, for assertions. */
|
|
106
|
+
readonly received: Json[];
|
|
107
|
+
connect(): MemorySocket;
|
|
108
|
+
/** @internal */
|
|
109
|
+
newCode(): string;
|
|
110
|
+
}
|
|
111
|
+
export declare class MemorySocket implements SocketLike {
|
|
112
|
+
private srv;
|
|
113
|
+
readyState: number;
|
|
114
|
+
onopen: SocketLike["onopen"];
|
|
115
|
+
onmessage: SocketLike["onmessage"];
|
|
116
|
+
onclose: SocketLike["onclose"];
|
|
117
|
+
onerror: SocketLike["onerror"];
|
|
118
|
+
private hello;
|
|
119
|
+
private lobby;
|
|
120
|
+
private peer;
|
|
121
|
+
private closed;
|
|
122
|
+
constructor(srv: MemoryServer);
|
|
123
|
+
send(data: string): void;
|
|
124
|
+
close(): void;
|
|
125
|
+
/** @internal server -> this client */
|
|
126
|
+
push(m: Json): void;
|
|
127
|
+
private err;
|
|
128
|
+
private lobbyMsg;
|
|
129
|
+
private broadcast;
|
|
130
|
+
private leaveLobby;
|
|
131
|
+
private handle;
|
|
132
|
+
}
|
|
133
|
+
/** Convenience: kill a socket as if the network dropped (server sees a timeout, others get peer_left). */
|
|
134
|
+
export declare function dropSocket(s: SocketLike): void;
|
|
135
|
+
export {};
|
package/dist/testing.js
ADDED
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
import { CROCKFORD_ALPHABET, normalizeCode } from "./crockford.js";
|
|
2
|
+
/** Deterministic clock: time only moves through `advance()`. Timers fire in (time, creation) order. */
|
|
3
|
+
export class ManualClock {
|
|
4
|
+
t = 0;
|
|
5
|
+
seq = 0;
|
|
6
|
+
timers = [];
|
|
7
|
+
now() {
|
|
8
|
+
return this.t;
|
|
9
|
+
}
|
|
10
|
+
setInterval(fn, ms) {
|
|
11
|
+
const timer = { at: this.t + ms, seq: this.seq++, fn, every: Math.max(1, ms) };
|
|
12
|
+
this.timers.push(timer);
|
|
13
|
+
return () => {
|
|
14
|
+
timer.dead = true;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
setTimeout(fn, ms) {
|
|
18
|
+
const timer = { at: this.t + ms, seq: this.seq++, fn };
|
|
19
|
+
this.timers.push(timer);
|
|
20
|
+
return () => {
|
|
21
|
+
timer.dead = true;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/** Advance time, firing due timers one at a time (microtasks flush between them when awaited via `run`). */
|
|
25
|
+
advance(ms) {
|
|
26
|
+
const end = this.t + ms;
|
|
27
|
+
for (;;) {
|
|
28
|
+
this.timers = this.timers.filter((x) => !x.dead);
|
|
29
|
+
let next;
|
|
30
|
+
for (const x of this.timers)
|
|
31
|
+
if (x.at <= end && (!next || x.at < next.at || (x.at === next.at && x.seq < next.seq)))
|
|
32
|
+
next = x;
|
|
33
|
+
if (!next)
|
|
34
|
+
break;
|
|
35
|
+
this.t = Math.max(this.t, next.at);
|
|
36
|
+
if (next.every) {
|
|
37
|
+
next.at += next.every;
|
|
38
|
+
next.seq = this.seq++;
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
next.dead = true;
|
|
42
|
+
}
|
|
43
|
+
next.fn();
|
|
44
|
+
}
|
|
45
|
+
this.t = end;
|
|
46
|
+
}
|
|
47
|
+
/** Advance in `step` ms slices, letting promises and queued microtasks settle between slices. */
|
|
48
|
+
async run(ms, step = 10) {
|
|
49
|
+
let left = ms;
|
|
50
|
+
await settle();
|
|
51
|
+
while (left > 0) {
|
|
52
|
+
const d = Math.min(step, left);
|
|
53
|
+
this.advance(d);
|
|
54
|
+
left -= d;
|
|
55
|
+
await settle();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** Let all pending microtasks (and chained promise continuations) run. */
|
|
60
|
+
export async function settle(rounds = 8) {
|
|
61
|
+
for (let i = 0; i < rounds; i++)
|
|
62
|
+
await Promise.resolve();
|
|
63
|
+
// A macrotask boundary guarantees the whole microtask queue (delivery chains) has drained.
|
|
64
|
+
await new Promise((r) => (globalThis.setImmediate?.(r) ?? setTimeout(r, 0)));
|
|
65
|
+
}
|
|
66
|
+
// ---- in-memory peer network ---------------------------------------------------------------------------------------
|
|
67
|
+
let linkSerial = 0;
|
|
68
|
+
/**
|
|
69
|
+
* Pairs `PeerLink`s through the signaling path like WebRTC would: the offerer's SDP string names it, the answerer
|
|
70
|
+
* resolves that name, and both open one microtask later. Messages are delivered FIFO, one microtask later.
|
|
71
|
+
*/
|
|
72
|
+
export class MemoryNetwork {
|
|
73
|
+
offers = new Map();
|
|
74
|
+
/** Every link ever created (for assertions and fault injection). */
|
|
75
|
+
links = [];
|
|
76
|
+
/** Set to drop all traffic in one direction pair for fault injection: key `${from}->${to}` is not tracked; use link.mute(). */
|
|
77
|
+
factory = { create: (opts) => new MemoryLink(this, opts) };
|
|
78
|
+
/** @internal */
|
|
79
|
+
register(offer, link) {
|
|
80
|
+
this.offers.set(offer, link);
|
|
81
|
+
this.links.push(link);
|
|
82
|
+
}
|
|
83
|
+
/** @internal */
|
|
84
|
+
lookup(offer) {
|
|
85
|
+
return this.offers.get(offer);
|
|
86
|
+
}
|
|
87
|
+
/** Kill every link whose far end is this identity-tagged factory owner (set `tag` via MemoryLink.owner). */
|
|
88
|
+
killOwner(owner) {
|
|
89
|
+
for (const l of this.links)
|
|
90
|
+
if (l.owner === owner)
|
|
91
|
+
l.close();
|
|
92
|
+
}
|
|
93
|
+
/** Per-owner factory so tests can tell whose link is whose and cut one peer off. */
|
|
94
|
+
factoryFor(owner) {
|
|
95
|
+
return {
|
|
96
|
+
create: (opts) => {
|
|
97
|
+
const l = new MemoryLink(this, opts);
|
|
98
|
+
l.owner = owner;
|
|
99
|
+
return l;
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
export class MemoryLink {
|
|
105
|
+
net;
|
|
106
|
+
remoteId;
|
|
107
|
+
isOpen = false;
|
|
108
|
+
owner = "";
|
|
109
|
+
onSignal = null;
|
|
110
|
+
onOpen = null;
|
|
111
|
+
onClose = null;
|
|
112
|
+
onMessage = null;
|
|
113
|
+
onPath = null;
|
|
114
|
+
/** Bytes sent per channel, for assertions. */
|
|
115
|
+
sent = { reliable: 0, fast: 0, voice: 0 };
|
|
116
|
+
/** While true, outgoing messages vanish (a frozen or partitioned peer). */
|
|
117
|
+
blackhole = false;
|
|
118
|
+
peer = null;
|
|
119
|
+
closed = false;
|
|
120
|
+
id = `mem-${++linkSerial}`;
|
|
121
|
+
constructor(net, opts) {
|
|
122
|
+
this.net = net;
|
|
123
|
+
this.remoteId = opts.remoteId;
|
|
124
|
+
}
|
|
125
|
+
startOffer() {
|
|
126
|
+
this.net.register(this.id, this);
|
|
127
|
+
queueMicrotask(() => this.onSignal?.({ sdp: { type: "offer", sdp: this.id } }));
|
|
128
|
+
}
|
|
129
|
+
handleSignal(s) {
|
|
130
|
+
if (s.sdp?.type === "offer") {
|
|
131
|
+
const offerer = this.net.lookup(s.sdp.sdp);
|
|
132
|
+
this.net.links.push(this);
|
|
133
|
+
if (!offerer)
|
|
134
|
+
return;
|
|
135
|
+
this.peer = offerer;
|
|
136
|
+
queueMicrotask(() => this.onSignal?.({ sdp: { type: "answer", sdp: this.id } }));
|
|
137
|
+
queueMicrotask(() => offerer.accept(this));
|
|
138
|
+
queueMicrotask(() => this.open());
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/** @internal */
|
|
142
|
+
accept(answerer) {
|
|
143
|
+
this.peer = answerer;
|
|
144
|
+
this.open();
|
|
145
|
+
}
|
|
146
|
+
open() {
|
|
147
|
+
if (this.closed || this.isOpen)
|
|
148
|
+
return;
|
|
149
|
+
this.isOpen = true;
|
|
150
|
+
this.onOpen?.();
|
|
151
|
+
this.onPath?.("direct");
|
|
152
|
+
}
|
|
153
|
+
send(channel, bytes) {
|
|
154
|
+
if (!this.isOpen || this.closed)
|
|
155
|
+
return;
|
|
156
|
+
this.sent[channel] += bytes.length;
|
|
157
|
+
if (this.blackhole)
|
|
158
|
+
return;
|
|
159
|
+
const peer = this.peer;
|
|
160
|
+
const copy = bytes.slice();
|
|
161
|
+
queueMicrotask(() => {
|
|
162
|
+
if (peer && peer.isOpen && !peer.closed && !this.closed)
|
|
163
|
+
peer.onMessage?.(channel, copy);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
close() {
|
|
167
|
+
if (this.closed)
|
|
168
|
+
return;
|
|
169
|
+
this.closed = true;
|
|
170
|
+
this.isOpen = false;
|
|
171
|
+
const peer = this.peer;
|
|
172
|
+
queueMicrotask(() => {
|
|
173
|
+
this.onClose?.("closed");
|
|
174
|
+
if (peer && !peer.closed)
|
|
175
|
+
peer.closeFromRemote();
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
closeFromRemote() {
|
|
179
|
+
if (this.closed)
|
|
180
|
+
return;
|
|
181
|
+
this.closed = true;
|
|
182
|
+
this.isOpen = false;
|
|
183
|
+
this.onClose?.("remote closed");
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/** Minimal onelobby-server (lk1) for client tests. Not a conformance target: stream A owns the real server. */
|
|
187
|
+
export class MemoryServer {
|
|
188
|
+
lobbies = new Map();
|
|
189
|
+
/** Every socket ever opened (to drop one from a test). */
|
|
190
|
+
sockets = [];
|
|
191
|
+
socketFactory = () => this.connect();
|
|
192
|
+
n = 0;
|
|
193
|
+
iceServers = [{ urls: ["stun:stun.example:3478"] }];
|
|
194
|
+
/** Log of every client message, for assertions. */
|
|
195
|
+
received = [];
|
|
196
|
+
connect() {
|
|
197
|
+
const s = new MemorySocket(this);
|
|
198
|
+
this.sockets.push(s);
|
|
199
|
+
return s;
|
|
200
|
+
}
|
|
201
|
+
/** @internal */
|
|
202
|
+
newCode() {
|
|
203
|
+
let x = 0x9e3779b1 * (++this.n + 17);
|
|
204
|
+
let s = "";
|
|
205
|
+
for (let i = 0; i < 6; i++) {
|
|
206
|
+
x = (Math.imul(x, 1103515245) + 12345) >>> 0;
|
|
207
|
+
s += CROCKFORD_ALPHABET[(x >>> 16) & 31];
|
|
208
|
+
}
|
|
209
|
+
return this.lobbies.has(s) ? this.newCode() : s;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
export class MemorySocket {
|
|
213
|
+
srv;
|
|
214
|
+
readyState = 0;
|
|
215
|
+
onopen = null;
|
|
216
|
+
onmessage = null;
|
|
217
|
+
onclose = null;
|
|
218
|
+
onerror = null;
|
|
219
|
+
hello = null;
|
|
220
|
+
lobby = null;
|
|
221
|
+
peer = null;
|
|
222
|
+
closed = false;
|
|
223
|
+
constructor(srv) {
|
|
224
|
+
this.srv = srv;
|
|
225
|
+
queueMicrotask(() => {
|
|
226
|
+
this.readyState = 1;
|
|
227
|
+
this.onopen?.();
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
// ---- socket API
|
|
231
|
+
send(data) {
|
|
232
|
+
if (this.closed)
|
|
233
|
+
return;
|
|
234
|
+
const m = JSON.parse(data);
|
|
235
|
+
queueMicrotask(() => this.handle(m));
|
|
236
|
+
}
|
|
237
|
+
close() {
|
|
238
|
+
if (this.closed)
|
|
239
|
+
return;
|
|
240
|
+
this.closed = true;
|
|
241
|
+
this.readyState = 3;
|
|
242
|
+
this.leaveLobby("timeout");
|
|
243
|
+
queueMicrotask(() => this.onclose?.());
|
|
244
|
+
}
|
|
245
|
+
/** @internal server -> this client */
|
|
246
|
+
push(m) {
|
|
247
|
+
if (this.closed)
|
|
248
|
+
return;
|
|
249
|
+
queueMicrotask(() => this.onmessage?.({ data: JSON.stringify(m) }));
|
|
250
|
+
}
|
|
251
|
+
err(code, msg = "", data) {
|
|
252
|
+
this.push({ t: "error", code, msg, ...(data ? { data } : {}) });
|
|
253
|
+
}
|
|
254
|
+
lobbyMsg(l, p) {
|
|
255
|
+
return {
|
|
256
|
+
t: "lobby",
|
|
257
|
+
code: l.code,
|
|
258
|
+
gameId: l.gameId,
|
|
259
|
+
netVersion: l.netVersion,
|
|
260
|
+
hostId: l.hostId,
|
|
261
|
+
you: p.peerId,
|
|
262
|
+
mode: l.mode,
|
|
263
|
+
tickRate: l.tickRate,
|
|
264
|
+
lateJoin: l.lateJoin,
|
|
265
|
+
phase: l.phase,
|
|
266
|
+
meta: l.meta,
|
|
267
|
+
peers: [...l.peers.values()].map((q) => ({ peerId: q.peerId, name: q.name, joinedAt: q.joinedAt })),
|
|
268
|
+
resumeToken: p.token,
|
|
269
|
+
name: l.name,
|
|
270
|
+
maxPlayers: l.maxPlayers,
|
|
271
|
+
visibility: l.visibility,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
broadcast(l, m, except) {
|
|
275
|
+
for (const q of l.peers.values())
|
|
276
|
+
if (q.peerId !== except)
|
|
277
|
+
q.sock?.push(m);
|
|
278
|
+
}
|
|
279
|
+
leaveLobby(reason) {
|
|
280
|
+
const l = this.lobby;
|
|
281
|
+
const p = this.peer;
|
|
282
|
+
if (!l || !p)
|
|
283
|
+
return;
|
|
284
|
+
this.lobby = null;
|
|
285
|
+
this.peer = null;
|
|
286
|
+
p.sock = null;
|
|
287
|
+
l.peers.delete(p.peerId);
|
|
288
|
+
if (reason === "timeout")
|
|
289
|
+
l.reserved.set(p.token, p); // slot kept for resume
|
|
290
|
+
if (l.peers.size === 0) {
|
|
291
|
+
this.srv.lobbies.delete(l.code);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
this.broadcast(l, { t: "peer_left", peerId: p.peerId, reason });
|
|
295
|
+
if (l.hostId === p.peerId) {
|
|
296
|
+
l.hostId = Math.min(...l.peers.keys());
|
|
297
|
+
l.epoch++;
|
|
298
|
+
this.broadcast(l, { t: "host_changed", hostId: l.hostId, epoch: l.epoch });
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
handle(m) {
|
|
302
|
+
if (this.closed)
|
|
303
|
+
return;
|
|
304
|
+
this.srv.received.push(m);
|
|
305
|
+
const t = String(m.t);
|
|
306
|
+
if (t === "ping")
|
|
307
|
+
return this.push({ t: "pong", n: m.n });
|
|
308
|
+
if (t === "hello") {
|
|
309
|
+
if (this.hello)
|
|
310
|
+
return this.err("bad_request", "second hello");
|
|
311
|
+
this.hello = m;
|
|
312
|
+
const tok = typeof m.resumeToken === "string" ? m.resumeToken : "";
|
|
313
|
+
for (const l of this.srv.lobbies.values()) {
|
|
314
|
+
const rp = l.reserved.get(tok);
|
|
315
|
+
if (!rp)
|
|
316
|
+
continue;
|
|
317
|
+
l.reserved.delete(tok);
|
|
318
|
+
rp.sock = this;
|
|
319
|
+
rp.token = tok + "+";
|
|
320
|
+
l.peers.set(rp.peerId, rp);
|
|
321
|
+
this.lobby = l;
|
|
322
|
+
this.peer = rp;
|
|
323
|
+
this.push({ t: "welcome", peerId: rp.peerId, ice: this.srv.iceServers, serverTime: Date.now(), limits: { maxMsg: 16384, maxPlayers: 64 } });
|
|
324
|
+
this.push(this.lobbyMsg(l, rp));
|
|
325
|
+
return this.broadcast(l, { t: "peer_joined", peer: { peerId: rp.peerId, name: rp.name, joinedAt: rp.joinedAt } }, rp.peerId);
|
|
326
|
+
}
|
|
327
|
+
return this.push({ t: "welcome", ice: this.srv.iceServers, serverTime: Date.now(), limits: { maxMsg: 16384, maxPlayers: 64 } });
|
|
328
|
+
}
|
|
329
|
+
if (!this.hello)
|
|
330
|
+
return this.err("bad_request", "hello first");
|
|
331
|
+
const gameId = String(this.hello.gameId);
|
|
332
|
+
const netVersion = String(this.hello.netVersion);
|
|
333
|
+
switch (t) {
|
|
334
|
+
case "create": {
|
|
335
|
+
const code = this.srv.newCode();
|
|
336
|
+
const p = { peerId: 1, name: String(this.hello.name), joinedAt: Date.now(), sock: this, token: `tok-${code}-1` };
|
|
337
|
+
const l = {
|
|
338
|
+
code, gameId, netVersion, name: String(m.name), hostId: 1, epoch: 0, mode: String(m.mode), tickRate: Number(m.tickRate),
|
|
339
|
+
lateJoin: !!m.lateJoin, phase: "lobby", meta: m.meta ?? {}, visibility: String(m.visibility),
|
|
340
|
+
maxPlayers: Number(m.maxPlayers), peers: new Map([[1, p]]), next: 2, kicked: new Set(), reserved: new Map(),
|
|
341
|
+
};
|
|
342
|
+
this.srv.lobbies.set(code, l);
|
|
343
|
+
this.lobby = l;
|
|
344
|
+
this.peer = p;
|
|
345
|
+
return this.push(this.lobbyMsg(l, p));
|
|
346
|
+
}
|
|
347
|
+
case "join": {
|
|
348
|
+
const code = normalizeCode(String(m.code));
|
|
349
|
+
if (!code)
|
|
350
|
+
return this.err("bad_request", "bad code");
|
|
351
|
+
const l = this.srv.lobbies.get(code);
|
|
352
|
+
if (!l || l.gameId !== gameId)
|
|
353
|
+
return this.err("unknown_code");
|
|
354
|
+
if (l.netVersion !== netVersion)
|
|
355
|
+
return this.err("version_mismatch", "", { hostNetVersion: l.netVersion, yourNetVersion: netVersion });
|
|
356
|
+
if (l.phase === "in_game" && !l.lateJoin)
|
|
357
|
+
return this.err("in_game");
|
|
358
|
+
if (l.peers.size >= l.maxPlayers)
|
|
359
|
+
return this.err("lobby_full");
|
|
360
|
+
const id = l.next++;
|
|
361
|
+
const p = { peerId: id, name: String(this.hello.name), joinedAt: Date.now(), sock: this, token: `tok-${code}-${id}` };
|
|
362
|
+
l.peers.set(id, p);
|
|
363
|
+
this.lobby = l;
|
|
364
|
+
this.peer = p;
|
|
365
|
+
this.push(this.lobbyMsg(l, p));
|
|
366
|
+
return this.broadcast(l, { t: "peer_joined", peer: { peerId: id, name: p.name, joinedAt: p.joinedAt } }, id);
|
|
367
|
+
}
|
|
368
|
+
case "list": {
|
|
369
|
+
const items = [...this.srv.lobbies.values()]
|
|
370
|
+
.filter((l) => l.gameId === gameId && l.netVersion === netVersion && l.visibility === "public")
|
|
371
|
+
.map((l) => ({
|
|
372
|
+
code: l.code, gameId: l.gameId, netVersion: l.netVersion, name: l.name, hostName: l.peers.get(l.hostId)?.name ?? "",
|
|
373
|
+
players: l.peers.size, maxPlayers: l.maxPlayers, phase: l.phase, meta: l.meta, region: "mem", createdAt: 0,
|
|
374
|
+
}));
|
|
375
|
+
return this.push({ t: "lobbies", items });
|
|
376
|
+
}
|
|
377
|
+
case "leave":
|
|
378
|
+
return this.leaveLobby("left");
|
|
379
|
+
case "signal": {
|
|
380
|
+
const l = this.lobby;
|
|
381
|
+
if (!l || !this.peer)
|
|
382
|
+
return this.err("bad_request");
|
|
383
|
+
const to = l.peers.get(Number(m.to));
|
|
384
|
+
if (!to)
|
|
385
|
+
return this.err("bad_request", "no such peer");
|
|
386
|
+
return to.sock?.push({ t: "signal", from: this.peer.peerId, ...(m.sdp ? { sdp: m.sdp } : {}), ...(m.ice ? { ice: m.ice } : {}) });
|
|
387
|
+
}
|
|
388
|
+
case "meta":
|
|
389
|
+
case "phase": {
|
|
390
|
+
const l = this.lobby;
|
|
391
|
+
if (!l || !this.peer)
|
|
392
|
+
return;
|
|
393
|
+
if (l.hostId !== this.peer.peerId)
|
|
394
|
+
return this.err("not_host");
|
|
395
|
+
if (t === "meta")
|
|
396
|
+
l.meta = m.meta;
|
|
397
|
+
else
|
|
398
|
+
l.phase = String(m.phase);
|
|
399
|
+
for (const q of l.peers.values())
|
|
400
|
+
q.sock?.push(this.lobbyMsg(l, q));
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
case "kick": {
|
|
404
|
+
const l = this.lobby;
|
|
405
|
+
if (!l || !this.peer)
|
|
406
|
+
return;
|
|
407
|
+
if (l.hostId !== this.peer.peerId)
|
|
408
|
+
return this.err("not_host");
|
|
409
|
+
const victim = l.peers.get(Number(m.peerId));
|
|
410
|
+
if (!victim || victim === this.peer)
|
|
411
|
+
return this.err("bad_request");
|
|
412
|
+
l.kicked.add(victim.peerId);
|
|
413
|
+
l.peers.delete(victim.peerId);
|
|
414
|
+
victim.sock?.push({ t: "error", code: "kicked", msg: "kicked" });
|
|
415
|
+
this.broadcast(l, { t: "peer_left", peerId: victim.peerId, reason: "kicked" });
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
case "host_changed": {
|
|
419
|
+
const l = this.lobby;
|
|
420
|
+
if (!l || !this.peer)
|
|
421
|
+
return;
|
|
422
|
+
if (Number(m.hostId) === this.peer.peerId && Number(m.epoch) > l.epoch) {
|
|
423
|
+
l.hostId = this.peer.peerId;
|
|
424
|
+
l.epoch = Number(m.epoch);
|
|
425
|
+
this.broadcast(l, { t: "host_changed", hostId: l.hostId, epoch: l.epoch });
|
|
426
|
+
}
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
default:
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
/** Convenience: kill a socket as if the network dropped (server sees a timeout, others get peer_left). */
|
|
435
|
+
export function dropSocket(s) {
|
|
436
|
+
s.close();
|
|
437
|
+
}
|
|
438
|
+
//# sourceMappingURL=testing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testing.js","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Test doubles: a manual clock, an in-memory lobby server speaking the lk1 protocol (spec/signaling.md, the parts the\n * client uses) and an in-memory peer network. Exported as `onelobby/testing`.\n */\nimport type { Clock } from \"./clock.js\";\nimport { CROCKFORD_ALPHABET, normalizeCode } from \"./crockford.js\";\nimport type {\n ChannelName,\n LinkFactory,\n LinkOptions,\n PeerLink,\n SignalPayload,\n SocketFactory,\n SocketLike,\n} from \"./transport.js\";\n\n// ---- clock ------------------------------------------------------------------------------------------------------\n\ninterface Timer {\n at: number;\n seq: number;\n fn: () => void;\n every?: number;\n dead?: boolean;\n}\n\n/** Deterministic clock: time only moves through `advance()`. Timers fire in (time, creation) order. */\nexport class ManualClock implements Clock {\n private t = 0;\n private seq = 0;\n private timers: Timer[] = [];\n\n now(): number {\n return this.t;\n }\n\n setInterval(fn: () => void, ms: number): () => void {\n const timer: Timer = { at: this.t + ms, seq: this.seq++, fn, every: Math.max(1, ms) };\n this.timers.push(timer);\n return () => {\n timer.dead = true;\n };\n }\n\n setTimeout(fn: () => void, ms: number): () => void {\n const timer: Timer = { at: this.t + ms, seq: this.seq++, fn };\n this.timers.push(timer);\n return () => {\n timer.dead = true;\n };\n }\n\n /** Advance time, firing due timers one at a time (microtasks flush between them when awaited via `run`). */\n advance(ms: number): void {\n const end = this.t + ms;\n for (;;) {\n this.timers = this.timers.filter((x) => !x.dead);\n let next: Timer | undefined;\n for (const x of this.timers) if (x.at <= end && (!next || x.at < next.at || (x.at === next.at && x.seq < next.seq))) next = x;\n if (!next) break;\n this.t = Math.max(this.t, next.at);\n if (next.every) {\n next.at += next.every;\n next.seq = this.seq++;\n } else {\n next.dead = true;\n }\n next.fn();\n }\n this.t = end;\n }\n\n /** Advance in `step` ms slices, letting promises and queued microtasks settle between slices. */\n async run(ms: number, step = 10): Promise<void> {\n let left = ms;\n await settle();\n while (left > 0) {\n const d = Math.min(step, left);\n this.advance(d);\n left -= d;\n await settle();\n }\n }\n}\n\n/** Let all pending microtasks (and chained promise continuations) run. */\nexport async function settle(rounds = 8): Promise<void> {\n for (let i = 0; i < rounds; i++) await Promise.resolve();\n // A macrotask boundary guarantees the whole microtask queue (delivery chains) has drained.\n await new Promise<void>((r) => ((globalThis as { setImmediate?: (f: () => void) => void }).setImmediate?.(r) ?? setTimeout(r, 0)));\n}\n\n// ---- in-memory peer network ---------------------------------------------------------------------------------------\n\nlet linkSerial = 0;\n\n/**\n * Pairs `PeerLink`s through the signaling path like WebRTC would: the offerer's SDP string names it, the answerer\n * resolves that name, and both open one microtask later. Messages are delivered FIFO, one microtask later.\n */\nexport class MemoryNetwork {\n private offers = new Map<string, MemoryLink>();\n /** Every link ever created (for assertions and fault injection). */\n readonly links: MemoryLink[] = [];\n /** Set to drop all traffic in one direction pair for fault injection: key `${from}->${to}` is not tracked; use link.mute(). */\n readonly factory: LinkFactory = { create: (opts) => new MemoryLink(this, opts) };\n\n /** @internal */\n register(offer: string, link: MemoryLink): void {\n this.offers.set(offer, link);\n this.links.push(link);\n }\n /** @internal */\n lookup(offer: string): MemoryLink | undefined {\n return this.offers.get(offer);\n }\n\n /** Kill every link whose far end is this identity-tagged factory owner (set `tag` via MemoryLink.owner). */\n killOwner(owner: string): void {\n for (const l of this.links) if (l.owner === owner) l.close();\n }\n\n /** Per-owner factory so tests can tell whose link is whose and cut one peer off. */\n factoryFor(owner: string): LinkFactory {\n return {\n create: (opts) => {\n const l = new MemoryLink(this, opts);\n l.owner = owner;\n return l;\n },\n };\n }\n}\n\nexport class MemoryLink implements PeerLink {\n readonly remoteId: number;\n isOpen = false;\n owner = \"\";\n onSignal: PeerLink[\"onSignal\"] = null;\n onOpen: PeerLink[\"onOpen\"] = null;\n onClose: PeerLink[\"onClose\"] = null;\n onMessage: PeerLink[\"onMessage\"] = null;\n onPath: PeerLink[\"onPath\"] = null;\n /** Bytes sent per channel, for assertions. */\n readonly sent: Record<ChannelName, number> = { reliable: 0, fast: 0, voice: 0 };\n /** While true, outgoing messages vanish (a frozen or partitioned peer). */\n blackhole = false;\n private peer: MemoryLink | null = null;\n private closed = false;\n private id = `mem-${++linkSerial}`;\n\n constructor(private net: MemoryNetwork, opts: LinkOptions) {\n this.remoteId = opts.remoteId;\n }\n\n startOffer(): void {\n this.net.register(this.id, this);\n queueMicrotask(() => this.onSignal?.({ sdp: { type: \"offer\", sdp: this.id } }));\n }\n\n handleSignal(s: SignalPayload): void {\n if (s.sdp?.type === \"offer\") {\n const offerer = this.net.lookup(s.sdp.sdp);\n this.net.links.push(this);\n if (!offerer) return;\n this.peer = offerer;\n queueMicrotask(() => this.onSignal?.({ sdp: { type: \"answer\", sdp: this.id } }));\n queueMicrotask(() => offerer.accept(this));\n queueMicrotask(() => this.open());\n }\n }\n\n /** @internal */\n accept(answerer: MemoryLink): void {\n this.peer = answerer;\n this.open();\n }\n\n private open(): void {\n if (this.closed || this.isOpen) return;\n this.isOpen = true;\n this.onOpen?.();\n this.onPath?.(\"direct\");\n }\n\n send(channel: ChannelName, bytes: Uint8Array): void {\n if (!this.isOpen || this.closed) return;\n this.sent[channel] += bytes.length;\n if (this.blackhole) return;\n const peer = this.peer;\n const copy = bytes.slice();\n queueMicrotask(() => {\n if (peer && peer.isOpen && !peer.closed && !this.closed) peer.onMessage?.(channel, copy);\n });\n }\n\n close(): void {\n if (this.closed) return;\n this.closed = true;\n this.isOpen = false;\n const peer = this.peer;\n queueMicrotask(() => {\n this.onClose?.(\"closed\");\n if (peer && !peer.closed) peer.closeFromRemote();\n });\n }\n\n private closeFromRemote(): void {\n if (this.closed) return;\n this.closed = true;\n this.isOpen = false;\n this.onClose?.(\"remote closed\");\n }\n}\n\n// ---- in-memory lobby server ---------------------------------------------------------------------------------------\n\ntype Json = Record<string, unknown>;\n\ninterface MPeer {\n peerId: number;\n name: string;\n joinedAt: number;\n sock: MemorySocket | null;\n token: string;\n}\ninterface MLobby {\n code: string;\n gameId: string;\n netVersion: string;\n name: string;\n hostId: number;\n epoch: number;\n mode: string;\n tickRate: number;\n lateJoin: boolean;\n phase: string;\n meta: Record<string, string>;\n visibility: string;\n maxPlayers: number;\n peers: Map<number, MPeer>;\n next: number;\n kicked: Set<number>;\n reserved: Map<string, MPeer>;\n}\n\n/** Minimal onelobby-server (lk1) for client tests. Not a conformance target: stream A owns the real server. */\nexport class MemoryServer {\n readonly lobbies = new Map<string, MLobby>();\n /** Every socket ever opened (to drop one from a test). */\n readonly sockets: MemorySocket[] = [];\n readonly socketFactory: SocketFactory = () => this.connect();\n private n = 0;\n iceServers = [{ urls: [\"stun:stun.example:3478\"] }];\n /** Log of every client message, for assertions. */\n readonly received: Json[] = [];\n\n connect(): MemorySocket {\n const s = new MemorySocket(this);\n this.sockets.push(s);\n return s;\n }\n\n /** @internal */\n newCode(): string {\n let x = 0x9e3779b1 * (++this.n + 17);\n let s = \"\";\n for (let i = 0; i < 6; i++) {\n x = (Math.imul(x, 1103515245) + 12345) >>> 0;\n s += CROCKFORD_ALPHABET[(x >>> 16) & 31];\n }\n return this.lobbies.has(s) ? this.newCode() : s;\n }\n}\n\nexport class MemorySocket implements SocketLike {\n readyState = 0;\n onopen: SocketLike[\"onopen\"] = null;\n onmessage: SocketLike[\"onmessage\"] = null;\n onclose: SocketLike[\"onclose\"] = null;\n onerror: SocketLike[\"onerror\"] = null;\n\n private hello: Json | null = null;\n private lobby: MLobby | null = null;\n private peer: MPeer | null = null;\n private closed = false;\n\n constructor(private srv: MemoryServer) {\n queueMicrotask(() => {\n this.readyState = 1;\n this.onopen?.();\n });\n }\n\n // ---- socket API\n send(data: string): void {\n if (this.closed) return;\n const m = JSON.parse(data) as Json;\n queueMicrotask(() => this.handle(m));\n }\n close(): void {\n if (this.closed) return;\n this.closed = true;\n this.readyState = 3;\n this.leaveLobby(\"timeout\");\n queueMicrotask(() => this.onclose?.());\n }\n\n /** @internal server -> this client */\n push(m: Json): void {\n if (this.closed) return;\n queueMicrotask(() => this.onmessage?.({ data: JSON.stringify(m) }));\n }\n\n private err(code: string, msg = \"\", data?: Json) {\n this.push({ t: \"error\", code, msg, ...(data ? { data } : {}) });\n }\n\n private lobbyMsg(l: MLobby, p: MPeer): Json {\n return {\n t: \"lobby\",\n code: l.code,\n gameId: l.gameId,\n netVersion: l.netVersion,\n hostId: l.hostId,\n you: p.peerId,\n mode: l.mode,\n tickRate: l.tickRate,\n lateJoin: l.lateJoin,\n phase: l.phase,\n meta: l.meta,\n peers: [...l.peers.values()].map((q) => ({ peerId: q.peerId, name: q.name, joinedAt: q.joinedAt })),\n resumeToken: p.token,\n name: l.name,\n maxPlayers: l.maxPlayers,\n visibility: l.visibility,\n };\n }\n\n private broadcast(l: MLobby, m: Json, except?: number) {\n for (const q of l.peers.values()) if (q.peerId !== except) q.sock?.push(m);\n }\n\n private leaveLobby(reason: string) {\n const l = this.lobby;\n const p = this.peer;\n if (!l || !p) return;\n this.lobby = null;\n this.peer = null;\n p.sock = null;\n l.peers.delete(p.peerId);\n if (reason === \"timeout\") l.reserved.set(p.token, p); // slot kept for resume\n if (l.peers.size === 0) {\n this.srv.lobbies.delete(l.code);\n return;\n }\n this.broadcast(l, { t: \"peer_left\", peerId: p.peerId, reason });\n if (l.hostId === p.peerId) {\n l.hostId = Math.min(...l.peers.keys());\n l.epoch++;\n this.broadcast(l, { t: \"host_changed\", hostId: l.hostId, epoch: l.epoch });\n }\n }\n\n private handle(m: Json) {\n if (this.closed) return;\n this.srv.received.push(m);\n const t = String(m.t);\n if (t === \"ping\") return this.push({ t: \"pong\", n: m.n });\n if (t === \"hello\") {\n if (this.hello) return this.err(\"bad_request\", \"second hello\");\n this.hello = m;\n const tok = typeof m.resumeToken === \"string\" ? m.resumeToken : \"\";\n for (const l of this.srv.lobbies.values()) {\n const rp = l.reserved.get(tok);\n if (!rp) continue;\n l.reserved.delete(tok);\n rp.sock = this;\n rp.token = tok + \"+\";\n l.peers.set(rp.peerId, rp);\n this.lobby = l;\n this.peer = rp;\n this.push({ t: \"welcome\", peerId: rp.peerId, ice: this.srv.iceServers, serverTime: Date.now(), limits: { maxMsg: 16384, maxPlayers: 64 } });\n this.push(this.lobbyMsg(l, rp));\n return this.broadcast(l, { t: \"peer_joined\", peer: { peerId: rp.peerId, name: rp.name, joinedAt: rp.joinedAt } }, rp.peerId);\n }\n return this.push({ t: \"welcome\", ice: this.srv.iceServers, serverTime: Date.now(), limits: { maxMsg: 16384, maxPlayers: 64 } });\n }\n if (!this.hello) return this.err(\"bad_request\", \"hello first\");\n const gameId = String(this.hello.gameId);\n const netVersion = String(this.hello.netVersion);\n switch (t) {\n case \"create\": {\n const code = this.srv.newCode();\n const p: MPeer = { peerId: 1, name: String(this.hello.name), joinedAt: Date.now(), sock: this, token: `tok-${code}-1` };\n const l: MLobby = {\n code, gameId, netVersion, name: String(m.name), hostId: 1, epoch: 0, mode: String(m.mode), tickRate: Number(m.tickRate),\n lateJoin: !!m.lateJoin, phase: \"lobby\", meta: (m.meta as Record<string, string>) ?? {}, visibility: String(m.visibility),\n maxPlayers: Number(m.maxPlayers), peers: new Map([[1, p]]), next: 2, kicked: new Set(), reserved: new Map(),\n };\n this.srv.lobbies.set(code, l);\n this.lobby = l;\n this.peer = p;\n return this.push(this.lobbyMsg(l, p));\n }\n case \"join\": {\n const code = normalizeCode(String(m.code));\n if (!code) return this.err(\"bad_request\", \"bad code\");\n const l = this.srv.lobbies.get(code);\n if (!l || l.gameId !== gameId) return this.err(\"unknown_code\");\n if (l.netVersion !== netVersion) return this.err(\"version_mismatch\", \"\", { hostNetVersion: l.netVersion, yourNetVersion: netVersion });\n if (l.phase === \"in_game\" && !l.lateJoin) return this.err(\"in_game\");\n if (l.peers.size >= l.maxPlayers) return this.err(\"lobby_full\");\n const id = l.next++;\n const p: MPeer = { peerId: id, name: String(this.hello.name), joinedAt: Date.now(), sock: this, token: `tok-${code}-${id}` };\n l.peers.set(id, p);\n this.lobby = l;\n this.peer = p;\n this.push(this.lobbyMsg(l, p));\n return this.broadcast(l, { t: \"peer_joined\", peer: { peerId: id, name: p.name, joinedAt: p.joinedAt } }, id);\n }\n case \"list\": {\n const items = [...this.srv.lobbies.values()]\n .filter((l) => l.gameId === gameId && l.netVersion === netVersion && l.visibility === \"public\")\n .map((l) => ({\n code: l.code, gameId: l.gameId, netVersion: l.netVersion, name: l.name, hostName: l.peers.get(l.hostId)?.name ?? \"\",\n players: l.peers.size, maxPlayers: l.maxPlayers, phase: l.phase, meta: l.meta, region: \"mem\", createdAt: 0,\n }));\n return this.push({ t: \"lobbies\", items });\n }\n case \"leave\":\n return this.leaveLobby(\"left\");\n case \"signal\": {\n const l = this.lobby;\n if (!l || !this.peer) return this.err(\"bad_request\");\n const to = l.peers.get(Number(m.to));\n if (!to) return this.err(\"bad_request\", \"no such peer\");\n return to.sock?.push({ t: \"signal\", from: this.peer.peerId, ...(m.sdp ? { sdp: m.sdp } : {}), ...(m.ice ? { ice: m.ice } : {}) });\n }\n case \"meta\":\n case \"phase\": {\n const l = this.lobby;\n if (!l || !this.peer) return;\n if (l.hostId !== this.peer.peerId) return this.err(\"not_host\");\n if (t === \"meta\") l.meta = m.meta as Record<string, string>;\n else l.phase = String(m.phase);\n for (const q of l.peers.values()) q.sock?.push(this.lobbyMsg(l, q));\n return;\n }\n case \"kick\": {\n const l = this.lobby;\n if (!l || !this.peer) return;\n if (l.hostId !== this.peer.peerId) return this.err(\"not_host\");\n const victim = l.peers.get(Number(m.peerId));\n if (!victim || victim === this.peer) return this.err(\"bad_request\");\n l.kicked.add(victim.peerId);\n l.peers.delete(victim.peerId);\n victim.sock?.push({ t: \"error\", code: \"kicked\", msg: \"kicked\" });\n this.broadcast(l, { t: \"peer_left\", peerId: victim.peerId, reason: \"kicked\" });\n return;\n }\n case \"host_changed\": {\n const l = this.lobby;\n if (!l || !this.peer) return;\n if (Number(m.hostId) === this.peer.peerId && Number(m.epoch) > l.epoch) {\n l.hostId = this.peer.peerId;\n l.epoch = Number(m.epoch);\n this.broadcast(l, { t: \"host_changed\", hostId: l.hostId, epoch: l.epoch });\n }\n return;\n }\n default:\n return;\n }\n }\n}\n\n/** Convenience: kill a socket as if the network dropped (server sees a timeout, others get peer_left). */\nexport function dropSocket(s: SocketLike): void {\n s.close();\n}\n"]}
|