onelobby 0.0.0-stage → 0.2.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 +57 -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 +35 -0
- package/dist/index.js +25 -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 +203 -0
- package/dist/lobby.js +840 -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 +151 -0
- package/dist/onelobby.global.js +3897 -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 +337 -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/socket.d.ts +63 -0
- package/dist/socket.js +142 -0
- package/dist/socket.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,137 @@
|
|
|
1
|
+
import type { Lobby } from "../lobby.js";
|
|
2
|
+
import type { HostOptions } from "../onelobby.js";
|
|
3
|
+
import { type FriendlyError } from "../friendly.js";
|
|
4
|
+
export type UiState = "idle" | "connecting" | "lobby" | "playing" | "error";
|
|
5
|
+
export interface Player {
|
|
6
|
+
id: number;
|
|
7
|
+
name: string;
|
|
8
|
+
isHost: boolean;
|
|
9
|
+
isMe: boolean;
|
|
10
|
+
}
|
|
11
|
+
/** What the controller needs from `OneLobby` (so tests can pass the in-memory rig). */
|
|
12
|
+
export interface KitLike {
|
|
13
|
+
host(h: HostOptions): Promise<Lobby>;
|
|
14
|
+
join(code: string): Promise<Lobby>;
|
|
15
|
+
/** Take back the seat this tab held before a reload (OneLobby `rememberSeat`); null when there is none. */
|
|
16
|
+
resume?(): Promise<Lobby | null>;
|
|
17
|
+
readonly rememberedCode?: string | null;
|
|
18
|
+
}
|
|
19
|
+
export interface ControllerEvents {
|
|
20
|
+
message: {
|
|
21
|
+
from: Player | null;
|
|
22
|
+
data: unknown;
|
|
23
|
+
};
|
|
24
|
+
"player-joined": {
|
|
25
|
+
player: Player;
|
|
26
|
+
};
|
|
27
|
+
"player-left": {
|
|
28
|
+
player: Player;
|
|
29
|
+
reason: string;
|
|
30
|
+
};
|
|
31
|
+
start: Record<string, never>;
|
|
32
|
+
error: FriendlyError;
|
|
33
|
+
"shared-change": {
|
|
34
|
+
key: string;
|
|
35
|
+
value: unknown;
|
|
36
|
+
from: Player | null;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export interface ControllerOptions {
|
|
40
|
+
makeKit: (name: string) => KitLike;
|
|
41
|
+
/** A number, or a function read each time a game is hosted. */
|
|
42
|
+
maxPlayers: number | (() => number);
|
|
43
|
+
/** Lobby name shown to the server (not to players). */
|
|
44
|
+
lobbyName?: string;
|
|
45
|
+
}
|
|
46
|
+
/** Wire envelope inside raw frames. Kept tiny and versioned by key names only. */
|
|
47
|
+
type Wire = {
|
|
48
|
+
k: "m";
|
|
49
|
+
d: unknown;
|
|
50
|
+
} | {
|
|
51
|
+
k: "set";
|
|
52
|
+
key: string;
|
|
53
|
+
v: unknown;
|
|
54
|
+
} | {
|
|
55
|
+
k: "add";
|
|
56
|
+
key: string;
|
|
57
|
+
n: number;
|
|
58
|
+
} | {
|
|
59
|
+
k: "S";
|
|
60
|
+
key: string;
|
|
61
|
+
v: unknown;
|
|
62
|
+
by: number;
|
|
63
|
+
q: number;
|
|
64
|
+
} | {
|
|
65
|
+
k: "SS";
|
|
66
|
+
all: Record<string, unknown>;
|
|
67
|
+
q: number;
|
|
68
|
+
};
|
|
69
|
+
export declare class SharedState {
|
|
70
|
+
private c;
|
|
71
|
+
/** @internal */ values: Map<string, unknown>;
|
|
72
|
+
constructor(c: LobbyController);
|
|
73
|
+
get(key: string): unknown;
|
|
74
|
+
/** Set a value for everyone. The host decides the order; everyone gets `shared-change`. */
|
|
75
|
+
set(key: string, value: unknown): void;
|
|
76
|
+
/** Add `n` (default 1) to a number, safely even when several players click at once. */
|
|
77
|
+
add(key: string, n?: number): void;
|
|
78
|
+
/** A plain copy of every value. */
|
|
79
|
+
all(): Record<string, unknown>;
|
|
80
|
+
keys(): string[];
|
|
81
|
+
}
|
|
82
|
+
export declare class LobbyController {
|
|
83
|
+
private opts;
|
|
84
|
+
state: UiState;
|
|
85
|
+
lobby: Lobby | null;
|
|
86
|
+
error: FriendlyError | null;
|
|
87
|
+
/** True while a joiner waits for its direct link to the host. */
|
|
88
|
+
linking: boolean;
|
|
89
|
+
name: string;
|
|
90
|
+
readonly shared: SharedState;
|
|
91
|
+
onChange: () => void;
|
|
92
|
+
onEvent: <K extends keyof ControllerEvents>(type: K, detail: ControllerEvents[K]) => void;
|
|
93
|
+
private lastAction;
|
|
94
|
+
private unsub;
|
|
95
|
+
private gen;
|
|
96
|
+
/** Host: changes made so far. Joiner: the newest change applied (older queued ones are skipped). */
|
|
97
|
+
private seq;
|
|
98
|
+
constructor(opts: ControllerOptions);
|
|
99
|
+
get isHost(): boolean;
|
|
100
|
+
get code(): string | null;
|
|
101
|
+
get players(): Player[];
|
|
102
|
+
/** One line of plain language for the status area. */
|
|
103
|
+
get statusText(): string;
|
|
104
|
+
private get hostName();
|
|
105
|
+
private toPlayer;
|
|
106
|
+
host(name: string): Promise<void>;
|
|
107
|
+
join(code: string, name: string): Promise<void>;
|
|
108
|
+
/**
|
|
109
|
+
* Take back this tab's seat after a page reload, if the kit remembers one (host or joiner). Quietly stays on the
|
|
110
|
+
* start screen when there is nothing to resume or the seat is gone. Resolves true when back in a game.
|
|
111
|
+
*/
|
|
112
|
+
resume(name: string): Promise<boolean>;
|
|
113
|
+
/** Repeat whatever failed last (host or join). */
|
|
114
|
+
retry(): Promise<void>;
|
|
115
|
+
start(): Promise<void>;
|
|
116
|
+
leave(): Promise<void>;
|
|
117
|
+
/** Back to the start screen (after an error, or to cancel). */
|
|
118
|
+
reset(): void;
|
|
119
|
+
/** Send any JSON-able value to everyone else. */
|
|
120
|
+
send(data: unknown): void;
|
|
121
|
+
private run;
|
|
122
|
+
private attach;
|
|
123
|
+
private linked;
|
|
124
|
+
private detach;
|
|
125
|
+
private fail;
|
|
126
|
+
private playerById;
|
|
127
|
+
private onRaw;
|
|
128
|
+
/** @internal from SharedState */
|
|
129
|
+
_sharedOp(op: Extract<Wire, {
|
|
130
|
+
k: "set";
|
|
131
|
+
} | {
|
|
132
|
+
k: "add";
|
|
133
|
+
}>): void;
|
|
134
|
+
private applyHost;
|
|
135
|
+
private applyValue;
|
|
136
|
+
}
|
|
137
|
+
export {};
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The DOM-free brain of `<one-lobby>`: a small state machine over OneLobby + Lobby (raw mode) that the element
|
|
3
|
+
* renders, plus the JSON message envelope and the host-authoritative shared state. Unit-tested without a browser.
|
|
4
|
+
*/
|
|
5
|
+
import { OneLobbyError } from "../signaling.js";
|
|
6
|
+
import { describeError } from "../friendly.js";
|
|
7
|
+
import { normalizeCode } from "../crockford.js";
|
|
8
|
+
const enc = new TextEncoder();
|
|
9
|
+
const dec = new TextDecoder();
|
|
10
|
+
const toBytes = (w) => enc.encode(JSON.stringify(w));
|
|
11
|
+
export class SharedState {
|
|
12
|
+
c;
|
|
13
|
+
/** @internal */ values = new Map();
|
|
14
|
+
constructor(c) {
|
|
15
|
+
this.c = c;
|
|
16
|
+
}
|
|
17
|
+
get(key) {
|
|
18
|
+
return this.values.get(key);
|
|
19
|
+
}
|
|
20
|
+
/** Set a value for everyone. The host decides the order; everyone gets `shared-change`. */
|
|
21
|
+
set(key, value) {
|
|
22
|
+
this.c._sharedOp({ k: "set", key: String(key), v: value === undefined ? null : value });
|
|
23
|
+
}
|
|
24
|
+
/** Add `n` (default 1) to a number, safely even when several players click at once. */
|
|
25
|
+
add(key, n = 1) {
|
|
26
|
+
this.c._sharedOp({ k: "add", key: String(key), n: Number(n) || 0 });
|
|
27
|
+
}
|
|
28
|
+
/** A plain copy of every value. */
|
|
29
|
+
all() {
|
|
30
|
+
return Object.fromEntries(this.values);
|
|
31
|
+
}
|
|
32
|
+
keys() {
|
|
33
|
+
return [...this.values.keys()];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export class LobbyController {
|
|
37
|
+
opts;
|
|
38
|
+
state = "idle";
|
|
39
|
+
lobby = null;
|
|
40
|
+
error = null;
|
|
41
|
+
/** True while a joiner waits for its direct link to the host. */
|
|
42
|
+
linking = false;
|
|
43
|
+
name = "";
|
|
44
|
+
shared = new SharedState(this);
|
|
45
|
+
onChange = () => { };
|
|
46
|
+
onEvent = () => { };
|
|
47
|
+
lastAction = null;
|
|
48
|
+
unsub = [];
|
|
49
|
+
gen = 0;
|
|
50
|
+
/** Host: changes made so far. Joiner: the newest change applied (older queued ones are skipped). */
|
|
51
|
+
seq = 0;
|
|
52
|
+
constructor(opts) {
|
|
53
|
+
this.opts = opts;
|
|
54
|
+
}
|
|
55
|
+
get isHost() {
|
|
56
|
+
return !!this.lobby && this.lobby.isHost;
|
|
57
|
+
}
|
|
58
|
+
get code() {
|
|
59
|
+
return this.lobby?.code ?? null;
|
|
60
|
+
}
|
|
61
|
+
get players() {
|
|
62
|
+
const l = this.lobby;
|
|
63
|
+
if (!l)
|
|
64
|
+
return [];
|
|
65
|
+
const list = l.peers.map((p) => this.toPlayer(p));
|
|
66
|
+
if (!list.some((p) => p.isMe))
|
|
67
|
+
list.push({ id: l.localPeerId, name: this.name || `Player ${l.localPeerId}`, isHost: l.isHost, isMe: true });
|
|
68
|
+
return list.sort((a, b) => a.id - b.id);
|
|
69
|
+
}
|
|
70
|
+
/** One line of plain language for the status area. */
|
|
71
|
+
get statusText() {
|
|
72
|
+
switch (this.state) {
|
|
73
|
+
case "idle":
|
|
74
|
+
return "Host a game, or type a code to join one.";
|
|
75
|
+
case "connecting":
|
|
76
|
+
return this.lastAction?.kind === "join" ? "Joining…" : this.lastAction?.kind === "resume" ? "Getting your seat back…" : "Creating a game…";
|
|
77
|
+
case "error":
|
|
78
|
+
return this.error?.message ?? "Something went wrong.";
|
|
79
|
+
case "lobby": {
|
|
80
|
+
if (this.linking)
|
|
81
|
+
return `Connecting to ${this.hostName}…`;
|
|
82
|
+
if (this.isHost) {
|
|
83
|
+
const n = this.players.length;
|
|
84
|
+
return n <= 1 ? "Waiting for players… Share the code with your friends." : `${n} players here. Press Start when everyone's in.`;
|
|
85
|
+
}
|
|
86
|
+
return `Connected! Waiting for ${this.hostName} to start.`;
|
|
87
|
+
}
|
|
88
|
+
case "playing":
|
|
89
|
+
return this.linking ? `Connecting to ${this.hostName}…` : "Game on!";
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
get hostName() {
|
|
93
|
+
const l = this.lobby;
|
|
94
|
+
if (!l)
|
|
95
|
+
return "the host";
|
|
96
|
+
return l.peer(l.hostPeerId)?.name || "the host";
|
|
97
|
+
}
|
|
98
|
+
toPlayer(p, id) {
|
|
99
|
+
const l = this.lobby;
|
|
100
|
+
const pid = p?.peerId ?? id ?? 0;
|
|
101
|
+
return { id: pid, name: p?.name || (pid === l.localPeerId && this.name) || `Player ${pid}`, isHost: pid === l.hostPeerId, isMe: pid === l.localPeerId };
|
|
102
|
+
}
|
|
103
|
+
// ---- actions -----------------------------------------------------------------------------------------------------
|
|
104
|
+
host(name) {
|
|
105
|
+
this.lastAction = { kind: "host" };
|
|
106
|
+
return this.run(name, (kit) => kit.host({ name: this.opts.lobbyName ?? "game", maxPlayers: typeof this.opts.maxPlayers === "function" ? this.opts.maxPlayers() : this.opts.maxPlayers, visibility: "unlisted", lateJoin: true }));
|
|
107
|
+
}
|
|
108
|
+
join(code, name) {
|
|
109
|
+
const norm = normalizeCode(code);
|
|
110
|
+
this.lastAction = { kind: "join", code };
|
|
111
|
+
if (!norm) {
|
|
112
|
+
this.fail(new OneLobbyError("bad_code"));
|
|
113
|
+
return Promise.resolve();
|
|
114
|
+
}
|
|
115
|
+
return this.run(name, (kit) => kit.join(norm));
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Take back this tab's seat after a page reload, if the kit remembers one (host or joiner). Quietly stays on the
|
|
119
|
+
* start screen when there is nothing to resume or the seat is gone. Resolves true when back in a game.
|
|
120
|
+
*/
|
|
121
|
+
async resume(name) {
|
|
122
|
+
const kit = this.opts.makeKit(name);
|
|
123
|
+
if (!kit.resume || !kit.rememberedCode)
|
|
124
|
+
return false;
|
|
125
|
+
this.lastAction = { kind: "resume" };
|
|
126
|
+
await this.run(name, () => kit.resume(), true);
|
|
127
|
+
return !!this.lobby;
|
|
128
|
+
}
|
|
129
|
+
/** Repeat whatever failed last (host or join). */
|
|
130
|
+
retry() {
|
|
131
|
+
const a = this.lastAction;
|
|
132
|
+
if (!a || a.kind === "resume") {
|
|
133
|
+
this.reset();
|
|
134
|
+
return Promise.resolve();
|
|
135
|
+
}
|
|
136
|
+
return a.kind === "host" ? this.host(this.name) : this.join(a.code, this.name);
|
|
137
|
+
}
|
|
138
|
+
async start() {
|
|
139
|
+
if (!this.lobby || !this.isHost || this.state === "playing")
|
|
140
|
+
return;
|
|
141
|
+
await this.lobby.start();
|
|
142
|
+
}
|
|
143
|
+
async leave() {
|
|
144
|
+
const l = this.lobby;
|
|
145
|
+
this.detach();
|
|
146
|
+
await l?.leave();
|
|
147
|
+
this.reset();
|
|
148
|
+
}
|
|
149
|
+
/** Back to the start screen (after an error, or to cancel). */
|
|
150
|
+
reset() {
|
|
151
|
+
this.detach();
|
|
152
|
+
this.state = "idle";
|
|
153
|
+
this.error = null;
|
|
154
|
+
this.onChange();
|
|
155
|
+
}
|
|
156
|
+
/** Send any JSON-able value to everyone else. */
|
|
157
|
+
send(data) {
|
|
158
|
+
if (!this.lobby)
|
|
159
|
+
return;
|
|
160
|
+
this.lobby.broadcast(toBytes({ k: "m", d: data === undefined ? null : data }));
|
|
161
|
+
}
|
|
162
|
+
// ---- internals ---------------------------------------------------------------------------------------------------
|
|
163
|
+
async run(name, go, quiet = false) {
|
|
164
|
+
this.detach();
|
|
165
|
+
const gen = ++this.gen;
|
|
166
|
+
this.name = (name || "").trim().slice(0, 24); // empty: other players see "Player N"
|
|
167
|
+
this.state = "connecting";
|
|
168
|
+
this.error = null;
|
|
169
|
+
this.onChange();
|
|
170
|
+
let lobby;
|
|
171
|
+
try {
|
|
172
|
+
lobby = await go(this.opts.makeKit(this.name));
|
|
173
|
+
}
|
|
174
|
+
catch (e) {
|
|
175
|
+
if (gen !== this.gen)
|
|
176
|
+
return;
|
|
177
|
+
if (quiet)
|
|
178
|
+
this.reset();
|
|
179
|
+
else
|
|
180
|
+
this.fail(e);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (gen !== this.gen) {
|
|
184
|
+
void lobby?.leave();
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (lobby)
|
|
188
|
+
this.attach(lobby);
|
|
189
|
+
else
|
|
190
|
+
this.reset();
|
|
191
|
+
}
|
|
192
|
+
attach(l) {
|
|
193
|
+
this.lobby = l;
|
|
194
|
+
// A joiner takes the host's values; a host keeps anything the page set up front (defaults).
|
|
195
|
+
if (!l.isHost)
|
|
196
|
+
this.shared.values.clear();
|
|
197
|
+
this.seq = 0;
|
|
198
|
+
this.state = l.phase === "in_game" ? "playing" : "lobby";
|
|
199
|
+
this.linking = !l.isHost;
|
|
200
|
+
const on = this.unsub;
|
|
201
|
+
on.push(l.on("ready", () => this.linked()));
|
|
202
|
+
on.push(l.on("peerJoined", (p) => {
|
|
203
|
+
this.onEvent("player-joined", { player: this.toPlayer(p) });
|
|
204
|
+
this.onChange();
|
|
205
|
+
}));
|
|
206
|
+
on.push(l.on("peerLeft", (p, reason) => {
|
|
207
|
+
this.onEvent("player-left", { player: this.toPlayer(p), reason });
|
|
208
|
+
this.onChange();
|
|
209
|
+
}));
|
|
210
|
+
on.push(l.on("peerReady", (p) => {
|
|
211
|
+
if (l.isHost && p.peerId !== l.localPeerId)
|
|
212
|
+
l.send(p.peerId, toBytes({ k: "SS", all: this.shared.all(), q: this.seq }));
|
|
213
|
+
}));
|
|
214
|
+
on.push(l.on("hostChanged", () => this.onChange()));
|
|
215
|
+
on.push(l.on("phase", (ph) => {
|
|
216
|
+
if (ph === "in_game" && this.state !== "playing") {
|
|
217
|
+
this.state = "playing";
|
|
218
|
+
this.onChange();
|
|
219
|
+
this.onEvent("start", {});
|
|
220
|
+
}
|
|
221
|
+
}));
|
|
222
|
+
on.push(l.on("message", (from, bytes) => this.onRaw(from, bytes)));
|
|
223
|
+
on.push(l.on("sessionEnded", (reason) => {
|
|
224
|
+
if (this.lobby !== l)
|
|
225
|
+
return;
|
|
226
|
+
if (reason === "left")
|
|
227
|
+
return;
|
|
228
|
+
this.fail(new OneLobbyError(reason));
|
|
229
|
+
}));
|
|
230
|
+
// A joiner that cannot reach the host: the kit gives up after its readyTimeoutMs, leaves, and ends the session
|
|
231
|
+
// with "ice_failed", which lands in the sessionEnded handler above.
|
|
232
|
+
l.ready.then(() => { if (this.lobby === l)
|
|
233
|
+
this.linked(); }, () => { });
|
|
234
|
+
this.onChange();
|
|
235
|
+
if (this.state === "playing" && !this.linking)
|
|
236
|
+
this.onEvent("start", {});
|
|
237
|
+
}
|
|
238
|
+
linked() {
|
|
239
|
+
if (!this.linking)
|
|
240
|
+
return;
|
|
241
|
+
this.linking = false;
|
|
242
|
+
this.onChange();
|
|
243
|
+
if (this.state === "playing")
|
|
244
|
+
this.onEvent("start", {});
|
|
245
|
+
}
|
|
246
|
+
detach() {
|
|
247
|
+
this.gen++;
|
|
248
|
+
for (const u of this.unsub.splice(0))
|
|
249
|
+
u();
|
|
250
|
+
this.lobby = null;
|
|
251
|
+
this.linking = false;
|
|
252
|
+
}
|
|
253
|
+
fail(e) {
|
|
254
|
+
const l = this.lobby;
|
|
255
|
+
this.detach();
|
|
256
|
+
if (l)
|
|
257
|
+
void l.leave();
|
|
258
|
+
this.state = "error";
|
|
259
|
+
this.error = describeError(e);
|
|
260
|
+
this.onChange();
|
|
261
|
+
this.onEvent("error", this.error);
|
|
262
|
+
}
|
|
263
|
+
playerById(id) {
|
|
264
|
+
const l = this.lobby;
|
|
265
|
+
if (!l || !id)
|
|
266
|
+
return null;
|
|
267
|
+
return this.toPlayer(l.peer(id), id);
|
|
268
|
+
}
|
|
269
|
+
onRaw(from, bytes) {
|
|
270
|
+
let w;
|
|
271
|
+
try {
|
|
272
|
+
w = JSON.parse(dec.decode(bytes));
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
return; // not ours
|
|
276
|
+
}
|
|
277
|
+
if (!w || typeof w !== "object")
|
|
278
|
+
return;
|
|
279
|
+
const l = this.lobby;
|
|
280
|
+
switch (w.k) {
|
|
281
|
+
case "m":
|
|
282
|
+
this.onEvent("message", { from: this.playerById(from), data: w.d });
|
|
283
|
+
break;
|
|
284
|
+
case "set":
|
|
285
|
+
case "add":
|
|
286
|
+
if (l.isHost)
|
|
287
|
+
this.applyHost(w, from);
|
|
288
|
+
break;
|
|
289
|
+
case "S":
|
|
290
|
+
if (!l.isHost && from === l.hostPeerId && !(w.q <= this.seq)) {
|
|
291
|
+
this.seq = w.q;
|
|
292
|
+
this.applyValue(w.key, w.v, w.by);
|
|
293
|
+
}
|
|
294
|
+
break;
|
|
295
|
+
case "SS":
|
|
296
|
+
if (!l.isHost && from === l.hostPeerId && w.all && typeof w.all === "object" && !(w.q < this.seq)) {
|
|
297
|
+
this.seq = w.q;
|
|
298
|
+
for (const k of [...this.shared.values.keys()])
|
|
299
|
+
if (!(k in w.all))
|
|
300
|
+
this.applyValue(k, null, 0);
|
|
301
|
+
for (const [k, v] of Object.entries(w.all))
|
|
302
|
+
this.applyValue(k, v, 0);
|
|
303
|
+
}
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
/** @internal from SharedState */
|
|
308
|
+
_sharedOp(op) {
|
|
309
|
+
const l = this.lobby;
|
|
310
|
+
if (!l) {
|
|
311
|
+
// Not in a game yet: keep it local, so a page can set defaults up front.
|
|
312
|
+
const v = op.k === "set" ? op.v : (Number(this.shared.values.get(op.key)) || 0) + op.n;
|
|
313
|
+
this.applyValue(op.key, v, 0);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if (l.isHost)
|
|
317
|
+
this.applyHost(op, l.localPeerId);
|
|
318
|
+
else
|
|
319
|
+
l.send(l.hostPeerId, toBytes(op));
|
|
320
|
+
}
|
|
321
|
+
applyHost(op, by) {
|
|
322
|
+
const l = this.lobby;
|
|
323
|
+
if (typeof op.key !== "string")
|
|
324
|
+
return;
|
|
325
|
+
const v = op.k === "set" ? op.v : (Number(this.shared.values.get(op.key)) || 0) + (Number(op.n) || 0);
|
|
326
|
+
this.applyValue(op.key, v, by);
|
|
327
|
+
l.broadcast(toBytes({ k: "S", key: op.key, v, by, q: ++this.seq }));
|
|
328
|
+
}
|
|
329
|
+
applyValue(key, v, by) {
|
|
330
|
+
if (v === null)
|
|
331
|
+
this.shared.values.delete(key);
|
|
332
|
+
else
|
|
333
|
+
this.shared.values.set(key, v);
|
|
334
|
+
this.onEvent("shared-change", { key, value: v, from: this.playerById(by) });
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
//# sourceMappingURL=controller.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"controller.js","sourceRoot":"","sources":["../../src/ui/controller.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The DOM-free brain of `<one-lobby>`: a small state machine over OneLobby + Lobby (raw mode) that the element\n * renders, plus the JSON message envelope and the host-authoritative shared state. Unit-tested without a browser.\n */\nimport { OneLobbyError } from \"../signaling.js\";\nimport type { Lobby, Peer } from \"../lobby.js\";\nimport type { HostOptions } from \"../onelobby.js\";\nimport { describeError, type FriendlyError } from \"../friendly.js\";\nimport { normalizeCode } from \"../crockford.js\";\n\nexport type UiState = \"idle\" | \"connecting\" | \"lobby\" | \"playing\" | \"error\";\n\nexport interface Player {\n id: number;\n name: string;\n isHost: boolean;\n isMe: boolean;\n}\n\n/** What the controller needs from `OneLobby` (so tests can pass the in-memory rig). */\nexport interface KitLike {\n host(h: HostOptions): Promise<Lobby>;\n join(code: string): Promise<Lobby>;\n /** Take back the seat this tab held before a reload (OneLobby `rememberSeat`); null when there is none. */\n resume?(): Promise<Lobby | null>;\n readonly rememberedCode?: string | null;\n}\n\nexport interface ControllerEvents {\n message: { from: Player | null; data: unknown };\n \"player-joined\": { player: Player };\n \"player-left\": { player: Player; reason: string };\n start: Record<string, never>;\n error: FriendlyError;\n \"shared-change\": { key: string; value: unknown; from: Player | null };\n}\n\nexport interface ControllerOptions {\n makeKit: (name: string) => KitLike;\n /** A number, or a function read each time a game is hosted. */\n maxPlayers: number | (() => number);\n /** Lobby name shown to the server (not to players). */\n lobbyName?: string;\n}\n\n/** Wire envelope inside raw frames. Kept tiny and versioned by key names only. */\ntype Wire =\n | { k: \"m\"; d: unknown } // game message\n | { k: \"set\"; key: string; v: unknown } // joiner -> host: please set\n | { k: \"add\"; key: string; n: number } // joiner -> host: please add\n | { k: \"S\"; key: string; v: unknown; by: number; q: number } // host -> all: value changed (q = host change counter)\n | { k: \"SS\"; all: Record<string, unknown>; q: number }; // host -> one: full state as of change q\n\nconst enc = new TextEncoder();\nconst dec = new TextDecoder();\nconst toBytes = (w: Wire) => enc.encode(JSON.stringify(w));\n\nexport class SharedState {\n /** @internal */ values = new Map<string, unknown>();\n constructor(private c: LobbyController) {}\n get(key: string): unknown {\n return this.values.get(key);\n }\n /** Set a value for everyone. The host decides the order; everyone gets `shared-change`. */\n set(key: string, value: unknown): void {\n this.c._sharedOp({ k: \"set\", key: String(key), v: value === undefined ? null : value });\n }\n /** Add `n` (default 1) to a number, safely even when several players click at once. */\n add(key: string, n = 1): void {\n this.c._sharedOp({ k: \"add\", key: String(key), n: Number(n) || 0 });\n }\n /** A plain copy of every value. */\n all(): Record<string, unknown> {\n return Object.fromEntries(this.values);\n }\n keys(): string[] {\n return [...this.values.keys()];\n }\n}\n\nexport class LobbyController {\n state: UiState = \"idle\";\n lobby: Lobby | null = null;\n error: FriendlyError | null = null;\n /** True while a joiner waits for its direct link to the host. */\n linking = false;\n name = \"\";\n readonly shared = new SharedState(this);\n\n onChange: () => void = () => {};\n onEvent: <K extends keyof ControllerEvents>(type: K, detail: ControllerEvents[K]) => void = () => {};\n\n private lastAction: { kind: \"host\" } | { kind: \"join\"; code: string } | { kind: \"resume\" } | null = null;\n private unsub: Array<() => void> = [];\n private gen = 0;\n /** Host: changes made so far. Joiner: the newest change applied (older queued ones are skipped). */\n private seq = 0;\n\n constructor(private opts: ControllerOptions) {}\n\n get isHost(): boolean {\n return !!this.lobby && this.lobby.isHost;\n }\n get code(): string | null {\n return this.lobby?.code ?? null;\n }\n get players(): Player[] {\n const l = this.lobby;\n if (!l) return [];\n const list = l.peers.map((p) => this.toPlayer(p));\n if (!list.some((p) => p.isMe)) list.push({ id: l.localPeerId, name: this.name || `Player ${l.localPeerId}`, isHost: l.isHost, isMe: true });\n return list.sort((a, b) => a.id - b.id);\n }\n\n /** One line of plain language for the status area. */\n get statusText(): string {\n switch (this.state) {\n case \"idle\":\n return \"Host a game, or type a code to join one.\";\n case \"connecting\":\n return this.lastAction?.kind === \"join\" ? \"Joining…\" : this.lastAction?.kind === \"resume\" ? \"Getting your seat back…\" : \"Creating a game…\";\n case \"error\":\n return this.error?.message ?? \"Something went wrong.\";\n case \"lobby\": {\n if (this.linking) return `Connecting to ${this.hostName}…`;\n if (this.isHost) {\n const n = this.players.length;\n return n <= 1 ? \"Waiting for players… Share the code with your friends.\" : `${n} players here. Press Start when everyone's in.`;\n }\n return `Connected! Waiting for ${this.hostName} to start.`;\n }\n case \"playing\":\n return this.linking ? `Connecting to ${this.hostName}…` : \"Game on!\";\n }\n }\n\n private get hostName(): string {\n const l = this.lobby;\n if (!l) return \"the host\";\n return l.peer(l.hostPeerId)?.name || \"the host\";\n }\n\n private toPlayer(p: Peer | undefined, id?: number): Player {\n const l = this.lobby!;\n const pid = p?.peerId ?? id ?? 0;\n return { id: pid, name: p?.name || (pid === l.localPeerId && this.name) || `Player ${pid}`, isHost: pid === l.hostPeerId, isMe: pid === l.localPeerId };\n }\n\n // ---- actions -----------------------------------------------------------------------------------------------------\n\n host(name: string): Promise<void> {\n this.lastAction = { kind: \"host\" };\n return this.run(name, (kit) => kit.host({ name: this.opts.lobbyName ?? \"game\", maxPlayers: typeof this.opts.maxPlayers === \"function\" ? this.opts.maxPlayers() : this.opts.maxPlayers, visibility: \"unlisted\", lateJoin: true }));\n }\n\n join(code: string, name: string): Promise<void> {\n const norm = normalizeCode(code);\n this.lastAction = { kind: \"join\", code };\n if (!norm) {\n this.fail(new OneLobbyError(\"bad_code\"));\n return Promise.resolve();\n }\n return this.run(name, (kit) => kit.join(norm));\n }\n\n /**\n * Take back this tab's seat after a page reload, if the kit remembers one (host or joiner). Quietly stays on the\n * start screen when there is nothing to resume or the seat is gone. Resolves true when back in a game.\n */\n async resume(name: string): Promise<boolean> {\n const kit = this.opts.makeKit(name);\n if (!kit.resume || !kit.rememberedCode) return false;\n this.lastAction = { kind: \"resume\" };\n await this.run(name, () => kit.resume!(), true);\n return !!this.lobby;\n }\n\n /** Repeat whatever failed last (host or join). */\n retry(): Promise<void> {\n const a = this.lastAction;\n if (!a || a.kind === \"resume\") {\n this.reset();\n return Promise.resolve();\n }\n return a.kind === \"host\" ? this.host(this.name) : this.join(a.code, this.name);\n }\n\n async start(): Promise<void> {\n if (!this.lobby || !this.isHost || this.state === \"playing\") return;\n await this.lobby.start();\n }\n\n async leave(): Promise<void> {\n const l = this.lobby;\n this.detach();\n await l?.leave();\n this.reset();\n }\n\n /** Back to the start screen (after an error, or to cancel). */\n reset(): void {\n this.detach();\n this.state = \"idle\";\n this.error = null;\n this.onChange();\n }\n\n /** Send any JSON-able value to everyone else. */\n send(data: unknown): void {\n if (!this.lobby) return;\n this.lobby.broadcast(toBytes({ k: \"m\", d: data === undefined ? null : data }));\n }\n\n // ---- internals ---------------------------------------------------------------------------------------------------\n\n private async run(name: string, go: (kit: KitLike) => Promise<Lobby | null>, quiet = false): Promise<void> {\n this.detach();\n const gen = ++this.gen;\n this.name = (name || \"\").trim().slice(0, 24); // empty: other players see \"Player N\"\n this.state = \"connecting\";\n this.error = null;\n this.onChange();\n let lobby: Lobby | null;\n try {\n lobby = await go(this.opts.makeKit(this.name));\n } catch (e) {\n if (gen !== this.gen) return;\n if (quiet) this.reset();\n else this.fail(e);\n return;\n }\n if (gen !== this.gen) {\n void lobby?.leave();\n return;\n }\n if (lobby) this.attach(lobby);\n else this.reset();\n }\n\n private attach(l: Lobby): void {\n this.lobby = l;\n // A joiner takes the host's values; a host keeps anything the page set up front (defaults).\n if (!l.isHost) this.shared.values.clear();\n this.seq = 0;\n this.state = l.phase === \"in_game\" ? \"playing\" : \"lobby\";\n this.linking = !l.isHost;\n const on = this.unsub;\n on.push(l.on(\"ready\", () => this.linked()));\n on.push(l.on(\"peerJoined\", (p) => {\n this.onEvent(\"player-joined\", { player: this.toPlayer(p) });\n this.onChange();\n }));\n on.push(l.on(\"peerLeft\", (p, reason) => {\n this.onEvent(\"player-left\", { player: this.toPlayer(p), reason });\n this.onChange();\n }));\n on.push(l.on(\"peerReady\", (p) => {\n if (l.isHost && p.peerId !== l.localPeerId) l.send(p.peerId, toBytes({ k: \"SS\", all: this.shared.all(), q: this.seq }));\n }));\n on.push(l.on(\"hostChanged\", () => this.onChange()));\n on.push(l.on(\"phase\", (ph) => {\n if (ph === \"in_game\" && this.state !== \"playing\") {\n this.state = \"playing\";\n this.onChange();\n this.onEvent(\"start\", {});\n }\n }));\n on.push(l.on(\"message\", (from, bytes) => this.onRaw(from, bytes)));\n on.push(l.on(\"sessionEnded\", (reason) => {\n if (this.lobby !== l) return;\n if (reason === \"left\") return;\n this.fail(new OneLobbyError(reason));\n }));\n // A joiner that cannot reach the host: the kit gives up after its readyTimeoutMs, leaves, and ends the session\n // with \"ice_failed\", which lands in the sessionEnded handler above.\n l.ready.then(() => { if (this.lobby === l) this.linked(); }, () => {});\n this.onChange();\n if (this.state === \"playing\" && !this.linking) this.onEvent(\"start\", {});\n }\n\n private linked(): void {\n if (!this.linking) return;\n this.linking = false;\n this.onChange();\n if (this.state === \"playing\") this.onEvent(\"start\", {});\n }\n\n private detach(): void {\n this.gen++;\n for (const u of this.unsub.splice(0)) u();\n this.lobby = null;\n this.linking = false;\n }\n\n private fail(e: unknown): void {\n const l = this.lobby;\n this.detach();\n if (l) void l.leave();\n this.state = \"error\";\n this.error = describeError(e);\n this.onChange();\n this.onEvent(\"error\", this.error);\n }\n\n private playerById(id: number): Player | null {\n const l = this.lobby;\n if (!l || !id) return null;\n return this.toPlayer(l.peer(id), id);\n }\n\n private onRaw(from: number, bytes: Uint8Array): void {\n let w: Wire;\n try {\n w = JSON.parse(dec.decode(bytes)) as Wire;\n } catch {\n return; // not ours\n }\n if (!w || typeof w !== \"object\") return;\n const l = this.lobby!;\n switch (w.k) {\n case \"m\":\n this.onEvent(\"message\", { from: this.playerById(from), data: w.d });\n break;\n case \"set\":\n case \"add\":\n if (l.isHost) this.applyHost(w, from);\n break;\n case \"S\":\n if (!l.isHost && from === l.hostPeerId && !(w.q <= this.seq)) {\n this.seq = w.q;\n this.applyValue(w.key, w.v, w.by);\n }\n break;\n case \"SS\":\n if (!l.isHost && from === l.hostPeerId && w.all && typeof w.all === \"object\" && !(w.q < this.seq)) {\n this.seq = w.q;\n for (const k of [...this.shared.values.keys()]) if (!(k in w.all)) this.applyValue(k, null, 0);\n for (const [k, v] of Object.entries(w.all)) this.applyValue(k, v, 0);\n }\n break;\n }\n }\n\n /** @internal from SharedState */\n _sharedOp(op: Extract<Wire, { k: \"set\" } | { k: \"add\" }>): void {\n const l = this.lobby;\n if (!l) {\n // Not in a game yet: keep it local, so a page can set defaults up front.\n const v = op.k === \"set\" ? op.v : (Number(this.shared.values.get(op.key)) || 0) + op.n;\n this.applyValue(op.key, v, 0);\n return;\n }\n if (l.isHost) this.applyHost(op, l.localPeerId);\n else l.send(l.hostPeerId, toBytes(op));\n }\n\n private applyHost(op: Extract<Wire, { k: \"set\" } | { k: \"add\" }>, by: number): void {\n const l = this.lobby!;\n if (typeof op.key !== \"string\") return;\n const v = op.k === \"set\" ? op.v : (Number(this.shared.values.get(op.key)) || 0) + (Number(op.n) || 0);\n this.applyValue(op.key, v, by);\n l.broadcast(toBytes({ k: \"S\", key: op.key, v, by, q: ++this.seq }));\n }\n\n private applyValue(key: string, v: unknown, by: number): void {\n if (v === null) this.shared.values.delete(key);\n else this.shared.values.set(key, v);\n this.onEvent(\"shared-change\", { key, value: v, from: this.playerById(by) });\n }\n}\n"]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type Player, type SharedState, type UiState } from "./controller.js";
|
|
2
|
+
export declare const LOBBY_TAG = "one-lobby";
|
|
3
|
+
/** The element's public surface (for TypeScript users). */
|
|
4
|
+
export interface OneLobbyElement extends HTMLElement {
|
|
5
|
+
readonly isHost: boolean;
|
|
6
|
+
readonly players: Player[];
|
|
7
|
+
readonly code: string | null;
|
|
8
|
+
readonly state: UiState;
|
|
9
|
+
readonly shared: SharedState;
|
|
10
|
+
send(data: unknown): void;
|
|
11
|
+
host(): Promise<void>;
|
|
12
|
+
join(code: string): Promise<void>;
|
|
13
|
+
start(): Promise<void>;
|
|
14
|
+
leave(): Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
/** Register `<one-lobby>` (safe to call more than once; a no-op outside a browser). */
|
|
17
|
+
export declare function defineLobbyElement(tag?: string): void;
|
|
18
|
+
declare global {
|
|
19
|
+
interface HTMLElementTagNameMap {
|
|
20
|
+
"one-lobby": OneLobbyElement;
|
|
21
|
+
}
|
|
22
|
+
}
|