@babylonjs-toolkit/server 0.0.0-stage → 9.35.0
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,114 @@
|
|
|
1
|
+
import type { Redis } from "ioredis";
|
|
2
|
+
import { type AdminClock } from "./AdminClock.js";
|
|
3
|
+
import { type AdminCollection, type ChainedRecordBuilder, type AdminStore, type AdminStream, type RecordQuery, type StoredDocument, type StoredRecord } from "./AdminStore.js";
|
|
4
|
+
/** Admin data in Redis (D7). */
|
|
5
|
+
export declare class RedisAdminStore implements AdminStore {
|
|
6
|
+
/** Always "redis". */
|
|
7
|
+
readonly kind: "redis";
|
|
8
|
+
/** Redis outlives every instance. */
|
|
9
|
+
readonly isPersistent: boolean;
|
|
10
|
+
/** The shared client (RedisPresence quits it, R4). */
|
|
11
|
+
private readonly redis;
|
|
12
|
+
/** The clock of `updatedAt` and `at`. */
|
|
13
|
+
private readonly clock;
|
|
14
|
+
/** A connection of its own for WATCH/MULTI, made on the first chained append, so no shared command clears the WATCH. */
|
|
15
|
+
private chainClient;
|
|
16
|
+
/** The pending chained append, so this process runs one WATCH at a time on chainClient. */
|
|
17
|
+
private chainQueue;
|
|
18
|
+
/**
|
|
19
|
+
* Wraps the shared client.
|
|
20
|
+
* @param redis The ioredis client also given to RedisPresence.
|
|
21
|
+
* @param clock The clock of stored times.
|
|
22
|
+
*/
|
|
23
|
+
constructor(redis: Redis, clock?: AdminClock);
|
|
24
|
+
/**
|
|
25
|
+
* Reads one document.
|
|
26
|
+
* @param collection The collection.
|
|
27
|
+
* @param documentId The document id.
|
|
28
|
+
* @returns The document, or null.
|
|
29
|
+
*/
|
|
30
|
+
getDocument<Value>(collection: AdminCollection, documentId: string): Promise<StoredDocument<Value> | null>;
|
|
31
|
+
/**
|
|
32
|
+
* Reads every document of a collection.
|
|
33
|
+
* @param collection The collection.
|
|
34
|
+
* @returns The documents.
|
|
35
|
+
*/
|
|
36
|
+
listDocuments<Value>(collection: AdminCollection): Promise<StoredDocument<Value>[]>;
|
|
37
|
+
/**
|
|
38
|
+
* Writes a document through PUT_DOCUMENT_SCRIPT.
|
|
39
|
+
* @param collection The collection.
|
|
40
|
+
* @param documentId The document id.
|
|
41
|
+
* @param value The document.
|
|
42
|
+
* @param expectedRevision The revision read, or null for no check.
|
|
43
|
+
* @returns The stored document.
|
|
44
|
+
* @throws AdminConflictError on a stale expectedRevision.
|
|
45
|
+
*/
|
|
46
|
+
putDocument<Value>(collection: AdminCollection, documentId: string, value: Value, expectedRevision: number | null): Promise<StoredDocument<Value>>;
|
|
47
|
+
/**
|
|
48
|
+
* Deletes a document.
|
|
49
|
+
* @param collection The collection.
|
|
50
|
+
* @param documentId The document id.
|
|
51
|
+
* @returns True when it existed.
|
|
52
|
+
*/
|
|
53
|
+
deleteDocument(collection: AdminCollection, documentId: string): Promise<boolean>;
|
|
54
|
+
/**
|
|
55
|
+
* Appends a record: INCR gives the sequence, ZADD stores it.
|
|
56
|
+
* @param stream The stream.
|
|
57
|
+
* @param value The record.
|
|
58
|
+
* @returns The stored record.
|
|
59
|
+
*/
|
|
60
|
+
appendRecord<Value>(stream: AdminStream, value: Value): Promise<StoredRecord<Value>>;
|
|
61
|
+
/**
|
|
62
|
+
* Appends a record built from the newest one: WATCH the sequence, read the newest record, build, then MULTI (SET
|
|
63
|
+
* the sequence + ZADD) EXEC, retried up to CHAINED_APPEND_ATTEMPTS times when another instance appended first (R21).
|
|
64
|
+
* @param stream The stream.
|
|
65
|
+
* @param build Builds the value.
|
|
66
|
+
* @returns The stored record.
|
|
67
|
+
* @throws Error when every attempt lost the race.
|
|
68
|
+
*/
|
|
69
|
+
appendChainedRecord<Value>(stream: AdminStream, build: ChainedRecordBuilder<Value>): Promise<StoredRecord<Value>>;
|
|
70
|
+
/**
|
|
71
|
+
* Reads records with ZRANGEBYSCORE / ZREVRANGEBYSCORE.
|
|
72
|
+
* @param stream The stream.
|
|
73
|
+
* @param query The range, limit and order.
|
|
74
|
+
* @returns The records.
|
|
75
|
+
*/
|
|
76
|
+
readRecords<Value>(stream: AdminStream, query: RecordQuery): Promise<StoredRecord<Value>[]>;
|
|
77
|
+
/**
|
|
78
|
+
* Removes records older than the cutoff, oldest first in batches of TRIM_BATCH_SIZE.
|
|
79
|
+
* @param stream The stream.
|
|
80
|
+
* @param cutoff The oldest time kept.
|
|
81
|
+
* @returns How many were removed.
|
|
82
|
+
*/
|
|
83
|
+
trimRecordsBefore(stream: AdminStream, cutoff: Date): Promise<number>;
|
|
84
|
+
/**
|
|
85
|
+
* Keeps only the newest records (ZREMRANGEBYRANK).
|
|
86
|
+
* @param stream The stream.
|
|
87
|
+
* @param keepCount How many to keep.
|
|
88
|
+
* @returns How many were removed.
|
|
89
|
+
*/
|
|
90
|
+
trimRecordsToCount(stream: AdminStream, keepCount: number): Promise<number>;
|
|
91
|
+
/**
|
|
92
|
+
* Takes the lease with SET NX PX, or renews it when this holder already has it.
|
|
93
|
+
* @param leaseName The lease.
|
|
94
|
+
* @param holderId The would-be holder.
|
|
95
|
+
* @param ttlMilliseconds The lease length.
|
|
96
|
+
* @returns True when this holder now holds it.
|
|
97
|
+
*/
|
|
98
|
+
tryAcquireLease(leaseName: string, holderId: string, ttlMilliseconds: number): Promise<boolean>;
|
|
99
|
+
/** Closes the store's own WATCH connection; the shared client belongs to RedisPresence (R4). */
|
|
100
|
+
close(): Promise<void>;
|
|
101
|
+
/**
|
|
102
|
+
* One chained append with its retries (see appendChainedRecord).
|
|
103
|
+
* @param stream The stream.
|
|
104
|
+
* @param build Builds the value.
|
|
105
|
+
* @returns The stored record.
|
|
106
|
+
* @throws Error when every attempt lost the race.
|
|
107
|
+
*/
|
|
108
|
+
private appendChainedOnce;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* @param stream The stream.
|
|
112
|
+
* @returns The key of its sequence counter.
|
|
113
|
+
*/
|
|
114
|
+
export declare function sequenceKey(stream: AdminStream): string;
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
// The Redis backend of the admin store (game-server-admin-console D7, DR §6.1), shared by every instance of a fleet.
|
|
2
|
+
// Documents are one hash per collection written by a compare-and-set script; streams are sorted sets scored by sequence.
|
|
3
|
+
import { ADMIN_REDIS_PREFIX } from "./AdminConstants.js";
|
|
4
|
+
import { SYSTEM_CLOCK } from "./AdminClock.js";
|
|
5
|
+
import { AdminConflictError, clampRecordLimit } from "./AdminStore.js";
|
|
6
|
+
/**
|
|
7
|
+
* Writes a document when its revision matches. KEYS[1] = the collection hash; ARGV = id, expected revision ("" for no
|
|
8
|
+
* check), value JSON, ISO time. Returns { 1, stored JSON } on success or { 0, current JSON or "" } on a conflict.
|
|
9
|
+
*/
|
|
10
|
+
const PUT_DOCUMENT_SCRIPT = `
|
|
11
|
+
local current = redis.call('HGET', KEYS[1], ARGV[1])
|
|
12
|
+
local revision = 0
|
|
13
|
+
if current then revision = cjson.decode(current).revision end
|
|
14
|
+
if ARGV[2] ~= '' and tonumber(ARGV[2]) ~= revision then return {0, current or ''} end
|
|
15
|
+
local stored = '{"revision":' .. (revision + 1) .. ',"updatedAt":' .. cjson.encode(ARGV[4]) .. ',"value":' .. ARGV[3] .. '}'
|
|
16
|
+
redis.call('HSET', KEYS[1], ARGV[1], stored)
|
|
17
|
+
return {1, stored}
|
|
18
|
+
`;
|
|
19
|
+
/** Extends a lease its holder already holds. KEYS[1] = the lease key; ARGV = holder id, TTL in milliseconds. */
|
|
20
|
+
const RENEW_LEASE_SCRIPT = `
|
|
21
|
+
if redis.call('GET', KEYS[1]) == ARGV[1] then
|
|
22
|
+
redis.call('PEXPIRE', KEYS[1], ARGV[2])
|
|
23
|
+
return 1
|
|
24
|
+
end
|
|
25
|
+
return 0
|
|
26
|
+
`;
|
|
27
|
+
/** Attempts of a chained append before it gives up (another instance kept appending, R21). */
|
|
28
|
+
const CHAINED_APPEND_ATTEMPTS = 10;
|
|
29
|
+
/** Records read per round while trimming by time. */
|
|
30
|
+
const TRIM_BATCH_SIZE = 500;
|
|
31
|
+
/** Number of keys the scripts take. */
|
|
32
|
+
const SINGLE_KEY = 1;
|
|
33
|
+
/** The script answer flag of a successful write. */
|
|
34
|
+
const WRITE_SUCCEEDED = 1;
|
|
35
|
+
/** Admin data in Redis (D7). */
|
|
36
|
+
export class RedisAdminStore {
|
|
37
|
+
/** Always "redis". */
|
|
38
|
+
kind = "redis";
|
|
39
|
+
/** Redis outlives every instance. */
|
|
40
|
+
isPersistent = true;
|
|
41
|
+
/** The shared client (RedisPresence quits it, R4). */
|
|
42
|
+
redis;
|
|
43
|
+
/** The clock of `updatedAt` and `at`. */
|
|
44
|
+
clock;
|
|
45
|
+
/** A connection of its own for WATCH/MULTI, made on the first chained append, so no shared command clears the WATCH. */
|
|
46
|
+
chainClient = null;
|
|
47
|
+
/** The pending chained append, so this process runs one WATCH at a time on chainClient. */
|
|
48
|
+
chainQueue = Promise.resolve();
|
|
49
|
+
/**
|
|
50
|
+
* Wraps the shared client.
|
|
51
|
+
* @param redis The ioredis client also given to RedisPresence.
|
|
52
|
+
* @param clock The clock of stored times.
|
|
53
|
+
*/
|
|
54
|
+
constructor(redis, clock = SYSTEM_CLOCK) {
|
|
55
|
+
this.redis = redis;
|
|
56
|
+
this.clock = clock;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Reads one document.
|
|
60
|
+
* @param collection The collection.
|
|
61
|
+
* @param documentId The document id.
|
|
62
|
+
* @returns The document, or null.
|
|
63
|
+
*/
|
|
64
|
+
async getDocument(collection, documentId) {
|
|
65
|
+
const json = await this.redis.hget(documentKey(collection), documentId);
|
|
66
|
+
return (json != null) ? toDocument(documentId, json) : null;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Reads every document of a collection.
|
|
70
|
+
* @param collection The collection.
|
|
71
|
+
* @returns The documents.
|
|
72
|
+
*/
|
|
73
|
+
async listDocuments(collection) {
|
|
74
|
+
const fields = await this.redis.hgetall(documentKey(collection));
|
|
75
|
+
return Object.entries(fields).map(([documentId, json]) => toDocument(documentId, json));
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Writes a document through PUT_DOCUMENT_SCRIPT.
|
|
79
|
+
* @param collection The collection.
|
|
80
|
+
* @param documentId The document id.
|
|
81
|
+
* @param value The document.
|
|
82
|
+
* @param expectedRevision The revision read, or null for no check.
|
|
83
|
+
* @returns The stored document.
|
|
84
|
+
* @throws AdminConflictError on a stale expectedRevision.
|
|
85
|
+
*/
|
|
86
|
+
async putDocument(collection, documentId, value, expectedRevision) {
|
|
87
|
+
const expectedText = (expectedRevision != null) ? String(expectedRevision) : "";
|
|
88
|
+
const updatedAt = new Date(this.clock.now()).toISOString();
|
|
89
|
+
const answer = await this.redis.eval(PUT_DOCUMENT_SCRIPT, SINGLE_KEY, documentKey(collection), documentId, expectedText, JSON.stringify(value), updatedAt);
|
|
90
|
+
const [flag, json] = answer;
|
|
91
|
+
if (flag !== WRITE_SUCCEEDED)
|
|
92
|
+
throw new AdminConflictError((json !== "") ? toDocument(documentId, json) : null);
|
|
93
|
+
return toDocument(documentId, json);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Deletes a document.
|
|
97
|
+
* @param collection The collection.
|
|
98
|
+
* @param documentId The document id.
|
|
99
|
+
* @returns True when it existed.
|
|
100
|
+
*/
|
|
101
|
+
async deleteDocument(collection, documentId) {
|
|
102
|
+
return (await this.redis.hdel(documentKey(collection), documentId)) > 0;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Appends a record: INCR gives the sequence, ZADD stores it.
|
|
106
|
+
* @param stream The stream.
|
|
107
|
+
* @param value The record.
|
|
108
|
+
* @returns The stored record.
|
|
109
|
+
*/
|
|
110
|
+
async appendRecord(stream, value) {
|
|
111
|
+
const sequence = await this.redis.incr(sequenceKey(stream));
|
|
112
|
+
const record = { sequence: sequence, at: new Date(this.clock.now()).toISOString(), value: value };
|
|
113
|
+
await this.redis.zadd(streamKey(stream), sequence, JSON.stringify(record));
|
|
114
|
+
return record;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Appends a record built from the newest one: WATCH the sequence, read the newest record, build, then MULTI (SET
|
|
118
|
+
* the sequence + ZADD) EXEC, retried up to CHAINED_APPEND_ATTEMPTS times when another instance appended first (R21).
|
|
119
|
+
* @param stream The stream.
|
|
120
|
+
* @param build Builds the value.
|
|
121
|
+
* @returns The stored record.
|
|
122
|
+
* @throws Error when every attempt lost the race.
|
|
123
|
+
*/
|
|
124
|
+
appendChainedRecord(stream, build) {
|
|
125
|
+
const run = () => this.appendChainedOnce(stream, build);
|
|
126
|
+
const next = this.chainQueue.then(run, run);
|
|
127
|
+
this.chainQueue = next.catch(() => undefined);
|
|
128
|
+
return next;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Reads records with ZRANGEBYSCORE / ZREVRANGEBYSCORE.
|
|
132
|
+
* @param stream The stream.
|
|
133
|
+
* @param query The range, limit and order.
|
|
134
|
+
* @returns The records.
|
|
135
|
+
*/
|
|
136
|
+
async readRecords(stream, query) {
|
|
137
|
+
const lowest = (query.afterSequence != null) ? "(" + query.afterSequence : "-inf";
|
|
138
|
+
const highest = (query.beforeSequence != null) ? "(" + query.beforeSequence : "+inf";
|
|
139
|
+
const limit = clampRecordLimit(query.limit);
|
|
140
|
+
const members = query.newestFirst
|
|
141
|
+
? await this.redis.zrevrangebyscore(streamKey(stream), highest, lowest, "LIMIT", 0, limit)
|
|
142
|
+
: await this.redis.zrangebyscore(streamKey(stream), lowest, highest, "LIMIT", 0, limit);
|
|
143
|
+
return members.map((member) => JSON.parse(member));
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Removes records older than the cutoff, oldest first in batches of TRIM_BATCH_SIZE.
|
|
147
|
+
* @param stream The stream.
|
|
148
|
+
* @param cutoff The oldest time kept.
|
|
149
|
+
* @returns How many were removed.
|
|
150
|
+
*/
|
|
151
|
+
async trimRecordsBefore(stream, cutoff) {
|
|
152
|
+
const cutoffMilliseconds = cutoff.getTime();
|
|
153
|
+
let removedCount = 0;
|
|
154
|
+
for (;;) {
|
|
155
|
+
const members = await this.redis.zrange(streamKey(stream), 0, TRIM_BATCH_SIZE - 1);
|
|
156
|
+
const records = members.map((member) => JSON.parse(member));
|
|
157
|
+
const expired = records.filter((record) => Date.parse(record.at) < cutoffMilliseconds);
|
|
158
|
+
if (expired.length === 0)
|
|
159
|
+
return removedCount;
|
|
160
|
+
const lastExpiredSequence = expired[expired.length - 1].sequence;
|
|
161
|
+
removedCount += await this.redis.zremrangebyscore(streamKey(stream), "-inf", lastExpiredSequence);
|
|
162
|
+
if (expired.length < records.length || records.length < TRIM_BATCH_SIZE)
|
|
163
|
+
return removedCount;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Keeps only the newest records (ZREMRANGEBYRANK).
|
|
168
|
+
* @param stream The stream.
|
|
169
|
+
* @param keepCount How many to keep.
|
|
170
|
+
* @returns How many were removed.
|
|
171
|
+
*/
|
|
172
|
+
async trimRecordsToCount(stream, keepCount) {
|
|
173
|
+
return this.redis.zremrangebyrank(streamKey(stream), 0, -(Math.max(0, keepCount) + 1));
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Takes the lease with SET NX PX, or renews it when this holder already has it.
|
|
177
|
+
* @param leaseName The lease.
|
|
178
|
+
* @param holderId The would-be holder.
|
|
179
|
+
* @param ttlMilliseconds The lease length.
|
|
180
|
+
* @returns True when this holder now holds it.
|
|
181
|
+
*/
|
|
182
|
+
async tryAcquireLease(leaseName, holderId, ttlMilliseconds) {
|
|
183
|
+
const taken = await this.redis.set(leaseKey(leaseName), holderId, "PX", ttlMilliseconds, "NX");
|
|
184
|
+
if (taken != null)
|
|
185
|
+
return true;
|
|
186
|
+
const renewed = await this.redis.eval(RENEW_LEASE_SCRIPT, SINGLE_KEY, leaseKey(leaseName), holderId, String(ttlMilliseconds));
|
|
187
|
+
return renewed === WRITE_SUCCEEDED;
|
|
188
|
+
}
|
|
189
|
+
/** Closes the store's own WATCH connection; the shared client belongs to RedisPresence (R4). */
|
|
190
|
+
async close() {
|
|
191
|
+
await this.chainQueue;
|
|
192
|
+
if (this.chainClient != null)
|
|
193
|
+
await this.chainClient.quit();
|
|
194
|
+
this.chainClient = null;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* One chained append with its retries (see appendChainedRecord).
|
|
198
|
+
* @param stream The stream.
|
|
199
|
+
* @param build Builds the value.
|
|
200
|
+
* @returns The stored record.
|
|
201
|
+
* @throws Error when every attempt lost the race.
|
|
202
|
+
*/
|
|
203
|
+
async appendChainedOnce(stream, build) {
|
|
204
|
+
this.chainClient ??= this.redis.duplicate();
|
|
205
|
+
const client = this.chainClient;
|
|
206
|
+
for (let attempt = 0; attempt < CHAINED_APPEND_ATTEMPTS; attempt++) {
|
|
207
|
+
await client.watch(sequenceKey(stream));
|
|
208
|
+
const lastSequence = Number(await client.get(sequenceKey(stream)) ?? 0);
|
|
209
|
+
const newest = await client.zrevrange(streamKey(stream), 0, 0);
|
|
210
|
+
const previous = (newest.length > 0) ? JSON.parse(newest[0]) : null;
|
|
211
|
+
const sequence = lastSequence + 1;
|
|
212
|
+
const at = new Date(this.clock.now()).toISOString();
|
|
213
|
+
const record = { sequence: sequence, at: at, value: build(previous, sequence, at) };
|
|
214
|
+
const committed = await client.multi().set(sequenceKey(stream), String(sequence)).zadd(streamKey(stream), sequence, JSON.stringify(record)).exec();
|
|
215
|
+
if (committed != null)
|
|
216
|
+
return record;
|
|
217
|
+
}
|
|
218
|
+
throw new Error("could not append to " + stream + " after " + CHAINED_APPEND_ATTEMPTS + " attempts");
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* @param collection The collection.
|
|
223
|
+
* @returns Its hash key.
|
|
224
|
+
*/
|
|
225
|
+
function documentKey(collection) {
|
|
226
|
+
return ADMIN_REDIS_PREFIX + "doc:" + collection;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* @param stream The stream.
|
|
230
|
+
* @returns Its sorted set key.
|
|
231
|
+
*/
|
|
232
|
+
function streamKey(stream) {
|
|
233
|
+
return ADMIN_REDIS_PREFIX + "stream:" + stream;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* @param stream The stream.
|
|
237
|
+
* @returns The key of its sequence counter.
|
|
238
|
+
*/
|
|
239
|
+
export function sequenceKey(stream) {
|
|
240
|
+
return ADMIN_REDIS_PREFIX + "seq:" + stream;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* @param leaseName The lease.
|
|
244
|
+
* @returns Its key.
|
|
245
|
+
*/
|
|
246
|
+
function leaseKey(leaseName) {
|
|
247
|
+
return ADMIN_REDIS_PREFIX + "lease:" + leaseName;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Parses a hash field into a document.
|
|
251
|
+
* @param documentId The id.
|
|
252
|
+
* @param json The stored JSON.
|
|
253
|
+
* @returns The document.
|
|
254
|
+
*/
|
|
255
|
+
function toDocument(documentId, json) {
|
|
256
|
+
const entry = JSON.parse(json);
|
|
257
|
+
return { id: documentId, revision: entry.revision, updatedAt: entry.updatedAt, value: entry.value };
|
|
258
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { RolloutsRequest, RolloutView } from "./AdminApiTypes.js";
|
|
2
|
+
import type { AdminCoreServices } from "./AdminServices.js";
|
|
3
|
+
import type { FleetDirectory } from "./FleetDirectory.js";
|
|
4
|
+
import type { ServerEvents } from "./ServerEvents.js";
|
|
5
|
+
/** The commands of POST rollouts/:rolloutId/:command. */
|
|
6
|
+
export type RolloutCommand = "pause" | "resume" | "cancel";
|
|
7
|
+
/** Starts, steers and drives the fleet's rolling updates. */
|
|
8
|
+
export declare class RolloutCoordinator {
|
|
9
|
+
/** The shared core services. */
|
|
10
|
+
private readonly services;
|
|
11
|
+
/** The fleet directory (instances and the drain command). */
|
|
12
|
+
private readonly fleet;
|
|
13
|
+
/** The activity feed's server events. */
|
|
14
|
+
private readonly serverEvents;
|
|
15
|
+
/**
|
|
16
|
+
* Creates the coordinator; nothing runs until the leader ticks.
|
|
17
|
+
* @param services The shared core services.
|
|
18
|
+
* @param fleet The fleet directory.
|
|
19
|
+
* @param serverEvents The activity feed's server events.
|
|
20
|
+
*/
|
|
21
|
+
constructor(services: AdminCoreServices, fleet: FleetDirectory, serverEvents: ServerEvents);
|
|
22
|
+
/**
|
|
23
|
+
* Every rollout, newest first.
|
|
24
|
+
* @returns The rollouts.
|
|
25
|
+
*/
|
|
26
|
+
list(): Promise<RolloutView[]>;
|
|
27
|
+
/**
|
|
28
|
+
* Starts a rollout over the live instances (POST rollouts). The instance that starts it goes last, so it keeps
|
|
29
|
+
* answering the console for as long as possible.
|
|
30
|
+
* @param request The notice and timeouts.
|
|
31
|
+
* @param startedBy Who starts it.
|
|
32
|
+
* @returns The rollout.
|
|
33
|
+
* @throws AdminRequestError 409 with fewer than two live instances or while another rollout is unfinished, 400 for
|
|
34
|
+
* negative times.
|
|
35
|
+
*/
|
|
36
|
+
start(request: RolloutsRequest, startedBy: string): Promise<RolloutView>;
|
|
37
|
+
/**
|
|
38
|
+
* Pauses, resumes or cancels a rollout. Resuming a failed step gives it another perInstanceTimeoutMinutes.
|
|
39
|
+
* @param rolloutId The rollout.
|
|
40
|
+
* @param command The command.
|
|
41
|
+
* @returns The rollout.
|
|
42
|
+
* @throws AdminRequestError 404 for an unknown rollout or command, 409 when the rollout cannot take the command.
|
|
43
|
+
*/
|
|
44
|
+
command(rolloutId: string, command: string): Promise<RolloutView>;
|
|
45
|
+
/** The leader tick: moves every running rollout on by one decision. */
|
|
46
|
+
tick(): Promise<void>;
|
|
47
|
+
/**
|
|
48
|
+
* Runs the current step of a running rollout (DR §7.8 steps 1–4), or finishes the rollout.
|
|
49
|
+
* @param record The rollout, changed in place.
|
|
50
|
+
*/
|
|
51
|
+
private advance;
|
|
52
|
+
/**
|
|
53
|
+
* Sends a step's drain (with the rollout's notice) to its instance.
|
|
54
|
+
* @param record The rollout.
|
|
55
|
+
* @param step The step.
|
|
56
|
+
* @param now The clock time.
|
|
57
|
+
*/
|
|
58
|
+
private startDrain;
|
|
59
|
+
/**
|
|
60
|
+
* Marks a step replaced once its instance is gone and a replacement is ready, or fails it after the timeout.
|
|
61
|
+
* @param record The rollout.
|
|
62
|
+
* @param step The step.
|
|
63
|
+
* @param now The clock time.
|
|
64
|
+
* @param stepStartedAt When the step's drain started, in milliseconds.
|
|
65
|
+
*/
|
|
66
|
+
private checkReplacement;
|
|
67
|
+
/**
|
|
68
|
+
* A ready instance that joined after the rollout started, runs at least its starting version and replaces no other
|
|
69
|
+
* step yet.
|
|
70
|
+
* @param record The rollout.
|
|
71
|
+
* @param instances The fleet now.
|
|
72
|
+
* @returns The replacement, or undefined.
|
|
73
|
+
*/
|
|
74
|
+
private findReplacement;
|
|
75
|
+
/**
|
|
76
|
+
* Resumes a paused rollout; its failed step waits for a replacement again.
|
|
77
|
+
* @param record The rollout, changed in place.
|
|
78
|
+
* @throws AdminRequestError 409 unless it is paused.
|
|
79
|
+
*/
|
|
80
|
+
private resumeRollout;
|
|
81
|
+
/**
|
|
82
|
+
* Stores a rollout the tick changed; a pause or cancel stored meanwhile wins and the change is dropped.
|
|
83
|
+
* @param stored The rollout as read.
|
|
84
|
+
* @param record The rollout after the tick.
|
|
85
|
+
*/
|
|
86
|
+
private saveIfChanged;
|
|
87
|
+
/**
|
|
88
|
+
* Every stored rollout.
|
|
89
|
+
* @returns The documents.
|
|
90
|
+
*/
|
|
91
|
+
private storedRollouts;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Compares two dotted versions by their numeric parts ("9.30.1" < "9.31.0"); a non-numeric part counts as 0.
|
|
95
|
+
* @param left One version.
|
|
96
|
+
* @param right The other.
|
|
97
|
+
* @returns A negative number, 0 or a positive number.
|
|
98
|
+
*/
|
|
99
|
+
export declare function compareVersions(left: string, right: string): number;
|