@helyx/bot 0.3.0 → 0.4.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/CHANGELOG.md +42 -0
- package/README.md +68 -0
- package/dist/cloud-connector.d.ts +36 -2
- package/dist/cloud-connector.js +41 -20
- package/dist/cloud-management-module.d.ts +5 -3
- package/dist/cloud-management-module.js +27 -18
- package/dist/command-sync.d.ts +32 -0
- package/dist/command-sync.js +67 -0
- package/dist/control-server.d.ts +10 -0
- package/dist/control-server.js +86 -18
- package/dist/developer-operations-module.d.ts +28 -0
- package/dist/developer-operations-module.js +249 -0
- package/dist/events.d.ts +5 -1
- package/dist/events.js +69 -3
- package/dist/guilds.d.ts +2 -2
- package/dist/guilds.js +17 -4
- package/dist/index.d.ts +2 -0
- package/dist/index.js +111 -10
- package/dist/interactions.d.ts +8 -1
- package/dist/interactions.js +50 -24
- package/dist/shard-cloud-proxy.d.ts +21 -0
- package/dist/shard-cloud-proxy.js +94 -0
- package/dist/shard-contracts.d.ts +751 -0
- package/dist/shard-contracts.js +445 -0
- package/dist/shard-coordinator.d.ts +90 -0
- package/dist/shard-coordinator.js +620 -0
- package/dist/shard-router.d.ts +28 -0
- package/dist/shard-router.js +211 -0
- package/dist/shard-supervisor.d.ts +36 -0
- package/dist/shard-supervisor.js +1230 -0
- package/dist/shard-worker.d.ts +62 -0
- package/dist/shard-worker.js +380 -0
- package/dist/supervisor-cloud-directory.d.ts +11 -0
- package/dist/supervisor-cloud-directory.js +66 -0
- package/package.json +9 -8
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { BotControlTransportError, } from "./control-server.js";
|
|
3
|
+
import { SHARD_CONTROL_TIMEOUT_MS, controlOperationGuildId, guildIdToShardId, shardControlOperationSchema, shardControlResponseSchema, } from "./shard-contracts.js";
|
|
4
|
+
const LEASE_VALIDATION_TIMEOUT_MS = 1_000;
|
|
5
|
+
export class ShardControlRouter {
|
|
6
|
+
input;
|
|
7
|
+
constructor(input) {
|
|
8
|
+
this.input = input;
|
|
9
|
+
}
|
|
10
|
+
getGuildState(guildId) {
|
|
11
|
+
return this.#route({ kind: "guild.state.get", guildId });
|
|
12
|
+
}
|
|
13
|
+
setModuleStatus(input) {
|
|
14
|
+
return this.#route({ kind: "guild.module.status.set", ...input });
|
|
15
|
+
}
|
|
16
|
+
updateConfiguration(input) {
|
|
17
|
+
return this.#route(shardControlOperationSchema.parse({
|
|
18
|
+
kind: "guild.module.configuration.update",
|
|
19
|
+
...input,
|
|
20
|
+
}));
|
|
21
|
+
}
|
|
22
|
+
updatePermissions(input) {
|
|
23
|
+
return this.#route({ kind: "guild.module.permissions.update", ...input });
|
|
24
|
+
}
|
|
25
|
+
updateLoggingPolicy(input) {
|
|
26
|
+
return this.#route({ kind: "guild.module.logging.update", ...input });
|
|
27
|
+
}
|
|
28
|
+
executeDashboardAction(input) {
|
|
29
|
+
return this.#route({ kind: "guild.module.action.execute", ...input });
|
|
30
|
+
}
|
|
31
|
+
listPrivacyProviders() {
|
|
32
|
+
return this.#routeLeader({ kind: "privacy.providers.list" });
|
|
33
|
+
}
|
|
34
|
+
capturePrivacy(input) {
|
|
35
|
+
const { cutoffAt, ...rest } = input;
|
|
36
|
+
return this.#routeLeader({
|
|
37
|
+
kind: "privacy.capture",
|
|
38
|
+
...rest,
|
|
39
|
+
cutoffAt: cutoffAt.toISOString(),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
erasePrivacy(input) {
|
|
43
|
+
const { executeThrough, ...rest } = input;
|
|
44
|
+
return this.#routeLeader({
|
|
45
|
+
kind: "privacy.erase",
|
|
46
|
+
...rest,
|
|
47
|
+
executeThrough: executeThrough.toISOString(),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
async #route(operation) {
|
|
51
|
+
const guildId = controlOperationGuildId(operation);
|
|
52
|
+
if (!guildId)
|
|
53
|
+
throw new BotControlTransportError(502, "The control operation has no shard route");
|
|
54
|
+
const shardId = guildIdToShardId(guildId, this.input.shardCount);
|
|
55
|
+
const localLease = this.input.localLeases.get(shardId);
|
|
56
|
+
let lease = localLease;
|
|
57
|
+
if (localLease && this.input.coordinator) {
|
|
58
|
+
const current = await this.#currentShardLease(shardId);
|
|
59
|
+
if (!current || !sameShardLease(localLease, current))
|
|
60
|
+
throw new BotControlTransportError(503, "The owning shard is temporarily unavailable");
|
|
61
|
+
lease = current;
|
|
62
|
+
}
|
|
63
|
+
else if (!lease && this.input.coordinator) {
|
|
64
|
+
lease = await this.#currentShardLease(shardId);
|
|
65
|
+
}
|
|
66
|
+
if (!lease || lease.generation !== this.input.generation)
|
|
67
|
+
throw new BotControlTransportError(503, "The owning shard is temporarily unavailable");
|
|
68
|
+
const request = {
|
|
69
|
+
version: 1,
|
|
70
|
+
type: "helyx.control.request",
|
|
71
|
+
requestId: randomUUID(),
|
|
72
|
+
generation: this.input.generation,
|
|
73
|
+
fencingToken: lease.fencingToken,
|
|
74
|
+
deadlineAt: new Date(Date.now() + SHARD_CONTROL_TIMEOUT_MS).toISOString(),
|
|
75
|
+
operation,
|
|
76
|
+
};
|
|
77
|
+
let response;
|
|
78
|
+
try {
|
|
79
|
+
response = localLease
|
|
80
|
+
? await withDeadline(this.input.dispatchLocal(shardId, request), SHARD_CONTROL_TIMEOUT_MS)
|
|
81
|
+
: await this.input.coordinator.routeRemote(lease, request);
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
const timedOut = error instanceof Error &&
|
|
85
|
+
error.message.toLowerCase().includes("timed out");
|
|
86
|
+
throw new BotControlTransportError(timedOut ? 504 : 503, timedOut
|
|
87
|
+
? "The owning shard did not respond in time"
|
|
88
|
+
: "The owning shard is temporarily unavailable");
|
|
89
|
+
}
|
|
90
|
+
return unwrapResponse(response);
|
|
91
|
+
}
|
|
92
|
+
async #routeLeader(operation) {
|
|
93
|
+
if (this.input.localLeaderLease && this.input.privacy) {
|
|
94
|
+
if (this.input.coordinator) {
|
|
95
|
+
const current = await this.#currentLeaderLease();
|
|
96
|
+
if (!current || !sameLeaderLease(this.input.localLeaderLease, current))
|
|
97
|
+
throw new BotControlTransportError(503, "The shard leader is temporarily unavailable");
|
|
98
|
+
}
|
|
99
|
+
return executePrivacy(this.input.privacy, operation);
|
|
100
|
+
}
|
|
101
|
+
const coordinator = this.input.coordinator;
|
|
102
|
+
const lease = await this.#currentLeaderLease();
|
|
103
|
+
if (!coordinator || !lease || lease.generation !== this.input.generation)
|
|
104
|
+
throw new BotControlTransportError(503, "The shard leader is temporarily unavailable");
|
|
105
|
+
const request = {
|
|
106
|
+
version: 1,
|
|
107
|
+
type: "helyx.control.request",
|
|
108
|
+
requestId: randomUUID(),
|
|
109
|
+
generation: this.input.generation,
|
|
110
|
+
fencingToken: lease.fencingToken,
|
|
111
|
+
deadlineAt: new Date(Date.now() + SHARD_CONTROL_TIMEOUT_MS).toISOString(),
|
|
112
|
+
operation,
|
|
113
|
+
};
|
|
114
|
+
try {
|
|
115
|
+
return unwrapResponse(await coordinator.routeLeader(lease, request));
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
if (error instanceof BotControlTransportError)
|
|
119
|
+
throw error;
|
|
120
|
+
throw new BotControlTransportError(error instanceof Error &&
|
|
121
|
+
error.message.toLowerCase().includes("timed out")
|
|
122
|
+
? 504
|
|
123
|
+
: 503, "The shard leader is temporarily unavailable");
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async #currentShardLease(shardId) {
|
|
127
|
+
try {
|
|
128
|
+
return await withDeadline(this.input.coordinator.getShardLease(shardId), LEASE_VALIDATION_TIMEOUT_MS);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async #currentLeaderLease() {
|
|
135
|
+
if (!this.input.coordinator)
|
|
136
|
+
return undefined;
|
|
137
|
+
try {
|
|
138
|
+
return await withDeadline(this.input.coordinator.getLeaderLease(), LEASE_VALIDATION_TIMEOUT_MS);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async function executePrivacy(privacy, operation) {
|
|
146
|
+
if (operation.kind === "privacy.providers.list")
|
|
147
|
+
return (await privacy.listPrivacyProviders());
|
|
148
|
+
if (operation.kind === "privacy.capture") {
|
|
149
|
+
return (await privacy.capturePrivacy({
|
|
150
|
+
providerId: operation.providerId,
|
|
151
|
+
providerVersion: operation.providerVersion,
|
|
152
|
+
requestId: operation.requestId,
|
|
153
|
+
subjectUserId: operation.subjectUserId,
|
|
154
|
+
cutoffAt: new Date(operation.cutoffAt),
|
|
155
|
+
...(operation.cursor === undefined ? {} : { cursor: operation.cursor }),
|
|
156
|
+
}));
|
|
157
|
+
}
|
|
158
|
+
if (operation.kind === "privacy.erase") {
|
|
159
|
+
return (await privacy.erasePrivacy({
|
|
160
|
+
providerId: operation.providerId,
|
|
161
|
+
providerVersion: operation.providerVersion,
|
|
162
|
+
requestId: operation.requestId,
|
|
163
|
+
subjectUserId: operation.subjectUserId,
|
|
164
|
+
executeThrough: new Date(operation.executeThrough),
|
|
165
|
+
...(operation.cursor === undefined ? {} : { cursor: operation.cursor }),
|
|
166
|
+
}));
|
|
167
|
+
}
|
|
168
|
+
throw new BotControlTransportError(502, "The leader operation is invalid");
|
|
169
|
+
}
|
|
170
|
+
function unwrapResponse(response) {
|
|
171
|
+
const accepted = shardControlResponseSchema.parse(response);
|
|
172
|
+
if (!accepted.ok)
|
|
173
|
+
throw new BotControlTransportError(accepted.status === 400 ||
|
|
174
|
+
accepted.status === 404 ||
|
|
175
|
+
accepted.status === 409 ||
|
|
176
|
+
accepted.status === 504
|
|
177
|
+
? accepted.status
|
|
178
|
+
: accepted.status >= 500
|
|
179
|
+
? 503
|
|
180
|
+
: 502, accepted.message);
|
|
181
|
+
return accepted.result;
|
|
182
|
+
}
|
|
183
|
+
function sameShardLease(left, right) {
|
|
184
|
+
return (left.generation === right.generation &&
|
|
185
|
+
left.deploymentOrder === right.deploymentOrder &&
|
|
186
|
+
left.supervisorId === right.supervisorId &&
|
|
187
|
+
left.shardId === right.shardId &&
|
|
188
|
+
left.fencingToken === right.fencingToken);
|
|
189
|
+
}
|
|
190
|
+
function sameLeaderLease(left, right) {
|
|
191
|
+
return (left.generation === right.generation &&
|
|
192
|
+
left.supervisorId === right.supervisorId &&
|
|
193
|
+
left.fencingToken === right.fencingToken);
|
|
194
|
+
}
|
|
195
|
+
async function withDeadline(operation, timeoutMs) {
|
|
196
|
+
let timer;
|
|
197
|
+
try {
|
|
198
|
+
return await Promise.race([
|
|
199
|
+
operation,
|
|
200
|
+
new Promise((_resolve, reject) => {
|
|
201
|
+
timer = setTimeout(() => reject(new Error("Shard control request timed out")), timeoutMs);
|
|
202
|
+
timer.unref();
|
|
203
|
+
}),
|
|
204
|
+
]);
|
|
205
|
+
}
|
|
206
|
+
finally {
|
|
207
|
+
if (timer)
|
|
208
|
+
clearTimeout(timer);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
//# sourceMappingURL=shard-router.js.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { BotEnvironment } from "@helyx/config";
|
|
2
|
+
import { type DatabasePool } from "@helyx/database";
|
|
3
|
+
import type { ModuleDefinition } from "@helyx/sdk";
|
|
4
|
+
import { type ShardCloudRequest, type ShardIdentity, type ShardProcessHealth } from "./shard-contracts.js";
|
|
5
|
+
import { type ShardCoordinator, type ShardGenerationDescriptor, type ShardLeaderLease, type ShardLease } from "./shard-coordinator.js";
|
|
6
|
+
import type { BotHandle } from "./index.js";
|
|
7
|
+
export declare const STEADY_STATE_LEASE_LOSS_BOUND_MS: number;
|
|
8
|
+
export declare function startShardSupervisor(environment: BotEnvironment, processEnvironment: NodeJS.ProcessEnv, workerEntry: string): Promise<BotHandle>;
|
|
9
|
+
export declare function buildShardWorkerEnvironment(source: NodeJS.ProcessEnv, identity: ShardIdentity, restartCount: number): NodeJS.ProcessEnv;
|
|
10
|
+
export declare function shardRuntimeCompositionHash(modules: readonly ModuleDefinition[], internalGuildId?: string): string;
|
|
11
|
+
export declare function boundedRespawnDelay(attempt: number): number;
|
|
12
|
+
export interface SteadyStateOwnershipRenewal {
|
|
13
|
+
generationRenewed: boolean;
|
|
14
|
+
lostShardIds: number[];
|
|
15
|
+
leaderRenewed: boolean;
|
|
16
|
+
leaderAvailable: boolean;
|
|
17
|
+
}
|
|
18
|
+
export declare function selectFencedShardHealth(input: {
|
|
19
|
+
generation: string;
|
|
20
|
+
shardCount: number;
|
|
21
|
+
coordinatedHealth: ReadonlyArray<{
|
|
22
|
+
identity: ShardIdentity;
|
|
23
|
+
health: ShardProcessHealth;
|
|
24
|
+
}>;
|
|
25
|
+
liveLeases: readonly ShardLease[];
|
|
26
|
+
localHealth: ReadonlyMap<number, ShardProcessHealth>;
|
|
27
|
+
localLeases: ReadonlyMap<number, ShardLease>;
|
|
28
|
+
coordinated: boolean;
|
|
29
|
+
}): Map<number, ShardProcessHealth>;
|
|
30
|
+
export declare function renewSteadyStateOwnership(coordinator: Pick<ShardCoordinator, "refreshGeneration" | "renewShard" | "renewLeader" | "getLeaderLease">, descriptor: ShardGenerationDescriptor, deploymentOrder: number, shardLeases: ReadonlyMap<number, ShardLease>, leaderLease?: ShardLeaderLease, timeoutMs?: number): Promise<SteadyStateOwnershipRenewal>;
|
|
31
|
+
export declare function acquireFencedLeader(coordinator: Pick<ShardCoordinator, "acquireLeader">, descriptor: ShardGenerationDescriptor, deploymentOrder: number, timeoutMs?: number): Promise<ShardLeaderLease | undefined>;
|
|
32
|
+
export declare function cloudRequestExpired(request: ShardCloudRequest): boolean;
|
|
33
|
+
export declare function spawnWithShutdownGuard(isShuttingDown: () => boolean, spawn: () => Promise<unknown>, kill: () => void): Promise<boolean>;
|
|
34
|
+
export declare function nextShardDeploymentOrder(database: DatabasePool): Promise<number>;
|
|
35
|
+
export declare function renewStartupOwnership(coordinator: Pick<ShardCoordinator, "refreshGeneration" | "renewLeader">, descriptor: ShardGenerationDescriptor, deploymentOrder: number, leaderLease?: ShardLeaderLease): Promise<void>;
|
|
36
|
+
//# sourceMappingURL=shard-supervisor.d.ts.map
|