@seedcord/core 0.1.0-next.4 → 0.1.0-next.6
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-eKhIe6Qt.d.mts} +496 -163
- package/dist/Plugin-C2-VHrsX.cjs +114 -0
- package/dist/Plugin-C2-VHrsX.cjs.map +1 -0
- package/dist/Plugin-CNyRKDHT.d.mts +226 -0
- package/dist/Plugin-PcdPjAzc.mjs +97 -0
- package/dist/Plugin-PcdPjAzc.mjs.map +1 -0
- package/dist/devChannel-CQU7hq7Q.mjs +14 -0
- package/dist/devChannel-CQU7hq7Q.mjs.map +1 -0
- package/dist/devChannel-CoCmAVDx.cjs +26 -0
- package/dist/devChannel-CoCmAVDx.cjs.map +1 -0
- package/dist/index.cjs +239 -105
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +182 -25
- package/dist/index.mjs +199 -79
- package/dist/index.mjs.map +1 -1
- package/dist/internal.index.cjs +463 -57
- package/dist/internal.index.cjs.map +1 -1
- package/dist/internal.index.d.mts +146 -28
- package/dist/internal.index.mjs +415 -29
- package/dist/internal.index.mjs.map +1 -1
- package/dist/is-never-Ddaba1Su.d.mts +58 -0
- package/dist/node-internal.index.cjs +645 -0
- package/dist/node-internal.index.cjs.map +1 -0
- package/dist/node-internal.index.d.cts +1 -0
- package/dist/node-internal.index.d.mts +2352 -0
- package/dist/node-internal.index.mjs +636 -0
- package/dist/node-internal.index.mjs.map +1 -0
- package/dist/{CustomId-DV7daU7r.cjs → permissions-DSW_EZfN.cjs} +448 -89
- package/dist/permissions-DSW_EZfN.cjs.map +1 -0
- package/dist/{CustomId-epJayEGO.mjs → permissions-RVCBhL2n.mjs} +341 -90
- package/dist/permissions-RVCBhL2n.mjs.map +1 -0
- package/dist/phases-BfpAG6w3.mjs +31 -0
- package/dist/phases-BfpAG6w3.mjs.map +1 -0
- package/dist/phases-C_BGNyVo.cjs +43 -0
- package/dist/phases-C_BGNyVo.cjs.map +1 -0
- package/dist/plugin.index.cjs +4 -0
- package/dist/plugin.index.d.cts +1 -0
- package/dist/plugin.index.d.mts +3 -0
- package/dist/plugin.index.mjs +3 -0
- package/package.json +32 -10
- package/dist/CustomId-DV7daU7r.cjs.map +0 -1
- package/dist/CustomId-epJayEGO.mjs.map +0 -1
|
@@ -0,0 +1,636 @@
|
|
|
1
|
+
import { n as StartupPhase, t as ShutdownPhase } from "./phases-BfpAG6w3.mjs";
|
|
2
|
+
import { n as resolvedLifecycleSpecOf, r as finalizePluginContext } from "./Plugin-PcdPjAzc.mjs";
|
|
3
|
+
import { SeedcordErrorCode } from "@seedcord/errors";
|
|
4
|
+
import { SeedcordError } from "@seedcord/errors/internal";
|
|
5
|
+
import { Logger, paint } from "@seedcord/logger";
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
import { TypedEventEmitter } from "@seedcord/event-emitter";
|
|
8
|
+
import { createServer } from "node:http";
|
|
9
|
+
|
|
10
|
+
//#region src/node/Lifecycle/withTimeout.ts
|
|
11
|
+
async function raceTimer(work, timeoutMs, onTimeout) {
|
|
12
|
+
let timer;
|
|
13
|
+
try {
|
|
14
|
+
await Promise.race([work, new Promise((resolve, reject) => {
|
|
15
|
+
timer = setTimeout(() => onTimeout(resolve, reject), timeoutMs);
|
|
16
|
+
})]);
|
|
17
|
+
} finally {
|
|
18
|
+
if (timer) clearTimeout(timer);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/** @internal rejects with a `LifecycleTaskTimeout` when `run` outlasts the bound. */
|
|
22
|
+
function withTimeout(name, run, timeoutMs) {
|
|
23
|
+
return raceTimer(run(), timeoutMs, (_, reject) => {
|
|
24
|
+
reject(new SeedcordError(SeedcordErrorCode.LifecycleTaskTimeout, [name, timeoutMs]));
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
/** @internal resolves when `work` settles or the bound elapses, whichever is first. */
|
|
28
|
+
function settleWithin(work, timeoutMs) {
|
|
29
|
+
return raceTimer(work, timeoutMs, (resolve) => resolve());
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
//#endregion
|
|
33
|
+
//#region src/node/Lifecycle/CoordinatedLifecycle.ts
|
|
34
|
+
/**
|
|
35
|
+
* Base for the startup and shutdown coordinators. Runs phase-ordered tasks off a per-phase map.
|
|
36
|
+
*
|
|
37
|
+
* @internal
|
|
38
|
+
*/
|
|
39
|
+
var CoordinatedLifecycle = class extends TypedEventEmitter {
|
|
40
|
+
phaseOrder;
|
|
41
|
+
phaseEnum;
|
|
42
|
+
logger;
|
|
43
|
+
tasksMap = /* @__PURE__ */ new Map();
|
|
44
|
+
constructor(loggerName, phaseOrder, phaseEnum) {
|
|
45
|
+
super();
|
|
46
|
+
this.phaseOrder = phaseOrder;
|
|
47
|
+
this.phaseEnum = phaseEnum;
|
|
48
|
+
this.logger = new Logger(loggerName);
|
|
49
|
+
this.phaseOrder.forEach((phase) => this.tasksMap.set(phase, []));
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Adds a task to `phase`. Tasks run in phase order, each bounded by `timeoutMs`.
|
|
53
|
+
*
|
|
54
|
+
* @param phase - The lifecycle phase to add the task to
|
|
55
|
+
* @param taskName - Unique name for the task, used for logging and removal
|
|
56
|
+
* @param task - Async function to execute during the phase
|
|
57
|
+
* @param timeoutMs - Maximum time allowed for task execution in milliseconds
|
|
58
|
+
*/
|
|
59
|
+
addTask(phase, taskName, task, timeoutMs) {
|
|
60
|
+
if (!this.canAddTask()) return;
|
|
61
|
+
const tasks = this.tasksMap.get(phase);
|
|
62
|
+
if (!tasks) throw new SeedcordError(SeedcordErrorCode.LifecycleUnknownPhase, [phase]);
|
|
63
|
+
tasks.push({
|
|
64
|
+
name: taskName,
|
|
65
|
+
task,
|
|
66
|
+
timeout: timeoutMs
|
|
67
|
+
});
|
|
68
|
+
this.logger.debug(`${chalk.italic("Added")} ${this.getTaskType()} task ${chalk.bold.cyan(taskName)} to phase ${chalk.bold.magenta(this.phaseEnum[phase])}`);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Removes a lifecycle task from a specific phase.
|
|
72
|
+
*
|
|
73
|
+
* @param phase - The lifecycle phase to remove the task from
|
|
74
|
+
* @param taskName - Name of the task to remove
|
|
75
|
+
* @returns True if the task was found and removed, false otherwise
|
|
76
|
+
*/
|
|
77
|
+
removeTask(phase, taskName) {
|
|
78
|
+
if (!this.canRemoveTask()) return false;
|
|
79
|
+
const tasks = this.tasksMap.get(phase);
|
|
80
|
+
if (!tasks) return false;
|
|
81
|
+
const initialLength = tasks.length;
|
|
82
|
+
const filteredTasks = tasks.filter((task) => task.name !== taskName);
|
|
83
|
+
this.tasksMap.set(phase, filteredTasks);
|
|
84
|
+
const removed = initialLength !== filteredTasks.length;
|
|
85
|
+
if (removed) this.logger.debug(`${chalk.italic("Removed")} ${this.getTaskType()} task ${chalk.bold.cyan(taskName)} from phase ${chalk.bold.magenta(this.phaseEnum[phase])}`);
|
|
86
|
+
return removed;
|
|
87
|
+
}
|
|
88
|
+
async runPhase(phase) {
|
|
89
|
+
const tasks = this.tasksMap.get(phase) ?? [];
|
|
90
|
+
if (tasks.length === 0) {
|
|
91
|
+
this.logger.debug(`No tasks to run in phase ${chalk.bold.magenta(this.phaseEnum[phase])}`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
this.logger.info(`${chalk.bold.yellow("Running")} ${this.getTaskType()} phase ${chalk.bold.magenta(this.phaseEnum[phase])} with ${chalk.bold.cyan(tasks.length)} tasks`);
|
|
95
|
+
this.emitPhase(phase, "start");
|
|
96
|
+
const failures = (await this.executeTasksInPhase(phase, tasks)).filter((r) => r.status === "rejected").length;
|
|
97
|
+
if (failures > 0) throw new SeedcordError(SeedcordErrorCode.LifecyclePhaseFailures, [this.phaseEnum[phase], failures]);
|
|
98
|
+
this.logger.info(`Phase ${chalk.bold.magenta(this.phaseEnum[phase])} ${chalk.bold.green("completed successfully")}`);
|
|
99
|
+
this.emitPhase(phase, "complete");
|
|
100
|
+
}
|
|
101
|
+
async runTaskWithTimeout(phase, task) {
|
|
102
|
+
this.logger.info(`${chalk.italic("Starting")} task ${chalk.bold.cyan(task.name)} in phase ${chalk.bold.magenta(this.phaseEnum[phase])}`);
|
|
103
|
+
try {
|
|
104
|
+
await withTimeout(task.name, task.task, task.timeout);
|
|
105
|
+
this.logger.info(`${chalk.italic("Completed")} task ${chalk.bold.cyan(task.name)} in phase ${chalk.bold.magenta(this.phaseEnum[phase])}`);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
this.logger.error(`${chalk.italic("Failed")} task ${chalk.bold.cyan(task.name)} in phase ${chalk.bold.magenta(this.phaseEnum[phase])}:`, error);
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
emitPhase(phase, action) {
|
|
112
|
+
this.emitSafeRaw(`phase:${phase}:${action}`);
|
|
113
|
+
}
|
|
114
|
+
onListenerError(error, event) {
|
|
115
|
+
this.logger.error(`listener for ${String(event)} threw`, error);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
//#endregion
|
|
120
|
+
//#region src/node/Lifecycle/CoordinatedShutdown.ts
|
|
121
|
+
const PHASE_ORDER$1 = [
|
|
122
|
+
1,
|
|
123
|
+
2,
|
|
124
|
+
3,
|
|
125
|
+
4
|
|
126
|
+
];
|
|
127
|
+
const LOG_FLUSH_DELAY_MS = 3e3;
|
|
128
|
+
/**
|
|
129
|
+
* Runs registered shutdown tasks across `ShutdownPhase` in order. Registers SIGINT and SIGTERM
|
|
130
|
+
* handlers. Tasks within a phase run in parallel.
|
|
131
|
+
*/
|
|
132
|
+
var CoordinatedShutdown = class extends CoordinatedLifecycle {
|
|
133
|
+
isShuttingDown = false;
|
|
134
|
+
hasShutdown = false;
|
|
135
|
+
exitCode = 0;
|
|
136
|
+
onSigTerm = null;
|
|
137
|
+
onSigInt = null;
|
|
138
|
+
startupGate;
|
|
139
|
+
constructor() {
|
|
140
|
+
super("Shutdown", PHASE_ORDER$1, ShutdownPhase);
|
|
141
|
+
this.registerSignalHandlers();
|
|
142
|
+
}
|
|
143
|
+
canAddTask() {
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
canRemoveTask() {
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
getTaskType() {
|
|
150
|
+
return "shutdown";
|
|
151
|
+
}
|
|
152
|
+
async executeTasksInPhase(phase, tasks) {
|
|
153
|
+
const promises = tasks.map((task) => this.runTaskWithTimeout(phase, task));
|
|
154
|
+
return Promise.allSettled(promises);
|
|
155
|
+
}
|
|
156
|
+
registerSignalHandlers() {
|
|
157
|
+
this.onSigTerm = () => {
|
|
158
|
+
this.logger.info(`Received ${chalk.yellow.bold("SIGTERM")} signal`);
|
|
159
|
+
this.run(0);
|
|
160
|
+
};
|
|
161
|
+
this.onSigInt = () => {
|
|
162
|
+
this.logger.info(`Received ${chalk.yellow.bold("SIGINT")} signal`);
|
|
163
|
+
this.run(0);
|
|
164
|
+
};
|
|
165
|
+
process.on("SIGTERM", this.onSigTerm);
|
|
166
|
+
process.on("SIGINT", this.onSigInt);
|
|
167
|
+
}
|
|
168
|
+
/** @internal */
|
|
169
|
+
removeSignalHandlers() {
|
|
170
|
+
if (this.onSigTerm) {
|
|
171
|
+
process.off("SIGTERM", this.onSigTerm);
|
|
172
|
+
this.onSigTerm = null;
|
|
173
|
+
}
|
|
174
|
+
if (this.onSigInt) {
|
|
175
|
+
process.off("SIGINT", this.onSigInt);
|
|
176
|
+
this.onSigInt = null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/** @internal run() awaits this so a shutdown during startup waits for startup to register its dispose tasks */
|
|
180
|
+
gateOnStartup(settled) {
|
|
181
|
+
this.startupGate = settled;
|
|
182
|
+
}
|
|
183
|
+
/** Adds a shutdown-phase task. @param timeoutMs - Task timeout in ms. {@default 5000} */
|
|
184
|
+
addTask(phase, taskName, task, timeoutMs = 5e3) {
|
|
185
|
+
super.addTask(phase, taskName, task, timeoutMs);
|
|
186
|
+
}
|
|
187
|
+
/** Removes a shutdown-phase task, returning whether one was removed. */
|
|
188
|
+
removeTask(phase, taskName) {
|
|
189
|
+
return super.removeTask(phase, taskName);
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Runs registered tasks across shutdown phases in `ShutdownPhase` order.
|
|
193
|
+
* Tasks within each phase run in parallel.
|
|
194
|
+
*
|
|
195
|
+
* @param exitCode - Process exit code. {@default `0`}
|
|
196
|
+
* @param exitProcess - Whether to exit the process after shutdown. {@default `true`}
|
|
197
|
+
* @example
|
|
198
|
+
* ```typescript
|
|
199
|
+
* shutdown.addTask(ShutdownPhase.Disconnect, 'database', () => db.disconnect(), 5000);
|
|
200
|
+
* await shutdown.run(0);
|
|
201
|
+
* ```
|
|
202
|
+
*/
|
|
203
|
+
async run(exitCode = 0, exitProcess = true) {
|
|
204
|
+
this.removeSignalHandlers();
|
|
205
|
+
if (this.hasShutdown || this.isShuttingDown) {
|
|
206
|
+
this.logger.warn("Shutdown sequence already ran or is in progress");
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
this.isShuttingDown = true;
|
|
210
|
+
this.exitCode = exitCode;
|
|
211
|
+
this.logger.info(`${chalk.bold.yellow("Starting")} coordinated shutdown with exit code ${chalk.bold.cyan(exitCode)}`);
|
|
212
|
+
this.emitSafe("shutdown:start");
|
|
213
|
+
try {
|
|
214
|
+
if (this.startupGate) await this.startupGate;
|
|
215
|
+
const failures = [];
|
|
216
|
+
for (const phase of PHASE_ORDER$1) try {
|
|
217
|
+
await this.runPhase(phase);
|
|
218
|
+
} catch (error) {
|
|
219
|
+
failures.push(error);
|
|
220
|
+
}
|
|
221
|
+
if (failures.length > 0) this.emitFailures(failures);
|
|
222
|
+
else {
|
|
223
|
+
this.logger.info(`${chalk.bold.green("Coordinated shutdown completed")} successfully`);
|
|
224
|
+
this.emitSafe("shutdown:complete");
|
|
225
|
+
}
|
|
226
|
+
} finally {
|
|
227
|
+
this.hasShutdown = true;
|
|
228
|
+
if (exitProcess) {
|
|
229
|
+
this.logger.info(`${chalk.bold.red("Exiting")} process with code ${chalk.bold.cyan(this.exitCode)}`);
|
|
230
|
+
setTimeout(() => {
|
|
231
|
+
process.exit(this.exitCode);
|
|
232
|
+
}, LOG_FLUSH_DELAY_MS);
|
|
233
|
+
} else {
|
|
234
|
+
this.logger.info(`${chalk.bold.yellow("Skipping")} process exit (dev mode)`);
|
|
235
|
+
this.isShuttingDown = false;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
emitFailures(failures) {
|
|
240
|
+
this.logger.error(`${chalk.bold.red("Coordinated shutdown failed")}`);
|
|
241
|
+
const error = failures.length === 1 ? failures[0] : new AggregateError(failures, "shutdown phase failures");
|
|
242
|
+
this.emitSafe("shutdown:error", error);
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
//#endregion
|
|
247
|
+
//#region src/node/Lifecycle/CoordinatedStartup.ts
|
|
248
|
+
const PHASE_ORDER = [
|
|
249
|
+
1,
|
|
250
|
+
2,
|
|
251
|
+
3
|
|
252
|
+
];
|
|
253
|
+
/**
|
|
254
|
+
* Runs registered startup tasks across `StartupPhase` in order. Tasks within a phase run in parallel.
|
|
255
|
+
*/
|
|
256
|
+
var CoordinatedStartup = class extends CoordinatedLifecycle {
|
|
257
|
+
isStartingUp = false;
|
|
258
|
+
hasStarted = false;
|
|
259
|
+
constructor() {
|
|
260
|
+
super("Startup", PHASE_ORDER, StartupPhase);
|
|
261
|
+
}
|
|
262
|
+
/** Adds a startup-phase task. @param timeoutMs - Task timeout in ms. {@default 10000} */
|
|
263
|
+
addTask(phase, taskName, task, timeoutMs = 1e4) {
|
|
264
|
+
super.addTask(phase, taskName, task, timeoutMs);
|
|
265
|
+
}
|
|
266
|
+
canAddTask() {
|
|
267
|
+
if (this.hasStarted) throw new SeedcordError(SeedcordErrorCode.LifecycleAddAfterCompletion);
|
|
268
|
+
if (this.isStartingUp) throw new SeedcordError(SeedcordErrorCode.LifecycleAddDuringRun);
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
canRemoveTask() {
|
|
272
|
+
if (this.isStartingUp) throw new SeedcordError(SeedcordErrorCode.LifecycleRemoveDuringRun);
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
getTaskType() {
|
|
276
|
+
return "startup";
|
|
277
|
+
}
|
|
278
|
+
async executeTasksInPhase(phase, tasks) {
|
|
279
|
+
const promises = tasks.map((task) => this.runTaskWithTimeout(phase, task));
|
|
280
|
+
return Promise.allSettled(promises);
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Runs the registered tasks across startup phases in `StartupPhase` order. Each phase completes
|
|
284
|
+
* before the next begins, and tasks within a phase run concurrently.
|
|
285
|
+
*
|
|
286
|
+
* @throws {@link SeedcordError} if a startup task throws.
|
|
287
|
+
*/
|
|
288
|
+
async run() {
|
|
289
|
+
if (this.hasStarted) {
|
|
290
|
+
this.logger.warn("Startup sequence has already completed");
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (this.isStartingUp) {
|
|
294
|
+
this.logger.warn("Startup sequence already in progress");
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
this.isStartingUp = true;
|
|
298
|
+
this.logger.info(`${chalk.bold.green("Starting")} coordinated startup sequence`);
|
|
299
|
+
this.emitSafe("startup:start");
|
|
300
|
+
try {
|
|
301
|
+
for (const phase of PHASE_ORDER) {
|
|
302
|
+
if (!this.isStartingUp) {
|
|
303
|
+
this.logger.warn("Startup sequence aborted");
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
await this.runPhase(phase);
|
|
307
|
+
}
|
|
308
|
+
this.hasStarted = true;
|
|
309
|
+
this.logger.info(`${chalk.bold.green("Coordinated startup completed")} successfully`);
|
|
310
|
+
this.emitSafe("startup:complete");
|
|
311
|
+
} catch (error) {
|
|
312
|
+
if (!this.isStartingUp) {
|
|
313
|
+
this.logger.warn("Startup sequence aborted during error handling");
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
this.logger.error(`${chalk.bold.red("Coordinated startup failed")}`);
|
|
317
|
+
this.emitSafe("startup:error", error);
|
|
318
|
+
throw error;
|
|
319
|
+
} finally {
|
|
320
|
+
this.isStartingUp = false;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
async runTaskWithTimeout(phase, task) {
|
|
324
|
+
this.logger.info(`${chalk.italic("Starting")} task ${chalk.bold.cyan(task.name)} in phase ${chalk.bold.magenta(StartupPhase[phase])}`);
|
|
325
|
+
try {
|
|
326
|
+
await withTimeout(task.name, task.task, task.timeout);
|
|
327
|
+
this.logger.info(`${chalk.italic("Completed")} task ${chalk.bold.cyan(task.name)} in phase ${chalk.bold.magenta(StartupPhase[phase])}`);
|
|
328
|
+
} catch (error) {
|
|
329
|
+
if (!this.isStartingUp) return;
|
|
330
|
+
this.logger.error(`${chalk.italic("Failed")} task ${chalk.bold.cyan(task.name)} in phase ${chalk.bold.magenta(StartupPhase[phase])}:`, error);
|
|
331
|
+
throw error;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Aborts the startup sequence if it is currently running.
|
|
336
|
+
*/
|
|
337
|
+
abort() {
|
|
338
|
+
if (!this.isStartingUp) return;
|
|
339
|
+
this.isStartingUp = false;
|
|
340
|
+
this.logger.warn("Aborting coordinated startup sequence");
|
|
341
|
+
}
|
|
342
|
+
get isReady() {
|
|
343
|
+
return this.hasStarted;
|
|
344
|
+
}
|
|
345
|
+
get isRunning() {
|
|
346
|
+
return this.isStartingUp;
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
//#endregion
|
|
351
|
+
//#region src/node/HealthCheck.ts
|
|
352
|
+
const HTTP_OK = 200;
|
|
353
|
+
const HTTP_NOT_FOUND = 404;
|
|
354
|
+
const DEFAULT_HEALTH_CHECK_PORT = 6967;
|
|
355
|
+
const DEFAULT_HEALTH_CHECK_PATH = "/health";
|
|
356
|
+
/**
|
|
357
|
+
* HTTP server answering `GET` on the configured path with a JSON `{ status, timestamp }` body.
|
|
358
|
+
*/
|
|
359
|
+
var HealthCheck = class HealthCheck {
|
|
360
|
+
/** @internal */
|
|
361
|
+
logger = new Logger("HealthCheck");
|
|
362
|
+
port;
|
|
363
|
+
path;
|
|
364
|
+
host;
|
|
365
|
+
server;
|
|
366
|
+
constructor(shutdown, options) {
|
|
367
|
+
this.port = options?.port ?? DEFAULT_HEALTH_CHECK_PORT;
|
|
368
|
+
this.path = options?.path ?? DEFAULT_HEALTH_CHECK_PATH;
|
|
369
|
+
this.host = options?.host;
|
|
370
|
+
shutdown.addTask(2, "stop-healthcheck-server", () => this.stop());
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Resolves the `healthCheck` option. `undefined` and `true` build the defaults, an object uses
|
|
374
|
+
* its config, `false` returns `undefined`.
|
|
375
|
+
*/
|
|
376
|
+
static fromOption(shutdown, option) {
|
|
377
|
+
if (option === false) return void 0;
|
|
378
|
+
return new HealthCheck(shutdown, typeof option === "object" ? option : void 0);
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Starts the health check server.
|
|
382
|
+
* @returns Promise that resolves when the server is listening
|
|
383
|
+
* @internal
|
|
384
|
+
*/
|
|
385
|
+
async init() {
|
|
386
|
+
return new Promise((resolve, reject) => {
|
|
387
|
+
const server = createServer((req, res) => {
|
|
388
|
+
if (req.method === "GET" && req.url === this.path) {
|
|
389
|
+
res.writeHead(HTTP_OK, { "Content-Type": "application/json" });
|
|
390
|
+
res.end(JSON.stringify({
|
|
391
|
+
status: "ok",
|
|
392
|
+
timestamp: Date.now()
|
|
393
|
+
}));
|
|
394
|
+
} else {
|
|
395
|
+
res.writeHead(HTTP_NOT_FOUND, { "Content-Type": "application/json" });
|
|
396
|
+
res.end(JSON.stringify({ status: "not found" }));
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
this.server = server;
|
|
400
|
+
const onListenError = (err) => reject(err);
|
|
401
|
+
server.on("error", onListenError);
|
|
402
|
+
server.once("listening", () => {
|
|
403
|
+
server.removeListener("error", onListenError);
|
|
404
|
+
server.on("error", (err) => this.logger.error("Health check server error", err));
|
|
405
|
+
const address = this.host ?? "localhost";
|
|
406
|
+
this.logger.info(`${paint.mint.bold("✓")} Health check server listening on ${paint.sky(`http://${address}:${this.port}${this.path}`)}`);
|
|
407
|
+
resolve();
|
|
408
|
+
});
|
|
409
|
+
if (this.host) {
|
|
410
|
+
this.logger.debug(`Binding health check server to ${this.host}`);
|
|
411
|
+
server.listen(this.port, this.host);
|
|
412
|
+
} else {
|
|
413
|
+
this.logger.debug("Binding health check server to all interfaces");
|
|
414
|
+
server.listen(this.port);
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Stops the health check server.
|
|
420
|
+
* @internal
|
|
421
|
+
*/
|
|
422
|
+
stop() {
|
|
423
|
+
const server = this.server;
|
|
424
|
+
if (!server?.listening) return Promise.resolve();
|
|
425
|
+
return new Promise((resolve, reject) => {
|
|
426
|
+
server.close((err) => {
|
|
427
|
+
if (err) {
|
|
428
|
+
reject(err);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
this.logger.info(paint.coral.bold("Health check server stopped"));
|
|
432
|
+
resolve();
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
//#endregion
|
|
439
|
+
//#region src/node/Pluggable.ts
|
|
440
|
+
/**
|
|
441
|
+
* Base class for objects that can have plugins attached.
|
|
442
|
+
*
|
|
443
|
+
* Plugins are attached during configuration and initialized during startup, sequentially in attach
|
|
444
|
+
* order within each phase. Not constructed directly, the host is a transport `Seedcord` class.
|
|
445
|
+
*/
|
|
446
|
+
var Pluggable = class Pluggable extends TypedEventEmitter {
|
|
447
|
+
isInitialized = false;
|
|
448
|
+
startFailed = false;
|
|
449
|
+
shutdown;
|
|
450
|
+
startup;
|
|
451
|
+
plugins = [];
|
|
452
|
+
pluginLogger = new Logger("Plugins");
|
|
453
|
+
attachments = [];
|
|
454
|
+
completedInits = /* @__PURE__ */ new Set();
|
|
455
|
+
disposePhases = /* @__PURE__ */ new Set();
|
|
456
|
+
pluginTasksRegistered = false;
|
|
457
|
+
static isInstantiated = false;
|
|
458
|
+
static liveShutdown;
|
|
459
|
+
constructor(shutdown, startup) {
|
|
460
|
+
if (Pluggable.isInstantiated) {
|
|
461
|
+
shutdown.removeSignalHandlers();
|
|
462
|
+
throw new SeedcordError(SeedcordErrorCode.CoreSingletonViolation);
|
|
463
|
+
}
|
|
464
|
+
super();
|
|
465
|
+
Pluggable.isInstantiated = true;
|
|
466
|
+
Pluggable.liveShutdown = shutdown;
|
|
467
|
+
this.shutdown = shutdown;
|
|
468
|
+
this.startup = startup;
|
|
469
|
+
}
|
|
470
|
+
/** @internal */
|
|
471
|
+
async init() {
|
|
472
|
+
if (this.isInitialized) return this;
|
|
473
|
+
if (this.startFailed) throw new SeedcordError(SeedcordErrorCode.LifecycleRestartAfterFailure);
|
|
474
|
+
this.registerPluginTasks();
|
|
475
|
+
const startupSettled = Promise.withResolvers();
|
|
476
|
+
this.shutdown.gateOnStartup(startupSettled.promise);
|
|
477
|
+
try {
|
|
478
|
+
await this.startup.run();
|
|
479
|
+
} catch (caught) {
|
|
480
|
+
await this.rollback();
|
|
481
|
+
throw caught;
|
|
482
|
+
} finally {
|
|
483
|
+
startupSettled.resolve();
|
|
484
|
+
}
|
|
485
|
+
this.isInitialized = true;
|
|
486
|
+
return this;
|
|
487
|
+
}
|
|
488
|
+
pluginCapabilities() {
|
|
489
|
+
return {};
|
|
490
|
+
}
|
|
491
|
+
pluginStore() {
|
|
492
|
+
return this.config.store ?? this.rateLimiter;
|
|
493
|
+
}
|
|
494
|
+
/** @internal releases the signal handlers and clears the singleton so the next host can construct */
|
|
495
|
+
static reset() {
|
|
496
|
+
Pluggable.liveShutdown?.removeSignalHandlers();
|
|
497
|
+
Pluggable.liveShutdown = void 0;
|
|
498
|
+
Pluggable.isInstantiated = false;
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Attaches a plugin to this instance.
|
|
502
|
+
*
|
|
503
|
+
* The plugin initializes during startup, after every plugin attached before it, and becomes
|
|
504
|
+
* available as a property on `core`. Augment the transport's `Core` interface with the plugin
|
|
505
|
+
* type for intellisense.
|
|
506
|
+
*
|
|
507
|
+
* @typeParam Key - The property name for accessing the plugin
|
|
508
|
+
* @typeParam Ctor - The plugin constructor type
|
|
509
|
+
* @param key - Property name to access the plugin instance
|
|
510
|
+
* @param Plugin - Plugin constructor class
|
|
511
|
+
* @param args - Additional arguments to pass to the plugin constructor
|
|
512
|
+
* @returns This instance with the plugin attached as a typed property
|
|
513
|
+
* @throws A **SeedcordError** When called after initialization or if key already exists
|
|
514
|
+
* @example
|
|
515
|
+
* ```typescript
|
|
516
|
+
* seedcord.attach('db', Mongo, { uri: 'mongodb://...', name: 'seedcord', dir: ... })
|
|
517
|
+
* ```
|
|
518
|
+
*/
|
|
519
|
+
attach(key, Plugin, ...args) {
|
|
520
|
+
if (this.isInitialized) throw new SeedcordError(SeedcordErrorCode.CorePluginAfterInit);
|
|
521
|
+
if (key in this) throw new SeedcordError(SeedcordErrorCode.CorePluginKeyExists, [key]);
|
|
522
|
+
const instance = new Plugin(this, ...args);
|
|
523
|
+
this.plugins.push(instance);
|
|
524
|
+
this.attachments.push({
|
|
525
|
+
key,
|
|
526
|
+
instance
|
|
527
|
+
});
|
|
528
|
+
this.finalizeContext(key, instance);
|
|
529
|
+
return Object.assign(this, { [key]: instance });
|
|
530
|
+
}
|
|
531
|
+
finalizeContext(key, instance) {
|
|
532
|
+
const caps = this.pluginCapabilities();
|
|
533
|
+
const readToken = () => this.pluginCapabilities().token;
|
|
534
|
+
finalizePluginContext(instance, {
|
|
535
|
+
logger: new Logger(key),
|
|
536
|
+
config: this.config,
|
|
537
|
+
store: this.pluginStore(),
|
|
538
|
+
client: caps.client,
|
|
539
|
+
get token() {
|
|
540
|
+
return readToken();
|
|
541
|
+
},
|
|
542
|
+
rest: caps.rest
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
registerPluginTasks() {
|
|
546
|
+
if (this.pluginTasksRegistered) return;
|
|
547
|
+
this.pluginTasksRegistered = true;
|
|
548
|
+
const groups = /* @__PURE__ */ new Map();
|
|
549
|
+
for (const attachment of this.attachments) {
|
|
550
|
+
const phase = resolvedLifecycleSpecOf(attachment.instance).init.phase;
|
|
551
|
+
const group = groups.get(phase) ?? [];
|
|
552
|
+
group.push(attachment);
|
|
553
|
+
groups.set(phase, group);
|
|
554
|
+
}
|
|
555
|
+
for (const [phase, group] of groups) {
|
|
556
|
+
if (phase === 3) continue;
|
|
557
|
+
const budget = group.reduce((sum, a) => sum + resolvedLifecycleSpecOf(a.instance).init.timeout, 0);
|
|
558
|
+
this.startup.addTask(phase, "Plugins:init", () => this.runInits(group), budget);
|
|
559
|
+
}
|
|
560
|
+
this.registerReadyTask(groups.get(3) ?? []);
|
|
561
|
+
}
|
|
562
|
+
async runInits(group) {
|
|
563
|
+
for (const attachment of group) {
|
|
564
|
+
const { key, instance } = attachment;
|
|
565
|
+
const spec = resolvedLifecycleSpecOf(instance);
|
|
566
|
+
instance.logger.utils.initialization(key, "start");
|
|
567
|
+
await withTimeout(`Plugin:${key}`, () => instance.init(), spec.init.timeout);
|
|
568
|
+
instance.logger.utils.initialization(key, "end");
|
|
569
|
+
this.completedInits.add(attachment);
|
|
570
|
+
if (instance.dispose) this.registerDisposeTask(spec.dispose.phase);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
registerReadyTask(readyInits) {
|
|
574
|
+
const steps = this.attachments.reduce((acc, a) => {
|
|
575
|
+
const ready = a.instance.ready?.bind(a.instance);
|
|
576
|
+
if (ready) acc.push({
|
|
577
|
+
key: a.key,
|
|
578
|
+
run: ready,
|
|
579
|
+
timeout: resolvedLifecycleSpecOf(a.instance).ready.timeout
|
|
580
|
+
});
|
|
581
|
+
return acc;
|
|
582
|
+
}, []);
|
|
583
|
+
if (readyInits.length === 0 && steps.length === 0) return;
|
|
584
|
+
const budget = readyInits.reduce((sum, a) => sum + resolvedLifecycleSpecOf(a.instance).init.timeout, 0) + steps.reduce((sum, step) => sum + step.timeout, 0);
|
|
585
|
+
this.startup.addTask(3, "Plugins", async () => {
|
|
586
|
+
await this.runInits(readyInits);
|
|
587
|
+
for (const step of steps) await withTimeout(`Plugin:${step.key}:ready`, step.run, step.timeout);
|
|
588
|
+
}, budget);
|
|
589
|
+
}
|
|
590
|
+
registerDisposeTask(phase) {
|
|
591
|
+
if (this.disposePhases.has(phase)) return;
|
|
592
|
+
this.disposePhases.add(phase);
|
|
593
|
+
const budget = this.attachments.reduce((sum, a) => {
|
|
594
|
+
const spec = resolvedLifecycleSpecOf(a.instance);
|
|
595
|
+
return a.instance.dispose && spec.dispose.phase === phase ? sum + spec.dispose.timeout : sum;
|
|
596
|
+
}, 0);
|
|
597
|
+
this.shutdown.addTask(phase, "Plugins:dispose", () => this.runDisposals(phase), budget);
|
|
598
|
+
}
|
|
599
|
+
async runDisposals(phase) {
|
|
600
|
+
const failures = [];
|
|
601
|
+
await this.disposeCompleted(phase, (caught) => failures.push(caught));
|
|
602
|
+
if (failures.length === 1) throw failures[0];
|
|
603
|
+
if (failures.length > 1) throw new AggregateError(failures, "plugin dispose failures");
|
|
604
|
+
}
|
|
605
|
+
async disposeCompleted(phase, onError) {
|
|
606
|
+
for (const attachment of [...this.attachments].reverse()) {
|
|
607
|
+
if (!this.completedInits.has(attachment)) continue;
|
|
608
|
+
const dispose = attachment.instance.dispose?.bind(attachment.instance);
|
|
609
|
+
if (!dispose) continue;
|
|
610
|
+
const spec = resolvedLifecycleSpecOf(attachment.instance);
|
|
611
|
+
if (phase !== void 0 && spec.dispose.phase !== phase) continue;
|
|
612
|
+
try {
|
|
613
|
+
await withTimeout(`Plugin:${attachment.key}:dispose`, dispose, spec.dispose.timeout);
|
|
614
|
+
} catch (caught) {
|
|
615
|
+
onError(caught);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
async rollback() {
|
|
620
|
+
this.startFailed = true;
|
|
621
|
+
await this.disposeCompleted(void 0, (caught) => this.pluginLogger.warn("rollback dispose failed", caught));
|
|
622
|
+
this.completedInits.clear();
|
|
623
|
+
}
|
|
624
|
+
/** @internal */
|
|
625
|
+
removeListener(event, listener) {
|
|
626
|
+
return super.removeListener(event, listener);
|
|
627
|
+
}
|
|
628
|
+
/** @internal */
|
|
629
|
+
removeAllListeners(event) {
|
|
630
|
+
return super.removeAllListeners(event);
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
|
|
634
|
+
//#endregion
|
|
635
|
+
export { CoordinatedShutdown, CoordinatedStartup, HealthCheck, Pluggable, ShutdownPhase, StartupPhase, settleWithin };
|
|
636
|
+
//# sourceMappingURL=node-internal.index.mjs.map
|