@seedcord/core 0.1.0-next.6 → 0.1.0-next.7

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.
Files changed (53) hide show
  1. package/dist/{AutocompleteOptions-eKhIe6Qt.d.mts → AutocompleteOptions-DLp_B8as.d.mts} +61 -112
  2. package/dist/HmrModuleHandler-3sNgKHdf.mjs +163 -0
  3. package/dist/HmrModuleHandler-3sNgKHdf.mjs.map +1 -0
  4. package/dist/HmrModuleHandler-CTBng7wV.cjs +170 -0
  5. package/dist/HmrModuleHandler-CTBng7wV.cjs.map +1 -0
  6. package/dist/Plugin-3rlH0uxH.d.mts +620 -0
  7. package/dist/{Plugin-C2-VHrsX.cjs → Plugin-Cgp1a0uQ.cjs} +13 -4
  8. package/dist/Plugin-Cgp1a0uQ.cjs.map +1 -0
  9. package/dist/{Plugin-PcdPjAzc.mjs → Plugin-j9GYdroS.mjs} +13 -4
  10. package/dist/Plugin-j9GYdroS.mjs.map +1 -0
  11. package/dist/{permissions-DSW_EZfN.cjs → Silence-DiP95iwD.cjs} +38 -307
  12. package/dist/Silence-DiP95iwD.cjs.map +1 -0
  13. package/dist/{permissions-RVCBhL2n.mjs → Silence-dbC2slOB.mjs} +35 -238
  14. package/dist/Silence-dbC2slOB.mjs.map +1 -0
  15. package/dist/basic-D7bDlgSP.d.mts +10 -0
  16. package/dist/hmr.index.cjs +2 -164
  17. package/dist/hmr.index.mjs +2 -162
  18. package/dist/index-Dueqztyy.d.mts +142 -0
  19. package/dist/index.cjs +50 -72
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.d.mts +7 -5
  22. package/dist/index.mjs +8 -35
  23. package/dist/index.mjs.map +1 -1
  24. package/dist/internal.index.cjs +262 -74
  25. package/dist/internal.index.cjs.map +1 -1
  26. package/dist/internal.index.d.mts +68 -4
  27. package/dist/internal.index.mjs +216 -35
  28. package/dist/internal.index.mjs.map +1 -1
  29. package/dist/node-internal.index.cjs +123 -52
  30. package/dist/node-internal.index.cjs.map +1 -1
  31. package/dist/node-internal.index.d.mts +67 -2158
  32. package/dist/node-internal.index.mjs +124 -54
  33. package/dist/node-internal.index.mjs.map +1 -1
  34. package/dist/plugin.index.cjs +1 -1
  35. package/dist/plugin.index.d.mts +1 -1
  36. package/dist/plugin.index.mjs +1 -1
  37. package/dist/publishDefault-CCf1azzW.mjs +264 -0
  38. package/dist/publishDefault-CCf1azzW.mjs.map +1 -0
  39. package/dist/publishDefault-l0NQ-SHo.cjs +353 -0
  40. package/dist/publishDefault-l0NQ-SHo.cjs.map +1 -0
  41. package/dist/subscribers-B1G6_LlK.cjs +517 -0
  42. package/dist/subscribers-B1G6_LlK.cjs.map +1 -0
  43. package/dist/subscribers-DYqFVP1X.mjs +480 -0
  44. package/dist/subscribers-DYqFVP1X.mjs.map +1 -0
  45. package/package.json +7 -6
  46. package/dist/Plugin-C2-VHrsX.cjs.map +0 -1
  47. package/dist/Plugin-CNyRKDHT.d.mts +0 -226
  48. package/dist/Plugin-PcdPjAzc.mjs.map +0 -1
  49. package/dist/hmr.index.cjs.map +0 -1
  50. package/dist/hmr.index.mjs.map +0 -1
  51. package/dist/is-never-Ddaba1Su.d.mts +0 -58
  52. package/dist/permissions-DSW_EZfN.cjs.map +0 -1
  53. package/dist/permissions-RVCBhL2n.mjs.map +0 -1
@@ -1,8 +1,8 @@
1
- import { t as IsNever } from "./is-never-Ddaba1Su.mjs";
2
- import { f as CoreBase } from "./Plugin-CNyRKDHT.mjs";
1
+ import { t as Constructor } from "./basic-D7bDlgSP.mjs";
2
+ import { A as Notice, N as AckState, W as InteractionRoutes, j as ReplyTelemetry, y as CoreBase } from "./Plugin-3rlH0uxH.mjs";
3
3
  import { APIMessageTopLevelComponent, APIModalInteractionResponseCallbackData, ApplicationCommandType, ChannelType, Snowflake } from "discord-api-types/v10";
4
4
  import { ActionRowBuilder, ButtonBuilder, ChannelSelectMenuBuilder, CheckboxBuilder, CheckboxGroupBuilder, CheckboxGroupOptionBuilder, ContainerBuilder, ContextMenuCommandBuilder, EmbedBuilder, FileBuilder, FileUploadBuilder, LabelBuilder, MediaGalleryBuilder, MentionableSelectMenuBuilder, ModalBuilder, RadioGroupBuilder, RadioGroupOptionBuilder, RoleSelectMenuBuilder, SectionBuilder, SeparatorBuilder, SlashCommandBuilder, SlashCommandSubcommandBuilder, SlashCommandSubcommandGroupBuilder, StringSelectMenuBuilder, StringSelectMenuOptionBuilder, TextDisplayBuilder, TextInputBuilder, UserSelectMenuBuilder } from "@discordjs/builders";
5
- import { DeferOpts, RenderContext, ReplyResponse, SendOpts } from "@seedcord/types";
5
+ import { DeferOpts, ReplyResponse, SendOpts } from "@seedcord/types";
6
6
 
7
7
  //#region src/registries/SlashOptionRegistry.d.ts
8
8
  /** A slash option kind as a plain string union (`string`, `channel`, ...), keyed to the registry. */
@@ -152,13 +152,61 @@ declare abstract class RowComponent<RowKey extends RowType> extends BaseComponen
152
152
  get component(): InstantiatedActionRow<RowKey>;
153
153
  }
154
154
  //#endregion
155
- //#region ../../node_modules/.pnpm/type-fest@5.7.0/node_modules/type-fest/source/basic.d.ts
155
+ //#region ../../node_modules/.pnpm/type-fest@5.7.0/node_modules/type-fest/source/is-never.d.ts
156
156
  /**
157
- Matches a [`class` constructor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes).
157
+ Returns a boolean for whether the given type is `never`.
158
158
 
159
- @category Class
159
+ @link https://github.com/microsoft/TypeScript/issues/31751#issuecomment-498526919
160
+ @link https://stackoverflow.com/a/53984913/10292952
161
+ @link https://www.zhenghao.io/posts/ts-never
162
+
163
+ Useful in type utilities, such as checking if something does not occur.
164
+
165
+ @example
166
+ ```
167
+ import type {IsNever, And} from 'type-fest';
168
+
169
+ type A = IsNever<never>;
170
+ //=> true
171
+
172
+ type B = IsNever<any>;
173
+ //=> false
174
+
175
+ type C = IsNever<unknown>;
176
+ //=> false
177
+
178
+ type D = IsNever<never[]>;
179
+ //=> false
180
+
181
+ type E = IsNever<object>;
182
+ //=> false
183
+
184
+ type F = IsNever<string>;
185
+ //=> false
186
+ ```
187
+
188
+ @example
189
+ ```
190
+ import type {IsNever} from 'type-fest';
191
+
192
+ type IsTrue<T> = T extends true ? true : false;
193
+
194
+ // When a distributive conditional is instantiated with `never`, the entire conditional results in `never`.
195
+ type A = IsTrue<never>;
196
+ //=> never
197
+
198
+ // If you don't want that behaviour, you can explicitly add an `IsNever` check before the distributive conditional.
199
+ type IsTrueFixed<T> =
200
+ IsNever<T> extends true ? false : T extends true ? true : false;
201
+
202
+ type B = IsTrueFixed<never>;
203
+ //=> false
204
+ ```
205
+
206
+ @category Type Guard
207
+ @category Utilities
160
208
  */
161
- type Constructor<T, Arguments extends unknown[] = any[]> = new (...arguments_: Arguments) => T;
209
+ type IsNever<T> = [T] extends [never] ? true : false;
162
210
  //#endregion
163
211
  //#region ../../node_modules/.pnpm/type-fest@5.7.0/node_modules/type-fest/source/non-empty-tuple.d.ts
164
212
  /**
@@ -220,29 +268,6 @@ declare function RegisterCommand(scope: 'global'): (ctor: CommandCtor) => void;
220
268
  */
221
269
  declare function RegisterCommand(scope: 'guild', guilds: string[]): (ctor: CommandCtor) => void;
222
270
  //#endregion
223
- //#region src/metadataKeys.d.ts
224
- declare enum InteractionRoutes {
225
- Slash = "slash",
226
- Button = "button",
227
- Modal = "modal",
228
- StringMenu = "stringMenu",
229
- UserMenu = "userMenu",
230
- RoleMenu = "roleMenu",
231
- ChannelMenu = "channelMenu",
232
- MentionableMenu = "mentionableMenu",
233
- MessageContextMenu = "messageContextMenu",
234
- UserContextMenu = "userContextMenu",
235
- Autocomplete = "autocomplete"
236
- }
237
- declare const InteractionMetadataKey: unique symbol;
238
- declare const InteractionRouteKeys: Record<InteractionRoutes, symbol>;
239
- declare const MiddlewareMetadataKey: unique symbol;
240
- declare const EventMetadataKey: unique symbol;
241
- declare const CommandMetadataKey: unique symbol;
242
- declare const GatedMetadataKey: unique symbol;
243
- declare const SubscribeMetadataKey: unique symbol;
244
- declare const WebhookUrlMetadataKey: unique symbol;
245
- //#endregion
246
271
  //#region src/customId/Field.d.ts
247
272
  /**
248
273
  * One field in a customId shape, its wire kind plus the type it decodes to.
@@ -433,19 +458,6 @@ declare function storeComponentRoute(route: InteractionRoutes, defs: readonly An
433
458
  declare function selectMenuRouteOf(kind: SelectMenuKind): InteractionRoutes;
434
459
  declare function contextMenuRouteOf(kind: ApplicationCommandType.User | ApplicationCommandType.Message): InteractionRoutes;
435
460
  //#endregion
436
- //#region src/reply/AckTrace.d.ts
437
- /** The first ack's stack, passed as the throw cause so a double-ack points to both lines. */
438
- declare class AckTrace extends Error {
439
- constructor(method: ReplyMethod);
440
- }
441
- //#endregion
442
- //#region src/reply/ackLegality.d.ts
443
- type AckState = 'unacked' | 'deferred-reply' | 'deferred-update' | 'replied';
444
- type ReplyMethod = 'reply' | 'defer' | 'deferUpdate' | 'update' | 'followUp' | 'edit' | 'delete' | 'send' | 'showModal';
445
- /** Throws before any API call. `ackedBy` becomes the throw cause, its stack points at the first ack. */
446
- declare function checkAckLegality(method: ReplyMethod, state: AckState, routeId: string, ackedBy?: AckTrace): void;
447
- declare function sendTarget(state: AckState): 'reply' | 'edit' | 'followUp';
448
- //#endregion
449
461
  //#region src/reply/serializeReply.d.ts
450
462
  /** Serialized message data the transport wire writers consume. */
451
463
  interface SerializedReply {
@@ -471,10 +483,13 @@ declare abstract class BaseReplySender<TMessage extends {
471
483
  id: string;
472
484
  }> {
473
485
  protected readonly routeId: string;
486
+ private readonly telemetry?;
474
487
  private state;
475
488
  private readonly sent;
476
489
  private ackedBy?;
477
- protected constructor(routeId: string, initialState?: AckState);
490
+ protected constructor(routeId: string, initialState?: AckState, telemetry?: ReplyTelemetry | undefined);
491
+ private report;
492
+ private attempt;
478
493
  reply(response: ReplyResponse | string, opts?: SendOpts): Promise<TMessage>;
479
494
  defer(opts?: DeferOpts): Promise<void>;
480
495
  deferUpdate(): Promise<void>;
@@ -722,72 +737,6 @@ declare function defineEffectGate<const Name extends string, Ctx extends GateCon
722
737
  */
723
738
  type RequiredOf<TGate> = TGate extends Gate<infer Ctx> ? Ctx : never;
724
739
  //#endregion
725
- //#region src/stops/Notice.d.ts
726
- /**
727
- * Base class for a user-facing refusal or a reported fault.
728
- *
729
- * Throw a `Notice` to stop a handler and reply to the user. The framework catches it at the controller
730
- * boundary and renders {@link Notice.render}, which always decides what the user sees. With `report`
731
- * false that render is all that happens. With `report` true the framework also logs the fault and
732
- * publishes it to the `handledException` bus. A raw, non-Notice throw shows the generic message.
733
- *
734
- * @example
735
- * ```ts
736
- * import { Notice, BuilderComponent, type RenderContext, type ReplyResponse } from '@seedcord/gateway';
737
- * import { TextDisplayBuilder } from '@discordjs/builders';
738
- *
739
- * // reading `.component` applies the configured bot color to the container accent
740
- * class TooPoorCard extends BuilderComponent<'container'> {
741
- * constructor(balance: number) {
742
- * super('container');
743
- * this.instance.addTextDisplayComponents(
744
- * new TextDisplayBuilder().setContent(`### Insufficient balance\nYou need more than ${balance} coins.`)
745
- * );
746
- * }
747
- * }
748
- *
749
- * class TooPoor extends Notice {
750
- * constructor(private readonly balance: number) {
751
- * super(`balance ${balance} is below the cost`);
752
- * }
753
- *
754
- * render(_ctx: RenderContext): ReplyResponse {
755
- * return { components: [new TooPoorCard(this.balance).component] };
756
- * }
757
- * }
758
- *
759
- * // in a handler, throwing stops the handler and replies with render(ctx)
760
- * if (wallet.balance < cost) throw new TooPoor(wallet.balance);
761
- * ```
762
- */
763
- declare abstract class Notice extends Error {
764
- /**
765
- * Whether this denial is a reported fault. True also logs it and publishes it to the `handledException`
766
- * bus. The user always sees {@link Notice.render} either way.
767
- *
768
- * @defaultValue `false`
769
- */
770
- report: boolean;
771
- /**
772
- * Whether the reply is ephemeral, so only the invoking user sees it. Set it false for a refusal the
773
- * whole channel should see.
774
- *
775
- * @defaultValue `true`
776
- */
777
- ephemeral: boolean;
778
- /**
779
- * A short one-line reason. When every arm of an `or` gate refuses and each refusal sets this, `or`
780
- * lists them instead of showing a neutral message.
781
- */
782
- summary?: string;
783
- protected constructor(message: string, options?: ErrorOptions);
784
- /**
785
- * Builds what the user sees. Called fresh each time the denial is shown, so the builders are new
786
- * and the bot color resolves at render time rather than at construction time.
787
- */
788
- abstract render(ctx: RenderContext): ReplyResponse;
789
- }
790
- //#endregion
791
740
  //#region src/gates/matching.d.ts
792
741
  type IntersectRequired<Gates extends readonly Gate<GateContextBase>[]> = Gates extends readonly [infer First extends Gate<GateContextBase>, ...infer Rest extends readonly Gate<GateContextBase>[]] ? RequiredOf<First> & IntersectRequired<Rest> : unknown;
793
742
  type TwoOrMore<Item> = readonly [Item, Item, ...Item[]];
@@ -989,5 +938,5 @@ type Getter<Route extends keyof SlashOptionRegistry, Kind extends OptionKind, Me
989
938
  */
990
939
  type AutocompleteOptions<Route extends keyof SlashOptionRegistry> = Getter<Route, 'string', 'getString'> & Getter<Route, 'integer', 'getInteger'> & Getter<Route, 'number', 'getNumber'> & Getter<Route, 'boolean', 'getBoolean'>;
991
940
  //#endregion
992
- export { InteractionMetadataKey as $, serializeReply as A, interactionRoutesOf as B, GuildPermissionsContext as C, BaseReplySender as D, defineGate as E, AckTrace as F, CustomId as G, storeComponentRoute as H, RoutableConstructor as I, CustomIdField as J, decodeFor as K, SelectMenuKind as L, ReplyMethod as M, checkAckLegality as N, ModalLike as O, sendTarget as P, GatedMetadataKey as Q, areRoutes as R, GateContextBase as S, defineEffectGate as T, storeInteractionRoute as U, selectMenuRouteOf as V, AnyCustomId as W, CommandMetadataKey as X, DecodedParams as Y, EventMetadataKey as Z, JoinNames as _, FocusedField as a, CommandMeta as at, EffectGate as b, RequireBotPermissions as c, BuilderComponent as ct, RequireRole as d, RowType as dt, InteractionRouteKeys as et, RequireRoleOptions as f, OptionKind as ft, IntersectRequired as g, GateFitsWith as h, EntryFor as i, WebhookUrlMetadataKey as it, AckState as j, SerializedReply as k, RequirePermissions as l, RowComponent as lt, pickNotice as m, SlashOptionRegistry as mt, AutocompleteOptions as n, MiddlewareMetadataKey as nt, PermissionNames as o, RegisterCommand as ot, GateNoticeOptions as p, SlashOption as pt, prefixOf as q, ChoiceValueOf as r, SubscribeMetadataKey as rt, PermissionScope as s, Constructor as st, AutocompletableNames as t, InteractionRoutes as tt, RequirePermissionsOptions as u, BuilderType as ut, TwoOrMore as v, RequiredOf as w, Gate as x, Notice as y, contextMenuRouteOf as z };
993
- //# sourceMappingURL=AutocompleteOptions-eKhIe6Qt.d.mts.map
941
+ export { SlashOptionRegistry as $, RoutableConstructor as A, decodeFor as B, RequiredOf as C, ModalLike as D, BaseReplySender as E, selectMenuRouteOf as F, RegisterCommand as G, CustomIdField as H, storeComponentRoute as I, RowComponent as J, IsNever as K, storeInteractionRoute as L, areRoutes as M, contextMenuRouteOf as N, SerializedReply as O, interactionRoutesOf as P, SlashOption as Q, AnyCustomId as R, GuildPermissionsContext as S, defineGate as T, DecodedParams as U, prefixOf as V, CommandMeta as W, RowType as X, BuilderType as Y, OptionKind as Z, JoinNames as _, FocusedField as a, Gate as b, RequireBotPermissions as c, RequireRole as d, RequireRoleOptions as f, IntersectRequired as g, GateFitsWith as h, EntryFor as i, SelectMenuKind as j, serializeReply as k, RequirePermissions as l, pickNotice as m, AutocompleteOptions as n, PermissionNames as o, GateNoticeOptions as p, BuilderComponent as q, ChoiceValueOf as r, PermissionScope as s, AutocompletableNames as t, RequirePermissionsOptions as u, TwoOrMore as v, defineEffectGate as w, GateContextBase as x, EffectGate as y, CustomId as z };
942
+ //# sourceMappingURL=AutocompleteOptions-DLp_B8as.d.mts.map
@@ -0,0 +1,163 @@
1
+ import { formatFilePath } from "@seedcord/utils";
2
+ import chalk from "chalk";
3
+ import { existsSync } from "node:fs";
4
+ import { resolve, sep } from "node:path";
5
+ import { pathToFileURL } from "node:url";
6
+
7
+ //#region src/hmr/HmrModuleHandler.ts
8
+ const displayName = (unit, fallback) => unit.name ?? fallback;
9
+ /**
10
+ * Reloads THandler and optional TMiddleware modules on HMR updates.
11
+ *
12
+ * Call `handle()` from your `HmrAware.onHmr()` for standard reload behavior, or write a fully custom `onHmr()` and skip this class.
13
+ *
14
+ * This is only useful during development.
15
+ */
16
+ var HmrModuleHandler = class {
17
+ store;
18
+ options;
19
+ constructor(options) {
20
+ this.options = {
21
+ ...options,
22
+ logger: options.logger.inChannel("hmr")
23
+ };
24
+ this.store = {
25
+ fileToHandlers: /* @__PURE__ */ new Map(),
26
+ fileToMiddlewares: /* @__PURE__ */ new Map(),
27
+ handlerArtifacts: /* @__PURE__ */ new Map()
28
+ };
29
+ }
30
+ /** Unloads or reloads the modules affected by an {@link HmrUpdateEvent}, scoped to the handler and middleware directories. */
31
+ async handle(event) {
32
+ const { file, affectedModules, type } = event;
33
+ const { logger, handlersDir, middlewaresDir } = this.options;
34
+ if (type === "delete" || type === "deleteDir") {
35
+ this.unload(file);
36
+ return;
37
+ }
38
+ if (type === "createDir") return;
39
+ if (type === "update" && !existsSync(file)) {
40
+ if (this.isTracked(file)) {
41
+ logger.info(`Received update for non-existent file: ${formatFilePath(file)}, treating as delete`);
42
+ this.unload(file);
43
+ }
44
+ return;
45
+ }
46
+ const rollbackEnabled = event.rollback ?? true;
47
+ const filesToReload = affectedModules && affectedModules.length > 0 ? affectedModules : [file];
48
+ const absHandlersDir = resolve(process.cwd(), handlersDir);
49
+ const absMiddlewaresDir = middlewaresDir ? resolve(process.cwd(), middlewaresDir) : null;
50
+ for (const fileToReload of filesToReload) {
51
+ const isHandler = fileToReload.startsWith(absHandlersDir + sep);
52
+ const isMiddleware = absMiddlewaresDir ? fileToReload.startsWith(absMiddlewaresDir + sep) : false;
53
+ if (!isHandler && !isMiddleware) continue;
54
+ await this.reloadWithRollback(fileToReload, rollbackEnabled);
55
+ }
56
+ }
57
+ async reloadWithRollback(file, rollbackEnabled) {
58
+ const { logger } = this.options;
59
+ const snapshot = this.snapshotUnits(file);
60
+ this.unload(file);
61
+ if (await this.reloadFile(file)) return;
62
+ this.unload(file);
63
+ if (!rollbackEnabled) return;
64
+ if (snapshot.handlers.length === 0 && snapshot.middlewares.length === 0) return;
65
+ this.restoreUnits(file, snapshot);
66
+ logger.warn(`${chalk.yellow.bold("Rolled back")} ${chalk.gray(formatFilePath(file))} to the last-good version`);
67
+ }
68
+ /** Links the handler to its source file so a later update can unload it. */
69
+ trackHandler(file, handler) {
70
+ let set = this.store.fileToHandlers.get(file);
71
+ if (!set) {
72
+ set = /* @__PURE__ */ new Set();
73
+ this.store.fileToHandlers.set(file, set);
74
+ }
75
+ set.add(handler);
76
+ if (this.options.getArtifacts) {
77
+ const artifacts = this.options.getArtifacts(handler);
78
+ this.store.handlerArtifacts.set(handler, artifacts);
79
+ }
80
+ }
81
+ /** Links the middleware to its source file so a later update can unload it. */
82
+ trackMiddleware(file, middleware) {
83
+ let set = this.store.fileToMiddlewares.get(file);
84
+ if (!set) {
85
+ set = /* @__PURE__ */ new Set();
86
+ this.store.fileToMiddlewares.set(file, set);
87
+ }
88
+ set.add(middleware);
89
+ }
90
+ isTracked(file) {
91
+ return this.store.fileToHandlers.has(file) || this.store.fileToMiddlewares.has(file);
92
+ }
93
+ unload(file) {
94
+ const { logger } = this.options;
95
+ const handlers = this.store.fileToHandlers.get(file);
96
+ if (handlers) {
97
+ for (const handler of handlers) {
98
+ const artifacts = this.store.handlerArtifacts.get(handler);
99
+ this.options.unregisterHandler(handler, artifacts);
100
+ this.store.handlerArtifacts.delete(handler);
101
+ const name = displayName(handler, "Handler");
102
+ logger.info(`${chalk.red.bold("Unloaded")} ${chalk.cyan.bold(name)} from ${chalk.gray(formatFilePath(file))}`);
103
+ }
104
+ this.store.fileToHandlers.delete(file);
105
+ }
106
+ const middlewares = this.store.fileToMiddlewares.get(file);
107
+ if (middlewares) {
108
+ for (const middleware of middlewares) {
109
+ this.options.unregisterMiddleware?.(middleware);
110
+ const name = displayName(middleware, "Middleware");
111
+ logger.info(`${chalk.red.bold("Unloaded")} ${chalk.cyan.bold(name)} from ${chalk.gray(formatFilePath(file))}`);
112
+ }
113
+ this.store.fileToMiddlewares.delete(file);
114
+ }
115
+ }
116
+ async reloadFile(file) {
117
+ const { logger, isHandler, isMiddleware, registerHandler, registerMiddleware } = this.options;
118
+ if (!existsSync(file)) {
119
+ logger.info(`File does not exist, skipping reload: ${formatFilePath(file)}`);
120
+ return false;
121
+ }
122
+ try {
123
+ let fileUrl = pathToFileURL(file).href;
124
+ if (process.env.VITEST === "true") fileUrl += `?update=${Date.now()}`;
125
+ const imported = await import(fileUrl);
126
+ for (const val of Object.values(imported)) if (isHandler(val)) {
127
+ registerHandler(val, file);
128
+ this.trackHandler(file, val);
129
+ const name = displayName(val, "Handler");
130
+ logger.info(`${chalk.blue.bold("Reloaded")} ${chalk.cyan.bold(name)} from ${chalk.gray(formatFilePath(file))}`);
131
+ } else if (isMiddleware && registerMiddleware && isMiddleware(val)) {
132
+ registerMiddleware(val, file);
133
+ this.trackMiddleware(file, val);
134
+ const name = displayName(val, "Middleware");
135
+ logger.info(`${chalk.blue.bold("Reloaded")} ${chalk.cyan.bold(name)} from ${chalk.gray(formatFilePath(file))}`);
136
+ }
137
+ return true;
138
+ } catch (error) {
139
+ logger.error(`Failed to reload file: ${file}`, error);
140
+ return false;
141
+ }
142
+ }
143
+ snapshotUnits(file) {
144
+ return {
145
+ handlers: [...this.store.fileToHandlers.get(file) ?? []],
146
+ middlewares: [...this.store.fileToMiddlewares.get(file) ?? []]
147
+ };
148
+ }
149
+ restoreUnits(file, snapshot) {
150
+ for (const handler of snapshot.handlers) {
151
+ this.options.registerHandler(handler, file);
152
+ this.trackHandler(file, handler);
153
+ }
154
+ if (this.options.registerMiddleware) for (const middleware of snapshot.middlewares) {
155
+ this.options.registerMiddleware(middleware, file);
156
+ this.trackMiddleware(file, middleware);
157
+ }
158
+ }
159
+ };
160
+
161
+ //#endregion
162
+ export { HmrModuleHandler as t };
163
+ //# sourceMappingURL=HmrModuleHandler-3sNgKHdf.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HmrModuleHandler-3sNgKHdf.mjs","names":[],"sources":["../src/hmr/HmrModuleHandler.ts"],"sourcesContent":["import { existsSync } from 'node:fs';\nimport { resolve, sep } from 'node:path';\nimport { pathToFileURL } from 'node:url';\n\nimport { formatFilePath } from '@seedcord/utils';\nimport chalk from 'chalk';\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 // the caller's options object stays unmutated\n this.options = { ...options, logger: options.logger.inChannel('hmr') };\n\n // no import.meta.hot.data stash needed, a leaf reload never reconstructs the singleton that holds 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 const { logger, handlersDir, middlewaresDir } = this.options;\n\n if (type === 'delete' || type === 'deleteDir') {\n this.unload(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 if (type === 'update' && !existsSync(file)) {\n if (this.isTracked(file)) {\n logger.info(`Received update for non-existent file: ${formatFilePath(file)}, treating as delete`);\n this.unload(file);\n }\n return;\n }\n\n const rollbackEnabled = event.rollback ?? true;\n const filesToReload = affectedModules && affectedModules.length > 0 ? affectedModules : [file];\n const absHandlersDir = resolve(process.cwd(), handlersDir);\n const absMiddlewaresDir = middlewaresDir ? resolve(process.cwd(), middlewaresDir) : null;\n\n for (const fileToReload of filesToReload) {\n // the separator keeps a sibling dir sharing the prefix (commands-extra vs commands) out\n const isHandler = fileToReload.startsWith(absHandlersDir + sep);\n const isMiddleware = absMiddlewaresDir ? fileToReload.startsWith(absMiddlewaresDir + sep) : false;\n\n if (!isHandler && !isMiddleware) {\n continue;\n }\n\n await this.reloadWithRollback(fileToReload, rollbackEnabled);\n }\n }\n\n private async reloadWithRollback(file: string, rollbackEnabled: boolean): Promise<void> {\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) return;\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 them live until the next good save\n this.restoreUnits(file, snapshot);\n logger.warn(`${chalk.yellow.bold('Rolled back')} ${chalk.gray(formatFilePath(file))} to the last-good version`);\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) {\n const artifacts = this.options.getArtifacts(handler);\n this.store.handlerArtifacts.set(handler, artifacts);\n }\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 isTracked(file: string): boolean {\n return this.store.fileToHandlers.has(file) || this.store.fileToMiddlewares.has(file);\n }\n\n private unload(file: string): void {\n const { logger } = this.options;\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 const name = displayName(handler, 'Handler');\n logger.info(\n `${chalk.red.bold('Unloaded')} ${chalk.cyan.bold(name)} from ${chalk.gray(formatFilePath(file))}`\n );\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 const name = displayName(middleware, 'Middleware');\n logger.info(\n `${chalk.red.bold('Unloaded')} ${chalk.cyan.bold(name)} from ${chalk.gray(formatFilePath(file))}`\n );\n }\n this.store.fileToMiddlewares.delete(file);\n }\n }\n\n private async reloadFile(file: string): Promise<boolean> {\n const { logger, isHandler, isMiddleware, registerHandler, registerMiddleware } = this.options;\n\n if (!existsSync(file)) {\n logger.info(`File does not exist, skipping reload: ${formatFilePath(file)}`);\n return false;\n }\n\n try {\n let fileUrl = pathToFileURL(file).href;\n // vitest has no real vite server managing the module graph, so bust the import cache manually.\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(fileUrl)) as Record<string, unknown>;\n\n for (const val of Object.values(imported)) {\n if (isHandler(val)) {\n registerHandler(val, file);\n this.trackHandler(file, val);\n const name = displayName(val, 'Handler');\n logger.info(\n `${chalk.blue.bold('Reloaded')} ${chalk.cyan.bold(name)} from ${chalk.gray(formatFilePath(file))}`\n );\n } else if (isMiddleware && registerMiddleware && isMiddleware(val)) {\n registerMiddleware(val, file);\n this.trackMiddleware(file, val);\n const name = displayName(val, 'Middleware');\n logger.info(\n `${chalk.blue.bold('Reloaded')} ${chalk.cyan.bold(name)} from ${chalk.gray(formatFilePath(file))}`\n );\n }\n }\n\n return true;\n } catch (error) {\n logger.error(`Failed to reload file: ${file}`, error);\n return false;\n }\n }\n\n // restoring re-registers the same class objects, so a live db connection survives 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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"}
@@ -0,0 +1,170 @@
1
+ const require_rolldown_runtime = require('./rolldown-runtime-C_NdSu1c.cjs');
2
+ let _seedcord_utils = require("@seedcord/utils");
3
+ let chalk = require("chalk");
4
+ chalk = require_rolldown_runtime.__toESM(chalk, 1);
5
+ let node_fs = require("node:fs");
6
+ let node_path = require("node:path");
7
+ let node_url = require("node:url");
8
+
9
+ //#region src/hmr/HmrModuleHandler.ts
10
+ const displayName = (unit, fallback) => unit.name ?? fallback;
11
+ /**
12
+ * Reloads THandler and optional TMiddleware modules on HMR updates.
13
+ *
14
+ * Call `handle()` from your `HmrAware.onHmr()` for standard reload behavior, or write a fully custom `onHmr()` and skip this class.
15
+ *
16
+ * This is only useful during development.
17
+ */
18
+ var HmrModuleHandler = class {
19
+ store;
20
+ options;
21
+ constructor(options) {
22
+ this.options = {
23
+ ...options,
24
+ logger: options.logger.inChannel("hmr")
25
+ };
26
+ this.store = {
27
+ fileToHandlers: /* @__PURE__ */ new Map(),
28
+ fileToMiddlewares: /* @__PURE__ */ new Map(),
29
+ handlerArtifacts: /* @__PURE__ */ new Map()
30
+ };
31
+ }
32
+ /** Unloads or reloads the modules affected by an {@link HmrUpdateEvent}, scoped to the handler and middleware directories. */
33
+ async handle(event) {
34
+ const { file, affectedModules, type } = event;
35
+ const { logger, handlersDir, middlewaresDir } = this.options;
36
+ if (type === "delete" || type === "deleteDir") {
37
+ this.unload(file);
38
+ return;
39
+ }
40
+ if (type === "createDir") return;
41
+ if (type === "update" && !(0, node_fs.existsSync)(file)) {
42
+ if (this.isTracked(file)) {
43
+ logger.info(`Received update for non-existent file: ${(0, _seedcord_utils.formatFilePath)(file)}, treating as delete`);
44
+ this.unload(file);
45
+ }
46
+ return;
47
+ }
48
+ const rollbackEnabled = event.rollback ?? true;
49
+ const filesToReload = affectedModules && affectedModules.length > 0 ? affectedModules : [file];
50
+ const absHandlersDir = (0, node_path.resolve)(process.cwd(), handlersDir);
51
+ const absMiddlewaresDir = middlewaresDir ? (0, node_path.resolve)(process.cwd(), middlewaresDir) : null;
52
+ for (const fileToReload of filesToReload) {
53
+ const isHandler = fileToReload.startsWith(absHandlersDir + node_path.sep);
54
+ const isMiddleware = absMiddlewaresDir ? fileToReload.startsWith(absMiddlewaresDir + node_path.sep) : false;
55
+ if (!isHandler && !isMiddleware) continue;
56
+ await this.reloadWithRollback(fileToReload, rollbackEnabled);
57
+ }
58
+ }
59
+ async reloadWithRollback(file, rollbackEnabled) {
60
+ const { logger } = this.options;
61
+ const snapshot = this.snapshotUnits(file);
62
+ this.unload(file);
63
+ if (await this.reloadFile(file)) return;
64
+ this.unload(file);
65
+ if (!rollbackEnabled) return;
66
+ if (snapshot.handlers.length === 0 && snapshot.middlewares.length === 0) return;
67
+ this.restoreUnits(file, snapshot);
68
+ logger.warn(`${chalk.default.yellow.bold("Rolled back")} ${chalk.default.gray((0, _seedcord_utils.formatFilePath)(file))} to the last-good version`);
69
+ }
70
+ /** Links the handler to its source file so a later update can unload it. */
71
+ trackHandler(file, handler) {
72
+ let set = this.store.fileToHandlers.get(file);
73
+ if (!set) {
74
+ set = /* @__PURE__ */ new Set();
75
+ this.store.fileToHandlers.set(file, set);
76
+ }
77
+ set.add(handler);
78
+ if (this.options.getArtifacts) {
79
+ const artifacts = this.options.getArtifacts(handler);
80
+ this.store.handlerArtifacts.set(handler, artifacts);
81
+ }
82
+ }
83
+ /** Links the middleware to its source file so a later update can unload it. */
84
+ trackMiddleware(file, middleware) {
85
+ let set = this.store.fileToMiddlewares.get(file);
86
+ if (!set) {
87
+ set = /* @__PURE__ */ new Set();
88
+ this.store.fileToMiddlewares.set(file, set);
89
+ }
90
+ set.add(middleware);
91
+ }
92
+ isTracked(file) {
93
+ return this.store.fileToHandlers.has(file) || this.store.fileToMiddlewares.has(file);
94
+ }
95
+ unload(file) {
96
+ const { logger } = this.options;
97
+ const handlers = this.store.fileToHandlers.get(file);
98
+ if (handlers) {
99
+ for (const handler of handlers) {
100
+ const artifacts = this.store.handlerArtifacts.get(handler);
101
+ this.options.unregisterHandler(handler, artifacts);
102
+ this.store.handlerArtifacts.delete(handler);
103
+ const name = displayName(handler, "Handler");
104
+ logger.info(`${chalk.default.red.bold("Unloaded")} ${chalk.default.cyan.bold(name)} from ${chalk.default.gray((0, _seedcord_utils.formatFilePath)(file))}`);
105
+ }
106
+ this.store.fileToHandlers.delete(file);
107
+ }
108
+ const middlewares = this.store.fileToMiddlewares.get(file);
109
+ if (middlewares) {
110
+ for (const middleware of middlewares) {
111
+ this.options.unregisterMiddleware?.(middleware);
112
+ const name = displayName(middleware, "Middleware");
113
+ logger.info(`${chalk.default.red.bold("Unloaded")} ${chalk.default.cyan.bold(name)} from ${chalk.default.gray((0, _seedcord_utils.formatFilePath)(file))}`);
114
+ }
115
+ this.store.fileToMiddlewares.delete(file);
116
+ }
117
+ }
118
+ async reloadFile(file) {
119
+ const { logger, isHandler, isMiddleware, registerHandler, registerMiddleware } = this.options;
120
+ if (!(0, node_fs.existsSync)(file)) {
121
+ logger.info(`File does not exist, skipping reload: ${(0, _seedcord_utils.formatFilePath)(file)}`);
122
+ return false;
123
+ }
124
+ try {
125
+ let fileUrl = (0, node_url.pathToFileURL)(file).href;
126
+ if (process.env.VITEST === "true") fileUrl += `?update=${Date.now()}`;
127
+ const imported = await import(fileUrl);
128
+ for (const val of Object.values(imported)) if (isHandler(val)) {
129
+ registerHandler(val, file);
130
+ this.trackHandler(file, val);
131
+ const name = displayName(val, "Handler");
132
+ logger.info(`${chalk.default.blue.bold("Reloaded")} ${chalk.default.cyan.bold(name)} from ${chalk.default.gray((0, _seedcord_utils.formatFilePath)(file))}`);
133
+ } else if (isMiddleware && registerMiddleware && isMiddleware(val)) {
134
+ registerMiddleware(val, file);
135
+ this.trackMiddleware(file, val);
136
+ const name = displayName(val, "Middleware");
137
+ logger.info(`${chalk.default.blue.bold("Reloaded")} ${chalk.default.cyan.bold(name)} from ${chalk.default.gray((0, _seedcord_utils.formatFilePath)(file))}`);
138
+ }
139
+ return true;
140
+ } catch (error) {
141
+ logger.error(`Failed to reload file: ${file}`, error);
142
+ return false;
143
+ }
144
+ }
145
+ snapshotUnits(file) {
146
+ return {
147
+ handlers: [...this.store.fileToHandlers.get(file) ?? []],
148
+ middlewares: [...this.store.fileToMiddlewares.get(file) ?? []]
149
+ };
150
+ }
151
+ restoreUnits(file, snapshot) {
152
+ for (const handler of snapshot.handlers) {
153
+ this.options.registerHandler(handler, file);
154
+ this.trackHandler(file, handler);
155
+ }
156
+ if (this.options.registerMiddleware) for (const middleware of snapshot.middlewares) {
157
+ this.options.registerMiddleware(middleware, file);
158
+ this.trackMiddleware(file, middleware);
159
+ }
160
+ }
161
+ };
162
+
163
+ //#endregion
164
+ Object.defineProperty(exports, 'HmrModuleHandler', {
165
+ enumerable: true,
166
+ get: function () {
167
+ return HmrModuleHandler;
168
+ }
169
+ });
170
+ //# sourceMappingURL=HmrModuleHandler-CTBng7wV.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HmrModuleHandler-CTBng7wV.cjs","names":["sep"],"sources":["../src/hmr/HmrModuleHandler.ts"],"sourcesContent":["import { existsSync } from 'node:fs';\nimport { resolve, sep } from 'node:path';\nimport { pathToFileURL } from 'node:url';\n\nimport { formatFilePath } from '@seedcord/utils';\nimport chalk from 'chalk';\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 // the caller's options object stays unmutated\n this.options = { ...options, logger: options.logger.inChannel('hmr') };\n\n // no import.meta.hot.data stash needed, a leaf reload never reconstructs the singleton that holds 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 const { logger, handlersDir, middlewaresDir } = this.options;\n\n if (type === 'delete' || type === 'deleteDir') {\n this.unload(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 if (type === 'update' && !existsSync(file)) {\n if (this.isTracked(file)) {\n logger.info(`Received update for non-existent file: ${formatFilePath(file)}, treating as delete`);\n this.unload(file);\n }\n return;\n }\n\n const rollbackEnabled = event.rollback ?? true;\n const filesToReload = affectedModules && affectedModules.length > 0 ? affectedModules : [file];\n const absHandlersDir = resolve(process.cwd(), handlersDir);\n const absMiddlewaresDir = middlewaresDir ? resolve(process.cwd(), middlewaresDir) : null;\n\n for (const fileToReload of filesToReload) {\n // the separator keeps a sibling dir sharing the prefix (commands-extra vs commands) out\n const isHandler = fileToReload.startsWith(absHandlersDir + sep);\n const isMiddleware = absMiddlewaresDir ? fileToReload.startsWith(absMiddlewaresDir + sep) : false;\n\n if (!isHandler && !isMiddleware) {\n continue;\n }\n\n await this.reloadWithRollback(fileToReload, rollbackEnabled);\n }\n }\n\n private async reloadWithRollback(file: string, rollbackEnabled: boolean): Promise<void> {\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) return;\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 them live until the next good save\n this.restoreUnits(file, snapshot);\n logger.warn(`${chalk.yellow.bold('Rolled back')} ${chalk.gray(formatFilePath(file))} to the last-good version`);\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) {\n const artifacts = this.options.getArtifacts(handler);\n this.store.handlerArtifacts.set(handler, artifacts);\n }\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 isTracked(file: string): boolean {\n return this.store.fileToHandlers.has(file) || this.store.fileToMiddlewares.has(file);\n }\n\n private unload(file: string): void {\n const { logger } = this.options;\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 const name = displayName(handler, 'Handler');\n logger.info(\n `${chalk.red.bold('Unloaded')} ${chalk.cyan.bold(name)} from ${chalk.gray(formatFilePath(file))}`\n );\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 const name = displayName(middleware, 'Middleware');\n logger.info(\n `${chalk.red.bold('Unloaded')} ${chalk.cyan.bold(name)} from ${chalk.gray(formatFilePath(file))}`\n );\n }\n this.store.fileToMiddlewares.delete(file);\n }\n }\n\n private async reloadFile(file: string): Promise<boolean> {\n const { logger, isHandler, isMiddleware, registerHandler, registerMiddleware } = this.options;\n\n if (!existsSync(file)) {\n logger.info(`File does not exist, skipping reload: ${formatFilePath(file)}`);\n return false;\n }\n\n try {\n let fileUrl = pathToFileURL(file).href;\n // vitest has no real vite server managing the module graph, so bust the import cache manually.\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(fileUrl)) as Record<string, unknown>;\n\n for (const val of Object.values(imported)) {\n if (isHandler(val)) {\n registerHandler(val, file);\n this.trackHandler(file, val);\n const name = displayName(val, 'Handler');\n logger.info(\n `${chalk.blue.bold('Reloaded')} ${chalk.cyan.bold(name)} from ${chalk.gray(formatFilePath(file))}`\n );\n } else if (isMiddleware && registerMiddleware && isMiddleware(val)) {\n registerMiddleware(val, file);\n this.trackMiddleware(file, val);\n const name = displayName(val, 'Middleware');\n logger.info(\n `${chalk.blue.bold('Reloaded')} ${chalk.cyan.bold(name)} from ${chalk.gray(formatFilePath(file))}`\n );\n }\n }\n\n return true;\n } catch (error) {\n logger.error(`Failed to reload file: ${file}`, error);\n return false;\n }\n }\n\n // restoring re-registers the same class objects, so a live db connection survives 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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"}