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 CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.6
4
+
5
+ Blue/green hand-over of one namespace. `hub.exportScope(scope)` moves one namespace's live rooms to another process without touching the others: pending changes go out, open actions fail ("server restarting"), profile saves still loading land first, saves are written and in the store before it resolves, the rooms are exported (`exportRooms(scope)` now takes an optional namespace) and closed here (save leases given up), and its sockets hear 1012. Joins to it meanwhile are refused with 1012. The other process passes the snapshot to `importRooms` and the players carry on in the same match (`kept`). A router in front can re-join each page over its open socket; a page on its own reconnects as after a restart. `hub.scopeList()`: namespaces with live rooms and their rooms, seats and online players. `createRemoteServer({ onSnapshot })`: a restart of a remote room server keeps its matches.
6
+
7
+ ## 0.1.5
8
+
9
+ Per-customer shares on a shared server. A deployment over its `kbPerSec` cap no longer only waits at the door: all its open rooms send less often (half, down to an eighth) until it is back under 80% of the cap, so one busy customer cannot take the whole uplink; other deployments keep their full rate and nobody is disconnected. New deployment cap `joinsPerMinute`: joins a minute across the deployment (reconnects count), on top of the per-address limit; over it the page hears 1013 busy and tries again. Hub: `scopeCaps` option (`{ outBytes, joinsPerMinute }` per namespace), `remote.scopeCaps(scope)`, `scopeStats(scope).slowed`, `stats().live.slowedScopes`, refusal reasons `scope_budget` and `scope_joins`.
10
+
3
11
  ## 0.1.4
4
12
 
5
13
  Idle pages send less. A visible page with no key, pointer, touch or gamepad input for 1 min asks the hub for a quarter of the update frames (nothing is lost, changes are merged into each frame); the first input brings the full rate back. In a 4-player test room the idle reader's download fell from 4.4 to 1.2 KB/s. Hidden pages already got no frames.
package/README.md CHANGED
@@ -248,17 +248,17 @@ import { createRemoteServer } from "tickcli-js/remote";
248
248
 
249
249
  const deployments = {
250
250
  // "http://localhost:*" lets the author test the same page from a laptop; drop it whenever you like
251
- dep_ab12: { id: "dep_ab12", origins: ["https://mygame.example", "https://*.portal.example", "http://localhost:*"], caps: { rooms: 20, players: 200, kbPerSec: 2048 } },
251
+ dep_ab12: { id: "dep_ab12", origins: ["https://mygame.example", "https://*.portal.example", "http://localhost:*"], caps: { rooms: 20, players: 200, kbPerSec: 2048, joinsPerMinute: 600 } },
252
252
  };
253
253
  const app = createRemoteServer({ deployment: (id) => deployments[id], secret: process.env.TICKCLI_SECRET, saves: "./saves" });
254
254
  await app.listen(8080); // behind https: the pages use wss://
255
255
  ```
256
256
 
257
- Or in your own server: `createRemote({ hub, deployment })` gives `upgrade(req, socket, head)` and `request(req, res)` (pass `roomsPerScope: (s) => remote.roomsPerScope(s)` to the hub for room caps).
257
+ Or in your own server: `createRemote({ hub, deployment })` gives `upgrade(req, socket, head)` and `request(req, res)` (pass `roomsPerScope: (s) => remote.roomsPerScope(s)` and `scopeCaps: (s) => remote.scopeCaps(s)` to the hub for the room, data and joins caps).
258
258
 
259
259
  - A deployment is a public id plus the sites allowed to use it: `https://site`, one wildcard level `https://*.example`, any port `:*`. The server checks the page's `Origin` when the socket opens: browsers send it and page scripts cannot change it, so another site cannot use your deployment. There is one kind of deployment: https sites, and http only for this machine (`localhost`, `127.0.0.1`, `[::1]`) when listed, e.g. `http://localhost:*` to test from a laptop. Pages with no site (`null`, `file://`) are refused.
260
260
  - Guests: the page keeps a random 128-bit key in its own `localStorage` and sends it as the socket's first message (no cookie, so it works from any site). The same key is the same seat; other players see a short id made from it, never the key.
261
- - Each deployment is its own namespace (rooms and saves never mix). `caps`: open rooms, connected sockets, KB a second going out (over it, new sockets are told the game is busy and retry; open rooms carry on). `revoked: true` refuses new sockets; `remote.revoke(id)` also closes its open rooms.
261
+ - Each deployment is its own namespace (rooms and saves never mix). `caps`: open rooms, connected sockets, KB a second going out (over it, new sockets are told the game is busy and retry, and its open rooms send less often, down to an eighth, until it is back under 80%; other deployments keep their full rate), joins a minute across the deployment (reconnects count: leave room for every player joining again after a restart). `revoked: true` refuses new sockets; `remote.revoke(id)` also closes its open rooms.
262
262
  - A refused page stops and shows why ("This site is not allowed for deployment dep_ab12.").
263
263
  - Not a guard: a script outside a browser can send any `Origin`. The caps, the per-address limits and revoking the id are what stop a misused deployment.
264
264
 
package/dist/hub.d.ts CHANGED
@@ -367,6 +367,8 @@ export interface LiveStats {
367
367
  /** over the data cap or a late loop now; rooms slowed by their own budget */
368
368
  pressure: boolean;
369
369
  slowedRooms: number;
370
+ /** namespaces slowed because they are over their own outBytes cap (ScopeCaps) */
371
+ slowedScopes: number;
370
372
  /** the 5 rooms sending the most */
371
373
  top: {
372
374
  players: number;
@@ -421,6 +423,14 @@ export interface RoomsSnapshot {
421
423
  at: number;
422
424
  rooms: SnapRoom[];
423
425
  }
426
+ /** One namespace's own share of the server (a host app's per-customer caps, tickcli-js/remote). */
427
+ export interface ScopeCaps {
428
+ /** 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
429
+ * less) until the faster rate would be under 80% of it again. Other namespaces keep their full rate. */
430
+ outBytes?: number;
431
+ /** joins a minute across the namespace (reconnects count), on top of the per-address limit; over it: 1013 busy, the page tries again */
432
+ joinsPerMinute?: number;
433
+ }
424
434
  export interface HubStats {
425
435
  rooms: number;
426
436
  players: number;
@@ -441,6 +451,10 @@ export declare class RoomHub {
441
451
  private perAddr;
442
452
  private addrHits;
443
453
  private joins;
454
+ /** per namespace with caps: its slow-down factor and moving average out (bytes a second) */
455
+ private scopeSlow;
456
+ /** per namespace with a joins cap: joins in this minute and the last one (a sliding estimate, no list per join) */
457
+ private scopeJoins;
444
458
  private refused;
445
459
  private timer;
446
460
  private sub;
@@ -478,6 +492,8 @@ export declare class RoomHub {
478
492
  pressure: boolean;
479
493
  /** world save leases: "scope|slot|who" -> the room key that may write it */
480
494
  private leases;
495
+ /** namespaces being handed to another process now (exportScope): joins to them are refused with 1012 */
496
+ private moving;
481
497
  /** shutdown() hands the live rooms to this (a restart writes them to disk; importRooms() in the next process takes them back) */
482
498
  private readonly onSnapshot;
483
499
  /** the room list (rooms {}) is answered; off: the answer is {off: true} */
@@ -486,6 +502,8 @@ export declare class RoomHub {
486
502
  private lists;
487
503
  /** rooms allowed in one namespace when it differs from limits.maxRoomsPerScope (a host app's per-customer cap, tickcli-js/remote) */
488
504
  private readonly roomsPerScope;
505
+ /** a namespace's own data and joins caps (tickcli-js/remote: a deployment's caps); undefined = only the server-wide limits */
506
+ private readonly scopeCaps;
489
507
  constructor(opts: {
490
508
  secret: string;
491
509
  now?: () => number;
@@ -494,6 +512,7 @@ export declare class RoomHub {
494
512
  onSnapshot?: (snap: RoomsSnapshot) => Promise<void> | void;
495
513
  listRooms?: boolean;
496
514
  roomsPerScope?: (scope: string) => number | undefined;
515
+ scopeCaps?: (scope: string) => ScopeCaps | undefined;
497
516
  });
498
517
  /** 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. */
499
518
  publicId(scope: string, seat: string): string;
@@ -596,9 +615,15 @@ export declare class RoomHub {
596
615
  /** 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
597
616
  * the faster rate would be under 80% of the budget. */
598
617
  private budget;
599
- /** 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
600
- * for rooms over 4 players. */
618
+ /** 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
619
+ * (over its outBytes cap), times 2 under server pressure for rooms over 4 players. */
601
620
  private every;
621
+ private scopeF;
622
+ /** 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
623
+ * 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. */
624
+ private scopeBudgets;
625
+ /** 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). */
626
+ private scopeJoinAllowed;
602
627
  /** Once a second: the rates over the last second (server and per room). */
603
628
  private meters;
604
629
  /** Everything but the room sends; `sendAll` false when subTick sends rooms on their own phase. */
@@ -614,8 +639,21 @@ export declare class RoomHub {
614
639
  /** Server shutdown: pending changes and saves are written, the live rooms go to onSnapshot (the next process takes them back), then every player
615
640
  * hears 1012 and reconnects. Without onSnapshot a restart ends every match. */
616
641
  shutdown(): Promise<void>;
617
- /** The live rooms as plain JSON: everything a new process needs to carry on (sockets, timers and open actions are not kept). */
618
- exportRooms(): RoomsSnapshot;
642
+ /** Blue/green hand-over of one namespace to another process: its pending changes go out, open actions fail ("server restarting"), saves are
643
+ * 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
644
+ * page on its own reconnects) and its rooms close here, giving up their save leases. Joins to it are refused with 1012 meanwhile. Resolves
645
+ * once every save is in the store, so the other process may take the snapshot (importRooms) and write the same saves. */
646
+ exportScope(scope: string): Promise<RoomsSnapshot>;
647
+ /** Namespaces with live rooms here and their size (a router's hand-over list, a dashboard). */
648
+ scopeList(): {
649
+ scope: string;
650
+ rooms: number;
651
+ players: number;
652
+ online: number;
653
+ }[];
654
+ /** The live rooms (of one namespace when given) as plain JSON: everything a new process needs to carry on (sockets, timers and open actions
655
+ * are not kept). */
656
+ exportRooms(scope?: string): RoomsSnapshot;
619
657
  /** 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
620
658
  * term, save leases taken again. A snapshot older than maxAgeMs or of another format is ignored. Returns how many rooms came back. */
621
659
  importRooms(snap: unknown, o?: {
@@ -629,11 +667,13 @@ export declare class RoomHub {
629
667
  /** Close every room of one namespace (a public link stopped). */
630
668
  closeScope(scope: string, reason?: string): void;
631
669
  /** One namespace's use right now: rooms, seats, connected players and bytes out a second (over the last second). For per-customer caps. */
670
+ /** `slowed`: how many times less often its rooms send because the namespace is over its own outBytes cap (1 = full rate). */
632
671
  scopeStats(scope: string): {
633
672
  rooms: number;
634
673
  players: number;
635
674
  online: number;
636
675
  bytesOut: number;
676
+ slowed: number;
637
677
  };
638
678
  stats(): HubStats;
639
679
  }
package/dist/hub.js CHANGED
@@ -350,6 +350,10 @@ export class RoomHub {
350
350
  perAddr = new Map();
351
351
  addrHits = new Map();
352
352
  joins = new Map();
353
+ /** per namespace with caps: its slow-down factor and moving average out (bytes a second) */
354
+ scopeSlow = new Map();
355
+ /** per namespace with a joins cap: joins in this minute and the last one (a sliding estimate, no list per join) */
356
+ scopeJoins = new Map();
353
357
  refused = {};
354
358
  timer = null;
355
359
  sub = 0;
@@ -381,6 +385,8 @@ export class RoomHub {
381
385
  pressure = false;
382
386
  /** world save leases: "scope|slot|who" -> the room key that may write it */
383
387
  leases = new Map();
388
+ /** namespaces being handed to another process now (exportScope): joins to them are refused with 1012 */
389
+ moving = new Set();
384
390
  /** shutdown() hands the live rooms to this (a restart writes them to disk; importRooms() in the next process takes them back) */
385
391
  onSnapshot;
386
392
  /** the room list (rooms {}) is answered; off: the answer is {off: true} */
@@ -389,8 +395,11 @@ export class RoomHub {
389
395
  lists = new Map();
390
396
  /** rooms allowed in one namespace when it differs from limits.maxRoomsPerScope (a host app's per-customer cap, tickcli-js/remote) */
391
397
  roomsPerScope;
398
+ /** a namespace's own data and joins caps (tickcli-js/remote: a deployment's caps); undefined = only the server-wide limits */
399
+ scopeCaps;
392
400
  constructor(opts) {
393
401
  this.roomsPerScope = opts.roomsPerScope ?? null;
402
+ this.scopeCaps = opts.scopeCaps ?? null;
394
403
  this.saves = opts.saves ?? null;
395
404
  this.listRooms = opts.listRooms !== false;
396
405
  this.onSnapshot = opts.onSnapshot ?? null;
@@ -575,6 +584,8 @@ export class RoomHub {
575
584
  const L = this.limits, now = this.now();
576
585
  if (s.room)
577
586
  return this.bad(s, "already in a room");
587
+ if (this.moving.has(s.scope))
588
+ return s.end(CLOSE.restart, "moving to another server");
578
589
  const spec = parseSpec(m.spec, L.maxPlayers);
579
590
  if (!spec)
580
591
  return this.bad(s, "bad multiplayer option");
@@ -589,6 +600,10 @@ export class RoomHub {
589
600
  for (const [k, v] of this.joins)
590
601
  if (!v.length || now - v[v.length - 1] > 60_000)
591
602
  this.joins.delete(k);
603
+ if (!this.scopeJoinAllowed(s.scope, now)) {
604
+ this.refuse("scope_joins");
605
+ return s.end(CLOSE.busy, "this game is busy right now, trying again");
606
+ }
592
607
  const who = s.who;
593
608
  if (who.guest && typeof m.name === "string") {
594
609
  const n = cleanName(m.name);
@@ -1439,7 +1454,7 @@ export class RoomHub {
1439
1454
  continue;
1440
1455
  const h = r.host?.session;
1441
1456
  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,
1442
- 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 });
1457
+ 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 });
1443
1458
  }
1444
1459
  rooms.sort((a, b) => Number(a.full) - Number(b.full) || Number(a.started) - Number(b.started) || b.players - a.players || b.created - a.created);
1445
1460
  if (this.lists.size > 1000)
@@ -1483,9 +1498,87 @@ export class RoomHub {
1483
1498
  r.outAvg *= 2;
1484
1499
  }
1485
1500
  }
1486
- /** 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
1487
- * for rooms over 4 players. */
1488
- every(r) { return (r.seats.size > 8 ? 2 : 1) * r.slowF * (this.pressure && r.seats.size > 4 ? 2 : 1); }
1501
+ /** 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
1502
+ * (over its outBytes cap), times 2 under server pressure for rooms over 4 players. */
1503
+ every(r) { return (r.seats.size > 8 ? 2 : 1) * r.slowF * this.scopeF(r.scope) * (this.pressure && r.seats.size > 4 ? 2 : 1); }
1504
+ scopeF(scope) { return this.scopeSlow.get(scope)?.f ?? 1; }
1505
+ /** 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
1506
+ * 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. */
1507
+ scopeBudgets() {
1508
+ if (!this.scopeCaps)
1509
+ return;
1510
+ const out = new Map();
1511
+ for (const r of this.rooms.values())
1512
+ out.set(r.scope, (out.get(r.scope) || 0) + r.outRate);
1513
+ for (const k of this.scopeSlow.keys())
1514
+ if (!out.has(k))
1515
+ this.scopeSlow.delete(k);
1516
+ for (const [scope, rate] of out) {
1517
+ let cap;
1518
+ try {
1519
+ cap = this.scopeCaps(scope)?.outBytes;
1520
+ }
1521
+ catch {
1522
+ cap = undefined;
1523
+ }
1524
+ if (!cap || cap <= 0) {
1525
+ this.scopeSlow.delete(scope);
1526
+ continue;
1527
+ }
1528
+ let x = this.scopeSlow.get(scope);
1529
+ if (!x) {
1530
+ if (rate <= cap)
1531
+ continue;
1532
+ x = { f: 1, avg: 0 };
1533
+ this.scopeSlow.set(scope, x);
1534
+ }
1535
+ x.avg = x.avg ? x.avg * 0.7 + rate * 0.3 : rate;
1536
+ if (x.avg > cap && x.f < 8) {
1537
+ x.f *= 2;
1538
+ x.avg /= 2;
1539
+ this.refuse("scope_budget");
1540
+ }
1541
+ else if (x.f > 1 && x.avg * 2 < cap * 0.8) {
1542
+ x.f /= 2;
1543
+ x.avg *= 2;
1544
+ }
1545
+ if (x.f === 1 && x.avg <= cap)
1546
+ this.scopeSlow.delete(scope);
1547
+ }
1548
+ }
1549
+ /** 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). */
1550
+ scopeJoinAllowed(scope, now) {
1551
+ if (!this.scopeCaps)
1552
+ return true;
1553
+ let cap;
1554
+ try {
1555
+ cap = this.scopeCaps(scope)?.joinsPerMinute;
1556
+ }
1557
+ catch {
1558
+ cap = undefined;
1559
+ }
1560
+ if (!cap || cap <= 0)
1561
+ return true;
1562
+ let w = this.scopeJoins.get(scope);
1563
+ if (!w || now - w.at >= 120_000) {
1564
+ w = { at: now, n: 0, prev: 0 };
1565
+ this.scopeJoins.set(scope, w);
1566
+ }
1567
+ else if (now - w.at >= 60_000) {
1568
+ w.prev = w.n;
1569
+ w.n = 0;
1570
+ w.at += 60_000;
1571
+ }
1572
+ if (this.scopeJoins.size > 10_000)
1573
+ for (const [k, v] of this.scopeJoins)
1574
+ if (now - v.at >= 120_000)
1575
+ this.scopeJoins.delete(k);
1576
+ const used = w.n + w.prev * Math.max(0, 1 - (now - w.at) / 60_000);
1577
+ if (used >= cap)
1578
+ return false;
1579
+ w.n++;
1580
+ return true;
1581
+ }
1489
1582
  /** Once a second: the rates over the last second (server and per room). */
1490
1583
  meters(now) {
1491
1584
  if (now - this.mark.at < 1000)
@@ -1501,6 +1594,7 @@ export class RoomHub {
1501
1594
  r.outMark = r.outBytes;
1502
1595
  this.budget(r);
1503
1596
  }
1597
+ this.scopeBudgets();
1504
1598
  // 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
1505
1599
  // send at half rate and new rooms wait; back to normal under 60% and a loop on time
1506
1600
  const mbit = (this.rates.bytesOut * 8) / 1e6, cap = this.limits.maxOutMbit;
@@ -1703,11 +1797,70 @@ export class RoomHub {
1703
1797
  if (snap)
1704
1798
  await this.onSnapshot(snap);
1705
1799
  }
1706
- /** The live rooms as plain JSON: everything a new process needs to carry on (sockets, timers and open actions are not kept). */
1707
- exportRooms() {
1800
+ /** Blue/green hand-over of one namespace to another process: its pending changes go out, open actions fail ("server restarting"), saves are
1801
+ * 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
1802
+ * page on its own reconnects) and its rooms close here, giving up their save leases. Joins to it are refused with 1012 meanwhile. Resolves
1803
+ * once every save is in the store, so the other process may take the snapshot (importRooms) and write the same saves. */
1804
+ async exportScope(scope) {
1805
+ this.moving.add(scope);
1806
+ try {
1807
+ // a profile save waits for its profile to load: let those land (2 s at most) so the write below carries them
1808
+ const loads = [];
1809
+ for (const r of this.rooms.values())
1810
+ if (r.scope === scope)
1811
+ for (const x of r.seats.values())
1812
+ if (x.profLoad)
1813
+ loads.push(x.profLoad.catch(() => null));
1814
+ if (loads.length)
1815
+ await Promise.race([Promise.all(loads), new Promise((r) => setTimeout(r, 2000).unref?.())]);
1816
+ const mine = [...this.rooms.values()].filter((r) => r.scope === scope);
1817
+ for (const room of mine) {
1818
+ this.flush(room);
1819
+ for (const [aid, a] of room.actions) {
1820
+ a.from.session?.send({ t: "result", id: a.ref, ok: false, error: "server restarting" });
1821
+ room.actions.delete(aid);
1822
+ }
1823
+ this.saveWorld(room, true);
1824
+ for (const x of room.seats.values())
1825
+ this.flushProfile(room, x, true);
1826
+ }
1827
+ const snap = this.exportRooms(scope);
1828
+ for (const s of [...this.sessions])
1829
+ if (s.scope === scope)
1830
+ s.end(CLOSE.restart, "moving to another server");
1831
+ for (const room of mine)
1832
+ this.closeRoom(room);
1833
+ await this.settle();
1834
+ return snap;
1835
+ }
1836
+ finally {
1837
+ this.moving.delete(scope);
1838
+ }
1839
+ }
1840
+ /** Namespaces with live rooms here and their size (a router's hand-over list, a dashboard). */
1841
+ scopeList() {
1842
+ const by = new Map();
1843
+ for (const r of this.rooms.values()) {
1844
+ let x = by.get(r.scope);
1845
+ if (!x) {
1846
+ x = { scope: r.scope, rooms: 0, players: 0, online: 0 };
1847
+ by.set(r.scope, x);
1848
+ }
1849
+ x.rooms++;
1850
+ for (const z of r.seats.values()) {
1851
+ x.players++;
1852
+ if (z.session)
1853
+ x.online++;
1854
+ }
1855
+ }
1856
+ return [...by.values()];
1857
+ }
1858
+ /** The live rooms (of one namespace when given) as plain JSON: everything a new process needs to carry on (sockets, timers and open actions
1859
+ * are not kept). */
1860
+ exportRooms(scope) {
1708
1861
  const rooms = [];
1709
1862
  for (const r of this.rooms.values()) {
1710
- if (r.seats.size === 0)
1863
+ if (r.seats.size === 0 || (scope !== undefined && r.scope !== scope))
1711
1864
  continue;
1712
1865
  rooms.push({
1713
1866
  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,
@@ -1793,6 +1946,7 @@ export class RoomHub {
1793
1946
  }
1794
1947
  }
1795
1948
  /** One namespace's use right now: rooms, seats, connected players and bytes out a second (over the last second). For per-customer caps. */
1949
+ /** `slowed`: how many times less often its rooms send because the namespace is over its own outBytes cap (1 = full rate). */
1796
1950
  scopeStats(scope) {
1797
1951
  let rooms = 0, players = 0, online = 0, bytesOut = 0;
1798
1952
  for (const r of this.rooms.values()) {
@@ -1806,7 +1960,7 @@ export class RoomHub {
1806
1960
  online++;
1807
1961
  }
1808
1962
  }
1809
- return { rooms, players, online, bytesOut };
1963
+ return { rooms, players, online, bytesOut, slowed: this.scopeF(scope) };
1810
1964
  }
1811
1965
  stats() {
1812
1966
  let players = 0, online = 0, started = 0, slow = 0;
@@ -1830,6 +1984,7 @@ export class RoomHub {
1830
1984
  ...this.rates, tickMs: { p50: pct(0.5), p99: pct(0.99), max: n ? Math.round(sorted[n - 1] * 100) / 100 : 0 }, slow, started,
1831
1985
  savesOk: this.counts.saves_ok || 0, savesFailed: this.refused.save_failed || 0, loadsFailed: this.refused.load_failed || 0, worldWritesPerMin: this.worldWrites.length,
1832
1986
  loop: this.loop, top, pressure: this.pressure, slowedRooms: [...this.rooms.values()].filter((r) => r.slowF > 1).length,
1987
+ slowedScopes: [...this.scopeSlow.values()].filter((x) => x.f > 1).length,
1833
1988
  };
1834
1989
  return { rooms: this.rooms.size, players, online, connections: this.sessions.size, pending: this.pending, refused: { ...this.refused }, live };
1835
1990
  }