@seedcord/services 0.1.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/LICENSE +190 -0
- package/README.md +5 -0
- package/dist/index.cjs +816 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +406 -0
- package/dist/index.d.mts +406 -0
- package/dist/index.d.ts +406 -0
- package/dist/index.mjs +803 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +56 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,803 @@
|
|
|
1
|
+
import { Envapter, Envapt } from 'envapt';
|
|
2
|
+
import { format, transports, createLogger } from 'winston';
|
|
3
|
+
import { createServer } from 'http';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { EventEmitter } from 'events';
|
|
6
|
+
|
|
7
|
+
var __defProp = Object.defineProperty;
|
|
8
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
9
|
+
var Logger = class _Logger {
|
|
10
|
+
static {
|
|
11
|
+
__name(this, "Logger");
|
|
12
|
+
}
|
|
13
|
+
static instances = /* @__PURE__ */ new Map();
|
|
14
|
+
static instance(prefix) {
|
|
15
|
+
let instance = this.instances.get(prefix);
|
|
16
|
+
if (!instance) {
|
|
17
|
+
instance = new _Logger(prefix);
|
|
18
|
+
this.instances.set(prefix, instance);
|
|
19
|
+
}
|
|
20
|
+
return instance;
|
|
21
|
+
}
|
|
22
|
+
constructor(transportName) {
|
|
23
|
+
const consoleTransport = this.createConsoleTransport(transportName);
|
|
24
|
+
this.initializeLogger(consoleTransport);
|
|
25
|
+
}
|
|
26
|
+
getFormatCustomizations() {
|
|
27
|
+
const padding = 7;
|
|
28
|
+
return [
|
|
29
|
+
format.errors({
|
|
30
|
+
stack: true
|
|
31
|
+
}),
|
|
32
|
+
format.splat(),
|
|
33
|
+
format.colorize({
|
|
34
|
+
level: true
|
|
35
|
+
}),
|
|
36
|
+
format.timestamp({
|
|
37
|
+
format: "D MMM, hh:mm:ss a"
|
|
38
|
+
}),
|
|
39
|
+
format.printf((info) => {
|
|
40
|
+
const ts = String(info.timestamp ?? "");
|
|
41
|
+
const lvl = String(info.level).padEnd(padding);
|
|
42
|
+
const lbl = String(info.label ?? "");
|
|
43
|
+
const msg = String(info.message ?? "");
|
|
44
|
+
const base = `${ts} [${lvl}]: ${lbl} - ${msg}`;
|
|
45
|
+
const splatSym = Symbol.for("splat");
|
|
46
|
+
const raw = info[splatSym];
|
|
47
|
+
const extras = Array.isArray(raw) ? raw : [];
|
|
48
|
+
const cleaned = extras.filter((x) => !(x instanceof Error)).filter((x) => {
|
|
49
|
+
if (!x) return false;
|
|
50
|
+
if (typeof x !== "object") return true;
|
|
51
|
+
return Object.keys(x).length > 0;
|
|
52
|
+
});
|
|
53
|
+
let rendered = base;
|
|
54
|
+
if (typeof info.stack === "string") {
|
|
55
|
+
rendered += `
|
|
56
|
+
${String(info.stack)}`;
|
|
57
|
+
}
|
|
58
|
+
if (cleaned.length) {
|
|
59
|
+
const parts = [];
|
|
60
|
+
for (const x of cleaned) {
|
|
61
|
+
if (typeof x === "string") parts.push(x);
|
|
62
|
+
else {
|
|
63
|
+
try {
|
|
64
|
+
parts.push(JSON.stringify(x, null, 2));
|
|
65
|
+
} catch {
|
|
66
|
+
parts.push(String(x));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
rendered += `
|
|
71
|
+
${parts.join(" ")}`;
|
|
72
|
+
}
|
|
73
|
+
return rendered;
|
|
74
|
+
})
|
|
75
|
+
];
|
|
76
|
+
}
|
|
77
|
+
createConsoleTransport(transportName) {
|
|
78
|
+
return new transports.Console({
|
|
79
|
+
format: format.combine(format.label({
|
|
80
|
+
label: transportName
|
|
81
|
+
}), ...this.getFormatCustomizations()),
|
|
82
|
+
level: Envapter.isDevelopment ? "silly" : Envapter.isStaging ? "debug" : "info"
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
initializeLogger(consoleTransport) {
|
|
86
|
+
const transportsArray = [
|
|
87
|
+
consoleTransport
|
|
88
|
+
];
|
|
89
|
+
if (Envapter.isDevelopment) {
|
|
90
|
+
const maxSizeInMB = 10;
|
|
91
|
+
transportsArray.push(new transports.File({
|
|
92
|
+
filename: "logs/application.log",
|
|
93
|
+
level: "debug",
|
|
94
|
+
format: format.combine(format.uncolorize(), format.errors({
|
|
95
|
+
stack: true
|
|
96
|
+
}), format.timestamp(), format.json({
|
|
97
|
+
bigint: true,
|
|
98
|
+
space: 2
|
|
99
|
+
})),
|
|
100
|
+
maxsize: maxSizeInMB * 1024 * 1024,
|
|
101
|
+
maxFiles: 5,
|
|
102
|
+
tailable: true
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
this.logger = createLogger({
|
|
106
|
+
transports: transportsArray
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Logs an error message with optional additional data.
|
|
111
|
+
*
|
|
112
|
+
* @param msg - The error message to log
|
|
113
|
+
* @param args - Additional data to include in the log entry
|
|
114
|
+
*/
|
|
115
|
+
error(msg, ...args) {
|
|
116
|
+
this.logger.error(msg, ...args);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Logs a warning message with optional additional data.
|
|
120
|
+
*
|
|
121
|
+
* @param msg - The warning message to log
|
|
122
|
+
* @param args - Additional data to include in the log entry
|
|
123
|
+
*/
|
|
124
|
+
warn(msg, ...args) {
|
|
125
|
+
this.logger.warn(msg, ...args);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Logs an informational message with optional additional data.
|
|
129
|
+
*
|
|
130
|
+
* @param msg - The informational message to log
|
|
131
|
+
* @param args - Additional data to include in the log entry
|
|
132
|
+
*/
|
|
133
|
+
info(msg, ...args) {
|
|
134
|
+
this.logger.info(msg, ...args);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Logs an HTTP-related message with optional additional data.
|
|
138
|
+
*
|
|
139
|
+
* @param msg - The HTTP message to log
|
|
140
|
+
* @param args - Additional data to include in the log entry
|
|
141
|
+
*/
|
|
142
|
+
http(msg, ...args) {
|
|
143
|
+
this.logger.http(msg, ...args);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Logs a verbose message with optional additional data.
|
|
147
|
+
*
|
|
148
|
+
* @param msg - The verbose message to log
|
|
149
|
+
* @param args - Additional data to include in the log entry
|
|
150
|
+
*/
|
|
151
|
+
verbose(msg, ...args) {
|
|
152
|
+
this.logger.verbose(msg, ...args);
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Logs a debug message with optional additional data.
|
|
156
|
+
*
|
|
157
|
+
* @param msg - The debug message to log
|
|
158
|
+
* @param args - Additional data to include in the log entry
|
|
159
|
+
*/
|
|
160
|
+
debug(msg, ...args) {
|
|
161
|
+
this.logger.debug(msg, ...args);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Logs a silly/trace level message with optional additional data.
|
|
165
|
+
*
|
|
166
|
+
* @param msg - The silly message to log
|
|
167
|
+
* @param args - Additional data to include in the log entry
|
|
168
|
+
*/
|
|
169
|
+
silly(msg, ...args) {
|
|
170
|
+
this.logger.silly(msg, ...args);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Static method to log an error message with a specific prefix.
|
|
174
|
+
* Creates or retrieves a logger instance for the given prefix.
|
|
175
|
+
*
|
|
176
|
+
* @param prefix - The logger prefix/label to use
|
|
177
|
+
* @param msg - The error message to log
|
|
178
|
+
* @param args - Additional data to include in the log entry
|
|
179
|
+
*/
|
|
180
|
+
static Error(prefix, msg, ...args) {
|
|
181
|
+
const logger = this.instance(prefix);
|
|
182
|
+
logger.error(msg, ...args);
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Static method to log an informational message with a specific prefix.
|
|
186
|
+
* Creates or retrieves a logger instance for the given prefix.
|
|
187
|
+
*
|
|
188
|
+
* @param prefix - The logger prefix/label to use
|
|
189
|
+
* @param msg - The informational message to log
|
|
190
|
+
* @param args - Additional data to include in the log entry
|
|
191
|
+
*/
|
|
192
|
+
static Info(prefix, msg, ...args) {
|
|
193
|
+
const logger = this.instance(prefix);
|
|
194
|
+
logger.info(msg, ...args);
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Static method to log a warning message with a specific prefix.
|
|
198
|
+
* Creates or retrieves a logger instance for the given prefix.
|
|
199
|
+
*
|
|
200
|
+
* @param prefix - The logger prefix/label to use
|
|
201
|
+
* @param msg - The warning message to log
|
|
202
|
+
* @param args - Additional data to include in the log entry
|
|
203
|
+
*/
|
|
204
|
+
static Warn(prefix, msg, ...args) {
|
|
205
|
+
const logger = this.instance(prefix);
|
|
206
|
+
logger.warn(msg, ...args);
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Static method to log a debug message with a specific prefix.
|
|
210
|
+
* Creates or retrieves a logger instance for the given prefix.
|
|
211
|
+
*
|
|
212
|
+
* @param prefix - The logger prefix/label to use
|
|
213
|
+
* @param msg - The debug message to log
|
|
214
|
+
* @param args - Additional data to include in the log entry
|
|
215
|
+
*/
|
|
216
|
+
static Debug(prefix, msg, ...args) {
|
|
217
|
+
const logger = this.instance(prefix);
|
|
218
|
+
logger.debug(msg, ...args);
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Static method to log a silly/trace level message with a specific prefix.
|
|
222
|
+
* Creates or retrieves a logger instance for the given prefix.
|
|
223
|
+
*
|
|
224
|
+
* @param prefix - The logger prefix/label to use
|
|
225
|
+
* @param msg - The silly message to log
|
|
226
|
+
* @param args - Additional data to include in the log entry
|
|
227
|
+
*/
|
|
228
|
+
static Silly(prefix, msg, ...args) {
|
|
229
|
+
const logger = this.instance(prefix);
|
|
230
|
+
logger.silly(msg, ...args);
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
var CoordinatedLifecycle = class {
|
|
234
|
+
static {
|
|
235
|
+
__name(this, "CoordinatedLifecycle");
|
|
236
|
+
}
|
|
237
|
+
phaseOrder;
|
|
238
|
+
phaseEnum;
|
|
239
|
+
logger;
|
|
240
|
+
events = new EventEmitter();
|
|
241
|
+
tasksMap = /* @__PURE__ */ new Map();
|
|
242
|
+
constructor(loggerName, phaseOrder, phaseEnum) {
|
|
243
|
+
this.phaseOrder = phaseOrder;
|
|
244
|
+
this.phaseEnum = phaseEnum;
|
|
245
|
+
this.logger = new Logger(loggerName);
|
|
246
|
+
this.phaseOrder.forEach((phase) => this.tasksMap.set(phase, []));
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Adds a lifecycle task to a specific phase.
|
|
250
|
+
*
|
|
251
|
+
* Tasks are executed in phase order during lifecycle operations.
|
|
252
|
+
* Each task has a timeout to prevent hanging operations.
|
|
253
|
+
*
|
|
254
|
+
* @param phase - The lifecycle phase to add the task to
|
|
255
|
+
* @param taskName - Unique name for the task (used for logging and removal)
|
|
256
|
+
* @param task - Async function to execute during the phase
|
|
257
|
+
* @param timeoutMs - Maximum time allowed for task execution in milliseconds
|
|
258
|
+
* @example
|
|
259
|
+
* ```typescript
|
|
260
|
+
* lifecycle.addTask(StartupPhase.Services, 'start-database', async () => {
|
|
261
|
+
* await database.connect();
|
|
262
|
+
* }, 10000);
|
|
263
|
+
* ```
|
|
264
|
+
*/
|
|
265
|
+
addTask(phase, taskName, task, timeoutMs) {
|
|
266
|
+
if (!this.canAddTask()) return;
|
|
267
|
+
const tasks = this.tasksMap.get(phase);
|
|
268
|
+
if (!tasks) throw new Error(`Unknown phase: ${phase}`);
|
|
269
|
+
tasks.push({
|
|
270
|
+
name: taskName,
|
|
271
|
+
task,
|
|
272
|
+
timeout: timeoutMs
|
|
273
|
+
});
|
|
274
|
+
this.logger.debug(`${chalk.italic("Added")} ${this.getTaskType()} task ${chalk.bold.cyan(taskName)} to phase ${chalk.bold.magenta(this.phaseEnum[phase])}`);
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Removes a lifecycle task from a specific phase.
|
|
278
|
+
*
|
|
279
|
+
* @param phase - The lifecycle phase to remove the task from
|
|
280
|
+
* @param taskName - Name of the task to remove
|
|
281
|
+
* @returns True if the task was found and removed, false otherwise
|
|
282
|
+
*/
|
|
283
|
+
removeTask(phase, taskName) {
|
|
284
|
+
if (!this.canRemoveTask()) return false;
|
|
285
|
+
const tasks = this.tasksMap.get(phase);
|
|
286
|
+
if (!tasks) return false;
|
|
287
|
+
const initialLength = tasks.length;
|
|
288
|
+
const filteredTasks = tasks.filter((task) => task.name !== taskName);
|
|
289
|
+
this.tasksMap.set(phase, filteredTasks);
|
|
290
|
+
const removed = initialLength !== filteredTasks.length;
|
|
291
|
+
if (removed) {
|
|
292
|
+
this.logger.debug(`${chalk.italic("Removed")} ${this.getTaskType()} task ${chalk.bold.cyan(taskName)} from phase ${chalk.bold.magenta(this.phaseEnum[phase])}`);
|
|
293
|
+
}
|
|
294
|
+
return removed;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Run all tasks in a specific phase
|
|
298
|
+
*/
|
|
299
|
+
async runPhase(phase) {
|
|
300
|
+
const tasks = this.tasksMap.get(phase) ?? [];
|
|
301
|
+
if (tasks.length === 0) {
|
|
302
|
+
this.logger.warn(`No tasks to run in phase ${chalk.bold.magenta(this.phaseEnum[phase])}`);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
this.logger.info(`${chalk.bold.yellow("Running")} ${this.getTaskType()} phase ${chalk.bold.magenta(this.phaseEnum[phase])} with ${chalk.bold.cyan(tasks.length)} tasks`);
|
|
306
|
+
this.emit(`phase:${phase}:start`);
|
|
307
|
+
const results = await this.executeTasksInPhase(phase, tasks);
|
|
308
|
+
const failures = results.filter((r) => r.status === "rejected").length;
|
|
309
|
+
if (failures > 0) {
|
|
310
|
+
const errorMessage = `Phase ${chalk.bold.magenta(this.phaseEnum[phase])} completed with ${chalk.bold.red(failures)} failed tasks`;
|
|
311
|
+
throw new Error(errorMessage);
|
|
312
|
+
} else {
|
|
313
|
+
this.logger.info(`Phase ${chalk.bold.magenta(this.phaseEnum[phase])} ${chalk.bold.green("completed successfully")}`);
|
|
314
|
+
}
|
|
315
|
+
this.emit(`phase:${phase}:complete`);
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Run a single task with timeout
|
|
319
|
+
*/
|
|
320
|
+
async runTaskWithTimeout(phase, task) {
|
|
321
|
+
this.logger.info(`${chalk.italic("Starting")} task ${chalk.bold.cyan(task.name)} in phase ${chalk.bold.magenta(this.phaseEnum[phase])}`);
|
|
322
|
+
try {
|
|
323
|
+
await Promise.race([
|
|
324
|
+
task.task(),
|
|
325
|
+
new Promise((_, reject) => {
|
|
326
|
+
setTimeout(() => {
|
|
327
|
+
reject(new Error(`Task '${task.name}' timed out after ${task.timeout}ms`));
|
|
328
|
+
}, task.timeout);
|
|
329
|
+
})
|
|
330
|
+
]);
|
|
331
|
+
this.logger.info(`${chalk.italic("Completed")} task ${chalk.bold.cyan(task.name)} in phase ${chalk.bold.magenta(this.phaseEnum[phase])}`);
|
|
332
|
+
} catch (error) {
|
|
333
|
+
this.logger.error(`${chalk.italic("Failed")} task ${chalk.bold.cyan(task.name)} in phase ${chalk.bold.magenta(this.phaseEnum[phase])}:`, error);
|
|
334
|
+
throw error;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Subscribe to lifecycle events
|
|
339
|
+
*/
|
|
340
|
+
on(event, listener) {
|
|
341
|
+
this.events.on(event, listener);
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Unsubscribe from lifecycle events
|
|
345
|
+
*/
|
|
346
|
+
off(event, listener) {
|
|
347
|
+
this.events.off(event, listener);
|
|
348
|
+
}
|
|
349
|
+
emit(event, ...args) {
|
|
350
|
+
return this.events.emit(event, ...args);
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
// src/Lifecycle/CoordinatedShutdown.ts
|
|
355
|
+
function _ts_decorate(decorators, target, key, desc) {
|
|
356
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
357
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
358
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
359
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
360
|
+
}
|
|
361
|
+
__name(_ts_decorate, "_ts_decorate");
|
|
362
|
+
function _ts_metadata(k, v) {
|
|
363
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
364
|
+
}
|
|
365
|
+
__name(_ts_metadata, "_ts_metadata");
|
|
366
|
+
var ShutdownPhase = /* @__PURE__ */ (function(ShutdownPhase2) {
|
|
367
|
+
ShutdownPhase2[ShutdownPhase2["StopAcceptingRequests"] = 1] = "StopAcceptingRequests";
|
|
368
|
+
ShutdownPhase2[ShutdownPhase2["StopServices"] = 2] = "StopServices";
|
|
369
|
+
ShutdownPhase2[ShutdownPhase2["ExternalResources"] = 3] = "ExternalResources";
|
|
370
|
+
ShutdownPhase2[ShutdownPhase2["DiscordCleanup"] = 4] = "DiscordCleanup";
|
|
371
|
+
ShutdownPhase2[ShutdownPhase2["FinalCleanup"] = 5] = "FinalCleanup";
|
|
372
|
+
return ShutdownPhase2;
|
|
373
|
+
})({});
|
|
374
|
+
var PHASE_ORDER = [
|
|
375
|
+
1,
|
|
376
|
+
2,
|
|
377
|
+
3,
|
|
378
|
+
4,
|
|
379
|
+
5
|
|
380
|
+
];
|
|
381
|
+
var LOG_FLUSH_DELAY_MS = 500;
|
|
382
|
+
var CoordinatedShutdown = class extends CoordinatedLifecycle {
|
|
383
|
+
static {
|
|
384
|
+
__name(this, "CoordinatedShutdown");
|
|
385
|
+
}
|
|
386
|
+
isShuttingDown = false;
|
|
387
|
+
exitCode = 0;
|
|
388
|
+
constructor() {
|
|
389
|
+
super("CoordinatedShutdown", PHASE_ORDER, ShutdownPhase);
|
|
390
|
+
this.registerSignalHandlers();
|
|
391
|
+
}
|
|
392
|
+
canAddTask() {
|
|
393
|
+
return this.isShutdownEnabled;
|
|
394
|
+
}
|
|
395
|
+
canRemoveTask() {
|
|
396
|
+
return true;
|
|
397
|
+
}
|
|
398
|
+
getTaskType() {
|
|
399
|
+
return "shutdown";
|
|
400
|
+
}
|
|
401
|
+
async executeTasksInPhase(phase, tasks) {
|
|
402
|
+
const results = [];
|
|
403
|
+
for (const task of tasks) {
|
|
404
|
+
results.push(await Promise.resolve().then(() => this.runTaskWithTimeout(phase, task)).then(
|
|
405
|
+
() => ({
|
|
406
|
+
status: "fulfilled",
|
|
407
|
+
value: void 0
|
|
408
|
+
}),
|
|
409
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
410
|
+
(reason) => ({
|
|
411
|
+
status: "rejected",
|
|
412
|
+
reason
|
|
413
|
+
})
|
|
414
|
+
));
|
|
415
|
+
}
|
|
416
|
+
return results;
|
|
417
|
+
}
|
|
418
|
+
registerSignalHandlers() {
|
|
419
|
+
if (!this.isShutdownEnabled) return;
|
|
420
|
+
process.on("SIGTERM", () => {
|
|
421
|
+
this.logger.info(`Received ${chalk.yellow.bold("SIGTERM")} signal`);
|
|
422
|
+
void this.run(0);
|
|
423
|
+
});
|
|
424
|
+
process.on("SIGINT", () => {
|
|
425
|
+
this.logger.info(`Received ${chalk.yellow.bold("SIGINT")} signal`);
|
|
426
|
+
void this.run(0);
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Adds a task to a specific shutdown phase with timeout.
|
|
431
|
+
*
|
|
432
|
+
* @param phase - The shutdown phase from {@link ShutdownPhase}
|
|
433
|
+
* @param taskName - Unique identifier for the task
|
|
434
|
+
* @param task - Async function to execute
|
|
435
|
+
* @param timeoutMs - Task timeout in milliseconds (default: 5000)
|
|
436
|
+
*/
|
|
437
|
+
addTask(phase, taskName, task, timeoutMs = 5e3) {
|
|
438
|
+
super.addTask(phase, taskName, task, timeoutMs);
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Removes a task from a specific shutdown phase.
|
|
442
|
+
*
|
|
443
|
+
* @param phase - The shutdown phase to remove from
|
|
444
|
+
* @param taskName - Name of the task to remove
|
|
445
|
+
* @returns True if task was found and removed
|
|
446
|
+
*/
|
|
447
|
+
removeTask(phase, taskName) {
|
|
448
|
+
return super.removeTask(phase, taskName);
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Executes the coordinated shutdown sequence.
|
|
452
|
+
*
|
|
453
|
+
* Runs all registered tasks across shutdown phases in reverse order.
|
|
454
|
+
* Tasks within each phase are executed in parallel for faster shutdown.
|
|
455
|
+
* Process exits with the specified code when complete.
|
|
456
|
+
*
|
|
457
|
+
* @param exitCode - Process exit code (default: 0)
|
|
458
|
+
* @returns Promise that resolves when shutdown is complete
|
|
459
|
+
* @example
|
|
460
|
+
* ```typescript
|
|
461
|
+
* shutdown.addTask(ShutdownPhase.Services, 'database', () => db.disconnect(), 5000);
|
|
462
|
+
* await shutdown.run(0); // Graceful shutdown
|
|
463
|
+
* ```
|
|
464
|
+
*/
|
|
465
|
+
async run(exitCode = 0) {
|
|
466
|
+
if (this.isShuttingDown) {
|
|
467
|
+
this.logger.warn("Shutdown sequence already in progress");
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
this.isShuttingDown = true;
|
|
471
|
+
this.exitCode = exitCode;
|
|
472
|
+
this.logger.info(`${chalk.bold.yellow("Starting")} coordinated shutdown with exit code ${chalk.bold.cyan(exitCode)}`);
|
|
473
|
+
this.emit("shutdown:start");
|
|
474
|
+
try {
|
|
475
|
+
for (const phase of PHASE_ORDER) {
|
|
476
|
+
await this.runPhase(phase);
|
|
477
|
+
}
|
|
478
|
+
this.logger.info(`${chalk.bold.green("Coordinated shutdown completed")} successfully`);
|
|
479
|
+
this.emit("shutdown:complete");
|
|
480
|
+
} catch (error) {
|
|
481
|
+
this.logger.error(`${chalk.bold.red("Coordinated shutdown failed")}`);
|
|
482
|
+
this.emit("shutdown:error", error);
|
|
483
|
+
} finally {
|
|
484
|
+
this.logger.info(`${chalk.bold.red("Exiting")} process with code ${chalk.bold.cyan(this.exitCode)}`);
|
|
485
|
+
setTimeout(() => {
|
|
486
|
+
process.exit(this.exitCode);
|
|
487
|
+
}, LOG_FLUSH_DELAY_MS);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Subscribe to shutdown events
|
|
492
|
+
*/
|
|
493
|
+
on(event, listener) {
|
|
494
|
+
super.on(event, listener);
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Unsubscribe from shutdown events
|
|
498
|
+
*/
|
|
499
|
+
off(event, listener) {
|
|
500
|
+
super.off(event, listener);
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
_ts_decorate([
|
|
504
|
+
Envapt("SHUTDOWN_IS_ENABLED", {
|
|
505
|
+
fallback: false
|
|
506
|
+
}),
|
|
507
|
+
_ts_metadata("design:type", Boolean)
|
|
508
|
+
], CoordinatedShutdown.prototype, "isShutdownEnabled", void 0);
|
|
509
|
+
|
|
510
|
+
// src/HealthCheck.ts
|
|
511
|
+
function _ts_decorate2(decorators, target, key, desc) {
|
|
512
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
513
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
514
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
515
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
516
|
+
}
|
|
517
|
+
__name(_ts_decorate2, "_ts_decorate");
|
|
518
|
+
function _ts_metadata2(k, v) {
|
|
519
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
520
|
+
}
|
|
521
|
+
__name(_ts_metadata2, "_ts_metadata");
|
|
522
|
+
var HTTP_OK = 200;
|
|
523
|
+
var HTTP_NOT_FOUND = 404;
|
|
524
|
+
var HealthCheck = class {
|
|
525
|
+
static {
|
|
526
|
+
__name(this, "HealthCheck");
|
|
527
|
+
}
|
|
528
|
+
logger = new Logger("HealthCheck");
|
|
529
|
+
server;
|
|
530
|
+
constructor(shutdown) {
|
|
531
|
+
shutdown.addTask(ShutdownPhase.StopServices, "stop-healthcheck-server", async () => await this.stop());
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Starts the health check server.
|
|
535
|
+
* @returns Promise that resolves when the server is listening
|
|
536
|
+
*/
|
|
537
|
+
async init() {
|
|
538
|
+
return new Promise((resolve, reject) => {
|
|
539
|
+
this.server = createServer((req, res) => {
|
|
540
|
+
if (req.method === "GET" && req.url === this.path) {
|
|
541
|
+
res.writeHead(HTTP_OK, {
|
|
542
|
+
"Content-Type": "application/json"
|
|
543
|
+
});
|
|
544
|
+
res.end(JSON.stringify({
|
|
545
|
+
status: "ok",
|
|
546
|
+
timestamp: Date.now()
|
|
547
|
+
}));
|
|
548
|
+
} else {
|
|
549
|
+
res.writeHead(HTTP_NOT_FOUND, {
|
|
550
|
+
"Content-Type": "application/json"
|
|
551
|
+
});
|
|
552
|
+
res.end(JSON.stringify({
|
|
553
|
+
status: "not found"
|
|
554
|
+
}));
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
this.server.on("error", reject);
|
|
558
|
+
this.server.once("listening", () => resolve());
|
|
559
|
+
this.server.listen(this.port, () => {
|
|
560
|
+
this.logger.info(`${chalk.green.bold("\u2713")} Health check server listening on ${chalk.cyan(`http://localhost:${this.port}${this.path}`)}`);
|
|
561
|
+
});
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Stops the health check server.
|
|
566
|
+
*
|
|
567
|
+
* @returns Promise that resolves when the server is closed
|
|
568
|
+
*/
|
|
569
|
+
stop() {
|
|
570
|
+
if (this.server !== void 0) {
|
|
571
|
+
const server = this.server;
|
|
572
|
+
return new Promise((resolve) => {
|
|
573
|
+
server.once("close", () => resolve());
|
|
574
|
+
server.close(() => {
|
|
575
|
+
this.logger.info(chalk.bold.red("Health check server stopped"));
|
|
576
|
+
});
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
return Promise.resolve();
|
|
580
|
+
}
|
|
581
|
+
};
|
|
582
|
+
_ts_decorate2([
|
|
583
|
+
Envapt("HEALTH_CHECK_PORT", {
|
|
584
|
+
fallback: 6956
|
|
585
|
+
}),
|
|
586
|
+
_ts_metadata2("design:type", Number)
|
|
587
|
+
], HealthCheck.prototype, "port", void 0);
|
|
588
|
+
_ts_decorate2([
|
|
589
|
+
Envapt("HEALTH_CHECK_PATH", {
|
|
590
|
+
fallback: "/healthcheck"
|
|
591
|
+
}),
|
|
592
|
+
_ts_metadata2("design:type", String)
|
|
593
|
+
], HealthCheck.prototype, "path", void 0);
|
|
594
|
+
var CooldownManager = class {
|
|
595
|
+
static {
|
|
596
|
+
__name(this, "CooldownManager");
|
|
597
|
+
}
|
|
598
|
+
window;
|
|
599
|
+
Err;
|
|
600
|
+
msg;
|
|
601
|
+
map = /* @__PURE__ */ new Map();
|
|
602
|
+
/**
|
|
603
|
+
* Creates a new CooldownManager instance.
|
|
604
|
+
*
|
|
605
|
+
* @param opts - Configuration options for the cooldown behavior
|
|
606
|
+
*/
|
|
607
|
+
constructor(opts = {}) {
|
|
608
|
+
this.window = opts.cooldown ?? 1e3;
|
|
609
|
+
this.Err = opts.err ?? Error;
|
|
610
|
+
this.msg = opts.message ?? "Cooldown active";
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Records usage timestamp for a key without any cooldown checks.
|
|
614
|
+
*
|
|
615
|
+
* @param key - The unique identifier for the cooldown entry
|
|
616
|
+
*/
|
|
617
|
+
set(key) {
|
|
618
|
+
this.map.set(key, Date.now());
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Verifies cooldown status for a key and updates timestamp if not active.
|
|
622
|
+
*
|
|
623
|
+
* If the cooldown is still active, throws the configured error.
|
|
624
|
+
* If not active, updates the timestamp and returns successfully.
|
|
625
|
+
*
|
|
626
|
+
* @param key - The unique identifier to check cooldown for
|
|
627
|
+
* @throws An {@link Err} When the cooldown is still active for the given key
|
|
628
|
+
*/
|
|
629
|
+
check(key) {
|
|
630
|
+
const now = Date.now();
|
|
631
|
+
const last = this.map.get(key);
|
|
632
|
+
const remaining = this.window - (now - (last ?? 0));
|
|
633
|
+
if (Envapter.isDevelopment && remaining > 0) {
|
|
634
|
+
Logger.Debug("CooldownManager", `${key} - ${remaining}ms remaining`);
|
|
635
|
+
}
|
|
636
|
+
if (last !== void 0 && remaining > 0) {
|
|
637
|
+
throw new this.Err(this.msg, remaining);
|
|
638
|
+
}
|
|
639
|
+
this.map.set(key, now);
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Checks if a key is currently cooling down without updating timestamp.
|
|
643
|
+
*
|
|
644
|
+
* @param key - The unique identifier to check
|
|
645
|
+
* @returns True if the key is still cooling down, false otherwise
|
|
646
|
+
*/
|
|
647
|
+
isActive(key) {
|
|
648
|
+
const last = this.map.get(key);
|
|
649
|
+
return last !== void 0 && Date.now() - last < this.window;
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Removes a key from the cooldown map.
|
|
653
|
+
*
|
|
654
|
+
* @param key - The unique identifier to remove (useful for manual resets)
|
|
655
|
+
*/
|
|
656
|
+
clear(key) {
|
|
657
|
+
this.map.delete(key);
|
|
658
|
+
}
|
|
659
|
+
};
|
|
660
|
+
var StartupPhase = /* @__PURE__ */ (function(StartupPhase2) {
|
|
661
|
+
StartupPhase2[StartupPhase2["Validation"] = 1] = "Validation";
|
|
662
|
+
StartupPhase2[StartupPhase2["Discovery"] = 2] = "Discovery";
|
|
663
|
+
StartupPhase2[StartupPhase2["Registration"] = 3] = "Registration";
|
|
664
|
+
StartupPhase2[StartupPhase2["Configuration"] = 4] = "Configuration";
|
|
665
|
+
StartupPhase2[StartupPhase2["Instantiation"] = 5] = "Instantiation";
|
|
666
|
+
StartupPhase2[StartupPhase2["Activation"] = 6] = "Activation";
|
|
667
|
+
StartupPhase2[StartupPhase2["Ready"] = 7] = "Ready";
|
|
668
|
+
return StartupPhase2;
|
|
669
|
+
})({});
|
|
670
|
+
var PHASE_ORDER2 = [
|
|
671
|
+
1,
|
|
672
|
+
2,
|
|
673
|
+
3,
|
|
674
|
+
4,
|
|
675
|
+
5,
|
|
676
|
+
6,
|
|
677
|
+
7
|
|
678
|
+
];
|
|
679
|
+
var CoordinatedStartup = class extends CoordinatedLifecycle {
|
|
680
|
+
static {
|
|
681
|
+
__name(this, "CoordinatedStartup");
|
|
682
|
+
}
|
|
683
|
+
isStartingUp = false;
|
|
684
|
+
hasStarted = false;
|
|
685
|
+
constructor() {
|
|
686
|
+
super("CoordinatedStartup", PHASE_ORDER2, StartupPhase);
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Adds a task to a specific startup phase with timeout.
|
|
690
|
+
*
|
|
691
|
+
* @param phase - The startup phase from {@link StartupPhase}
|
|
692
|
+
* @param taskName - Unique identifier for the task
|
|
693
|
+
* @param task - Async function to execute
|
|
694
|
+
* @param timeoutMs - Task timeout in milliseconds (default: 10000)
|
|
695
|
+
*/
|
|
696
|
+
addTask(phase, taskName, task, timeoutMs = 1e4) {
|
|
697
|
+
super.addTask(phase, taskName, task, timeoutMs);
|
|
698
|
+
}
|
|
699
|
+
canAddTask() {
|
|
700
|
+
if (this.hasStarted) {
|
|
701
|
+
throw new Error("Cannot add tasks after startup sequence has already completed");
|
|
702
|
+
}
|
|
703
|
+
if (this.isStartingUp) {
|
|
704
|
+
throw new Error("Cannot add tasks while startup sequence is in progress");
|
|
705
|
+
}
|
|
706
|
+
return true;
|
|
707
|
+
}
|
|
708
|
+
canRemoveTask() {
|
|
709
|
+
if (this.isStartingUp) {
|
|
710
|
+
throw new Error("Cannot remove tasks while startup sequence is in progress");
|
|
711
|
+
}
|
|
712
|
+
return true;
|
|
713
|
+
}
|
|
714
|
+
getTaskType() {
|
|
715
|
+
return "startup";
|
|
716
|
+
}
|
|
717
|
+
async executeTasksInPhase(phase, tasks) {
|
|
718
|
+
const results = [];
|
|
719
|
+
for (const task of tasks) {
|
|
720
|
+
results.push(await Promise.resolve().then(() => this.runTaskWithTimeout(phase, task)).then(
|
|
721
|
+
() => ({
|
|
722
|
+
status: "fulfilled",
|
|
723
|
+
value: void 0
|
|
724
|
+
}),
|
|
725
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
726
|
+
(reason) => ({
|
|
727
|
+
status: "rejected",
|
|
728
|
+
reason
|
|
729
|
+
})
|
|
730
|
+
));
|
|
731
|
+
}
|
|
732
|
+
return results;
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Executes the coordinated startup sequence.
|
|
736
|
+
*
|
|
737
|
+
* Runs all registered tasks across startup phases in the correct order.
|
|
738
|
+
* Each phase completes before the next phase begins. Tasks within a phase
|
|
739
|
+
* are executed sequentially to maintain predictable initialization.
|
|
740
|
+
*
|
|
741
|
+
* @returns Promise that resolves when startup is complete
|
|
742
|
+
* @throws An {@link Error} If startup fails or is called multiple times
|
|
743
|
+
* @example
|
|
744
|
+
* ```typescript
|
|
745
|
+
* const startup = new CoordinatedStartup();
|
|
746
|
+
* startup.addTask(StartupPhase.Services, 'database', () => db.connect(), 10000);
|
|
747
|
+
* await startup.run();
|
|
748
|
+
* ```
|
|
749
|
+
*/
|
|
750
|
+
async run() {
|
|
751
|
+
if (this.hasStarted) {
|
|
752
|
+
this.logger.warn("Startup sequence has already completed");
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
if (this.isStartingUp) {
|
|
756
|
+
this.logger.warn("Startup sequence already in progress");
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
this.isStartingUp = true;
|
|
760
|
+
this.logger.info(`${chalk.bold.green("Starting")} coordinated startup sequence`);
|
|
761
|
+
this.emit("startup:start");
|
|
762
|
+
try {
|
|
763
|
+
for (const phase of PHASE_ORDER2) await this.runPhase(phase);
|
|
764
|
+
this.hasStarted = true;
|
|
765
|
+
this.logger.info(`${chalk.bold.green("Coordinated startup completed")} successfully`);
|
|
766
|
+
this.emit("startup:complete");
|
|
767
|
+
} catch (error) {
|
|
768
|
+
this.logger.error(`${chalk.bold.red("Coordinated startup failed")}`);
|
|
769
|
+
this.emit("startup:error", error);
|
|
770
|
+
throw error;
|
|
771
|
+
} finally {
|
|
772
|
+
this.isStartingUp = false;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* Subscribe to startup events
|
|
777
|
+
*/
|
|
778
|
+
on(event, listener) {
|
|
779
|
+
super.on(event, listener);
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Unsubscribe from startup events
|
|
783
|
+
*/
|
|
784
|
+
off(event, listener) {
|
|
785
|
+
super.off(event, listener);
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Check if startup has completed
|
|
789
|
+
*/
|
|
790
|
+
get isReady() {
|
|
791
|
+
return this.hasStarted;
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Check if startup is currently running
|
|
795
|
+
*/
|
|
796
|
+
get isRunning() {
|
|
797
|
+
return this.isStartingUp;
|
|
798
|
+
}
|
|
799
|
+
};
|
|
800
|
+
|
|
801
|
+
export { CooldownManager, CoordinatedLifecycle, CoordinatedShutdown, CoordinatedStartup, HealthCheck, Logger, ShutdownPhase, StartupPhase };
|
|
802
|
+
//# sourceMappingURL=index.mjs.map
|
|
803
|
+
//# sourceMappingURL=index.mjs.map
|