@seedcord/core 0.5.0 → 0.7.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.
Files changed (33) hide show
  1. package/dist/{Bus-dGy6xq3F.mjs → Bus-Djm6688b.mjs} +6 -5
  2. package/dist/Bus-Djm6688b.mjs.map +1 -0
  3. package/dist/{Pluggable-BKe2uCa0.d.mts → Pluggable-CqKvMXm5.d.mts} +11 -4
  4. package/dist/{Pluggable-CtMiOP3p.mjs → Pluggable-keys1cVr.mjs} +34 -10
  5. package/dist/Pluggable-keys1cVr.mjs.map +1 -0
  6. package/dist/Plugin-BJfRk0s_.mjs.map +1 -1
  7. package/dist/{Plugin-iAaWcLaD.d.mts → Plugin-BdqfRjU0.d.mts} +9 -3
  8. package/dist/{cursor-8Y2zpF-Y.d.mts → cursor-BY5dmiz6.d.mts} +121 -7
  9. package/dist/{cursor-BkI9lxy2.mjs → cursor-CFNKLVlk.mjs} +49 -11
  10. package/dist/cursor-CFNKLVlk.mjs.map +1 -0
  11. package/dist/{index-CD8l15aZ.d.mts → index-BS1hH9Sh.d.mts} +2 -2
  12. package/dist/index.d.mts +27 -46
  13. package/dist/index.mjs +31 -19
  14. package/dist/index.mjs.map +1 -1
  15. package/dist/internal.index.d.mts +79 -8
  16. package/dist/internal.index.mjs +121 -16
  17. package/dist/internal.index.mjs.map +1 -1
  18. package/dist/node-internal.index.d.mts +10 -5
  19. package/dist/node-internal.index.mjs +18 -9
  20. package/dist/node-internal.index.mjs.map +1 -1
  21. package/dist/node.index.d.mts +1 -1
  22. package/dist/node.index.mjs +31 -10
  23. package/dist/node.index.mjs.map +1 -1
  24. package/dist/{phases-D0a8BV9Z.d.mts → phases-DJf12l5T.d.mts} +48 -9
  25. package/dist/plugin.index.d.mts +1 -1
  26. package/dist/{slashRouteLeaves-C1msUE9Q.mjs → slashRouteLeaves-DTFHzCKN.mjs} +2 -2
  27. package/dist/{slashRouteLeaves-C1msUE9Q.mjs.map → slashRouteLeaves-DTFHzCKN.mjs.map} +1 -1
  28. package/dist/{subscribers-A8lNU5yM.mjs → subscribers-Bf4cBTvK.mjs} +2 -2
  29. package/dist/{subscribers-A8lNU5yM.mjs.map → subscribers-Bf4cBTvK.mjs.map} +1 -1
  30. package/package.json +14 -14
  31. package/dist/Bus-dGy6xq3F.mjs.map +0 -1
  32. package/dist/Pluggable-CtMiOP3p.mjs.map +0 -1
  33. package/dist/cursor-BkI9lxy2.mjs.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"node-internal.index.mjs","names":[],"sources":["../src/node/HealthResponder.ts","../src/node/HealthCheck.ts","../src/node/subscribers/SubscriberLoader.ts","../src/node/commands/CommandRegistry.ts","../src/node-internal.index.ts"],"sourcesContent":["import type { IncomingMessage, ServerResponse } from 'node:http';\n\nconst HTTP_OK = 200;\nconst DEFAULT_HEALTH_CHECK_PATH = '/health';\n\nexport class HealthResponder {\n public readonly path: string;\n\n constructor(path?: string) {\n this.path = path ?? DEFAULT_HEALTH_CHECK_PATH;\n }\n\n public tryRespond(req: IncomingMessage, res: ServerResponse): boolean {\n if (req.method !== 'GET' || req.url !== this.path) return false;\n\n res.writeHead(HTTP_OK, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ status: 'ok', timestamp: Date.now() }));\n return true;\n }\n}\n","import { createServer } from 'node:http';\n\nimport { paint } from '@seedcord/errors';\nimport { Logger } from '@seedcord/logger';\n\nimport { ShutdownPhase } from '#src/lifecycle/phases';\n\nimport { HealthResponder } from './HealthResponder';\n\nimport type { CoordinatedShutdown } from './Lifecycle/CoordinatedShutdown';\nimport type { HealthCheckConfig, HealthCheckOption } from '@seedcord/types';\nimport type { IncomingMessage, Server, ServerResponse } from 'node:http';\n\nconst HTTP_NOT_FOUND = 404;\n\nconst DEFAULT_HEALTH_CHECK_PORT = 6967;\n\nexport class HealthCheck {\n public readonly logger = new Logger('HealthCheck', { channel: 'health' });\n\n private readonly _port: number;\n private readonly _host: string | undefined;\n\n private readonly responder: HealthResponder;\n private server?: Server;\n\n constructor(shutdown: CoordinatedShutdown, options?: HealthCheckConfig) {\n this._port = options?.port ?? DEFAULT_HEALTH_CHECK_PORT;\n this._host = options?.host;\n this.responder = new HealthResponder(options?.path);\n\n shutdown.addTask(ShutdownPhase.Drain, 'stop-healthcheck-server', () => this.stop());\n }\n\n public get port(): number {\n return this._port;\n }\n\n public get host(): string | undefined {\n return this._host;\n }\n\n public get path(): string {\n return this.responder.path;\n }\n\n public static fromOption(shutdown: CoordinatedShutdown, option?: HealthCheckOption): HealthCheck | undefined {\n if (option === false) return undefined;\n return new HealthCheck(shutdown, typeof option === 'object' ? option : undefined);\n }\n\n public async init(): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n const server = createServer((req: IncomingMessage, res: ServerResponse) => {\n if (this.responder.tryRespond(req, res)) return;\n\n res.writeHead(HTTP_NOT_FOUND, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ status: 'not found' }));\n });\n this.server = server;\n\n const onListenError = (err: Error): void => reject(err);\n server.on('error', onListenError);\n\n server.once('listening', () => {\n // a late error would reject an already-settled promise, and node throws process-wide without a listener here\n server.removeListener('error', onListenError);\n server.on('error', (err) => this.logger.error('Health check server error', err));\n\n // binds all interfaces, so log an address a browser can open\n const address = this._host ?? 'localhost';\n this.logger.info(\n `${paint.mint.bold('✔︎')} Health check server listening on ${paint.sky.bold(`http://${address}:${this._port}${this.path}`)}`\n );\n resolve();\n });\n\n if (this._host) {\n this.logger.debug(`Binding health check server to ${this._host}`);\n server.listen(this._port, this._host);\n } else {\n this.logger.debug('Binding health check server to all interfaces');\n server.listen(this._port);\n }\n });\n }\n\n public stop(): Promise<void> {\n const server = this.server;\n // close() on a non-listening server invokes its callback with ERR_SERVER_NOT_RUNNING and\n // never fires 'close', so without this guard the promise would hang the shutdown phase.\n if (!server?.listening) return Promise.resolve();\n\n return new Promise((resolve, reject) => {\n // settle only in the callback, a 'close'-event resolve would swallow the callback's error\n server.close((err) => {\n if (err) {\n reject(err);\n return;\n }\n this.logger.info(paint.coral.bold('Health check server stopped'));\n resolve();\n });\n });\n }\n}\n","import { paint } from '@seedcord/errors';\nimport { traverseDirectory } from '@seedcord/utils/node';\nimport { Envapter } from 'envapt';\n\nimport { HmrModuleHandler } from '#hmr/HmrModuleHandler';\nimport { SubscribeMetadataKey } from '#src/metadataKeys';\nimport { busLoggerOf } from '#subscribers/Bus';\nimport { registrationFor, Subscriber } from '#subscribers/index';\nimport {\n RegisterDefaults,\n RegisteredCount,\n RegisterSubscriber,\n UnregisterSubscriber,\n VerifyWebhooks\n} from '#subscribers/slots';\n\nimport type { Initializeable } from '#src/plugin/Plugin';\nimport type { SubscribeMetadataEntry } from '#subscribers/decorators/Subscribe';\nimport type { StoredSubscriberCtor, Bus } from '#subscribers/index';\nimport type { SubscriptionKey } from '#subscribers/types/Subscriptions';\nimport type { HmrAware, HmrUpdateEvent } from '@seedcord/types';\n\ntype SubscriberArtifact = SubscriptionKey[];\n\n// loads subscribers off the filesystem into a Bus and stores their hot-reload wiring. node only, a bundled isolate registers from the manifest instead\nexport class SubscriberLoader implements Initializeable, HmrAware {\n private isInitialized = false;\n private readonly hmrHandler?: HmrModuleHandler<StoredSubscriberCtor, void, SubscriberArtifact>;\n\n constructor(\n private readonly bus: Bus,\n private readonly directory: string | null\n ) {\n if (!this.directory) return;\n if (!Envapter.isDevelopment && !Envapter.isTest) return;\n\n this.hmrHandler = new HmrModuleHandler({\n handlersDir: this.directory,\n isHandler: this.isSubscriber.bind(this),\n registerHandler: this.registerSubscriber.bind(this),\n unregisterHandler: this.unregisterSubscriber.bind(this),\n getArtifacts: this.getArtifacts.bind(this),\n logger: busLoggerOf(this.bus)\n });\n }\n\n /** @internal */\n public async init(): Promise<void> {\n if (this.isInitialized) return;\n this.isInitialized = true;\n\n this.bus[RegisterDefaults]();\n\n const { directory } = this;\n if (directory) {\n busLoggerOf(this.bus).debug(paint.mute(directory));\n await this.load(directory);\n busLoggerOf(this.bus).utils.list(\n [`${paint.iris.bold(this.bus[RegisteredCount])} subscribers`],\n paint.mint.bold('Loaded'),\n 'debug'\n );\n }\n\n if (!Envapter.isTest) await this.bus[VerifyWebhooks]();\n }\n\n /** @internal */\n public async onHmr(event: HmrUpdateEvent): Promise<void> {\n if (this.hmrHandler) await this.hmrHandler.handle(event);\n }\n\n private async load(dir: string): Promise<void> {\n await traverseDirectory(dir, (fullPath, relativePath, imported) => {\n for (const exportName of Object.keys(imported)) {\n const value = imported[exportName];\n if (!this.isSubscriber(value)) continue;\n\n this.registerSubscriber(value);\n this.hmrHandler?.trackHandler(fullPath, value);\n busLoggerOf(this.bus).utils.registration(value.name, relativePath, undefined, 'trace');\n }\n });\n }\n\n private registerSubscriber(ctor: StoredSubscriberCtor): void {\n this.bus[RegisterSubscriber](registrationFor(ctor));\n }\n\n private unregisterSubscriber(ctor: StoredSubscriberCtor, artifacts?: SubscriberArtifact): void {\n this.bus[UnregisterSubscriber](ctor, artifacts);\n }\n\n private getArtifacts(ctor: StoredSubscriberCtor): SubscriberArtifact {\n const meta = Reflect.getMetadata(SubscribeMetadataKey, ctor) as SubscribeMetadataEntry;\n return [meta.subscriber];\n }\n\n private isSubscriber(value: unknown): value is StoredSubscriberCtor {\n if (typeof value !== 'function') return false;\n return value.prototype instanceof Subscriber && Reflect.hasMetadata(SubscribeMetadataKey, value);\n }\n}\n","import { resolve } from 'node:path';\n\nimport { SlashCommandBuilder } from '@discordjs/builders';\nimport { SeedcordErrorCode, paint } from '@seedcord/errors';\nimport { SeedcordError } from '@seedcord/errors/internal';\nimport { Logger } from '@seedcord/logger';\nimport { formatFilePath } from '@seedcord/utils';\nimport { traverseDirectory } from '@seedcord/utils/node';\nimport { Routes } from 'discord-api-types/v10';\nimport { Envapter } from 'envapt';\n\nimport { getDevChannel } from '#hmr/devChannel';\nimport { HmrModuleHandler } from '#hmr/HmrModuleHandler';\nimport { CommandInjector } from '#src/commands/CommandInjector';\nimport { contextMenuLeaves } from '#src/commands/contextMenuLeaves';\nimport { isCommandClass } from '#src/commands/isCommandClass';\nimport { slashRouteLeaves } from '#src/commands/slashRouteLeaves';\nimport { CommandMetadataKey } from '#src/metadataKeys';\nimport { PublishDefault } from '#subscribers/publishDefault';\n\nimport type { CommandMeta } from '#decorators/Command';\nimport type { CoreBase } from '#interfaces/CoreBase';\nimport type { ContextMenuLeaves } from '#src/commands/contextMenuLeaves';\nimport type { CommandCtor } from '#src/commands/isCommandClass';\nimport type { CommandBuilder, DeployResult } from '#src/commands/types';\nimport type { Initializeable } from '#src/plugin/Plugin';\nimport type { AllSubscriptions } from '#subscribers/types/Subscriptions';\nimport type { HmrAware, HmrUpdateEvent } from '@seedcord/types';\nimport type { APIApplicationCommand } from 'discord-api-types/v10';\n\ninterface CommandArtifact {\n name: string;\n scope: 'global' | 'guild';\n guilds?: string[];\n}\n\nfunction indexById(commands: readonly APIApplicationCommand[]): Map<string, APIApplicationCommand> {\n return new Map(commands.map((command) => [command.id, command]));\n}\n\n/**\n * Scans the command directory, builds each command, and PUTs the global and guild scopes to Discord.\n *\n * @internal\n */\nexport class CommandRegistry implements Initializeable, HmrAware {\n private readonly logger = new Logger('Commands', { channel: 'commands' });\n private isInitialised = false;\n\n public readonly globalCommands: CommandBuilder[] = [];\n public readonly guildCommands = new Map<string, CommandBuilder[]>();\n\n private readonly ctorToCommand = new Map<CommandCtor, CommandArtifact>();\n\n // batched during bulk load. A reload reports on the hmr channel instead\n private loading = false;\n private readonly loadedCommands: { name: string; from: string; kind: 'slash command' | 'context menu' }[] = [];\n\n private readonly hmrHandler?: HmrModuleHandler<CommandCtor, void, CommandArtifact | undefined>;\n private readonly pendingEvents = new Map<string, HmrUpdateEvent>();\n private readonly injector = new CommandInjector();\n\n private readonly dir: string;\n\n public constructor(private readonly core: CoreBase) {\n const dir = core.config.bot.commands.path;\n if (!dir) {\n throw new SeedcordError(SeedcordErrorCode.CoreControllerPathMissing, ['CommandRegistry', 'commands']);\n }\n this.dir = dir;\n\n if (!Envapter.isDevelopment && !Envapter.isTest) return;\n this.hmrHandler = new HmrModuleHandler<CommandCtor, void, CommandArtifact | undefined>({\n handlersDir: dir,\n isHandler: isCommandClass,\n registerHandler: this.registerCommand.bind(this),\n unregisterHandler: this.unregisterCommand.bind(this),\n getArtifacts: this.getArtifacts.bind(this),\n logger: this.logger\n });\n }\n\n private getArtifacts(ctor: CommandCtor): CommandArtifact | undefined {\n return this.ctorToCommand.get(ctor);\n }\n\n /** @internal */\n public async init(): Promise<void> {\n if (this.isInitialised) return;\n this.isInitialised = true;\n\n this.loading = true;\n this.loadedCommands.length = 0;\n try {\n await this.loadCommands(this.dir);\n } finally {\n this.loading = false;\n }\n\n this.reportLoad();\n\n getDevChannel()?.on('seedcord:refresh-commands', (data) => {\n void this.refresh(data.shouldRefresh);\n });\n }\n\n private reportLoad(): void {\n const { utils } = this.logger;\n\n utils.summary(\n 'Loaded commands',\n { global: this.globalCommands.length, 'guild groups': this.guildCommands.size },\n 'debug'\n );\n\n let slash = 0;\n let menu = 0;\n for (const command of this.loadedCommands) {\n if (command.kind === 'slash command') slash++;\n else menu++;\n }\n\n utils.block(\n 'Loaded commands',\n [\n ...utils.entries(this.loadedCommands),\n ...utils.counts({ 'slash commands': slash, 'context menus': menu })\n ],\n 'debug'\n );\n }\n\n /** @internal */\n public async refresh(shouldRefresh = true): Promise<void> {\n if (!shouldRefresh) {\n this.logger.debug(paint.italic('Command refresh cancelled.'));\n this.pendingEvents.clear();\n return;\n }\n\n this.logger.debug(paint.italic('Refreshing commands...'));\n for (const event of this.pendingEvents.values()) {\n await this.hmrHandler?.handle(event);\n }\n this.pendingEvents.clear();\n await this.setCommands();\n }\n\n /** @internal */\n public async onHmr(event: HmrUpdateEvent): Promise<void> {\n if (event.file.startsWith(resolve(process.cwd(), this.dir))) {\n this.pendingEvents.delete(event.file);\n this.pendingEvents.set(event.file, event);\n getDevChannel()?.send('seedcord:commands-update-prompt', {\n files: [...this.pendingEvents.keys()].map((f) => formatFilePath(f))\n });\n } else {\n await this.hmrHandler?.handle(event);\n }\n }\n\n private async loadCommands(dir: string): Promise<void> {\n await traverseDirectory(dir, (fullPath, rel, mod) => {\n for (const exported of Object.values(mod))\n if (isCommandClass(exported)) {\n this.registerCommand(exported, rel);\n this.hmrHandler?.trackHandler(fullPath, exported);\n }\n });\n }\n\n private registerCommand(ctor: CommandCtor, rel: string): void {\n const meta = Reflect.getMetadata(CommandMetadataKey, ctor) as CommandMeta | undefined;\n\n if (!meta) return;\n\n const instance = new ctor();\n const comp = instance.component;\n const kind = comp instanceof SlashCommandBuilder ? 'slash command' : 'context menu';\n\n if (meta.scope === 'global') {\n this.globalCommands.push(comp);\n } else {\n for (const g of meta.guilds) {\n const arr = this.guildCommands.get(g) ?? [];\n arr.push(comp);\n this.guildCommands.set(g, arr);\n }\n }\n\n this.ctorToCommand.set(ctor, {\n name: comp.name,\n scope: meta.scope,\n ...(meta.scope === 'guild' && { guilds: meta.guilds })\n });\n\n if (this.loading) {\n this.loadedCommands.push({ name: comp.name, from: formatFilePath(rel), kind });\n }\n }\n\n private unregisterCommand(ctor: CommandCtor, artifacts?: CommandArtifact): void {\n const info = artifacts ?? this.ctorToCommand.get(ctor);\n if (!info) return;\n\n if (info.scope === 'global') {\n const idx = this.globalCommands.findIndex((c) => c.name === info.name);\n if (idx !== -1) this.globalCommands.splice(idx, 1);\n } else {\n for (const g of info.guilds ?? []) {\n const arr = this.guildCommands.get(g);\n if (arr) {\n const idx = arr.findIndex((c) => c.name === info.name);\n if (idx !== -1) arr.splice(idx, 1);\n }\n }\n }\n this.ctorToCommand.delete(ctor);\n }\n\n /** @internal */\n public async setCommands(): Promise<DeployResult> {\n const result: DeployResult = { global: new Map(), guilds: new Map() };\n const appId = this.core.applicationId;\n\n if (this.globalCommands.length > 0) {\n const deployed = await this.put(Routes.applicationCommands(appId), this.globalCommands);\n result.global = indexById(deployed);\n const tag = this.globalCommands.length === 1 ? 'command' : 'commands';\n this.logger.utils.block(\n `Deployed ${this.globalCommands.length} global ${tag}`,\n this.logger.utils.wrap(this.globalCommands.map((command) => command.name)),\n 'info'\n );\n }\n\n for (const [guildId, commands] of this.guildCommands.entries()) {\n const deployed = await this.put(Routes.applicationGuildCommands(appId, guildId), commands);\n result.guilds.set(guildId, indexById(deployed));\n const tag = commands.length === 1 ? 'command' : 'commands';\n this.logger.utils.block(\n `Deployed ${paint.iris.bold(commands.length)} ${tag} to ${paint.sky.bold(guildId)}`,\n this.logger.utils.wrap(commands.map((command) => command.name)),\n 'info'\n );\n }\n\n this.injector.inject(result, this.allCommands());\n this.core.bus[PublishDefault]('commandsDeployed', deployedPayload(result));\n return result;\n }\n\n private async put(route: `/${string}`, commands: readonly CommandBuilder[]): Promise<APIApplicationCommand[]> {\n const body = commands.map((command) => command.toJSON());\n // read here because gateway assigns core.rest after this registry is constructed\n const { rest } = this.core;\n // justified: the bulk-overwrite routes return the deployed command array\n return (await rest.put(route, { body })) as APIApplicationCommand[];\n }\n\n /** Every registered builder, global first. A guild command in N guilds appears once per guild. @internal */\n public allCommands(): CommandBuilder[] {\n return [...this.globalCommands, ...this.guildCommands.values()].flat();\n }\n\n /** @internal */\n public routeLeaves(): Set<string> {\n return slashRouteLeaves(this.allCommands());\n }\n\n /** @internal */\n public contextMenuLeaves(): ContextMenuLeaves {\n return contextMenuLeaves(this.allCommands());\n }\n}\n\nfunction deployedPayload(result: DeployResult): AllSubscriptions['commandsDeployed'] {\n const guilds: Record<string, APIApplicationCommand[]> = {};\n for (const [guildId, deployed] of result.guilds) guilds[guildId] = [...deployed.values()];\n\n return { global: [...result.global.values()], guilds };\n}\n","import { assertNodeVersion } from '#node/assertNodeVersion';\n\n// the edge bundle never reaches this entry\nassertNodeVersion(process.env.PACKAGE_NODE_RANGE ?? '', process.version);\n\nexport * from '#node/Lifecycle';\nexport * from '#node/HealthCheck';\nexport { HealthResponder } from '#node/HealthResponder';\nexport { settleWithin } from '#node/Lifecycle/withTimeout';\nexport { SubscriberLoader } from '#node/subscribers/SubscriberLoader';\nexport { CommandRegistry } from '#node/commands/CommandRegistry';\nexport { shutdownOf } from '#node/Pluggable';\n"],"mappings":"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"}
1
+ {"version":3,"file":"node-internal.index.mjs","names":[],"sources":["../src/node/Lifecycle/drainWindow.ts","../src/node/HealthResponder.ts","../src/node/HealthCheck.ts","../src/node/subscribers/SubscriberLoader.ts","../src/node/commands/CommandRegistry.ts","../src/node-internal.index.ts"],"sourcesContent":["import { settleWithin } from './withTimeout';\n\nimport type { Logger } from '@seedcord/logger';\n\n// gateway already used 5s. discord's 3s first-ack deadline is the floor.\nexport const DRAIN_WINDOW_MS = 5000;\n\nconst DRAIN_HEADROOM_MS = 1000;\nexport const DRAIN_TASK_TIMEOUT_MS = DRAIN_WINDOW_MS + DRAIN_HEADROOM_MS;\n\nexport async function drainInFlight(\n inFlight: ReadonlySet<Promise<unknown>>,\n timeoutMs: number,\n logger: Logger,\n label: string\n): Promise<void> {\n await settleWithin(Promise.allSettled(inFlight), timeoutMs);\n // nothing cancels these before the process exits\n if (inFlight.size > 0) logger.warn(`${label}: ${String(inFlight.size)} still running when the drain window closed`);\n}\n","import type { IncomingMessage, ServerResponse } from 'node:http';\n\nconst HTTP_OK = 200;\nconst DEFAULT_HEALTH_CHECK_PATH = '/health';\n\nexport class HealthResponder {\n public readonly path: string;\n\n constructor(path?: string) {\n this.path = path ?? DEFAULT_HEALTH_CHECK_PATH;\n }\n\n public tryRespond(req: IncomingMessage, res: ServerResponse): boolean {\n if (req.method !== 'GET' || req.url !== this.path) return false;\n\n res.writeHead(HTTP_OK, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ status: 'ok', timestamp: Date.now() }));\n return true;\n }\n}\n","import { createServer } from 'node:http';\n\nimport { paint } from '@seedcord/errors';\nimport { Logger } from '@seedcord/logger';\n\nimport { ShutdownPhase } from '#src/lifecycle/phases';\n\nimport { HealthResponder } from './HealthResponder';\n\nimport type { CoordinatedShutdown } from './Lifecycle/CoordinatedShutdown';\nimport type { HealthCheckConfig, HealthCheckOption } from '@seedcord/types';\nimport type { IncomingMessage, Server, ServerResponse } from 'node:http';\n\nconst HTTP_NOT_FOUND = 404;\n\nconst DEFAULT_HEALTH_CHECK_PORT = 6967;\n\nexport class HealthCheck {\n public readonly logger = new Logger('HealthCheck', { channel: 'health' });\n\n private readonly _port: number;\n private readonly _host: string | undefined;\n\n private readonly responder: HealthResponder;\n private server?: Server;\n\n constructor(shutdown: CoordinatedShutdown, options?: HealthCheckConfig) {\n this._port = options?.port ?? DEFAULT_HEALTH_CHECK_PORT;\n this._host = options?.host;\n this.responder = new HealthResponder(options?.path);\n\n shutdown.addTask(ShutdownPhase.Drain, 'stop-healthcheck-server', () => this.stop());\n }\n\n public get port(): number {\n return this._port;\n }\n\n public get host(): string | undefined {\n return this._host;\n }\n\n public get path(): string {\n return this.responder.path;\n }\n\n public static fromOption(shutdown: CoordinatedShutdown, option?: HealthCheckOption): HealthCheck | undefined {\n if (option === false) return undefined;\n return new HealthCheck(shutdown, typeof option === 'object' ? option : undefined);\n }\n\n public async init(): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n const server = createServer((req: IncomingMessage, res: ServerResponse) => {\n if (this.responder.tryRespond(req, res)) return;\n\n res.writeHead(HTTP_NOT_FOUND, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ status: 'not found' }));\n });\n this.server = server;\n\n const onListenError = (err: Error): void => reject(err);\n server.on('error', onListenError);\n\n server.once('listening', () => {\n // a late error would reject an already-settled promise, and node throws process-wide without a listener here\n server.removeListener('error', onListenError);\n server.on('error', (err) => this.logger.error('Health check server error', err));\n\n // binds all interfaces, so log an address a browser can open\n const address = this._host ?? 'localhost';\n this.logger.info(\n `${paint.mint.bold('✔︎')} Health check server listening on ${paint.sky.bold(`http://${address}:${this._port}${this.path}`)}`\n );\n resolve();\n });\n\n if (this._host) {\n this.logger.debug(`Binding health check server to ${this._host}`);\n server.listen(this._port, this._host);\n } else {\n this.logger.debug('Binding health check server to all interfaces');\n server.listen(this._port);\n }\n });\n }\n\n public stop(): Promise<void> {\n const server = this.server;\n // close() on a non-listening server invokes its callback with ERR_SERVER_NOT_RUNNING and\n // never fires 'close', so without this guard the promise would hang the shutdown phase.\n if (!server?.listening) return Promise.resolve();\n\n return new Promise((resolve, reject) => {\n // settle only in the callback, a 'close'-event resolve would swallow the callback's error\n server.close((err) => {\n if (err) {\n reject(err);\n return;\n }\n this.logger.info(paint.coral.bold('Health check server stopped'));\n resolve();\n });\n });\n }\n}\n","import { paint } from '@seedcord/errors';\nimport { traverseDirectory } from '@seedcord/utils/node';\nimport { Envapter } from 'envapt';\n\nimport { HmrModuleHandler } from '#hmr/HmrModuleHandler';\nimport { SubscribeMetadataKey } from '#src/metadataKeys';\nimport { busLoggerOf } from '#subscribers/Bus';\nimport { registrationFor, Subscriber } from '#subscribers/index';\nimport {\n RegisterDefaults,\n RegisteredCount,\n RegisterSubscriber,\n UnregisterSubscriber,\n VerifyWebhooks\n} from '#subscribers/slots';\n\nimport type { Initializeable } from '#src/plugin/Plugin';\nimport type { SubscribeMetadataEntry } from '#subscribers/decorators/Subscribe';\nimport type { StoredSubscriberCtor, Bus } from '#subscribers/index';\nimport type { SubscriptionKey } from '#subscribers/types/Subscriptions';\nimport type { HmrAware, HmrUpdateEvent } from '@seedcord/types';\n\ntype SubscriberArtifact = SubscriptionKey[];\n\n// loads subscribers off the filesystem into a Bus and stores their hot-reload wiring. node only, a bundled isolate registers from the manifest instead\nexport class SubscriberLoader implements Initializeable, HmrAware {\n private isInitialized = false;\n private readonly hmrHandler?: HmrModuleHandler<StoredSubscriberCtor, void, SubscriberArtifact>;\n\n constructor(\n private readonly bus: Bus,\n private readonly directory: string | null\n ) {\n if (!this.directory) return;\n if (!Envapter.isDevelopment && !Envapter.isTest) return;\n\n this.hmrHandler = new HmrModuleHandler({\n handlersDir: this.directory,\n isHandler: this.isSubscriber.bind(this),\n registerHandler: this.registerSubscriber.bind(this),\n unregisterHandler: this.unregisterSubscriber.bind(this),\n getArtifacts: this.getArtifacts.bind(this),\n logger: busLoggerOf(this.bus)\n });\n }\n\n /** @internal */\n public async init(): Promise<void> {\n if (this.isInitialized) return;\n this.isInitialized = true;\n\n this.bus[RegisterDefaults]();\n\n const { directory } = this;\n if (directory) {\n busLoggerOf(this.bus).debug(paint.mute(directory));\n await this.load(directory);\n busLoggerOf(this.bus).utils.list(\n [`${paint.iris.bold(this.bus[RegisteredCount])} subscribers`],\n paint.mint.bold('Loaded'),\n 'debug'\n );\n }\n\n if (!Envapter.isTest) await this.bus[VerifyWebhooks]();\n }\n\n /** @internal */\n public async onHmr(event: HmrUpdateEvent): Promise<void> {\n if (this.hmrHandler) await this.hmrHandler.handle(event);\n }\n\n private async load(dir: string): Promise<void> {\n await traverseDirectory(dir, (fullPath, relativePath, imported) => {\n for (const exportName of Object.keys(imported)) {\n const value = imported[exportName];\n if (!this.isSubscriber(value)) continue;\n\n this.registerSubscriber(value);\n this.hmrHandler?.trackHandler(fullPath, value);\n busLoggerOf(this.bus).utils.registration(value.name, relativePath, undefined, 'trace');\n }\n });\n }\n\n private registerSubscriber(ctor: StoredSubscriberCtor): void {\n this.bus[RegisterSubscriber](registrationFor(ctor));\n }\n\n private unregisterSubscriber(ctor: StoredSubscriberCtor, artifacts?: SubscriberArtifact): void {\n this.bus[UnregisterSubscriber](ctor, artifacts);\n }\n\n private getArtifacts(ctor: StoredSubscriberCtor): SubscriberArtifact {\n const meta = Reflect.getMetadata(SubscribeMetadataKey, ctor) as SubscribeMetadataEntry;\n return [meta.subscriber];\n }\n\n private isSubscriber(value: unknown): value is StoredSubscriberCtor {\n if (typeof value !== 'function') return false;\n return value.prototype instanceof Subscriber && Reflect.hasMetadata(SubscribeMetadataKey, value);\n }\n}\n","import { resolve } from 'node:path';\n\nimport { SlashCommandBuilder } from '@discordjs/builders';\nimport { SeedcordErrorCode, paint } from '@seedcord/errors';\nimport { SeedcordError } from '@seedcord/errors/internal';\nimport { Logger } from '@seedcord/logger';\nimport { formatFilePath } from '@seedcord/utils';\nimport { traverseDirectory } from '@seedcord/utils/node';\nimport { Routes } from 'discord-api-types/v10';\nimport { Envapter } from 'envapt';\n\nimport { getDevChannel } from '#hmr/devChannel';\nimport { HmrModuleHandler } from '#hmr/HmrModuleHandler';\nimport { CommandInjector } from '#src/commands/CommandInjector';\nimport { contextMenuLeaves } from '#src/commands/contextMenuLeaves';\nimport { isCommandClass } from '#src/commands/isCommandClass';\nimport { slashRouteLeaves } from '#src/commands/slashRouteLeaves';\nimport { CommandMetadataKey } from '#src/metadataKeys';\nimport { PublishDefault } from '#subscribers/publishDefault';\n\nimport type { CommandMeta } from '#decorators/Command';\nimport type { CoreBase } from '#interfaces/CoreBase';\nimport type { ContextMenuLeaves } from '#src/commands/contextMenuLeaves';\nimport type { CommandCtor } from '#src/commands/isCommandClass';\nimport type { CommandBuilder, DeployResult } from '#src/commands/types';\nimport type { Initializeable } from '#src/plugin/Plugin';\nimport type { AllSubscriptions } from '#subscribers/types/Subscriptions';\nimport type { HmrAware, HmrUpdateEvent } from '@seedcord/types';\nimport type { APIApplicationCommand } from 'discord-api-types/v10';\n\ninterface CommandArtifact {\n name: string;\n scope: 'global' | 'guild';\n guilds?: string[];\n}\n\nfunction indexById(commands: readonly APIApplicationCommand[]): Map<string, APIApplicationCommand> {\n return new Map(commands.map((command) => [command.id, command]));\n}\n\n/**\n * Scans the command directory, builds each command, and PUTs the global and guild scopes to Discord.\n *\n * @internal\n */\nexport class CommandRegistry implements Initializeable, HmrAware {\n private readonly logger = new Logger('Commands', { channel: 'commands' });\n private isInitialised = false;\n\n public readonly globalCommands: CommandBuilder[] = [];\n public readonly guildCommands = new Map<string, CommandBuilder[]>();\n\n private readonly ctorToCommand = new Map<CommandCtor, CommandArtifact>();\n\n // batched during bulk load. A reload reports on the hmr channel instead\n private loading = false;\n private readonly loadedCommands: { name: string; from: string; kind: 'slash command' | 'context menu' }[] = [];\n\n private readonly hmrHandler?: HmrModuleHandler<CommandCtor, void, CommandArtifact | undefined>;\n private readonly pendingEvents = new Map<string, HmrUpdateEvent>();\n private readonly injector = new CommandInjector();\n\n private readonly dir: string;\n\n public constructor(private readonly core: CoreBase) {\n const dir = core.config.bot.commands.path;\n if (!dir) {\n throw new SeedcordError(SeedcordErrorCode.CoreControllerPathMissing, ['CommandRegistry', 'commands']);\n }\n this.dir = dir;\n\n if (!Envapter.isDevelopment && !Envapter.isTest) return;\n this.hmrHandler = new HmrModuleHandler<CommandCtor, void, CommandArtifact | undefined>({\n handlersDir: dir,\n isHandler: isCommandClass,\n registerHandler: this.registerCommand.bind(this),\n unregisterHandler: this.unregisterCommand.bind(this),\n getArtifacts: this.getArtifacts.bind(this),\n logger: this.logger\n });\n }\n\n private getArtifacts(ctor: CommandCtor): CommandArtifact | undefined {\n return this.ctorToCommand.get(ctor);\n }\n\n /** @internal */\n public async init(): Promise<void> {\n if (this.isInitialised) return;\n this.isInitialised = true;\n\n this.loading = true;\n this.loadedCommands.length = 0;\n try {\n await this.loadCommands(this.dir);\n } finally {\n this.loading = false;\n }\n\n this.reportLoad();\n\n getDevChannel()?.on('seedcord:refresh-commands', (data) => {\n void this.refresh(data.shouldRefresh);\n });\n }\n\n private reportLoad(): void {\n const { utils } = this.logger;\n\n utils.summary(\n 'Loaded commands',\n { global: this.globalCommands.length, 'guild groups': this.guildCommands.size },\n 'debug'\n );\n\n let slash = 0;\n let menu = 0;\n for (const command of this.loadedCommands) {\n if (command.kind === 'slash command') slash++;\n else menu++;\n }\n\n utils.block(\n 'Loaded commands',\n [\n ...utils.entries(this.loadedCommands),\n ...utils.counts({ 'slash commands': slash, 'context menus': menu })\n ],\n 'debug'\n );\n }\n\n /** @internal */\n public async refresh(shouldRefresh = true): Promise<void> {\n if (!shouldRefresh) {\n this.logger.debug(paint.italic('Command refresh cancelled.'));\n this.pendingEvents.clear();\n return;\n }\n\n this.logger.debug(paint.italic('Refreshing commands...'));\n for (const event of this.pendingEvents.values()) {\n await this.hmrHandler?.handle(event);\n }\n this.pendingEvents.clear();\n await this.setCommands();\n }\n\n /** @internal */\n public async onHmr(event: HmrUpdateEvent): Promise<void> {\n if (event.file.startsWith(resolve(process.cwd(), this.dir))) {\n this.pendingEvents.delete(event.file);\n this.pendingEvents.set(event.file, event);\n getDevChannel()?.send('seedcord:commands-update-prompt', {\n files: [...this.pendingEvents.keys()].map((f) => formatFilePath(f))\n });\n } else {\n await this.hmrHandler?.handle(event);\n }\n }\n\n private async loadCommands(dir: string): Promise<void> {\n await traverseDirectory(dir, (fullPath, rel, mod) => {\n for (const exported of Object.values(mod))\n if (isCommandClass(exported)) {\n this.registerCommand(exported, rel);\n this.hmrHandler?.trackHandler(fullPath, exported);\n }\n });\n }\n\n private registerCommand(Ctor: CommandCtor, rel: string): void {\n const meta = Reflect.getMetadata(CommandMetadataKey, Ctor) as CommandMeta | undefined;\n\n if (!meta) return;\n\n const instance = new Ctor();\n const comp = instance.component;\n const kind = comp instanceof SlashCommandBuilder ? 'slash command' : 'context menu';\n\n if (meta.scope === 'global') {\n this.globalCommands.push(comp);\n } else {\n for (const g of meta.guilds) {\n const arr = this.guildCommands.get(g) ?? [];\n arr.push(comp);\n this.guildCommands.set(g, arr);\n }\n }\n\n this.ctorToCommand.set(Ctor, {\n name: comp.name,\n scope: meta.scope,\n ...(meta.scope === 'guild' && { guilds: meta.guilds })\n });\n\n if (this.loading) {\n this.loadedCommands.push({ name: comp.name, from: formatFilePath(rel), kind });\n }\n }\n\n private unregisterCommand(ctor: CommandCtor, artifacts?: CommandArtifact): void {\n const info = artifacts ?? this.ctorToCommand.get(ctor);\n if (!info) return;\n\n if (info.scope === 'global') {\n const idx = this.globalCommands.findIndex((c) => c.name === info.name);\n if (idx !== -1) this.globalCommands.splice(idx, 1);\n } else {\n for (const g of info.guilds ?? []) {\n const arr = this.guildCommands.get(g);\n if (arr) {\n const idx = arr.findIndex((c) => c.name === info.name);\n if (idx !== -1) arr.splice(idx, 1);\n }\n }\n }\n this.ctorToCommand.delete(ctor);\n }\n\n /** @internal */\n public async setCommands(): Promise<DeployResult> {\n const result: DeployResult = { global: new Map(), guilds: new Map() };\n const appId = this.core.applicationId;\n\n if (this.globalCommands.length > 0) {\n const deployed = await this.put(Routes.applicationCommands(appId), this.globalCommands);\n result.global = indexById(deployed);\n const tag = this.globalCommands.length === 1 ? 'command' : 'commands';\n this.logger.utils.block(\n `Deployed ${this.globalCommands.length} global ${tag}`,\n this.logger.utils.wrap(this.globalCommands.map((command) => command.name)),\n 'info'\n );\n }\n\n for (const [guildId, commands] of this.guildCommands.entries()) {\n const deployed = await this.put(Routes.applicationGuildCommands(appId, guildId), commands);\n result.guilds.set(guildId, indexById(deployed));\n const tag = commands.length === 1 ? 'command' : 'commands';\n this.logger.utils.block(\n `Deployed ${paint.iris.bold(commands.length)} ${tag} to ${paint.sky.bold(guildId)}`,\n this.logger.utils.wrap(commands.map((command) => command.name)),\n 'info'\n );\n }\n\n this.injector.inject(result, this.allCommands());\n this.core.bus[PublishDefault]('commandsDeployed', deployedPayload(result));\n return result;\n }\n\n private async put(route: `/${string}`, commands: readonly CommandBuilder[]): Promise<APIApplicationCommand[]> {\n const body = commands.map((command) => command.toJSON());\n // read here because gateway assigns core.rest after this registry is constructed\n const { rest } = this.core;\n // justified: the bulk-overwrite routes return the deployed command array\n return (await rest.put(route, { body })) as APIApplicationCommand[];\n }\n\n /** Every registered builder, global first. A guild command in N guilds appears once per guild. @internal */\n public allCommands(): CommandBuilder[] {\n return [...this.globalCommands, ...this.guildCommands.values()].flat();\n }\n\n /** @internal */\n public routeLeaves(): Set<string> {\n return slashRouteLeaves(this.allCommands());\n }\n\n /** @internal */\n public contextMenuLeaves(): ContextMenuLeaves {\n return contextMenuLeaves(this.allCommands());\n }\n}\n\nfunction deployedPayload(result: DeployResult): AllSubscriptions['commandsDeployed'] {\n const guilds: Record<string, APIApplicationCommand[]> = {};\n for (const [guildId, deployed] of result.guilds) guilds[guildId] = [...deployed.values()];\n\n return { global: [...result.global.values()], guilds };\n}\n","import { assertNodeVersion } from '#node/assertNodeVersion';\n\n// the edge bundle never reaches this entry\nassertNodeVersion(process.env.PACKAGE_NODE_RANGE ?? '', process.version);\n\nexport * from '#node/Lifecycle';\nexport * from '#node/HealthCheck';\nexport { HealthResponder } from '#node/HealthResponder';\nexport { settleWithin } from '#node/Lifecycle/withTimeout';\nexport { SubscriberLoader } from '#node/subscribers/SubscriberLoader';\nexport { CommandRegistry } from '#node/commands/CommandRegistry';\nexport { shutdownOf } from '#node/Pluggable';\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import { i as CoordinatedShutdown, r as CoordinatedStartup, t as Pluggable } from "./Pluggable-BKe2uCa0.mjs";
1
+ import { i as CoordinatedShutdown, r as CoordinatedStartup, t as Pluggable } from "./Pluggable-CqKvMXm5.mjs";
2
2
  export { CoordinatedShutdown, CoordinatedStartup, Pluggable };
@@ -1,7 +1,7 @@
1
1
  import { n as StartupPhase, t as ShutdownPhase } from "./phases-BauKJOTT.mjs";
2
- import { a as withTimeout, t as Pluggable } from "./Pluggable-CtMiOP3p.mjs";
2
+ import { a as withTimeout, i as settleWithin, t as Pluggable } from "./Pluggable-keys1cVr.mjs";
3
3
  import { SeedcordErrorCode, paint } from "@seedcord/errors";
4
- import { SeedcordAggregateError, SeedcordError, SeedcordTypeError } from "@seedcord/errors/internal";
4
+ import { SeedcordAggregateError, SeedcordError, SeedcordRangeError, SeedcordTypeError } from "@seedcord/errors/internal";
5
5
  import { Logger } from "@seedcord/logger";
6
6
 
7
7
  //#region src/node/Lifecycle/CoordinatedLifecycle.ts
@@ -77,6 +77,7 @@ const PHASE_ORDER$1 = [
77
77
  4
78
78
  ];
79
79
  const LOG_FLUSH_DELAY_MS = 3e3;
80
+ const DEFAULT_SHUTDOWN_DEADLINE_MS = 25e3;
80
81
  var CoordinatedShutdown = class extends CoordinatedLifecycle {
81
82
  isShuttingDown = false;
82
83
  hasShutdown = false;
@@ -84,10 +85,31 @@ var CoordinatedShutdown = class extends CoordinatedLifecycle {
84
85
  onSigTerm = null;
85
86
  onSigInt = null;
86
87
  startupGate;
87
- constructor() {
88
+ deadlineMs = DEFAULT_SHUTDOWN_DEADLINE_MS;
89
+ phasesExpireAt = Infinity;
90
+ runningPhase;
91
+ constructor(deadlineMs) {
88
92
  super("Shutdown", PHASE_ORDER$1, ShutdownPhase);
93
+ if (deadlineMs !== void 0) this.setDeadline(deadlineMs);
89
94
  this.registerSignalHandlers();
90
95
  }
96
+ /** @internal */
97
+ setDeadline(deadlineMs) {
98
+ if (!Number.isFinite(deadlineMs) || deadlineMs <= 0) throw new SeedcordRangeError(SeedcordErrorCode.LifecycleInvalidShutdownDeadline, [deadlineMs]);
99
+ this.deadlineMs = deadlineMs;
100
+ }
101
+ async runPhases(failures) {
102
+ for (const phase of PHASE_ORDER$1) {
103
+ this.runningPhase = phase;
104
+ if (performance.now() >= this.phasesExpireAt) return;
105
+ try {
106
+ await this.runPhase(phase);
107
+ } catch (error) {
108
+ failures.push(error);
109
+ }
110
+ }
111
+ this.runningPhase = void 0;
112
+ }
91
113
  canAddTask() {
92
114
  return true;
93
115
  }
@@ -147,15 +169,14 @@ var CoordinatedShutdown = class extends CoordinatedLifecycle {
147
169
  this.exitCode = exitCode;
148
170
  this.logger.info(`${paint.amber.bold("Starting")} coordinated shutdown with exit code ${paint.sky.bold(exitCode)}`);
149
171
  try {
150
- if (this.startupGate) await this.startupGate;
172
+ this.phasesExpireAt = performance.now() + this.deadlineMs;
173
+ if (this.startupGate) await settleWithin(this.startupGate, this.deadlineMs);
151
174
  const failures = [];
152
- for (const phase of PHASE_ORDER$1) try {
153
- await this.runPhase(phase);
154
- } catch (error) {
155
- failures.push(error);
156
- }
175
+ await settleWithin(this.runPhases(failures), Math.max(this.phasesExpireAt - performance.now(), 0));
176
+ const caughtPhase = this.runningPhase;
177
+ if (caughtPhase !== void 0) this.logger.error(`Shutdown deadline of ${paint.sky.bold(this.deadlineMs)}ms elapsed at phase ${paint.iris.bold(this.phaseEnum[caughtPhase])}`);
157
178
  if (failures.length > 0) this.logger.error(`${paint.coral.bold("Coordinated shutdown failed")}`, ...failures);
158
- else this.logger.info(`${paint.mint.bold("Coordinated shutdown completed")} successfully`);
179
+ else if (caughtPhase === void 0) this.logger.info(`${paint.mint.bold("Coordinated shutdown completed")} successfully`);
159
180
  } finally {
160
181
  this.hasShutdown = true;
161
182
  if (exitProcess) {
@@ -1 +1 @@
1
- {"version":3,"file":"node.index.mjs","names":["PHASE_ORDER"],"sources":["../src/node/Lifecycle/CoordinatedLifecycle.ts","../src/node/Lifecycle/CoordinatedShutdown.ts","../src/node/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\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: () => Promise<void>, 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 this.logger.debug(\n `${paint.italic('Added')} ${this.getTaskType()} task ${paint.sky.bold(taskName)} to phase ${paint.iris.bold(this.phaseEnum[phase])}`\n );\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 const removed = initialLength !== filteredTasks.length;\n if (removed) {\n this.logger.debug(\n `${paint.italic('Removed')} ${this.getTaskType()} task ${paint.sky.bold(taskName)} from phase ${paint.iris.bold(this.phaseEnum[phase])}`\n );\n }\n\n return removed;\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 try {\n await withTimeout(task.name, task.task, 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 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 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 { paint } from '@seedcord/errors';\n\nimport { ShutdownPhase } from '#src/lifecycle/phases';\n\nimport { CoordinatedLifecycle } from './CoordinatedLifecycle';\n\nimport type { LifecycleTask } from './LifecycleTypes';\n\nconst PHASE_ORDER: ShutdownPhase[] = [\n ShutdownPhase.Unbind,\n ShutdownPhase.Drain,\n ShutdownPhase.Disconnect,\n ShutdownPhase.Logout\n];\n\n// gives the logger's file sink time to flush before process.exit\nconst LOG_FLUSH_DELAY_MS = 3000;\n\nexport class CoordinatedShutdown extends CoordinatedLifecycle<ShutdownPhase> {\n private isShuttingDown = false;\n private hasShutdown = false;\n private exitCode = 0;\n private onSigTerm: (() => void) | null = null;\n private onSigInt: (() => void) | null = null;\n private startupGate?: Promise<void>;\n\n public constructor() {\n super('Shutdown', PHASE_ORDER, ShutdownPhase);\n\n this.registerSignalHandlers();\n }\n\n protected canAddTask(): boolean {\n return true;\n }\n\n protected canRemoveTask(): boolean {\n return true;\n }\n\n protected getTaskType(): string {\n return 'shutdown';\n }\n\n protected async executeTasksInPhase(\n phase: ShutdownPhase,\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 private registerSignalHandlers(): void {\n this.onSigTerm = () => {\n this.logger.info(`Received ${paint.amber.bold('SIGTERM')} signal`);\n void this.run(0);\n };\n\n this.onSigInt = () => {\n this.logger.info(`Received ${paint.amber.bold('SIGINT')} signal`);\n void this.run(0);\n };\n\n process.on('SIGTERM', this.onSigTerm);\n process.on('SIGINT', this.onSigInt);\n }\n\n /** @internal */\n public removeSignalHandlers(): void {\n if (this.onSigTerm) {\n process.off('SIGTERM', this.onSigTerm);\n this.onSigTerm = null;\n }\n if (this.onSigInt) {\n process.off('SIGINT', this.onSigInt);\n this.onSigInt = null;\n }\n }\n\n /** @internal run() awaits this so boot finishes registering its dispose tasks first */\n public gateOnStartup(settled: Promise<void>): void {\n this.startupGate = settled;\n }\n\n public override addTask(phase: ShutdownPhase, taskName: string, task: () => Promise<void>, timeoutMs = 5000): void {\n super.addTask(phase, taskName, task, timeoutMs);\n }\n\n /** @internal */\n public override removeTask(phase: ShutdownPhase, taskName: string): boolean {\n return super.removeTask(phase, taskName);\n }\n\n /** @internal */\n public async run(exitCode = 0, exitProcess = true): Promise<void> {\n this.removeSignalHandlers();\n\n // a dev-mode run leaves the process alive, so a second call would re-execute every task\n if (this.hasShutdown || this.isShuttingDown) {\n // a crash mid-shutdown must still leave a failing code for whatever supervises the process\n if (exitCode > this.exitCode) this.exitCode = exitCode;\n this.logger.warn('Shutdown sequence already ran or is in progress');\n return;\n }\n\n this.isShuttingDown = true;\n this.exitCode = exitCode;\n this.logger.info(\n `${paint.amber.bold('Starting')} coordinated shutdown with exit code ${paint.sky.bold(exitCode)}`\n );\n\n try {\n if (this.startupGate) await this.startupGate;\n const failures: unknown[] = [];\n for (const phase of PHASE_ORDER) {\n // run every phase so a mid-shutdown failure still attempts the later teardowns\n try {\n await this.runPhase(phase);\n } catch (error) {\n failures.push(error);\n }\n }\n\n if (failures.length > 0) {\n this.logger.error(`${paint.coral.bold('Coordinated shutdown failed')}`, ...failures);\n } else {\n this.logger.info(`${paint.mint.bold('Coordinated shutdown completed')} successfully`);\n }\n } finally {\n this.hasShutdown = true;\n if (exitProcess) {\n this.logger.debug(`${paint.coral.bold('Exiting')} process with code ${paint.sky.bold(this.exitCode)}`);\n setTimeout(() => {\n process.exit(this.exitCode);\n }, LOG_FLUSH_DELAY_MS);\n } else {\n this.logger.debug(`${paint.amber.bold('Skipping')} process exit (dev mode)`);\n this.isShuttingDown = false;\n }\n }\n }\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';\n\nconst PHASE_ORDER: StartupPhase[] = [StartupPhase.Configuration, StartupPhase.Login, StartupPhase.Ready];\n\nexport class CoordinatedStartup extends CoordinatedLifecycle<StartupPhase> {\n private isStartingUp = false;\n private hasStarted = false;\n\n public constructor() {\n super('Startup', PHASE_ORDER, StartupPhase);\n }\n\n public override addTask(\n phase: StartupPhase,\n taskName: string,\n task: () => Promise<void>,\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.LifecycleAddAfterCompletion);\n }\n\n if (this.isStartingUp) {\n throw new SeedcordError(SeedcordErrorCode.LifecycleAddDuringRun);\n }\n\n return true;\n }\n\n protected canRemoveTask(): boolean {\n if (this.isStartingUp) {\n throw new SeedcordError(SeedcordErrorCode.LifecycleRemoveDuringRun);\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(): 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.logger.info(`${paint.mint.bold('Starting')} coordinated startup sequence`);\n\n try {\n for (const phase of PHASE_ORDER) {\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 await this.runPhase(phase);\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.logger.warn('Aborting coordinated startup sequence');\n }\n\n /** @internal */\n public get isReady(): boolean {\n return this.hasStarted;\n }\n}\n"],"mappings":";;;;;;;AAcA,IAAsB,uBAAtB,MAAkE;CAMvC;CACA;CANvB,AAAmB;CACnB,AAAmB,2BAAW,IAAI,IAA6B;CAE/D,AAAU,YACN,YACA,AAAmB,YACnB,AAAmB,WACrB;EAFqB;EACA;EAEnB,KAAK,SAAS,IAAI,OAAO,YAAY,EAAE,SAAS,YAAY,CAAC;EAC7D,KAAK,WAAW,SAAS,UAAU,KAAK,SAAS,IAAI,OAAO,CAAC,CAAC,CAAC;CACnE;CAEA,AAAO,QAAQ,OAAe,UAAkB,MAA2B,WAAyB;EAChG,IAAI,CAAC,KAAK,WAAW,GAAG;EAExB,MAAM,QAAQ,KAAK,SAAS,IAAI,KAAK;EACrC,IAAI,CAAC,OACD,MAAM,IAAI,kBAAkB,kBAAkB,uBAAuB,CAAC,KAAK,CAAC;EAGhF,MAAM,KAAK;GAAE,MAAM;GAAU;GAAM,SAAS;EAAU,CAAC;EACvD,KAAK,OAAO,MACR,GAAG,MAAM,OAAO,OAAO,EAAE,GAAG,KAAK,YAAY,EAAE,QAAQ,MAAM,IAAI,KAAK,QAAQ,EAAE,YAAY,MAAM,KAAK,KAAK,KAAK,UAAU,MAAM,GACrI;CACJ;CAEA,AAAO,WAAW,OAAe,UAA2B;EACxD,IAAI,CAAC,KAAK,cAAc,GAAG,OAAO;EAElC,MAAM,QAAQ,KAAK,SAAS,IAAI,KAAK;EACrC,IAAI,CAAC,OAAO,OAAO;EAEnB,MAAM,gBAAgB,MAAM;EAC5B,MAAM,gBAAgB,MAAM,QAAQ,SAAS,KAAK,SAAS,QAAQ;EACnE,KAAK,SAAS,IAAI,OAAO,aAAa;EAEtC,MAAM,UAAU,kBAAkB,cAAc;EAChD,IAAI,SACA,KAAK,OAAO,MACR,GAAG,MAAM,OAAO,SAAS,EAAE,GAAG,KAAK,YAAY,EAAE,QAAQ,MAAM,IAAI,KAAK,QAAQ,EAAE,cAAc,MAAM,KAAK,KAAK,KAAK,UAAU,MAAM,GACzI;EAGJ,OAAO;CACX;CAEA,MAAgB,SAAS,OAA8B;EACnD,MAAM,QAAQ,KAAK,SAAS,IAAI,KAAK,KAAK,CAAC;EAC3C,IAAI,MAAM,WAAW,GAAG;GACpB,KAAK,OAAO,MAAM,4BAA4B,MAAM,KAAK,KAAK,KAAK,UAAU,MAAM,GAAG;GACtF;EACJ;EAEA,KAAK,OAAO,MACR,GAAG,MAAM,MAAM,KAAK,SAAS,EAAE,GAAG,KAAK,YAAY,EAAE,SAAS,MAAM,KAAK,KAAK,KAAK,UAAU,MAAM,EAAE,QAAQ,MAAM,IAAI,KAAK,OAAO,MAAM,MAAM,CAAC,EAAE,OACtJ;EAIA,MAAM,WAAU,MAFoC,KAAK,oBAAoB,OAAO,KAAK,EAElE,CAAC,QAAmB,UAAU,WAAW;GAC5D,IAAI,OAAO,WAAW,YAAY,SAAS,KAAK,OAAO,MAAiB;GACxE,OAAO;EACX,GAAG,CAAC,CAAC;EACL,IAAI,QAAQ,SAAS,GAEjB,MAAM,IAAI,uBAAuB,kBAAkB,wBAAwB,SAAS,CAChF,KAAK,UAAU,QACf,QAAQ,MACZ,CAAC;EAGL,KAAK,OAAO,MACR,SAAS,MAAM,KAAK,KAAK,KAAK,UAAU,MAAM,EAAE,GAAG,MAAM,KAAK,KAAK,wBAAwB,GAC/F;CACJ;CAEA,MAAgB,mBAAmB,OAAe,MAAoC;EAClF,KAAK,OAAO,MACR,GAAG,MAAM,OAAO,UAAU,EAAE,QAAQ,MAAM,IAAI,KAAK,KAAK,IAAI,EAAE,YAAY,MAAM,KAAK,KAAK,KAAK,UAAU,MAAM,GACnH;EAEA,IAAI;GACA,MAAM,YAAY,KAAK,MAAM,KAAK,MAAM,KAAK,OAAO;GAEpD,KAAK,OAAO,MACR,GAAG,MAAM,OAAO,WAAW,EAAE,QAAQ,MAAM,IAAI,KAAK,KAAK,IAAI,EAAE,YAAY,MAAM,KAAK,KAAK,KAAK,UAAU,MAAM,GACpH;EACJ,SAAS,OAAO;GACZ,IAAI,KAAK,UAAU,GAAG;GACtB,KAAK,OAAO,MACR,GAAG,MAAM,OAAO,QAAQ,EAAE,QAAQ,MAAM,IAAI,KAAK,KAAK,IAAI,EAAE,YAAY,MAAM,KAAK,KAAK,KAAK,UAAU,MAAM,EAAE,IAC/G,KACJ;GACA,MAAM;EACV;CACJ;CAEA,AAAU,YAAqB;EAC3B,OAAO;CACX;AASJ;;;;ACnHA,MAAMA,gBAA+B;;;;;AAKrC;AAGA,MAAM,qBAAqB;AAE3B,IAAa,sBAAb,cAAyC,qBAAoC;CACzE,AAAQ,iBAAiB;CACzB,AAAQ,cAAc;CACtB,AAAQ,WAAW;CACnB,AAAQ,YAAiC;CACzC,AAAQ,WAAgC;CACxC,AAAQ;CAER,AAAO,cAAc;EACjB,MAAM,YAAYA,eAAa,aAAa;EAE5C,KAAK,uBAAuB;CAChC;CAEA,AAAU,aAAsB;EAC5B,OAAO;CACX;CAEA,AAAU,gBAAyB;EAC/B,OAAO;CACX;CAEA,AAAU,cAAsB;EAC5B,OAAO;CACX;CAEA,MAAgB,oBACZ,OACA,OACqC;EACrC,MAAM,WAAW,MAAM,KAAK,SAAS,KAAK,mBAAmB,OAAO,IAAI,CAAC;EACzE,OAAO,QAAQ,WAAW,QAAQ;CACtC;CAEA,AAAQ,yBAA+B;EACnC,KAAK,kBAAkB;GACnB,KAAK,OAAO,KAAK,YAAY,MAAM,MAAM,KAAK,SAAS,EAAE,QAAQ;GACjE,AAAK,KAAK,IAAI,CAAC;EACnB;EAEA,KAAK,iBAAiB;GAClB,KAAK,OAAO,KAAK,YAAY,MAAM,MAAM,KAAK,QAAQ,EAAE,QAAQ;GAChE,AAAK,KAAK,IAAI,CAAC;EACnB;EAEA,QAAQ,GAAG,WAAW,KAAK,SAAS;EACpC,QAAQ,GAAG,UAAU,KAAK,QAAQ;CACtC;;CAGA,AAAO,uBAA6B;EAChC,IAAI,KAAK,WAAW;GAChB,QAAQ,IAAI,WAAW,KAAK,SAAS;GACrC,KAAK,YAAY;EACrB;EACA,IAAI,KAAK,UAAU;GACf,QAAQ,IAAI,UAAU,KAAK,QAAQ;GACnC,KAAK,WAAW;EACpB;CACJ;;CAGA,AAAO,cAAc,SAA8B;EAC/C,KAAK,cAAc;CACvB;CAEA,AAAgB,QAAQ,OAAsB,UAAkB,MAA2B,YAAY,KAAY;EAC/G,MAAM,QAAQ,OAAO,UAAU,MAAM,SAAS;CAClD;;CAGA,AAAgB,WAAW,OAAsB,UAA2B;EACxE,OAAO,MAAM,WAAW,OAAO,QAAQ;CAC3C;;CAGA,MAAa,IAAI,WAAW,GAAG,cAAc,MAAqB;EAC9D,KAAK,qBAAqB;EAG1B,IAAI,KAAK,eAAe,KAAK,gBAAgB;GAEzC,IAAI,WAAW,KAAK,UAAU,KAAK,WAAW;GAC9C,KAAK,OAAO,KAAK,iDAAiD;GAClE;EACJ;EAEA,KAAK,iBAAiB;EACtB,KAAK,WAAW;EAChB,KAAK,OAAO,KACR,GAAG,MAAM,MAAM,KAAK,UAAU,EAAE,uCAAuC,MAAM,IAAI,KAAK,QAAQ,GAClG;EAEA,IAAI;GACA,IAAI,KAAK,aAAa,MAAM,KAAK;GACjC,MAAM,WAAsB,CAAC;GAC7B,KAAK,MAAM,SAASA,eAEhB,IAAI;IACA,MAAM,KAAK,SAAS,KAAK;GAC7B,SAAS,OAAO;IACZ,SAAS,KAAK,KAAK;GACvB;GAGJ,IAAI,SAAS,SAAS,GAClB,KAAK,OAAO,MAAM,GAAG,MAAM,MAAM,KAAK,6BAA6B,KAAK,GAAG,QAAQ;QAEnF,KAAK,OAAO,KAAK,GAAG,MAAM,KAAK,KAAK,gCAAgC,EAAE,cAAc;EAE5F,UAAU;GACN,KAAK,cAAc;GACnB,IAAI,aAAa;IACb,KAAK,OAAO,MAAM,GAAG,MAAM,MAAM,KAAK,SAAS,EAAE,qBAAqB,MAAM,IAAI,KAAK,KAAK,QAAQ,GAAG;IACrG,iBAAiB;KACb,QAAQ,KAAK,KAAK,QAAQ;IAC9B,GAAG,kBAAkB;GACzB,OAAO;IACH,KAAK,OAAO,MAAM,GAAG,MAAM,MAAM,KAAK,UAAU,EAAE,yBAAyB;IAC3E,KAAK,iBAAiB;GAC1B;EACJ;CACJ;AACJ;;;;ACpIA,MAAM,cAA8B;;;;AAAmE;AAEvG,IAAa,qBAAb,cAAwC,qBAAmC;CACvE,AAAQ,eAAe;CACvB,AAAQ,aAAa;CAErB,AAAO,cAAc;EACjB,MAAM,WAAW,aAAa,YAAY;CAC9C;CAEA,AAAgB,QACZ,OACA,UACA,MACA,YAAY,KACR;EACJ,MAAM,QAAQ,OAAO,UAAU,MAAM,SAAS;CAClD;CAEA,AAAU,aAAsB;EAC5B,IAAI,KAAK,YACL,MAAM,IAAI,cAAc,kBAAkB,2BAA2B;EAGzE,IAAI,KAAK,cACL,MAAM,IAAI,cAAc,kBAAkB,qBAAqB;EAGnE,OAAO;CACX;CAEA,AAAU,gBAAyB;EAC/B,IAAI,KAAK,cACL,MAAM,IAAI,cAAc,kBAAkB,wBAAwB;EAGtE,OAAO;CACX;CAEA,AAAU,cAAsB;EAC5B,OAAO;CACX;CAEA,MAAgB,oBACZ,OACA,OACqC;EACrC,MAAM,WAAW,MAAM,KAAK,SAAS,KAAK,mBAAmB,OAAO,IAAI,CAAC;EACzE,OAAO,QAAQ,WAAW,QAAQ;CACtC;;CAIA,MAAa,MAAqB;EAC9B,IAAI,KAAK,YAAY;GACjB,KAAK,OAAO,KAAK,wCAAwC;GACzD;EACJ;EAEA,IAAI,KAAK,cAAc;GACnB,KAAK,OAAO,KAAK,sCAAsC;GACvD;EACJ;EAEA,KAAK,eAAe;EACpB,KAAK,OAAO,KAAK,GAAG,MAAM,KAAK,KAAK,UAAU,EAAE,8BAA8B;EAE9E,IAAI;GACA,KAAK,MAAM,SAAS,aAAa;IAE7B,IAAI,CAAC,KAAK,cAAc;KACpB,KAAK,OAAO,KAAK,0BAA0B;KAC3C;IACJ;IACA,MAAM,KAAK,SAAS,KAAK;GAC7B;GAEA,KAAK,aAAa;GAClB,KAAK,OAAO,KAAK,GAAG,MAAM,KAAK,KAAK,+BAA+B,EAAE,cAAc;EACvF,SAAS,OAAO;GAEZ,IAAI,CAAC,KAAK,cAAc;IACpB,KAAK,OAAO,KAAK,gDAAgD;IACjE;GACJ;GACA,KAAK,OAAO,MAAM,GAAG,MAAM,MAAM,KAAK,4BAA4B,GAAG;GACrE,MAAM;EACV,UAAU;GACN,KAAK,eAAe;EACxB;CACJ;CAEA,AAAmB,YAAqB;EACpC,OAAO,CAAC,KAAK;CACjB;;CAGA,AAAO,QAAc;EACjB,IAAI,CAAC,KAAK,cAAc;EAExB,KAAK,eAAe;EACpB,KAAK,OAAO,KAAK,uCAAuC;CAC5D;;CAGA,IAAW,UAAmB;EAC1B,OAAO,KAAK;CAChB;AACJ"}
1
+ {"version":3,"file":"node.index.mjs","names":["PHASE_ORDER"],"sources":["../src/node/Lifecycle/CoordinatedLifecycle.ts","../src/node/Lifecycle/CoordinatedShutdown.ts","../src/node/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\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: () => Promise<void>, 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 this.logger.debug(\n `${paint.italic('Added')} ${this.getTaskType()} task ${paint.sky.bold(taskName)} to phase ${paint.iris.bold(this.phaseEnum[phase])}`\n );\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 const removed = initialLength !== filteredTasks.length;\n if (removed) {\n this.logger.debug(\n `${paint.italic('Removed')} ${this.getTaskType()} task ${paint.sky.bold(taskName)} from phase ${paint.iris.bold(this.phaseEnum[phase])}`\n );\n }\n\n return removed;\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 try {\n await withTimeout(task.name, task.task, 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 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 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 { SeedcordRangeError } from '@seedcord/errors/internal';\n\nimport { ShutdownPhase } from '#src/lifecycle/phases';\n\nimport { CoordinatedLifecycle } from './CoordinatedLifecycle';\nimport { settleWithin } from './withTimeout';\n\nimport type { LifecycleTask } from './LifecycleTypes';\n\nconst PHASE_ORDER: ShutdownPhase[] = [\n ShutdownPhase.Unbind,\n ShutdownPhase.Drain,\n ShutdownPhase.Disconnect,\n ShutdownPhase.Logout\n];\n\n// gives the logger's file sink time to flush before process.exit\nconst LOG_FLUSH_DELAY_MS = 3000;\n\n// 25s plus LOG_FLUSH_DELAY_MS stays under kubernetes' 30s SIGKILL window\nconst DEFAULT_SHUTDOWN_DEADLINE_MS = 25_000;\n\nexport class CoordinatedShutdown extends CoordinatedLifecycle<ShutdownPhase> {\n private isShuttingDown = false;\n private hasShutdown = false;\n private exitCode = 0;\n private onSigTerm: (() => void) | null = null;\n private onSigInt: (() => void) | null = null;\n private startupGate?: Promise<void>;\n private deadlineMs = DEFAULT_SHUTDOWN_DEADLINE_MS;\n private phasesExpireAt = Infinity;\n private runningPhase: ShutdownPhase | undefined;\n\n // the signal handlers below can fire before start() ever runs\n public constructor(deadlineMs?: number) {\n super('Shutdown', PHASE_ORDER, ShutdownPhase);\n\n if (deadlineMs !== undefined) this.setDeadline(deadlineMs);\n this.registerSignalHandlers();\n }\n\n /** @internal */\n public setDeadline(deadlineMs: number): void {\n if (!Number.isFinite(deadlineMs) || deadlineMs <= 0) {\n throw new SeedcordRangeError(SeedcordErrorCode.LifecycleInvalidShutdownDeadline, [deadlineMs]);\n }\n this.deadlineMs = deadlineMs;\n }\n\n private async runPhases(failures: unknown[]): Promise<void> {\n for (const phase of PHASE_ORDER) {\n // set above the check so the reported phase is the one that never started\n this.runningPhase = phase;\n // runPhases keeps going after settleWithin stops waiting on it\n if (performance.now() >= this.phasesExpireAt) return;\n try {\n await this.runPhase(phase);\n } catch (error) {\n failures.push(error);\n }\n }\n this.runningPhase = undefined;\n }\n\n protected canAddTask(): boolean {\n return true;\n }\n\n protected canRemoveTask(): boolean {\n return true;\n }\n\n protected getTaskType(): string {\n return 'shutdown';\n }\n\n protected async executeTasksInPhase(\n phase: ShutdownPhase,\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 private registerSignalHandlers(): void {\n this.onSigTerm = () => {\n this.logger.info(`Received ${paint.amber.bold('SIGTERM')} signal`);\n void this.run(0);\n };\n\n this.onSigInt = () => {\n this.logger.info(`Received ${paint.amber.bold('SIGINT')} signal`);\n void this.run(0);\n };\n\n process.on('SIGTERM', this.onSigTerm);\n process.on('SIGINT', this.onSigInt);\n }\n\n /** @internal */\n public removeSignalHandlers(): void {\n if (this.onSigTerm) {\n process.off('SIGTERM', this.onSigTerm);\n this.onSigTerm = null;\n }\n if (this.onSigInt) {\n process.off('SIGINT', this.onSigInt);\n this.onSigInt = null;\n }\n }\n\n /** @internal run() awaits this so boot finishes registering its dispose tasks first */\n public gateOnStartup(settled: Promise<void>): void {\n this.startupGate = settled;\n }\n\n public override addTask(phase: ShutdownPhase, taskName: string, task: () => Promise<void>, timeoutMs = 5000): void {\n super.addTask(phase, taskName, task, timeoutMs);\n }\n\n /** @internal */\n public override removeTask(phase: ShutdownPhase, taskName: string): boolean {\n return super.removeTask(phase, taskName);\n }\n\n /** @internal */\n public async run(exitCode = 0, exitProcess = true): Promise<void> {\n this.removeSignalHandlers();\n\n // a dev-mode run leaves the process alive, so a second call would re-execute every task\n if (this.hasShutdown || this.isShuttingDown) {\n // a crash mid-shutdown must still leave a failing code for whatever supervises the process\n if (exitCode > this.exitCode) this.exitCode = exitCode;\n this.logger.warn('Shutdown sequence already ran or is in progress');\n return;\n }\n\n this.isShuttingDown = true;\n this.exitCode = exitCode;\n this.logger.info(\n `${paint.amber.bold('Starting')} coordinated shutdown with exit code ${paint.sky.bold(exitCode)}`\n );\n\n try {\n this.phasesExpireAt = performance.now() + this.deadlineMs;\n // a startup that outlasts the deadline leaves its own dispose tasks unregistered\n if (this.startupGate) await settleWithin(this.startupGate, this.deadlineMs);\n\n const failures: unknown[] = [];\n await settleWithin(this.runPhases(failures), Math.max(this.phasesExpireAt - performance.now(), 0));\n\n const caughtPhase = this.runningPhase;\n if (caughtPhase !== undefined) {\n this.logger.error(\n `Shutdown deadline of ${paint.sky.bold(this.deadlineMs)}ms elapsed at phase ${paint.iris.bold(this.phaseEnum[caughtPhase])}`\n );\n }\n\n if (failures.length > 0) {\n this.logger.error(`${paint.coral.bold('Coordinated shutdown failed')}`, ...failures);\n } else if (caughtPhase === undefined) {\n this.logger.info(`${paint.mint.bold('Coordinated shutdown completed')} successfully`);\n }\n } finally {\n this.hasShutdown = true;\n if (exitProcess) {\n this.logger.debug(`${paint.coral.bold('Exiting')} process with code ${paint.sky.bold(this.exitCode)}`);\n setTimeout(() => {\n process.exit(this.exitCode);\n }, LOG_FLUSH_DELAY_MS);\n } else {\n this.logger.debug(`${paint.amber.bold('Skipping')} process exit (dev mode)`);\n this.isShuttingDown = false;\n }\n }\n }\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';\n\nconst PHASE_ORDER: StartupPhase[] = [StartupPhase.Configuration, StartupPhase.Login, StartupPhase.Ready];\n\nexport class CoordinatedStartup extends CoordinatedLifecycle<StartupPhase> {\n private isStartingUp = false;\n private hasStarted = false;\n\n public constructor() {\n super('Startup', PHASE_ORDER, StartupPhase);\n }\n\n public override addTask(\n phase: StartupPhase,\n taskName: string,\n task: () => Promise<void>,\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.LifecycleAddAfterCompletion);\n }\n\n if (this.isStartingUp) {\n throw new SeedcordError(SeedcordErrorCode.LifecycleAddDuringRun);\n }\n\n return true;\n }\n\n protected canRemoveTask(): boolean {\n if (this.isStartingUp) {\n throw new SeedcordError(SeedcordErrorCode.LifecycleRemoveDuringRun);\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(): 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.logger.info(`${paint.mint.bold('Starting')} coordinated startup sequence`);\n\n try {\n for (const phase of PHASE_ORDER) {\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 await this.runPhase(phase);\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.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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"}
@@ -2,8 +2,8 @@ import { APIApplicationCommand } from "discord-api-types/v10";
2
2
  import { Logger } from "@seedcord/logger";
3
3
  import { TypedEventEmitter } from "@seedcord/event-emitter";
4
4
  import { REST } from "@discordjs/rest";
5
- import { UUID } from "node:crypto";
6
- import { Config, EventFrequency, IRateLimiter, RenderContext, ReplyResponse, TypedExclude } from "@seedcord/types";
5
+ import { UUID } from "@seedcord/types/internal";
6
+ import { Config, DispatchBag, DispatchState, EventFrequency, IRateLimiter, RenderContext, ReplyResponse, TypedExclude } from "@seedcord/types";
7
7
  //#region src/stops/Notice.d.ts
8
8
  /**
9
9
  * Base class for a user-facing refusal or a reported fault.
@@ -88,15 +88,41 @@ declare enum InteractionKind {
88
88
  UserContextMenu = "userContextMenu",
89
89
  Autocomplete = "autocomplete"
90
90
  }
91
+ /**
92
+ * The interaction kinds a middleware can filter on. Autocomplete carries no reply target.
93
+ */
94
+ type MiddlewareKind = TypedExclude<InteractionKind, InteractionKind.Autocomplete>;
91
95
  declare const CommandMetadataKey: unique symbol;
92
96
  declare const InteractionMetadataKey: unique symbol;
93
- declare const MiddlewareMetadataKey: unique symbol;
97
+ declare const InteractionMiddlewareMetadataKey: unique symbol;
98
+ declare const EventMiddlewareMetadataKey: unique symbol;
94
99
  declare const SubscribeMetadataKey: unique symbol;
95
100
  declare const EventMetadataKey: unique symbol;
96
101
  declare const GatedMetadataKey: unique symbol;
97
102
  declare const WebhookUrlMetadataKey: unique symbol;
98
103
  declare const InteractionRouteKeys: Record<InteractionKind, symbol>;
99
104
  //#endregion
105
+ //#region src/dispatch/DispatchContext.d.ts
106
+ /**
107
+ * A typed bag allocated once per interaction dispatch and handed to the handler. The middleware and i18n
108
+ * work reads and writes it through {@link DispatchState}. Today the bag is empty, so it only carries the
109
+ * dispatched `routeId`.
110
+ */
111
+ declare class DispatchContext implements DispatchBag {
112
+ readonly routeId: string;
113
+ private readonly state;
114
+ readonly id: `${string}-${string}-${string}-${string}-${string}`;
115
+ constructor(routeId: string);
116
+ set<Key extends keyof DispatchState>(key: Key, value: DispatchState[Key]): void;
117
+ get<Key extends keyof DispatchState>(key: Key): DispatchState[Key] | undefined;
118
+ /**
119
+ * Read a key that must be present, with `undefined` stripped from the result.
120
+ *
121
+ * @throws A **SeedcordError** If nothing set the key on this dispatch.
122
+ */
123
+ require<Key extends keyof DispatchState>(key: Key): Exclude<DispatchState[Key], undefined>;
124
+ }
125
+ //#endregion
100
126
  //#region src/subscribers/publishDefault.d.ts
101
127
  /** @internal PublishableKey excludes this key so publish() never emits it externally. */
102
128
  declare const PublishDefault: unique symbol;
@@ -122,10 +148,11 @@ declare function sendTarget(state: AckState): 'reply' | 'edit' | 'followUp';
122
148
  //#region src/reply/responseReport.d.ts
123
149
  interface ReplyTelemetry {
124
150
  readonly bus: Bus;
151
+ readonly dispatch: DispatchContext;
125
152
  readonly interactionId: string;
126
153
  }
127
154
  type WriteMethod = ReplyMethod | 'respond';
128
- declare function reportedWrite<Result>(telemetry: ReplyTelemetry, routeId: string, method: WriteMethod, write: () => Promise<Result>): Promise<Result>;
155
+ declare function reportedWrite<Result>(telemetry: ReplyTelemetry, method: WriteMethod, write: () => Promise<Result>): Promise<Result>;
129
156
  //#endregion
130
157
  //#region src/subscribers/types/Subscriptions.d.ts
131
158
  /**
@@ -176,6 +203,8 @@ interface EventFaultSource {
176
203
  * The fields both `responseAttempted` arms carry.
177
204
  */
178
205
  interface AttemptedWrite {
206
+ /** The dispatch this write belongs to, for joining against every other key it published. */
207
+ readonly dispatchId: string;
179
208
  readonly routeId: string;
180
209
  /** The interaction this write belongs to, for joining against `interactionDispatched`. */
181
210
  readonly interactionId: string;
@@ -216,9 +245,11 @@ interface DefaultSubscriptions {
216
245
  /** Triggered when an unhandled exception (a raw non-Notice throw) occurs. */
217
246
  readonly unknownException: {
218
247
  readonly uuid: UUID;
248
+ /** The dispatch that raised it. Null on a process-level throw, which has no dispatch behind it. */
249
+ readonly dispatchId: string | null;
219
250
  readonly error: Error;
220
251
  /** Where the throw came from, `slash:ban` for an interaction and `event:name:handler` for an event. */
221
- readonly routeId: string;
252
+ readonly origin: string;
222
253
  readonly guild?: {
223
254
  readonly id: string;
224
255
  readonly name: string;
@@ -233,8 +264,10 @@ interface DefaultSubscriptions {
233
264
  readonly handledException: {
234
265
  readonly denial: Notice;
235
266
  readonly uuid: UUID;
236
- /** The same id `interactionDispatched` publishes. An event reads `event:name:handler`. */
237
- readonly routeId: string;
267
+ /** The dispatch that reported it. */
268
+ readonly dispatchId: string;
269
+ /** Where the reported Notice came from, the same shape `unknownException.origin` carries. */
270
+ readonly origin: string;
238
271
  readonly source: FaultSource;
239
272
  };
240
273
  /** Triggered when an interaction dispatch throws past the fault boundary. */
@@ -243,6 +276,8 @@ interface DefaultSubscriptions {
243
276
  };
244
277
  /** Triggered once per interaction dispatch, after the handler chain settles. */
245
278
  readonly interactionDispatched: {
279
+ /** This dispatch, for joining against every other key it published. */
280
+ readonly dispatchId: string;
246
281
  readonly routeId: string;
247
282
  /** The interaction this dispatch ran, for joining against `responseAttempted`. */
248
283
  readonly interactionId: string;
@@ -250,6 +285,10 @@ interface DefaultSubscriptions {
250
285
  readonly outcome: DispatchOutcome;
251
286
  /** True when no route matched and the unhandled default ran. */
252
287
  readonly fallback: boolean;
288
+ /** Who ran the route. Null when the payload carried no user. */
289
+ readonly userId: string | null;
290
+ /** Guild the interaction came from, null in DMs. */
291
+ readonly guildId: string | null;
253
292
  /** Dispatch entry until the user has a response, replies included. A clock change never affects it. */
254
293
  readonly durationMs: number;
255
294
  /**
@@ -459,5 +498,5 @@ declare enum ShutdownPhase {
459
498
  Logout = 4
460
499
  }
461
500
  //#endregion
462
- export { UnregisterSubscriber as A, SubscribeMetadataKey as B, ReplyMethod as C, RegisterDefaults as D, AckTrace as E, GatedMetadataKey as F, Notice as H, InteractionKind as I, InteractionMetadataKey as L, PublishDefault as M, CommandMetadataKey as N, RegisterSubscriber as O, EventMetadataKey as P, InteractionRouteKeys as R, AckState as S, sendTarget as T, WebhookUrlMetadataKey as V, SubscriptionData as _, StoredSubscriberCtor as a, ReplyTelemetry as b, Subscriber as c, DispatchOutcome as d, FaultSource as f, ResponseSent as g, ResponseOutcome as h, Bus as i, VerifyWebhooks as j, RegisteredCount as k, AttemptedWrite as l, ResponseFailed as m, StartupPhase as n, SubscriberRegistration as o, ResponseAttempt as p, CoreBase as r, busLoggerOf as s, ShutdownPhase as t, DefaultSubscriptions as u, SubscriptionKey as v, checkAckLegality as w, reportedWrite as x, Subscriptions as y, MiddlewareMetadataKey as z };
463
- //# sourceMappingURL=phases-D0a8BV9Z.d.mts.map
501
+ export { UnregisterSubscriber as A, InteractionMiddlewareMetadataKey as B, ReplyMethod as C, RegisterDefaults as D, AckTrace as E, EventMetadataKey as F, Notice as G, MiddlewareKind as H, EventMiddlewareMetadataKey as I, GatedMetadataKey as L, PublishDefault as M, DispatchContext as N, RegisterSubscriber as O, CommandMetadataKey as P, InteractionKind as R, AckState as S, sendTarget as T, SubscribeMetadataKey as U, InteractionRouteKeys as V, WebhookUrlMetadataKey as W, SubscriptionData as _, StoredSubscriberCtor as a, ReplyTelemetry as b, Subscriber as c, DispatchOutcome as d, FaultSource as f, ResponseSent as g, ResponseOutcome as h, Bus as i, VerifyWebhooks as j, RegisteredCount as k, AttemptedWrite as l, ResponseFailed as m, StartupPhase as n, SubscriberRegistration as o, ResponseAttempt as p, CoreBase as r, busLoggerOf as s, ShutdownPhase as t, DefaultSubscriptions as u, SubscriptionKey as v, checkAckLegality as w, reportedWrite as x, Subscriptions as y, InteractionMetadataKey as z };
502
+ //# sourceMappingURL=phases-DJf12l5T.d.mts.map
@@ -1,3 +1,3 @@
1
- import { c as PluginOptions, p as PluginLifecycleSpec, r as Plugin } from "./Plugin-iAaWcLaD.mjs";
1
+ import { c as PluginOptions, p as PluginLifecycleSpec, r as Plugin } from "./Plugin-BdqfRjU0.mjs";
2
2
  import { HmrAware, HmrUpdateEvent } from "@seedcord/types";
3
3
  export { type HmrAware, type HmrUpdateEvent, Plugin, type PluginLifecycleSpec, type PluginOptions };
@@ -1,4 +1,4 @@
1
- import { g as CommandMetadataKey, j as isBuilderComponentClass } from "./Bus-dGy6xq3F.mjs";
1
+ import { M as isBuilderComponentClass, g as CommandMetadataKey } from "./Bus-Djm6688b.mjs";
2
2
  import { SlashCommandBuilder } from "@discordjs/builders";
3
3
  import { routeLeavesOf } from "@seedcord/utils/internal";
4
4
 
@@ -28,4 +28,4 @@ function slashRouteLeaves(commands) {
28
28
 
29
29
  //#endregion
30
30
  export { isCommandClass as n, slashRouteLeaves as t };
31
- //# sourceMappingURL=slashRouteLeaves-C1msUE9Q.mjs.map
31
+ //# sourceMappingURL=slashRouteLeaves-DTFHzCKN.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"slashRouteLeaves-C1msUE9Q.mjs","names":[],"sources":["../src/commands/isCommandClass.ts","../src/commands/slashRouteLeaves.ts"],"sourcesContent":["import { isBuilderComponentClass } from '#components/Component';\nimport { CommandMetadataKey } from '#src/metadataKeys';\n\nimport type { BuilderComponent } from '#components/Component';\n\n/** @internal */\nexport type CommandCtor = new () => BuilderComponent<'command' | 'context_menu'>;\n\n// the cli's codegen scan reads this too\n/** @internal */\nexport function isCommandClass(value: unknown): value is CommandCtor {\n return isBuilderComponentClass(value) && Reflect.hasMetadata(CommandMetadataKey, value);\n}\n","import { SlashCommandBuilder } from '@discordjs/builders';\nimport { routeLeavesOf } from '@seedcord/utils/internal';\n\nimport type { ContextMenuCommandBuilder } from '@discordjs/builders';\n\n/**\n * dedupes route keys since the same builder appears once per guild.\n * these come from the same {@link routeLeavesOf} that `seedcord codegen` reads, so the boot-time\n * check compares against the generated registry's keys.\n *\n * @internal\n */\nexport function slashRouteLeaves(commands: Iterable<SlashCommandBuilder | ContextMenuCommandBuilder>): Set<string> {\n const leaves = new Set<string>();\n for (const command of commands) {\n if (!(command instanceof SlashCommandBuilder)) continue;\n for (const leaf of routeLeavesOf(command.toJSON())) leaves.add(leaf.route);\n }\n return leaves;\n}\n"],"mappings":";;;;;;AAUA,SAAgB,eAAe,OAAsC;CACjE,OAAO,wBAAwB,KAAK,KAAK,QAAQ,YAAY,oBAAoB,KAAK;AAC1F;;;;;;;;;;;ACAA,SAAgB,iBAAiB,UAAkF;CAC/G,MAAM,yBAAS,IAAI,IAAY;CAC/B,KAAK,MAAM,WAAW,UAAU;EAC5B,IAAI,EAAE,mBAAmB,sBAAsB;EAC/C,KAAK,MAAM,QAAQ,cAAc,QAAQ,OAAO,CAAC,GAAG,OAAO,IAAI,KAAK,KAAK;CAC7E;CACA,OAAO;AACX"}
1
+ {"version":3,"file":"slashRouteLeaves-DTFHzCKN.mjs","names":[],"sources":["../src/commands/isCommandClass.ts","../src/commands/slashRouteLeaves.ts"],"sourcesContent":["import { isBuilderComponentClass } from '#components/Component';\nimport { CommandMetadataKey } from '#src/metadataKeys';\n\nimport type { BuilderComponent } from '#components/Component';\n\n/** @internal */\nexport type CommandCtor = new () => BuilderComponent<'command' | 'context_menu'>;\n\n// the cli's codegen scan reads this too\n/** @internal */\nexport function isCommandClass(value: unknown): value is CommandCtor {\n return isBuilderComponentClass(value) && Reflect.hasMetadata(CommandMetadataKey, value);\n}\n","import { SlashCommandBuilder } from '@discordjs/builders';\nimport { routeLeavesOf } from '@seedcord/utils/internal';\n\nimport type { ContextMenuCommandBuilder } from '@discordjs/builders';\n\n/**\n * dedupes route keys since the same builder appears once per guild.\n * these come from the same {@link routeLeavesOf} that `seedcord codegen` reads, so the boot-time\n * check compares against the generated registry's keys.\n *\n * @internal\n */\nexport function slashRouteLeaves(commands: Iterable<SlashCommandBuilder | ContextMenuCommandBuilder>): Set<string> {\n const leaves = new Set<string>();\n for (const command of commands) {\n if (!(command instanceof SlashCommandBuilder)) continue;\n for (const leaf of routeLeavesOf(command.toJSON())) leaves.add(leaf.route);\n }\n return leaves;\n}\n"],"mappings":";;;;;;AAUA,SAAgB,eAAe,OAAsC;CACjE,OAAO,wBAAwB,KAAK,KAAK,QAAQ,YAAY,oBAAoB,KAAK;AAC1F;;;;;;;;;;;ACAA,SAAgB,iBAAiB,UAAkF;CAC/G,MAAM,yBAAS,IAAI,IAAY;CAC/B,KAAK,MAAM,WAAW,UAAU;EAC5B,IAAI,EAAE,mBAAmB,sBAAsB;EAC/C,KAAK,MAAM,QAAQ,cAAc,QAAQ,OAAO,CAAC,GAAG,OAAO,IAAI,KAAK,KAAK;CAC7E;CACA,OAAO;AACX"}
@@ -1,4 +1,4 @@
1
- import { D as guardedAccessor, E as clearStore, O as contextMenuLeaves, T as accessorStore } from "./Bus-dGy6xq3F.mjs";
1
+ import { D as clearStore, E as accessorStore, O as guardedAccessor, k as contextMenuLeaves } from "./Bus-Djm6688b.mjs";
2
2
  import { SlashCommandBuilder } from "@discordjs/builders";
3
3
  import { ApplicationCommandType } from "discord-api-types/v10";
4
4
  import { paint } from "@seedcord/errors";
@@ -136,4 +136,4 @@ var CommandInjector = class {
136
136
 
137
137
  //#endregion
138
138
  export { Commands as n, ContextMenus as r, CommandInjector as t };
139
- //# sourceMappingURL=subscribers-A8lNU5yM.mjs.map
139
+ //# sourceMappingURL=subscribers-Bf4cBTvK.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"subscribers-A8lNU5yM.mjs","names":[],"sources":["../src/commands/CommandInjector.ts"],"sourcesContent":["import { SlashCommandBuilder } from '@discordjs/builders';\nimport { paint } from '@seedcord/errors';\nimport { Logger } from '@seedcord/logger';\nimport { routeLeavesOf } from '@seedcord/utils/internal';\nimport { ApplicationCommandType } from 'discord-api-types/v10';\n\nimport { contextMenuLeaves } from '#src/commands/contextMenuLeaves';\nimport { accessorStore, clearStore, guardedAccessor } from '#src/miscellaneous/guarded';\n\nimport type {\n ContextMenuKind,\n MessageContextMenuRegistry,\n UserContextMenuRegistry\n} from '#registries/ContextMenuRegistry';\nimport type { SlashRegistry } from '#registries/SlashRegistry';\nimport type { CommandBuilder, DeployResult } from '#src/commands/types';\nimport type { APIApplicationCommand, Snowflake } from 'discord-api-types/v10';\n\nconst commandStorage = accessorStore<CommandInfo>();\nconst userMenuStorage = accessorStore<ContextMenuInfo>();\nconst messageMenuStorage = accessorStore<ContextMenuInfo>();\n\n/** What {@link Commands} resolves to for one slash route. */\nexport interface CommandInfo {\n /** The clickable `</config set:id>` mention. A route whose id did not resolve falls back to `/config set`. */\n readonly mention: string;\n /** The deployed command id. Absent when the command deployed to two or more guilds. */\n readonly id?: string;\n /** The full route, e.g. `config/set`. */\n readonly route: string;\n /** The route's own description, taken from the subcommand for a nested route. */\n readonly description: string;\n}\n\n/** Each registered slash route mapped to its deployed command. */\nexport type InjectedCommandMap = {\n [K in keyof SlashRegistry]: CommandInfo;\n};\n\n/**\n * The bot's deployed slash commands, keyed by {@link SlashRegistry}. Filled by\n * {@link CommandInjector} after each deploy, and a read before that throws.\n *\n * @example\n * ```ts\n * import { Commands } from '@seedcord/gateway';\n *\n * await this.reply(`Try ${Commands['config/set'].mention}`);\n *\n * // a help listing\n * const lines = Object.values(Commands).map((c) => `${c.mention} ${c.description}`);\n * ```\n */\nexport const Commands = guardedAccessor('Commands', commandStorage) as InjectedCommandMap;\n\n/** What {@link ContextMenus} resolves to for one context-menu command. */\nexport interface ContextMenuInfo {\n /** The deployed command id. Absent when the command deployed to two or more guilds. */\n readonly id?: string;\n /** The name shown in Discord's right-click menu. */\n readonly name: string;\n readonly kind: ContextMenuKind;\n}\n\n/** Deployed context-menu commands, split by kind because Discord scopes name uniqueness per command type. */\nexport interface InjectedContextMenuMap {\n readonly user: { [K in keyof UserContextMenuRegistry]: ContextMenuInfo };\n readonly message: { [K in keyof MessageContextMenuRegistry]: ContextMenuInfo };\n}\n\n/**\n * The bot's deployed context-menu commands. Filled by {@link CommandInjector} after each deploy, and a read\n * before that throws.\n *\n * @example\n * ```ts\n * import { ContextMenus } from '@seedcord/gateway';\n *\n * const id = ContextMenus.user['View Profile'].id;\n * ```\n */\nexport const ContextMenus = {\n user: guardedAccessor('ContextMenus.user', userMenuStorage),\n message: guardedAccessor('ContextMenus.message', messageMenuStorage)\n} as InjectedContextMenuMap;\n\nconst MULTI_GUILD = Symbol('seedcord:commands:multi-guild');\n\n// Discord scopes name uniqueness per type, and a context-menu name may be all lowercase like a slash name\nfunction commandKey(type: ApplicationCommandType, name: string): string {\n return `${type}:${name}`;\n}\n\nfunction slashBuildersOf(all: readonly CommandBuilder[]): SlashCommandBuilder[] {\n const slash = all.filter((command): command is SlashCommandBuilder => command instanceof SlashCommandBuilder);\n // a guild command in N guilds is the same builder pushed once per guild\n return [...new Set(slash)];\n}\n\ninterface DeployedIds {\n readonly global: Map<string, Snowflake>;\n readonly guild: Map<string, Snowflake | typeof MULTI_GUILD>;\n readonly warned: Set<string>;\n}\n\n/** @internal */\nexport class CommandInjector {\n private readonly logger = new Logger('Commands', { channel: 'bot' });\n\n public inject(deploy: DeployResult, all: readonly CommandBuilder[]): void {\n this.clear();\n\n const ids: DeployedIds = {\n global: this.indexByKey(deploy.global),\n guild: this.indexGuilds(deploy.guilds),\n warned: new Set<string>()\n };\n let clickable = 0;\n\n for (const command of slashBuildersOf(all)) {\n const id = this.resolveId(\n ApplicationCommandType.ChatInput,\n command.name,\n ids,\n 'falling back to plain text (not clickable)'\n );\n for (const leaf of routeLeavesOf(command.toJSON())) {\n commandStorage[leaf.route] = {\n mention: id ? this.toMention(leaf.route, id) : this.toPlain(leaf.route),\n ...(id && { id }),\n route: leaf.route,\n description: leaf.description\n };\n if (id) clickable++;\n }\n }\n\n const menus = contextMenuLeaves(all);\n this.injectMenus(menus.user, ApplicationCommandType.User, userMenuStorage, ids);\n this.injectMenus(menus.message, ApplicationCommandType.Message, messageMenuStorage, ids);\n\n this.logger.utils.summary('Linked commands', {\n clickable,\n plain: Object.keys(commandStorage).length - clickable,\n 'context menus': menus.user.size + menus.message.size\n });\n }\n\n private injectMenus(\n names: ReadonlySet<string>,\n kind: ContextMenuKind,\n storage: Record<string, ContextMenuInfo>,\n ids: DeployedIds\n ): void {\n for (const name of names) {\n const id = this.resolveId(kind, name, ids, 'leaving its id unset');\n storage[name] = { ...(id && { id }), name, kind };\n }\n }\n\n private indexByKey(collection: Map<Snowflake, APIApplicationCommand>): Map<string, Snowflake> {\n const map = new Map<string, Snowflake>();\n for (const command of collection.values()) map.set(commandKey(command.type, command.name), command.id);\n return map;\n }\n\n private indexGuilds(\n guilds: Map<Snowflake, Map<Snowflake, APIApplicationCommand>>\n ): Map<string, Snowflake | typeof MULTI_GUILD> {\n const map = new Map<string, Snowflake | typeof MULTI_GUILD>();\n for (const collection of guilds.values()) {\n for (const command of collection.values()) {\n const key = commandKey(command.type, command.name);\n const existing = map.get(key);\n // Discord assigns a separate id per guild, so two ids for one name means no single mention\n if (existing === undefined) map.set(key, command.id);\n else if (existing !== command.id) map.set(key, MULTI_GUILD);\n }\n }\n return map;\n }\n\n private resolveId(\n type: ApplicationCommandType,\n name: string,\n ids: DeployedIds,\n consequence: string\n ): Snowflake | undefined {\n const key = commandKey(type, name);\n const global = ids.global.get(key);\n if (global) return global;\n\n const guild = ids.guild.get(key);\n if (guild === undefined) return undefined;\n if (guild === MULTI_GUILD) {\n if (!ids.warned.has(key)) {\n this.logger.warn(`${paint.sky.bold(name)} is deployed to multiple guilds, ${consequence}.`);\n ids.warned.add(key);\n }\n return undefined;\n }\n return guild;\n }\n\n private toMention(route: string, id: Snowflake): string {\n return `</${route.replaceAll('/', ' ')}:${id}>`;\n }\n\n private toPlain(route: string): string {\n return `/${route.replaceAll('/', ' ')}`;\n }\n\n private clear(): void {\n for (const store of [commandStorage, userMenuStorage, messageMenuStorage]) clearStore(store);\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"subscribers-Bf4cBTvK.mjs","names":[],"sources":["../src/commands/CommandInjector.ts"],"sourcesContent":["import { SlashCommandBuilder } from '@discordjs/builders';\nimport { paint } from '@seedcord/errors';\nimport { Logger } from '@seedcord/logger';\nimport { routeLeavesOf } from '@seedcord/utils/internal';\nimport { ApplicationCommandType } from 'discord-api-types/v10';\n\nimport { contextMenuLeaves } from '#src/commands/contextMenuLeaves';\nimport { accessorStore, clearStore, guardedAccessor } from '#src/miscellaneous/guarded';\n\nimport type {\n ContextMenuKind,\n MessageContextMenuRegistry,\n UserContextMenuRegistry\n} from '#registries/ContextMenuRegistry';\nimport type { SlashRegistry } from '#registries/SlashRegistry';\nimport type { CommandBuilder, DeployResult } from '#src/commands/types';\nimport type { APIApplicationCommand, Snowflake } from 'discord-api-types/v10';\n\nconst commandStorage = accessorStore<CommandInfo>();\nconst userMenuStorage = accessorStore<ContextMenuInfo>();\nconst messageMenuStorage = accessorStore<ContextMenuInfo>();\n\n/** What {@link Commands} resolves to for one slash route. */\nexport interface CommandInfo {\n /** The clickable `</config set:id>` mention. A route whose id did not resolve falls back to `/config set`. */\n readonly mention: string;\n /** The deployed command id. Absent when the command deployed to two or more guilds. */\n readonly id?: string;\n /** The full route, e.g. `config/set`. */\n readonly route: string;\n /** The route's own description, taken from the subcommand for a nested route. */\n readonly description: string;\n}\n\n/** Each registered slash route mapped to its deployed command. */\nexport type InjectedCommandMap = {\n [K in keyof SlashRegistry]: CommandInfo;\n};\n\n/**\n * The bot's deployed slash commands, keyed by {@link SlashRegistry}. Filled by\n * {@link CommandInjector} after each deploy, and a read before that throws.\n *\n * @example\n * ```ts\n * import { Commands } from '@seedcord/gateway';\n *\n * await this.reply(`Try ${Commands['config/set'].mention}`);\n *\n * // a help listing\n * const lines = Object.values(Commands).map((c) => `${c.mention} ${c.description}`);\n * ```\n */\nexport const Commands = guardedAccessor('Commands', commandStorage) as InjectedCommandMap;\n\n/** What {@link ContextMenus} resolves to for one context-menu command. */\nexport interface ContextMenuInfo {\n /** The deployed command id. Absent when the command deployed to two or more guilds. */\n readonly id?: string;\n /** The name shown in Discord's right-click menu. */\n readonly name: string;\n readonly kind: ContextMenuKind;\n}\n\n/** Deployed context-menu commands, split by kind because Discord scopes name uniqueness per command type. */\nexport interface InjectedContextMenuMap {\n readonly user: { [K in keyof UserContextMenuRegistry]: ContextMenuInfo };\n readonly message: { [K in keyof MessageContextMenuRegistry]: ContextMenuInfo };\n}\n\n/**\n * The bot's deployed context-menu commands. Filled by {@link CommandInjector} after each deploy, and a read\n * before that throws.\n *\n * @example\n * ```ts\n * import { ContextMenus } from '@seedcord/gateway';\n *\n * const id = ContextMenus.user['View Profile'].id;\n * ```\n */\nexport const ContextMenus = {\n user: guardedAccessor('ContextMenus.user', userMenuStorage),\n message: guardedAccessor('ContextMenus.message', messageMenuStorage)\n} as InjectedContextMenuMap;\n\nconst MULTI_GUILD = Symbol('seedcord:commands:multi-guild');\n\n// Discord scopes name uniqueness per type, and a context-menu name may be all lowercase like a slash name\nfunction commandKey(type: ApplicationCommandType, name: string): string {\n return `${type}:${name}`;\n}\n\nfunction slashBuildersOf(all: readonly CommandBuilder[]): SlashCommandBuilder[] {\n const slash = all.filter((command): command is SlashCommandBuilder => command instanceof SlashCommandBuilder);\n // a guild command in N guilds is the same builder pushed once per guild\n return [...new Set(slash)];\n}\n\ninterface DeployedIds {\n readonly global: Map<string, Snowflake>;\n readonly guild: Map<string, Snowflake | typeof MULTI_GUILD>;\n readonly warned: Set<string>;\n}\n\n/** @internal */\nexport class CommandInjector {\n private readonly logger = new Logger('Commands', { channel: 'bot' });\n\n public inject(deploy: DeployResult, all: readonly CommandBuilder[]): void {\n this.clear();\n\n const ids: DeployedIds = {\n global: this.indexByKey(deploy.global),\n guild: this.indexGuilds(deploy.guilds),\n warned: new Set<string>()\n };\n let clickable = 0;\n\n for (const command of slashBuildersOf(all)) {\n const id = this.resolveId(\n ApplicationCommandType.ChatInput,\n command.name,\n ids,\n 'falling back to plain text (not clickable)'\n );\n for (const leaf of routeLeavesOf(command.toJSON())) {\n commandStorage[leaf.route] = {\n mention: id ? this.toMention(leaf.route, id) : this.toPlain(leaf.route),\n ...(id && { id }),\n route: leaf.route,\n description: leaf.description\n };\n if (id) clickable++;\n }\n }\n\n const menus = contextMenuLeaves(all);\n this.injectMenus(menus.user, ApplicationCommandType.User, userMenuStorage, ids);\n this.injectMenus(menus.message, ApplicationCommandType.Message, messageMenuStorage, ids);\n\n this.logger.utils.summary('Linked commands', {\n clickable,\n plain: Object.keys(commandStorage).length - clickable,\n 'context menus': menus.user.size + menus.message.size\n });\n }\n\n private injectMenus(\n names: ReadonlySet<string>,\n kind: ContextMenuKind,\n storage: Record<string, ContextMenuInfo>,\n ids: DeployedIds\n ): void {\n for (const name of names) {\n const id = this.resolveId(kind, name, ids, 'leaving its id unset');\n storage[name] = { ...(id && { id }), name, kind };\n }\n }\n\n private indexByKey(collection: Map<Snowflake, APIApplicationCommand>): Map<string, Snowflake> {\n const map = new Map<string, Snowflake>();\n for (const command of collection.values()) map.set(commandKey(command.type, command.name), command.id);\n return map;\n }\n\n private indexGuilds(\n guilds: Map<Snowflake, Map<Snowflake, APIApplicationCommand>>\n ): Map<string, Snowflake | typeof MULTI_GUILD> {\n const map = new Map<string, Snowflake | typeof MULTI_GUILD>();\n for (const collection of guilds.values()) {\n for (const command of collection.values()) {\n const key = commandKey(command.type, command.name);\n const existing = map.get(key);\n // Discord assigns a separate id per guild, so two ids for one name means no single mention\n if (existing === undefined) map.set(key, command.id);\n else if (existing !== command.id) map.set(key, MULTI_GUILD);\n }\n }\n return map;\n }\n\n private resolveId(\n type: ApplicationCommandType,\n name: string,\n ids: DeployedIds,\n consequence: string\n ): Snowflake | undefined {\n const key = commandKey(type, name);\n const global = ids.global.get(key);\n if (global) return global;\n\n const guild = ids.guild.get(key);\n if (guild === undefined) return undefined;\n if (guild === MULTI_GUILD) {\n if (!ids.warned.has(key)) {\n this.logger.warn(`${paint.sky.bold(name)} is deployed to multiple guilds, ${consequence}.`);\n ids.warned.add(key);\n }\n return undefined;\n }\n return guild;\n }\n\n private toMention(route: string, id: Snowflake): string {\n return `</${route.replaceAll('/', ' ')}:${id}>`;\n }\n\n private toPlain(route: string): string {\n return `/${route.replaceAll('/', ' ')}`;\n }\n\n private clear(): void {\n for (const store of [commandStorage, userMenuStorage, messageMenuStorage]) clearStore(store);\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@seedcord/core",
3
3
  "type": "module",
4
- "version": "0.5.0",
4
+ "version": "0.7.0",
5
5
  "engines": {
6
6
  "node": ">=24.11"
7
7
  },
@@ -75,24 +75,24 @@
75
75
  "dependencies": {
76
76
  "@discordjs/builders": "^1.14.1",
77
77
  "@discordjs/rest": "^2.6.3",
78
- "discord-api-types": "^0.38.52",
79
- "reflect-metadata": "^0.2.2",
80
- "@seedcord/custom-id": "^0.1.1",
81
- "@seedcord/errors": "^0.6.0",
78
+ "@seedcord/custom-id": "^0.2.1",
79
+ "@seedcord/errors": "^0.8.0",
82
80
  "@seedcord/event-emitter": "^0.1.5",
83
- "@seedcord/logger": "^0.3.0",
84
- "@seedcord/types": "^0.11.0",
85
- "@seedcord/utils": "^0.8.9"
81
+ "@seedcord/logger": "^0.3.2",
82
+ "@seedcord/types": "^0.13.0",
83
+ "@seedcord/utils": "^0.8.11",
84
+ "discord-api-types": "^0.38.52",
85
+ "reflect-metadata": "^0.2.2"
86
86
  },
87
87
  "devDependencies": {
88
- "envapt": "^8.1.1",
89
- "type-fest": "^5.8.0",
90
- "vite": "^8.2.0",
91
- "@seedcord/eslint-config": "^2.1.0",
92
- "@seedcord/rate-limiter": "^0.1.6",
88
+ "@seedcord/eslint-config": "^2.2.1",
89
+ "@seedcord/rate-limiter": "^0.1.8",
93
90
  "@seedcord/tsconfig": "^2.0.3",
94
91
  "@seedcord/tsdown-config": "^2.0.1",
95
- "@seedcord/vitest-config": "^0.0.1"
92
+ "@seedcord/vitest-config": "^0.0.1",
93
+ "envapt": "^8.1.1",
94
+ "type-fest": "^5.8.0",
95
+ "vite": "^8.2.0"
96
96
  },
97
97
  "publishConfig": {
98
98
  "access": "public",