tickcli-js 0.1.4 → 0.1.6
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 +8 -0
- package/README.md +3 -3
- package/dist/hub.d.ts +44 -4
- package/dist/hub.js +163 -8
- package/dist/hub.js.map +1 -1
- package/dist/remote.d.ts +10 -2
- package/dist/remote.js +5 -1
- package/dist/remote.js.map +1 -1
- package/package.json +1 -1
- package/src/hub.ts +111 -10
- package/src/remote.ts +15 -3
package/src/hub.ts
CHANGED
|
@@ -474,6 +474,8 @@ export interface LiveStats {
|
|
|
474
474
|
/** over the data cap or a late loop now; rooms slowed by their own budget */
|
|
475
475
|
pressure: boolean;
|
|
476
476
|
slowedRooms: number;
|
|
477
|
+
/** namespaces slowed because they are over their own outBytes cap (ScopeCaps) */
|
|
478
|
+
slowedScopes: number;
|
|
477
479
|
/** the 5 rooms sending the most */
|
|
478
480
|
top: { players: number; bytesOut: number; sharedBytes: number }[];
|
|
479
481
|
}
|
|
@@ -487,6 +489,15 @@ export interface SnapRoom {
|
|
|
487
489
|
}
|
|
488
490
|
export interface RoomsSnapshot { format: number; at: number; rooms: SnapRoom[] }
|
|
489
491
|
|
|
492
|
+
/** One namespace's own share of the server (a host app's per-customer caps, tickcli-js/remote). */
|
|
493
|
+
export interface ScopeCaps {
|
|
494
|
+
/** data going out to its rooms, bytes a second (moving average over all its rooms): over it, all its rooms send half as often (up to 8 times
|
|
495
|
+
* less) until the faster rate would be under 80% of it again. Other namespaces keep their full rate. */
|
|
496
|
+
outBytes?: number;
|
|
497
|
+
/** joins a minute across the namespace (reconnects count), on top of the per-address limit; over it: 1013 busy, the page tries again */
|
|
498
|
+
joinsPerMinute?: number;
|
|
499
|
+
}
|
|
500
|
+
|
|
490
501
|
export interface HubStats { rooms: number; players: number; online: number; connections: number; pending: number; refused: Record<string, number>; live: LiveStats }
|
|
491
502
|
|
|
492
503
|
export class RoomHub {
|
|
@@ -500,6 +511,10 @@ export class RoomHub {
|
|
|
500
511
|
private perAddr = new Map<string, number>();
|
|
501
512
|
private addrHits = new Map<string, { at: number; n: number }>();
|
|
502
513
|
private joins = new Map<string, number[]>();
|
|
514
|
+
/** per namespace with caps: its slow-down factor and moving average out (bytes a second) */
|
|
515
|
+
private scopeSlow = new Map<string, { f: number; avg: number }>();
|
|
516
|
+
/** per namespace with a joins cap: joins in this minute and the last one (a sliding estimate, no list per join) */
|
|
517
|
+
private scopeJoins = new Map<string, { at: number; n: number; prev: number }>();
|
|
503
518
|
private refused: Record<string, number> = {};
|
|
504
519
|
private timer: ReturnType<typeof setInterval> | null = null;
|
|
505
520
|
private sub = 0;
|
|
@@ -531,6 +546,8 @@ export class RoomHub {
|
|
|
531
546
|
pressure = false;
|
|
532
547
|
/** world save leases: "scope|slot|who" -> the room key that may write it */
|
|
533
548
|
private leases = new Map<string, string>();
|
|
549
|
+
/** namespaces being handed to another process now (exportScope): joins to them are refused with 1012 */
|
|
550
|
+
private moving = new Set<string>();
|
|
534
551
|
|
|
535
552
|
/** shutdown() hands the live rooms to this (a restart writes them to disk; importRooms() in the next process takes them back) */
|
|
536
553
|
private readonly onSnapshot: ((snap: RoomsSnapshot) => Promise<void> | void) | null;
|
|
@@ -541,9 +558,12 @@ export class RoomHub {
|
|
|
541
558
|
|
|
542
559
|
/** rooms allowed in one namespace when it differs from limits.maxRoomsPerScope (a host app's per-customer cap, tickcli-js/remote) */
|
|
543
560
|
private readonly roomsPerScope: ((scope: string) => number | undefined) | null;
|
|
561
|
+
/** a namespace's own data and joins caps (tickcli-js/remote: a deployment's caps); undefined = only the server-wide limits */
|
|
562
|
+
private readonly scopeCaps: ((scope: string) => ScopeCaps | undefined) | null;
|
|
544
563
|
|
|
545
|
-
constructor(opts: { secret: string; now?: () => number; limits?: Partial<Limits>; saves?: SavePort; onSnapshot?: (snap: RoomsSnapshot) => Promise<void> | void; listRooms?: boolean; roomsPerScope?: (scope: string) => number | undefined }) {
|
|
564
|
+
constructor(opts: { secret: string; now?: () => number; limits?: Partial<Limits>; saves?: SavePort; onSnapshot?: (snap: RoomsSnapshot) => Promise<void> | void; listRooms?: boolean; roomsPerScope?: (scope: string) => number | undefined; scopeCaps?: (scope: string) => ScopeCaps | undefined }) {
|
|
546
565
|
this.roomsPerScope = opts.roomsPerScope ?? null;
|
|
566
|
+
this.scopeCaps = opts.scopeCaps ?? null;
|
|
547
567
|
this.saves = opts.saves ?? null;
|
|
548
568
|
this.listRooms = opts.listRooms !== false;
|
|
549
569
|
this.onSnapshot = opts.onSnapshot ?? null;
|
|
@@ -675,12 +695,14 @@ export class RoomHub {
|
|
|
675
695
|
private join(s: Session, m: Record<string, unknown>) {
|
|
676
696
|
const L = this.limits, now = this.now();
|
|
677
697
|
if (s.room) return this.bad(s, "already in a room");
|
|
698
|
+
if (this.moving.has(s.scope)) return s.end(CLOSE.restart, "moving to another server");
|
|
678
699
|
const spec = parseSpec(m.spec, L.maxPlayers);
|
|
679
700
|
if (!spec) return this.bad(s, "bad multiplayer option");
|
|
680
701
|
const hits = (this.joins.get(s.addr) || []).filter((x) => now - x < 60_000);
|
|
681
702
|
if (hits.length >= L.joinsPerMinute) { this.refuse("joins"); return s.end(CLOSE.tooFast, "too many joins, wait a minute"); }
|
|
682
703
|
hits.push(now); this.joins.set(s.addr, hits);
|
|
683
704
|
if (this.joins.size > 10_000) for (const [k, v] of this.joins) if (!v.length || now - v[v.length - 1] > 60_000) this.joins.delete(k);
|
|
705
|
+
if (!this.scopeJoinAllowed(s.scope, now)) { this.refuse("scope_joins"); return s.end(CLOSE.busy, "this game is busy right now, trying again"); }
|
|
684
706
|
const who = s.who!;
|
|
685
707
|
if (who.guest && typeof m.name === "string") { const n = cleanName(m.name); if (n) who.name = n; }
|
|
686
708
|
const specKey = stable(spec);
|
|
@@ -1313,7 +1335,7 @@ export class RoomHub {
|
|
|
1313
1335
|
if (!on) continue;
|
|
1314
1336
|
const h = r.host?.session;
|
|
1315
1337
|
rooms.push({ _r: r, code: r.code, host: r.host?.name ?? null, players: on, away, max: r.spec.max, started: r.started, match: r.match,
|
|
1316
|
-
ping: h?.rtt ?? 0, slowed: r.slowF > 1 || (this.pressure && r.seats.size > 4), full: taken >= r.spec.max, spec: r.specKey, created: r.created });
|
|
1338
|
+
ping: h?.rtt ?? 0, slowed: r.slowF > 1 || (this.pressure && r.seats.size > 4) || this.scopeF(r.scope) > 1, full: taken >= r.spec.max, spec: r.specKey, created: r.created });
|
|
1317
1339
|
}
|
|
1318
1340
|
rooms.sort((a, b) => Number(a.full) - Number(b.full) || Number(a.started) - Number(b.started) || (b.players as number) - (a.players as number) || (b.created as number) - (a.created as number));
|
|
1319
1341
|
if (this.lists.size > 1000) this.lists.clear();
|
|
@@ -1345,9 +1367,46 @@ export class RoomHub {
|
|
|
1345
1367
|
if (!r.slowTold && r.host?.session) { r.slowTold = true; this.err(r.host.session, "too_much_data"); }
|
|
1346
1368
|
} else if (r.slowF > 1 && r.outAvg * 2 < L.roomOutBytes * 0.8) { r.slowF /= 2; r.outAvg *= 2; }
|
|
1347
1369
|
}
|
|
1348
|
-
/** How many ticks apart a room sends its state: 1 (50 ms), 2 in rooms over 8 players, times its own budget factor, times
|
|
1349
|
-
* for rooms over 4 players. */
|
|
1350
|
-
private every(r: Room) { return (r.seats.size > 8 ? 2 : 1) * r.slowF * (this.pressure && r.seats.size > 4 ? 2 : 1); }
|
|
1370
|
+
/** How many ticks apart a room sends its state: 1 (50 ms), 2 in rooms over 8 players, times its own budget factor, times its namespace's factor
|
|
1371
|
+
* (over its outBytes cap), times 2 under server pressure for rooms over 4 players. */
|
|
1372
|
+
private every(r: Room) { return (r.seats.size > 8 ? 2 : 1) * r.slowF * this.scopeF(r.scope) * (this.pressure && r.seats.size > 4 ? 2 : 1); }
|
|
1373
|
+
private scopeF(scope: string) { return this.scopeSlow.get(scope)?.f ?? 1; }
|
|
1374
|
+
|
|
1375
|
+
/** Once a second: each namespace with an outBytes cap over its share slows all its rooms (twice as slow per second over, up to 8 times), and
|
|
1376
|
+
* speeds up one step when the faster rate would be under 80% of the cap. A busy customer then costs its own players smoothness, not everyone's. */
|
|
1377
|
+
private scopeBudgets() {
|
|
1378
|
+
if (!this.scopeCaps) return;
|
|
1379
|
+
const out = new Map<string, number>();
|
|
1380
|
+
for (const r of this.rooms.values()) out.set(r.scope, (out.get(r.scope) || 0) + r.outRate);
|
|
1381
|
+
for (const k of this.scopeSlow.keys()) if (!out.has(k)) this.scopeSlow.delete(k);
|
|
1382
|
+
for (const [scope, rate] of out) {
|
|
1383
|
+
let cap: number | undefined;
|
|
1384
|
+
try { cap = this.scopeCaps(scope)?.outBytes; } catch { cap = undefined; }
|
|
1385
|
+
if (!cap || cap <= 0) { this.scopeSlow.delete(scope); continue; }
|
|
1386
|
+
let x = this.scopeSlow.get(scope);
|
|
1387
|
+
if (!x) { if (rate <= cap) continue; x = { f: 1, avg: 0 }; this.scopeSlow.set(scope, x); }
|
|
1388
|
+
x.avg = x.avg ? x.avg * 0.7 + rate * 0.3 : rate;
|
|
1389
|
+
if (x.avg > cap && x.f < 8) { x.f *= 2; x.avg /= 2; this.refuse("scope_budget"); }
|
|
1390
|
+
else if (x.f > 1 && x.avg * 2 < cap * 0.8) { x.f /= 2; x.avg *= 2; }
|
|
1391
|
+
if (x.f === 1 && x.avg <= cap) this.scopeSlow.delete(scope);
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
/** One more join in a namespace with a joinsPerMinute cap, counted over a sliding minute (this minute plus the share of the last one still in it). */
|
|
1396
|
+
private scopeJoinAllowed(scope: string, now: number) {
|
|
1397
|
+
if (!this.scopeCaps) return true;
|
|
1398
|
+
let cap: number | undefined;
|
|
1399
|
+
try { cap = this.scopeCaps(scope)?.joinsPerMinute; } catch { cap = undefined; }
|
|
1400
|
+
if (!cap || cap <= 0) return true;
|
|
1401
|
+
let w = this.scopeJoins.get(scope);
|
|
1402
|
+
if (!w || now - w.at >= 120_000) { w = { at: now, n: 0, prev: 0 }; this.scopeJoins.set(scope, w); }
|
|
1403
|
+
else if (now - w.at >= 60_000) { w.prev = w.n; w.n = 0; w.at += 60_000; }
|
|
1404
|
+
if (this.scopeJoins.size > 10_000) for (const [k, v] of this.scopeJoins) if (now - v.at >= 120_000) this.scopeJoins.delete(k);
|
|
1405
|
+
const used = w.n + w.prev * Math.max(0, 1 - (now - w.at) / 60_000);
|
|
1406
|
+
if (used >= cap) return false;
|
|
1407
|
+
w.n++;
|
|
1408
|
+
return true;
|
|
1409
|
+
}
|
|
1351
1410
|
|
|
1352
1411
|
/** Once a second: the rates over the last second (server and per room). */
|
|
1353
1412
|
private meters(now: number) {
|
|
@@ -1356,6 +1415,7 @@ export class RoomHub {
|
|
|
1356
1415
|
for (const k of ["framesOut", "bytesOut", "msgsIn"] as const) { this.rates[k] = Math.round((this.meter[k] - this.mark[k]) / sec); this.mark[k] = this.meter[k]; }
|
|
1357
1416
|
this.mark.at = now;
|
|
1358
1417
|
for (const r of this.rooms.values()) { r.outRate = Math.round((r.outBytes - r.outMark) / sec); r.outMark = r.outBytes; this.budget(r); }
|
|
1418
|
+
this.scopeBudgets();
|
|
1359
1419
|
// the whole server: over 80% of its data cap, or the event loop running late (p99 over 30 ms for 10 s, from the host process), bigger rooms
|
|
1360
1420
|
// send at half rate and new rooms wait; back to normal under 60% and a loop on time
|
|
1361
1421
|
const mbit = (this.rates.bytesOut * 8) / 1e6, cap = this.limits.maxOutMbit;
|
|
@@ -1478,11 +1538,50 @@ export class RoomHub {
|
|
|
1478
1538
|
if (snap) await this.onSnapshot!(snap);
|
|
1479
1539
|
}
|
|
1480
1540
|
|
|
1481
|
-
/**
|
|
1482
|
-
|
|
1541
|
+
/** Blue/green hand-over of one namespace to another process: its pending changes go out, open actions fail ("server restarting"), saves are
|
|
1542
|
+
* written, its rooms are exported, its sockets hear 1012 (a router in front re-joins them on the other process over the same page socket; a
|
|
1543
|
+
* page on its own reconnects) and its rooms close here, giving up their save leases. Joins to it are refused with 1012 meanwhile. Resolves
|
|
1544
|
+
* once every save is in the store, so the other process may take the snapshot (importRooms) and write the same saves. */
|
|
1545
|
+
async exportScope(scope: string): Promise<RoomsSnapshot> {
|
|
1546
|
+
this.moving.add(scope);
|
|
1547
|
+
try {
|
|
1548
|
+
// a profile save waits for its profile to load: let those land (2 s at most) so the write below carries them
|
|
1549
|
+
const loads: Promise<unknown>[] = [];
|
|
1550
|
+
for (const r of this.rooms.values()) if (r.scope === scope) for (const x of r.seats.values()) if (x.profLoad) loads.push(x.profLoad.catch(() => null));
|
|
1551
|
+
if (loads.length) await Promise.race([Promise.all(loads), new Promise((r) => setTimeout(r, 2000).unref?.())]);
|
|
1552
|
+
const mine = [...this.rooms.values()].filter((r) => r.scope === scope);
|
|
1553
|
+
for (const room of mine) {
|
|
1554
|
+
this.flush(room);
|
|
1555
|
+
for (const [aid, a] of room.actions) { a.from.session?.send({ t: "result", id: a.ref, ok: false, error: "server restarting" }); room.actions.delete(aid); }
|
|
1556
|
+
this.saveWorld(room, true);
|
|
1557
|
+
for (const x of room.seats.values()) this.flushProfile(room, x, true);
|
|
1558
|
+
}
|
|
1559
|
+
const snap = this.exportRooms(scope);
|
|
1560
|
+
for (const s of [...this.sessions]) if (s.scope === scope) s.end(CLOSE.restart, "moving to another server");
|
|
1561
|
+
for (const room of mine) this.closeRoom(room);
|
|
1562
|
+
await this.settle();
|
|
1563
|
+
return snap;
|
|
1564
|
+
} finally { this.moving.delete(scope); }
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
/** Namespaces with live rooms here and their size (a router's hand-over list, a dashboard). */
|
|
1568
|
+
scopeList(): { scope: string; rooms: number; players: number; online: number }[] {
|
|
1569
|
+
const by = new Map<string, { scope: string; rooms: number; players: number; online: number }>();
|
|
1570
|
+
for (const r of this.rooms.values()) {
|
|
1571
|
+
let x = by.get(r.scope);
|
|
1572
|
+
if (!x) { x = { scope: r.scope, rooms: 0, players: 0, online: 0 }; by.set(r.scope, x); }
|
|
1573
|
+
x.rooms++;
|
|
1574
|
+
for (const z of r.seats.values()) { x.players++; if (z.session) x.online++; }
|
|
1575
|
+
}
|
|
1576
|
+
return [...by.values()];
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
/** The live rooms (of one namespace when given) as plain JSON: everything a new process needs to carry on (sockets, timers and open actions
|
|
1580
|
+
* are not kept). */
|
|
1581
|
+
exportRooms(scope?: string): RoomsSnapshot {
|
|
1483
1582
|
const rooms: SnapRoom[] = [];
|
|
1484
1583
|
for (const r of this.rooms.values()) {
|
|
1485
|
-
if (r.seats.size === 0) continue;
|
|
1584
|
+
if (r.seats.size === 0 || (scope !== undefined && r.scope !== scope)) continue;
|
|
1486
1585
|
rooms.push({
|
|
1487
1586
|
key: r.key, scope: r.scope, game: r.game, spec: r.spec, code: r.code, priv: r.priv, slot: r.slot, owner: r.owner, world: r.world, lease: r.lease, saveBusy: r.saveBusy,
|
|
1488
1587
|
worldState: r.worldState === "ready" ? "ready" : "loading", saved: r.saved, wrev: r.wrev, savedWrev: r.savedWrev, noSave: r.noSave, resumed: r.resumed, sid: r.sid,
|
|
@@ -1548,14 +1647,15 @@ export class RoomHub {
|
|
|
1548
1647
|
}
|
|
1549
1648
|
|
|
1550
1649
|
/** One namespace's use right now: rooms, seats, connected players and bytes out a second (over the last second). For per-customer caps. */
|
|
1551
|
-
|
|
1650
|
+
/** `slowed`: how many times less often its rooms send because the namespace is over its own outBytes cap (1 = full rate). */
|
|
1651
|
+
scopeStats(scope: string): { rooms: number; players: number; online: number; bytesOut: number; slowed: number } {
|
|
1552
1652
|
let rooms = 0, players = 0, online = 0, bytesOut = 0;
|
|
1553
1653
|
for (const r of this.rooms.values()) {
|
|
1554
1654
|
if (r.scope !== scope) continue;
|
|
1555
1655
|
rooms++; bytesOut += r.outRate;
|
|
1556
1656
|
for (const x of r.seats.values()) { players++; if (x.session) online++; }
|
|
1557
1657
|
}
|
|
1558
|
-
return { rooms, players, online, bytesOut };
|
|
1658
|
+
return { rooms, players, online, bytesOut, slowed: this.scopeF(scope) };
|
|
1559
1659
|
}
|
|
1560
1660
|
|
|
1561
1661
|
stats(): HubStats {
|
|
@@ -1570,6 +1670,7 @@ export class RoomHub {
|
|
|
1570
1670
|
...this.rates, tickMs: { p50: pct(0.5), p99: pct(0.99), max: n ? Math.round(sorted[n - 1] * 100) / 100 : 0 }, slow, started,
|
|
1571
1671
|
savesOk: this.counts.saves_ok || 0, savesFailed: this.refused.save_failed || 0, loadsFailed: this.refused.load_failed || 0, worldWritesPerMin: this.worldWrites.length,
|
|
1572
1672
|
loop: this.loop, top, pressure: this.pressure, slowedRooms: [...this.rooms.values()].filter((r) => r.slowF > 1).length,
|
|
1673
|
+
slowedScopes: [...this.scopeSlow.values()].filter((x) => x.f > 1).length,
|
|
1573
1674
|
};
|
|
1574
1675
|
return { rooms: this.rooms.size, players, online, connections: this.sessions.size, pending: this.pending, refused: { ...this.refused }, live };
|
|
1575
1676
|
}
|
package/src/remote.ts
CHANGED
|
@@ -19,7 +19,7 @@ import crypto from "node:crypto";
|
|
|
19
19
|
import type { Duplex } from "node:stream";
|
|
20
20
|
import { fileURLToPath } from "node:url";
|
|
21
21
|
import { WebSocketServer, type WebSocket } from "ws";
|
|
22
|
-
import { CLOSE, RoomHub, addressKey, type Limits, type SavePort } from "./hub.ts";
|
|
22
|
+
import { CLOSE, RoomHub, addressKey, type Limits, type SavePort, type ScopeCaps } from "./hub.ts";
|
|
23
23
|
import { GuestPasses, Heartbeat, WS_OPTIONS, attach, clientAddress, fileSaves, gameHash, hardenServer } from "./node.ts";
|
|
24
24
|
|
|
25
25
|
export interface Deployment {
|
|
@@ -36,8 +36,12 @@ export interface Deployment {
|
|
|
36
36
|
rooms?: number;
|
|
37
37
|
/** connected sockets at once */
|
|
38
38
|
players?: number;
|
|
39
|
-
/** data going out to its players, KB a second
|
|
39
|
+
/** data going out to its players, KB a second: over it, new sockets wait (busy) and all its open rooms send less often (half, down to an
|
|
40
|
+
* eighth) until it is back under 80% of it, so one busy deployment cannot take the server's whole uplink */
|
|
40
41
|
kbPerSec?: number;
|
|
42
|
+
/** joins a minute across the deployment, reconnects included (on top of the hub's per-address joinsPerMinute); over it: 1013 busy, the page
|
|
43
|
+
* tries again. Leave room for a restart: every connected player joins again within a few seconds. */
|
|
44
|
+
joinsPerMinute?: number;
|
|
41
45
|
};
|
|
42
46
|
}
|
|
43
47
|
|
|
@@ -65,6 +69,8 @@ export interface Remote {
|
|
|
65
69
|
scope(id: string): string;
|
|
66
70
|
/** The rooms cap the hub should use for a namespace (pass to new RoomHub({ roomsPerScope })). */
|
|
67
71
|
roomsPerScope(scope: string): number | undefined;
|
|
72
|
+
/** The data and joins caps the hub should use for a namespace (pass to new RoomHub({ scopeCaps })). */
|
|
73
|
+
scopeCaps(scope: string): ScopeCaps | undefined;
|
|
68
74
|
/** connected sockets per deployment id */
|
|
69
75
|
sockets(id: string): number;
|
|
70
76
|
}
|
|
@@ -175,6 +181,10 @@ export function createRemote(o: RemoteOptions): Remote {
|
|
|
175
181
|
scope,
|
|
176
182
|
sockets: (id) => live.get(id) || 0,
|
|
177
183
|
roomsPerScope: (sc) => (sc.startsWith("dep:") ? cache.get(sc.slice(4))?.caps?.rooms : undefined),
|
|
184
|
+
scopeCaps: (sc) => {
|
|
185
|
+
const c = sc.startsWith("dep:") ? cache.get(sc.slice(4))?.caps : undefined;
|
|
186
|
+
return c && (c.kbPerSec !== undefined || c.joinsPerMinute !== undefined) ? { outBytes: c.kbPerSec !== undefined ? c.kbPerSec * 1024 : undefined, joinsPerMinute: c.joinsPerMinute } : undefined;
|
|
187
|
+
},
|
|
178
188
|
revoke(id) { cache.delete(id); seen.delete(id); o.hub.closeScope(scope(id), "this game was turned off by its owner"); },
|
|
179
189
|
forget(id) { cache.delete(id); seen.delete(id); },
|
|
180
190
|
|
|
@@ -241,13 +251,15 @@ export interface RemoteServerOptions {
|
|
|
241
251
|
address?: RemoteOptions["address"];
|
|
242
252
|
/** behind a proxy on a private network: players' addresses from X-Forwarded-For (see clientAddress in tickcli-js/node) */
|
|
243
253
|
trustProxy?: boolean;
|
|
254
|
+
/** close() hands the live rooms to this (write them somewhere; the next process passes them to hub.importRooms): a restart keeps matches */
|
|
255
|
+
onSnapshot?: ConstructorParameters<typeof RoomHub>[0]["onSnapshot"];
|
|
244
256
|
}
|
|
245
257
|
|
|
246
258
|
/** A complete remote room server: node:http + the hub + createRemote. Add your own routes on `server` before listen(). */
|
|
247
259
|
export function createRemoteServer(o: RemoteServerOptions) {
|
|
248
260
|
let remote: Remote | null = null;
|
|
249
261
|
const saves = typeof o.saves === "string" ? fileSaves(o.saves) : o.saves;
|
|
250
|
-
const hub = new RoomHub({ secret: o.secret ?? crypto.randomBytes(32).toString("base64url"), limits: o.limits, saves, roomsPerScope: (sc) => remote?.roomsPerScope(sc) });
|
|
262
|
+
const hub = new RoomHub({ secret: o.secret ?? crypto.randomBytes(32).toString("base64url"), limits: o.limits, saves, onSnapshot: o.onSnapshot, roomsPerScope: (sc) => remote?.roomsPerScope(sc), scopeCaps: (sc) => remote?.scopeCaps(sc) });
|
|
251
263
|
hub.start();
|
|
252
264
|
const heartbeat = new Heartbeat();
|
|
253
265
|
remote = createRemote({ hub, deployment: o.deployment, heartbeat, address: o.address ?? (o.trustProxy ? (req) => clientAddress(req, true) : undefined) });
|