@seedcord/core 0.11.0 → 0.12.0-next.2

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 (54) hide show
  1. package/README.md +3 -3
  2. package/dist/Bus-VJw-QpoY.mjs +986 -0
  3. package/dist/Bus-VJw-QpoY.mjs.map +1 -0
  4. package/dist/{CoordinatedShutdown-H5YjZn8A.d.mts → CoordinatedShutdown-fPKLWjc5.d.mts} +13 -4
  5. package/dist/{CoordinatedStartup-CGnJrqcl.mjs → CoordinatedStartup-Zw_Lk7Zk.mjs} +22 -20
  6. package/dist/CoordinatedStartup-Zw_Lk7Zk.mjs.map +1 -0
  7. package/dist/Generated-ByC53_A2.d.mts +26 -0
  8. package/dist/HmrModuleHandler-Btgu8r96.mjs +165 -0
  9. package/dist/HmrModuleHandler-Btgu8r96.mjs.map +1 -0
  10. package/dist/{PluginHost-DB4j8unu.d.mts → PluginHost-DFnkIldj.d.mts} +43 -15
  11. package/dist/PluginHost-DTtxWRiL.mjs +552 -0
  12. package/dist/PluginHost-DTtxWRiL.mjs.map +1 -0
  13. package/dist/{PluginLifecycle-AhQ46NIr.mjs → PluginLifecycle-hx5zUc-P.mjs} +4 -4
  14. package/dist/PluginLifecycle-hx5zUc-P.mjs.map +1 -0
  15. package/dist/{ServerHost-DYeh0KTk.d.mts → ServerHost-B5o3ScCF.d.mts} +7 -6
  16. package/dist/{ServerHost-CtmAhEXn.mjs → ServerHost-CDaEDR4C.mjs} +17 -6
  17. package/dist/{ServerHost-CtmAhEXn.mjs.map → ServerHost-CDaEDR4C.mjs.map} +1 -1
  18. package/dist/bindViteEnv-NFzvdqlT.mjs +32 -0
  19. package/dist/{bindViteEnv-DWq5rftJ.mjs.map → bindViteEnv-NFzvdqlT.mjs.map} +1 -1
  20. package/dist/env-internal.index.mjs +1 -1
  21. package/dist/hmr.index.d.mts +20 -20
  22. package/dist/hmr.index.mjs +1 -1
  23. package/dist/{index-Du71Xx69.d.mts → index-DGHng1IN.d.mts} +115 -201
  24. package/dist/index.d.mts +321 -74
  25. package/dist/index.mjs +323 -102
  26. package/dist/index.mjs.map +1 -1
  27. package/dist/internal.index.d.mts +223 -83
  28. package/dist/internal.index.mjs +573 -74
  29. package/dist/internal.index.mjs.map +1 -1
  30. package/dist/node-internal.index.d.mts +8 -4
  31. package/dist/node-internal.index.mjs +19 -6
  32. package/dist/node-internal.index.mjs.map +1 -1
  33. package/dist/node.index.d.mts +3 -3
  34. package/dist/node.index.mjs +22 -28
  35. package/dist/node.index.mjs.map +1 -1
  36. package/dist/{phases-FTM69J3a.d.mts → phases-C2QCSqRu.d.mts} +164 -55
  37. package/dist/plugin.index.d.mts +3 -2
  38. package/dist/plugin.index.mjs +1 -1
  39. package/dist/subscribers-CGuVjbrg.mjs +982 -0
  40. package/dist/subscribers-CGuVjbrg.mjs.map +1 -0
  41. package/dist/{bindViteEnv-DWq5rftJ.mjs → usingCtx-Cf2cZi96.mjs} +1 -31
  42. package/package.json +14 -11
  43. package/dist/CoordinatedStartup-CGnJrqcl.mjs.map +0 -1
  44. package/dist/HmrModuleHandler-8ZC-UuZh.mjs +0 -162
  45. package/dist/HmrModuleHandler-8ZC-UuZh.mjs.map +0 -1
  46. package/dist/PluginHost-B8kxLOeb.mjs +0 -245
  47. package/dist/PluginHost-B8kxLOeb.mjs.map +0 -1
  48. package/dist/PluginLifecycle-AhQ46NIr.mjs.map +0 -1
  49. package/dist/phases-BauKJOTT.mjs +0 -31
  50. package/dist/phases-BauKJOTT.mjs.map +0 -1
  51. package/dist/publishDefault-D3dAyzZ9.mjs +0 -7
  52. package/dist/publishDefault-D3dAyzZ9.mjs.map +0 -1
  53. package/dist/subscribers-Ce3SWy23.mjs +0 -1913
  54. package/dist/subscribers-Ce3SWy23.mjs.map +0 -1
@@ -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-fPKLWjc5.mjs";
2
+ import { t as ServerHost } from "./ServerHost-B5o3ScCF.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 { y as ShutdownPhase } from "./Bus-VJw-QpoY.mjs";
2
+ import { n as settleWithin } from "./PluginLifecycle-hx5zUc-P.mjs";
3
+ import { n as CoordinatedLifecycle, t as CoordinatedStartup } from "./CoordinatedStartup-Zw_Lk7Zk.mjs";
4
+ import { t as ServerHost } from "./ServerHost-CDaEDR4C.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":"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"}
@@ -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.
@@ -34,7 +34,7 @@ import { Constructor } from "type-fest";
34
34
  * super(`balance ${balance} is below the cost`);
35
35
  * }
36
36
  *
37
- * render(_ctx: RenderContext): ReplyResponse {
37
+ * override render(_ctx: RenderContext): ReplyResponse {
38
38
  * return { components: [new TooPoorCard(this.balance).component] };
39
39
  * }
40
40
  * }
@@ -59,7 +59,7 @@ declare abstract class Notice extends Error {
59
59
  */
60
60
  ephemeral: boolean;
61
61
  /**
62
- * A short one-line reason. When every arm of an {@link or} gate refuses and each one sets this, `or`
62
+ * A short one-line reason. When every gate in an {@link or} refuses and each one sets this, `or`
63
63
  * throws a refusal listing them under `You need to meet at least one of these:`. Write a verb phrase that
64
64
  * finishes that line, for example, "be the bot owner".
65
65
  */
@@ -67,10 +67,27 @@ declare abstract class Notice extends Error {
67
67
  protected constructor(message: string, options?: ErrorOptions);
68
68
  /**
69
69
  * Builds what the user sees. Called fresh each time the denial is shown, so the builders are new
70
- * and the bot color resolves at render time.
70
+ * and the bot color resolves at render time. By default it returns seedcord's refusal card with the message as
71
+ * its text. Override it to show your own components.
71
72
  */
72
- abstract render(ctx: RenderContext): ReplyResponse;
73
+ render(_ctx: RenderContext): ReplyResponse;
73
74
  }
75
+ /**
76
+ * A Notice class that seedcord constructs and throws in place of a default refusal. It passes the data that
77
+ * refusal carries, like the retry time for `Cooldown` or the subject and permission names for a permission check.
78
+ *
79
+ * @example
80
+ * ```ts
81
+ * class OwnersOnly extends Notice {
82
+ * public constructor() {
83
+ * super('Owners only.');
84
+ * }
85
+ * }
86
+ *
87
+ * OwnerOnly({ notice: OwnersOnly });
88
+ * ```
89
+ */
90
+ type NoticeConstructor<Args extends unknown[] = []> = Constructor<Notice, Args>;
74
91
  //#endregion
75
92
  //#region src/metadataKeys.d.ts
76
93
  /**
@@ -104,16 +121,42 @@ declare const WebhookUrlMetadataKey: unique symbol;
104
121
  declare const InteractionRouteKeys: Record<InteractionKind, symbol>;
105
122
  //#endregion
106
123
  //#region src/dispatch/DispatchContext.d.ts
124
+ type DispatchKind = `${InteractionKind}` | 'event';
125
+ type InteractionDispatch = DispatchContext<InteractionKind>;
107
126
  /**
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`.
127
+ * State shared by middleware, gates, and handlers during one dispatch.
128
+ *
129
+ * @typeParam Kind - What started this dispatch, an interaction kind or `event`.
111
130
  */
112
- declare class DispatchContext implements DispatchBag {
113
- readonly routeId: string;
131
+ declare class DispatchContext<Kind extends DispatchKind = DispatchKind> implements DispatchBag {
132
+ /** What started this dispatch, an interaction kind or `event`. */
133
+ readonly kind: Kind;
134
+ /**
135
+ * The slash path, context menu name, custom-ID prefix, or event name that started this dispatch.
136
+ * For a custom ID that seedcord didn't make, it can be empty or a cut-off piece of that ID.
137
+ */
138
+ readonly route: string;
139
+ /** True when this route doesn't have a registered handler. */
140
+ readonly fallback: boolean;
114
141
  private readonly state;
115
142
  readonly id: `${string}-${string}-${string}-${string}-${string}`;
116
- constructor(routeId: string);
143
+ constructor(
144
+ /** What started this dispatch, an interaction kind or `event`. */
145
+ kind: Kind,
146
+ /**
147
+ * The slash path, context menu name, custom-ID prefix, or event name that started this dispatch.
148
+ * For a custom ID that seedcord didn't make, it can be empty or a cut-off piece of that ID.
149
+ */
150
+ route: string,
151
+ /** True when this route doesn't have a registered handler. */
152
+ fallback?: boolean);
153
+ /**
154
+ * The dispatch identifier, such as `slash:search/docs`, `button:approve`, or `event:messageCreate`.
155
+ * Component identifiers contain the custom-ID prefix without encoded params.
156
+ */
157
+ get routeId(): string;
158
+ /** The {@link routeId} of the handler's own route. It's `null` on an event and on the unhandled default. */
159
+ get declaredRoute(): string | null;
117
160
  set<Key extends keyof DispatchState>(key: Key, value: DispatchState[Key]): void;
118
161
  get<Key extends keyof DispatchState>(key: Key): DispatchState[Key] | undefined;
119
162
  /**
@@ -124,6 +167,97 @@ declare class DispatchContext implements DispatchBag {
124
167
  require<Key extends keyof DispatchState>(key: Key): Exclude<DispatchState[Key], undefined>;
125
168
  }
126
169
  //#endregion
170
+ //#region src/kv/KeyedQueue.d.ts
171
+ declare class KeyedQueue {
172
+ private readonly tails;
173
+ run<TResult>(key: string, task: () => Promise<TResult>): Promise<TResult>;
174
+ }
175
+ //#endregion
176
+ //#region src/kv/KvCollection.d.ts
177
+ type Stored<TRecord> = Simplify<Jsonify<TRecord>>;
178
+ /**
179
+ * The records of one type, stored under one name in `config.kv`. Get one from
180
+ * `kv.collection<T>('name')`.
181
+ *
182
+ * Every record goes through JSON on its way in and out. A `Date` you store reads back from `get()`
183
+ * as an ISO string.
184
+ *
185
+ * @example
186
+ * ```ts
187
+ * interface Profile { bio: string; joinedAt: Date }
188
+ * const profiles = this.core.kv.collection<Profile>('profile');
189
+ *
190
+ * await profiles.set(user.id, { bio: 'hi', joinedAt: new Date() });
191
+ * const profile = await profiles.get(user.id); // { bio: string; joinedAt: string } | undefined
192
+ * await profiles.delete(user.id);
193
+ * ```
194
+ */
195
+ declare class KvCollection<TRecord> {
196
+ #private;
197
+ constructor(driver: KvDriver, prefix: string, updates: KeyedQueue);
198
+ /** Reads the record stored under `id`. Resolves to `undefined` when nothing is there or the record expired. */
199
+ get(id: string): Promise<Stored<TRecord> | undefined>;
200
+ /**
201
+ * Stores `value` under `id` and replaces whatever was there.
202
+ *
203
+ * @param ttlMs - How long the record lasts, in milliseconds. Leave it out to keep the record until you delete it.
204
+ */
205
+ set(id: string, value: TRecord, ttlMs?: number): Promise<void>;
206
+ /** Removes the record under `id`. */
207
+ delete(id: string): Promise<void>;
208
+ /**
209
+ * Stores what `next` returns for the record under `id`, then resolves to that value. `next` gets
210
+ * the current record, or `undefined` when there is none.
211
+ *
212
+ * Use it over a `get()` then a `set()` when two calls can change one record at the same time.
213
+ * `set()`, `delete()`, and `update()` calls on one id run one after the other in this process.
214
+ * None of them loses another's change. Two processes sharing one backend can still both read
215
+ * before either writes.
216
+ *
217
+ * @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.
218
+ * @example
219
+ * ```ts
220
+ * // counts a run of the command and resolves to the new count
221
+ * const runs = await counts.update(user.id, (count) => (count ?? 0) + 1);
222
+ * ```
223
+ */
224
+ update(id: string, next: (current: Stored<TRecord> | undefined) => TRecord, ttlMs?: number | ((value: TRecord) => number)): Promise<TRecord>;
225
+ /**
226
+ * Yields `[id, record]` for every record in this collection that hasn't expired. Use it to read
227
+ * back what the bot saved before a restart.
228
+ *
229
+ * @throws A **SeedcordError** when the backend can't list its keys, like `@keyv/memcache`.
230
+ * @example
231
+ * ```ts
232
+ * // schedules every saved reminder again after a restart
233
+ * for await (const [id, reminder] of reminders.entries()) schedule(id, reminder);
234
+ * ```
235
+ */
236
+ entries(): AsyncIterable<[id: string, record: Stored<TRecord>]>;
237
+ }
238
+ //#endregion
239
+ //#region src/kv/Kv.d.ts
240
+ type ConfiguredDriver = NonNullable<Config['kv']>;
241
+ type JsonSafe<TValue> = TValue extends string | number | boolean | null | undefined ? TValue : TValue extends {
242
+ toJSON(): unknown;
243
+ } ? 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;
244
+ 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 that JSON cannot keep, like a function, Map, Set, or bigint.';
245
+ /**
246
+ * The bot's key-value store, or one prefixed slice of it. `core.kv` holds your handlers' records. A
247
+ * plugin's `this.kv` holds the plugin's.
248
+ */
249
+ declare class Kv {
250
+ #private;
251
+ /** What you passed as `config.kv`. Register its type in `KvTypes` to call the library's own methods on it. */
252
+ readonly driver: ConfiguredDriver;
253
+ constructor(driver: ConfiguredDriver, ...path: string[]);
254
+ /**
255
+ * Records of one type under `name`. Pass the type argument. A record type holding a function,
256
+ * `Map`, `Set`, or `bigint` fails to compile here.
257
+ */
258
+ collection<TRecord>(name: CollectionName<TRecord>): KvCollection<TRecord>;
259
+ }
260
+ //#endregion
127
261
  //#region src/subscribers/publishDefault.d.ts
128
262
  /** @internal PublishableKey excludes this key so publish() never emits it externally. */
129
263
  declare const PublishDefault: unique symbol;
@@ -201,7 +335,7 @@ interface EventFaultSource {
201
335
  readonly raw: unknown;
202
336
  }
203
337
  /**
204
- * The fields both `responseAttempted` arms carry.
338
+ * The fields both `responseAttempted` shapes carry.
205
339
  */
206
340
  interface AttemptedWrite {
207
341
  /** The dispatch this write belongs to, for joining against every other key it published. */
@@ -345,42 +479,11 @@ type PublishableKey = TypedExclude<SubscriptionKey, keyof DefaultSubscriptions>;
345
479
  type SubscriptionData<KeyOfSubscribers extends SubscriptionKey> = AllSubscriptions[KeyOfSubscribers];
346
480
  type SubscriptionTuples = { [K in SubscriptionKey]: [AllSubscriptions[K]]; };
347
481
  //#endregion
348
- //#region src/subscribers/Subscriber.d.ts
349
- /**
350
- * Abstract base class for handling application subscribers.
351
- *
352
- * Provides type-safe access to subscribers data and the core framework instance.
353
- * Extend this class to create custom subscriber handlers.
354
- *
355
- * @typeParam KeyOfSubscribers - The specific subscriber type this handler processes
356
- * @typeParam TCore - The transport's Core
357
- */
358
- declare abstract class Subscriber<KeyOfSubscribers extends SubscriptionKey, TCore extends CoreBase> {
359
- protected readonly data: AllSubscriptions[KeyOfSubscribers];
360
- protected readonly core: TCore;
361
- protected readonly logger: Logger;
362
- /**
363
- * Instantiates the subscriber with event data and core.
364
- *
365
- * @param data - The subscriber event data
366
- * @param core - The core framework instance
367
- */
368
- constructor(data: AllSubscriptions[KeyOfSubscribers], core: TCore);
369
- /**
370
- * Executes the subscriber handler logic.
371
- */
372
- abstract execute(): Promise<void>;
373
- }
374
- //#endregion
375
482
  //#region src/subscribers/Bus.d.ts
376
483
  /** @internal */
377
- type StoredSubscriberCtor = Constructor<Subscriber<SubscriptionKey, CoreBase>, never[]>;
378
- /** @internal */
379
- interface SubscriberRegistration {
380
- readonly keys: readonly SubscriptionKey[];
381
- readonly frequency: EventFrequency;
382
- readonly ctor: StoredSubscriberCtor;
383
- }
484
+ type StoredSubscriberCtor = Constructor<{
485
+ execute(): Promise<void>;
486
+ }, never[]>;
384
487
  declare const loggerSlot: unique symbol;
385
488
  /**
386
489
  * Registers and dispatches subscribers. Subscribers run on a programmatic or framework publish.
@@ -392,18 +495,18 @@ declare class Bus extends TypedEventEmitter<SubscriptionTuples> {
392
495
  /** @internal */
393
496
  readonly [loggerSlot]: Logger;
394
497
  private readonly subscribersMap;
395
- private readonly executedOnce;
498
+ private readonly onceRuns;
396
499
  private readonly webhookProbes;
397
500
  constructor(core: CoreBase);
398
501
  /** @internal keep this inside start(). a bad webhook url thrown outside it never resets the singleton guard */
399
502
  [RegisterDefaults](): void;
400
503
  /** @internal */
401
- [RegisterSubscriber](registration: SubscriberRegistration): void;
504
+ [RegisterSubscriber](ctor: StoredSubscriberCtor): void;
402
505
  /** @internal */
403
506
  [UnregisterSubscriber](ctor: StoredSubscriberCtor, keys?: readonly SubscriptionKey[]): void;
404
507
  /** @internal */
405
508
  get [RegisteredCount](): number;
406
- /** @internal registration already dropped every reporter with no url set */
509
+ /** @internal registration already skipped every reporter with no url set */
407
510
  [VerifyWebhooks](): Promise<void>;
408
511
  private probeWebhook;
409
512
  /**
@@ -436,8 +539,6 @@ declare class Bus extends TypedEventEmitter<SubscriptionTuples> {
436
539
  private runSubscriber;
437
540
  }
438
541
  /** @internal */
439
- declare function registrationFor(ctor: StoredSubscriberCtor): SubscriberRegistration;
440
- /** @internal */
441
542
  declare function busLoggerOf(bus: Bus): Logger;
442
543
  //#endregion
443
544
  //#region src/interfaces/CoreBase.d.ts
@@ -460,6 +561,8 @@ interface CoreBase {
460
561
  readonly rateLimiter: IRateLimiter;
461
562
  /** Publish and subscribe to framework and application events. */
462
563
  readonly bus: Bus;
564
+ /** Your handlers' records in `config.kv`, under the `app:` prefix. */
565
+ readonly kv: Kv;
463
566
  }
464
567
  //#endregion
465
568
  //#region src/lifecycle/phases.d.ts
@@ -485,8 +588,14 @@ declare enum ShutdownPhase {
485
588
  /** External resources close. Plugin dispose runs here by default. */
486
589
  Disconnect = 3,
487
590
  /** The client disconnects last so in-flight handlers can still reference it during drain. */
488
- Logout = 4
591
+ Logout = 4,
592
+ /**
593
+ * Resources seedcord holds for the bot, like the `config.kv` driver, close after every plugin
594
+ * has stopped. Only seedcord runs work here.
595
+ */
596
+ Release = 5
489
597
  }
598
+ type TaskShutdownPhase = Exclude<ShutdownPhase, ShutdownPhase.Release>;
490
599
  //#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
600
+ export { RegisteredCount as A, EventMiddlewareMetadataKey as B, AckState as C, AckTrace as D, sendTarget as E, KvCollection as F, InteractionRouteKeys as G, InteractionKind as H, DispatchContext as I, WebhookUrlMetadataKey as J, MiddlewareKind as K, InteractionDispatch as L, VerifyWebhooks as M, PublishDefault as N, RegisterDefaults as O, Kv as P, CommandMetadataKey as R, reportedWrite as S, checkAckLegality as T, InteractionMetadataKey as U, GatedMetadataKey as V, InteractionMiddlewareMetadataKey as W, NoticeConstructor as X, Notice as Y, ResponseSent as _, Bus as a, Subscriptions as b, AllSubscriptions as c, DispatchOutcome as d, FaultSource as f, ResponseOutcome as g, ResponseFailed as h, CoreBase as i, UnregisterSubscriber as j, RegisterSubscriber as k, AttemptedWrite as l, ResponseAttempt as m, StartupPhase as n, StoredSubscriberCtor as o, InteractionFaultSource as p, SubscribeMetadataKey as q, TaskShutdownPhase as r, busLoggerOf as s, ShutdownPhase as t, DefaultSubscriptions as u, SubscriptionData as v, ReplyMethod as w, ReplyTelemetry as x, SubscriptionKey as y, EventMetadataKey as z };
601
+ //# sourceMappingURL=phases-C2QCSqRu.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-DFnkIldj.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 { s as Plugin, t as PluginHost } from "./PluginHost-DTtxWRiL.mjs";
2
2
 
3
3
  export { Plugin, PluginHost };