@seedcord/services 0.7.1 → 0.8.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/README.md +1 -1
- package/dist/index.cjs +135 -113
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +153 -102
- package/dist/index.mjs +102 -105
- package/dist/index.mjs.map +1 -1
- package/package.json +10 -21
- package/dist/SeedcordError-C1GYc1BF.cjs +0 -413
- package/dist/SeedcordError-C1GYc1BF.cjs.map +0 -1
- package/dist/SeedcordError-D6uPv6qc.d.mts +0 -339
- package/dist/SeedcordError-E2D_RTuy.mjs +0 -350
- package/dist/SeedcordError-E2D_RTuy.mjs.map +0 -1
- package/dist/internal.index.cjs +0 -6
- package/dist/internal.index.d.cts +0 -1
- package/dist/internal.index.d.mts +0 -2
- package/dist/internal.index.mjs +0 -3
package/dist/index.mjs
CHANGED
|
@@ -1,15 +1,94 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import { createServer } from "http";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { EventEmitter } from "node:events";
|
|
4
|
+
import { SeedcordErrorCode } from "@seedcord/errors";
|
|
5
|
+
import { SeedcordError } from "@seedcord/errors/internal";
|
|
3
6
|
import path from "node:path";
|
|
7
|
+
import { Envapter } from "envapt";
|
|
4
8
|
import winston, { createLogger, format, transports } from "winston";
|
|
5
9
|
import fs from "node:fs";
|
|
6
10
|
import stripAnsi from "strip-ansi";
|
|
7
11
|
import TransportStream from "winston-transport";
|
|
8
12
|
import { formatFilePath } from "@seedcord/utils";
|
|
9
|
-
import chalk from "chalk";
|
|
10
|
-
import { createServer } from "http";
|
|
11
|
-
import { EventEmitter } from "node:events";
|
|
12
13
|
|
|
14
|
+
//#region src/RateLimiter.ts
|
|
15
|
+
const SWEEP_INTERVAL_MS = 6e4;
|
|
16
|
+
/**
|
|
17
|
+
* Tracks per-key usage windows and reports when a key is limited and when it frees up.
|
|
18
|
+
*
|
|
19
|
+
* Each key holds a sliding window of hit expiry times, and is limited once its live-hit count
|
|
20
|
+
* reaches the window's `limit`. Expired hits are dropped on the next read, and a background sweep
|
|
21
|
+
* drops fully-expired keys so the map does not grow without bound.
|
|
22
|
+
*/
|
|
23
|
+
var RateLimiter = class {
|
|
24
|
+
map = /* @__PURE__ */ new Map();
|
|
25
|
+
constructor() {
|
|
26
|
+
setInterval(() => {
|
|
27
|
+
this.sweep();
|
|
28
|
+
}, SWEEP_INTERVAL_MS).unref();
|
|
29
|
+
}
|
|
30
|
+
/** Number of keys currently tracked. */
|
|
31
|
+
get size() {
|
|
32
|
+
return this.map.size;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Records a hit for `key` and reports whether the key is now limited.
|
|
36
|
+
*
|
|
37
|
+
* @param key - The bucket to record against. The caller builds it from its own scope.
|
|
38
|
+
* @param window - The usage window to apply for this hit.
|
|
39
|
+
*/
|
|
40
|
+
hit(key, window) {
|
|
41
|
+
const now = Date.now();
|
|
42
|
+
const limit = Math.max(1, window.limit ?? 1);
|
|
43
|
+
const live = this.live(key, now);
|
|
44
|
+
if (live.length >= limit) {
|
|
45
|
+
this.map.set(key, live);
|
|
46
|
+
return {
|
|
47
|
+
limited: true,
|
|
48
|
+
expires: Math.min(...live)
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const expires = now + window.delay;
|
|
52
|
+
live.push(expires);
|
|
53
|
+
this.map.set(key, live);
|
|
54
|
+
return {
|
|
55
|
+
limited: false,
|
|
56
|
+
expires
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Reports whether `key` is limited right now without recording a hit.
|
|
61
|
+
*
|
|
62
|
+
* The read half of a peek-then-commit. A gate calls `peek` to decide whether to refuse, and
|
|
63
|
+
* `hit` to charge the slot only once it is the gate that let the request through.
|
|
64
|
+
*/
|
|
65
|
+
peek(key, window) {
|
|
66
|
+
const now = Date.now();
|
|
67
|
+
const limit = Math.max(1, window.limit ?? 1);
|
|
68
|
+
const live = this.live(key, now);
|
|
69
|
+
if (live.length >= limit) return {
|
|
70
|
+
limited: true,
|
|
71
|
+
expires: Math.min(...live)
|
|
72
|
+
};
|
|
73
|
+
return {
|
|
74
|
+
limited: false,
|
|
75
|
+
expires: now + window.delay
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
live(key, now) {
|
|
79
|
+
return (this.map.get(key) ?? []).filter((exp) => exp > now);
|
|
80
|
+
}
|
|
81
|
+
sweep() {
|
|
82
|
+
const now = Date.now();
|
|
83
|
+
for (const [key, live] of this.map) {
|
|
84
|
+
const kept = live.filter((exp) => exp > now);
|
|
85
|
+
if (kept.length === 0) this.map.delete(key);
|
|
86
|
+
else this.map.set(key, kept);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
//#endregion
|
|
13
92
|
//#region src/Logger/LogFormatter.ts
|
|
14
93
|
/**
|
|
15
94
|
* Formats log records for console and file outputs.
|
|
@@ -665,26 +744,13 @@ var Logger = class Logger {
|
|
|
665
744
|
channel;
|
|
666
745
|
registry = LoggerChannelRegistry.instance;
|
|
667
746
|
utils;
|
|
668
|
-
static instances = /* @__PURE__ */ new Map();
|
|
669
|
-
static instance(prefix, channel) {
|
|
670
|
-
const key = channel ? `${channel}::${prefix}` : prefix;
|
|
671
|
-
let instance = this.instances.get(key);
|
|
672
|
-
if (!instance) {
|
|
673
|
-
instance = new Logger(prefix, channel ? { channel } : void 0);
|
|
674
|
-
this.instances.set(key, instance);
|
|
675
|
-
}
|
|
676
|
-
return instance;
|
|
677
|
-
}
|
|
678
747
|
/**
|
|
679
|
-
* Configures global logger settings.
|
|
680
|
-
*
|
|
681
|
-
* Applies configuration to all channels and clears instance cache.
|
|
748
|
+
* Configures global logger settings, applied to all channels.
|
|
682
749
|
*
|
|
683
750
|
* @param config - Partial configuration to merge with defaults
|
|
684
751
|
*/
|
|
685
752
|
static configure(config) {
|
|
686
753
|
LoggerChannelRegistry.instance.configure(config);
|
|
687
|
-
this.instances.clear();
|
|
688
754
|
}
|
|
689
755
|
/**
|
|
690
756
|
* Creates a new Logger instance.
|
|
@@ -714,12 +780,12 @@ var Logger = class Logger {
|
|
|
714
780
|
});
|
|
715
781
|
}
|
|
716
782
|
/**
|
|
717
|
-
* Returns a new Logger
|
|
783
|
+
* Returns a new Logger for this label on the specified channel.
|
|
718
784
|
*
|
|
719
785
|
* @param channel - Channel name to use
|
|
720
786
|
*/
|
|
721
787
|
inChannel(channel) {
|
|
722
|
-
return Logger
|
|
788
|
+
return new Logger(this.label, { channel });
|
|
723
789
|
}
|
|
724
790
|
/**
|
|
725
791
|
* Logs an error message with optional additional data.
|
|
@@ -786,75 +852,6 @@ var Logger = class Logger {
|
|
|
786
852
|
}
|
|
787
853
|
};
|
|
788
854
|
|
|
789
|
-
//#endregion
|
|
790
|
-
//#region src/CooldownManager.ts
|
|
791
|
-
const logger = new Logger("CooldownManager");
|
|
792
|
-
/**
|
|
793
|
-
* Lightweight utility for per-key cooldowns.
|
|
794
|
-
*
|
|
795
|
-
* Manages time-based restrictions on operations by key,
|
|
796
|
-
* useful for rate limiting, command cooldowns, and spam prevention.
|
|
797
|
-
*/
|
|
798
|
-
var CooldownManager = class {
|
|
799
|
-
window;
|
|
800
|
-
Err;
|
|
801
|
-
msg;
|
|
802
|
-
map = /* @__PURE__ */ new Map();
|
|
803
|
-
/**
|
|
804
|
-
* Creates a new CooldownManager instance.
|
|
805
|
-
*
|
|
806
|
-
* @param opts - Configuration options for the cooldown behavior
|
|
807
|
-
*/
|
|
808
|
-
constructor(opts = {}) {
|
|
809
|
-
this.window = opts.cooldown ?? 1e3;
|
|
810
|
-
this.Err = opts.err ?? Error;
|
|
811
|
-
this.msg = opts.message ?? "Cooldown active";
|
|
812
|
-
}
|
|
813
|
-
/**
|
|
814
|
-
* Records usage timestamp for a key without any cooldown checks.
|
|
815
|
-
*
|
|
816
|
-
* @param key - The unique identifier for the cooldown entry
|
|
817
|
-
*/
|
|
818
|
-
set(key) {
|
|
819
|
-
this.map.set(key, Date.now());
|
|
820
|
-
}
|
|
821
|
-
/**
|
|
822
|
-
* Verifies cooldown status for a key and updates timestamp if not active.
|
|
823
|
-
*
|
|
824
|
-
* If the cooldown is still active, throws the configured error.
|
|
825
|
-
* If not active, updates the timestamp and returns successfully.
|
|
826
|
-
*
|
|
827
|
-
* @param key - The unique identifier to check cooldown for
|
|
828
|
-
* @throws An {@link Err} When the cooldown is still active for the given key
|
|
829
|
-
*/
|
|
830
|
-
check(key) {
|
|
831
|
-
const now = Date.now();
|
|
832
|
-
const last = this.map.get(key);
|
|
833
|
-
const remaining = this.window - (now - (last ?? 0));
|
|
834
|
-
if (Envapter.isDevelopment && remaining > 0) logger.debug(`${key} - ${remaining}ms remaining`);
|
|
835
|
-
if (last !== void 0 && remaining > 0) throw new this.Err(this.msg, remaining);
|
|
836
|
-
this.map.set(key, now);
|
|
837
|
-
}
|
|
838
|
-
/**
|
|
839
|
-
* Checks if a key is currently cooling down without updating timestamp.
|
|
840
|
-
*
|
|
841
|
-
* @param key - The unique identifier to check
|
|
842
|
-
* @returns True if the key is still cooling down, false otherwise
|
|
843
|
-
*/
|
|
844
|
-
isActive(key) {
|
|
845
|
-
const last = this.map.get(key);
|
|
846
|
-
return last !== void 0 && Date.now() - last < this.window;
|
|
847
|
-
}
|
|
848
|
-
/**
|
|
849
|
-
* Removes a key from the cooldown map.
|
|
850
|
-
*
|
|
851
|
-
* @param key - The unique identifier to remove (useful for manual resets)
|
|
852
|
-
*/
|
|
853
|
-
clear(key) {
|
|
854
|
-
this.map.delete(key);
|
|
855
|
-
}
|
|
856
|
-
};
|
|
857
|
-
|
|
858
855
|
//#endregion
|
|
859
856
|
//#region src/StrictEventEmitter.ts
|
|
860
857
|
/**
|
|
@@ -965,7 +962,7 @@ var StrictEventEmitter = class extends EventEmitter {
|
|
|
965
962
|
};
|
|
966
963
|
const onAbort = () => {
|
|
967
964
|
cleanup();
|
|
968
|
-
reject(new SeedcordError(
|
|
965
|
+
reject(new SeedcordError(SeedcordErrorCode.EventEmitterWaitForAborted));
|
|
969
966
|
};
|
|
970
967
|
let timeoutId = null;
|
|
971
968
|
const cleanup = () => {
|
|
@@ -982,7 +979,7 @@ var StrictEventEmitter = class extends EventEmitter {
|
|
|
982
979
|
const timeoutMs = opts.timeoutMs;
|
|
983
980
|
timeoutId = setTimeout(() => {
|
|
984
981
|
cleanup();
|
|
985
|
-
reject(new SeedcordError(
|
|
982
|
+
reject(new SeedcordError(SeedcordErrorCode.EventEmitterWaitForTimeout, [timeoutMs]));
|
|
986
983
|
}, timeoutMs);
|
|
987
984
|
}
|
|
988
985
|
});
|
|
@@ -1026,7 +1023,7 @@ var CoordinatedLifecycle = class extends StrictEventEmitter {
|
|
|
1026
1023
|
addTask(phase, taskName, task, timeoutMs) {
|
|
1027
1024
|
if (!this.canAddTask()) return;
|
|
1028
1025
|
const tasks = this.tasksMap.get(phase);
|
|
1029
|
-
if (!tasks) throw new SeedcordError(
|
|
1026
|
+
if (!tasks) throw new SeedcordError(SeedcordErrorCode.LifecycleUnknownPhase, [phase]);
|
|
1030
1027
|
tasks.push({
|
|
1031
1028
|
name: taskName,
|
|
1032
1029
|
task,
|
|
@@ -1064,7 +1061,7 @@ var CoordinatedLifecycle = class extends StrictEventEmitter {
|
|
|
1064
1061
|
this.logger.info(`${chalk.bold.yellow("Running")} ${this.getTaskType()} phase ${chalk.bold.magenta(this.phaseEnum[phase])} with ${chalk.bold.cyan(tasks.length)} tasks`);
|
|
1065
1062
|
this.emitPhase(phase, "start");
|
|
1066
1063
|
const failures = (await this.executeTasksInPhase(phase, tasks)).filter((r) => r.status === "rejected").length;
|
|
1067
|
-
if (failures > 0) throw new SeedcordError(
|
|
1064
|
+
if (failures > 0) throw new SeedcordError(SeedcordErrorCode.LifecyclePhaseFailures, [this.phaseEnum[phase], failures]);
|
|
1068
1065
|
else this.logger.info(`Phase ${chalk.bold.magenta(this.phaseEnum[phase])} ${chalk.bold.green("completed successfully")}`);
|
|
1069
1066
|
this.emitPhase(phase, "complete");
|
|
1070
1067
|
}
|
|
@@ -1077,7 +1074,7 @@ var CoordinatedLifecycle = class extends StrictEventEmitter {
|
|
|
1077
1074
|
try {
|
|
1078
1075
|
await Promise.race([task.task(), new Promise((_, reject) => {
|
|
1079
1076
|
timeoutId = setTimeout(() => {
|
|
1080
|
-
reject(new SeedcordError(
|
|
1077
|
+
reject(new SeedcordError(SeedcordErrorCode.LifecycleTaskTimeout, [task.name, task.timeout]));
|
|
1081
1078
|
}, task.timeout);
|
|
1082
1079
|
})]);
|
|
1083
1080
|
this.logger.info(`${chalk.italic("Completed")} task ${chalk.bold.cyan(task.name)} in phase ${chalk.bold.magenta(this.phaseEnum[phase])}`);
|
|
@@ -1179,7 +1176,7 @@ var CoordinatedShutdown = class extends CoordinatedLifecycle {
|
|
|
1179
1176
|
* @param phase - The shutdown phase from {@link ShutdownPhase}
|
|
1180
1177
|
* @param taskName - Unique identifier for the task
|
|
1181
1178
|
* @param task - Async function to execute
|
|
1182
|
-
* @param timeoutMs - Task timeout in milliseconds {@default 5000}
|
|
1179
|
+
* @param timeoutMs - Task timeout in milliseconds. {@default `5000`}
|
|
1183
1180
|
*/
|
|
1184
1181
|
addTask(phase, taskName, task, timeoutMs = 5e3) {
|
|
1185
1182
|
super.addTask(phase, taskName, task, timeoutMs);
|
|
@@ -1201,8 +1198,8 @@ var CoordinatedShutdown = class extends CoordinatedLifecycle {
|
|
|
1201
1198
|
* Tasks within each phase are executed in parallel for faster shutdown.
|
|
1202
1199
|
* Process exits with the specified code when complete.
|
|
1203
1200
|
*
|
|
1204
|
-
* @param exitCode - Process exit code {@default 0}
|
|
1205
|
-
* @param exitProcess - Whether to exit the process after shutdown {@default true}
|
|
1201
|
+
* @param exitCode - Process exit code. {@default `0`}
|
|
1202
|
+
* @param exitProcess - Whether to exit the process after shutdown. {@default `true`}
|
|
1206
1203
|
* @returns Promise that resolves when shutdown is complete
|
|
1207
1204
|
* @example
|
|
1208
1205
|
* ```typescript
|
|
@@ -1375,18 +1372,18 @@ var CoordinatedStartup = class extends CoordinatedLifecycle {
|
|
|
1375
1372
|
* @param phase - The startup phase from {@link StartupPhase}
|
|
1376
1373
|
* @param taskName - Unique identifier for the task
|
|
1377
1374
|
* @param task - Async function to execute
|
|
1378
|
-
* @param timeoutMs - Task timeout in milliseconds {@default 10000}
|
|
1375
|
+
* @param timeoutMs - Task timeout in milliseconds. {@default `10000`}
|
|
1379
1376
|
*/
|
|
1380
1377
|
addTask(phase, taskName, task, timeoutMs = 1e4) {
|
|
1381
1378
|
super.addTask(phase, taskName, task, timeoutMs);
|
|
1382
1379
|
}
|
|
1383
1380
|
canAddTask() {
|
|
1384
|
-
if (this.hasStarted) throw new SeedcordError(
|
|
1385
|
-
if (this.isStartingUp) throw new SeedcordError(
|
|
1381
|
+
if (this.hasStarted) throw new SeedcordError(SeedcordErrorCode.LifecycleAddAfterCompletion);
|
|
1382
|
+
if (this.isStartingUp) throw new SeedcordError(SeedcordErrorCode.LifecycleAddDuringRun);
|
|
1386
1383
|
return true;
|
|
1387
1384
|
}
|
|
1388
1385
|
canRemoveTask() {
|
|
1389
|
-
if (this.isStartingUp) throw new SeedcordError(
|
|
1386
|
+
if (this.isStartingUp) throw new SeedcordError(SeedcordErrorCode.LifecycleRemoveDuringRun);
|
|
1390
1387
|
return true;
|
|
1391
1388
|
}
|
|
1392
1389
|
getTaskType() {
|
|
@@ -1453,7 +1450,7 @@ var CoordinatedStartup = class extends CoordinatedLifecycle {
|
|
|
1453
1450
|
try {
|
|
1454
1451
|
await Promise.race([task.task(), new Promise((_, reject) => {
|
|
1455
1452
|
timeoutId = setTimeout(() => {
|
|
1456
|
-
reject(new SeedcordError(
|
|
1453
|
+
reject(new SeedcordError(SeedcordErrorCode.LifecycleTaskTimeout, [task.name, task.timeout]));
|
|
1457
1454
|
}, task.timeout);
|
|
1458
1455
|
})]);
|
|
1459
1456
|
this.logger.info(`${chalk.italic("Completed")} task ${chalk.bold.cyan(task.name)} in phase ${chalk.bold.magenta(StartupPhase[phase])}`);
|
|
@@ -1491,8 +1488,8 @@ var CoordinatedStartup = class extends CoordinatedLifecycle {
|
|
|
1491
1488
|
//#endregion
|
|
1492
1489
|
//#region src/index.ts
|
|
1493
1490
|
/** Package version */
|
|
1494
|
-
const version = "0.
|
|
1491
|
+
const version = "0.8.0";
|
|
1495
1492
|
|
|
1496
1493
|
//#endregion
|
|
1497
|
-
export {
|
|
1494
|
+
export { CoordinatedLifecycle, CoordinatedShutdown, CoordinatedStartup, HealthCheck, Logger, LoggerChannelRegistry, LoggerUtilities, RateLimiter, ShutdownPhase, StartupPhase, StrictEventEmitter, version };
|
|
1498
1495
|
//# sourceMappingURL=index.mjs.map
|