tickcli-js 0.0.0-stage → 0.1.1
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/CHANGELOG.md +19 -0
- package/LICENSE +21 -0
- package/README.md +360 -2
- package/client/game.js +1003 -0
- package/client/game3d.js +109 -0
- package/client/relay.js +196 -0
- package/client/room.js +632 -0
- package/dist/cli.d.ts +11 -0
- package/dist/cli.js +82 -0
- package/dist/cli.js.map +1 -0
- package/dist/hub.d.ts +619 -0
- package/dist/hub.js +1777 -0
- package/dist/hub.js.map +1 -0
- package/dist/node.d.ts +106 -0
- package/dist/node.js +301 -0
- package/dist/node.js.map +1 -0
- package/dist/remote.d.ts +75 -0
- package/dist/remote.js +214 -0
- package/dist/remote.js.map +1 -0
- package/dist/serve.d.ts +42 -0
- package/dist/serve.js +231 -0
- package/dist/serve.js.map +1 -0
- package/examples/games/arena.html +108 -0
- package/examples/games/arena3d.html +110 -0
- package/examples/games/chess.html +132 -0
- package/examples/games/cup.html +52 -0
- package/examples/games/delve.html +137 -0
- package/examples/games/dungeon.html +110 -0
- package/examples/games/neon-paint.html +79 -0
- package/examples/games/own-engine.html +41 -0
- package/examples/games/race.html +83 -0
- package/examples/games/shooter.html +95 -0
- package/examples/games/sky-duel.html +136 -0
- package/examples/games/space-squad.html +3627 -0
- package/examples/games/tag.html +51 -0
- package/examples/games/tictactoe.html +53 -0
- package/package.json +101 -4
- package/src/cli.ts +59 -0
- package/src/hub.ts +1521 -0
- package/src/node.ts +245 -0
- package/src/remote.ts +236 -0
- package/src/serve.ts +224 -0
package/dist/hub.d.ts
ADDED
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
export declare const PROTOCOL = 1;
|
|
2
|
+
/** Close codes (also in docs/security.md). 1012 = restart. */
|
|
3
|
+
export declare const CLOSE: {
|
|
4
|
+
readonly restart: 1012;
|
|
5
|
+
readonly roomError: 1011;
|
|
6
|
+
readonly busy: 1013;
|
|
7
|
+
readonly silent: 4009;
|
|
8
|
+
readonly notAllowed: 4001;
|
|
9
|
+
readonly full: 4002;
|
|
10
|
+
readonly replaced: 4003;
|
|
11
|
+
readonly tooFast: 4004;
|
|
12
|
+
readonly badMessage: 4005;
|
|
13
|
+
readonly removed: 4006;
|
|
14
|
+
readonly gameUpdated: 4007;
|
|
15
|
+
readonly tooSlow: 4008;
|
|
16
|
+
readonly idle: 4010;
|
|
17
|
+
};
|
|
18
|
+
export interface Limits {
|
|
19
|
+
maxPlayers: number;
|
|
20
|
+
maxRooms: number;
|
|
21
|
+
maxRoomsPerScope: number;
|
|
22
|
+
maxConnections: number;
|
|
23
|
+
maxPending: number;
|
|
24
|
+
maxPerAddress: number;
|
|
25
|
+
joinsPerMinute: number;
|
|
26
|
+
messageBytes: number;
|
|
27
|
+
playerStateBytes: number;
|
|
28
|
+
sharedStateBytes: number;
|
|
29
|
+
stateRate: number;
|
|
30
|
+
sharedRate: number;
|
|
31
|
+
actionRate: number;
|
|
32
|
+
timeRate: number;
|
|
33
|
+
pendingMs: number;
|
|
34
|
+
seatHoldMs: number;
|
|
35
|
+
evictAfterMs: number;
|
|
36
|
+
awayHoldMs: number;
|
|
37
|
+
hostSilenceMs: number;
|
|
38
|
+
roomOutBytes: number;
|
|
39
|
+
maxOutMbit: number;
|
|
40
|
+
silentMs: number;
|
|
41
|
+
acceptRate: number;
|
|
42
|
+
actionTimeoutMs: number;
|
|
43
|
+
slowBytes: number;
|
|
44
|
+
dropBytes: number;
|
|
45
|
+
rateBytes: number;
|
|
46
|
+
rateUpMs: number;
|
|
47
|
+
listRate: number;
|
|
48
|
+
idleMs: number;
|
|
49
|
+
idleWarnMs: number;
|
|
50
|
+
listIdleMs: number;
|
|
51
|
+
tickMs: number;
|
|
52
|
+
profileBytes: number;
|
|
53
|
+
profileEveryMs: number;
|
|
54
|
+
worldEveryMs: number;
|
|
55
|
+
saveDays: number;
|
|
56
|
+
}
|
|
57
|
+
export declare const DEFAULT_LIMITS: Limits;
|
|
58
|
+
/** One open socket, as the hub sees it. */
|
|
59
|
+
export interface Conn {
|
|
60
|
+
send(data: string): void;
|
|
61
|
+
close(code: number, reason: string): void;
|
|
62
|
+
/** bytes queued and not yet written to the network */
|
|
63
|
+
buffered(): number;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Where saves live; the host app supplies it. `who` is a seat key ("u:<user id>") for a person's profile, null for a game's shared world save (rooms
|
|
67
|
+
* played together), "alone:<seat key>" for one person's own world save (Play alone: a room of one keeps its own game, so playing together never
|
|
68
|
+
* touches it). `slot` is the app's stable key for the game. store(..., null) deletes. Keep a world at least `saveDays` (90).
|
|
69
|
+
*/
|
|
70
|
+
export interface SavePort {
|
|
71
|
+
/** null = nothing saved. Reject only when the save could not be read (the hub then writes nothing over it and tries again later). */
|
|
72
|
+
load(scope: string, slot: string, who: string | null): Promise<{
|
|
73
|
+
at: number;
|
|
74
|
+
data: unknown;
|
|
75
|
+
v?: number;
|
|
76
|
+
} | null>;
|
|
77
|
+
/** meta.session: one id per room session for world saves, so a store that keeps an earlier copy can take it once per session. meta.v: the game's
|
|
78
|
+
* save format number (multiplayer.saveVersion), kept with the data and given back by load (a file without it counts as 1). */
|
|
79
|
+
store(scope: string, slot: string, who: string | null, data: unknown | null, meta?: {
|
|
80
|
+
session?: string;
|
|
81
|
+
v?: number;
|
|
82
|
+
}): Promise<void>;
|
|
83
|
+
}
|
|
84
|
+
/** Who a connection is, decided by the server (ticket or guest pass), never by the page. */
|
|
85
|
+
export interface Who {
|
|
86
|
+
/** stable seat key: "u:<user id>" or "g:<guest pass id>" */
|
|
87
|
+
seat: string;
|
|
88
|
+
name: string;
|
|
89
|
+
guest: boolean;
|
|
90
|
+
}
|
|
91
|
+
/** The page's declaration: room size and the number range of each player field. */
|
|
92
|
+
/** saveVersion: the game's save format (whole number, default 1); a page edited to read another shape raises it (README: saved games). */
|
|
93
|
+
export interface Spec {
|
|
94
|
+
max: number;
|
|
95
|
+
player: Record<string, [number, number]>;
|
|
96
|
+
save: boolean;
|
|
97
|
+
saveVersion: number;
|
|
98
|
+
}
|
|
99
|
+
export interface PlayerView {
|
|
100
|
+
id: string;
|
|
101
|
+
name: string;
|
|
102
|
+
online: boolean;
|
|
103
|
+
hidden: boolean;
|
|
104
|
+
guest: boolean;
|
|
105
|
+
state: Record<string, unknown>;
|
|
106
|
+
}
|
|
107
|
+
type Json = null | boolean | number | string | Json[] | {
|
|
108
|
+
[k: string]: Json;
|
|
109
|
+
};
|
|
110
|
+
/** The shape limits of everything a page writes (also in room.js, which checks before sending, and in the README). */
|
|
111
|
+
export declare const SHAPE: {
|
|
112
|
+
readonly text: 200;
|
|
113
|
+
readonly list: 64;
|
|
114
|
+
readonly keys: 64;
|
|
115
|
+
readonly depth: 4;
|
|
116
|
+
readonly key: 40;
|
|
117
|
+
};
|
|
118
|
+
/** Plain JSON within the shape limits: depth, keys, text and list lengths, finite numbers. Returns false for anything else. */
|
|
119
|
+
export declare function plain(v: unknown, depth?: number): v is Json;
|
|
120
|
+
/** Where a value breaks the shape limits: the path of the first bad part ("mobs.12.name", "" for the value itself), or null when it is fine. */
|
|
121
|
+
export declare function badPath(v: unknown, depth?: number, at?: string): string | null;
|
|
122
|
+
/** A checked spec, or null. */
|
|
123
|
+
export declare function parseSpec(raw: unknown, maxPlayers?: number): Spec | null;
|
|
124
|
+
/** Apply a patch to state: objects merge key by key, null removes, anything else (lists too) replaces. Returns a new object; `into` is not changed. */
|
|
125
|
+
export declare function applyPatch(into: Record<string, unknown>, patch: Record<string, unknown>): Record<string, unknown>;
|
|
126
|
+
/** The part of a patch that changes `cur`: leaves equal to the current value are left out, a null for a key that is not there too, lists equal by
|
|
127
|
+
* their JSON text. Null = nothing changes. So a game may write whole objects every step: only what changed is applied, counted and sent. */
|
|
128
|
+
export declare function changedOnly(cur: Record<string, unknown>, patch: Record<string, unknown>): Record<string, unknown> | null;
|
|
129
|
+
/** How many bytes the JSON text of `cur` grows by when `patch` (already reduced by changedOnly) is applied: only the changed leaves are measured. */
|
|
130
|
+
export declare function sizeDelta(cur: Record<string, unknown>, patch: Record<string, unknown>): number;
|
|
131
|
+
/** Combine two patches that go out together (nulls kept, so removals still reach the others). */
|
|
132
|
+
export declare function joinPatch(into: Record<string, unknown>, patch: Record<string, unknown>): void;
|
|
133
|
+
declare class Bucket {
|
|
134
|
+
tokens: number;
|
|
135
|
+
at: number;
|
|
136
|
+
rate: number;
|
|
137
|
+
constructor(rate: number, now: number);
|
|
138
|
+
take(now: number): boolean;
|
|
139
|
+
}
|
|
140
|
+
interface Seat {
|
|
141
|
+
key: string;
|
|
142
|
+
id: string;
|
|
143
|
+
name: string;
|
|
144
|
+
guest: boolean;
|
|
145
|
+
state: Record<string, unknown>;
|
|
146
|
+
session: Session | null;
|
|
147
|
+
hidden: boolean;
|
|
148
|
+
/** the host's game loop stopped reporting (a frozen tab) while its socket stayed open */
|
|
149
|
+
stalled: boolean;
|
|
150
|
+
since: number;
|
|
151
|
+
offlineAt: number;
|
|
152
|
+
lastAct: number;
|
|
153
|
+
/** the save format number of the profile as loaded */
|
|
154
|
+
profV: number;
|
|
155
|
+
/** restored from a snapshot after a restart: the first welcome says kept (the page carries on in the same match) */
|
|
156
|
+
kept: boolean;
|
|
157
|
+
/** the person's saved profile once loaded (null = not loaded, or saving is off) */
|
|
158
|
+
prof: Record<string, unknown> | null;
|
|
159
|
+
profLoad: Promise<Record<string, unknown> | null> | null;
|
|
160
|
+
profDirty: boolean;
|
|
161
|
+
profAt: number;
|
|
162
|
+
/** JSON size of state, kept up to date by each write (an exact count near the limit and every 256 writes) */
|
|
163
|
+
stateBytes: number;
|
|
164
|
+
}
|
|
165
|
+
interface Room {
|
|
166
|
+
key: string;
|
|
167
|
+
scope: string;
|
|
168
|
+
game: string;
|
|
169
|
+
spec: Spec;
|
|
170
|
+
specKey: string;
|
|
171
|
+
code: string;
|
|
172
|
+
priv: boolean;
|
|
173
|
+
/** the app's stable key for saves ("" = none) and whether this room keeps a world save */
|
|
174
|
+
slot: string;
|
|
175
|
+
/** Play alone: the one seat this room belongs to ("" = a room for anyone). Its world save is that person's own, not the shared one. */
|
|
176
|
+
owner: string;
|
|
177
|
+
world: boolean;
|
|
178
|
+
/** the world save this room holds the lease on ("" = none). One room at a time writes a given save. */
|
|
179
|
+
lease: string;
|
|
180
|
+
/** the game keeps a world save, but another room is playing it: this room is not saved */
|
|
181
|
+
saveBusy: boolean;
|
|
182
|
+
/** the world save's load: nothing is written (and nothing cleared) until it is "ready"; "failed" retries on a timer (loadAt) */
|
|
183
|
+
worldState: "off" | "loading" | "ready" | "failed";
|
|
184
|
+
loadTries: number;
|
|
185
|
+
loadAt: number;
|
|
186
|
+
/** a Start that came while the save was still loading: run once it is loaded (Continue / New game is offered first when a save turns up) */
|
|
187
|
+
startWait: {
|
|
188
|
+
resume: boolean;
|
|
189
|
+
at: number;
|
|
190
|
+
} | null;
|
|
191
|
+
/** this room's session id for the store (one earlier copy per session) */
|
|
192
|
+
sid: string;
|
|
193
|
+
saved: {
|
|
194
|
+
at: number;
|
|
195
|
+
data: Record<string, unknown>;
|
|
196
|
+
v: number;
|
|
197
|
+
} | null;
|
|
198
|
+
/** how many shared writes the room had at the last world write; a world save is due when it differs */
|
|
199
|
+
wrev: number;
|
|
200
|
+
savedWrev: number;
|
|
201
|
+
savedAt: number;
|
|
202
|
+
/** the host cleared the save (the game is won or lost): nothing is written until the next start */
|
|
203
|
+
noSave: boolean;
|
|
204
|
+
resumed: boolean;
|
|
205
|
+
seats: Map<string, Seat>;
|
|
206
|
+
byId: Map<string, Seat>;
|
|
207
|
+
banned: Set<string>;
|
|
208
|
+
match: number;
|
|
209
|
+
rev: number;
|
|
210
|
+
term: number;
|
|
211
|
+
host: Seat | null;
|
|
212
|
+
hostAliveAt: number;
|
|
213
|
+
started: boolean;
|
|
214
|
+
/** a match ended; the next start begins a new one */
|
|
215
|
+
played: boolean;
|
|
216
|
+
shared: Record<string, unknown>;
|
|
217
|
+
pendPlayers: Map<string, Record<string, unknown>>;
|
|
218
|
+
pendShared: {
|
|
219
|
+
patch: Record<string, unknown>;
|
|
220
|
+
term: number;
|
|
221
|
+
} | null;
|
|
222
|
+
/** when the pending values were sampled by their senders (shared clock): per player id, and for the shared part */
|
|
223
|
+
pendPT: Map<string, number>;
|
|
224
|
+
pendSC: number;
|
|
225
|
+
pendAt: number;
|
|
226
|
+
actions: Map<number, {
|
|
227
|
+
from: Seat;
|
|
228
|
+
ref: number;
|
|
229
|
+
at: number;
|
|
230
|
+
}>;
|
|
231
|
+
nextAction: number;
|
|
232
|
+
tickN: number;
|
|
233
|
+
/** bytes sent to this room's sockets: total, at the last one-second mark, and the rate over the last second */
|
|
234
|
+
outBytes: number;
|
|
235
|
+
outMark: number;
|
|
236
|
+
outRate: number;
|
|
237
|
+
/** effects waiting for the next frame (g.fx, shots): they go out with the state, in order */
|
|
238
|
+
/** 0..9: which sub-tick this room sends on, so all rooms do not write their sockets in the same instant */
|
|
239
|
+
phase: number;
|
|
240
|
+
/** data budget: the room's moving average of bytes out a second, its send interval multiplier (1, 2, 4, 8) and whether its host was told */
|
|
241
|
+
outAvg: number;
|
|
242
|
+
slowF: number;
|
|
243
|
+
slowTold: boolean;
|
|
244
|
+
pendFx: {
|
|
245
|
+
from: string;
|
|
246
|
+
name: string;
|
|
247
|
+
data: unknown;
|
|
248
|
+
}[];
|
|
249
|
+
pendFxBytes: number;
|
|
250
|
+
/** when the room was made (room list: newest first among equals) */
|
|
251
|
+
created: number;
|
|
252
|
+
/** when a person last did something here (moved, acted, sent an effect, joined, left, started, came back to the page, pressed stay);
|
|
253
|
+
* the host's automatic shared writes and the pages' clock pings do not count, so a game left running with nobody at it still ends */
|
|
254
|
+
activeAt: number;
|
|
255
|
+
/** the players were told the room closes soon */
|
|
256
|
+
idleWarned: boolean;
|
|
257
|
+
/** state frames sent on the tick so far; a reader on rate 1/d gets every d-th one (see Session.div) */
|
|
258
|
+
flushN: number;
|
|
259
|
+
/** per slower rate (2, 4): everything since that rate's last frame, merged; null while every reader is on the full rate */
|
|
260
|
+
tiers: Map<number, Frame> | null;
|
|
261
|
+
/** JSON size of shared, kept like seat.stateBytes; writes since the last exact count */
|
|
262
|
+
sharedBytes: number;
|
|
263
|
+
sizeChecks: number;
|
|
264
|
+
}
|
|
265
|
+
/** A frame's content: changed player fields and their sample times, the shared patch and its sample time, effects. */
|
|
266
|
+
interface Frame {
|
|
267
|
+
players: Map<string, Record<string, unknown>>;
|
|
268
|
+
pt: Map<string, number>;
|
|
269
|
+
shared: {
|
|
270
|
+
patch: Record<string, unknown>;
|
|
271
|
+
term: number;
|
|
272
|
+
} | null;
|
|
273
|
+
sc: number;
|
|
274
|
+
fx: {
|
|
275
|
+
from: string;
|
|
276
|
+
name: string;
|
|
277
|
+
data: unknown;
|
|
278
|
+
}[];
|
|
279
|
+
}
|
|
280
|
+
/** One socket's life in the hub: pending (not yet identified) -> identified -> joined a room -> closed. */
|
|
281
|
+
export declare class Session {
|
|
282
|
+
who: Who | null;
|
|
283
|
+
scope: string;
|
|
284
|
+
game: string;
|
|
285
|
+
slot: string;
|
|
286
|
+
room: Room | null;
|
|
287
|
+
seat: Seat | null;
|
|
288
|
+
dead: boolean;
|
|
289
|
+
lagging: boolean;
|
|
290
|
+
readonly opened: number;
|
|
291
|
+
state: Bucket;
|
|
292
|
+
acts: Bucket;
|
|
293
|
+
/** clock pings: their own small bucket, and a count before sign-in */
|
|
294
|
+
clock: Bucket;
|
|
295
|
+
early: number;
|
|
296
|
+
strikes: number;
|
|
297
|
+
/** when this socket was last told too_fast */
|
|
298
|
+
slowAt: number;
|
|
299
|
+
/** when the last message came in (a visible page sends a clock ping every 5 s) */
|
|
300
|
+
lastRx: number;
|
|
301
|
+
/** shared writes: a bucket of their own (room.js sends at most 12.5 a second plus split parts) */
|
|
302
|
+
sharedB: Bucket;
|
|
303
|
+
/** Update rate: this reader gets every div-th state frame (1, 2 or 4; effects and changes in between are merged into it, nothing is lost).
|
|
304
|
+
* netDiv is the hub's own reading of the socket (its send queue), askDiv what the page asked for (rate {div}: a slow link or device seen by
|
|
305
|
+
* the page); the slower of the two is used, taken over only when every rate's frame has just gone out (so nobody misses a change). */
|
|
306
|
+
div: number;
|
|
307
|
+
netDiv: number;
|
|
308
|
+
askDiv: number;
|
|
309
|
+
/** when netDiv last went down, and since when the send queue has been near empty */
|
|
310
|
+
downAt: number;
|
|
311
|
+
clearAt: number;
|
|
312
|
+
/** the page's round trip to the hub (ms, its own best recent ping, sent along with its clock pings); 0 = not known */
|
|
313
|
+
rtt: number;
|
|
314
|
+
/** room-list requests */
|
|
315
|
+
listB: Bucket;
|
|
316
|
+
/** what the page understands (join f: ["fxb", ...]): "fxb" = effects inside the delta frame. Pages without it get one fx message per effect. */
|
|
317
|
+
feats: Set<string>;
|
|
318
|
+
readonly hub: RoomHub;
|
|
319
|
+
readonly conn: Conn;
|
|
320
|
+
readonly addr: string;
|
|
321
|
+
constructor(hub: RoomHub, conn: Conn, addr: string);
|
|
322
|
+
/** The server decided who this is (ticket or guest pass) and which namespace and game version they may join. */
|
|
323
|
+
identify(scope: string, game: string, who: Who, slot?: string): void;
|
|
324
|
+
receive(text: string): void;
|
|
325
|
+
/** The socket closed (any reason). */
|
|
326
|
+
closed(): void;
|
|
327
|
+
send(m: Record<string, unknown>): void;
|
|
328
|
+
end(code: number, reason: string): void;
|
|
329
|
+
}
|
|
330
|
+
/** Live numbers (per second over the last second unless named otherwise). No names, no scope ids. */
|
|
331
|
+
export interface LiveStats {
|
|
332
|
+
framesOut: number;
|
|
333
|
+
bytesOut: number;
|
|
334
|
+
msgsIn: number;
|
|
335
|
+
/** time one sub-tick took (one every 10 ms), over the last 1,200 (about 12 s; ms) */
|
|
336
|
+
tickMs: {
|
|
337
|
+
p50: number;
|
|
338
|
+
p99: number;
|
|
339
|
+
max: number;
|
|
340
|
+
};
|
|
341
|
+
/** sockets behind on their reads now */
|
|
342
|
+
slow: number;
|
|
343
|
+
/** rooms with a match in progress */
|
|
344
|
+
started: number;
|
|
345
|
+
savesOk: number;
|
|
346
|
+
savesFailed: number;
|
|
347
|
+
loadsFailed: number;
|
|
348
|
+
worldWritesPerMin: number;
|
|
349
|
+
/** event-loop delay, set by the host process (main.ts) once a minute */
|
|
350
|
+
loop: {
|
|
351
|
+
p50: number;
|
|
352
|
+
p99: number;
|
|
353
|
+
max: number;
|
|
354
|
+
} | null;
|
|
355
|
+
/** over the data cap or a late loop now; rooms slowed by their own budget */
|
|
356
|
+
pressure: boolean;
|
|
357
|
+
slowedRooms: number;
|
|
358
|
+
/** the 5 rooms sending the most */
|
|
359
|
+
top: {
|
|
360
|
+
players: number;
|
|
361
|
+
bytesOut: number;
|
|
362
|
+
sharedBytes: number;
|
|
363
|
+
}[];
|
|
364
|
+
}
|
|
365
|
+
/** A restart's hand-over of live rooms (exportRooms / importRooms). The format number changes when the shape does; another format is ignored. */
|
|
366
|
+
export declare const SNAPSHOT_FORMAT = 1;
|
|
367
|
+
export interface SnapRoom {
|
|
368
|
+
key: string;
|
|
369
|
+
scope: string;
|
|
370
|
+
game: string;
|
|
371
|
+
spec: Spec;
|
|
372
|
+
code: string;
|
|
373
|
+
priv: boolean;
|
|
374
|
+
slot: string;
|
|
375
|
+
owner: string;
|
|
376
|
+
world: boolean;
|
|
377
|
+
lease: string;
|
|
378
|
+
saveBusy: boolean;
|
|
379
|
+
worldState: "ready" | "loading";
|
|
380
|
+
saved: {
|
|
381
|
+
at: number;
|
|
382
|
+
data: Record<string, unknown>;
|
|
383
|
+
v: number;
|
|
384
|
+
} | null;
|
|
385
|
+
wrev: number;
|
|
386
|
+
savedWrev: number;
|
|
387
|
+
noSave: boolean;
|
|
388
|
+
resumed: boolean;
|
|
389
|
+
sid: string;
|
|
390
|
+
match: number;
|
|
391
|
+
rev: number;
|
|
392
|
+
term: number;
|
|
393
|
+
started: boolean;
|
|
394
|
+
played: boolean;
|
|
395
|
+
shared: Record<string, unknown>;
|
|
396
|
+
banned: string[];
|
|
397
|
+
seats: {
|
|
398
|
+
key: string;
|
|
399
|
+
id: string;
|
|
400
|
+
name: string;
|
|
401
|
+
guest: boolean;
|
|
402
|
+
state: Record<string, unknown>;
|
|
403
|
+
hidden: boolean;
|
|
404
|
+
since: number;
|
|
405
|
+
}[];
|
|
406
|
+
}
|
|
407
|
+
export interface RoomsSnapshot {
|
|
408
|
+
format: number;
|
|
409
|
+
at: number;
|
|
410
|
+
rooms: SnapRoom[];
|
|
411
|
+
}
|
|
412
|
+
export interface HubStats {
|
|
413
|
+
rooms: number;
|
|
414
|
+
players: number;
|
|
415
|
+
online: number;
|
|
416
|
+
connections: number;
|
|
417
|
+
pending: number;
|
|
418
|
+
refused: Record<string, number>;
|
|
419
|
+
live: LiveStats;
|
|
420
|
+
}
|
|
421
|
+
export declare class RoomHub {
|
|
422
|
+
readonly limits: Limits;
|
|
423
|
+
readonly now: () => number;
|
|
424
|
+
readonly boot: string;
|
|
425
|
+
private readonly secret;
|
|
426
|
+
private rooms;
|
|
427
|
+
private sessions;
|
|
428
|
+
pending: number;
|
|
429
|
+
private perAddr;
|
|
430
|
+
private joins;
|
|
431
|
+
private refused;
|
|
432
|
+
private timer;
|
|
433
|
+
private sub;
|
|
434
|
+
private acceptAt;
|
|
435
|
+
private accepted;
|
|
436
|
+
private houseAt;
|
|
437
|
+
/** games whose room failed once (logged once each) */
|
|
438
|
+
private erred;
|
|
439
|
+
private closing;
|
|
440
|
+
private readonly saves;
|
|
441
|
+
private writing;
|
|
442
|
+
/** the newest write of each save ("scope|slot|who"): a load waits for that one only, not for other rooms' saves */
|
|
443
|
+
private writingKey;
|
|
444
|
+
private loading;
|
|
445
|
+
/** plain counters for stats() (saves_ok, ...) */
|
|
446
|
+
private counts;
|
|
447
|
+
/** meters: totals, the totals at the last one-second mark, the per-second rates, tick times, world writes in the last minute */
|
|
448
|
+
private meter;
|
|
449
|
+
private mark;
|
|
450
|
+
private rates;
|
|
451
|
+
private tickTimes;
|
|
452
|
+
private tickCount;
|
|
453
|
+
private worldWrites;
|
|
454
|
+
/** event-loop delay from the host process (main.ts sets it once a minute) */
|
|
455
|
+
loop: {
|
|
456
|
+
p50: number;
|
|
457
|
+
p99: number;
|
|
458
|
+
max: number;
|
|
459
|
+
} | null;
|
|
460
|
+
/** the same over the last 10 s (main.ts), for the half-rate switch */
|
|
461
|
+
loopRecent: {
|
|
462
|
+
p99: number;
|
|
463
|
+
} | null;
|
|
464
|
+
/** over the data cap or a late loop: bigger rooms at half rate, no new rooms */
|
|
465
|
+
pressure: boolean;
|
|
466
|
+
/** world save leases: "scope|slot|who" -> the room key that may write it */
|
|
467
|
+
private leases;
|
|
468
|
+
/** shutdown() hands the live rooms to this (a restart writes them to disk; importRooms() in the next process takes them back) */
|
|
469
|
+
private readonly onSnapshot;
|
|
470
|
+
/** the room list (rooms {}) is answered; off: the answer is {off: true} */
|
|
471
|
+
readonly listRooms: boolean;
|
|
472
|
+
/** room lists made in the last 250 ms, per namespace, game and save slot (many pages asking at once cost one build) */
|
|
473
|
+
private lists;
|
|
474
|
+
/** rooms allowed in one namespace when it differs from limits.maxRoomsPerScope (a host app's per-customer cap, tickcli-js/remote) */
|
|
475
|
+
private readonly roomsPerScope;
|
|
476
|
+
constructor(opts: {
|
|
477
|
+
secret: string;
|
|
478
|
+
now?: () => number;
|
|
479
|
+
limits?: Partial<Limits>;
|
|
480
|
+
saves?: SavePort;
|
|
481
|
+
onSnapshot?: (snap: RoomsSnapshot) => Promise<void> | void;
|
|
482
|
+
listRooms?: boolean;
|
|
483
|
+
roomsPerScope?: (scope: string) => number | undefined;
|
|
484
|
+
});
|
|
485
|
+
/** A short public id for a seat in one namespace; the seat key itself (a user id or guest pass id) is never shown to others. */
|
|
486
|
+
publicId(scope: string, seat: string): string;
|
|
487
|
+
/** The save slot a socket plays under; without a store there are no slots. */
|
|
488
|
+
private slotOf;
|
|
489
|
+
private refuse;
|
|
490
|
+
private count;
|
|
491
|
+
/** A new socket. Null = refused (the caller closes it with the code given). */
|
|
492
|
+
connect(conn: Conn, addr: string): Session | {
|
|
493
|
+
refused: number;
|
|
494
|
+
reason: string;
|
|
495
|
+
};
|
|
496
|
+
/** Meter: `n` copies of a text of `len` bytes (UTF-8, as on the wire) went out (to one room's sockets when given). */
|
|
497
|
+
sent(len: number, n: number, room?: Room | null): void;
|
|
498
|
+
receive(s: Session, text: string): void;
|
|
499
|
+
private bad;
|
|
500
|
+
/** Over the rate: the message is dropped; a sender that keeps pushing far past it is disconnected. */
|
|
501
|
+
private strike;
|
|
502
|
+
private err;
|
|
503
|
+
private join;
|
|
504
|
+
/** A person did something in the room: its idle time starts over (a warning already sent is taken back). */
|
|
505
|
+
private active;
|
|
506
|
+
/** Nobody did anything for idleMs: saves are written, every page hears 4010 (game.js offers a new room), the room is gone. */
|
|
507
|
+
private closeIdle;
|
|
508
|
+
/** Moves a room running an older version of its game off its code (see join): a new hidden code, private, its save lease kept; its pages hear
|
|
509
|
+
* `newer` (game.js: press restart to play the new version). */
|
|
510
|
+
private retire;
|
|
511
|
+
private create;
|
|
512
|
+
private snapshot;
|
|
513
|
+
/** Host = the longest-connected player who is online and visible. None = the match waits. Every change starts a new term. */
|
|
514
|
+
private elect;
|
|
515
|
+
private alive;
|
|
516
|
+
private vis;
|
|
517
|
+
private me;
|
|
518
|
+
private sharedWrite;
|
|
519
|
+
/** The sender's sample time of a write (its shared clock, whole ms), trusted within 500 ms back and 50 ms ahead; else now. Pages draw others
|
|
520
|
+
* at these times, so updates sent 80 ms apart are drawn 80 ms apart whatever the fan-out tick did to them. */
|
|
521
|
+
private stamp;
|
|
522
|
+
/** A state size after a write: the running count, or the exact one when it is within 10% of the limit and every 256th time (a drift is counted). */
|
|
523
|
+
private sized;
|
|
524
|
+
private fx;
|
|
525
|
+
private rename;
|
|
526
|
+
private act;
|
|
527
|
+
private result;
|
|
528
|
+
private startMatch;
|
|
529
|
+
private beginMatch;
|
|
530
|
+
private endMatch;
|
|
531
|
+
private kick;
|
|
532
|
+
private removeSeat;
|
|
533
|
+
/** Forget a room (its last write is already queued) and give up its save lease. Every path that ends a room goes through here. */
|
|
534
|
+
private closeRoom;
|
|
535
|
+
/** A socket is gone. Its seat stays (offline) for seatHoldMs, without host duty. */
|
|
536
|
+
drop(s: Session): void;
|
|
537
|
+
private write;
|
|
538
|
+
/** The person's profile, loaded once per seat. Null = saving is off here, or the store failed (nothing is ever written over a profile that did not load). */
|
|
539
|
+
private loadProfile;
|
|
540
|
+
private sendProfile;
|
|
541
|
+
private saveProfile;
|
|
542
|
+
private flushProfile;
|
|
543
|
+
/** Load the room's world save. Until it is loaded nothing is written or cleared (a save that could not be read is never written over); a failed
|
|
544
|
+
* load is tried again after 2, 5, 15 s and then every 30 s, and the lobby says the game is not being saved meanwhile. */
|
|
545
|
+
private loadWorld;
|
|
546
|
+
private loadFailed;
|
|
547
|
+
/** Write the shared state as the world save when it changed (every worldEveryMs, or now when forced) and holds something besides the shell's own keys. */
|
|
548
|
+
private saveWorld;
|
|
549
|
+
/** {t:"world", clear:true}: the host ends the saved game (won, lost, abandoned); nothing is saved again until the next start. */
|
|
550
|
+
private worldMsg;
|
|
551
|
+
private broadcast;
|
|
552
|
+
/** Send a room's pending changes and effects now. Variants of one frame, each made once: the host does not get its own shared writes back
|
|
553
|
+
* (it applied them), pages without "fxb" get effects as single fx messages, an effect never goes back to its sender's old-style page, and hidden
|
|
554
|
+
* pages get nothing (one fresh copy when they come back). fxOnly: effects alone (big rooms send state every second tick, effects every tick).
|
|
555
|
+
* tick: called by the fan-out tick. Readers on a slower rate (Session.div) get every div-th tick frame, with everything since their last one
|
|
556
|
+
* merged in; any other call (a join, a leave, a new host, the start) sends every rate's frame at once, so those events never overtake a change. */
|
|
557
|
+
private flush;
|
|
558
|
+
/** The room's pending changes as one frame (null when there is nothing), and the pending lists cleared. */
|
|
559
|
+
private takePending;
|
|
560
|
+
/** Readers take their wanted rate (the slower of the hub's reading and the page's ask); the merged frames exist only for rates in use. */
|
|
561
|
+
private retier;
|
|
562
|
+
/** One frame to the room's readers on rate 1/div. A room of one gets none: everything in it came from that page (its own moves, its
|
|
563
|
+
* own shared writes as host, its own effects), so playing alone costs no data coming down. */
|
|
564
|
+
private emit;
|
|
565
|
+
/** The open rooms a page could join instead (rooms {spec?}): same namespace, game version and save slot; not private, not someone's game
|
|
566
|
+
* alone, not empty. Per room: code, host name, players connected and away (dropped, seat held), max, started, match, the host's round trip to the hub (ms, 0 = not
|
|
567
|
+
* known: the host's browser runs the rules, so its link is what a newcomer feels), slowed (the room sends less often than normal), full,
|
|
568
|
+
* yours. Up to 50, the ones you can join first, then those waiting to start, then the fullest, then the newest. Made at most every 250 ms
|
|
569
|
+
* per game; asking costs one token of a 1-a-second bucket. */
|
|
570
|
+
private roomList;
|
|
571
|
+
/** Every tickMs: send changes (every second tick in rooms over 8), handle slow readers, timeouts, silent hosts, expired seats. */
|
|
572
|
+
tick(): void;
|
|
573
|
+
/** A room over its data budget sends half as often (up to 8 times less), its host is told once (error too_much_data); it speeds up again when
|
|
574
|
+
* the faster rate would be under 80% of the budget. */
|
|
575
|
+
private budget;
|
|
576
|
+
/** How many ticks apart a room sends its state: 1 (50 ms), 2 in rooms over 8 players, times its own budget factor, times 2 under server pressure
|
|
577
|
+
* for rooms over 4 players. */
|
|
578
|
+
private every;
|
|
579
|
+
/** Once a second: the rates over the last second (server and per room). */
|
|
580
|
+
private meters;
|
|
581
|
+
/** Everything but the room sends; `sendAll` false when subTick sends rooms on their own phase. */
|
|
582
|
+
private tickInner;
|
|
583
|
+
private housekeep;
|
|
584
|
+
/** One room's part of a tick. An error there closes that room (1011) and the server keeps going: one bad room never stops the process. */
|
|
585
|
+
private guard;
|
|
586
|
+
/** Called every tickMs / 5 by start(). Housekeeping once per tickMs; each room sends on its own phase (every 5 sub-ticks, every 10 in rooms
|
|
587
|
+
* over 8 players, effects every 5), so 2,000 sockets are not written in one burst. tick() stays the one-call form for tests and simple hosts. */
|
|
588
|
+
subTick(): void;
|
|
589
|
+
start(): void;
|
|
590
|
+
stop(): void;
|
|
591
|
+
/** Server shutdown: pending changes and saves are written, the live rooms go to onSnapshot (the next process takes them back), then every player
|
|
592
|
+
* hears 1012 and reconnects. Without onSnapshot a restart ends every match. */
|
|
593
|
+
shutdown(): Promise<void>;
|
|
594
|
+
/** The live rooms as plain JSON: everything a new process needs to carry on (sockets, timers and open actions are not kept). */
|
|
595
|
+
exportRooms(): RoomsSnapshot;
|
|
596
|
+
/** Take back rooms from exportRooms() after a restart: every seat offline (held 60 s for its player to come back), no host until one is back, a new
|
|
597
|
+
* term, save leases taken again. A snapshot older than maxAgeMs or of another format is ignored. Returns how many rooms came back. */
|
|
598
|
+
importRooms(snap: unknown, o?: {
|
|
599
|
+
maxAgeMs?: number;
|
|
600
|
+
}): number;
|
|
601
|
+
/** Write every changed profile and world now and wait until the store has taken them (a restart, a test). */
|
|
602
|
+
flushSaves(): Promise<void>;
|
|
603
|
+
/** Resolves once every world save being read now has been read (tests, a bench). */
|
|
604
|
+
whenLoaded(): Promise<void>;
|
|
605
|
+
private settle;
|
|
606
|
+
/** Close every room of one namespace (a public link stopped). */
|
|
607
|
+
closeScope(scope: string, reason?: string): void;
|
|
608
|
+
/** One namespace's use right now: rooms, seats, connected players and bytes out a second (over the last second). For per-customer caps. */
|
|
609
|
+
scopeStats(scope: string): {
|
|
610
|
+
rooms: number;
|
|
611
|
+
players: number;
|
|
612
|
+
online: number;
|
|
613
|
+
bytesOut: number;
|
|
614
|
+
};
|
|
615
|
+
stats(): HubStats;
|
|
616
|
+
}
|
|
617
|
+
/** A guest's display name: letters, digits, spaces, "_" and "-", at most 20 characters. */
|
|
618
|
+
export declare function cleanName(raw: unknown): string | null;
|
|
619
|
+
export {};
|