@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.
- package/CUSTOM.md +1690 -0
- package/DEPLOY.md +2012 -0
- package/README.md +1081 -2
- package/lib/AbuseLimits.d.ts +64 -0
- package/lib/AbuseLimits.js +121 -0
- package/lib/Authentication.d.ts +86 -0
- package/lib/Authentication.js +272 -0
- package/lib/BootSessionOwner.d.ts +86 -0
- package/lib/BootSessionOwner.js +116 -0
- package/lib/ColyseusLoggerAdapter.d.ts +32 -0
- package/lib/ColyseusLoggerAdapter.js +73 -0
- package/lib/DedicatedSceneRegistry.d.ts +52 -0
- package/lib/DedicatedSceneRegistry.js +74 -0
- package/lib/DedicatedSession.d.ts +152 -0
- package/lib/DedicatedSession.js +173 -0
- package/lib/DualTransport.d.ts +112 -0
- package/lib/DualTransport.js +197 -0
- package/lib/GameServer.d.ts +64 -0
- package/lib/GameServer.js +647 -0
- package/lib/GameServerConfig.d.ts +387 -0
- package/lib/GameServerConfig.js +1059 -0
- package/lib/GameServerLobbyRoom.d.ts +33 -0
- package/lib/GameServerLobbyRoom.js +56 -0
- package/lib/GameServerLogger.d.ts +111 -0
- package/lib/GameServerLogger.js +190 -0
- package/lib/GameServerMetrics.d.ts +158 -0
- package/lib/GameServerMetrics.js +276 -0
- package/lib/GameServerState.d.ts +164 -0
- package/lib/GameServerState.js +26 -0
- package/lib/HeadlessRuntime.d.ts +316 -0
- package/lib/HeadlessRuntime.js +380 -0
- package/lib/JoinGate.d.ts +34 -0
- package/lib/JoinGate.js +221 -0
- package/lib/NetworkAddresses.d.ts +33 -0
- package/lib/NetworkAddresses.js +90 -0
- package/lib/PlayerTokens.d.ts +17 -0
- package/lib/PlayerTokens.js +24 -0
- package/lib/RoomServerTransport.d.ts +73 -0
- package/lib/RoomServerTransport.js +90 -0
- package/lib/ServerEndpoints.d.ts +133 -0
- package/lib/ServerEndpoints.js +247 -0
- package/lib/ServerInit.d.ts +28 -0
- package/lib/ServerInit.js +113 -0
- package/lib/SessionRoom.d.ts +391 -0
- package/lib/SessionRoom.js +930 -0
- package/lib/TlsCertificates.d.ts +35 -0
- package/lib/TlsCertificates.js +130 -0
- package/lib/admin/AccountRoutes.d.ts +8 -0
- package/lib/admin/AccountRoutes.js +502 -0
- package/lib/admin/AdminAccounts.d.ts +329 -0
- package/lib/admin/AdminAccounts.js +607 -0
- package/lib/admin/AdminApiRouter.d.ts +202 -0
- package/lib/admin/AdminApiRouter.js +465 -0
- package/lib/admin/AdminApiTypes.d.ts +1782 -0
- package/lib/admin/AdminApiTypes.js +4 -0
- package/lib/admin/AdminAudit.d.ts +113 -0
- package/lib/admin/AdminAudit.js +266 -0
- package/lib/admin/AdminCli.d.ts +7 -0
- package/lib/admin/AdminCli.js +351 -0
- package/lib/admin/AdminClock.d.ts +51 -0
- package/lib/admin/AdminClock.js +10 -0
- package/lib/admin/AdminConsole.d.ts +39 -0
- package/lib/admin/AdminConsole.js +325 -0
- package/lib/admin/AdminConstants.d.ts +147 -0
- package/lib/admin/AdminConstants.js +167 -0
- package/lib/admin/AdminCrypto.d.ts +19 -0
- package/lib/admin/AdminCrypto.js +29 -0
- package/lib/admin/AdminExtensionsHost.d.ts +176 -0
- package/lib/admin/AdminExtensionsHost.js +307 -0
- package/lib/admin/AdminPasswords.d.ts +33 -0
- package/lib/admin/AdminPasswords.js +92 -0
- package/lib/admin/AdminPermissions.d.ts +28 -0
- package/lib/admin/AdminPermissions.js +74 -0
- package/lib/admin/AdminRecords.d.ts +196 -0
- package/lib/admin/AdminRecords.js +3 -0
- package/lib/admin/AdminRequestBodies.d.ts +47 -0
- package/lib/admin/AdminRequestBodies.js +84 -0
- package/lib/admin/AdminRequestGate.d.ts +38 -0
- package/lib/admin/AdminRequestGate.js +59 -0
- package/lib/admin/AdminResponses.d.ts +103 -0
- package/lib/admin/AdminResponses.js +164 -0
- package/lib/admin/AdminServices.d.ts +99 -0
- package/lib/admin/AdminServices.js +4 -0
- package/lib/admin/AdminStaticFiles.d.ts +29 -0
- package/lib/admin/AdminStaticFiles.js +97 -0
- package/lib/admin/AdminStore.d.ts +152 -0
- package/lib/admin/AdminStore.js +46 -0
- package/lib/admin/AlertChannels.d.ts +43 -0
- package/lib/admin/AlertChannels.js +120 -0
- package/lib/admin/AlertEngine.d.ts +231 -0
- package/lib/admin/AlertEngine.js +681 -0
- package/lib/admin/AlertRoutes.d.ts +8 -0
- package/lib/admin/AlertRoutes.js +180 -0
- package/lib/admin/ApiKeyRoutes.d.ts +8 -0
- package/lib/admin/ApiKeyRoutes.js +53 -0
- package/lib/admin/ApiKeys.d.ts +63 -0
- package/lib/admin/ApiKeys.js +167 -0
- package/lib/admin/AuditRoutes.d.ts +8 -0
- package/lib/admin/AuditRoutes.js +106 -0
- package/lib/admin/AuthToolRoutes.d.ts +13 -0
- package/lib/admin/AuthToolRoutes.js +164 -0
- package/lib/admin/BanList.d.ts +87 -0
- package/lib/admin/BanList.js +152 -0
- package/lib/admin/CborDecoder.d.ts +18 -0
- package/lib/admin/CborDecoder.js +201 -0
- package/lib/admin/ColyseusCloudClient.d.ts +313 -0
- package/lib/admin/ColyseusCloudClient.js +272 -0
- package/lib/admin/ColyseusCloudRoutes.d.ts +8 -0
- package/lib/admin/ColyseusCloudRoutes.js +91 -0
- package/lib/admin/ColyseusCloudService.d.ts +180 -0
- package/lib/admin/ColyseusCloudService.js +476 -0
- package/lib/admin/ColyseusCloudSettings.d.ts +16 -0
- package/lib/admin/ColyseusCloudSettings.js +29 -0
- package/lib/admin/ColyseusCloudViews.d.ts +105 -0
- package/lib/admin/ColyseusCloudViews.js +371 -0
- package/lib/admin/CommonPasswords.d.ts +6 -0
- package/lib/admin/CommonPasswords.js +19 -0
- package/lib/admin/ConfigOverrides.d.ts +174 -0
- package/lib/admin/ConfigOverrides.js +406 -0
- package/lib/admin/ConfigRoutes.d.ts +8 -0
- package/lib/admin/ConfigRoutes.js +91 -0
- package/lib/admin/ConsoleSessions.d.ts +112 -0
- package/lib/admin/ConsoleSessions.js +201 -0
- package/lib/admin/DedicatedRoutes.d.ts +21 -0
- package/lib/admin/DedicatedRoutes.js +254 -0
- package/lib/admin/ExtensionRoutes.d.ts +8 -0
- package/lib/admin/ExtensionRoutes.js +145 -0
- package/lib/admin/FileAdminStore.d.ts +157 -0
- package/lib/admin/FileAdminStore.js +349 -0
- package/lib/admin/FleetDirectory.d.ts +323 -0
- package/lib/admin/FleetDirectory.js +489 -0
- package/lib/admin/FleetLeader.d.ts +43 -0
- package/lib/admin/FleetLeader.js +89 -0
- package/lib/admin/FleetRoutes.d.ts +55 -0
- package/lib/admin/FleetRoutes.js +465 -0
- package/lib/admin/HealthWarnings.d.ts +10 -0
- package/lib/admin/HealthWarnings.js +95 -0
- package/lib/admin/LeaderboardRoutes.d.ts +8 -0
- package/lib/admin/LeaderboardRoutes.js +569 -0
- package/lib/admin/LogBuffer.d.ts +60 -0
- package/lib/admin/LogBuffer.js +92 -0
- package/lib/admin/LogRedaction.d.ts +15 -0
- package/lib/admin/LogRedaction.js +72 -0
- package/lib/admin/LogRoutes.d.ts +45 -0
- package/lib/admin/LogRoutes.js +307 -0
- package/lib/admin/MaintenanceMode.d.ts +41 -0
- package/lib/admin/MaintenanceMode.js +74 -0
- package/lib/admin/MaintenanceWindows.d.ts +91 -0
- package/lib/admin/MaintenanceWindows.js +207 -0
- package/lib/admin/MetricsRoutes.d.ts +8 -0
- package/lib/admin/MetricsRoutes.js +209 -0
- package/lib/admin/MetricsSampler.d.ts +124 -0
- package/lib/admin/MetricsSampler.js +211 -0
- package/lib/admin/OneTimeCodes.d.ts +60 -0
- package/lib/admin/OneTimeCodes.js +162 -0
- package/lib/admin/OpenIdConnect.d.ts +146 -0
- package/lib/admin/OpenIdConnect.js +335 -0
- package/lib/admin/Passkeys.d.ts +130 -0
- package/lib/admin/Passkeys.js +432 -0
- package/lib/admin/PlayerRoutes.d.ts +20 -0
- package/lib/admin/PlayerRoutes.js +676 -0
- package/lib/admin/RedisAdminStore.d.ts +114 -0
- package/lib/admin/RedisAdminStore.js +258 -0
- package/lib/admin/RolloutCoordinator.d.ts +99 -0
- package/lib/admin/RolloutCoordinator.js +321 -0
- package/lib/admin/ServerEvents.d.ts +57 -0
- package/lib/admin/ServerEvents.js +89 -0
- package/lib/admin/SessionRoutes.d.ts +113 -0
- package/lib/admin/SessionRoutes.js +386 -0
- package/lib/admin/UserRoutes.d.ts +8 -0
- package/lib/admin/UserRoutes.js +214 -0
- package/lib/cli.d.ts +2 -0
- package/lib/cli.js +221 -0
- package/lib/console/assets/index-CI5N7c-T.js +21 -0
- package/lib/console/assets/index-DPitvzLp.css +1 -0
- package/lib/console/favicon.svg +1 -0
- package/lib/console/index.html +16 -0
- package/lib/discord/DiscordOAuth.d.ts +57 -0
- package/lib/discord/DiscordOAuth.js +127 -0
- package/lib/discord/DiscordSessions.d.ts +79 -0
- package/lib/discord/DiscordSessions.js +135 -0
- package/lib/discord/DiscordSignInConstants.d.ts +54 -0
- package/lib/discord/DiscordSignInConstants.js +55 -0
- package/lib/discord/DiscordSignInRoutes.d.ts +51 -0
- package/lib/discord/DiscordSignInRoutes.js +359 -0
- package/lib/index.d.ts +24 -0
- package/lib/index.js +18 -0
- package/lib/leaderboards/FileLeaderboardRankStore.d.ts +293 -0
- package/lib/leaderboards/FileLeaderboardRankStore.js +808 -0
- package/lib/leaderboards/LeaderboardConstants.d.ts +63 -0
- package/lib/leaderboards/LeaderboardConstants.js +70 -0
- package/lib/leaderboards/LeaderboardError.d.ts +15 -0
- package/lib/leaderboards/LeaderboardError.js +20 -0
- package/lib/leaderboards/LeaderboardIdentity.d.ts +44 -0
- package/lib/leaderboards/LeaderboardIdentity.js +72 -0
- package/lib/leaderboards/LeaderboardPublicApi.d.ts +17 -0
- package/lib/leaderboards/LeaderboardPublicApi.js +389 -0
- package/lib/leaderboards/LeaderboardRankStore.d.ts +296 -0
- package/lib/leaderboards/LeaderboardRankStore.js +18 -0
- package/lib/leaderboards/LeaderboardRules.d.ts +122 -0
- package/lib/leaderboards/LeaderboardRules.js +472 -0
- package/lib/leaderboards/LeaderboardScheduler.d.ts +43 -0
- package/lib/leaderboards/LeaderboardScheduler.js +103 -0
- package/lib/leaderboards/LeaderboardService.d.ts +669 -0
- package/lib/leaderboards/LeaderboardService.js +1667 -0
- package/lib/leaderboards/LeaderboardTransfer.d.ts +35 -0
- package/lib/leaderboards/LeaderboardTransfer.js +580 -0
- package/lib/leaderboards/RedisLeaderboardRankStore.d.ts +282 -0
- package/lib/leaderboards/RedisLeaderboardRankStore.js +691 -0
- package/package.json +64 -4
- package/templates/init/Caddyfile +6 -0
- package/templates/init/DEPLOY.md +233 -0
- package/templates/init/Dockerfile +12 -0
- package/templates/init/app.json +21 -0
- package/templates/init/aws/README.md +118 -0
- package/templates/init/aws/ecs-fargate-alb.yml +207 -0
- package/templates/init/aws/ecs-fargate-nlb-quic.yml +210 -0
- package/templates/init/babylon-game-server.config.mjs +158 -0
- package/templates/init/colyseus-cloud/README.md +133 -0
- package/templates/init/colyseus-cloud/babylon-game-server.config.mjs +36 -0
- package/templates/init/colyseus-cloud/ecosystem.config.cjs +35 -0
- package/templates/init/colyseus-cloud/gitignore +6 -0
- package/templates/init/colyseus-cloud/package.json +15 -0
- package/templates/init/digitalocean-app.yaml +30 -0
- package/templates/init/docker-compose.full.yml +45 -0
- package/templates/init/docker-compose.websocket.yml +41 -0
- package/templates/init/fly.toml +56 -0
- package/templates/init/heroku.yml +11 -0
- package/templates/init/kubernetes/deployment.yaml +71 -0
- package/templates/init/kubernetes/redis.yaml +73 -0
- package/templates/init/kubernetes/service.yaml +24 -0
- package/templates/init/monitoring/grafana-dashboard.json +539 -0
- package/templates/init/monitoring/prometheus-alerts.yml +51 -0
- package/templates/init/monitoring/prometheus-scrape.yml +17 -0
- package/templates/init/railway.json +13 -0
- package/templates/init/render.yaml +19 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// The extensions and docs routes (game-server-admin-console DR §6.11 extensions/* and docs rows, D25): the custom
|
|
2
|
+
// actions and panels a caller may use, running an action with its own permission, a panel's content, and the package's
|
|
3
|
+
// README, DEPLOY and CUSTOM documents for the console's help pages.
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { AdminRequestError } from "./AdminApiRouter.js";
|
|
7
|
+
import { bodyObject } from "./AdminRequestBodies.js";
|
|
8
|
+
/** HTTP status of a successful answer. */
|
|
9
|
+
const HTTP_OK = 200;
|
|
10
|
+
/** HTTP statuses of the refusals. */
|
|
11
|
+
const HTTP_BAD_REQUEST = 400;
|
|
12
|
+
const HTTP_UNAUTHORIZED = 401;
|
|
13
|
+
const HTTP_NOT_FOUND = 404;
|
|
14
|
+
/** The package root (SVR/), two folders above lib/admin/. */
|
|
15
|
+
const PACKAGE_ROOT = new URL("../../", import.meta.url);
|
|
16
|
+
/** The documents GET docs/:documentName may read. */
|
|
17
|
+
const READABLE_DOCUMENTS = new Set(["README.md", "DEPLOY.md", "CUSTOM.md"]);
|
|
18
|
+
/**
|
|
19
|
+
* Registers the extensions/* and docs routes.
|
|
20
|
+
* @param router The admin API router.
|
|
21
|
+
* @param services The shared services.
|
|
22
|
+
*/
|
|
23
|
+
export function registerExtensionRoutes(router, services) {
|
|
24
|
+
const routes = [
|
|
25
|
+
{ method: "GET", pattern: "extensions", permission: "signedIn", requiresStepUp: false, auditAction: null, handler: async (request) => ok(services.extensions.listFor(requireCaller(request))) },
|
|
26
|
+
{
|
|
27
|
+
method: "POST",
|
|
28
|
+
pattern: "extensions/actions/:actionName",
|
|
29
|
+
permission: (request) => services.extensions.permissionFor(request.params.actionName),
|
|
30
|
+
requiresStepUp: false,
|
|
31
|
+
auditAction: (request) => "extension." + request.params.actionName,
|
|
32
|
+
handler: async (request) => runAction(services, request)
|
|
33
|
+
},
|
|
34
|
+
{ method: "GET", pattern: "extensions/panels/:panelName", permission: "signedIn", requiresStepUp: false, auditAction: null, handler: async (request) => ok(await services.extensions.panel(request.params.panelName, targetIdsOfQuery(request.query))) },
|
|
35
|
+
{ method: "GET", pattern: "docs/:documentName", permission: "signedIn", requiresStepUp: false, auditAction: null, handler: async (request) => ok(await readDocument(request.params.documentName)) }
|
|
36
|
+
];
|
|
37
|
+
for (const route of routes)
|
|
38
|
+
router.register(route);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* A 200 answer.
|
|
42
|
+
* @param body The JSON body.
|
|
43
|
+
* @returns The result.
|
|
44
|
+
*/
|
|
45
|
+
function ok(body) {
|
|
46
|
+
return { status: HTTP_OK, body: body };
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* POST extensions/actions/:actionName: runs the action; the audit record has its inputs without secret fields.
|
|
50
|
+
* @param services The shared services.
|
|
51
|
+
* @param request The request.
|
|
52
|
+
* @returns The action's outcome.
|
|
53
|
+
*/
|
|
54
|
+
async function runAction(services, request) {
|
|
55
|
+
const body = bodyObject(request.body);
|
|
56
|
+
const target = targetIdsOfBody(body.target);
|
|
57
|
+
const inputs = bodyObject(body.inputs);
|
|
58
|
+
const actionName = request.params.actionName;
|
|
59
|
+
const result = await services.extensions.run(actionName, target, inputs, requireCaller(request));
|
|
60
|
+
const parameters = { target: target, inputs: services.extensions.auditParametersOf(actionName, inputs), ok: result.ok };
|
|
61
|
+
return { status: HTTP_OK, body: result, audit: { target: auditTargetOf(target, actionName), parameters: parameters } };
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* GET docs/:documentName: one of the package's documents.
|
|
65
|
+
* @param documentName README.md, DEPLOY.md or CUSTOM.md.
|
|
66
|
+
* @returns The markdown.
|
|
67
|
+
* @throws AdminRequestError 404 for another name or a missing file.
|
|
68
|
+
*/
|
|
69
|
+
async function readDocument(documentName) {
|
|
70
|
+
if (!READABLE_DOCUMENTS.has(documentName))
|
|
71
|
+
throw new AdminRequestError(HTTP_NOT_FOUND, "no such document");
|
|
72
|
+
try {
|
|
73
|
+
const markdown = await readFile(fileURLToPath(new URL(documentName, PACKAGE_ROOT)), "utf8");
|
|
74
|
+
return { markdown: markdown };
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
throw new AdminRequestError(HTTP_NOT_FOUND, "no such document");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The caller of a route that needs one (the router has checked it).
|
|
82
|
+
* @param request The request.
|
|
83
|
+
* @returns The caller.
|
|
84
|
+
* @throws AdminRequestError 401 without one.
|
|
85
|
+
*/
|
|
86
|
+
function requireCaller(request) {
|
|
87
|
+
if (request.caller == null)
|
|
88
|
+
throw new AdminRequestError(HTTP_UNAUTHORIZED, "sign in first");
|
|
89
|
+
return request.caller;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The target ids of an action body.
|
|
93
|
+
* @param value body.target.
|
|
94
|
+
* @returns The ids (only the known, well-typed ones).
|
|
95
|
+
* @throws AdminRequestError 400 when it is not an object.
|
|
96
|
+
*/
|
|
97
|
+
function targetIdsOfBody(value) {
|
|
98
|
+
if (value == null)
|
|
99
|
+
return {};
|
|
100
|
+
if (typeof value !== "object" || Array.isArray(value))
|
|
101
|
+
throw new AdminRequestError(HTTP_BAD_REQUEST, "target must be an object");
|
|
102
|
+
const fields = value;
|
|
103
|
+
const target = {};
|
|
104
|
+
if (typeof fields.processId === "string")
|
|
105
|
+
target.processId = fields.processId;
|
|
106
|
+
if (typeof fields.roomId === "string")
|
|
107
|
+
target.roomId = fields.roomId;
|
|
108
|
+
if (typeof fields.playerId === "number")
|
|
109
|
+
target.playerId = fields.playerId;
|
|
110
|
+
if (typeof fields.userId === "string" || fields.userId === null)
|
|
111
|
+
target.userId = fields.userId;
|
|
112
|
+
return target;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The target ids of a panel query (processId, roomId, playerId, userId).
|
|
116
|
+
* @param query The query.
|
|
117
|
+
* @returns The ids.
|
|
118
|
+
*/
|
|
119
|
+
function targetIdsOfQuery(query) {
|
|
120
|
+
const target = {};
|
|
121
|
+
const processId = query.get("processId");
|
|
122
|
+
const roomId = query.get("roomId");
|
|
123
|
+
const playerId = Number(query.get("playerId"));
|
|
124
|
+
const userId = query.get("userId");
|
|
125
|
+
if (processId != null)
|
|
126
|
+
target.processId = processId;
|
|
127
|
+
if (roomId != null)
|
|
128
|
+
target.roomId = roomId;
|
|
129
|
+
if (query.has("playerId") && Number.isInteger(playerId))
|
|
130
|
+
target.playerId = playerId;
|
|
131
|
+
if (userId != null)
|
|
132
|
+
target.userId = userId;
|
|
133
|
+
return target;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* The audit target of an action call: the most specific target id, or "server".
|
|
137
|
+
* @param target The target ids.
|
|
138
|
+
* @param actionName The action (the target of a server action).
|
|
139
|
+
* @returns The target text.
|
|
140
|
+
*/
|
|
141
|
+
function auditTargetOf(target, actionName) {
|
|
142
|
+
if (target.roomId != null && target.playerId != null)
|
|
143
|
+
return target.roomId + "/" + target.playerId;
|
|
144
|
+
return target.roomId ?? target.processId ?? actionName;
|
|
145
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { type AdminClock } from "./AdminClock.js";
|
|
2
|
+
import { type AdminCollection, type ChainedRecordBuilder, type AdminStore, type AdminStream, type RecordQuery, type StoredDocument, type StoredRecord } from "./AdminStore.js";
|
|
3
|
+
/** Admin data in JSON files (single instance, D7). */
|
|
4
|
+
export declare class FileAdminStore implements AdminStore {
|
|
5
|
+
/** Always "file". */
|
|
6
|
+
readonly kind: "file";
|
|
7
|
+
/** False when the folder sits on a container's own filesystem, so its data is lost on restart. */
|
|
8
|
+
readonly isPersistent: boolean;
|
|
9
|
+
/** The absolute data folder. */
|
|
10
|
+
private readonly rootFolder;
|
|
11
|
+
/** The clock of `updatedAt` and `at`. */
|
|
12
|
+
private readonly clock;
|
|
13
|
+
/** The pending write of each file, so writes to one file run one at a time. */
|
|
14
|
+
private readonly writeChains;
|
|
15
|
+
/** The cached last sequence of each stream. */
|
|
16
|
+
private readonly streamPositions;
|
|
17
|
+
/**
|
|
18
|
+
* Opens the store; nothing is read or created yet.
|
|
19
|
+
* @param dataDir The data folder (relative to the working directory).
|
|
20
|
+
* @param clock The clock of stored times.
|
|
21
|
+
*/
|
|
22
|
+
constructor(dataDir: string, clock?: AdminClock);
|
|
23
|
+
/**
|
|
24
|
+
* Reads one document.
|
|
25
|
+
* @param collection The collection.
|
|
26
|
+
* @param documentId The document id.
|
|
27
|
+
* @returns The document, or null.
|
|
28
|
+
*/
|
|
29
|
+
getDocument<Value>(collection: AdminCollection, documentId: string): Promise<StoredDocument<Value> | null>;
|
|
30
|
+
/**
|
|
31
|
+
* Reads every document of a collection.
|
|
32
|
+
* @param collection The collection.
|
|
33
|
+
* @returns The documents.
|
|
34
|
+
*/
|
|
35
|
+
listDocuments<Value>(collection: AdminCollection): Promise<StoredDocument<Value>[]>;
|
|
36
|
+
/**
|
|
37
|
+
* Writes a document after checking its revision.
|
|
38
|
+
* @param collection The collection.
|
|
39
|
+
* @param documentId The document id.
|
|
40
|
+
* @param value The document.
|
|
41
|
+
* @param expectedRevision The revision read, or null for no check.
|
|
42
|
+
* @returns The stored document.
|
|
43
|
+
* @throws AdminConflictError on a stale expectedRevision.
|
|
44
|
+
*/
|
|
45
|
+
putDocument<Value>(collection: AdminCollection, documentId: string, value: Value, expectedRevision: number | null): Promise<StoredDocument<Value>>;
|
|
46
|
+
/**
|
|
47
|
+
* Deletes a document.
|
|
48
|
+
* @param collection The collection.
|
|
49
|
+
* @param documentId The document id.
|
|
50
|
+
* @returns True when it existed.
|
|
51
|
+
*/
|
|
52
|
+
deleteDocument(collection: AdminCollection, documentId: string): Promise<boolean>;
|
|
53
|
+
/**
|
|
54
|
+
* Appends a record with the stream's next sequence.
|
|
55
|
+
* @param stream The stream.
|
|
56
|
+
* @param value The record.
|
|
57
|
+
* @returns The stored record.
|
|
58
|
+
*/
|
|
59
|
+
appendRecord<Value>(stream: AdminStream, value: Value): Promise<StoredRecord<Value>>;
|
|
60
|
+
/**
|
|
61
|
+
* Appends a record built from the newest one; the per-file write chain makes it atomic in this process (R21).
|
|
62
|
+
* @param stream The stream.
|
|
63
|
+
* @param build Builds the value.
|
|
64
|
+
* @returns The stored record.
|
|
65
|
+
*/
|
|
66
|
+
appendChainedRecord<Value>(stream: AdminStream, build: ChainedRecordBuilder<Value>): Promise<StoredRecord<Value>>;
|
|
67
|
+
/**
|
|
68
|
+
* Appends a built record after the pending writes of its file.
|
|
69
|
+
* @param stream The stream.
|
|
70
|
+
* @param build Builds the value.
|
|
71
|
+
* @param needsPrevious True to read the newest record for the builder (chained appends); plain appends skip the read.
|
|
72
|
+
* @returns The stored record.
|
|
73
|
+
*/
|
|
74
|
+
private appendBuilt;
|
|
75
|
+
/**
|
|
76
|
+
* Reads records of a stream.
|
|
77
|
+
* @param stream The stream.
|
|
78
|
+
* @param query The range, limit and order.
|
|
79
|
+
* @returns The records.
|
|
80
|
+
*/
|
|
81
|
+
readRecords<Value>(stream: AdminStream, query: RecordQuery): Promise<StoredRecord<Value>[]>;
|
|
82
|
+
/**
|
|
83
|
+
* Removes records appended before the cutoff.
|
|
84
|
+
* @param stream The stream.
|
|
85
|
+
* @param cutoff The oldest time kept.
|
|
86
|
+
* @returns How many were removed.
|
|
87
|
+
*/
|
|
88
|
+
trimRecordsBefore(stream: AdminStream, cutoff: Date): Promise<number>;
|
|
89
|
+
/**
|
|
90
|
+
* Keeps only the newest records.
|
|
91
|
+
* @param stream The stream.
|
|
92
|
+
* @param keepCount How many to keep.
|
|
93
|
+
* @returns How many were removed.
|
|
94
|
+
*/
|
|
95
|
+
trimRecordsToCount(stream: AdminStream, keepCount: number): Promise<number>;
|
|
96
|
+
/**
|
|
97
|
+
* A file store serves one instance, so its holder always holds every lease.
|
|
98
|
+
* @returns True.
|
|
99
|
+
*/
|
|
100
|
+
tryAcquireLease(): Promise<boolean>;
|
|
101
|
+
/** Waits for pending writes; nothing else is held open. */
|
|
102
|
+
close(): Promise<void>;
|
|
103
|
+
/**
|
|
104
|
+
* Runs a task after the pending writes of one file, so a read-modify-write never interleaves with another.
|
|
105
|
+
* @param fileKey The file the task writes.
|
|
106
|
+
* @param task The task.
|
|
107
|
+
* @returns The task's result.
|
|
108
|
+
*/
|
|
109
|
+
private serialize;
|
|
110
|
+
/**
|
|
111
|
+
* Reads a collection file (empty when it does not exist yet).
|
|
112
|
+
* @param collection The collection.
|
|
113
|
+
* @returns Document id → entry.
|
|
114
|
+
*/
|
|
115
|
+
private readCollection;
|
|
116
|
+
/**
|
|
117
|
+
* Writes a collection file atomically: a temporary file renamed over the old one.
|
|
118
|
+
* @param collection The collection.
|
|
119
|
+
* @param entries Document id → entry.
|
|
120
|
+
*/
|
|
121
|
+
private writeCollection;
|
|
122
|
+
/**
|
|
123
|
+
* Reads every record of a stream, oldest first.
|
|
124
|
+
* @param stream The stream.
|
|
125
|
+
* @returns The records.
|
|
126
|
+
*/
|
|
127
|
+
private readStream;
|
|
128
|
+
/**
|
|
129
|
+
* Replaces a stream's records with a filtered list, remembering the last sequence so it never restarts.
|
|
130
|
+
* @param stream The stream.
|
|
131
|
+
* @param keep Picks the records kept from the oldest-first list.
|
|
132
|
+
* @returns How many were removed.
|
|
133
|
+
*/
|
|
134
|
+
private rewriteStream;
|
|
135
|
+
/**
|
|
136
|
+
* The last sequence of a stream: cached while the file is as this store left it, else read from the file (the CLI
|
|
137
|
+
* may have appended) and the sequence file of an earlier trim.
|
|
138
|
+
* @param stream The stream.
|
|
139
|
+
* @returns The last sequence, or 0 for a new stream.
|
|
140
|
+
*/
|
|
141
|
+
private lastSequence;
|
|
142
|
+
/**
|
|
143
|
+
* @param collection The collection.
|
|
144
|
+
* @returns The path of its file.
|
|
145
|
+
*/
|
|
146
|
+
private collectionFile;
|
|
147
|
+
/**
|
|
148
|
+
* @param stream The stream.
|
|
149
|
+
* @returns The path of its file (the name is URI-encoded, since metrics streams contain a colon).
|
|
150
|
+
*/
|
|
151
|
+
private streamFile;
|
|
152
|
+
/**
|
|
153
|
+
* @param stream The stream.
|
|
154
|
+
* @returns The path of its sequence file.
|
|
155
|
+
*/
|
|
156
|
+
private sequenceFile;
|
|
157
|
+
}
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
// The file backend of the admin store (game-server-admin-console D7, DR §6.1): one JSON file per collection and one
|
|
2
|
+
// JSON-lines file per stream under admin.dataDir. It creates nothing until its first write (R3).
|
|
3
|
+
import { existsSync, statSync } from "node:fs";
|
|
4
|
+
import { appendFile, mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { SYSTEM_CLOCK } from "./AdminClock.js";
|
|
7
|
+
import { AdminConflictError, clampRecordLimit } from "./AdminStore.js";
|
|
8
|
+
/** Folder of the collection files. */
|
|
9
|
+
const COLLECTIONS_FOLDER = "collections";
|
|
10
|
+
/** Folder of the stream files. */
|
|
11
|
+
const STREAMS_FOLDER = "streams";
|
|
12
|
+
/** Extension of a collection file. */
|
|
13
|
+
const COLLECTION_EXTENSION = ".json";
|
|
14
|
+
/** Extension of a stream file. */
|
|
15
|
+
const STREAM_EXTENSION = ".jsonl";
|
|
16
|
+
/** Extension of the file that remembers a stream's last sequence after a trim emptied it. */
|
|
17
|
+
const SEQUENCE_EXTENSION = ".seq";
|
|
18
|
+
/** Marker file Docker creates in every container. */
|
|
19
|
+
const DOCKER_MARKER_FILE = "/.dockerenv";
|
|
20
|
+
/** Environment variable Kubernetes sets in every pod. */
|
|
21
|
+
const KUBERNETES_VARIABLE = "KUBERNETES_SERVICE_HOST";
|
|
22
|
+
/** The container's own root folder. */
|
|
23
|
+
const FILESYSTEM_ROOT = "/";
|
|
24
|
+
/** Node's error code of a missing file. */
|
|
25
|
+
const FILE_MISSING_CODE = "ENOENT";
|
|
26
|
+
/** Admin data in JSON files (single instance, D7). */
|
|
27
|
+
export class FileAdminStore {
|
|
28
|
+
/** Always "file". */
|
|
29
|
+
kind = "file";
|
|
30
|
+
/** False when the folder sits on a container's own filesystem, so its data is lost on restart. */
|
|
31
|
+
isPersistent;
|
|
32
|
+
/** The absolute data folder. */
|
|
33
|
+
rootFolder;
|
|
34
|
+
/** The clock of `updatedAt` and `at`. */
|
|
35
|
+
clock;
|
|
36
|
+
/** The pending write of each file, so writes to one file run one at a time. */
|
|
37
|
+
writeChains = new Map();
|
|
38
|
+
/** The cached last sequence of each stream. */
|
|
39
|
+
streamPositions = new Map();
|
|
40
|
+
/**
|
|
41
|
+
* Opens the store; nothing is read or created yet.
|
|
42
|
+
* @param dataDir The data folder (relative to the working directory).
|
|
43
|
+
* @param clock The clock of stored times.
|
|
44
|
+
*/
|
|
45
|
+
constructor(dataDir, clock = SYSTEM_CLOCK) {
|
|
46
|
+
this.rootFolder = path.resolve(dataDir);
|
|
47
|
+
this.clock = clock;
|
|
48
|
+
this.isPersistent = !(isInsideContainer() && isOnContainerFilesystem(this.rootFolder));
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Reads one document.
|
|
52
|
+
* @param collection The collection.
|
|
53
|
+
* @param documentId The document id.
|
|
54
|
+
* @returns The document, or null.
|
|
55
|
+
*/
|
|
56
|
+
async getDocument(collection, documentId) {
|
|
57
|
+
const entries = await this.readCollection(collection);
|
|
58
|
+
const entry = entries[documentId];
|
|
59
|
+
return (entry != null) ? toDocument(documentId, entry) : null;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Reads every document of a collection.
|
|
63
|
+
* @param collection The collection.
|
|
64
|
+
* @returns The documents.
|
|
65
|
+
*/
|
|
66
|
+
async listDocuments(collection) {
|
|
67
|
+
const entries = await this.readCollection(collection);
|
|
68
|
+
return Object.entries(entries).map(([documentId, entry]) => toDocument(documentId, entry));
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Writes a document after checking its revision.
|
|
72
|
+
* @param collection The collection.
|
|
73
|
+
* @param documentId The document id.
|
|
74
|
+
* @param value The document.
|
|
75
|
+
* @param expectedRevision The revision read, or null for no check.
|
|
76
|
+
* @returns The stored document.
|
|
77
|
+
* @throws AdminConflictError on a stale expectedRevision.
|
|
78
|
+
*/
|
|
79
|
+
putDocument(collection, documentId, value, expectedRevision) {
|
|
80
|
+
return this.serialize(this.collectionFile(collection), async () => {
|
|
81
|
+
const entries = await this.readCollection(collection);
|
|
82
|
+
const current = entries[documentId];
|
|
83
|
+
const currentRevision = current?.revision ?? 0;
|
|
84
|
+
if (expectedRevision != null && expectedRevision !== currentRevision) {
|
|
85
|
+
throw new AdminConflictError((current != null) ? toDocument(documentId, current) : null);
|
|
86
|
+
}
|
|
87
|
+
const entry = { revision: currentRevision + 1, updatedAt: new Date(this.clock.now()).toISOString(), value: value };
|
|
88
|
+
entries[documentId] = entry;
|
|
89
|
+
await this.writeCollection(collection, entries);
|
|
90
|
+
return toDocument(documentId, entry);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Deletes a document.
|
|
95
|
+
* @param collection The collection.
|
|
96
|
+
* @param documentId The document id.
|
|
97
|
+
* @returns True when it existed.
|
|
98
|
+
*/
|
|
99
|
+
deleteDocument(collection, documentId) {
|
|
100
|
+
return this.serialize(this.collectionFile(collection), async () => {
|
|
101
|
+
const entries = await this.readCollection(collection);
|
|
102
|
+
if (entries[documentId] == null)
|
|
103
|
+
return false;
|
|
104
|
+
delete entries[documentId];
|
|
105
|
+
await this.writeCollection(collection, entries);
|
|
106
|
+
return true;
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Appends a record with the stream's next sequence.
|
|
111
|
+
* @param stream The stream.
|
|
112
|
+
* @param value The record.
|
|
113
|
+
* @returns The stored record.
|
|
114
|
+
*/
|
|
115
|
+
appendRecord(stream, value) {
|
|
116
|
+
return this.appendBuilt(stream, () => value, false);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Appends a record built from the newest one; the per-file write chain makes it atomic in this process (R21).
|
|
120
|
+
* @param stream The stream.
|
|
121
|
+
* @param build Builds the value.
|
|
122
|
+
* @returns The stored record.
|
|
123
|
+
*/
|
|
124
|
+
appendChainedRecord(stream, build) {
|
|
125
|
+
return this.appendBuilt(stream, build, true);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Appends a built record after the pending writes of its file.
|
|
129
|
+
* @param stream The stream.
|
|
130
|
+
* @param build Builds the value.
|
|
131
|
+
* @param needsPrevious True to read the newest record for the builder (chained appends); plain appends skip the read.
|
|
132
|
+
* @returns The stored record.
|
|
133
|
+
*/
|
|
134
|
+
appendBuilt(stream, build, needsPrevious) {
|
|
135
|
+
const filePath = this.streamFile(stream);
|
|
136
|
+
return this.serialize(filePath, async () => {
|
|
137
|
+
const lastSequence = await this.lastSequence(stream);
|
|
138
|
+
const records = needsPrevious ? await this.readStream(stream) : [];
|
|
139
|
+
const previous = (records.length > 0) ? records[records.length - 1] : null;
|
|
140
|
+
const sequence = lastSequence + 1;
|
|
141
|
+
const at = new Date(this.clock.now()).toISOString();
|
|
142
|
+
const record = { sequence: sequence, at: at, value: build(previous, sequence, at) };
|
|
143
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
144
|
+
await appendFile(filePath, JSON.stringify(record) + "\n", "utf8");
|
|
145
|
+
const fileStats = await stat(filePath);
|
|
146
|
+
this.streamPositions.set(stream, { sequence: record.sequence, byteLength: fileStats.size });
|
|
147
|
+
return record;
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Reads records of a stream.
|
|
152
|
+
* @param stream The stream.
|
|
153
|
+
* @param query The range, limit and order.
|
|
154
|
+
* @returns The records.
|
|
155
|
+
*/
|
|
156
|
+
async readRecords(stream, query) {
|
|
157
|
+
// Appends are not atomic, so reads wait for them and never see half a line.
|
|
158
|
+
const records = await this.serialize(this.streamFile(stream), () => this.readStream(stream));
|
|
159
|
+
const inRange = records.filter((record) => (query.afterSequence == null || record.sequence > query.afterSequence) && (query.beforeSequence == null || record.sequence < query.beforeSequence));
|
|
160
|
+
if (query.newestFirst)
|
|
161
|
+
inRange.reverse();
|
|
162
|
+
return inRange.slice(0, clampRecordLimit(query.limit));
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Removes records appended before the cutoff.
|
|
166
|
+
* @param stream The stream.
|
|
167
|
+
* @param cutoff The oldest time kept.
|
|
168
|
+
* @returns How many were removed.
|
|
169
|
+
*/
|
|
170
|
+
trimRecordsBefore(stream, cutoff) {
|
|
171
|
+
const cutoffMilliseconds = cutoff.getTime();
|
|
172
|
+
return this.rewriteStream(stream, (records) => records.filter((record) => Date.parse(record.at) >= cutoffMilliseconds));
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Keeps only the newest records.
|
|
176
|
+
* @param stream The stream.
|
|
177
|
+
* @param keepCount How many to keep.
|
|
178
|
+
* @returns How many were removed.
|
|
179
|
+
*/
|
|
180
|
+
trimRecordsToCount(stream, keepCount) {
|
|
181
|
+
return this.rewriteStream(stream, (records) => records.slice(Math.max(0, records.length - Math.max(0, keepCount))));
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* A file store serves one instance, so its holder always holds every lease.
|
|
185
|
+
* @returns True.
|
|
186
|
+
*/
|
|
187
|
+
async tryAcquireLease() {
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
/** Waits for pending writes; nothing else is held open. */
|
|
191
|
+
async close() {
|
|
192
|
+
await Promise.all(Array.from(this.writeChains.values()));
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Runs a task after the pending writes of one file, so a read-modify-write never interleaves with another.
|
|
196
|
+
* @param fileKey The file the task writes.
|
|
197
|
+
* @param task The task.
|
|
198
|
+
* @returns The task's result.
|
|
199
|
+
*/
|
|
200
|
+
serialize(fileKey, task) {
|
|
201
|
+
const previous = this.writeChains.get(fileKey) ?? Promise.resolve();
|
|
202
|
+
const next = previous.then(task, task);
|
|
203
|
+
this.writeChains.set(fileKey, next.catch(() => undefined));
|
|
204
|
+
return next;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Reads a collection file (empty when it does not exist yet).
|
|
208
|
+
* @param collection The collection.
|
|
209
|
+
* @returns Document id → entry.
|
|
210
|
+
*/
|
|
211
|
+
async readCollection(collection) {
|
|
212
|
+
const text = await readTextIfPresent(this.collectionFile(collection));
|
|
213
|
+
return (text != null) ? JSON.parse(text) : {};
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Writes a collection file atomically: a temporary file renamed over the old one.
|
|
217
|
+
* @param collection The collection.
|
|
218
|
+
* @param entries Document id → entry.
|
|
219
|
+
*/
|
|
220
|
+
async writeCollection(collection, entries) {
|
|
221
|
+
await writeFileAtomically(this.collectionFile(collection), JSON.stringify(entries, null, 2));
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Reads every record of a stream, oldest first.
|
|
225
|
+
* @param stream The stream.
|
|
226
|
+
* @returns The records.
|
|
227
|
+
*/
|
|
228
|
+
async readStream(stream) {
|
|
229
|
+
const text = await readTextIfPresent(this.streamFile(stream));
|
|
230
|
+
if (text == null)
|
|
231
|
+
return [];
|
|
232
|
+
return text.split("\n").filter((line) => line.trim() !== "").map((line) => JSON.parse(line));
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Replaces a stream's records with a filtered list, remembering the last sequence so it never restarts.
|
|
236
|
+
* @param stream The stream.
|
|
237
|
+
* @param keep Picks the records kept from the oldest-first list.
|
|
238
|
+
* @returns How many were removed.
|
|
239
|
+
*/
|
|
240
|
+
rewriteStream(stream, keep) {
|
|
241
|
+
const filePath = this.streamFile(stream);
|
|
242
|
+
return this.serialize(filePath, async () => {
|
|
243
|
+
const records = await this.readStream(stream);
|
|
244
|
+
const kept = keep(records);
|
|
245
|
+
const removedCount = records.length - kept.length;
|
|
246
|
+
if (removedCount === 0)
|
|
247
|
+
return 0;
|
|
248
|
+
const lastSequence = await this.lastSequence(stream);
|
|
249
|
+
await writeFileAtomically(this.sequenceFile(stream), String(lastSequence));
|
|
250
|
+
await writeFileAtomically(filePath, kept.map((record) => JSON.stringify(record) + "\n").join(""));
|
|
251
|
+
const fileStats = await stat(filePath);
|
|
252
|
+
this.streamPositions.set(stream, { sequence: lastSequence, byteLength: fileStats.size });
|
|
253
|
+
return removedCount;
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* The last sequence of a stream: cached while the file is as this store left it, else read from the file (the CLI
|
|
258
|
+
* may have appended) and the sequence file of an earlier trim.
|
|
259
|
+
* @param stream The stream.
|
|
260
|
+
* @returns The last sequence, or 0 for a new stream.
|
|
261
|
+
*/
|
|
262
|
+
async lastSequence(stream) {
|
|
263
|
+
const filePath = this.streamFile(stream);
|
|
264
|
+
const cached = this.streamPositions.get(stream);
|
|
265
|
+
const currentSize = existsSync(filePath) ? (await stat(filePath)).size : 0;
|
|
266
|
+
if (cached != null && cached.byteLength === currentSize)
|
|
267
|
+
return cached.sequence;
|
|
268
|
+
const records = await this.readStream(stream);
|
|
269
|
+
const fileSequence = (records.length > 0) ? records[records.length - 1].sequence : 0;
|
|
270
|
+
const trimmedSequence = Number(await readTextIfPresent(this.sequenceFile(stream)) ?? 0);
|
|
271
|
+
return Math.max(fileSequence, trimmedSequence);
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* @param collection The collection.
|
|
275
|
+
* @returns The path of its file.
|
|
276
|
+
*/
|
|
277
|
+
collectionFile(collection) {
|
|
278
|
+
return path.join(this.rootFolder, COLLECTIONS_FOLDER, collection + COLLECTION_EXTENSION);
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* @param stream The stream.
|
|
282
|
+
* @returns The path of its file (the name is URI-encoded, since metrics streams contain a colon).
|
|
283
|
+
*/
|
|
284
|
+
streamFile(stream) {
|
|
285
|
+
return path.join(this.rootFolder, STREAMS_FOLDER, encodeURIComponent(stream) + STREAM_EXTENSION);
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* @param stream The stream.
|
|
289
|
+
* @returns The path of its sequence file.
|
|
290
|
+
*/
|
|
291
|
+
sequenceFile(stream) {
|
|
292
|
+
return path.join(this.rootFolder, STREAMS_FOLDER, encodeURIComponent(stream) + SEQUENCE_EXTENSION);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Builds a document from a collection file entry.
|
|
297
|
+
* @param documentId The id.
|
|
298
|
+
* @param entry The entry.
|
|
299
|
+
* @returns The document.
|
|
300
|
+
*/
|
|
301
|
+
function toDocument(documentId, entry) {
|
|
302
|
+
return { id: documentId, revision: entry.revision, updatedAt: entry.updatedAt, value: entry.value };
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Reads a text file.
|
|
306
|
+
* @param filePath The file.
|
|
307
|
+
* @returns Its text, or null when it does not exist.
|
|
308
|
+
* @throws Any read error other than a missing file.
|
|
309
|
+
*/
|
|
310
|
+
async function readTextIfPresent(filePath) {
|
|
311
|
+
try {
|
|
312
|
+
return await readFile(filePath, "utf8");
|
|
313
|
+
}
|
|
314
|
+
catch (error) {
|
|
315
|
+
if (error.code === FILE_MISSING_CODE)
|
|
316
|
+
return null;
|
|
317
|
+
throw error;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Writes a file through a temporary file and a rename, so a crash never leaves half a file.
|
|
322
|
+
* @param filePath The file.
|
|
323
|
+
* @param text Its new text.
|
|
324
|
+
*/
|
|
325
|
+
async function writeFileAtomically(filePath, text) {
|
|
326
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
327
|
+
const temporaryPath = filePath + ".tmp-" + process.pid;
|
|
328
|
+
await writeFile(temporaryPath, text, "utf8");
|
|
329
|
+
await rename(temporaryPath, filePath);
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Whether this process runs in a container (Docker or Kubernetes).
|
|
333
|
+
* @returns True inside a container.
|
|
334
|
+
*/
|
|
335
|
+
function isInsideContainer() {
|
|
336
|
+
return existsSync(DOCKER_MARKER_FILE) || process.env[KUBERNETES_VARIABLE] != null;
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Whether a folder lives on the container's own filesystem rather than a mounted volume. The folder may not exist yet
|
|
340
|
+
* (R3), so its nearest existing parent is checked.
|
|
341
|
+
* @param folder The absolute folder.
|
|
342
|
+
* @returns True when it shares the root filesystem's device.
|
|
343
|
+
*/
|
|
344
|
+
function isOnContainerFilesystem(folder) {
|
|
345
|
+
let existing = folder;
|
|
346
|
+
while (!existsSync(existing) && path.dirname(existing) !== existing)
|
|
347
|
+
existing = path.dirname(existing);
|
|
348
|
+
return statSync(existing).dev === statSync(FILESYSTEM_ROOT).dev;
|
|
349
|
+
}
|