@babylonjs-toolkit/server 0.0.0-stage → 9.35.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.
Files changed (236) hide show
  1. package/CUSTOM.md +1690 -0
  2. package/DEPLOY.md +2012 -0
  3. package/README.md +1081 -2
  4. package/lib/AbuseLimits.d.ts +64 -0
  5. package/lib/AbuseLimits.js +121 -0
  6. package/lib/Authentication.d.ts +86 -0
  7. package/lib/Authentication.js +272 -0
  8. package/lib/BootSessionOwner.d.ts +86 -0
  9. package/lib/BootSessionOwner.js +116 -0
  10. package/lib/ColyseusLoggerAdapter.d.ts +32 -0
  11. package/lib/ColyseusLoggerAdapter.js +73 -0
  12. package/lib/DedicatedSceneRegistry.d.ts +52 -0
  13. package/lib/DedicatedSceneRegistry.js +74 -0
  14. package/lib/DedicatedSession.d.ts +152 -0
  15. package/lib/DedicatedSession.js +173 -0
  16. package/lib/DualTransport.d.ts +112 -0
  17. package/lib/DualTransport.js +197 -0
  18. package/lib/GameServer.d.ts +64 -0
  19. package/lib/GameServer.js +647 -0
  20. package/lib/GameServerConfig.d.ts +387 -0
  21. package/lib/GameServerConfig.js +1059 -0
  22. package/lib/GameServerLobbyRoom.d.ts +33 -0
  23. package/lib/GameServerLobbyRoom.js +56 -0
  24. package/lib/GameServerLogger.d.ts +111 -0
  25. package/lib/GameServerLogger.js +190 -0
  26. package/lib/GameServerMetrics.d.ts +158 -0
  27. package/lib/GameServerMetrics.js +276 -0
  28. package/lib/GameServerState.d.ts +164 -0
  29. package/lib/GameServerState.js +26 -0
  30. package/lib/HeadlessRuntime.d.ts +316 -0
  31. package/lib/HeadlessRuntime.js +380 -0
  32. package/lib/JoinGate.d.ts +34 -0
  33. package/lib/JoinGate.js +221 -0
  34. package/lib/NetworkAddresses.d.ts +33 -0
  35. package/lib/NetworkAddresses.js +90 -0
  36. package/lib/PlayerTokens.d.ts +17 -0
  37. package/lib/PlayerTokens.js +24 -0
  38. package/lib/RoomServerTransport.d.ts +73 -0
  39. package/lib/RoomServerTransport.js +90 -0
  40. package/lib/ServerEndpoints.d.ts +133 -0
  41. package/lib/ServerEndpoints.js +247 -0
  42. package/lib/ServerInit.d.ts +28 -0
  43. package/lib/ServerInit.js +113 -0
  44. package/lib/SessionRoom.d.ts +391 -0
  45. package/lib/SessionRoom.js +930 -0
  46. package/lib/TlsCertificates.d.ts +35 -0
  47. package/lib/TlsCertificates.js +130 -0
  48. package/lib/admin/AccountRoutes.d.ts +8 -0
  49. package/lib/admin/AccountRoutes.js +502 -0
  50. package/lib/admin/AdminAccounts.d.ts +329 -0
  51. package/lib/admin/AdminAccounts.js +607 -0
  52. package/lib/admin/AdminApiRouter.d.ts +202 -0
  53. package/lib/admin/AdminApiRouter.js +465 -0
  54. package/lib/admin/AdminApiTypes.d.ts +1782 -0
  55. package/lib/admin/AdminApiTypes.js +4 -0
  56. package/lib/admin/AdminAudit.d.ts +113 -0
  57. package/lib/admin/AdminAudit.js +266 -0
  58. package/lib/admin/AdminCli.d.ts +7 -0
  59. package/lib/admin/AdminCli.js +351 -0
  60. package/lib/admin/AdminClock.d.ts +51 -0
  61. package/lib/admin/AdminClock.js +10 -0
  62. package/lib/admin/AdminConsole.d.ts +39 -0
  63. package/lib/admin/AdminConsole.js +325 -0
  64. package/lib/admin/AdminConstants.d.ts +147 -0
  65. package/lib/admin/AdminConstants.js +167 -0
  66. package/lib/admin/AdminCrypto.d.ts +19 -0
  67. package/lib/admin/AdminCrypto.js +29 -0
  68. package/lib/admin/AdminExtensionsHost.d.ts +176 -0
  69. package/lib/admin/AdminExtensionsHost.js +307 -0
  70. package/lib/admin/AdminPasswords.d.ts +33 -0
  71. package/lib/admin/AdminPasswords.js +92 -0
  72. package/lib/admin/AdminPermissions.d.ts +28 -0
  73. package/lib/admin/AdminPermissions.js +74 -0
  74. package/lib/admin/AdminRecords.d.ts +196 -0
  75. package/lib/admin/AdminRecords.js +3 -0
  76. package/lib/admin/AdminRequestBodies.d.ts +47 -0
  77. package/lib/admin/AdminRequestBodies.js +84 -0
  78. package/lib/admin/AdminRequestGate.d.ts +38 -0
  79. package/lib/admin/AdminRequestGate.js +59 -0
  80. package/lib/admin/AdminResponses.d.ts +103 -0
  81. package/lib/admin/AdminResponses.js +164 -0
  82. package/lib/admin/AdminServices.d.ts +99 -0
  83. package/lib/admin/AdminServices.js +4 -0
  84. package/lib/admin/AdminStaticFiles.d.ts +29 -0
  85. package/lib/admin/AdminStaticFiles.js +97 -0
  86. package/lib/admin/AdminStore.d.ts +152 -0
  87. package/lib/admin/AdminStore.js +46 -0
  88. package/lib/admin/AlertChannels.d.ts +43 -0
  89. package/lib/admin/AlertChannels.js +120 -0
  90. package/lib/admin/AlertEngine.d.ts +231 -0
  91. package/lib/admin/AlertEngine.js +681 -0
  92. package/lib/admin/AlertRoutes.d.ts +8 -0
  93. package/lib/admin/AlertRoutes.js +180 -0
  94. package/lib/admin/ApiKeyRoutes.d.ts +8 -0
  95. package/lib/admin/ApiKeyRoutes.js +53 -0
  96. package/lib/admin/ApiKeys.d.ts +63 -0
  97. package/lib/admin/ApiKeys.js +167 -0
  98. package/lib/admin/AuditRoutes.d.ts +8 -0
  99. package/lib/admin/AuditRoutes.js +106 -0
  100. package/lib/admin/AuthToolRoutes.d.ts +13 -0
  101. package/lib/admin/AuthToolRoutes.js +164 -0
  102. package/lib/admin/BanList.d.ts +87 -0
  103. package/lib/admin/BanList.js +152 -0
  104. package/lib/admin/CborDecoder.d.ts +18 -0
  105. package/lib/admin/CborDecoder.js +201 -0
  106. package/lib/admin/ColyseusCloudClient.d.ts +313 -0
  107. package/lib/admin/ColyseusCloudClient.js +272 -0
  108. package/lib/admin/ColyseusCloudRoutes.d.ts +8 -0
  109. package/lib/admin/ColyseusCloudRoutes.js +91 -0
  110. package/lib/admin/ColyseusCloudService.d.ts +180 -0
  111. package/lib/admin/ColyseusCloudService.js +476 -0
  112. package/lib/admin/ColyseusCloudSettings.d.ts +16 -0
  113. package/lib/admin/ColyseusCloudSettings.js +29 -0
  114. package/lib/admin/ColyseusCloudViews.d.ts +105 -0
  115. package/lib/admin/ColyseusCloudViews.js +371 -0
  116. package/lib/admin/CommonPasswords.d.ts +6 -0
  117. package/lib/admin/CommonPasswords.js +19 -0
  118. package/lib/admin/ConfigOverrides.d.ts +174 -0
  119. package/lib/admin/ConfigOverrides.js +406 -0
  120. package/lib/admin/ConfigRoutes.d.ts +8 -0
  121. package/lib/admin/ConfigRoutes.js +91 -0
  122. package/lib/admin/ConsoleSessions.d.ts +112 -0
  123. package/lib/admin/ConsoleSessions.js +201 -0
  124. package/lib/admin/DedicatedRoutes.d.ts +21 -0
  125. package/lib/admin/DedicatedRoutes.js +254 -0
  126. package/lib/admin/ExtensionRoutes.d.ts +8 -0
  127. package/lib/admin/ExtensionRoutes.js +145 -0
  128. package/lib/admin/FileAdminStore.d.ts +157 -0
  129. package/lib/admin/FileAdminStore.js +349 -0
  130. package/lib/admin/FleetDirectory.d.ts +323 -0
  131. package/lib/admin/FleetDirectory.js +489 -0
  132. package/lib/admin/FleetLeader.d.ts +43 -0
  133. package/lib/admin/FleetLeader.js +89 -0
  134. package/lib/admin/FleetRoutes.d.ts +55 -0
  135. package/lib/admin/FleetRoutes.js +465 -0
  136. package/lib/admin/HealthWarnings.d.ts +10 -0
  137. package/lib/admin/HealthWarnings.js +95 -0
  138. package/lib/admin/LeaderboardRoutes.d.ts +8 -0
  139. package/lib/admin/LeaderboardRoutes.js +569 -0
  140. package/lib/admin/LogBuffer.d.ts +60 -0
  141. package/lib/admin/LogBuffer.js +92 -0
  142. package/lib/admin/LogRedaction.d.ts +15 -0
  143. package/lib/admin/LogRedaction.js +72 -0
  144. package/lib/admin/LogRoutes.d.ts +45 -0
  145. package/lib/admin/LogRoutes.js +307 -0
  146. package/lib/admin/MaintenanceMode.d.ts +41 -0
  147. package/lib/admin/MaintenanceMode.js +74 -0
  148. package/lib/admin/MaintenanceWindows.d.ts +91 -0
  149. package/lib/admin/MaintenanceWindows.js +207 -0
  150. package/lib/admin/MetricsRoutes.d.ts +8 -0
  151. package/lib/admin/MetricsRoutes.js +209 -0
  152. package/lib/admin/MetricsSampler.d.ts +124 -0
  153. package/lib/admin/MetricsSampler.js +211 -0
  154. package/lib/admin/OneTimeCodes.d.ts +60 -0
  155. package/lib/admin/OneTimeCodes.js +162 -0
  156. package/lib/admin/OpenIdConnect.d.ts +146 -0
  157. package/lib/admin/OpenIdConnect.js +335 -0
  158. package/lib/admin/Passkeys.d.ts +130 -0
  159. package/lib/admin/Passkeys.js +432 -0
  160. package/lib/admin/PlayerRoutes.d.ts +20 -0
  161. package/lib/admin/PlayerRoutes.js +676 -0
  162. package/lib/admin/RedisAdminStore.d.ts +114 -0
  163. package/lib/admin/RedisAdminStore.js +258 -0
  164. package/lib/admin/RolloutCoordinator.d.ts +99 -0
  165. package/lib/admin/RolloutCoordinator.js +321 -0
  166. package/lib/admin/ServerEvents.d.ts +57 -0
  167. package/lib/admin/ServerEvents.js +89 -0
  168. package/lib/admin/SessionRoutes.d.ts +113 -0
  169. package/lib/admin/SessionRoutes.js +386 -0
  170. package/lib/admin/UserRoutes.d.ts +8 -0
  171. package/lib/admin/UserRoutes.js +214 -0
  172. package/lib/cli.d.ts +2 -0
  173. package/lib/cli.js +221 -0
  174. package/lib/console/assets/index-CI5N7c-T.js +21 -0
  175. package/lib/console/assets/index-DPitvzLp.css +1 -0
  176. package/lib/console/favicon.svg +1 -0
  177. package/lib/console/index.html +16 -0
  178. package/lib/discord/DiscordOAuth.d.ts +57 -0
  179. package/lib/discord/DiscordOAuth.js +127 -0
  180. package/lib/discord/DiscordSessions.d.ts +79 -0
  181. package/lib/discord/DiscordSessions.js +135 -0
  182. package/lib/discord/DiscordSignInConstants.d.ts +54 -0
  183. package/lib/discord/DiscordSignInConstants.js +55 -0
  184. package/lib/discord/DiscordSignInRoutes.d.ts +51 -0
  185. package/lib/discord/DiscordSignInRoutes.js +359 -0
  186. package/lib/index.d.ts +24 -0
  187. package/lib/index.js +18 -0
  188. package/lib/leaderboards/FileLeaderboardRankStore.d.ts +293 -0
  189. package/lib/leaderboards/FileLeaderboardRankStore.js +808 -0
  190. package/lib/leaderboards/LeaderboardConstants.d.ts +63 -0
  191. package/lib/leaderboards/LeaderboardConstants.js +70 -0
  192. package/lib/leaderboards/LeaderboardError.d.ts +15 -0
  193. package/lib/leaderboards/LeaderboardError.js +20 -0
  194. package/lib/leaderboards/LeaderboardIdentity.d.ts +44 -0
  195. package/lib/leaderboards/LeaderboardIdentity.js +72 -0
  196. package/lib/leaderboards/LeaderboardPublicApi.d.ts +17 -0
  197. package/lib/leaderboards/LeaderboardPublicApi.js +389 -0
  198. package/lib/leaderboards/LeaderboardRankStore.d.ts +296 -0
  199. package/lib/leaderboards/LeaderboardRankStore.js +18 -0
  200. package/lib/leaderboards/LeaderboardRules.d.ts +122 -0
  201. package/lib/leaderboards/LeaderboardRules.js +472 -0
  202. package/lib/leaderboards/LeaderboardScheduler.d.ts +43 -0
  203. package/lib/leaderboards/LeaderboardScheduler.js +103 -0
  204. package/lib/leaderboards/LeaderboardService.d.ts +669 -0
  205. package/lib/leaderboards/LeaderboardService.js +1667 -0
  206. package/lib/leaderboards/LeaderboardTransfer.d.ts +35 -0
  207. package/lib/leaderboards/LeaderboardTransfer.js +580 -0
  208. package/lib/leaderboards/RedisLeaderboardRankStore.d.ts +282 -0
  209. package/lib/leaderboards/RedisLeaderboardRankStore.js +691 -0
  210. package/package.json +64 -4
  211. package/templates/init/Caddyfile +6 -0
  212. package/templates/init/DEPLOY.md +233 -0
  213. package/templates/init/Dockerfile +12 -0
  214. package/templates/init/app.json +21 -0
  215. package/templates/init/aws/README.md +118 -0
  216. package/templates/init/aws/ecs-fargate-alb.yml +207 -0
  217. package/templates/init/aws/ecs-fargate-nlb-quic.yml +210 -0
  218. package/templates/init/babylon-game-server.config.mjs +158 -0
  219. package/templates/init/colyseus-cloud/README.md +133 -0
  220. package/templates/init/colyseus-cloud/babylon-game-server.config.mjs +36 -0
  221. package/templates/init/colyseus-cloud/ecosystem.config.cjs +35 -0
  222. package/templates/init/colyseus-cloud/gitignore +6 -0
  223. package/templates/init/colyseus-cloud/package.json +15 -0
  224. package/templates/init/digitalocean-app.yaml +30 -0
  225. package/templates/init/docker-compose.full.yml +45 -0
  226. package/templates/init/docker-compose.websocket.yml +41 -0
  227. package/templates/init/fly.toml +56 -0
  228. package/templates/init/heroku.yml +11 -0
  229. package/templates/init/kubernetes/deployment.yaml +71 -0
  230. package/templates/init/kubernetes/redis.yaml +73 -0
  231. package/templates/init/kubernetes/service.yaml +24 -0
  232. package/templates/init/monitoring/grafana-dashboard.json +539 -0
  233. package/templates/init/monitoring/prometheus-alerts.yml +51 -0
  234. package/templates/init/monitoring/prometheus-scrape.yml +17 -0
  235. package/templates/init/railway.json +13 -0
  236. package/templates/init/render.yaml +19 -0
@@ -0,0 +1,91 @@
1
+ import type { AdminNoticePayload, MaintenanceWindowsRequest, MaintenanceWindowView } from "./AdminApiTypes.js";
2
+ import type { MaintenanceWindowRecord } from "./AdminRecords.js";
3
+ import type { AdminCoreServices } from "./AdminServices.js";
4
+ import type { FleetDirectory } from "./FleetDirectory.js";
5
+ import { type MaintenanceMode } from "./MaintenanceMode.js";
6
+ import type { ServerEvents } from "./ServerEvents.js";
7
+ /** Called when a window turns fleet maintenance on or off (the onMaintenanceChanged admin hook, R29). */
8
+ export type WindowMaintenanceListener = (enabled: boolean, message: string | null, window: MaintenanceWindowRecord) => void;
9
+ /** Fills a notice payload (FleetRoutes.createNoticePayload over the services). */
10
+ export type NoticeFactory = (text: string, countdownSeconds: number | null) => AdminNoticePayload;
11
+ /** The fleet's maintenance windows. */
12
+ export declare class MaintenanceWindows {
13
+ /** The shared core services. */
14
+ private readonly services;
15
+ /** The fleet directory (notices and change announcements). */
16
+ private readonly fleet;
17
+ /** This instance's maintenance mode. */
18
+ private readonly maintenance;
19
+ /** The activity feed's server events. */
20
+ private readonly serverEvents;
21
+ /** Builds the warning notices. */
22
+ private readonly createNotice;
23
+ /** Listeners of maintenance turned on or off by a window. */
24
+ private readonly maintenanceListeners;
25
+ /**
26
+ * Creates the scheduler; nothing runs until the leader ticks.
27
+ * @param services The shared core services.
28
+ * @param fleet The fleet directory.
29
+ * @param maintenance This instance's maintenance mode.
30
+ * @param serverEvents The activity feed's server events.
31
+ * @param createNotice Builds a warning notice.
32
+ */
33
+ constructor(services: AdminCoreServices, fleet: FleetDirectory, maintenance: MaintenanceMode, serverEvents: ServerEvents, createNotice: NoticeFactory);
34
+ /**
35
+ * Calls back when a window turns maintenance on or off.
36
+ * @param listener The listener.
37
+ */
38
+ onMaintenanceChanged(listener: WindowMaintenanceListener): void;
39
+ /**
40
+ * Every window, soonest first.
41
+ * @returns The windows.
42
+ */
43
+ list(): Promise<MaintenanceWindowView[]>;
44
+ /**
45
+ * Schedules a window (POST maintenance/windows).
46
+ * @param request The window.
47
+ * @param createdBy Who schedules it.
48
+ * @returns The window.
49
+ * @throws AdminRequestError 400 for unreadable times, an end not after its start or in the past, or a negative warning.
50
+ */
51
+ create(request: MaintenanceWindowsRequest, createdBy: string): Promise<MaintenanceWindowView>;
52
+ /**
53
+ * Cancels a window; an active one turns fleet maintenance off first.
54
+ * @param windowId The window.
55
+ * @throws AdminRequestError 404 for an unknown window.
56
+ */
57
+ cancel(windowId: string): Promise<void>;
58
+ /** The leader tick: moves every open window on by the clock. */
59
+ tick(): Promise<void>;
60
+ /**
61
+ * Moves one window to the state the time calls for, doing what the step does.
62
+ * @param window The window.
63
+ * @param now The clock time.
64
+ * @returns The window's new state.
65
+ */
66
+ private advance;
67
+ /**
68
+ * Sends the fleet notice of an upcoming window (severity warning, with a countdown to its start).
69
+ * @param window The window.
70
+ * @param startsAt Its start, in milliseconds.
71
+ * @param now The clock time.
72
+ */
73
+ private sendWarning;
74
+ /**
75
+ * Turns fleet maintenance on with the window's message and allow lists.
76
+ * @param window The window.
77
+ */
78
+ private startMaintenance;
79
+ /**
80
+ * Turns fleet maintenance off at a window's end.
81
+ * @param window The window.
82
+ */
83
+ private endMaintenance;
84
+ /** Applies the stored fleet setting here at once and announces it to the other instances. */
85
+ private applyFleetSetting;
86
+ /**
87
+ * Every stored window.
88
+ * @returns The documents.
89
+ */
90
+ private storedWindows;
91
+ }
@@ -0,0 +1,207 @@
1
+ // Scheduled maintenance windows (game-server-admin-console T14 step 3, MaintenanceWindowView): a fleet leader tick
2
+ // handler sends a warning notice warningMinutes before a window, turns fleet maintenance on with the window's message at
3
+ // startsAt and off at endsAt. Without Redis the windows apply to this one instance.
4
+ import { randomUUID } from "node:crypto";
5
+ import { AdminRequestError } from "./AdminApiRouter.js";
6
+ import { MAINTENANCE_SETTINGS_ID } from "./MaintenanceMode.js";
7
+ /** HTTP statuses of the refusals. */
8
+ const HTTP_BAD_REQUEST = 400;
9
+ const HTTP_NOT_FOUND = 404;
10
+ /** Milliseconds per second and per minute. */
11
+ const MILLISECONDS_PER_SECOND = 1000;
12
+ const MILLISECONDS_PER_MINUTE = 60000;
13
+ /** Window states the tick still acts on. */
14
+ const OPEN_STATES = new Set(["scheduled", "warning", "active"]);
15
+ /** The fleet's maintenance windows. */
16
+ export class MaintenanceWindows {
17
+ /** The shared core services. */
18
+ services;
19
+ /** The fleet directory (notices and change announcements). */
20
+ fleet;
21
+ /** This instance's maintenance mode. */
22
+ maintenance;
23
+ /** The activity feed's server events. */
24
+ serverEvents;
25
+ /** Builds the warning notices. */
26
+ createNotice;
27
+ /** Listeners of maintenance turned on or off by a window. */
28
+ maintenanceListeners = [];
29
+ /**
30
+ * Creates the scheduler; nothing runs until the leader ticks.
31
+ * @param services The shared core services.
32
+ * @param fleet The fleet directory.
33
+ * @param maintenance This instance's maintenance mode.
34
+ * @param serverEvents The activity feed's server events.
35
+ * @param createNotice Builds a warning notice.
36
+ */
37
+ constructor(services, fleet, maintenance, serverEvents, createNotice) {
38
+ this.services = services;
39
+ this.fleet = fleet;
40
+ this.maintenance = maintenance;
41
+ this.serverEvents = serverEvents;
42
+ this.createNotice = createNotice;
43
+ }
44
+ /**
45
+ * Calls back when a window turns maintenance on or off.
46
+ * @param listener The listener.
47
+ */
48
+ onMaintenanceChanged(listener) {
49
+ this.maintenanceListeners.push(listener);
50
+ }
51
+ /**
52
+ * Every window, soonest first.
53
+ * @returns The windows.
54
+ */
55
+ async list() {
56
+ const records = (await this.storedWindows()).map((stored) => stored.value);
57
+ return records.sort((left, right) => left.startsAt.localeCompare(right.startsAt)).map(windowView);
58
+ }
59
+ /**
60
+ * Schedules a window (POST maintenance/windows).
61
+ * @param request The window.
62
+ * @param createdBy Who schedules it.
63
+ * @returns The window.
64
+ * @throws AdminRequestError 400 for unreadable times, an end not after its start or in the past, or a negative warning.
65
+ */
66
+ async create(request, createdBy) {
67
+ const startsAt = Date.parse(request.startsAt);
68
+ const endsAt = Date.parse(request.endsAt);
69
+ if (!Number.isFinite(startsAt) || !Number.isFinite(endsAt))
70
+ throw new AdminRequestError(HTTP_BAD_REQUEST, "startsAt and endsAt must be ISO times");
71
+ if (endsAt <= startsAt)
72
+ throw new AdminRequestError(HTTP_BAD_REQUEST, "endsAt must be after startsAt");
73
+ if (endsAt <= this.services.clock.now())
74
+ throw new AdminRequestError(HTTP_BAD_REQUEST, "endsAt must be in the future");
75
+ if (request.warningMinutes != null && request.warningMinutes < 0)
76
+ throw new AdminRequestError(HTTP_BAD_REQUEST, "warningMinutes may not be negative");
77
+ const record = {
78
+ windowId: randomUUID(),
79
+ startsAt: new Date(startsAt).toISOString(),
80
+ endsAt: new Date(endsAt).toISOString(),
81
+ message: request.message,
82
+ warningMinutes: request.warningMinutes,
83
+ allowUserIds: request.allowUserIds.slice(),
84
+ allowAddresses: request.allowAddresses.slice(),
85
+ leaveReadiness: request.leaveReadiness,
86
+ state: "scheduled",
87
+ createdBy: createdBy
88
+ };
89
+ await this.services.store.putDocument("maintenanceWindows", record.windowId, record, 0);
90
+ return windowView(record);
91
+ }
92
+ /**
93
+ * Cancels a window; an active one turns fleet maintenance off first.
94
+ * @param windowId The window.
95
+ * @throws AdminRequestError 404 for an unknown window.
96
+ */
97
+ async cancel(windowId) {
98
+ const stored = await this.services.store.getDocument("maintenanceWindows", windowId);
99
+ if (stored == null)
100
+ throw new AdminRequestError(HTTP_NOT_FOUND, "no such maintenance window");
101
+ if (stored.value.state === "active")
102
+ await this.endMaintenance(stored.value);
103
+ if (!OPEN_STATES.has(stored.value.state))
104
+ return;
105
+ await this.services.store.putDocument("maintenanceWindows", windowId, Object.assign({}, stored.value, { state: "cancelled" }), null);
106
+ }
107
+ /** The leader tick: moves every open window on by the clock. */
108
+ async tick() {
109
+ const now = this.services.clock.now();
110
+ for (const stored of await this.storedWindows()) {
111
+ if (!OPEN_STATES.has(stored.value.state))
112
+ continue;
113
+ const nextState = await this.advance(stored.value, now);
114
+ if (nextState === stored.value.state)
115
+ continue;
116
+ await this.services.store.putDocument("maintenanceWindows", stored.id, Object.assign({}, stored.value, { state: nextState }), stored.revision);
117
+ }
118
+ }
119
+ /**
120
+ * Moves one window to the state the time calls for, doing what the step does.
121
+ * @param window The window.
122
+ * @param now The clock time.
123
+ * @returns The window's new state.
124
+ */
125
+ async advance(window, now) {
126
+ const startsAt = Date.parse(window.startsAt);
127
+ const endsAt = Date.parse(window.endsAt);
128
+ if (now >= endsAt) {
129
+ if (window.state === "active")
130
+ await this.endMaintenance(window);
131
+ return "done";
132
+ }
133
+ if (now >= startsAt) {
134
+ if (window.state !== "active")
135
+ await this.startMaintenance(window);
136
+ return "active";
137
+ }
138
+ const warningAt = (window.warningMinutes != null) ? startsAt - window.warningMinutes * MILLISECONDS_PER_MINUTE : null;
139
+ if (window.state === "scheduled" && warningAt != null && now >= warningAt) {
140
+ await this.sendWarning(window, startsAt, now);
141
+ return "warning";
142
+ }
143
+ return window.state;
144
+ }
145
+ /**
146
+ * Sends the fleet notice of an upcoming window (severity warning, with a countdown to its start).
147
+ * @param window The window.
148
+ * @param startsAt Its start, in milliseconds.
149
+ * @param now The clock time.
150
+ */
151
+ async sendWarning(window, startsAt, now) {
152
+ const countdownSeconds = Math.ceil((startsAt - now) / MILLISECONDS_PER_SECOND);
153
+ const notice = this.createNotice(window.message, countdownSeconds);
154
+ await this.fleet.runOnAll("sendNotice", { roomId: null, playerId: 0, notice: notice });
155
+ }
156
+ /**
157
+ * Turns fleet maintenance on with the window's message and allow lists.
158
+ * @param window The window.
159
+ */
160
+ async startMaintenance(window) {
161
+ const settings = { enabled: true, message: window.message, allowUserIds: window.allowUserIds, allowAddresses: window.allowAddresses, leaveReadiness: window.leaveReadiness };
162
+ await this.services.store.putDocument("settings", MAINTENANCE_SETTINGS_ID, settings, null);
163
+ await this.applyFleetSetting();
164
+ this.serverEvents.record("maintenance on (scheduled window): " + window.message, null);
165
+ for (const listener of this.maintenanceListeners)
166
+ listener(true, window.message, window);
167
+ }
168
+ /**
169
+ * Turns fleet maintenance off at a window's end.
170
+ * @param window The window.
171
+ */
172
+ async endMaintenance(window) {
173
+ await this.services.store.deleteDocument("settings", MAINTENANCE_SETTINGS_ID);
174
+ await this.applyFleetSetting();
175
+ this.serverEvents.record("maintenance off (scheduled window ended)", null);
176
+ for (const listener of this.maintenanceListeners)
177
+ listener(false, null, window);
178
+ }
179
+ /** Applies the stored fleet setting here at once and announces it to the other instances. */
180
+ async applyFleetSetting() {
181
+ await this.maintenance.reloadFleetSetting(this.services.store);
182
+ await this.fleet.announceChange("settings");
183
+ }
184
+ /**
185
+ * Every stored window.
186
+ * @returns The documents.
187
+ */
188
+ storedWindows() {
189
+ return this.services.store.listDocuments("maintenanceWindows");
190
+ }
191
+ }
192
+ /**
193
+ * A window as the console sees it.
194
+ * @param record The stored window.
195
+ * @returns The view.
196
+ */
197
+ function windowView(record) {
198
+ return {
199
+ windowId: record.windowId,
200
+ startsAt: record.startsAt,
201
+ endsAt: record.endsAt,
202
+ message: record.message,
203
+ warningMinutes: record.warningMinutes,
204
+ state: record.state,
205
+ createdBy: record.createdBy
206
+ };
207
+ }
@@ -0,0 +1,8 @@
1
+ import { type AdminApiRouter } from "./AdminApiRouter.js";
2
+ import type { AdminServices } from "./AdminServices.js";
3
+ /**
4
+ * Registers the metrics route.
5
+ * @param router The admin API router.
6
+ * @param services The shared services.
7
+ */
8
+ export declare function registerMetricsRoutes(router: AdminApiRouter, services: AdminServices): void;
@@ -0,0 +1,209 @@
1
+ // The metrics route (game-server-admin-console DR §6.11 metrics row, D22, DR §8 rates): chart points for one instance or
2
+ // the whole fleet over 15 min, 1 h, 6 h or 24 h, computed from the samples of MetricsSampler. With Redis every instance's
3
+ // samples are read from its store stream; without Redis the fleet is this instance and its in-memory ring.
4
+ import { AdminRequestError } from "./AdminApiRouter.js";
5
+ import { RECORD_QUERY_MAX_LIMIT } from "./AdminStore.js";
6
+ import { metricsStreamOf } from "./MetricsSampler.js";
7
+ /** HTTP status of a successful read. */
8
+ const HTTP_OK = 200;
9
+ /** HTTP status of an invalid query. */
10
+ const HTTP_BAD_REQUEST = 400;
11
+ /** HTTP status of an unknown instance. */
12
+ const HTTP_NOT_FOUND = 404;
13
+ /** The processId query value of the whole fleet. */
14
+ const FLEET_PROCESS_ID = "fleet";
15
+ /** The range a query without one charts. */
16
+ const DEFAULT_RANGE = "1h";
17
+ /** Milliseconds per minute. */
18
+ const MILLISECONDS_PER_MINUTE = 60000;
19
+ /** How far back each range reaches, in milliseconds. */
20
+ const RANGE_MILLISECONDS = {
21
+ "15m": 15 * MILLISECONDS_PER_MINUTE,
22
+ "1h": 60 * MILLISECONDS_PER_MINUTE,
23
+ "6h": 360 * MILLISECONDS_PER_MINUTE,
24
+ "24h": 1440 * MILLISECONDS_PER_MINUTE
25
+ };
26
+ /** Milliseconds per second. */
27
+ const MILLISECONDS_PER_SECOND = 1000;
28
+ /** Percent of one fully used CPU core. */
29
+ const FULL_CORE_PERCENT = 100;
30
+ /** Point fields the fleet series takes the largest instance value of; every other field is summed. */
31
+ const FLEET_MAXIMUM_FIELDS = new Set(["eventLoopLagMilliseconds", "dedicatedFrameMillisecondsAverage"]);
32
+ /**
33
+ * Registers the metrics route.
34
+ * @param router The admin API router.
35
+ * @param services The shared services.
36
+ */
37
+ export function registerMetricsRoutes(router, services) {
38
+ const route = { method: "GET", pattern: "metrics", permission: "metrics.view", requiresStepUp: false, auditAction: null, handler: async (request) => ({ status: HTTP_OK, body: await buildMetricsSeries(services, request) }) };
39
+ router.register(route);
40
+ }
41
+ /**
42
+ * GET metrics: the chart points of one instance or of the fleet.
43
+ * @param services The shared services.
44
+ * @param request The request (query processId, range).
45
+ * @returns The series.
46
+ * @throws AdminRequestError 400 for an unknown range.
47
+ */
48
+ async function buildMetricsSeries(services, request) {
49
+ const range = rangeOf(request.query.get("range"));
50
+ const processId = request.query.get("processId") ?? FLEET_PROCESS_ID;
51
+ const rangeMilliseconds = RANGE_MILLISECONDS[range];
52
+ const points = (processId === FLEET_PROCESS_ID)
53
+ ? sumFleetPoints(await readFleetSamples(services, rangeMilliseconds), services.metricsSampleIntervalMs)
54
+ : pointsFromSamples(await readInstanceSamples(services, processId, rangeMilliseconds));
55
+ return { processId: processId, range: range, points: points, grafanaUrl: services.config.admin.grafanaUrl };
56
+ }
57
+ /**
58
+ * The range of a query.
59
+ * @param text The range query value, or null.
60
+ * @returns The range (DEFAULT_RANGE when missing).
61
+ * @throws AdminRequestError 400 for an unknown value.
62
+ */
63
+ function rangeOf(text) {
64
+ if (text == null || text === "")
65
+ return DEFAULT_RANGE;
66
+ if (!Object.hasOwn(RANGE_MILLISECONDS, text))
67
+ throw new AdminRequestError(HTTP_BAD_REQUEST, "range must be 15m, 1h, 6h or 24h");
68
+ return text;
69
+ }
70
+ /**
71
+ * One instance's samples: its store stream with Redis (which survives its restart), else this instance's ring.
72
+ * @param services The shared services.
73
+ * @param processId The instance.
74
+ * @param rangeMilliseconds How far back.
75
+ * @returns The samples, oldest first.
76
+ * @throws AdminRequestError 404 for another instance without Redis.
77
+ */
78
+ async function readInstanceSamples(services, processId, rangeMilliseconds) {
79
+ if (services.store.kind === "redis")
80
+ return readStoredSamples(services, processId, rangeMilliseconds);
81
+ if (processId !== services.fleet.localProcessId)
82
+ throw new AdminRequestError(HTTP_NOT_FOUND, "no such instance");
83
+ return services.metricsSampler.samples(rangeMilliseconds);
84
+ }
85
+ /**
86
+ * The samples of every up instance (R33: without Redis the fleet is this instance).
87
+ * @param services The shared services.
88
+ * @param rangeMilliseconds How far back.
89
+ * @returns Each instance's samples, oldest first.
90
+ */
91
+ async function readFleetSamples(services, rangeMilliseconds) {
92
+ if (services.store.kind !== "redis")
93
+ return [services.metricsSampler.samples(rangeMilliseconds)];
94
+ const instances = (await services.fleet.listInstances()).filter((instance) => instance.status === "up");
95
+ return Promise.all(instances.map((instance) => readStoredSamples(services, instance.processId, rangeMilliseconds)));
96
+ }
97
+ /**
98
+ * Reads an instance's stored samples of a recent period, newest pages first, until the period is covered.
99
+ * @param services The shared services.
100
+ * @param processId The instance.
101
+ * @param rangeMilliseconds How far back.
102
+ * @returns The samples, oldest first.
103
+ */
104
+ async function readStoredSamples(services, processId, rangeMilliseconds) {
105
+ const cutoff = services.clock.now() - rangeMilliseconds;
106
+ const newestFirst = [];
107
+ let beforeSequence = undefined;
108
+ for (;;) {
109
+ const page = await services.store.readRecords(metricsStreamOf(processId), { beforeSequence: beforeSequence, limit: RECORD_QUERY_MAX_LIMIT, newestFirst: true });
110
+ const inRange = page.filter((record) => Date.parse(record.value.at) >= cutoff);
111
+ newestFirst.push(...inRange.map((record) => record.value));
112
+ if (page.length < RECORD_QUERY_MAX_LIMIT || inRange.length < page.length)
113
+ break;
114
+ beforeSequence = page[page.length - 1].sequence;
115
+ }
116
+ return newestFirst.reverse();
117
+ }
118
+ /**
119
+ * Chart points from one instance's consecutive samples (DR §8): the first sample only starts the rates.
120
+ * @param samples The samples, oldest first.
121
+ * @returns The points.
122
+ */
123
+ function pointsFromSamples(samples) {
124
+ const points = [];
125
+ for (let i = 1; i < samples.length; i++) {
126
+ const point = pointBetween(samples[i - 1], samples[i]);
127
+ if (point != null)
128
+ points.push(point);
129
+ }
130
+ return points;
131
+ }
132
+ /**
133
+ * The chart point of a sample, with rates per second measured from the sample before it.
134
+ * @param previous The earlier sample.
135
+ * @param current The sample.
136
+ * @returns The point, or null when the samples share a time.
137
+ */
138
+ function pointBetween(previous, current) {
139
+ const seconds = (Date.parse(current.at) - Date.parse(previous.at)) / MILLISECONDS_PER_SECOND;
140
+ if (!(seconds > 0))
141
+ return null;
142
+ // Samples stored before a counter existed lack it, so a missing total reads as 0.
143
+ const perSecond = (field) => counterRate(previous[field] ?? 0, current[field] ?? 0, seconds);
144
+ return {
145
+ at: current.at,
146
+ players: current.players,
147
+ sessions: current.sessions,
148
+ dedicatedSessions: current.dedicatedSessions,
149
+ joinsPerSecond: perSecond("joinsAccepted"),
150
+ refusalsPerSecond: perSecond("joinsRefused"),
151
+ authFailuresPerSecond: perSecond("authFailures"),
152
+ bytesInPerSecond: perSecond("bytesIn"),
153
+ bytesOutPerSecond: perSecond("bytesOut"),
154
+ messagesInPerSecond: perSecond("messagesIn"),
155
+ cpuPercent: perSecond("cpuSecondsTotal") * FULL_CORE_PERCENT,
156
+ residentMemoryBytes: current.residentMemoryBytes,
157
+ heapUsedBytes: current.heapUsedBytes,
158
+ eventLoopLagMilliseconds: current.eventLoopLagMilliseconds,
159
+ gcMillisecondsPerSecond: perSecond("gcSecondsTotal") * MILLISECONDS_PER_SECOND,
160
+ dedicatedFrameMillisecondsAverage: current.dedicatedFrameMillisecondsAverage,
161
+ dedicatedTickOverrunsPerSecond: perSecond("dedicatedTickOverruns"),
162
+ leaderboardAcceptedPerSecond: perSecond("leaderboardScoresAccepted"),
163
+ leaderboardRefusedPerSecond: perSecond("leaderboardScoresRefused")
164
+ };
165
+ }
166
+ /**
167
+ * The per-second rate of a counter between two samples (DR §8): a drop means the instance restarted, so it is 0.
168
+ * @param previous The earlier total.
169
+ * @param current The later total.
170
+ * @param seconds Seconds between the samples (more than 0).
171
+ * @returns The rate, never negative.
172
+ */
173
+ function counterRate(previous, current, seconds) {
174
+ return (current >= previous) ? (current - previous) / seconds : 0;
175
+ }
176
+ /**
177
+ * The fleet series (DR §8): each instance's points aligned to the sample grid by flooring their time, then summed per
178
+ * grid step, except event-loop lag and dedicated frame time, where the worst instance is shown.
179
+ * @param samplesByInstance Each instance's samples, oldest first.
180
+ * @param gridMilliseconds The sample interval the times are floored to.
181
+ * @returns The fleet points, oldest first.
182
+ */
183
+ function sumFleetPoints(samplesByInstance, gridMilliseconds) {
184
+ const buckets = new Map();
185
+ for (const samples of samplesByInstance) {
186
+ for (const point of pointsFromSamples(samples)) {
187
+ const bucketStart = Math.floor(Date.parse(point.at) / gridMilliseconds) * gridMilliseconds;
188
+ const existing = buckets.get(bucketStart);
189
+ if (existing == null)
190
+ buckets.set(bucketStart, Object.assign({}, point, { at: new Date(bucketStart).toISOString() }));
191
+ else
192
+ addPoint(existing, point);
193
+ }
194
+ }
195
+ return Array.from(buckets.entries()).sort((left, right) => left[0] - right[0]).map((entry) => entry[1]);
196
+ }
197
+ /**
198
+ * Adds another instance's point to a fleet point.
199
+ * @param total The fleet point (changed in place).
200
+ * @param point The instance's point of the same grid step.
201
+ */
202
+ function addPoint(total, point) {
203
+ for (const field of Object.keys(point)) {
204
+ if (field === "at")
205
+ continue;
206
+ const figure = field;
207
+ total[figure] = FLEET_MAXIMUM_FIELDS.has(figure) ? Math.max(total[figure], point[figure]) : total[figure] + point[figure];
208
+ }
209
+ }
@@ -0,0 +1,124 @@
1
+ import type { AdminCoreServices } from "./AdminServices.js";
2
+ import type { AdminStream } from "./AdminStore.js";
3
+ /** One sample of an instance's figures (D22). Counters are totals since start; rates are computed by the reader. */
4
+ export interface MetricsSample {
5
+ /** ISO time of the sample. */
6
+ at: string;
7
+ /** Players connected to this instance's sessions. */
8
+ players: number;
9
+ /** Live sessions. */
10
+ sessions: number;
11
+ /** Live sessions a dedicated runner hosts. */
12
+ dedicatedSessions: number;
13
+ /** Accepted joins since start. */
14
+ joinsAccepted: number;
15
+ /** Refused joins since start. */
16
+ joinsRefused: number;
17
+ /** Refused joins since start, by ShutdownReason name. */
18
+ refusalsByReason: Record<string, number>;
19
+ /** Failed token checks since start. */
20
+ authFailures: number;
21
+ /** Frames from clients since start. */
22
+ framesIn: number;
23
+ /** Frames to clients since start. */
24
+ framesOut: number;
25
+ /** Frame bytes from clients since start. */
26
+ bytesIn: number;
27
+ /** Frame bytes to clients since start. */
28
+ bytesOut: number;
29
+ /** Frames (both directions) since start, by the client's transport. */
30
+ framesByTransport: Record<"websocket" | "webtransport", number>;
31
+ /** Room messages from clients since start. */
32
+ messagesIn: number;
33
+ /** Process CPU time since start, in seconds. */
34
+ cpuSecondsTotal: number;
35
+ /** Resident memory now, in bytes. */
36
+ residentMemoryBytes: number;
37
+ /** Used V8 heap now, in bytes. */
38
+ heapUsedBytes: number;
39
+ /** Mean event-loop delay since the previous sample, in milliseconds. */
40
+ eventLoopLagMilliseconds: number;
41
+ /** Time spent in garbage collection since start, in seconds. */
42
+ gcSecondsTotal: number;
43
+ /** Mean dedicated runner frame time since the previous sample, in milliseconds (0 without dedicated frames). */
44
+ dedicatedFrameMillisecondsAverage: number;
45
+ /** Dedicated frames longer than one tick since start. */
46
+ dedicatedTickOverruns: number;
47
+ /** Accepted leaderboard scores since start (game-server-leaderboards D21). */
48
+ leaderboardScoresAccepted: number;
49
+ /** Refused leaderboard scores since start (game-server-leaderboards D21). */
50
+ leaderboardScoresRefused: number;
51
+ }
52
+ /**
53
+ * The store stream of an instance's samples.
54
+ * @param processId The instance.
55
+ * @returns The stream name.
56
+ */
57
+ export declare function metricsStreamOf(processId: string): AdminStream;
58
+ /** Takes this instance's samples while the console runs. */
59
+ export declare class MetricsSampler {
60
+ /** The shared core services. */
61
+ private readonly services;
62
+ /** The samples, oldest first. */
63
+ private readonly ring;
64
+ /** How many samples the ring and the store stream keep. */
65
+ private readonly capacity;
66
+ /** The sampler's own event-loop delay histogram, so Prometheus' windows are untouched (D22). */
67
+ private eventLoopMonitor;
68
+ /** The sample timer, while started. */
69
+ private sampleTimer;
70
+ /** True while a sample is being taken, so a slow store never stacks them. */
71
+ private isSampling;
72
+ /** The counters of the previous sample, for the dedicated frame mean between samples. */
73
+ private previousCounters;
74
+ /** Clock time of the last "metrics sample failed" warning. */
75
+ private lastSampleWarningAt;
76
+ /**
77
+ * Prepares an empty history; nothing is sampled until start().
78
+ * @param services The shared core services.
79
+ */
80
+ constructor(services: AdminCoreServices);
81
+ /** Starts sampling every metricsSampleIntervalMs. */
82
+ start(): void;
83
+ /** Stops sampling; the history taken so far stays readable. */
84
+ stop(): void;
85
+ /**
86
+ * This instance's samples of a recent period, from the in-memory ring.
87
+ * @param rangeMilliseconds How far back, in milliseconds.
88
+ * @returns The samples, oldest first.
89
+ */
90
+ samples(rangeMilliseconds: number): MetricsSample[];
91
+ /** Takes one sample into the ring and, with Redis, into the store stream; a failure is logged. */
92
+ private takeSample;
93
+ /**
94
+ * Appends a sample to this instance's store stream and trims it to the ring's capacity.
95
+ * @param sample The sample.
96
+ */
97
+ private storeSample;
98
+ /**
99
+ * This instance's figures now (DR §6.7).
100
+ * @returns The sample.
101
+ */
102
+ private buildSample;
103
+ /**
104
+ * The metrics counters, read fresh for this sample (R22), or zeros when metrics are off.
105
+ * @returns The counters.
106
+ */
107
+ private readCounters;
108
+ /**
109
+ * The mean dedicated frame time since the previous sample.
110
+ * @param counters The counters now.
111
+ * @returns The mean, in milliseconds (0 when no dedicated frame ran).
112
+ */
113
+ private dedicatedFrameAverage;
114
+ /**
115
+ * The mean event-loop delay since the previous sample; the histogram restarts afterwards.
116
+ * @returns The delay, in milliseconds (0 before any measurement).
117
+ */
118
+ private sampleEventLoopLag;
119
+ /**
120
+ * Logs a failed sample at warn, at most once per SAMPLE_WARNING_INTERVAL_MS.
121
+ * @param error What failed.
122
+ */
123
+ private warnSampleFailure;
124
+ }