@seedcord/core 0.10.0 → 0.11.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 +27 -9
- package/dist/CoordinatedShutdown-H5YjZn8A.d.mts +78 -0
- package/dist/CoordinatedStartup-CGnJrqcl.mjs +168 -0
- package/dist/CoordinatedStartup-CGnJrqcl.mjs.map +1 -0
- package/dist/{HmrModuleHandler-DOAtah3j.mjs → HmrModuleHandler-8ZC-UuZh.mjs} +9 -22
- package/dist/HmrModuleHandler-8ZC-UuZh.mjs.map +1 -0
- package/dist/PluginHost-B8kxLOeb.mjs +245 -0
- package/dist/PluginHost-B8kxLOeb.mjs.map +1 -0
- package/dist/{Plugin-Dhx_ivqj.d.mts → PluginHost-DB4j8unu.d.mts} +110 -17
- package/dist/PluginLifecycle-AhQ46NIr.mjs +158 -0
- package/dist/PluginLifecycle-AhQ46NIr.mjs.map +1 -0
- package/dist/ServerHost-CtmAhEXn.mjs +168 -0
- package/dist/ServerHost-CtmAhEXn.mjs.map +1 -0
- package/dist/ServerHost-DYeh0KTk.d.mts +40 -0
- package/dist/bindViteEnv-BgFewlZ0.d.mts +11 -0
- package/dist/bindViteEnv-DWq5rftJ.mjs +88 -0
- package/dist/bindViteEnv-DWq5rftJ.mjs.map +1 -0
- package/dist/env-internal.index.d.mts +2 -0
- package/dist/env-internal.index.mjs +3 -0
- package/dist/hmr.index.d.mts +0 -1
- package/dist/hmr.index.mjs +1 -1
- package/dist/{cursor-BTU5bGoo.d.mts → index-Du71Xx69.d.mts} +253 -6
- package/dist/index.d.mts +19 -84
- package/dist/index.mjs +17 -37
- package/dist/index.mjs.map +1 -1
- package/dist/internal.index.d.mts +186 -22
- package/dist/internal.index.mjs +714 -19
- package/dist/internal.index.mjs.map +1 -1
- package/dist/node-internal.index.d.mts +25 -70
- package/dist/node-internal.index.mjs +83 -297
- package/dist/node-internal.index.mjs.map +1 -1
- package/dist/node.index.d.mts +3 -2
- package/dist/node.index.mjs +53 -200
- package/dist/node.index.mjs.map +1 -1
- package/dist/{phases-D_zQBXga.d.mts → phases-FTM69J3a.d.mts} +4 -4
- package/dist/plugin.index.d.mts +2 -2
- package/dist/plugin.index.mjs +2 -2
- package/dist/publishDefault-D3dAyzZ9.mjs +7 -0
- package/dist/publishDefault-D3dAyzZ9.mjs.map +1 -0
- package/dist/subscribers-Ce3SWy23.mjs +1913 -0
- package/dist/subscribers-Ce3SWy23.mjs.map +1 -0
- package/package.json +24 -17
- package/dist/Bus-BxPa-Uvp.mjs +0 -866
- package/dist/Bus-BxPa-Uvp.mjs.map +0 -1
- package/dist/HmrModuleHandler-DOAtah3j.mjs.map +0 -1
- package/dist/Pluggable-CwKbLui3.d.mts +0 -162
- package/dist/Pluggable-afy2H0Dr.mjs +0 -339
- package/dist/Pluggable-afy2H0Dr.mjs.map +0 -1
- package/dist/Plugin-CmNW5n6a.mjs +0 -122
- package/dist/Plugin-CmNW5n6a.mjs.map +0 -1
- package/dist/contextMenuLeaves-COzDYa6c.d.mts +0 -17
- package/dist/cursor-CgNBnJfu.mjs +0 -802
- package/dist/cursor-CgNBnJfu.mjs.map +0 -1
- package/dist/devChannel-CQU7hq7Q.mjs +0 -14
- package/dist/devChannel-CQU7hq7Q.mjs.map +0 -1
- package/dist/index-B_-EjyvS.d.mts +0 -135
- package/dist/publishDefault-BuSZPbjT.mjs +0 -14
- package/dist/publishDefault-BuSZPbjT.mjs.map +0 -1
- package/dist/slashRouteLeaves-k1hJnXDO.mjs +0 -31
- package/dist/slashRouteLeaves-k1hJnXDO.mjs.map +0 -1
- package/dist/subscribers-xGy7a7xR.mjs +0 -167
- package/dist/subscribers-xGy7a7xR.mjs.map +0 -1
- package/dist/types-BG0BimVr.d.mts +0 -13
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
<a href="https://seedcord.org">Website</a> ·
|
|
11
11
|
<a href="https://seedcord.org/guide">Guide</a> ·
|
|
12
12
|
<a href="https://seedcord.org/docs">Reference</a> ·
|
|
13
|
-
<a href="https://
|
|
13
|
+
<a href="https://seedcord.org/chat/">Discord</a>
|
|
14
14
|
</div>
|
|
15
15
|
|
|
16
16
|
<br />
|
|
@@ -23,20 +23,38 @@
|
|
|
23
23
|
|
|
24
24
|
## About
|
|
25
25
|
|
|
26
|
-
`@seedcord/core` is the
|
|
26
|
+
`@seedcord/core` is the code both seedcord transports share. It has the route decorators, the handler bases, checks, pagination, the startup and shutdown phases, and the plugin base. [`@seedcord/gateway`](https://www.npmjs.com/package/@seedcord/gateway) and [`@seedcord/http`](https://www.npmjs.com/package/@seedcord/http) connect it to Discord.
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
Both transports depend on core and re-export all of it, so a bot imports everything from its transport. Install core directly when you write a plugin that works on both transports.
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
seedcord is pre-1.0, so a minor version can break your plugin. Each release's changelog says what changed and how to update.
|
|
31
31
|
|
|
32
32
|
## Installation
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
```sh
|
|
35
|
+
pnpm add -D @seedcord/core envapt typescript
|
|
36
|
+
```
|
|
35
37
|
|
|
36
|
-
|
|
38
|
+
Also list `@seedcord/core` in your plugin's `peerDependencies`, with the versions you tested against. A plugin that extends a different copy of core than the bot's throws `CorePluginFromOtherCore` when the bot attaches it.
|
|
37
39
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
A plugin extends `Plugin` from `@seedcord/core/plugin` and implements `init()`:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { Plugin } from '@seedcord/core/plugin';
|
|
46
|
+
|
|
47
|
+
export class Uptime extends Plugin {
|
|
48
|
+
private startedAt = 0;
|
|
49
|
+
|
|
50
|
+
public async init(): Promise<void> {
|
|
51
|
+
this.startedAt = Date.now();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
public elapsed(): number {
|
|
55
|
+
return Date.now() - this.startedAt;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
40
58
|
```
|
|
41
59
|
|
|
42
|
-
|
|
60
|
+
A gateway or http bot attaches it with `.attach('uptime', Uptime)`, and handlers call `this.core.uptime.elapsed()`. The [Writing plugins guide](https://seedcord.org/guide/writing-plugins) covers options, lifecycle phases, testing a plugin locally, and publishing.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { n as StartupPhase, t as ShutdownPhase } from "./phases-FTM69J3a.mjs";
|
|
2
|
+
import { Logger } from "@seedcord/logger";
|
|
3
|
+
import { Promisable } from "type-fest";
|
|
4
|
+
//#region src/lifecycle/LifecycleTypes.d.ts
|
|
5
|
+
interface LifecycleTask {
|
|
6
|
+
name: string;
|
|
7
|
+
task: (signal: AbortSignal) => Promise<void>;
|
|
8
|
+
timeout: number;
|
|
9
|
+
}
|
|
10
|
+
//#endregion
|
|
11
|
+
//#region src/lifecycle/CoordinatedLifecycle.d.ts
|
|
12
|
+
declare abstract class CoordinatedLifecycle<TPhase extends number> {
|
|
13
|
+
#private;
|
|
14
|
+
protected readonly phaseOrder: TPhase[];
|
|
15
|
+
protected readonly phaseEnum: Record<number, string>;
|
|
16
|
+
protected readonly logger: Logger;
|
|
17
|
+
protected readonly tasksMap: Map<TPhase, LifecycleTask[]>;
|
|
18
|
+
protected constructor(loggerName: string, phaseOrder: TPhase[], phaseEnum: Record<number, string>);
|
|
19
|
+
addTask(phase: TPhase, taskName: string, task: LifecycleTask['task'], timeoutMs: number): void;
|
|
20
|
+
removeTask(phase: TPhase, taskName: string): boolean;
|
|
21
|
+
protected runPhase(phase: TPhase): Promise<void>;
|
|
22
|
+
protected runTaskWithTimeout(phase: TPhase, task: LifecycleTask): Promise<void>;
|
|
23
|
+
protected isAborted(): boolean;
|
|
24
|
+
protected beginRun(): void;
|
|
25
|
+
protected abortRun(): void;
|
|
26
|
+
protected abstract canAddTask(): boolean;
|
|
27
|
+
protected abstract canRemoveTask(): boolean;
|
|
28
|
+
protected abstract getTaskType(): string;
|
|
29
|
+
protected abstract executeTasksInPhase(phase: TPhase, tasks: LifecycleTask[]): Promise<PromiseSettledResult<void>[]>;
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region src/lifecycle/CoordinatedStartup.d.ts
|
|
33
|
+
declare class CoordinatedStartup extends CoordinatedLifecycle<StartupPhase> {
|
|
34
|
+
#private;
|
|
35
|
+
constructor();
|
|
36
|
+
/**
|
|
37
|
+
* Adds a task to a startup phase. The task gets a signal that aborts when it runs past `timeoutMs`
|
|
38
|
+
* or when startup is aborted. Pass it to any request the task makes.
|
|
39
|
+
*/
|
|
40
|
+
addTask(phase: StartupPhase, taskName: string, task: LifecycleTask['task'], timeoutMs?: number): void;
|
|
41
|
+
protected canAddTask(): boolean;
|
|
42
|
+
protected canRemoveTask(): boolean;
|
|
43
|
+
protected getTaskType(): string;
|
|
44
|
+
protected executeTasksInPhase(phase: StartupPhase, tasks: LifecycleTask[]): Promise<PromiseSettledResult<void>[]>;
|
|
45
|
+
/** @internal */
|
|
46
|
+
run(afterReady?: () => Promisable<void>): Promise<void>;
|
|
47
|
+
protected isAborted(): boolean;
|
|
48
|
+
/** @internal */
|
|
49
|
+
abort(): void;
|
|
50
|
+
/** @internal */
|
|
51
|
+
get isReady(): boolean;
|
|
52
|
+
}
|
|
53
|
+
//#endregion
|
|
54
|
+
//#region src/node/Lifecycle/CoordinatedShutdown.d.ts
|
|
55
|
+
declare class CoordinatedShutdown extends CoordinatedLifecycle<ShutdownPhase> {
|
|
56
|
+
#private;
|
|
57
|
+
constructor(deadlineMs?: number);
|
|
58
|
+
/** @internal */
|
|
59
|
+
setDeadline(deadlineMs: number): void;
|
|
60
|
+
protected canAddTask(): boolean;
|
|
61
|
+
protected canRemoveTask(): boolean;
|
|
62
|
+
protected getTaskType(): string;
|
|
63
|
+
protected executeTasksInPhase(phase: ShutdownPhase, tasks: LifecycleTask[]): Promise<PromiseSettledResult<void>[]>;
|
|
64
|
+
/** @internal */
|
|
65
|
+
registerSignalHandlers(): void;
|
|
66
|
+
/** @internal */
|
|
67
|
+
removeSignalHandlers(): void;
|
|
68
|
+
/** @internal run() awaits this so boot finishes registering its dispose tasks first */
|
|
69
|
+
gateOnStartup(settled: Promise<void>): void;
|
|
70
|
+
addTask(phase: ShutdownPhase, taskName: string, task: LifecycleTask['task'], timeoutMs?: number): void;
|
|
71
|
+
/** @internal */
|
|
72
|
+
removeTask(phase: ShutdownPhase, taskName: string): boolean;
|
|
73
|
+
/** @internal */
|
|
74
|
+
run(exitCode?: number, exitProcess?: boolean): Promise<void>;
|
|
75
|
+
}
|
|
76
|
+
//#endregion
|
|
77
|
+
export { CoordinatedStartup as n, LifecycleTask as r, CoordinatedShutdown as t };
|
|
78
|
+
//# sourceMappingURL=CoordinatedShutdown-H5YjZn8A.d.mts.map
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { n as StartupPhase } from "./phases-BauKJOTT.mjs";
|
|
2
|
+
import { r as withTimeout } from "./PluginLifecycle-AhQ46NIr.mjs";
|
|
3
|
+
import { SeedcordErrorCode, paint } from "@seedcord/errors";
|
|
4
|
+
import { SeedcordAggregateError, SeedcordError, SeedcordTypeError } from "@seedcord/errors/internal";
|
|
5
|
+
import { Logger } from "@seedcord/logger";
|
|
6
|
+
|
|
7
|
+
//#region src/lifecycle/CoordinatedLifecycle.ts
|
|
8
|
+
var CoordinatedLifecycle = class {
|
|
9
|
+
phaseOrder;
|
|
10
|
+
phaseEnum;
|
|
11
|
+
logger;
|
|
12
|
+
tasksMap = /* @__PURE__ */ new Map();
|
|
13
|
+
#run = new AbortController();
|
|
14
|
+
constructor(loggerName, phaseOrder, phaseEnum) {
|
|
15
|
+
this.phaseOrder = phaseOrder;
|
|
16
|
+
this.phaseEnum = phaseEnum;
|
|
17
|
+
this.logger = new Logger(loggerName, { channel: "lifecycle" });
|
|
18
|
+
this.phaseOrder.forEach((phase) => this.tasksMap.set(phase, []));
|
|
19
|
+
}
|
|
20
|
+
addTask(phase, taskName, task, timeoutMs) {
|
|
21
|
+
if (!this.canAddTask()) return;
|
|
22
|
+
const tasks = this.tasksMap.get(phase);
|
|
23
|
+
if (!tasks) throw new SeedcordTypeError(SeedcordErrorCode.LifecycleUnknownPhase, [phase]);
|
|
24
|
+
tasks.push({
|
|
25
|
+
name: taskName,
|
|
26
|
+
task,
|
|
27
|
+
timeout: timeoutMs
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
removeTask(phase, taskName) {
|
|
31
|
+
if (!this.canRemoveTask()) return false;
|
|
32
|
+
const tasks = this.tasksMap.get(phase);
|
|
33
|
+
if (!tasks) return false;
|
|
34
|
+
const initialLength = tasks.length;
|
|
35
|
+
const filteredTasks = tasks.filter((task) => task.name !== taskName);
|
|
36
|
+
this.tasksMap.set(phase, filteredTasks);
|
|
37
|
+
return initialLength !== filteredTasks.length;
|
|
38
|
+
}
|
|
39
|
+
async runPhase(phase) {
|
|
40
|
+
const tasks = this.tasksMap.get(phase) ?? [];
|
|
41
|
+
if (tasks.length === 0) {
|
|
42
|
+
this.logger.trace(`No tasks to run in phase ${paint.iris.bold(this.phaseEnum[phase])}`);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
this.logger.debug(`${paint.amber.bold("Running")} ${this.getTaskType()} phase ${paint.iris.bold(this.phaseEnum[phase])} with ${paint.sky.bold(String(tasks.length))} tasks`);
|
|
46
|
+
const reasons = (await this.executeTasksInPhase(phase, tasks)).reduce((rejected, result) => {
|
|
47
|
+
if (result.status === "rejected") rejected.push(result.reason);
|
|
48
|
+
return rejected;
|
|
49
|
+
}, []);
|
|
50
|
+
if (reasons.length > 0) throw new SeedcordAggregateError(SeedcordErrorCode.LifecyclePhaseFailures, reasons, [this.phaseEnum[phase], reasons.length]);
|
|
51
|
+
this.logger.debug(`Phase ${paint.iris.bold(this.phaseEnum[phase])} ${paint.mint.bold("completed successfully")}`);
|
|
52
|
+
}
|
|
53
|
+
async runTaskWithTimeout(phase, task) {
|
|
54
|
+
this.logger.trace(`${paint.italic("Starting")} task ${paint.sky.bold(task.name)} in phase ${paint.iris.bold(this.phaseEnum[phase])}`);
|
|
55
|
+
const stopTask = new AbortController();
|
|
56
|
+
const signal = AbortSignal.any([stopTask.signal, this.#run.signal]);
|
|
57
|
+
try {
|
|
58
|
+
await withTimeout(task.name, () => task.task(signal), task.timeout);
|
|
59
|
+
this.logger.trace(`${paint.italic("Completed")} task ${paint.sky.bold(task.name)} in phase ${paint.iris.bold(this.phaseEnum[phase])}`);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
stopTask.abort(error);
|
|
62
|
+
if (this.isAborted()) return;
|
|
63
|
+
this.logger.error(`${paint.italic("Failed")} task ${paint.sky.bold(task.name)} in phase ${paint.iris.bold(this.phaseEnum[phase])}:`, error);
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
isAborted() {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
beginRun() {
|
|
71
|
+
this.#run = new AbortController();
|
|
72
|
+
}
|
|
73
|
+
abortRun() {
|
|
74
|
+
this.#run.abort();
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
//#endregion
|
|
79
|
+
//#region src/lifecycle/CoordinatedStartup.ts
|
|
80
|
+
const PHASE_ORDER = [
|
|
81
|
+
1,
|
|
82
|
+
2,
|
|
83
|
+
3
|
|
84
|
+
];
|
|
85
|
+
var CoordinatedStartup = class extends CoordinatedLifecycle {
|
|
86
|
+
#isStartingUp = false;
|
|
87
|
+
#hasStarted = false;
|
|
88
|
+
constructor() {
|
|
89
|
+
super("Startup", PHASE_ORDER, StartupPhase);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Adds a task to a startup phase. The task gets a signal that aborts when it runs past `timeoutMs`
|
|
93
|
+
* or when startup is aborted. Pass it to any request the task makes.
|
|
94
|
+
*/
|
|
95
|
+
addTask(phase, taskName, task, timeoutMs = 1e4) {
|
|
96
|
+
super.addTask(phase, taskName, task, timeoutMs);
|
|
97
|
+
}
|
|
98
|
+
canAddTask() {
|
|
99
|
+
if (this.#hasStarted) throw new SeedcordError(SeedcordErrorCode.LifecycleTaskChangeRejected, ["add", "finished"]);
|
|
100
|
+
if (this.#isStartingUp) throw new SeedcordError(SeedcordErrorCode.LifecycleTaskChangeRejected, ["add", "running"]);
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
canRemoveTask() {
|
|
104
|
+
if (this.#isStartingUp) throw new SeedcordError(SeedcordErrorCode.LifecycleTaskChangeRejected, ["remove", "running"]);
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
getTaskType() {
|
|
108
|
+
return "startup";
|
|
109
|
+
}
|
|
110
|
+
async executeTasksInPhase(phase, tasks) {
|
|
111
|
+
const promises = tasks.map((task) => this.runTaskWithTimeout(phase, task));
|
|
112
|
+
return Promise.allSettled(promises);
|
|
113
|
+
}
|
|
114
|
+
/** @internal */
|
|
115
|
+
async run(afterReady) {
|
|
116
|
+
if (this.#hasStarted) {
|
|
117
|
+
this.logger.warn("Startup sequence has already completed");
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (this.#isStartingUp) {
|
|
121
|
+
this.logger.warn("Startup sequence already in progress");
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
this.#isStartingUp = true;
|
|
125
|
+
this.beginRun();
|
|
126
|
+
this.logger.info(`${paint.mint.bold("Starting")} coordinated startup sequence`);
|
|
127
|
+
const steps = PHASE_ORDER.map((phase) => () => this.runPhase(phase));
|
|
128
|
+
if (afterReady) steps.push(afterReady);
|
|
129
|
+
try {
|
|
130
|
+
for (const step of steps) {
|
|
131
|
+
await step();
|
|
132
|
+
if (!this.#isStartingUp) {
|
|
133
|
+
this.logger.warn("Startup sequence aborted");
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
this.#hasStarted = true;
|
|
138
|
+
this.logger.info(`${paint.mint.bold("Coordinated startup completed")} successfully`);
|
|
139
|
+
} catch (error) {
|
|
140
|
+
if (!this.#isStartingUp) {
|
|
141
|
+
this.logger.warn("Startup sequence aborted during error handling");
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
this.logger.error(`${paint.coral.bold("Coordinated startup failed")}`);
|
|
145
|
+
throw error;
|
|
146
|
+
} finally {
|
|
147
|
+
this.#isStartingUp = false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
isAborted() {
|
|
151
|
+
return !this.#isStartingUp;
|
|
152
|
+
}
|
|
153
|
+
/** @internal */
|
|
154
|
+
abort() {
|
|
155
|
+
if (!this.#isStartingUp) return;
|
|
156
|
+
this.#isStartingUp = false;
|
|
157
|
+
this.abortRun();
|
|
158
|
+
this.logger.warn("Aborting coordinated startup sequence");
|
|
159
|
+
}
|
|
160
|
+
/** @internal */
|
|
161
|
+
get isReady() {
|
|
162
|
+
return this.#hasStarted;
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
//#endregion
|
|
167
|
+
export { CoordinatedLifecycle as n, CoordinatedStartup as t };
|
|
168
|
+
//# sourceMappingURL=CoordinatedStartup-CGnJrqcl.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CoordinatedStartup-CGnJrqcl.mjs","names":[],"sources":["../src/lifecycle/CoordinatedLifecycle.ts","../src/lifecycle/CoordinatedStartup.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, paint } from '@seedcord/errors';\nimport { SeedcordAggregateError, SeedcordTypeError } from '@seedcord/errors/internal';\nimport { Logger } from '@seedcord/logger';\n\nimport { withTimeout } from './withTimeout';\n\nimport type { LifecycleTask } from './LifecycleTypes';\n\n// base for the startup and shutdown coordinators, runs phase-ordered tasks off a per-phase map\nexport abstract class CoordinatedLifecycle<TPhase extends number> {\n protected readonly logger: Logger;\n protected readonly tasksMap = new Map<TPhase, LifecycleTask[]>();\n #run = new AbortController();\n\n protected constructor(\n loggerName: string,\n protected readonly phaseOrder: TPhase[],\n protected readonly phaseEnum: Record<number, string>\n ) {\n this.logger = new Logger(loggerName, { channel: 'lifecycle' });\n this.phaseOrder.forEach((phase) => this.tasksMap.set(phase, []));\n }\n\n public addTask(phase: TPhase, taskName: string, task: LifecycleTask['task'], timeoutMs: number): void {\n if (!this.canAddTask()) return;\n\n const tasks = this.tasksMap.get(phase);\n if (!tasks) {\n throw new SeedcordTypeError(SeedcordErrorCode.LifecycleUnknownPhase, [phase]);\n }\n\n tasks.push({ name: taskName, task, timeout: timeoutMs });\n }\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 return initialLength !== filteredTasks.length;\n }\n\n protected async runPhase(phase: TPhase): Promise<void> {\n const tasks = this.tasksMap.get(phase) ?? [];\n if (tasks.length === 0) {\n this.logger.trace(`No tasks to run in phase ${paint.iris.bold(this.phaseEnum[phase])}`);\n return;\n }\n\n this.logger.debug(\n `${paint.amber.bold('Running')} ${this.getTaskType()} phase ${paint.iris.bold(this.phaseEnum[phase])} with ${paint.sky.bold(String(tasks.length))} tasks`\n );\n\n const results: PromiseSettledResult<void>[] = await this.executeTasksInPhase(phase, tasks);\n\n const reasons = results.reduce<unknown[]>((rejected, result) => {\n if (result.status === 'rejected') rejected.push(result.reason as unknown);\n return rejected;\n }, []);\n if (reasons.length > 0) {\n // paint here would leak ANSI codes into the serialized error message (the unknown-exception webhook)\n throw new SeedcordAggregateError(SeedcordErrorCode.LifecyclePhaseFailures, reasons, [\n this.phaseEnum[phase],\n reasons.length\n ]);\n }\n\n this.logger.debug(\n `Phase ${paint.iris.bold(this.phaseEnum[phase])} ${paint.mint.bold('completed successfully')}`\n );\n }\n\n protected async runTaskWithTimeout(phase: TPhase, task: LifecycleTask): Promise<void> {\n this.logger.trace(\n `${paint.italic('Starting')} task ${paint.sky.bold(task.name)} in phase ${paint.iris.bold(this.phaseEnum[phase])}`\n );\n\n const stopTask = new AbortController();\n const signal = AbortSignal.any([stopTask.signal, this.#run.signal]);\n try {\n await withTimeout(task.name, () => task.task(signal), task.timeout);\n\n this.logger.trace(\n `${paint.italic('Completed')} task ${paint.sky.bold(task.name)} in phase ${paint.iris.bold(this.phaseEnum[phase])}`\n );\n } catch (error) {\n stopTask.abort(error);\n if (this.isAborted()) return;\n this.logger.error(\n `${paint.italic('Failed')} task ${paint.sky.bold(task.name)} in phase ${paint.iris.bold(this.phaseEnum[phase])}:`,\n error\n );\n throw error;\n }\n }\n\n protected isAborted(): boolean {\n return false;\n }\n\n protected beginRun(): void {\n this.#run = new AbortController();\n }\n\n protected abortRun(): void {\n this.#run.abort();\n }\n\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 { SeedcordErrorCode, paint } from '@seedcord/errors';\nimport { SeedcordError } from '@seedcord/errors/internal';\n\nimport { StartupPhase } from '#src/lifecycle/phases';\n\nimport { CoordinatedLifecycle } from './CoordinatedLifecycle';\n\nimport type { LifecycleTask } from './LifecycleTypes';\nimport type { Promisable } from 'type-fest';\n\nconst PHASE_ORDER: StartupPhase[] = [StartupPhase.Configuration, StartupPhase.Login, StartupPhase.Ready];\n\nexport class CoordinatedStartup extends CoordinatedLifecycle<StartupPhase> {\n #isStartingUp = false;\n #hasStarted = false;\n\n public constructor() {\n super('Startup', PHASE_ORDER, StartupPhase);\n }\n\n /**\n * Adds a task to a startup phase. The task gets a signal that aborts when it runs past `timeoutMs`\n * or when startup is aborted. Pass it to any request the task makes.\n */\n public override addTask(\n phase: StartupPhase,\n taskName: string,\n task: LifecycleTask['task'],\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.LifecycleTaskChangeRejected, ['add', 'finished']);\n }\n\n if (this.#isStartingUp) {\n throw new SeedcordError(SeedcordErrorCode.LifecycleTaskChangeRejected, ['add', 'running']);\n }\n\n return true;\n }\n\n protected canRemoveTask(): boolean {\n if (this.#isStartingUp) {\n throw new SeedcordError(SeedcordErrorCode.LifecycleTaskChangeRejected, ['remove', 'running']);\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 // each phase completes fully before the next begins\n /** @internal */\n public async run(afterReady?: () => Promisable<void>): 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.beginRun();\n this.logger.info(`${paint.mint.bold('Starting')} coordinated startup sequence`);\n\n const steps: (() => Promisable<void>)[] = PHASE_ORDER.map((phase) => () => this.runPhase(phase));\n if (afterReady) steps.push(afterReady);\n\n try {\n for (const step of steps) {\n await step();\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 }\n\n this.#hasStarted = true;\n this.logger.info(`${paint.mint.bold('Coordinated startup completed')} successfully`);\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(`${paint.coral.bold('Coordinated startup failed')}`);\n throw error;\n } finally {\n this.#isStartingUp = false;\n }\n }\n\n protected override isAborted(): boolean {\n return !this.#isStartingUp;\n }\n\n /** @internal */\n public abort(): void {\n if (!this.#isStartingUp) return;\n\n this.#isStartingUp = false;\n this.abortRun();\n this.logger.warn('Aborting coordinated startup sequence');\n }\n\n /** @internal */\n public get isReady(): boolean {\n return this.#hasStarted;\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { paint } from "@seedcord/errors";
|
|
2
|
+
import { botFolder, vitePluginThrew } from "@seedcord/utils/internal";
|
|
2
3
|
import { formatFilePath } from "@seedcord/utils";
|
|
3
|
-
import { existsSync } from "node:fs";
|
|
4
|
-
import { resolve, sep } from "node:path";
|
|
5
4
|
import { pathToFileURL } from "node:url";
|
|
6
5
|
|
|
7
6
|
//#region src/hmr/HmrModuleHandler.ts
|
|
@@ -30,19 +29,11 @@ var HmrModuleHandler = class {
|
|
|
30
29
|
/** Unloads or reloads the modules affected by an {@link HmrUpdateEvent}, scoped to the handler and middleware directories. */
|
|
31
30
|
async handle(event) {
|
|
32
31
|
const { file, affectedModules, type } = event;
|
|
33
|
-
const { logger } = this.options;
|
|
34
32
|
if (type === "delete" || type === "deleteDir") {
|
|
35
33
|
this.report("Unloaded", this.unload(file), [file]);
|
|
36
34
|
return;
|
|
37
35
|
}
|
|
38
36
|
if (type === "createDir") return;
|
|
39
|
-
if (type === "update" && !existsSync(file)) {
|
|
40
|
-
if (this.isTracked(file)) {
|
|
41
|
-
logger.debug(`Received update for non-existent file: ${formatFilePath(file)}, treating as delete`);
|
|
42
|
-
this.report("Unloaded", this.unload(file), [file]);
|
|
43
|
-
}
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
37
|
const rollbackEnabled = event.rollback ?? true;
|
|
47
38
|
const filesToReload = affectedModules && affectedModules.length > 0 ? affectedModules : [file];
|
|
48
39
|
const startedAt = Date.now();
|
|
@@ -59,8 +50,8 @@ var HmrModuleHandler = class {
|
|
|
59
50
|
}
|
|
60
51
|
inScope(file) {
|
|
61
52
|
const { handlersDir, middlewaresDir } = this.options;
|
|
62
|
-
if (
|
|
63
|
-
return middlewaresDir ?
|
|
53
|
+
if (botFolder(handlersDir).holdsModule(file)) return true;
|
|
54
|
+
return middlewaresDir ? botFolder(middlewaresDir).holdsModule(file) : false;
|
|
64
55
|
}
|
|
65
56
|
report(verb, names, files, elapsedMs) {
|
|
66
57
|
if (names.length === 0) return;
|
|
@@ -101,9 +92,6 @@ var HmrModuleHandler = class {
|
|
|
101
92
|
}
|
|
102
93
|
set.add(middleware);
|
|
103
94
|
}
|
|
104
|
-
isTracked(file) {
|
|
105
|
-
return this.store.fileToHandlers.has(file) || this.store.fileToMiddlewares.has(file);
|
|
106
|
-
}
|
|
107
95
|
unload(file) {
|
|
108
96
|
const names = [];
|
|
109
97
|
const handlers = this.store.fileToHandlers.get(file);
|
|
@@ -128,14 +116,13 @@ var HmrModuleHandler = class {
|
|
|
128
116
|
}
|
|
129
117
|
async reloadFile(file) {
|
|
130
118
|
const { logger, isHandler, isMiddleware, registerHandler, registerMiddleware } = this.options;
|
|
131
|
-
if (!existsSync(file)) {
|
|
132
|
-
logger.debug(`File does not exist, skipping reload: ${formatFilePath(file)}`);
|
|
133
|
-
return null;
|
|
134
|
-
}
|
|
135
119
|
try {
|
|
136
120
|
let fileUrl = pathToFileURL(file).href;
|
|
137
121
|
if (process.env.VITEST === "true") fileUrl += `?update=${Date.now()}`;
|
|
138
|
-
const imported = await import(
|
|
122
|
+
const imported = await import(
|
|
123
|
+
/* @vite-ignore */
|
|
124
|
+
fileUrl
|
|
125
|
+
);
|
|
139
126
|
const names = [];
|
|
140
127
|
for (const val of Object.values(imported)) if (isHandler(val)) {
|
|
141
128
|
registerHandler(val, file);
|
|
@@ -148,7 +135,7 @@ var HmrModuleHandler = class {
|
|
|
148
135
|
}
|
|
149
136
|
return names;
|
|
150
137
|
} catch (error) {
|
|
151
|
-
logger.error(`Failed to reload file: ${file}`, error);
|
|
138
|
+
logger.error(`Failed to reload file: ${formatFilePath(file)}`, vitePluginThrew(error) ?? error);
|
|
152
139
|
return null;
|
|
153
140
|
}
|
|
154
141
|
}
|
|
@@ -172,4 +159,4 @@ var HmrModuleHandler = class {
|
|
|
172
159
|
|
|
173
160
|
//#endregion
|
|
174
161
|
export { HmrModuleHandler as t };
|
|
175
|
-
//# sourceMappingURL=HmrModuleHandler-
|
|
162
|
+
//# sourceMappingURL=HmrModuleHandler-8ZC-UuZh.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HmrModuleHandler-8ZC-UuZh.mjs","names":[],"sources":["../src/hmr/HmrModuleHandler.ts"],"sourcesContent":["import { pathToFileURL } from 'node:url';\n\nimport { paint } from '@seedcord/errors';\nimport { formatFilePath } from '@seedcord/utils';\nimport { botFolder, vitePluginThrew } from '@seedcord/utils/internal';\n\nimport type { Logger } from '@seedcord/logger';\nimport type { HmrUpdateEvent } from '@seedcord/types';\n\ninterface HmrStore<THandler, TMiddleware, TArtifacts> {\n fileToHandlers: Map<string, Set<THandler>>;\n fileToMiddlewares: Map<string, Set<TMiddleware>>;\n handlerArtifacts: Map<THandler, TArtifacts>;\n}\n\n// justified: units are caller-supplied classes, name is read for log labels only\nconst displayName = (unit: unknown, fallback: string): string => (unit as { name?: string }).name ?? fallback;\n\ninterface HmrModuleHandlerOptions<THandler, TMiddleware = void, TArtifacts = unknown> {\n handlersDir: string;\n middlewaresDir?: string;\n isHandler: (val: unknown) => val is THandler;\n isMiddleware?: (val: unknown) => val is TMiddleware;\n registerHandler: (handler: THandler, file: string) => void;\n registerMiddleware?: (middleware: TMiddleware, file: string) => void;\n unregisterHandler: (handler: THandler, artifacts?: TArtifacts) => void;\n unregisterMiddleware?: (middleware: TMiddleware) => void;\n /** Snapshotted per handler so a rollback re-registers with the same artifacts after the module re-imports. */\n getArtifacts?: (handler: THandler) => TArtifacts;\n logger: Logger;\n}\n\n/**\n * Reloads THandler and optional TMiddleware modules on HMR updates.\n *\n * Call `handle()` from your `HmrAware.onHmr()` for standard reload behavior, or write a fully custom `onHmr()` and skip this class.\n *\n * This is only useful during development.\n */\nexport class HmrModuleHandler<THandler, TMiddleware = void, TArtifacts = unknown> {\n private readonly store: HmrStore<THandler, TMiddleware, TArtifacts>;\n private readonly options: HmrModuleHandlerOptions<THandler, TMiddleware, TArtifacts>;\n\n constructor(options: HmrModuleHandlerOptions<THandler, TMiddleware, TArtifacts>) {\n this.options = { ...options, logger: options.logger.inChannel('hmr') };\n\n // no import.meta.hot.data stash needed because a leaf reload never reconstructs the singleton\n // holding these Maps\n this.store = {\n fileToHandlers: new Map(),\n fileToMiddlewares: new Map(),\n handlerArtifacts: new Map()\n };\n }\n\n /** Unloads or reloads the modules affected by an {@link HmrUpdateEvent}, scoped to the handler and middleware directories. */\n public async handle(event: HmrUpdateEvent): Promise<void> {\n const { file, affectedModules, type } = event;\n\n if (type === 'delete' || type === 'deleteDir') {\n this.report('Unloaded', this.unload(file), [file]);\n return;\n }\n\n // a directory is never importable. its files arrive as their own create events\n if (type === 'createDir') return;\n\n const rollbackEnabled = event.rollback ?? true;\n const filesToReload = affectedModules && affectedModules.length > 0 ? affectedModules : [file];\n\n const startedAt = Date.now();\n const names: string[] = [];\n const touched: string[] = [];\n\n for (const fileToReload of filesToReload) {\n if (!this.inScope(fileToReload)) continue;\n\n const reloaded = await this.reloadWithRollback(fileToReload, rollbackEnabled);\n if (reloaded.length === 0) continue;\n\n names.push(...reloaded);\n touched.push(fileToReload);\n }\n\n this.report(type === 'create' ? 'Registered' : 'Reloaded', names, touched, Date.now() - startedAt);\n }\n\n private inScope(file: string): boolean {\n const { handlersDir, middlewaresDir } = this.options;\n\n if (botFolder(handlersDir).holdsModule(file)) return true;\n return middlewaresDir ? botFolder(middlewaresDir).holdsModule(file) : false;\n }\n\n // one line per save, since one edit can reload every module that imports the file\n private report(verb: string, names: string[], files: string[], elapsedMs?: number): void {\n if (names.length === 0) return;\n\n const from = files.length === 1 ? formatFilePath(files[0] ?? '') : `${String(files.length)} files`;\n const timing = elapsedMs === undefined ? '' : ` ${paint.mute('in')} ${String(elapsedMs)}ms`;\n this.options.logger.debug(`${verb} ${paint.sky.bold(names.join(', '))} from ${paint.mute(from)}${timing}`);\n }\n\n private async reloadWithRollback(file: string, rollbackEnabled: boolean): Promise<string[]> {\n const { logger } = this.options;\n const snapshot = this.snapshotUnits(file);\n this.unload(file);\n\n const reloaded = await this.reloadFile(file);\n if (reloaded !== null) return reloaded;\n\n // a failed reload can leave a partial registration\n this.unload(file);\n if (!rollbackEnabled) return [];\n if (snapshot.handlers.length === 0 && snapshot.middlewares.length === 0) return [];\n\n // a typo keeps the last-good units live until the next good save\n this.restoreUnits(file, snapshot);\n logger.warn(`${paint.amber.bold('Rolled back')} ${paint.mute(formatFilePath(file))} to the last-good version`);\n return [];\n }\n\n /** Links the handler to its source file so a later update can unload it. */\n public trackHandler(file: string, handler: THandler): void {\n let set = this.store.fileToHandlers.get(file);\n if (!set) {\n set = new Set();\n this.store.fileToHandlers.set(file, set);\n }\n set.add(handler);\n\n if (!this.options.getArtifacts) return;\n this.store.handlerArtifacts.set(handler, this.options.getArtifacts(handler));\n }\n\n /** Links the middleware to its source file so a later update can unload it. */\n public trackMiddleware(file: string, middleware: TMiddleware): void {\n let set = this.store.fileToMiddlewares.get(file);\n if (!set) {\n set = new Set();\n this.store.fileToMiddlewares.set(file, set);\n }\n set.add(middleware);\n }\n\n private unload(file: string): string[] {\n const names: string[] = [];\n const handlers = this.store.fileToHandlers.get(file);\n if (handlers) {\n for (const handler of handlers) {\n const artifacts = this.store.handlerArtifacts.get(handler);\n this.options.unregisterHandler(handler, artifacts);\n this.store.handlerArtifacts.delete(handler);\n names.push(displayName(handler, 'Handler'));\n }\n this.store.fileToHandlers.delete(file);\n }\n\n const middlewares = this.store.fileToMiddlewares.get(file);\n if (middlewares) {\n for (const middleware of middlewares) {\n this.options.unregisterMiddleware?.(middleware);\n names.push(displayName(middleware, 'Middleware'));\n }\n this.store.fileToMiddlewares.delete(file);\n }\n\n return names;\n }\n\n private async reloadFile(file: string): Promise<string[] | null> {\n const { logger, isHandler, isMiddleware, registerHandler, registerMiddleware } = this.options;\n\n try {\n let fileUrl = pathToFileURL(file).href;\n // vitest has no real vite server managing the module graph\n if (process.env.VITEST === 'true') fileUrl += `?update=${Date.now()}`;\n\n // justified: a dynamic import resolves to an untyped export map\n const imported = (await import(/* @vite-ignore */ fileUrl)) as Record<string, unknown>;\n\n const names: string[] = [];\n for (const val of Object.values(imported)) {\n if (isHandler(val)) {\n registerHandler(val, file);\n this.trackHandler(file, val);\n names.push(displayName(val, 'Handler'));\n } else if (isMiddleware && registerMiddleware && isMiddleware(val)) {\n registerMiddleware(val, file);\n this.trackMiddleware(file, val);\n names.push(displayName(val, 'Middleware'));\n }\n }\n\n return names;\n } catch (error) {\n logger.error(`Failed to reload file: ${formatFilePath(file)}`, vitePluginThrew(error) ?? error);\n return null;\n }\n }\n\n // these are the same class objects, which is how a live db connection survives a rollback\n private snapshotUnits(file: string): { handlers: THandler[]; middlewares: TMiddleware[] } {\n return {\n handlers: [...(this.store.fileToHandlers.get(file) ?? [])],\n middlewares: [...(this.store.fileToMiddlewares.get(file) ?? [])]\n };\n }\n\n private restoreUnits(file: string, snapshot: { handlers: THandler[]; middlewares: TMiddleware[] }): void {\n for (const handler of snapshot.handlers) {\n this.options.registerHandler(handler, file);\n this.trackHandler(file, handler);\n }\n\n if (this.options.registerMiddleware) {\n for (const middleware of snapshot.middlewares) {\n this.options.registerMiddleware(middleware, file);\n this.trackMiddleware(file, middleware);\n }\n }\n }\n}\n"],"mappings":"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"}
|