@seedcord/core 0.11.0 → 0.12.0-next.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 (49) hide show
  1. package/dist/Bus-W1K-uMeO.mjs +930 -0
  2. package/dist/Bus-W1K-uMeO.mjs.map +1 -0
  3. package/dist/{CoordinatedShutdown-H5YjZn8A.d.mts → CoordinatedShutdown-BVtNNSsF.d.mts} +13 -4
  4. package/dist/{CoordinatedStartup-CGnJrqcl.mjs → CoordinatedStartup-DA3gW8op.mjs} +22 -20
  5. package/dist/CoordinatedStartup-DA3gW8op.mjs.map +1 -0
  6. package/dist/Generated-ByC53_A2.d.mts +26 -0
  7. package/dist/{HmrModuleHandler-8ZC-UuZh.mjs → HmrModuleHandler-BMD7dg8N.mjs} +2 -2
  8. package/dist/HmrModuleHandler-BMD7dg8N.mjs.map +1 -0
  9. package/dist/PluginHost-CYe-UVpp.mjs +552 -0
  10. package/dist/PluginHost-CYe-UVpp.mjs.map +1 -0
  11. package/dist/{PluginHost-DB4j8unu.d.mts → PluginHost-DWBgBS6Q.d.mts} +35 -8
  12. package/dist/{PluginLifecycle-AhQ46NIr.mjs → PluginLifecycle-DdwRzcDj.mjs} +3 -3
  13. package/dist/PluginLifecycle-DdwRzcDj.mjs.map +1 -0
  14. package/dist/{ServerHost-CtmAhEXn.mjs → ServerHost-DHg-Y2_q.mjs} +17 -6
  15. package/dist/{ServerHost-CtmAhEXn.mjs.map → ServerHost-DHg-Y2_q.mjs.map} +1 -1
  16. package/dist/{ServerHost-DYeh0KTk.d.mts → ServerHost-whpI0h4d.d.mts} +7 -6
  17. package/dist/bindViteEnv-NFzvdqlT.mjs +32 -0
  18. package/dist/{bindViteEnv-DWq5rftJ.mjs.map → bindViteEnv-NFzvdqlT.mjs.map} +1 -1
  19. package/dist/env-internal.index.mjs +1 -1
  20. package/dist/hmr.index.mjs +1 -1
  21. package/dist/{index-Du71Xx69.d.mts → index-Boo33hh3.d.mts} +10 -11
  22. package/dist/index.d.mts +108 -6
  23. package/dist/index.mjs +118 -12
  24. package/dist/index.mjs.map +1 -1
  25. package/dist/internal.index.d.mts +25 -7
  26. package/dist/internal.index.mjs +8 -8
  27. package/dist/node-internal.index.d.mts +7 -4
  28. package/dist/node-internal.index.mjs +19 -6
  29. package/dist/node-internal.index.mjs.map +1 -1
  30. package/dist/node.index.d.mts +3 -3
  31. package/dist/node.index.mjs +22 -28
  32. package/dist/node.index.mjs.map +1 -1
  33. package/dist/{phases-FTM69J3a.d.mts → phases-Dojnr-ng.d.mts} +111 -12
  34. package/dist/plugin.index.d.mts +3 -2
  35. package/dist/plugin.index.mjs +1 -1
  36. package/dist/{subscribers-Ce3SWy23.mjs → subscribers-D4_HZcgw.mjs} +12 -829
  37. package/dist/subscribers-D4_HZcgw.mjs.map +1 -0
  38. package/dist/{bindViteEnv-DWq5rftJ.mjs → usingCtx-Cf2cZi96.mjs} +1 -31
  39. package/package.json +11 -9
  40. package/dist/CoordinatedStartup-CGnJrqcl.mjs.map +0 -1
  41. package/dist/HmrModuleHandler-8ZC-UuZh.mjs.map +0 -1
  42. package/dist/PluginHost-B8kxLOeb.mjs +0 -245
  43. package/dist/PluginHost-B8kxLOeb.mjs.map +0 -1
  44. package/dist/PluginLifecycle-AhQ46NIr.mjs.map +0 -1
  45. package/dist/phases-BauKJOTT.mjs +0 -31
  46. package/dist/phases-BauKJOTT.mjs.map +0 -1
  47. package/dist/publishDefault-D3dAyzZ9.mjs +0 -7
  48. package/dist/publishDefault-D3dAyzZ9.mjs.map +0 -1
  49. package/dist/subscribers-Ce3SWy23.mjs.map +0 -1
@@ -1,10 +1,10 @@
1
- import { At as guardedAccessor, B as NotInGuild, Bt as isBuilderComponentClass, C as PermissionNames, Ct as warnLinks, D as pickNotice, Dt as linkCommands, Et as indexById, F as MissingPermissions, Ft as skippedNote, G as serializeReply, H as OnCooldown, Ht as Notice, I as MissingRole, It as contextMenuLeaves, K as translateSerializationError, L as NeedsAny, Lt as NoticeCard, M as runCommits, Mt as commandsRoute, N as GateNotice, Nt as globalFirst, O as discardCommits, Ot as accessorStore, P as HasDangerousPermissions, Pt as scopeIdsOf, Q as sendTarget, R as NotAllowed, St as installLinks, Tt as commandRefOf, V as NotOwner, Vt as bindBotColor, X as AckTrace, Y as reportedWrite, Z as checkAckLegality, _ as pageCursor, _t as SubscribeMetadataKey, a as RegisterSubscriber, at as ComponentDefsKey, c as VerifyWebhooks, dt as EventMiddlewareMetadataKey, et as interactionMiddlewareMetaOf, f as Subscriber, ft as GatedMetadataKey, g as PAGE_MAX, gt as InteractionRouteKeys, h as sendFlags, ht as InteractionMiddlewareMetadataKey, i as RegisterDefaults, it as storeInteractionRoute, j as runCheck, jt as GLOBAL_ID, kt as clearStore, lt as CommandMetadataKey, m as deferFlags, mt as InteractionMetadataKey, n as busLoggerOf, nt as interactionRoutesOf, o as RegisteredCount, ot as decodeComponentRoute, p as ReportThrottle, r as registrationFor, rt as storeComponentRoute, s as UnregisterSubscriber, st as ConfigGuilds, tt as contextMenuRouteOf, ut as EventMetadataKey, v as ResolvedEmoji, vt as WebhookUrlMetadataKey, wt as commandKey, y as Silence, yt as CommandInjector, z as NotInDm } from "./subscribers-Ce3SWy23.mjs";
2
- import { t as PublishDefault } from "./publishDefault-D3dAyzZ9.mjs";
3
- import { c as setDevChannel, i as FixedScope, n as attachmentsOf, r as sealAttachments, s as getDevChannel } from "./PluginHost-B8kxLOeb.mjs";
4
- import { t as HmrModuleHandler } from "./HmrModuleHandler-8ZC-UuZh.mjs";
5
- import { i as logUnknownFault, t as PluginLifecycle } from "./PluginLifecycle-AhQ46NIr.mjs";
6
- import { t as CoordinatedStartup } from "./CoordinatedStartup-CGnJrqcl.mjs";
7
- import { t as bindEnv } from "./bindViteEnv-DWq5rftJ.mjs";
1
+ import { $ as commandKey, A as serializeReply, B as contextMenuRouteOf, C as NotAllowed, D as OnCooldown, E as NotOwner, F as AckTrace, G as decodeComponentRoute, H as storeComponentRoute, I as checkAckLegality, J as CommandInjector, K as ConfigGuilds, L as sendTarget, P as reportedWrite, Q as warnLinks, S as NeedsAny, T as NotInGuild, U as storeInteractionRoute, V as interactionRoutesOf, W as ComponentDefsKey, Z as installLinks, _ as runCommits, at as guardedAccessor, b as MissingPermissions, c as PermissionNames, ct as globalFirst, dt as contextMenuLeaves, et as commandRefOf, f as pickNotice, ft as NoticeCard, g as runCheck, i as Silence, it as clearStore, j as translateSerializationError, lt as scopeIdsOf, n as pageCursor, nt as linkCommands, ot as GLOBAL_ID, p as discardCommits, pt as Notice, r as ResolvedEmoji, rt as accessorStore, st as commandsRoute, t as PAGE_MAX, tt as indexById, ut as skippedNote, v as GateNotice, w as NotInDm, x as MissingRole, y as HasDangerousPermissions, z as interactionMiddlewareMetaOf } from "./subscribers-D4_HZcgw.mjs";
2
+ import { A as WebhookUrlMetadataKey, C as EventMiddlewareMetadataKey, D as InteractionMiddlewareMetadataKey, E as InteractionMetadataKey, N as isBuilderComponentClass, O as InteractionRouteKeys, P as bindBotColor, S as EventMetadataKey, a as RegisterSubscriber, b as PublishDefault, c as VerifyWebhooks, f as Subscriber, g as OnceTracker, h as sendFlags, i as RegisterDefaults, k as SubscribeMetadataKey, m as deferFlags, n as busLoggerOf, o as RegisteredCount, p as ReportThrottle, r as registrationFor, s as UnregisterSubscriber, w as GatedMetadataKey, x as CommandMetadataKey } from "./Bus-W1K-uMeO.mjs";
3
+ import { a as FixedScope, c as getDevChannel, i as splitPluginKey, l as setDevChannel, n as attachmentsOf, r as sealAttachments } from "./PluginHost-CYe-UVpp.mjs";
4
+ import { t as HmrModuleHandler } from "./HmrModuleHandler-BMD7dg8N.mjs";
5
+ import { i as logUnknownFault, t as PluginLifecycle } from "./PluginLifecycle-DdwRzcDj.mjs";
6
+ import { t as CoordinatedStartup } from "./CoordinatedStartup-DA3gW8op.mjs";
7
+ import { t as bindEnv } from "./bindViteEnv-NFzvdqlT.mjs";
8
8
  import "reflect-metadata";
9
9
  import { SlashCommandBuilder } from "@discordjs/builders";
10
10
  import { Routes } from "discord-api-types/v10";
@@ -969,5 +969,5 @@ async function runAfter(ran, result, logger) {
969
969
  }
970
970
 
971
971
  //#endregion
972
- export { AckTrace, AutocompleteRouteBrand, BAKED_IDS_KEY, COMMAND_DEPLOY_TIMEOUT_MS, CommandMetadataKey, CommandRegistry, ComponentDefsBrand, ComponentDefsKey, ComponentKindBrand, ContextMenuKindBrand, ContextMenuNamesBrand, CoordinatedStartup, EventMetadataKey, EventMiddlewareMetadataKey, FixedScope, GLOBAL_ID, GateNotice, GatedMetadataKey, HasDangerousPermissions, HmrManager, InteractionMetadataKey, InteractionMiddlewareMetadataKey, InteractionRouteKeys, MiddlewareKindsBrand, MiddlewareRegistry, MissingPermissions, MissingRole, NeedsAny, NotAllowed, NotInDm, NotInGuild, NotOwner, NoticeCard, OnCooldown, PAGE_MAX, PermissionNames, PluginLifecycle, PublishDefault, RegisterDefaults, RegisterSubscriber, RegisteredCount, ReportThrottle, SlashRouteBrand, SubscribeMetadataKey, SubscriberLoader, UnregisterSubscriber, VerifyWebhooks, WebhookUrlMetadataKey, accessorStore, attachmentsOf, bindBotColor, bindEnv, busLoggerOf, checkAckLegality, clearStore, commandKey, commandRefOf, commandsRoute, connectDevChannel, contextMenuLeaves, contextMenuRouteOf, currentApplication, decodeComponentRoute, deferFlags, eventResultFor, forwardLogsToCli, fromDevLogLine, fromWireError, getDevChannel, globalFirst, guardedAccessor, guildSummaries, indexById, installLinks, interactionMiddleware, interactionMiddlewareMetaOf, interactionRoutesOf, isCommandClass, isEmojiTuple, linkCommands, logUnknownFault, outcomeFor, pageCursor, pickNotice, planDeploy, putCommands, queuedMsFor, readBotToken, readCommands, readCommandsAt, registrationFor, reportDispatch, reportedWrite, resolveDeployTarget, resolveEmojis, resultFor, runAfter, runGates, runHandlerGates, scopeIdsOf, sealAttachments, sendFlags, sendTarget, serializeReply, setDevChannel, skippedNote, slashRouteLeaves, slowGateMonitor, storeComponentRoute, storeInteractionRoute, toWireError, translateSerializationError, warnLinks, wrapHot };
972
+ export { AckTrace, AutocompleteRouteBrand, BAKED_IDS_KEY, COMMAND_DEPLOY_TIMEOUT_MS, CommandMetadataKey, CommandRegistry, ComponentDefsBrand, ComponentDefsKey, ComponentKindBrand, ContextMenuKindBrand, ContextMenuNamesBrand, CoordinatedStartup, EventMetadataKey, EventMiddlewareMetadataKey, FixedScope, GLOBAL_ID, GateNotice, GatedMetadataKey, HasDangerousPermissions, HmrManager, InteractionMetadataKey, InteractionMiddlewareMetadataKey, InteractionRouteKeys, MiddlewareKindsBrand, MiddlewareRegistry, MissingPermissions, MissingRole, NeedsAny, NotAllowed, NotInDm, NotInGuild, NotOwner, NoticeCard, OnCooldown, OnceTracker, PAGE_MAX, PermissionNames, PluginLifecycle, PublishDefault, RegisterDefaults, RegisterSubscriber, RegisteredCount, ReportThrottle, SlashRouteBrand, SubscribeMetadataKey, SubscriberLoader, UnregisterSubscriber, VerifyWebhooks, WebhookUrlMetadataKey, accessorStore, attachmentsOf, bindBotColor, bindEnv, busLoggerOf, checkAckLegality, clearStore, commandKey, commandRefOf, commandsRoute, connectDevChannel, contextMenuLeaves, contextMenuRouteOf, currentApplication, decodeComponentRoute, deferFlags, eventResultFor, forwardLogsToCli, fromDevLogLine, fromWireError, getDevChannel, globalFirst, guardedAccessor, guildSummaries, indexById, installLinks, interactionMiddleware, interactionMiddlewareMetaOf, interactionRoutesOf, isCommandClass, isEmojiTuple, linkCommands, logUnknownFault, outcomeFor, pageCursor, pickNotice, planDeploy, putCommands, queuedMsFor, readBotToken, readCommands, readCommandsAt, registrationFor, reportDispatch, reportedWrite, resolveDeployTarget, resolveEmojis, resultFor, runAfter, runGates, runHandlerGates, scopeIdsOf, sealAttachments, sendFlags, sendTarget, serializeReply, setDevChannel, skippedNote, slashRouteLeaves, slowGateMonitor, splitPluginKey, storeComponentRoute, storeInteractionRoute, toWireError, translateSerializationError, warnLinks, wrapHot };
973
973
  //# sourceMappingURL=internal.index.mjs.map
@@ -1,6 +1,6 @@
1
- import { n as StartupPhase, t as ShutdownPhase } from "./phases-FTM69J3a.mjs";
2
- import { r as LifecycleTask, t as CoordinatedShutdown } from "./CoordinatedShutdown-H5YjZn8A.mjs";
3
- import { n as shutdownOf } from "./ServerHost-DYeh0KTk.mjs";
1
+ import { n as StartupPhase, t as ShutdownPhase } from "./phases-Dojnr-ng.mjs";
2
+ import { a as LifecycleTask, t as CoordinatedShutdown } from "./CoordinatedShutdown-BVtNNSsF.mjs";
3
+ import { n as shutdownOf } from "./ServerHost-whpI0h4d.mjs";
4
4
  import { Logger } from "@seedcord/logger";
5
5
  import { IncomingMessage, ServerResponse } from "node:http";
6
6
  import { HealthCheckConfig, HealthCheckOption } from "@seedcord/types";
@@ -13,10 +13,13 @@ export declare class InFlight {
13
13
  private readonly logger;
14
14
  private readonly label;
15
15
  private readonly running;
16
- private isClosed;
16
+ private readonly opening;
17
+ private state;
17
18
  constructor(logger: Logger, label: string);
18
19
  get closed(): boolean;
20
+ open(): void;
19
21
  close(): void;
22
+ admit<Result>(work: () => Promise<Result>): Promise<Result | undefined>;
20
23
  track(work: Promise<unknown>): void;
21
24
  drain(timeoutMs: number): Promise<void>;
22
25
  private settleAll;
@@ -1,6 +1,6 @@
1
- import { n as StartupPhase, t as ShutdownPhase } from "./phases-BauKJOTT.mjs";
2
- import { n as settleWithin } from "./PluginLifecycle-AhQ46NIr.mjs";
3
- import { n as shutdownOf, r as assertDeclaredRuntime } from "./ServerHost-CtmAhEXn.mjs";
1
+ import { v as ShutdownPhase, y as StartupPhase } from "./Bus-W1K-uMeO.mjs";
2
+ import { n as settleWithin } from "./PluginLifecycle-DdwRzcDj.mjs";
3
+ import { n as shutdownOf, r as assertDeclaredRuntime } from "./ServerHost-DHg-Y2_q.mjs";
4
4
  import { SeedcordErrorCode, paint } from "@seedcord/errors";
5
5
  import { SeedcordError } from "@seedcord/errors/internal";
6
6
  import { Logger } from "@seedcord/logger";
@@ -18,16 +18,29 @@ var InFlight = class {
18
18
  logger;
19
19
  label;
20
20
  running = /* @__PURE__ */ new Set();
21
- isClosed = false;
21
+ opening = Promise.withResolvers();
22
+ state = "held";
22
23
  constructor(logger, label) {
23
24
  this.logger = logger;
24
25
  this.label = label;
25
26
  }
26
27
  get closed() {
27
- return this.isClosed;
28
+ return this.state === "closed";
29
+ }
30
+ open() {
31
+ if (this.state !== "held") return;
32
+ this.state = "open";
33
+ this.opening.resolve(true);
28
34
  }
29
35
  close() {
30
- this.isClosed = true;
36
+ this.state = "closed";
37
+ this.opening.resolve(false);
38
+ }
39
+ admit(work) {
40
+ if (this.state === "closed") return Promise.resolve(void 0);
41
+ const admitted = this.state === "open" ? work() : this.opening.promise.then((opened) => opened ? work() : void 0);
42
+ this.track(admitted);
43
+ return admitted;
31
44
  }
32
45
  track(work) {
33
46
  this.running.add(work);
@@ -1 +1 @@
1
- {"version":3,"file":"node-internal.index.mjs","names":[],"sources":["../src/node/Lifecycle/drainWindow.ts","../src/node/Lifecycle/InFlight.ts","../src/node/HealthResponder.ts","../src/node/HealthCheck.ts","../src/node/envaptBaseDir.ts","../src/node-internal.index.ts"],"sourcesContent":["// 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","import { settleWithin } from '#src/lifecycle/withTimeout';\n\nimport type { Logger } from '@seedcord/logger';\n\nexport class InFlight {\n private readonly running = new Set<Promise<unknown>>();\n private isClosed = false;\n\n constructor(\n private readonly logger: Logger,\n private readonly label: string\n ) {}\n\n public get closed(): boolean {\n return this.isClosed;\n }\n\n public close(): void {\n this.isClosed = true;\n }\n\n public track(work: Promise<unknown>): void {\n this.running.add(work);\n const forget = (): void => {\n this.running.delete(work);\n };\n void work.then(forget, forget);\n }\n\n public async drain(timeoutMs: number): Promise<void> {\n await settleWithin(this.settleAll(), timeoutMs);\n // nothing cancels these before the process exits\n if (this.running.size > 0) {\n this.logger.warn(`${this.label}: ${String(this.running.size)} still running when the drain window closed`);\n }\n }\n\n // settling work can track more work\n private async settleAll(): Promise<void> {\n while (this.running.size > 0) await Promise.allSettled(this.running);\n }\n}\n","import type { HealthCheckConfig, HealthCheckOption } from '@seedcord/types';\nimport type { IncomingMessage, ServerResponse } from 'node:http';\n\nconst HTTP_OK = 200;\nconst HTTP_SERVICE_UNAVAILABLE = 503;\nconst DEFAULT_HEALTH_CHECK_PATH = '/health';\nconst JSON_HEADERS = { 'Content-Type': 'application/json' };\n\nexport function healthCheckConfig(option?: HealthCheckOption): HealthCheckConfig | undefined {\n if (option === false) return undefined;\n return typeof option === 'object' ? option : {};\n}\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 static fromOption(option?: HealthCheckOption): HealthResponder | undefined {\n const config = healthCheckConfig(option);\n return config && new HealthResponder(config.path);\n }\n\n public tryRespond(req: IncomingMessage, res: ServerResponse): boolean {\n if (!this.matches(req.method, req.url)) return false;\n\n res.writeHead(HTTP_OK, JSON_HEADERS);\n res.end(this.body());\n return true;\n }\n\n public answer(request: Request, ready: boolean): Response | undefined {\n if (!this.matches(request.method, new URL(request.url).pathname)) return undefined;\n if (!ready) return new Response(null, { status: HTTP_SERVICE_UNAVAILABLE });\n return new Response(this.body(), { status: HTTP_OK, headers: JSON_HEADERS });\n }\n\n private matches(method: string | undefined, path: string | undefined): boolean {\n return method === 'GET' && path === this.path;\n }\n\n private body(): string {\n return JSON.stringify({ status: 'ok', timestamp: Date.now() });\n }\n}\n","import { createServer } from 'node:http';\n\nimport { paint, SeedcordErrorCode } from '@seedcord/errors';\nimport { SeedcordError } from '@seedcord/errors/internal';\nimport { Logger } from '@seedcord/logger';\nimport { isPortInUse } from '@seedcord/utils/internal';\n\nimport { ShutdownPhase } from '#src/lifecycle/phases';\n\nimport { healthCheckConfig, 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 const config = healthCheckConfig(option);\n return config && new HealthCheck(shutdown, config);\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 = (error: Error): void => {\n if (isPortInUse(error)) {\n reject(\n new SeedcordError(SeedcordErrorCode.PortInUse, [this._port, 'healthCheck.port'], {\n cause: error\n })\n );\n return;\n }\n reject(error);\n };\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(paint.check)} 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 { botConfigDir, setBuiltFilesHook } from '@seedcord/utils/internal';\nimport { Envapter } from 'envapt';\n\nexport function setEnvaptBaseDir(): void {\n setBuiltFilesHook(() => {\n const configDir = botConfigDir();\n if (configDir !== undefined && Envapter.source.supportsFiles) Envapter.baseDir = configDir;\n });\n}\n","import { assertDeclaredRuntime } from '#node/assertRuntimeVersion';\n\n// the edge bundle never reaches this entry\nassertDeclaredRuntime();\n\nexport * from '#node/Lifecycle';\nexport * from '#node/HealthCheck';\nexport { HealthResponder } from '#node/HealthResponder';\nexport { settleWithin } from '#src/lifecycle/withTimeout';\nexport { shutdownOf } from '#node/ServerHost';\nexport { setEnvaptBaseDir } from '#node/envaptBaseDir';\n"],"mappings":"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"}
1
+ {"version":3,"file":"node-internal.index.mjs","names":[],"sources":["../src/node/Lifecycle/drainWindow.ts","../src/node/Lifecycle/InFlight.ts","../src/node/HealthResponder.ts","../src/node/HealthCheck.ts","../src/node/envaptBaseDir.ts","../src/node-internal.index.ts"],"sourcesContent":["// 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","import { settleWithin } from '#src/lifecycle/withTimeout';\n\nimport type { Logger } from '@seedcord/logger';\n\nexport class InFlight {\n private readonly running = new Set<Promise<unknown>>();\n private readonly opening = Promise.withResolvers<boolean>();\n private state: 'held' | 'open' | 'closed' = 'held';\n\n constructor(\n private readonly logger: Logger,\n private readonly label: string\n ) {}\n\n public get closed(): boolean {\n return this.state === 'closed';\n }\n\n public open(): void {\n if (this.state !== 'held') return;\n this.state = 'open';\n this.opening.resolve(true);\n }\n\n public close(): void {\n this.state = 'closed';\n this.opening.resolve(false);\n }\n\n public admit<Result>(work: () => Promise<Result>): Promise<Result | undefined> {\n if (this.state === 'closed') return Promise.resolve(undefined);\n const admitted =\n this.state === 'open' ? work() : this.opening.promise.then((opened) => (opened ? work() : undefined));\n this.track(admitted);\n return admitted;\n }\n\n public track(work: Promise<unknown>): void {\n this.running.add(work);\n const forget = (): void => {\n this.running.delete(work);\n };\n void work.then(forget, forget);\n }\n\n public async drain(timeoutMs: number): Promise<void> {\n await settleWithin(this.settleAll(), timeoutMs);\n // nothing cancels these before the process exits\n if (this.running.size > 0) {\n this.logger.warn(`${this.label}: ${String(this.running.size)} still running when the drain window closed`);\n }\n }\n\n // settling work can track more work\n private async settleAll(): Promise<void> {\n while (this.running.size > 0) await Promise.allSettled(this.running);\n }\n}\n","import type { HealthCheckConfig, HealthCheckOption } from '@seedcord/types';\nimport type { IncomingMessage, ServerResponse } from 'node:http';\n\nconst HTTP_OK = 200;\nconst HTTP_SERVICE_UNAVAILABLE = 503;\nconst DEFAULT_HEALTH_CHECK_PATH = '/health';\nconst JSON_HEADERS = { 'Content-Type': 'application/json' };\n\nexport function healthCheckConfig(option?: HealthCheckOption): HealthCheckConfig | undefined {\n if (option === false) return undefined;\n return typeof option === 'object' ? option : {};\n}\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 static fromOption(option?: HealthCheckOption): HealthResponder | undefined {\n const config = healthCheckConfig(option);\n return config && new HealthResponder(config.path);\n }\n\n public tryRespond(req: IncomingMessage, res: ServerResponse): boolean {\n if (!this.matches(req.method, req.url)) return false;\n\n res.writeHead(HTTP_OK, JSON_HEADERS);\n res.end(this.body());\n return true;\n }\n\n public answer(request: Request, ready: boolean): Response | undefined {\n if (!this.matches(request.method, new URL(request.url).pathname)) return undefined;\n if (!ready) return new Response(null, { status: HTTP_SERVICE_UNAVAILABLE });\n return new Response(this.body(), { status: HTTP_OK, headers: JSON_HEADERS });\n }\n\n private matches(method: string | undefined, path: string | undefined): boolean {\n return method === 'GET' && path === this.path;\n }\n\n private body(): string {\n return JSON.stringify({ status: 'ok', timestamp: Date.now() });\n }\n}\n","import { createServer } from 'node:http';\n\nimport { paint, SeedcordErrorCode } from '@seedcord/errors';\nimport { SeedcordError } from '@seedcord/errors/internal';\nimport { Logger } from '@seedcord/logger';\nimport { isPortInUse } from '@seedcord/utils/internal';\n\nimport { ShutdownPhase } from '#src/lifecycle/phases';\n\nimport { healthCheckConfig, 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 const config = healthCheckConfig(option);\n return config && new HealthCheck(shutdown, config);\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 = (error: Error): void => {\n if (isPortInUse(error)) {\n reject(\n new SeedcordError(SeedcordErrorCode.PortInUse, [this._port, 'healthCheck.port'], {\n cause: error\n })\n );\n return;\n }\n reject(error);\n };\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(paint.check)} 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 emits '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 { botConfigDir, setBuiltFilesHook } from '@seedcord/utils/internal';\nimport { Envapter } from 'envapt';\n\nexport function setEnvaptBaseDir(): void {\n setBuiltFilesHook(() => {\n const configDir = botConfigDir();\n if (configDir !== undefined && Envapter.source.supportsFiles) Envapter.baseDir = configDir;\n });\n}\n","import { assertDeclaredRuntime } from '#node/assertRuntimeVersion';\n\n// the edge bundle never reaches this entry\nassertDeclaredRuntime();\n\nexport * from '#node/Lifecycle';\nexport * from '#node/HealthCheck';\nexport { HealthResponder } from '#node/HealthResponder';\nexport { settleWithin } from '#src/lifecycle/withTimeout';\nexport { shutdownOf } from '#node/ServerHost';\nexport { setEnvaptBaseDir } from '#node/envaptBaseDir';\n"],"mappings":"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"}
@@ -1,3 +1,3 @@
1
- import { n as CoordinatedStartup, t as CoordinatedShutdown } from "./CoordinatedShutdown-H5YjZn8A.mjs";
2
- import { t as ServerHost } from "./ServerHost-DYeh0KTk.mjs";
3
- export { CoordinatedShutdown, CoordinatedStartup, ServerHost };
1
+ import { i as StartupTasks, n as ShutdownTasks, r as CoordinatedStartup, t as CoordinatedShutdown } from "./CoordinatedShutdown-BVtNNSsF.mjs";
2
+ import { t as ServerHost } from "./ServerHost-whpI0h4d.mjs";
3
+ export { CoordinatedShutdown, CoordinatedStartup, ServerHost, type ShutdownTasks, type StartupTasks };
@@ -1,7 +1,7 @@
1
- import { t as ShutdownPhase } from "./phases-BauKJOTT.mjs";
2
- import { n as settleWithin } from "./PluginLifecycle-AhQ46NIr.mjs";
3
- import { n as CoordinatedLifecycle, t as CoordinatedStartup } from "./CoordinatedStartup-CGnJrqcl.mjs";
4
- import { t as ServerHost } from "./ServerHost-CtmAhEXn.mjs";
1
+ import { v as ShutdownPhase } from "./Bus-W1K-uMeO.mjs";
2
+ import { n as settleWithin } from "./PluginLifecycle-DdwRzcDj.mjs";
3
+ import { n as CoordinatedLifecycle, t as CoordinatedStartup } from "./CoordinatedStartup-DA3gW8op.mjs";
4
+ import { t as ServerHost } from "./ServerHost-DHg-Y2_q.mjs";
5
5
  import { SeedcordErrorCode, paint } from "@seedcord/errors";
6
6
  import { SeedcordRangeError } from "@seedcord/errors/internal";
7
7
 
@@ -10,15 +10,16 @@ const PHASE_ORDER = [
10
10
  1,
11
11
  2,
12
12
  3,
13
- 4
13
+ 4,
14
+ 5
14
15
  ];
16
+ const SHUTDOWN_SIGNALS = ["SIGTERM", "SIGINT"];
15
17
  const LOG_FLUSH_DELAY_MS = 3e3;
16
18
  const DEFAULT_SHUTDOWN_DEADLINE_MS = 25e3;
17
19
  var CoordinatedShutdown = class extends CoordinatedLifecycle {
18
20
  #running;
19
21
  #exitCode = 0;
20
- #onSigTerm = null;
21
- #onSigInt = null;
22
+ #signalHandlers = /* @__PURE__ */ new Map();
22
23
  #startupGate;
23
24
  #deadlineMs = DEFAULT_SHUTDOWN_DEADLINE_MS;
24
25
  #phasesExpireAt = Infinity;
@@ -59,28 +60,21 @@ var CoordinatedShutdown = class extends CoordinatedLifecycle {
59
60
  }
60
61
  /** @internal */
61
62
  registerSignalHandlers() {
62
- this.#onSigTerm = () => {
63
- this.logger.info(`Received ${paint.amber.bold("SIGTERM")} signal`);
64
- this.run(0);
65
- };
66
- this.#onSigInt = () => {
67
- this.logger.info(`Received ${paint.amber.bold("SIGINT")} signal`);
68
- this.run(0);
69
- };
70
- process.on("SIGTERM", this.#onSigTerm);
71
- process.on("SIGINT", this.#onSigInt);
63
+ for (const signal of SHUTDOWN_SIGNALS) {
64
+ const handler = () => {
65
+ this.logger.info(`Received ${paint.amber.bold(signal)} signal`);
66
+ this.run(0);
67
+ };
68
+ this.#signalHandlers.set(signal, handler);
69
+ process.on(signal, handler);
70
+ }
72
71
  }
73
72
  /** @internal */
74
73
  removeSignalHandlers() {
75
- if (this.#onSigTerm) {
76
- process.off("SIGTERM", this.#onSigTerm);
77
- this.#onSigTerm = null;
78
- }
79
- if (!this.#onSigInt) return;
80
- process.off("SIGINT", this.#onSigInt);
81
- this.#onSigInt = null;
74
+ for (const [signal, handler] of this.#signalHandlers) process.off(signal, handler);
75
+ this.#signalHandlers.clear();
82
76
  }
83
- /** @internal run() awaits this so boot finishes registering its dispose tasks first */
77
+ /** @internal */
84
78
  gateOnStartup(settled) {
85
79
  this.#startupGate = settled;
86
80
  }
@@ -109,10 +103,10 @@ var CoordinatedShutdown = class extends CoordinatedLifecycle {
109
103
  if (this.#startupGate) await settleWithin(this.#startupGate, this.#deadlineMs);
110
104
  const failures = [];
111
105
  await settleWithin(this.#runPhases(failures), Math.max(this.#phasesExpireAt - performance.now(), 0));
112
- const caughtPhase = this.#runningPhase;
113
- 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])}`);
106
+ const unfinishedPhase = this.#runningPhase;
107
+ if (unfinishedPhase !== void 0) this.logger.error(`Shutdown deadline of ${paint.sky.bold(this.#deadlineMs)}ms elapsed at phase ${paint.iris.bold(this.phaseEnum[unfinishedPhase])}`);
114
108
  if (failures.length > 0) this.logger.error(`${paint.coral.bold("Coordinated shutdown failed")}`, ...failures);
115
- else if (caughtPhase === void 0) this.logger.info(`${paint.mint.bold("Coordinated shutdown completed")} successfully`);
109
+ else if (unfinishedPhase === void 0) this.logger.info(`${paint.mint.bold("Coordinated shutdown completed")} successfully`);
116
110
  } finally {
117
111
  if (exitProcess) {
118
112
  this.logger.debug(`${paint.coral.bold("Exiting")} process with code ${paint.sky.bold(this.#exitCode)}`);
@@ -1 +1 @@
1
- {"version":3,"file":"node.index.mjs","names":[],"sources":["../src/node/Lifecycle/CoordinatedShutdown.ts"],"sourcesContent":["import { SeedcordErrorCode, paint } from '@seedcord/errors';\nimport { SeedcordRangeError } from '@seedcord/errors/internal';\n\nimport { CoordinatedLifecycle } from '#src/lifecycle/CoordinatedLifecycle';\nimport { ShutdownPhase } from '#src/lifecycle/phases';\nimport { settleWithin } from '#src/lifecycle/withTimeout';\n\nimport type { LifecycleTask } from '#src/lifecycle/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 #running?: Promise<void>;\n #exitCode = 0;\n #onSigTerm: (() => void) | null = null;\n #onSigInt: (() => void) | null = null;\n #startupGate?: Promise<void>;\n #deadlineMs = DEFAULT_SHUTDOWN_DEADLINE_MS;\n #phasesExpireAt = Infinity;\n #runningPhase: ShutdownPhase | undefined;\n\n public constructor(deadlineMs?: number) {\n super('Shutdown', PHASE_ORDER, ShutdownPhase);\n\n if (deadlineMs !== undefined) this.setDeadline(deadlineMs);\n }\n\n /** @internal */\n public setDeadline(deadlineMs: number): void {\n if (!Number.isFinite(deadlineMs) || deadlineMs <= 0) {\n throw new SeedcordRangeError(SeedcordErrorCode.LifecycleInvalidTimeout, [\n 'lifecycle.shutdownDeadline',\n deadlineMs\n ]);\n }\n this.#deadlineMs = deadlineMs;\n }\n\n 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 /** @internal */\n public 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 return;\n }\n\n process.off('SIGINT', this.#onSigInt);\n this.#onSigInt = null;\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(\n phase: ShutdownPhase,\n taskName: string,\n task: LifecycleTask['task'],\n timeoutMs = 5000\n ): 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 // a later call waits on the first run and never repeats a task\n /** @internal */\n public run(exitCode = 0, exitProcess = true): Promise<void> {\n this.removeSignalHandlers();\n\n if (this.#running) {\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 return this.#running;\n }\n\n this.#exitCode = exitCode;\n this.#running = this.#shutDown(exitProcess);\n return this.#running;\n }\n\n async #shutDown(exitProcess: boolean): Promise<void> {\n this.logger.info(\n `${paint.amber.bold('Starting')} coordinated shutdown with exit code ${paint.sky.bold(this.#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 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 }\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"node.index.mjs","names":[],"sources":["../src/node/Lifecycle/CoordinatedShutdown.ts"],"sourcesContent":["import { SeedcordErrorCode, paint } from '@seedcord/errors';\nimport { SeedcordRangeError } from '@seedcord/errors/internal';\n\nimport { CoordinatedLifecycle } from '#src/lifecycle/CoordinatedLifecycle';\nimport { ShutdownPhase } from '#src/lifecycle/phases';\nimport { settleWithin } from '#src/lifecycle/withTimeout';\n\nimport type { LifecycleTask } from '#src/lifecycle/LifecycleTypes';\nimport type { TaskShutdownPhase } from '#src/lifecycle/phases';\nimport type { Tail } from '@seedcord/types';\n\n/** Adds work to a node bot's shutdown, in any phase before `ShutdownPhase.Release`. */\nexport interface ShutdownTasks {\n addTask(phase: TaskShutdownPhase, ...rest: Tail<Parameters<CoordinatedShutdown['addTask']>>): void;\n}\n\nconst PHASE_ORDER: ShutdownPhase[] = [\n ShutdownPhase.Unbind,\n ShutdownPhase.Drain,\n ShutdownPhase.Disconnect,\n ShutdownPhase.Logout,\n ShutdownPhase.Release\n];\n\nconst SHUTDOWN_SIGNALS: NodeJS.Signals[] = ['SIGTERM', 'SIGINT'];\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 #running?: Promise<void>;\n #exitCode = 0;\n readonly #signalHandlers = new Map<NodeJS.Signals, () => void>();\n #startupGate?: Promise<void>;\n #deadlineMs = DEFAULT_SHUTDOWN_DEADLINE_MS;\n #phasesExpireAt = Infinity;\n #runningPhase: ShutdownPhase | undefined;\n\n public constructor(deadlineMs?: number) {\n super('Shutdown', PHASE_ORDER, ShutdownPhase);\n\n if (deadlineMs !== undefined) this.setDeadline(deadlineMs);\n }\n\n /** @internal */\n public setDeadline(deadlineMs: number): void {\n if (!Number.isFinite(deadlineMs) || deadlineMs <= 0) {\n throw new SeedcordRangeError(SeedcordErrorCode.LifecycleInvalidTimeout, [\n 'lifecycle.shutdownDeadline',\n deadlineMs\n ]);\n }\n this.#deadlineMs = deadlineMs;\n }\n\n async #runPhases(failures: unknown[]): Promise<void> {\n for (const phase of PHASE_ORDER) {\n // set first so the deadline log shows this phase\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 /** @internal */\n public registerSignalHandlers(): void {\n for (const signal of SHUTDOWN_SIGNALS) {\n const handler = (): void => {\n this.logger.info(`Received ${paint.amber.bold(signal)} signal`);\n void this.run(0);\n };\n this.#signalHandlers.set(signal, handler);\n process.on(signal, handler);\n }\n }\n\n /** @internal */\n public removeSignalHandlers(): void {\n for (const [signal, handler] of this.#signalHandlers) process.off(signal, handler);\n this.#signalHandlers.clear();\n }\n\n // run() awaits this so boot finishes registering its dispose tasks first\n /** @internal */\n public gateOnStartup(settled: Promise<void>): void {\n this.#startupGate = settled;\n }\n\n public override addTask(\n phase: ShutdownPhase,\n taskName: string,\n task: LifecycleTask['task'],\n timeoutMs = 5000\n ): 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 // a later call waits on the first run and never repeats a task\n /** @internal */\n public run(exitCode = 0, exitProcess = true): Promise<void> {\n this.removeSignalHandlers();\n\n if (this.#running) {\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 return this.#running;\n }\n\n this.#exitCode = exitCode;\n this.#running = this.#shutDown(exitProcess);\n return this.#running;\n }\n\n async #shutDown(exitProcess: boolean): Promise<void> {\n this.logger.info(\n `${paint.amber.bold('Starting')} coordinated shutdown with exit code ${paint.sky.bold(this.#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 unfinishedPhase = this.#runningPhase;\n if (unfinishedPhase !== undefined) {\n this.logger.error(\n `Shutdown deadline of ${paint.sky.bold(this.#deadlineMs)}ms elapsed at phase ${paint.iris.bold(this.phaseEnum[unfinishedPhase])}`\n );\n }\n\n if (failures.length > 0) {\n this.logger.error(`${paint.coral.bold('Coordinated shutdown failed')}`, ...failures);\n } else if (unfinishedPhase === undefined) {\n this.logger.info(`${paint.mint.bold('Coordinated shutdown completed')} successfully`);\n }\n } finally {\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 }\n }\n }\n}\n"],"mappings":";;;;;;;;AAgBA,MAAM,cAA+B;;;;;;AAMrC;AAEA,MAAM,mBAAqC,CAAC,WAAW,QAAQ;AAG/D,MAAM,qBAAqB;AAG3B,MAAM,+BAA+B;AAErC,IAAa,sBAAb,cAAyC,qBAAoC;CACzE;CACA,YAAY;CACZ,AAAS,kCAAkB,IAAI,IAAgC;CAC/D;CACA,cAAc;CACd,kBAAkB;CAClB;CAEA,AAAO,YAAY,YAAqB;EACpC,MAAM,YAAY,aAAa,aAAa;EAE5C,IAAI,eAAe,QAAW,KAAK,YAAY,UAAU;CAC7D;;CAGA,AAAO,YAAY,YAA0B;EACzC,IAAI,CAAC,OAAO,SAAS,UAAU,KAAK,cAAc,GAC9C,MAAM,IAAI,mBAAmB,kBAAkB,yBAAyB,CACpE,8BACA,UACJ,CAAC;EAEL,KAAK,cAAc;CACvB;CAEA,MAAM,WAAW,UAAoC;EACjD,KAAK,MAAM,SAAS,aAAa;GAE7B,KAAK,gBAAgB;GAErB,IAAI,YAAY,IAAI,KAAK,KAAK,iBAAiB;GAC/C,IAAI;IACA,MAAM,KAAK,SAAS,KAAK;GAC7B,SAAS,OAAO;IACZ,SAAS,KAAK,KAAK;GACvB;EACJ;EACA,KAAK,gBAAgB;CACzB;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;;CAGA,AAAO,yBAA+B;EAClC,KAAK,MAAM,UAAU,kBAAkB;GACnC,MAAM,gBAAsB;IACxB,KAAK,OAAO,KAAK,YAAY,MAAM,MAAM,KAAK,MAAM,EAAE,QAAQ;IAC9D,AAAK,KAAK,IAAI,CAAC;GACnB;GACA,KAAK,gBAAgB,IAAI,QAAQ,OAAO;GACxC,QAAQ,GAAG,QAAQ,OAAO;EAC9B;CACJ;;CAGA,AAAO,uBAA6B;EAChC,KAAK,MAAM,CAAC,QAAQ,YAAY,KAAK,iBAAiB,QAAQ,IAAI,QAAQ,OAAO;EACjF,KAAK,gBAAgB,MAAM;CAC/B;;CAIA,AAAO,cAAc,SAA8B;EAC/C,KAAK,eAAe;CACxB;CAEA,AAAgB,QACZ,OACA,UACA,MACA,YAAY,KACR;EACJ,MAAM,QAAQ,OAAO,UAAU,MAAM,SAAS;CAClD;;CAGA,AAAgB,WAAW,OAAsB,UAA2B;EACxE,OAAO,MAAM,WAAW,OAAO,QAAQ;CAC3C;;CAIA,AAAO,IAAI,WAAW,GAAG,cAAc,MAAqB;EACxD,KAAK,qBAAqB;EAE1B,IAAI,KAAK,UAAU;GAEf,IAAI,WAAW,KAAK,WAAW,KAAK,YAAY;GAChD,OAAO,KAAK;EAChB;EAEA,KAAK,YAAY;EACjB,KAAK,WAAW,KAAK,UAAU,WAAW;EAC1C,OAAO,KAAK;CAChB;CAEA,MAAM,UAAU,aAAqC;EACjD,KAAK,OAAO,KACR,GAAG,MAAM,MAAM,KAAK,UAAU,EAAE,uCAAuC,MAAM,IAAI,KAAK,KAAK,SAAS,GACxG;EAEA,IAAI;GACA,KAAK,kBAAkB,YAAY,IAAI,IAAI,KAAK;GAEhD,IAAI,KAAK,cAAc,MAAM,aAAa,KAAK,cAAc,KAAK,WAAW;GAE7E,MAAM,WAAsB,CAAC;GAC7B,MAAM,aAAa,KAAK,WAAW,QAAQ,GAAG,KAAK,IAAI,KAAK,kBAAkB,YAAY,IAAI,GAAG,CAAC,CAAC;GAEnG,MAAM,kBAAkB,KAAK;GAC7B,IAAI,oBAAoB,QACpB,KAAK,OAAO,MACR,wBAAwB,MAAM,IAAI,KAAK,KAAK,WAAW,EAAE,sBAAsB,MAAM,KAAK,KAAK,KAAK,UAAU,gBAAgB,GAClI;GAGJ,IAAI,SAAS,SAAS,GAClB,KAAK,OAAO,MAAM,GAAG,MAAM,MAAM,KAAK,6BAA6B,KAAK,GAAG,QAAQ;QAChF,IAAI,oBAAoB,QAC3B,KAAK,OAAO,KAAK,GAAG,MAAM,KAAK,KAAK,gCAAgC,EAAE,cAAc;EAE5F,UAAU;GACN,IAAI,aAAa;IACb,KAAK,OAAO,MAAM,GAAG,MAAM,MAAM,KAAK,SAAS,EAAE,qBAAqB,MAAM,IAAI,KAAK,KAAK,SAAS,GAAG;IACtG,iBAAiB;KACb,QAAQ,KAAK,KAAK,SAAS;IAC/B,GAAG,kBAAkB;GACzB,OACI,KAAK,OAAO,MAAM,GAAG,MAAM,MAAM,KAAK,UAAU,EAAE,yBAAyB;EAEnF;CACJ;AACJ"}
@@ -3,8 +3,8 @@ import { Logger } from "@seedcord/logger";
3
3
  import { TypedEventEmitter } from "@seedcord/event-emitter";
4
4
  import { REST } from "@discordjs/rest";
5
5
  import { UUID } from "@seedcord/types/internal";
6
- import { Config, DispatchBag, DispatchState, EventFrequency, IRateLimiter, RenderContext, ReplyResponse, TypedExclude } from "@seedcord/types";
7
- import { Constructor } from "type-fest";
6
+ import { Config, DispatchBag, DispatchState, EventFrequency, IRateLimiter, KvDriver, RenderContext, ReplyResponse, TypedExclude } from "@seedcord/types";
7
+ import { Constructor, Jsonify, Simplify } from "type-fest";
8
8
  //#region src/stops/Notice.d.ts
9
9
  /**
10
10
  * Base class for a user-facing refusal or a reported fault.
@@ -104,15 +104,15 @@ declare const WebhookUrlMetadataKey: unique symbol;
104
104
  declare const InteractionRouteKeys: Record<InteractionKind, symbol>;
105
105
  //#endregion
106
106
  //#region src/dispatch/DispatchContext.d.ts
107
- /**
108
- * A typed bag allocated once per interaction dispatch and handed to the handler. The middleware and i18n
109
- * work reads and writes it through {@link DispatchState}. Today the bag is empty, so it only carries the
110
- * dispatched `routeId`.
111
- */
107
+ /** State shared by middleware and handlers during one dispatch. */
112
108
  declare class DispatchContext implements DispatchBag {
113
- readonly routeId: string;
114
109
  private readonly state;
115
110
  readonly id: `${string}-${string}-${string}-${string}-${string}`;
111
+ /**
112
+ * The dispatch identifier, such as `slash:search/docs`, `button:approve`, or `event:messageCreate`.
113
+ * Component identifiers contain the custom-ID prefix without encoded params.
114
+ */
115
+ readonly routeId: string;
116
116
  constructor(routeId: string);
117
117
  set<Key extends keyof DispatchState>(key: Key, value: DispatchState[Key]): void;
118
118
  get<Key extends keyof DispatchState>(key: Key): DispatchState[Key] | undefined;
@@ -124,6 +124,97 @@ declare class DispatchContext implements DispatchBag {
124
124
  require<Key extends keyof DispatchState>(key: Key): Exclude<DispatchState[Key], undefined>;
125
125
  }
126
126
  //#endregion
127
+ //#region src/kv/KeyedQueue.d.ts
128
+ declare class KeyedQueue {
129
+ private readonly tails;
130
+ run<TResult>(key: string, task: () => Promise<TResult>): Promise<TResult>;
131
+ }
132
+ //#endregion
133
+ //#region src/kv/KvCollection.d.ts
134
+ type Stored<TRecord> = Simplify<Jsonify<TRecord>>;
135
+ /**
136
+ * The records of one type, stored under one name in `config.kv`. Get one from
137
+ * `kv.collection<T>('name')`.
138
+ *
139
+ * Every record goes through JSON on its way in and out. A `Date` you store reads back from `get()`
140
+ * as an ISO string.
141
+ *
142
+ * @example
143
+ * ```ts
144
+ * interface Profile { bio: string; joinedAt: Date }
145
+ * const profiles = this.core.kv.collection<Profile>('profile');
146
+ *
147
+ * await profiles.set(user.id, { bio: 'hi', joinedAt: new Date() });
148
+ * const profile = await profiles.get(user.id); // { bio: string; joinedAt: string } | undefined
149
+ * await profiles.delete(user.id);
150
+ * ```
151
+ */
152
+ declare class KvCollection<TRecord> {
153
+ #private;
154
+ constructor(driver: KvDriver, prefix: string, updates: KeyedQueue);
155
+ /** Reads the record stored under `id`. Resolves to `undefined` when nothing is there or the record expired. */
156
+ get(id: string): Promise<Stored<TRecord> | undefined>;
157
+ /**
158
+ * Stores `value` under `id` and replaces whatever was there.
159
+ *
160
+ * @param ttlMs - How long the record lasts, in milliseconds. Leave it out to keep the record until you delete it.
161
+ */
162
+ set(id: string, value: TRecord, ttlMs?: number): Promise<void>;
163
+ /** Removes the record under `id`. */
164
+ delete(id: string): Promise<void>;
165
+ /**
166
+ * Stores what `next` returns for the record under `id`, then resolves to that value. `next` gets
167
+ * the current record, or `undefined` when there is none.
168
+ *
169
+ * Use it over a `get()` then a `set()` when two calls can change one record at the same time.
170
+ * `set()`, `delete()`, and `update()` calls on one id run one after the other in this process.
171
+ * None of them loses another's change. Two processes sharing one backend can still both read
172
+ * before either writes.
173
+ *
174
+ * @param ttlMs - How many milliseconds the stored record lasts. Pass a function to work it out from the record. Leave it out to keep the record until you delete it.
175
+ * @example
176
+ * ```ts
177
+ * // counts a run of the command and resolves to the new count
178
+ * const runs = await counts.update(user.id, (count) => (count ?? 0) + 1);
179
+ * ```
180
+ */
181
+ update(id: string, next: (current: Stored<TRecord> | undefined) => TRecord, ttlMs?: number | ((value: TRecord) => number)): Promise<TRecord>;
182
+ /**
183
+ * Yields `[id, record]` for every record in this collection that hasn't expired. Use it to read
184
+ * back what the bot saved before a restart.
185
+ *
186
+ * @throws A **SeedcordError** when the backend can't list its keys, like `@keyv/memcache`.
187
+ * @example
188
+ * ```ts
189
+ * // schedules every saved reminder again after a restart
190
+ * for await (const [id, reminder] of reminders.entries()) schedule(id, reminder);
191
+ * ```
192
+ */
193
+ entries(): AsyncIterable<[id: string, record: Stored<TRecord>]>;
194
+ }
195
+ //#endregion
196
+ //#region src/kv/Kv.d.ts
197
+ type ConfiguredDriver = NonNullable<Config['kv']>;
198
+ type JsonSafe<TValue> = TValue extends string | number | boolean | null | undefined ? TValue : TValue extends {
199
+ toJSON(): unknown;
200
+ } ? TValue : TValue extends ((...args: never[]) => unknown) ? never : TValue extends ReadonlyMap<unknown, unknown> | ReadonlySet<unknown> ? never : TValue extends readonly (infer Item)[] ? readonly JsonSafe<Item>[] : TValue extends object ? { [K in keyof TValue]: JsonSafe<TValue[K]>; } : never;
201
+ type CollectionName<TRecord> = unknown extends TRecord ? 'Pass the record type, like kv.collection<Profile>(name).' : [TRecord] extends [JsonSafe<TRecord>] ? string : 'This record type holds a value JSON cannot keep, like a function, Map, Set, or bigint.';
202
+ /**
203
+ * The bot's key-value store, or one prefixed slice of it. `core.kv` holds your handlers' records. A
204
+ * plugin's `this.kv` holds the plugin's.
205
+ */
206
+ declare class Kv {
207
+ #private;
208
+ /** What you passed as `config.kv`. Register its type in `KvTypes` to call the library's own methods on it. */
209
+ readonly driver: ConfiguredDriver;
210
+ constructor(driver: ConfiguredDriver, ...path: string[]);
211
+ /**
212
+ * Records of one type under `name`. Pass the type argument. A record type holding a function,
213
+ * `Map`, `Set`, or `bigint` fails to compile here.
214
+ */
215
+ collection<TRecord>(name: CollectionName<TRecord>): KvCollection<TRecord>;
216
+ }
217
+ //#endregion
127
218
  //#region src/subscribers/publishDefault.d.ts
128
219
  /** @internal PublishableKey excludes this key so publish() never emits it externally. */
129
220
  declare const PublishDefault: unique symbol;
@@ -392,7 +483,7 @@ declare class Bus extends TypedEventEmitter<SubscriptionTuples> {
392
483
  /** @internal */
393
484
  readonly [loggerSlot]: Logger;
394
485
  private readonly subscribersMap;
395
- private readonly executedOnce;
486
+ private readonly onceRuns;
396
487
  private readonly webhookProbes;
397
488
  constructor(core: CoreBase);
398
489
  /** @internal keep this inside start(). a bad webhook url thrown outside it never resets the singleton guard */
@@ -460,6 +551,8 @@ interface CoreBase {
460
551
  readonly rateLimiter: IRateLimiter;
461
552
  /** Publish and subscribe to framework and application events. */
462
553
  readonly bus: Bus;
554
+ /** Your handlers' records in `config.kv`, under the `app:` prefix. */
555
+ readonly kv: Kv;
463
556
  }
464
557
  //#endregion
465
558
  //#region src/lifecycle/phases.d.ts
@@ -485,8 +578,14 @@ declare enum ShutdownPhase {
485
578
  /** External resources close. Plugin dispose runs here by default. */
486
579
  Disconnect = 3,
487
580
  /** The client disconnects last so in-flight handlers can still reference it during drain. */
488
- Logout = 4
581
+ Logout = 4,
582
+ /**
583
+ * Resources seedcord holds for the bot, like the `config.kv` driver, close after every plugin
584
+ * has stopped. Only seedcord runs work here.
585
+ */
586
+ Release = 5
489
587
  }
588
+ type TaskShutdownPhase = Exclude<ShutdownPhase, ShutdownPhase.Release>;
490
589
  //#endregion
491
- export { RegisteredCount as A, InteractionMetadataKey as B, AckState as C, AckTrace as D, sendTarget as E, CommandMetadataKey as F, WebhookUrlMetadataKey as G, InteractionRouteKeys as H, EventMetadataKey as I, Notice as K, EventMiddlewareMetadataKey as L, VerifyWebhooks as M, PublishDefault as N, RegisterDefaults as O, DispatchContext as P, GatedMetadataKey as R, reportedWrite as S, checkAckLegality as T, MiddlewareKind as U, InteractionMiddlewareMetadataKey as V, SubscribeMetadataKey as W, ResponseSent as _, StoredSubscriberCtor as a, Subscriptions as b, registrationFor as c, DefaultSubscriptions as d, DispatchOutcome as f, ResponseOutcome as g, ResponseFailed as h, Bus as i, UnregisterSubscriber as j, RegisterSubscriber as k, Subscriber as l, ResponseAttempt as m, StartupPhase as n, SubscriberRegistration as o, FaultSource as p, CoreBase as r, busLoggerOf as s, ShutdownPhase as t, AttemptedWrite as u, SubscriptionData as v, ReplyMethod as w, ReplyTelemetry as x, SubscriptionKey as y, InteractionKind as z };
492
- //# sourceMappingURL=phases-FTM69J3a.d.mts.map
590
+ export { RegisterSubscriber as A, EventMiddlewareMetadataKey as B, reportedWrite as C, sendTarget as D, checkAckLegality as E, Kv as F, InteractionRouteKeys as G, InteractionKind as H, KvCollection as I, WebhookUrlMetadataKey as J, MiddlewareKind as K, DispatchContext as L, UnregisterSubscriber as M, VerifyWebhooks as N, AckTrace as O, PublishDefault as P, CommandMetadataKey as R, ReplyTelemetry as S, ReplyMethod as T, InteractionMetadataKey as U, GatedMetadataKey as V, InteractionMiddlewareMetadataKey as W, Notice as Y, ResponseOutcome as _, Bus as a, SubscriptionKey as b, busLoggerOf as c, AttemptedWrite as d, DefaultSubscriptions as f, ResponseFailed as g, ResponseAttempt as h, CoreBase as i, RegisteredCount as j, RegisterDefaults as k, registrationFor as l, FaultSource as m, StartupPhase as n, StoredSubscriberCtor as o, DispatchOutcome as p, SubscribeMetadataKey as q, TaskShutdownPhase as r, SubscriberRegistration as s, ShutdownPhase as t, Subscriber as u, ResponseSent as v, AckState as w, Subscriptions as x, SubscriptionData as y, EventMetadataKey as z };
591
+ //# sourceMappingURL=phases-Dojnr-ng.d.mts.map
@@ -1,3 +1,4 @@
1
- import { f as PluginLifecycleSpec, n as PluginHost, s as Plugin, u as PluginOptions } from "./PluginHost-DB4j8unu.mjs";
1
+ import { n as PluginTypes, t as Generated } from "./Generated-ByC53_A2.mjs";
2
+ import { n as PluginHost, p as PluginLifecycleSpec, s as Plugin, u as PluginOptions } from "./PluginHost-DWBgBS6Q.mjs";
2
3
  import { HmrAware, HmrUpdateEvent } from "@seedcord/types";
3
- export { type HmrAware, type HmrUpdateEvent, Plugin, PluginHost, type PluginLifecycleSpec, type PluginOptions };
4
+ export { type Generated, type HmrAware, type HmrUpdateEvent, Plugin, PluginHost, type PluginLifecycleSpec, type PluginOptions, type PluginTypes };
@@ -1,3 +1,3 @@
1
- import { a as Plugin, t as PluginHost } from "./PluginHost-B8kxLOeb.mjs";
1
+ import { o as Plugin, t as PluginHost } from "./PluginHost-CYe-UVpp.mjs";
2
2
 
3
3
  export { Plugin, PluginHost };