@seedcord/core 0.1.0-next.4 → 0.1.0-next.5
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/dist/{CustomId--QizzBOn.d.mts → AutocompleteOptions-eB87SZ4a.d.mts} +494 -154
- package/dist/index.cjs +236 -105
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +181 -25
- package/dist/index.mjs +198 -79
- package/dist/index.mjs.map +1 -1
- package/dist/internal.index.cjs +461 -45
- package/dist/internal.index.cjs.map +1 -1
- package/dist/internal.index.d.mts +145 -28
- package/dist/internal.index.mjs +417 -20
- package/dist/internal.index.mjs.map +1 -1
- package/dist/is-never-Ddaba1Su.d.mts +58 -0
- package/dist/node.index.cjs +521 -0
- package/dist/node.index.cjs.map +1 -0
- package/dist/node.index.d.cts +1 -0
- package/dist/node.index.d.mts +2391 -0
- package/dist/node.index.mjs +513 -0
- package/dist/node.index.mjs.map +1 -0
- package/dist/{CustomId-DV7daU7r.cjs → permissions-DQL5ViZn.cjs} +430 -89
- package/dist/permissions-DQL5ViZn.cjs.map +1 -0
- package/dist/{CustomId-epJayEGO.mjs → permissions-SokZLJMj.mjs} +329 -90
- package/dist/permissions-SokZLJMj.mjs.map +1 -0
- package/package.json +20 -9
- package/dist/CustomId-DV7daU7r.cjs.map +0 -1
- package/dist/CustomId-epJayEGO.mjs.map +0 -1
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
import { SeedcordErrorCode } from "@seedcord/errors";
|
|
2
|
+
import { SeedcordError } from "@seedcord/errors/internal";
|
|
3
|
+
import { Logger } from "@seedcord/logger";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import { TypedEventEmitter } from "@seedcord/event-emitter";
|
|
6
|
+
import { createServer } from "node:http";
|
|
7
|
+
|
|
8
|
+
//#region src/node/Lifecycle/CoordinatedLifecycle.ts
|
|
9
|
+
/**
|
|
10
|
+
* Abstract base class for coordinated lifecycle management (startup/shutdown)
|
|
11
|
+
*/
|
|
12
|
+
var CoordinatedLifecycle = class extends TypedEventEmitter {
|
|
13
|
+
phaseOrder;
|
|
14
|
+
phaseEnum;
|
|
15
|
+
logger;
|
|
16
|
+
tasksMap = /* @__PURE__ */ new Map();
|
|
17
|
+
constructor(loggerName, phaseOrder, phaseEnum) {
|
|
18
|
+
super();
|
|
19
|
+
this.phaseOrder = phaseOrder;
|
|
20
|
+
this.phaseEnum = phaseEnum;
|
|
21
|
+
this.logger = new Logger(loggerName);
|
|
22
|
+
this.phaseOrder.forEach((phase) => this.tasksMap.set(phase, []));
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Adds a lifecycle task to a specific phase.
|
|
26
|
+
*
|
|
27
|
+
* Tasks are executed in phase order during lifecycle operations.
|
|
28
|
+
* Each task has a timeout to prevent hanging operations.
|
|
29
|
+
*
|
|
30
|
+
* @param phase - The lifecycle phase to add the task to
|
|
31
|
+
* @param taskName - Unique name for the task (used for logging and removal)
|
|
32
|
+
* @param task - Async function to execute during the phase
|
|
33
|
+
* @param timeoutMs - Maximum time allowed for task execution in milliseconds
|
|
34
|
+
* @example
|
|
35
|
+
* ```typescript
|
|
36
|
+
* lifecycle.addTask(StartupPhase.Services, 'start-database', async () => {
|
|
37
|
+
* await database.connect();
|
|
38
|
+
* }, 10000);
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
addTask(phase, taskName, task, timeoutMs) {
|
|
42
|
+
if (!this.canAddTask()) return;
|
|
43
|
+
const tasks = this.tasksMap.get(phase);
|
|
44
|
+
if (!tasks) throw new SeedcordError(SeedcordErrorCode.LifecycleUnknownPhase, [phase]);
|
|
45
|
+
tasks.push({
|
|
46
|
+
name: taskName,
|
|
47
|
+
task,
|
|
48
|
+
timeout: timeoutMs
|
|
49
|
+
});
|
|
50
|
+
this.logger.debug(`${chalk.italic("Added")} ${this.getTaskType()} task ${chalk.bold.cyan(taskName)} to phase ${chalk.bold.magenta(this.phaseEnum[phase])}`);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Removes a lifecycle task from a specific phase.
|
|
54
|
+
*
|
|
55
|
+
* @param phase - The lifecycle phase to remove the task from
|
|
56
|
+
* @param taskName - Name of the task to remove
|
|
57
|
+
* @returns True if the task was found and removed, false otherwise
|
|
58
|
+
*/
|
|
59
|
+
removeTask(phase, taskName) {
|
|
60
|
+
if (!this.canRemoveTask()) return false;
|
|
61
|
+
const tasks = this.tasksMap.get(phase);
|
|
62
|
+
if (!tasks) return false;
|
|
63
|
+
const initialLength = tasks.length;
|
|
64
|
+
const filteredTasks = tasks.filter((task) => task.name !== taskName);
|
|
65
|
+
this.tasksMap.set(phase, filteredTasks);
|
|
66
|
+
const removed = initialLength !== filteredTasks.length;
|
|
67
|
+
if (removed) this.logger.debug(`${chalk.italic("Removed")} ${this.getTaskType()} task ${chalk.bold.cyan(taskName)} from phase ${chalk.bold.magenta(this.phaseEnum[phase])}`);
|
|
68
|
+
return removed;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Run all tasks in a specific phase
|
|
72
|
+
*/
|
|
73
|
+
async runPhase(phase) {
|
|
74
|
+
const tasks = this.tasksMap.get(phase) ?? [];
|
|
75
|
+
if (tasks.length === 0) {
|
|
76
|
+
this.logger.warn(`No tasks to run in phase ${chalk.bold.magenta(this.phaseEnum[phase])}`);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
this.logger.info(`${chalk.bold.yellow("Running")} ${this.getTaskType()} phase ${chalk.bold.magenta(this.phaseEnum[phase])} with ${chalk.bold.cyan(tasks.length)} tasks`);
|
|
80
|
+
this.emitPhase(phase, "start");
|
|
81
|
+
const failures = (await this.executeTasksInPhase(phase, tasks)).filter((r) => r.status === "rejected").length;
|
|
82
|
+
if (failures > 0) throw new SeedcordError(SeedcordErrorCode.LifecyclePhaseFailures, [this.phaseEnum[phase], failures]);
|
|
83
|
+
this.logger.info(`Phase ${chalk.bold.magenta(this.phaseEnum[phase])} ${chalk.bold.green("completed successfully")}`);
|
|
84
|
+
this.emitPhase(phase, "complete");
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Run a single task with timeout
|
|
88
|
+
*/
|
|
89
|
+
async runTaskWithTimeout(phase, task) {
|
|
90
|
+
this.logger.info(`${chalk.italic("Starting")} task ${chalk.bold.cyan(task.name)} in phase ${chalk.bold.magenta(this.phaseEnum[phase])}`);
|
|
91
|
+
let timeoutId;
|
|
92
|
+
try {
|
|
93
|
+
await Promise.race([task.task(), new Promise((_, reject) => {
|
|
94
|
+
timeoutId = setTimeout(() => {
|
|
95
|
+
reject(new SeedcordError(SeedcordErrorCode.LifecycleTaskTimeout, [task.name, task.timeout]));
|
|
96
|
+
}, task.timeout);
|
|
97
|
+
})]);
|
|
98
|
+
this.logger.info(`${chalk.italic("Completed")} task ${chalk.bold.cyan(task.name)} in phase ${chalk.bold.magenta(this.phaseEnum[phase])}`);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
this.logger.error(`${chalk.italic("Failed")} task ${chalk.bold.cyan(task.name)} in phase ${chalk.bold.magenta(this.phaseEnum[phase])}:`, error);
|
|
101
|
+
throw error;
|
|
102
|
+
} finally {
|
|
103
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
emitPhase(phase, action) {
|
|
107
|
+
this.emitSafeRaw(`phase:${phase}:${action}`);
|
|
108
|
+
}
|
|
109
|
+
onListenerError(error, event) {
|
|
110
|
+
this.logger.error(`listener for ${String(event)} threw`, error);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
//#endregion
|
|
115
|
+
//#region src/node/Lifecycle/CoordinatedShutdown.ts
|
|
116
|
+
/**
|
|
117
|
+
* Shutdown phases for coordinated application shutdown.
|
|
118
|
+
*/
|
|
119
|
+
let ShutdownPhase = /* @__PURE__ */ function(ShutdownPhase) {
|
|
120
|
+
/** Stop accepting new requests/interactions */
|
|
121
|
+
ShutdownPhase[ShutdownPhase["StopAcceptingRequests"] = 1] = "StopAcceptingRequests";
|
|
122
|
+
/** Stop background services (health checks, etc.) */
|
|
123
|
+
ShutdownPhase[ShutdownPhase["StopServices"] = 2] = "StopServices";
|
|
124
|
+
/** Disconnect from external resources (database, APIs) */
|
|
125
|
+
ShutdownPhase[ShutdownPhase["ExternalResources"] = 3] = "ExternalResources";
|
|
126
|
+
/** Disconnect from Discord */
|
|
127
|
+
ShutdownPhase[ShutdownPhase["DiscordCleanup"] = 4] = "DiscordCleanup";
|
|
128
|
+
/** Final cleanup tasks */
|
|
129
|
+
ShutdownPhase[ShutdownPhase["FinalCleanup"] = 5] = "FinalCleanup";
|
|
130
|
+
return ShutdownPhase;
|
|
131
|
+
}({});
|
|
132
|
+
/** Define the order of phases */
|
|
133
|
+
const PHASE_ORDER$1 = [
|
|
134
|
+
1,
|
|
135
|
+
2,
|
|
136
|
+
3,
|
|
137
|
+
4,
|
|
138
|
+
5
|
|
139
|
+
];
|
|
140
|
+
const LOG_FLUSH_DELAY_MS = 500;
|
|
141
|
+
/**
|
|
142
|
+
* CoordinatedShutdown manages graceful application shutdown by executing registered tasks across defined phases.
|
|
143
|
+
*
|
|
144
|
+
* It listens for termination signals (SIGINT, SIGTERM) and runs tasks in parallel within each phase.
|
|
145
|
+
* Tasks can be added or removed dynamically, and each task has an associated timeout.
|
|
146
|
+
*/
|
|
147
|
+
var CoordinatedShutdown = class extends CoordinatedLifecycle {
|
|
148
|
+
isShutdownEnabled;
|
|
149
|
+
isShuttingDown = false;
|
|
150
|
+
exitCode = 0;
|
|
151
|
+
onSigTerm = null;
|
|
152
|
+
onSigInt = null;
|
|
153
|
+
constructor(enabled = true) {
|
|
154
|
+
super("Shutdown", PHASE_ORDER$1, ShutdownPhase);
|
|
155
|
+
this.isShutdownEnabled = enabled;
|
|
156
|
+
this.registerSignalHandlers();
|
|
157
|
+
}
|
|
158
|
+
canAddTask() {
|
|
159
|
+
return this.isShutdownEnabled;
|
|
160
|
+
}
|
|
161
|
+
canRemoveTask() {
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
getTaskType() {
|
|
165
|
+
return "shutdown";
|
|
166
|
+
}
|
|
167
|
+
async executeTasksInPhase(phase, tasks) {
|
|
168
|
+
const promises = tasks.map((task) => this.runTaskWithTimeout(phase, task));
|
|
169
|
+
return Promise.allSettled(promises);
|
|
170
|
+
}
|
|
171
|
+
registerSignalHandlers() {
|
|
172
|
+
if (!this.isShutdownEnabled) return;
|
|
173
|
+
this.onSigTerm = () => {
|
|
174
|
+
this.logger.info(`Received ${chalk.yellow.bold("SIGTERM")} signal`);
|
|
175
|
+
this.run(0);
|
|
176
|
+
};
|
|
177
|
+
this.onSigInt = () => {
|
|
178
|
+
this.logger.info(`Received ${chalk.yellow.bold("SIGINT")} signal`);
|
|
179
|
+
this.run(0);
|
|
180
|
+
};
|
|
181
|
+
process.on("SIGTERM", this.onSigTerm);
|
|
182
|
+
process.on("SIGINT", this.onSigInt);
|
|
183
|
+
}
|
|
184
|
+
removeSignalHandlers() {
|
|
185
|
+
if (this.onSigTerm) {
|
|
186
|
+
process.off("SIGTERM", this.onSigTerm);
|
|
187
|
+
this.onSigTerm = null;
|
|
188
|
+
}
|
|
189
|
+
if (this.onSigInt) {
|
|
190
|
+
process.off("SIGINT", this.onSigInt);
|
|
191
|
+
this.onSigInt = null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Adds a task to a specific shutdown phase with timeout.
|
|
196
|
+
*
|
|
197
|
+
* @param phase - The shutdown phase from {@link ShutdownPhase}
|
|
198
|
+
* @param taskName - Unique identifier for the task
|
|
199
|
+
* @param task - Async function to execute
|
|
200
|
+
* @param timeoutMs - Task timeout in milliseconds. {@default `5000`}
|
|
201
|
+
*/
|
|
202
|
+
addTask(phase, taskName, task, timeoutMs = 5e3) {
|
|
203
|
+
super.addTask(phase, taskName, task, timeoutMs);
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Removes a task from a specific shutdown phase.
|
|
207
|
+
*
|
|
208
|
+
* @param phase - The shutdown phase to remove from
|
|
209
|
+
* @param taskName - Name of the task to remove
|
|
210
|
+
* @returns True if task was found and removed
|
|
211
|
+
*/
|
|
212
|
+
removeTask(phase, taskName) {
|
|
213
|
+
return super.removeTask(phase, taskName);
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Executes the coordinated shutdown sequence.
|
|
217
|
+
*
|
|
218
|
+
* Runs all registered tasks across shutdown phases in reverse order.
|
|
219
|
+
* Tasks within each phase are executed in parallel for faster shutdown.
|
|
220
|
+
* Process exits with the specified code when complete.
|
|
221
|
+
*
|
|
222
|
+
* @param exitCode - Process exit code. {@default `0`}
|
|
223
|
+
* @param exitProcess - Whether to exit the process after shutdown. {@default `true`}
|
|
224
|
+
* @returns Promise that resolves when shutdown is complete
|
|
225
|
+
* @example
|
|
226
|
+
* ```typescript
|
|
227
|
+
* shutdown.addTask(ShutdownPhase.Services, 'database', () => db.disconnect(), 5000);
|
|
228
|
+
* await shutdown.run(0); // Graceful shutdown
|
|
229
|
+
* ```
|
|
230
|
+
*/
|
|
231
|
+
async run(exitCode = 0, exitProcess = true) {
|
|
232
|
+
this.removeSignalHandlers();
|
|
233
|
+
if (this.isShuttingDown) {
|
|
234
|
+
this.logger.warn("Shutdown sequence already in progress");
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
this.isShuttingDown = true;
|
|
238
|
+
this.exitCode = exitCode;
|
|
239
|
+
this.logger.info(`${chalk.bold.yellow("Starting")} coordinated shutdown with exit code ${chalk.bold.cyan(exitCode)}`);
|
|
240
|
+
this.emitSafe("shutdown:start");
|
|
241
|
+
try {
|
|
242
|
+
for (const phase of PHASE_ORDER$1) await this.runPhase(phase);
|
|
243
|
+
this.logger.info(`${chalk.bold.green("Coordinated shutdown completed")} successfully`);
|
|
244
|
+
this.emitSafe("shutdown:complete");
|
|
245
|
+
} catch (error) {
|
|
246
|
+
this.logger.error(`${chalk.bold.red("Coordinated shutdown failed")}`);
|
|
247
|
+
this.emitSafe("shutdown:error", error);
|
|
248
|
+
} finally {
|
|
249
|
+
if (exitProcess) {
|
|
250
|
+
this.logger.info(`${chalk.bold.red("Exiting")} process with code ${chalk.bold.cyan(this.exitCode)}`);
|
|
251
|
+
setTimeout(() => {
|
|
252
|
+
process.exit(this.exitCode);
|
|
253
|
+
}, LOG_FLUSH_DELAY_MS);
|
|
254
|
+
} else {
|
|
255
|
+
this.logger.info(`${chalk.bold.yellow("Skipping")} process exit (dev mode)`);
|
|
256
|
+
this.isShuttingDown = false;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
//#endregion
|
|
263
|
+
//#region src/node/Lifecycle/CoordinatedStartup.ts
|
|
264
|
+
/**
|
|
265
|
+
* Startup phases for coordinated initialization
|
|
266
|
+
*
|
|
267
|
+
* Defines the order in which different components are initialized during bot startup.
|
|
268
|
+
*/
|
|
269
|
+
let StartupPhase = /* @__PURE__ */ function(StartupPhase) {
|
|
270
|
+
/** Validate environment variables and config files */
|
|
271
|
+
StartupPhase[StartupPhase["Validation"] = 1] = "Validation";
|
|
272
|
+
/** Discover plugin constructors via decorators or registry */
|
|
273
|
+
StartupPhase[StartupPhase["Discovery"] = 2] = "Discovery";
|
|
274
|
+
/** Register plugin metadata and declared dependencies */
|
|
275
|
+
StartupPhase[StartupPhase["Registration"] = 3] = "Registration";
|
|
276
|
+
/** Inject and validate plugin-specific configuration */
|
|
277
|
+
StartupPhase[StartupPhase["Configuration"] = 4] = "Configuration";
|
|
278
|
+
/** Instantiate plugin classes with Core and arguments */
|
|
279
|
+
StartupPhase[StartupPhase["Instantiation"] = 5] = "Instantiation";
|
|
280
|
+
/** Activate plugins by calling their init/setup methods */
|
|
281
|
+
StartupPhase[StartupPhase["Activation"] = 6] = "Activation";
|
|
282
|
+
/** Mark seedcord as ready and start handling interactions */
|
|
283
|
+
StartupPhase[StartupPhase["Ready"] = 7] = "Ready";
|
|
284
|
+
return StartupPhase;
|
|
285
|
+
}({});
|
|
286
|
+
/** Define the order of phases */
|
|
287
|
+
const PHASE_ORDER = [
|
|
288
|
+
1,
|
|
289
|
+
2,
|
|
290
|
+
3,
|
|
291
|
+
4,
|
|
292
|
+
5,
|
|
293
|
+
6,
|
|
294
|
+
7
|
|
295
|
+
];
|
|
296
|
+
/**
|
|
297
|
+
* Manages bot startup lifecycle with ordered phases
|
|
298
|
+
*
|
|
299
|
+
* Coordinates initialization of all bot components in a predictable sequence.
|
|
300
|
+
* Tasks are executed within their designated phases to ensure proper dependency order.
|
|
301
|
+
*/
|
|
302
|
+
var CoordinatedStartup = class extends CoordinatedLifecycle {
|
|
303
|
+
isStartingUp = false;
|
|
304
|
+
hasStarted = false;
|
|
305
|
+
constructor() {
|
|
306
|
+
super("Startup", PHASE_ORDER, StartupPhase);
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Adds a task to a specific startup phase with timeout.
|
|
310
|
+
*
|
|
311
|
+
* @param phase - The startup phase from {@link StartupPhase}
|
|
312
|
+
* @param taskName - Unique identifier for the task
|
|
313
|
+
* @param task - Async function to execute
|
|
314
|
+
* @param timeoutMs - Task timeout in milliseconds. {@default `10000`}
|
|
315
|
+
*/
|
|
316
|
+
addTask(phase, taskName, task, timeoutMs = 1e4) {
|
|
317
|
+
super.addTask(phase, taskName, task, timeoutMs);
|
|
318
|
+
}
|
|
319
|
+
canAddTask() {
|
|
320
|
+
if (this.hasStarted) throw new SeedcordError(SeedcordErrorCode.LifecycleAddAfterCompletion);
|
|
321
|
+
if (this.isStartingUp) throw new SeedcordError(SeedcordErrorCode.LifecycleAddDuringRun);
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
canRemoveTask() {
|
|
325
|
+
if (this.isStartingUp) throw new SeedcordError(SeedcordErrorCode.LifecycleRemoveDuringRun);
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
getTaskType() {
|
|
329
|
+
return "startup";
|
|
330
|
+
}
|
|
331
|
+
async executeTasksInPhase(phase, tasks) {
|
|
332
|
+
const promises = tasks.map((task) => this.runTaskWithTimeout(phase, task));
|
|
333
|
+
return Promise.allSettled(promises);
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Executes the coordinated startup sequence.
|
|
337
|
+
*
|
|
338
|
+
* Runs all registered tasks across startup phases in the correct order.
|
|
339
|
+
* Each phase completes before the next phase begins. Tasks within a phase
|
|
340
|
+
* are executed sequentially to maintain predictable initialization.
|
|
341
|
+
*
|
|
342
|
+
* @returns Promise that resolves when startup is complete
|
|
343
|
+
* @throws An {@link Error} If startup fails or is called multiple times
|
|
344
|
+
* @example
|
|
345
|
+
* ```typescript
|
|
346
|
+
* const startup = new CoordinatedStartup();
|
|
347
|
+
* startup.addTask(StartupPhase.Services, 'database', () => db.connect(), 10000);
|
|
348
|
+
* await startup.run();
|
|
349
|
+
* ```
|
|
350
|
+
*/
|
|
351
|
+
async run() {
|
|
352
|
+
if (this.hasStarted) {
|
|
353
|
+
this.logger.warn("Startup sequence has already completed");
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
if (this.isStartingUp) {
|
|
357
|
+
this.logger.warn("Startup sequence already in progress");
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
this.isStartingUp = true;
|
|
361
|
+
this.logger.info(`${chalk.bold.green("Starting")} coordinated startup sequence`);
|
|
362
|
+
this.emitSafe("startup:start");
|
|
363
|
+
try {
|
|
364
|
+
for (const phase of PHASE_ORDER) {
|
|
365
|
+
if (!this.isStartingUp) {
|
|
366
|
+
this.logger.warn("Startup sequence aborted");
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
await this.runPhase(phase);
|
|
370
|
+
}
|
|
371
|
+
this.hasStarted = true;
|
|
372
|
+
this.logger.info(`${chalk.bold.green("Coordinated startup completed")} successfully`);
|
|
373
|
+
this.emitSafe("startup:complete");
|
|
374
|
+
} catch (error) {
|
|
375
|
+
if (!this.isStartingUp) {
|
|
376
|
+
this.logger.warn("Startup sequence aborted during error handling");
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
this.logger.error(`${chalk.bold.red("Coordinated startup failed")}`);
|
|
380
|
+
this.emitSafe("startup:error", error);
|
|
381
|
+
throw error;
|
|
382
|
+
} finally {
|
|
383
|
+
this.isStartingUp = false;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
async runTaskWithTimeout(phase, task) {
|
|
387
|
+
this.logger.info(`${chalk.italic("Starting")} task ${chalk.bold.cyan(task.name)} in phase ${chalk.bold.magenta(StartupPhase[phase])}`);
|
|
388
|
+
let timeoutId;
|
|
389
|
+
try {
|
|
390
|
+
await Promise.race([task.task(), new Promise((_, reject) => {
|
|
391
|
+
timeoutId = setTimeout(() => {
|
|
392
|
+
reject(new SeedcordError(SeedcordErrorCode.LifecycleTaskTimeout, [task.name, task.timeout]));
|
|
393
|
+
}, task.timeout);
|
|
394
|
+
})]);
|
|
395
|
+
this.logger.info(`${chalk.italic("Completed")} task ${chalk.bold.cyan(task.name)} in phase ${chalk.bold.magenta(StartupPhase[phase])}`);
|
|
396
|
+
} catch (error) {
|
|
397
|
+
if (!this.isStartingUp) return;
|
|
398
|
+
this.logger.error(`${chalk.italic("Failed")} task ${chalk.bold.cyan(task.name)} in phase ${chalk.bold.magenta(StartupPhase[phase])}:`, error);
|
|
399
|
+
throw error;
|
|
400
|
+
} finally {
|
|
401
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Aborts the startup sequence if it is currently running.
|
|
406
|
+
*/
|
|
407
|
+
abort() {
|
|
408
|
+
if (!this.isStartingUp) return;
|
|
409
|
+
this.isStartingUp = false;
|
|
410
|
+
this.logger.warn("Aborting coordinated startup sequence");
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Check if startup has completed
|
|
414
|
+
*/
|
|
415
|
+
get isReady() {
|
|
416
|
+
return this.hasStarted;
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Check if startup is currently running
|
|
420
|
+
*/
|
|
421
|
+
get isRunning() {
|
|
422
|
+
return this.isStartingUp;
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
//#endregion
|
|
427
|
+
//#region src/node/HealthCheck.ts
|
|
428
|
+
const HTTP_OK = 200;
|
|
429
|
+
const HTTP_NOT_FOUND = 404;
|
|
430
|
+
const DEFAULT_HEALTH_CHECK_PORT = 6967;
|
|
431
|
+
const DEFAULT_HEALTH_CHECK_PATH = "/healthcheck";
|
|
432
|
+
/**
|
|
433
|
+
* HTTP health check service for monitoring bot status.
|
|
434
|
+
*
|
|
435
|
+
* Provides a simple HTTP endpoint that responds with JSON status
|
|
436
|
+
* information, useful for container orchestration and monitoring.
|
|
437
|
+
*/
|
|
438
|
+
var HealthCheck = class {
|
|
439
|
+
/** @internal */
|
|
440
|
+
logger = new Logger("HealthCheck");
|
|
441
|
+
port;
|
|
442
|
+
path;
|
|
443
|
+
host;
|
|
444
|
+
server;
|
|
445
|
+
constructor(shutdown, options) {
|
|
446
|
+
this.port = options?.port ?? DEFAULT_HEALTH_CHECK_PORT;
|
|
447
|
+
this.path = options?.path ?? DEFAULT_HEALTH_CHECK_PATH;
|
|
448
|
+
this.host = options?.host;
|
|
449
|
+
shutdown.addTask(2, "stop-healthcheck-server", async () => await this.stop());
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Starts the health check server.
|
|
453
|
+
* @returns Promise that resolves when the server is listening
|
|
454
|
+
* @internal
|
|
455
|
+
*/
|
|
456
|
+
async init() {
|
|
457
|
+
return new Promise((resolve, reject) => {
|
|
458
|
+
const server = createServer((req, res) => {
|
|
459
|
+
if (req.method === "GET" && req.url === this.path) {
|
|
460
|
+
res.writeHead(HTTP_OK, { "Content-Type": "application/json" });
|
|
461
|
+
res.end(JSON.stringify({
|
|
462
|
+
status: "ok",
|
|
463
|
+
timestamp: Date.now()
|
|
464
|
+
}));
|
|
465
|
+
} else {
|
|
466
|
+
res.writeHead(HTTP_NOT_FOUND, { "Content-Type": "application/json" });
|
|
467
|
+
res.end(JSON.stringify({ status: "not found" }));
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
this.server = server;
|
|
471
|
+
const onListenError = (err) => reject(err);
|
|
472
|
+
server.on("error", onListenError);
|
|
473
|
+
server.once("listening", () => {
|
|
474
|
+
server.removeListener("error", onListenError);
|
|
475
|
+
server.on("error", (err) => this.logger.error("Health check server error", err));
|
|
476
|
+
const address = this.host ?? "localhost";
|
|
477
|
+
this.logger.info(`${chalk.green.bold("✓")} Health check server listening on ${chalk.cyan(`http://${address}:${this.port}${this.path}`)}`);
|
|
478
|
+
resolve();
|
|
479
|
+
});
|
|
480
|
+
if (this.host) {
|
|
481
|
+
this.logger.debug(`Binding health check server to ${this.host}`);
|
|
482
|
+
server.listen(this.port, this.host);
|
|
483
|
+
} else {
|
|
484
|
+
this.logger.debug("Binding health check server to all interfaces");
|
|
485
|
+
server.listen(this.port);
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Stops the health check server.
|
|
491
|
+
*
|
|
492
|
+
* @returns Promise that resolves when the server is closed
|
|
493
|
+
* @internal
|
|
494
|
+
*/
|
|
495
|
+
stop() {
|
|
496
|
+
const server = this.server;
|
|
497
|
+
if (!server?.listening) return Promise.resolve();
|
|
498
|
+
return new Promise((resolve, reject) => {
|
|
499
|
+
server.once("close", () => resolve());
|
|
500
|
+
server.close((err) => {
|
|
501
|
+
if (err) {
|
|
502
|
+
reject(err);
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
this.logger.info(chalk.bold.red("Health check server stopped"));
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
//#endregion
|
|
512
|
+
export { CoordinatedLifecycle, CoordinatedShutdown, CoordinatedStartup, HealthCheck, ShutdownPhase, StartupPhase };
|
|
513
|
+
//# sourceMappingURL=node.index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node.index.mjs","names":["PHASE_ORDER"],"sources":["../src/node/Lifecycle/CoordinatedLifecycle.ts","../src/node/Lifecycle/CoordinatedShutdown.ts","../src/node/Lifecycle/CoordinatedStartup.ts","../src/node/HealthCheck.ts"],"sourcesContent":["/*\n * Inspired by Akka Coordinated Shutdown: https://doc.akka.io/libraries/akka-core/current/coordinated-shutdown.html\n * and Lewis's implementation in a private repo elsewhere (https://github.com/Yomanz)\n */\n\nimport { SeedcordErrorCode } from '@seedcord/errors';\nimport { SeedcordError } from '@seedcord/errors/internal';\nimport { TypedEventEmitter } from '@seedcord/event-emitter';\nimport { Logger } from '@seedcord/logger';\nimport chalk from 'chalk';\n\nimport type { LifecycleTask } from './LifecycleTypes';\nimport type { EventMap } from '@seedcord/event-emitter';\n\n/**\n * Abstract base class for coordinated lifecycle management (startup/shutdown)\n */\nexport abstract class CoordinatedLifecycle<\n TPhase extends number,\n TEvents extends EventMap<TEvents>\n> extends TypedEventEmitter<TEvents> {\n protected readonly logger: Logger;\n protected readonly tasksMap = new Map<TPhase, LifecycleTask[]>();\n\n protected constructor(\n loggerName: string,\n protected readonly phaseOrder: TPhase[],\n protected readonly phaseEnum: Record<number, string>\n ) {\n super();\n this.logger = new Logger(loggerName);\n this.phaseOrder.forEach((phase) => this.tasksMap.set(phase, []));\n }\n\n /**\n * Adds a lifecycle task to a specific phase.\n *\n * Tasks are executed in phase order during lifecycle operations.\n * Each task has a timeout to prevent hanging operations.\n *\n * @param phase - The lifecycle phase to add the task to\n * @param taskName - Unique name for the task (used for logging and removal)\n * @param task - Async function to execute during the phase\n * @param timeoutMs - Maximum time allowed for task execution in milliseconds\n * @example\n * ```typescript\n * lifecycle.addTask(StartupPhase.Services, 'start-database', async () => {\n * await database.connect();\n * }, 10000);\n * ```\n */\n public addTask(phase: TPhase, taskName: string, task: () => Promise<void>, timeoutMs: number): void {\n if (!this.canAddTask()) return;\n\n const tasks = this.tasksMap.get(phase);\n if (!tasks) {\n throw new SeedcordError(SeedcordErrorCode.LifecycleUnknownPhase, [phase]);\n }\n\n tasks.push({ name: taskName, task, timeout: timeoutMs });\n this.logger.debug(\n `${chalk.italic('Added')} ${this.getTaskType()} task ${chalk.bold.cyan(taskName)} to phase ${chalk.bold.magenta(this.phaseEnum[phase])}`\n );\n }\n\n /**\n * Removes a lifecycle task from a specific phase.\n *\n * @param phase - The lifecycle phase to remove the task from\n * @param taskName - Name of the task to remove\n * @returns True if the task was found and removed, false otherwise\n */\n public removeTask(phase: TPhase, taskName: string): boolean {\n if (!this.canRemoveTask()) return false;\n\n const tasks = this.tasksMap.get(phase);\n if (!tasks) return false;\n\n const initialLength = tasks.length;\n const filteredTasks = tasks.filter((task) => task.name !== taskName);\n this.tasksMap.set(phase, filteredTasks);\n\n const removed = initialLength !== filteredTasks.length;\n if (removed) {\n this.logger.debug(\n `${chalk.italic('Removed')} ${this.getTaskType()} task ${chalk.bold.cyan(taskName)} from phase ${chalk.bold.magenta(this.phaseEnum[phase])}`\n );\n }\n\n return removed;\n }\n\n /**\n * Run all tasks in a specific phase\n */\n protected async runPhase(phase: TPhase): Promise<void> {\n const tasks = this.tasksMap.get(phase) ?? [];\n if (tasks.length === 0) {\n this.logger.warn(`No tasks to run in phase ${chalk.bold.magenta(this.phaseEnum[phase])}`);\n return;\n }\n\n this.logger.info(\n `${chalk.bold.yellow('Running')} ${this.getTaskType()} phase ${chalk.bold.magenta(this.phaseEnum[phase])} with ${chalk.bold.cyan(tasks.length)} tasks`\n );\n this.emitPhase(phase, 'start');\n\n const results: PromiseSettledResult<void>[] = await this.executeTasksInPhase(phase, tasks);\n\n const failures = results.filter((r) => r.status === 'rejected').length;\n if (failures > 0) {\n // raw phase name here. chalk's ANSI codes would otherwise leak into the serialized\n // error message (e.g. the unknown-exception webhook payload).\n throw new SeedcordError(SeedcordErrorCode.LifecyclePhaseFailures, [this.phaseEnum[phase], failures]);\n }\n\n this.logger.info(\n `Phase ${chalk.bold.magenta(this.phaseEnum[phase])} ${chalk.bold.green('completed successfully')}`\n );\n\n this.emitPhase(phase, 'complete');\n }\n\n /**\n * Run a single task with timeout\n */\n protected async runTaskWithTimeout(phase: TPhase, task: LifecycleTask): Promise<void> {\n this.logger.info(\n `${chalk.italic('Starting')} task ${chalk.bold.cyan(task.name)} in phase ${chalk.bold.magenta(this.phaseEnum[phase])}`\n );\n\n let timeoutId: NodeJS.Timeout | undefined;\n\n try {\n await Promise.race([\n task.task(),\n new Promise<void>((_, reject) => {\n timeoutId = setTimeout(() => {\n reject(new SeedcordError(SeedcordErrorCode.LifecycleTaskTimeout, [task.name, task.timeout]));\n }, task.timeout);\n })\n ]);\n\n this.logger.info(\n `${chalk.italic('Completed')} task ${chalk.bold.cyan(task.name)} in phase ${chalk.bold.magenta(this.phaseEnum[phase])}`\n );\n } catch (error) {\n this.logger.error(\n `${chalk.italic('Failed')} task ${chalk.bold.cyan(task.name)} in phase ${chalk.bold.magenta(this.phaseEnum[phase])}:`,\n error\n );\n throw error;\n } finally {\n if (timeoutId) {\n clearTimeout(timeoutId);\n }\n }\n }\n\n // The phase key is interpolated from phaseOrder at runtime and is always a valid no-payload key of\n // the subclass event map, but TS can't correlate a template-literal key with the generic TEvents.\n private emitPhase(phase: TPhase, action: 'start' | 'complete'): void {\n this.emitSafeRaw(`phase:${phase}:${action}`);\n }\n\n protected override onListenerError(error: unknown, event: string | symbol): void {\n this.logger.error(`listener for ${String(event)} threw`, error);\n }\n\n // Abstract methods to be implemented by subclasses\n protected abstract canAddTask(): boolean;\n protected abstract canRemoveTask(): boolean;\n protected abstract getTaskType(): string;\n protected abstract executeTasksInPhase(\n phase: TPhase,\n tasks: LifecycleTask[]\n ): Promise<PromiseSettledResult<void>[]>;\n}\n","import chalk from 'chalk';\n\nimport { CoordinatedLifecycle } from './CoordinatedLifecycle';\n\nimport type { LifecycleTask, PhaseEventMap } from './LifecycleTypes';\nimport type { UnionToTuple } from 'type-fest';\n\n/**\n * Shutdown phases for coordinated application shutdown.\n */\nexport enum ShutdownPhase {\n /** Stop accepting new requests/interactions */\n StopAcceptingRequests = 1,\n /** Stop background services (health checks, etc.) */\n StopServices,\n /** Disconnect from external resources (database, APIs) */\n ExternalResources,\n /** Disconnect from Discord */\n DiscordCleanup,\n /** Final cleanup tasks */\n FinalCleanup\n}\n\n/** Define the order of phases */\nconst PHASE_ORDER: ShutdownPhase[] = [\n ShutdownPhase.StopAcceptingRequests,\n ShutdownPhase.StopServices,\n ShutdownPhase.ExternalResources,\n ShutdownPhase.DiscordCleanup,\n ShutdownPhase.FinalCleanup\n];\n\n/**\n * Strict-event-emitter payload map for coordinated shutdown phases.\n */\nexport type CoordinatedShutdownEvents = PhaseEventMap<'shutdown', UnionToTuple<ShutdownPhase>>;\n\n// Delay process.exit so the logger's file sink has a window to flush before the event loop dies.\nconst LOG_FLUSH_DELAY_MS = 500;\n\n/**\n * CoordinatedShutdown manages graceful application shutdown by executing registered tasks across defined phases.\n *\n * It listens for termination signals (SIGINT, SIGTERM) and runs tasks in parallel within each phase.\n * Tasks can be added or removed dynamically, and each task has an associated timeout.\n */\nexport class CoordinatedShutdown extends CoordinatedLifecycle<ShutdownPhase, CoordinatedShutdownEvents> {\n private readonly isShutdownEnabled: boolean;\n\n private isShuttingDown = false;\n private exitCode = 0;\n private onSigTerm: (() => void) | null = null;\n private onSigInt: (() => void) | null = null;\n\n public constructor(enabled = true) {\n super('Shutdown', PHASE_ORDER, ShutdownPhase);\n\n this.isShutdownEnabled = enabled;\n this.registerSignalHandlers();\n }\n\n protected canAddTask(): boolean {\n return this.isShutdownEnabled;\n }\n\n protected canRemoveTask(): boolean {\n return true;\n }\n\n protected getTaskType(): string {\n return 'shutdown';\n }\n\n protected async executeTasksInPhase(\n phase: ShutdownPhase,\n tasks: LifecycleTask[]\n ): Promise<PromiseSettledResult<void>[]> {\n const promises = tasks.map((task) => this.runTaskWithTimeout(phase, task));\n return Promise.allSettled(promises);\n }\n\n private registerSignalHandlers(): void {\n if (!this.isShutdownEnabled) return;\n\n this.onSigTerm = () => {\n this.logger.info(`Received ${chalk.yellow.bold('SIGTERM')} signal`);\n void this.run(0);\n };\n\n this.onSigInt = () => {\n this.logger.info(`Received ${chalk.yellow.bold('SIGINT')} signal`);\n void this.run(0);\n };\n\n process.on('SIGTERM', this.onSigTerm);\n process.on('SIGINT', this.onSigInt);\n }\n\n private removeSignalHandlers(): void {\n if (this.onSigTerm) {\n process.off('SIGTERM', this.onSigTerm);\n this.onSigTerm = null;\n }\n if (this.onSigInt) {\n process.off('SIGINT', this.onSigInt);\n this.onSigInt = null;\n }\n }\n\n /**\n * Adds a task to a specific shutdown phase with timeout.\n *\n * @param phase - The shutdown phase from {@link ShutdownPhase}\n * @param taskName - Unique identifier for the task\n * @param task - Async function to execute\n * @param timeoutMs - Task timeout in milliseconds. {@default `5000`}\n */\n public override addTask(phase: ShutdownPhase, taskName: string, task: () => Promise<void>, timeoutMs = 5000): void {\n super.addTask(phase, taskName, task, timeoutMs);\n }\n\n /**\n * Removes a task from a specific shutdown phase.\n *\n * @param phase - The shutdown phase to remove from\n * @param taskName - Name of the task to remove\n * @returns True if task was found and removed\n */\n public override removeTask(phase: ShutdownPhase, taskName: string): boolean {\n return super.removeTask(phase, taskName);\n }\n\n /**\n * Executes the coordinated shutdown sequence.\n *\n * Runs all registered tasks across shutdown phases in reverse order.\n * Tasks within each phase are executed in parallel for faster shutdown.\n * Process exits with the specified code when complete.\n *\n * @param exitCode - Process exit code. {@default `0`}\n * @param exitProcess - Whether to exit the process after shutdown. {@default `true`}\n * @returns Promise that resolves when shutdown is complete\n * @example\n * ```typescript\n * shutdown.addTask(ShutdownPhase.Services, 'database', () => db.disconnect(), 5000);\n * await shutdown.run(0); // Graceful shutdown\n * ```\n */\n public async run(exitCode = 0, exitProcess = true): Promise<void> {\n this.removeSignalHandlers();\n\n if (this.isShuttingDown) {\n this.logger.warn('Shutdown sequence already in progress');\n return;\n }\n\n this.isShuttingDown = true;\n this.exitCode = exitCode;\n this.logger.info(\n `${chalk.bold.yellow('Starting')} coordinated shutdown with exit code ${chalk.bold.cyan(exitCode)}`\n );\n this.emitSafe('shutdown:start');\n\n try {\n for (const phase of PHASE_ORDER) {\n await this.runPhase(phase);\n }\n\n this.logger.info(`${chalk.bold.green('Coordinated shutdown completed')} successfully`);\n this.emitSafe('shutdown:complete');\n } catch (error) {\n this.logger.error(`${chalk.bold.red('Coordinated shutdown failed')}`);\n this.emitSafe('shutdown:error', error);\n } finally {\n if (exitProcess) {\n this.logger.info(`${chalk.bold.red('Exiting')} process with code ${chalk.bold.cyan(this.exitCode)}`);\n setTimeout(() => {\n process.exit(this.exitCode);\n }, LOG_FLUSH_DELAY_MS);\n } else {\n this.logger.info(`${chalk.bold.yellow('Skipping')} process exit (dev mode)`);\n this.isShuttingDown = false;\n }\n }\n }\n}\n","import { SeedcordErrorCode } from '@seedcord/errors';\nimport { SeedcordError } from '@seedcord/errors/internal';\nimport chalk from 'chalk';\n\nimport { CoordinatedLifecycle } from './CoordinatedLifecycle';\n\nimport type { LifecycleTask, PhaseEventMap } from './LifecycleTypes';\nimport type { UnionToTuple } from 'type-fest';\n\n/**\n * Startup phases for coordinated initialization\n *\n * Defines the order in which different components are initialized during bot startup.\n */\nexport enum StartupPhase {\n /** Validate environment variables and config files */\n Validation = 1,\n /** Discover plugin constructors via decorators or registry */\n Discovery,\n /** Register plugin metadata and declared dependencies */\n Registration,\n /** Inject and validate plugin-specific configuration */\n Configuration,\n /** Instantiate plugin classes with Core and arguments */\n Instantiation,\n /** Activate plugins by calling their init/setup methods */\n Activation,\n /** Mark seedcord as ready and start handling interactions */\n Ready\n}\n\n/** Define the order of phases */\nconst PHASE_ORDER: StartupPhase[] = [\n StartupPhase.Validation,\n StartupPhase.Discovery,\n StartupPhase.Registration,\n StartupPhase.Configuration,\n StartupPhase.Instantiation,\n StartupPhase.Activation,\n StartupPhase.Ready\n];\n\n/**\n * Strict-event-emitter payload map for coordinated startup phases.\n */\nexport type CoordinatedStartupEvents = PhaseEventMap<'startup', UnionToTuple<StartupPhase>>;\n\n/**\n * Manages bot startup lifecycle with ordered phases\n *\n * Coordinates initialization of all bot components in a predictable sequence.\n * Tasks are executed within their designated phases to ensure proper dependency order.\n */\nexport class CoordinatedStartup extends CoordinatedLifecycle<StartupPhase, CoordinatedStartupEvents> {\n private isStartingUp = false;\n private hasStarted = false;\n\n public constructor() {\n super('Startup', PHASE_ORDER, StartupPhase);\n }\n\n /**\n * Adds a task to a specific startup phase with timeout.\n *\n * @param phase - The startup phase from {@link StartupPhase}\n * @param taskName - Unique identifier for the task\n * @param task - Async function to execute\n * @param timeoutMs - Task timeout in milliseconds. {@default `10000`}\n */\n public override addTask(\n phase: StartupPhase,\n taskName: string,\n task: () => Promise<void>,\n timeoutMs = 10_000\n ): void {\n super.addTask(phase, taskName, task, timeoutMs);\n }\n\n protected canAddTask(): boolean {\n if (this.hasStarted) {\n throw new SeedcordError(SeedcordErrorCode.LifecycleAddAfterCompletion);\n }\n\n if (this.isStartingUp) {\n throw new SeedcordError(SeedcordErrorCode.LifecycleAddDuringRun);\n }\n\n return true;\n }\n\n protected canRemoveTask(): boolean {\n if (this.isStartingUp) {\n throw new SeedcordError(SeedcordErrorCode.LifecycleRemoveDuringRun);\n }\n\n return true;\n }\n\n protected getTaskType(): string {\n return 'startup';\n }\n\n protected async executeTasksInPhase(\n phase: StartupPhase,\n tasks: LifecycleTask[]\n ): Promise<PromiseSettledResult<void>[]> {\n const promises = tasks.map((task) => this.runTaskWithTimeout(phase, task));\n return Promise.allSettled(promises);\n }\n\n /**\n * Executes the coordinated startup sequence.\n *\n * Runs all registered tasks across startup phases in the correct order.\n * Each phase completes before the next phase begins. Tasks within a phase\n * are executed sequentially to maintain predictable initialization.\n *\n * @returns Promise that resolves when startup is complete\n * @throws An {@link Error} If startup fails or is called multiple times\n * @example\n * ```typescript\n * const startup = new CoordinatedStartup();\n * startup.addTask(StartupPhase.Services, 'database', () => db.connect(), 10000);\n * await startup.run();\n * ```\n */\n public async run(): Promise<void> {\n if (this.hasStarted) {\n this.logger.warn('Startup sequence has already completed');\n return;\n }\n\n if (this.isStartingUp) {\n this.logger.warn('Startup sequence already in progress');\n return;\n }\n\n this.isStartingUp = true;\n this.logger.info(`${chalk.bold.green('Starting')} coordinated startup sequence`);\n this.emitSafe('startup:start');\n\n try {\n for (const phase of PHASE_ORDER) {\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- abort() can flip isStartingUp to false mid-loop from the cli\n if (!this.isStartingUp) {\n this.logger.warn('Startup sequence aborted');\n return;\n }\n await this.runPhase(phase);\n }\n\n this.hasStarted = true;\n this.logger.info(`${chalk.bold.green('Coordinated startup completed')} successfully`);\n this.emitSafe('startup:complete');\n } catch (error) {\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- abort() can flip isStartingUp to false before this catch runs\n if (!this.isStartingUp) {\n this.logger.warn('Startup sequence aborted during error handling');\n return;\n }\n this.logger.error(`${chalk.bold.red('Coordinated startup failed')}`);\n this.emitSafe('startup:error', error);\n throw error;\n } finally {\n this.isStartingUp = false;\n }\n }\n\n protected override async runTaskWithTimeout(phase: StartupPhase, task: LifecycleTask): Promise<void> {\n this.logger.info(\n `${chalk.italic('Starting')} task ${chalk.bold.cyan(task.name)} in phase ${chalk.bold.magenta(StartupPhase[phase])}`\n );\n\n let timeoutId: NodeJS.Timeout | undefined;\n\n try {\n await Promise.race([\n task.task(),\n new Promise<void>((_, reject) => {\n timeoutId = setTimeout(() => {\n reject(new SeedcordError(SeedcordErrorCode.LifecycleTaskTimeout, [task.name, task.timeout]));\n }, task.timeout);\n })\n ]);\n\n this.logger.info(\n `${chalk.italic('Completed')} task ${chalk.bold.cyan(task.name)} in phase ${chalk.bold.magenta(StartupPhase[phase])}`\n );\n } catch (error) {\n if (!this.isStartingUp) {\n return;\n }\n\n this.logger.error(\n `${chalk.italic('Failed')} task ${chalk.bold.cyan(task.name)} in phase ${chalk.bold.magenta(StartupPhase[phase])}:`,\n error\n );\n throw error;\n } finally {\n if (timeoutId) {\n clearTimeout(timeoutId);\n }\n }\n }\n\n /**\n * Aborts the startup sequence if it is currently running.\n */\n public abort(): void {\n if (!this.isStartingUp) return;\n\n this.isStartingUp = false;\n this.logger.warn('Aborting coordinated startup sequence');\n }\n\n /**\n * Check if startup has completed\n */\n public get isReady(): boolean {\n return this.hasStarted;\n }\n\n /**\n * Check if startup is currently running\n */\n public get isRunning(): boolean {\n return this.isStartingUp;\n }\n}\n","import { createServer } from 'node:http';\n\nimport { Logger } from '@seedcord/logger';\nimport chalk from 'chalk';\n\nimport { ShutdownPhase } from './Lifecycle/CoordinatedShutdown';\n\nimport type { CoordinatedShutdown } from './Lifecycle/CoordinatedShutdown';\nimport type { HealthCheckConfig } from '@seedcord/types';\nimport type { IncomingMessage, Server, ServerResponse } from 'node:http';\n\nconst HTTP_OK = 200;\nconst HTTP_NOT_FOUND = 404;\n\nconst DEFAULT_HEALTH_CHECK_PORT = 6967;\nconst DEFAULT_HEALTH_CHECK_PATH = '/healthcheck';\n\n/**\n * HTTP health check service for monitoring bot status.\n *\n * Provides a simple HTTP endpoint that responds with JSON status\n * information, useful for container orchestration and monitoring.\n */\nexport class HealthCheck {\n /** @internal */\n public readonly logger = new Logger('HealthCheck');\n\n public readonly port: number;\n public readonly path: string;\n public readonly host: string | undefined;\n\n private server?: Server;\n\n constructor(shutdown: CoordinatedShutdown, options?: HealthCheckConfig) {\n this.port = options?.port ?? DEFAULT_HEALTH_CHECK_PORT;\n this.path = options?.path ?? DEFAULT_HEALTH_CHECK_PATH;\n this.host = options?.host;\n\n shutdown.addTask(ShutdownPhase.StopServices, 'stop-healthcheck-server', async () => await this.stop());\n }\n\n /**\n * Starts the health check server.\n * @returns Promise that resolves when the server is listening\n * @internal\n */\n public async init(): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n const server = createServer((req: IncomingMessage, res: ServerResponse) => {\n if (req.method === 'GET' && req.url === this.path) {\n res.writeHead(HTTP_OK, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ status: 'ok', timestamp: Date.now() }));\n } else {\n res.writeHead(HTTP_NOT_FOUND, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ status: 'not found' }));\n }\n });\n this.server = server;\n\n const onListenError = (err: Error): void => reject(err);\n server.on('error', onListenError);\n\n server.once('listening', () => {\n // Swap the listen-time reject handler for a logging one: keeping it would reject an\n // already-settled promise on a late error, and removing it without a replacement\n // would crash the process on an unhandled 'error' event.\n server.removeListener('error', onListenError);\n server.on('error', (err) => this.logger.error('Health check server error', err));\n\n const address = this.host ?? 'localhost';\n this.logger.info(\n `${chalk.green.bold('✓')} Health check server listening on ${chalk.cyan(`http://${address}:${this.port}${this.path}`)}`\n );\n resolve();\n });\n\n if (this.host) {\n this.logger.debug(`Binding health check server to ${this.host}`);\n server.listen(this.port, this.host);\n } else {\n this.logger.debug('Binding health check server to all interfaces');\n server.listen(this.port);\n }\n });\n }\n\n /**\n * Stops the health check server.\n *\n * @returns Promise that resolves when the server is closed\n * @internal\n */\n public stop(): Promise<void> {\n const server = this.server;\n // close() on a non-listening server invokes its callback with ERR_SERVER_NOT_RUNNING and\n // never fires 'close', so without this guard the promise would hang the shutdown phase.\n if (!server?.listening) return Promise.resolve();\n\n return new Promise((resolve, reject) => {\n server.once('close', () => resolve());\n server.close((err) => {\n if (err) {\n reject(err);\n return;\n }\n this.logger.info(chalk.bold.red('Health check server stopped'));\n });\n });\n }\n}\n"],"mappings":"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"}
|