@iskra-bun/core 0.1.0 â 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +77 -0
- package/README.md +3 -2
- package/dist/index.d.ts +658 -107
- package/dist/index.js +483 -41
- package/dist/index.js.map +1 -1
- package/package.json +6 -2
- package/src/app.ts +219 -21
- package/src/config/loader.ts +9 -2
- package/src/config/schema.ts +78 -23
- package/src/env.ts +31 -0
- package/src/errors.ts +9 -2
- package/src/index.ts +1 -0
- package/src/logger/index.ts +192 -8
- package/src/otel.ts +221 -25
- package/src/types.ts +72 -8
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/app.ts","../src/logger/index.ts","../src/config/loader.ts","../src/config/schema.ts","../src/errors.ts","../src/otel.ts"],"sourcesContent":["import mitt, { type Emitter } from 'mitt';\nimport { createLogger, type Logger } from './logger';\nimport { loadAppConfig } from './config/loader';\nimport type { AppConfig, Driver, Plugin, Context } from './types';\nimport { LifecycleError } from './errors';\nimport { initOtel, shutdownOtel } from './otel';\n\nexport class App {\n public config: AppConfig;\n public logger: Logger;\n public events: Emitter<any>;\n private drivers: Driver[] = [];\n public context: Map<string, any> = new Map();\n\n constructor(config?: AppConfig) {\n this.events = mitt();\n // Temporary config until init() is called (or passed in constructor)\n this.config = config || { name: 'Bootstrapping', logger: { level: 'info' } } as AppConfig;\n this.logger = createLogger(this.config.name, this.config.logger?.level);\n }\n\n async init() {\n // If config was not passed, load it\n if (this.config.name === 'Bootstrapping') {\n this.config = await loadAppConfig();\n this.logger = createLogger(this.config.name, this.config.logger?.level);\n }\n\n // Initialize OpenTelemetry SDK before drivers (so auto-instrumentation patches libraries)\n if (this.config.otel && this.config.otel.enabled !== false) {\n await initOtel(this.config.otel, this.config.name);\n this.logger.info({ endpoint: this.config.otel.endpoint }, 'OpenTelemetry initialized');\n }\n\n this.logger.info('Initializing App...');\n\n for (const driver of this.drivers) {\n if (driver.init) {\n this.logger.debug(`Initializing driver: ${driver.name}`);\n await driver.init(this);\n }\n }\n }\n\n register(driver: Driver) {\n this.drivers.push(driver);\n return this;\n }\n\n use(plugin: Plugin) {\n plugin.install(this);\n return this;\n }\n\n on(event: string, handler: (ctx: Context) => Promise<void> | void) {\n this.events.on(event, async (payload) => {\n const ctx: Context = {\n app: this,\n logger: this.logger.child({ event }),\n payload,\n reply: (data) => this.events.emit(`${event}:reply`, data)\n };\n\n try {\n await handler(ctx);\n } catch (err) {\n this.logger.error({ err, event }, 'Error handling event');\n }\n });\n return this;\n }\n\n emit(event: string, payload: any) {\n this.events.emit(event, payload);\n }\n\n async start() {\n await this.init();\n this.logger.info('Starting drivers...');\n await Promise.all(this.drivers.map(d => d.start ? d.start() : Promise.resolve()));\n this.logger.info('App started successfully đ');\n }\n\n async stop() {\n this.logger.info('Stopping app...');\n const results = await Promise.allSettled(this.drivers.map(d => d.stop ? d.stop() : Promise.resolve()));\n\n const failures = results.filter((r): r is PromiseRejectedResult => r.status === 'rejected');\n if (failures.length > 0) {\n this.logger.error({ failures }, 'Some drivers failed to stop');\n throw new LifecycleError('Some drivers failed to stop', {\n failures,\n context: { driverCount: this.drivers.length, failedCount: failures.length },\n });\n }\n\n // Shutdown OTel SDK after drivers (flush pending spans/metrics)\n await shutdownOtel();\n\n this.logger.info('App stopped.');\n }\n}\n","import pino from 'pino';\n\nexport const createLogger = (name: string, level: string = 'info') => {\n return pino({\n name,\n level,\n transport: {\n target: 'pino-pretty',\n options: {\n colorize: true\n }\n }\n });\n};\n\nexport type Logger = pino.Logger;\n","import { loadConfig } from 'c12';\nimport { AppConfigSchema, type AppConfigOutput } from './schema';\nimport type { AppConfig } from '../types';\n\nexport async function loadAppConfig(cwd: string = process.cwd()): Promise<AppConfig> {\n const { config } = await loadConfig<AppConfig>({\n name: 'app',\n configFile: 'app.config',\n cwd,\n dotenv: true\n });\n\n const validConfig = AppConfigSchema.parse(config || {});\n return validConfig as AppConfig;\n}\n","import { z } from 'zod';\n\nexport const AppConfigSchema = z.object({\n name: z.string().default('IskraApp'),\n debug: z.boolean().default(false),\n logger: z.object({\n level: z.string().default('info')\n }).default({}),\n otel: z.object({\n enabled: z.boolean().default(true),\n endpoint: z.string().default('http://localhost:4318'),\n serviceName: z.string().optional(),\n serviceVersion: z.string().default('0.1.0'),\n environment: z.string().optional(),\n metricIntervalMs: z.number().default(60_000),\n resourceAttributes: z.record(z.string()).optional(),\n instrumentations: z.record(z.object({ enabled: z.boolean().optional() })).optional(),\n }).optional(),\n processes: z.record(z.object({\n command: z.string(),\n args: z.array(z.string()).optional(),\n mode: z.enum(['daemon', 'oneshot', 'stdio']).default('daemon'),\n restartOnCrash: z.boolean().default(false),\n env: z.record(z.string()).optional()\n })).optional()\n});\n\nexport type AppConfigInput = z.input<typeof AppConfigSchema>;\nexport type AppConfigOutput = z.output<typeof AppConfigSchema>;\n","export const ErrorCodes = {\n // General\n INTERNAL_ERROR: 'INTERNAL_ERROR',\n NOT_INITIALIZED: 'NOT_INITIALIZED',\n ADAPTER_ERROR: 'ADAPTER_ERROR',\n\n // Config\n CONFIG_INVALID: 'CONFIG_INVALID',\n CONFIG_MISSING: 'CONFIG_MISSING',\n\n // Driver\n DRIVER_INIT_FAILED: 'DRIVER_INIT_FAILED',\n DRIVER_START_FAILED: 'DRIVER_START_FAILED',\n DRIVER_STOP_FAILED: 'DRIVER_STOP_FAILED',\n DRIVER_NOT_FOUND: 'DRIVER_NOT_FOUND',\n\n // Plugin\n PLUGIN_INSTALL_FAILED: 'PLUGIN_INSTALL_FAILED',\n\n // Lifecycle\n LIFECYCLE_START_FAILED: 'LIFECYCLE_START_FAILED',\n LIFECYCLE_STOP_FAILED: 'LIFECYCLE_STOP_FAILED',\n\n // HTTP\n BAD_REQUEST: 'BAD_REQUEST',\n UNAUTHORIZED: 'UNAUTHORIZED',\n FORBIDDEN: 'FORBIDDEN',\n NOT_FOUND: 'NOT_FOUND',\n CONFLICT: 'CONFLICT',\n VALIDATION_ERROR: 'VALIDATION_ERROR',\n\n // Data\n DATABASE_ERROR: 'DATABASE_ERROR',\n CONNECTION_ERROR: 'CONNECTION_ERROR',\n QUERY_ERROR: 'QUERY_ERROR',\n MIGRATION_ERROR: 'MIGRATION_ERROR',\n CACHE_ERROR: 'CACHE_ERROR',\n\n // Storage / Upload\n STORAGE_ERROR: 'STORAGE_ERROR',\n FILE_UPLOAD_ERROR: 'FILE_UPLOAD_ERROR',\n FILE_NOT_FOUND: 'FILE_NOT_FOUND',\n\n // Worker\n QUEUE_ERROR: 'QUEUE_ERROR',\n JOB_ERROR: 'JOB_ERROR',\n\n // Socket\n SOCKET_CONNECTION_ERROR: 'SOCKET_CONNECTION_ERROR',\n SOCKET_MESSAGE_ERROR: 'SOCKET_MESSAGE_ERROR',\n} as const;\n\nexport type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];\n\nexport interface IskraErrorOptions {\n code: ErrorCode;\n cause?: Error;\n context?: Record<string, unknown>;\n}\n\n/**\n * Clase base para todos los errores del framework Iskra.\n * Soporta encadenamiento de errores vĂa `cause` y metadata arbitraria en `context`.\n */\nexport class IskraError extends Error {\n public readonly code: ErrorCode;\n public readonly context: Record<string, unknown>;\n\n constructor(message: string, options: IskraErrorOptions) {\n super(message, { cause: options.cause });\n this.name = 'IskraError';\n this.code = options.code;\n this.context = options.context ?? {};\n }\n\n toJSON() {\n return {\n name: this.name,\n message: this.message,\n code: this.code,\n context: this.context,\n ...(this.cause instanceof Error ? { cause: this.cause.message } : {}),\n };\n }\n}\n\n// âââ Config Errors âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ\n\nexport class ConfigError extends IskraError {\n constructor(message: string, options?: { cause?: Error; context?: Record<string, unknown> }) {\n super(message, { code: ErrorCodes.CONFIG_INVALID, ...options });\n this.name = 'ConfigError';\n }\n}\n\n// âââ Driver Errors âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ\n\nexport class DriverError extends IskraError {\n constructor(message: string, options?: { code?: ErrorCode; cause?: Error; context?: Record<string, unknown> }) {\n super(message, { code: options?.code ?? ErrorCodes.DRIVER_INIT_FAILED, cause: options?.cause, context: options?.context });\n this.name = 'DriverError';\n }\n}\n\n// âââ Plugin Errors âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ\n\nexport class PluginError extends IskraError {\n constructor(message: string, options?: { cause?: Error; context?: Record<string, unknown> }) {\n super(message, { code: ErrorCodes.PLUGIN_INSTALL_FAILED, ...options });\n this.name = 'PluginError';\n }\n}\n\n// âââ Lifecycle Errors ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ\n\nexport class LifecycleError extends IskraError {\n public readonly failures: PromiseRejectedResult[];\n\n constructor(message: string, options: { failures?: PromiseRejectedResult[]; cause?: Error; context?: Record<string, unknown> }) {\n super(message, { code: ErrorCodes.LIFECYCLE_STOP_FAILED, cause: options.cause, context: options.context });\n this.name = 'LifecycleError';\n this.failures = options.failures ?? [];\n }\n}\n","/**\n * OpenTelemetry SDK initialization for Iskra apps.\n *\n * All OTel packages are dynamic-imported so they remain optional peer deps.\n * Apps that don't set `config.otel` never load this code.\n *\n * The module specifiers are held in variables rather than passed as string\n * literals to `import()`. This stops `tsc` from statically resolving the\n * optional `@opentelemetry/*` packages at compile time, so the module\n * typechecks cleanly even when those peer deps are absent. At runtime the\n * imports still resolve normally, and a missing package degrades to a clear,\n * actionable error instead of an unhandled module-resolution failure.\n */\nimport type { OtelConfig } from './types';\n\nlet sdkInstance: any = null;\n\n/** Optional OTel package specifiers, indirected so tsc does not resolve them statically. */\nconst OTEL_MODULES = {\n sdkNode: '@opentelemetry/sdk-node',\n autoInstrumentations: '@opentelemetry/auto-instrumentations-node',\n traceExporter: '@opentelemetry/exporter-trace-otlp-http',\n metricExporter: '@opentelemetry/exporter-metrics-otlp-http',\n sdkMetrics: '@opentelemetry/sdk-metrics',\n resources: '@opentelemetry/resources',\n semconv: '@opentelemetry/semantic-conventions',\n} as const;\n\n/** Import an optional module by specifier; the indirection keeps tsc from resolving it. */\nfunction importOptional(specifier: string): Promise<any> {\n return import(specifier);\n}\n\n/**\n * Initialize the OpenTelemetry NodeSDK with OTLP exporters.\n * Must be called before any drivers start so auto-instrumentation can patch libraries.\n */\nexport async function initOtel(config: OtelConfig, appName: string): Promise<void> {\n if (sdkInstance) return; // Already initialized\n\n try {\n const [\n { NodeSDK },\n { getNodeAutoInstrumentations },\n { OTLPTraceExporter },\n { OTLPMetricExporter },\n { PeriodicExportingMetricReader },\n { Resource },\n semconv,\n ] = await Promise.all([\n importOptional(OTEL_MODULES.sdkNode),\n importOptional(OTEL_MODULES.autoInstrumentations),\n importOptional(OTEL_MODULES.traceExporter),\n importOptional(OTEL_MODULES.metricExporter),\n importOptional(OTEL_MODULES.sdkMetrics),\n importOptional(OTEL_MODULES.resources),\n importOptional(OTEL_MODULES.semconv),\n ]);\n\n const serviceName = config.serviceName || appName;\n const endpoint = config.endpoint || 'http://localhost:4318';\n\n const resource = new Resource({\n [semconv.ATTR_SERVICE_NAME]: serviceName,\n [semconv.ATTR_SERVICE_VERSION]: config.serviceVersion || '0.1.0',\n 'deployment.environment': config.environment || process.env.NODE_ENV || 'development',\n ...config.resourceAttributes,\n });\n\n const instrumentationOverrides: Record<string, any> = {\n '@opentelemetry/instrumentation-fs': { enabled: false },\n ...config.instrumentations,\n };\n\n const sdk = new NodeSDK({\n resource,\n traceExporter: new OTLPTraceExporter({ url: `${endpoint}/v1/traces` }),\n metricReader: new PeriodicExportingMetricReader({\n exporter: new OTLPMetricExporter({ url: `${endpoint}/v1/metrics` }),\n exportIntervalMillis: config.metricIntervalMs ?? 60_000,\n }),\n instrumentations: [getNodeAutoInstrumentations(instrumentationOverrides)],\n });\n\n sdk.start();\n sdkInstance = sdk;\n } catch (err: any) {\n // Provide a clear error when OTel packages are not installed\n if (err?.code === 'ERR_MODULE_NOT_FOUND' || err?.code === 'MODULE_NOT_FOUND' || err?.message?.includes('Cannot find')) {\n throw new Error(\n `[iskra/otel] OpenTelemetry packages are not installed. ` +\n `Install them with: bun add @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node ` +\n `@opentelemetry/exporter-trace-otlp-http @opentelemetry/exporter-metrics-otlp-http ` +\n `@opentelemetry/sdk-metrics @opentelemetry/resources @opentelemetry/semantic-conventions`,\n );\n }\n throw err;\n }\n}\n\n/**\n * Gracefully shut down the OTel SDK, flushing any pending spans/metrics.\n */\nexport async function shutdownOtel(): Promise<void> {\n if (sdkInstance) {\n await sdkInstance.shutdown();\n sdkInstance = null;\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/app.ts","../src/logger/index.ts","../src/env.ts","../src/config/loader.ts","../src/config/schema.ts","../src/errors.ts","../src/otel.ts"],"sourcesContent":["import mitt, { type Emitter } from 'mitt';\nimport { createLogger, type Logger } from './logger';\nimport { loadAppConfig } from './config/loader';\nimport type { AppConfig, AppContextRegistry, AppEvents, Driver, Plugin, Context } from './types';\nimport { LifecycleError } from './errors';\nimport { describeOtelEndpoint, initOtel, shutdownOtel } from './otel';\nimport { nodeEnv } from './env';\n\n/**\n * `app.context`: a Map whose keys in AppContextRegistry (`'db'`, `'kv'`, âŚ)\n * are typed; any other key holds `unknown`, or the type given as `T`.\n */\nexport class AppContext extends Map<string, unknown> {\n override get<K extends keyof AppContextRegistry>(key: K): AppContextRegistry[K] | undefined;\n override get<T = unknown>(key: string): T | undefined;\n override get(key: string): unknown {\n return super.get(key);\n }\n\n override set<K extends keyof AppContextRegistry>(key: K, value: AppContextRegistry[K]): this;\n override set(key: string, value: unknown): this;\n override set(key: string, value: unknown): this {\n return super.set(key, value);\n }\n}\n\nconst DEFAULT_SHUTDOWN_SIGNALS: NodeJS.Signals[] = ['SIGTERM', 'SIGINT'];\nconst DEFAULT_SHUTDOWN_TIMEOUT_MS = 10_000;\n\nexport class App {\n public config: AppConfig;\n public logger: Logger;\n public events: Emitter<Record<string, unknown>>;\n private drivers: Driver[] = [];\n /** Drivers whose start() succeeded, in start order; null until start() runs. */\n private startedDrivers: Driver[] | null = null;\n private pendingInstalls: PromiseLike<void>[] = [];\n /** The shutdown in progress, shared by concurrent stop() calls. */\n private stopping: Promise<void> | null = null;\n private signalHandler?: (signal: NodeJS.Signals) => void;\n private signals: NodeJS.Signals[] = [];\n public context = new AppContext();\n\n constructor(config?: AppConfig) {\n this.events = mitt();\n // Temporary config until init() is called (or passed in constructor)\n this.config = config || ({ name: 'Bootstrapping', logger: { level: 'info' } } as AppConfig);\n this.logger = createLogger(this.config.name, this.config.logger?.level);\n }\n\n async init() {\n // If config was not passed, load it\n if (this.config.name === 'Bootstrapping') {\n this.config = await loadAppConfig();\n this.logger = createLogger(this.config.name, this.config.logger?.level);\n }\n\n // Initialize OpenTelemetry SDK before drivers (so auto-instrumentation patches libraries)\n if (this.config.otel && this.config.otel.enabled !== false) {\n await initOtel(this.config.otel, this.config.name);\n // Its origin only: the endpoint's path, query or userinfo can hold an API key.\n const { origin, plaintext } = describeOtelEndpoint(this.config.otel);\n this.logger.info({ endpoint: origin }, 'OpenTelemetry initialized');\n if (plaintext) {\n this.logger.warn(\n { endpoint: origin },\n 'OpenTelemetry endpoint is plain http:// on a remote host: spans (URLs, captured headers) travel unencrypted; use https://',\n );\n }\n }\n\n this.logger.info('Initializing App...');\n\n // Async plugin installs are awaited here so a failure surfaces from\n // start() instead of crashing the process as an unhandled rejection.\n await Promise.all(this.pendingInstalls);\n this.pendingInstalls = [];\n\n // Only the drivers whose init() ran are rolled back: one without an\n // init hook was never initialized and has nothing to release yet.\n const initialized: Driver[] = [];\n for (const driver of this.drivers) {\n if (!driver.init) continue;\n this.logger.debug(`Initializing driver: ${driver.name}`);\n initialized.push(driver);\n try {\n await driver.init(this);\n } catch (err) {\n // The failing driver may hold part of what it opened (a pool, a\n // child process), so it is stopped too, with the ones before it.\n this.logger.error(\n { err, driver: driver.name },\n 'Driver failed to initialize; stopping the drivers already initialized',\n );\n await this.rollback(initialized.reverse());\n throw err;\n }\n }\n }\n\n register(driver: Driver) {\n this.drivers.push(driver);\n return this;\n }\n\n use(plugin: Plugin) {\n const result = plugin.install(this);\n if (result && typeof (result as PromiseLike<void>).then === 'function') {\n const install = Promise.resolve(result);\n // Observed now so an early rejection is not reported as unhandled;\n // init() awaits the original promise and rethrows its error.\n install.catch(() => {});\n this.pendingInstalls.push(install);\n }\n return this;\n }\n\n /**\n * Runs `handler` for every `event`. Events in AppEvents give the handler a\n * typed payload; for others it is `unknown`, or the type given as `T`.\n */\n on<K extends keyof AppEvents>(event: K, handler: (ctx: Context<AppEvents[K]>) => Promise<void> | void): this;\n on<T = unknown>(event: string, handler: (ctx: Context<T>) => Promise<void> | void): this;\n on(event: string, handler: (ctx: Context<unknown>) => Promise<void> | void): this {\n this.events.on(event, async (payload) => {\n const ctx: Context<unknown> = {\n app: this,\n logger: this.logger.child({ event }),\n payload,\n reply: (data) => this.events.emit(`${event}:reply`, data),\n };\n\n try {\n await handler(ctx);\n } catch (err) {\n this.logger.error({ err, event }, 'Error handling event');\n }\n });\n return this;\n }\n\n /** Emits `event`; for events in AppEvents the payload must match their type. */\n emit<K extends keyof AppEvents>(event: K, payload: AppEvents[K]): void;\n emit(event: string, payload?: unknown): void;\n emit(event: string, payload?: unknown): void {\n this.events.emit(event, payload);\n }\n\n /**\n * Starts drivers one at a time, in registration order, so a driver can rely\n * on the ones registered before it (e.g. the DB before the web server). If\n * one fails, every driver is stopped in reverse order (all were initialized\n * and may hold connections, including the failing one), OpenTelemetry is\n * shut down and the error is rethrown, instead of leaving ports bound and\n * children running.\n */\n async start() {\n await this.init();\n if (!nodeEnv()) {\n this.logger.warn(\n 'NODE_ENV is not set: production defaults apply (Secure cookies, no sample secrets, JSON logs). ' +\n 'Set NODE_ENV=development for local development.',\n );\n }\n this.logger.info('Starting drivers...');\n const started: Driver[] = [];\n for (const driver of this.drivers) {\n try {\n if (driver.start) await driver.start();\n started.push(driver);\n } catch (err) {\n this.logger.error({ err, driver: driver.name }, 'Driver failed to start; stopping every driver');\n await this.rollback([...this.drivers].reverse());\n throw err;\n }\n }\n this.startedDrivers = started;\n this.installSignalHandlers();\n this.logger.info('App started successfully đ');\n }\n\n /**\n * Stops drivers in reverse start order (the web server before the DB it\n * uses), continuing past failures, and always flushes OpenTelemetry.\n *\n * A call made while a stop is in progress (e.g. an app's own signal\n * handler next to the App's) gets that same stop: it resolves only once\n * every driver has stopped, instead of right away with nothing left to do.\n */\n stop(): Promise<void> {\n this.stopping ??= this.stopOnce().finally(() => {\n this.stopping = null;\n });\n return this.stopping;\n }\n\n private async stopOnce() {\n this.logger.info('Stopping app...');\n const toStop = [...(this.startedDrivers ?? this.drivers)].reverse();\n this.startedDrivers = [];\n\n let failures: PromiseRejectedResult[];\n try {\n failures = await this.stopDrivers(toStop);\n } finally {\n // Shutdown OTel SDK after drivers (flush pending spans/metrics)\n await shutdownOtel();\n // Removed only now: while the drivers stop, a second signal must\n // still reach the handler (forced exit) instead of the default action.\n this.removeSignalHandlers();\n }\n\n if (failures.length > 0) {\n this.logger.error({ failures }, 'Some drivers failed to stop');\n throw new LifecycleError('Some drivers failed to stop', {\n failures,\n context: { driverCount: toStop.length, failedCount: failures.length },\n });\n }\n\n this.logger.info('App stopped.');\n }\n\n /**\n * Stops `drivers` after a failed init() or start() and shuts down\n * OpenTelemetry. Failures to stop are logged, not thrown: the caller\n * rethrows the original error. A later stop() stops nothing more.\n */\n private async rollback(drivers: Driver[]) {\n this.startedDrivers = [];\n await this.stopDrivers(drivers);\n try {\n await shutdownOtel();\n } catch (err) {\n this.logger.error({ err }, 'OpenTelemetry failed to shut down');\n }\n }\n\n private async stopDrivers(drivers: Driver[]): Promise<PromiseRejectedResult[]> {\n const failures: PromiseRejectedResult[] = [];\n for (const driver of drivers) {\n try {\n if (driver.stop) await driver.stop();\n } catch (reason) {\n this.logger.error({ err: reason, driver: driver.name }, 'Driver failed to stop');\n failures.push({ status: 'rejected', reason });\n }\n }\n return failures;\n }\n\n /**\n * Stops the app gracefully on SIGTERM/SIGINT (configurable with\n * `shutdownSignals`, off by default under NODE_ENV=test). Once a listener\n * exists the runtime no longer exits on its own, so the handler exits with\n * 0 after a clean stop, or 1 on failure / after `shutdownTimeoutMs`. A\n * second signal forces exit.\n */\n private installSignalHandlers() {\n const configured = this.config.shutdownSignals;\n const signals =\n configured === false ? [] : (configured ?? (nodeEnv() === 'test' ? [] : DEFAULT_SHUTDOWN_SIGNALS));\n if (signals.length === 0 || this.signalHandler) return;\n\n const timeoutMs = this.config.shutdownTimeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS;\n let shuttingDown = false;\n this.signalHandler = (signal) => {\n if (shuttingDown) {\n this.logger.warn({ signal }, 'Second shutdown signal; exiting immediately');\n process.exit(1);\n }\n shuttingDown = true;\n this.logger.info({ signal }, 'Shutdown signal received; stopping app');\n const timer = setTimeout(() => {\n this.logger.error({ timeoutMs }, 'Graceful shutdown timed out; exiting');\n process.exit(1);\n }, timeoutMs);\n this.stop().then(\n () => {\n clearTimeout(timer);\n process.exit(0);\n },\n (err) => {\n clearTimeout(timer);\n this.logger.error({ err }, 'Graceful shutdown failed');\n process.exit(1);\n },\n );\n };\n this.signals = signals as NodeJS.Signals[];\n for (const signal of this.signals) process.on(signal, this.signalHandler);\n }\n\n private removeSignalHandlers() {\n if (!this.signalHandler) return;\n for (const signal of this.signals) process.off(signal, this.signalHandler);\n this.signalHandler = undefined;\n this.signals = [];\n }\n}\n","import pino from 'pino';\nimport pretty from 'pino-pretty';\nimport { isDevelopmentEnv } from '../env';\n\n/**\n * Field names whose values are replaced with `[REDACTED]`, at any depth. Keys\n * are compared lowercased and without `-` or `_`, so `apiKey`, `api_key` and\n * `X-API-Key` are the same key.\n */\nconst REDACTED_KEYS = [\n 'password',\n 'pass',\n 'passwd',\n 'apiKey',\n 'apiSecret',\n 'token',\n 'authToken',\n 'accessToken',\n 'refreshToken',\n 'idToken',\n 'secret',\n 'clientSecret',\n 'secretKey',\n 'privateKey',\n 'authorization',\n 'proxyAuthorization',\n 'cookie',\n 'setCookie',\n 'sessionId',\n] as const;\n\n/**\n * Keys ending in one of these are redacted too (normalized the same way):\n * `dbPassword`, `x-api-key`, `x-auth-token`, `AWS_SECRET_ACCESS_KEY`,\n * `webhookSecret`.\n */\nconst REDACTED_SUFFIXES = ['password', 'passwd', 'secret', 'token', 'apikey', 'secretkey', 'privatekey', 'accesskey'];\n\nconst normalizeKey = (key: string): string => key.toLowerCase().replace(/[-_]/g, '');\nconst SENSITIVE = new Set<string>(REDACTED_KEYS.map(normalizeKey));\nconst isSensitive = (key: string): boolean => {\n const normalized = normalizeKey(key);\n return SENSITIVE.has(normalized) || REDACTED_SUFFIXES.some((suffix) => normalized.endsWith(suffix));\n};\nconst CENSOR = '[REDACTED]';\n/** Objects deeper than this are logged as they are. */\nconst MAX_DEPTH = 8;\nconst IN_PROGRESS = Symbol('in progress');\n\n/** The prototype of what pino's err serializer returns (an error's fields, causes included). */\nconst SERIALIZED_ERROR_PROTO = Object.getPrototypeOf(pino.stdSerializers.err(new Error()));\n\nconst isPlainObject = (value: object): boolean => {\n const proto = Object.getPrototypeOf(value);\n return proto === Object.prototype || proto === null || proto === SERIALIZED_ERROR_PROTO;\n};\n\n/** Query parameter names whose values are masked in messages. */\nconst SECRET_PARAM = /pass|secret|token|key|sig|auth/i;\n\n/**\n * Passwords in `scheme://user:password@host` and secret-looking query\n * parameters (`?authToken=`, `&X-Amz-Signature=`) of a message: drivers put\n * the connection string in the errors they throw. Bounded quantifiers keep\n * both patterns linear on long input.\n */\nfunction maskCredentials(text: string): string {\n return text\n .replace(/\\b([a-z][\\w+.-]{0,30}:\\/\\/[^\\s:@/]{0,256}):[^\\s@/]{1,256}@/gi, '$1:[REDACTED]@')\n .replace(/([?&])([\\w.-]{1,100})=([^&\\s'\"#]+)/g, (match, sep: string, name: string) =>\n SECRET_PARAM.test(name) ? `${sep}${name}=[REDACTED]` : match,\n );\n}\n\n/**\n * An Error as pino's err serializer writes it (type, message and stack with\n * their causes, and its own fields), scrubbed. pino serializes errors after\n * the log formatter runs, so an HTTP client's error (`config.headers\n * .Authorization`) or a Redis error (`command.args` of AUTH) went out as is.\n */\nfunction scrubError(err: Error, depth: number, done: WeakMap<object, unknown>): unknown {\n const serialized = { ...pino.stdSerializers.err(err) } as Record<string, unknown>;\n if (typeof serialized.message === 'string') serialized.message = maskCredentials(serialized.message);\n if (typeof serialized.stack === 'string') serialized.stack = maskCredentials(serialized.stack);\n // Deep: an error keeps the client's objects (axios's `request`, whose\n // `_options.headers` hold the Authorization header), which are class\n // instances that JSON.stringify writes out in full.\n return scrubEntries(serialized, depth, done, true);\n}\n\n/**\n * Replaces the value of every sensitive key, at any depth, in a copy of the\n * objects that contain one (the caller's objects are not modified). pino's\n * own `redact` has no recursive wildcard, and listing each key at every depth\n * made logging 14 times slower. Errors are serialized and scrubbed here (see\n * scrubError), and objects with a toJSON() are scrubbed as what it returns;\n * other class instances are left to pino, except inside an error.\n */\nfunction scrub(value: unknown, depth: number, done: WeakMap<object, unknown>, deep = false): unknown {\n if (value === null || typeof value !== 'object' || depth > MAX_DEPTH) return value;\n if (done.has(value)) {\n const result = done.get(value);\n // Still being walked: a cycle, which pino would print as [Circular].\n return result === IN_PROGRESS ? '[Circular]' : result;\n }\n const isError = value instanceof Error;\n const toJSON = (value as { toJSON?: unknown }).toJSON;\n const walk = isError || Array.isArray(value) || isPlainObject(value) || typeof toJSON === 'function' || deep;\n if (!walk) return value;\n done.set(value, IN_PROGRESS);\n let result: unknown;\n if (isError) {\n result = scrubError(value, depth, done);\n } else if (Array.isArray(value)) {\n let copy: unknown[] | undefined;\n value.forEach((item, i) => {\n const scrubbed = scrub(item, depth + 1, done, deep);\n if (scrubbed !== item) (copy ??= value.slice())[i] = scrubbed;\n });\n result = copy ?? value;\n } else if (isPlainObject(value)) {\n result = scrubEntries(value as Record<string, unknown>, depth, done, deep);\n } else if (typeof toJSON === 'function') {\n // Written as its toJSON(), which is what JSON.stringify writes: an axios\n // error's `config.headers` is an AxiosHeaders instance whose\n // Authorization header went out as is.\n result = scrub(toJSON.call(value), depth, done, deep);\n } else {\n // Inside an error: another class instance, as JSON.stringify writes it\n // (its own enumerable fields).\n result = scrubEntries({ ...(value as Record<string, unknown>) }, depth, done, deep);\n }\n done.set(value, result);\n return result;\n}\n\n/** An object with its sensitive keys censored and its values scrubbed; a copy if anything changed. */\nfunction scrubEntries(\n value: Record<string, unknown>,\n depth: number,\n done: WeakMap<object, unknown>,\n deep = false,\n): Record<string, unknown> {\n let copy: Record<string, unknown> | undefined;\n for (const [key, item] of Object.entries(value)) {\n const scrubbed = isSensitive(key) ? CENSOR : scrub(item, depth + 1, done, deep);\n if (scrubbed !== item) (copy ??= { ...value })[key] = scrubbed;\n }\n // A nested serialized error (pino's prototype) becomes a plain object either way.\n return copy ?? (Object.getPrototypeOf(value) === SERIALIZED_ERROR_PROTO ? { ...value } : value);\n}\n\nexport const createLogger = (name: string, level: string = 'info') => {\n // Pretty output only in development/test: JSON otherwise, NODE_ENV unset included.\n const isDev = isDevelopmentEnv();\n const options: pino.LoggerOptions = {\n name,\n level,\n formatters: {\n log: (object) => scrub(object, 0, new WeakMap()) as Record<string, unknown>,\n },\n // The formatter above already serialized errors (scrubbed): pino's own\n // err serializer would take that plain object for an error again and\n // rewrite its `type` as \"Object\".\n serializers: {\n err: (value: unknown) => (value instanceof Error ? scrub(value, 0, new WeakMap()) : value),\n },\n hooks: {\n // Messages are strings the formatter never sees: mask connection\n // string passwords there too, including the message pino takes\n // from an error logged on its own (`logger.error(err)`).\n logMethod(args, method) {\n const masked: unknown[] = args.map((arg) => (typeof arg === 'string' ? maskCredentials(arg) : arg));\n if (masked[0] instanceof Error && typeof masked[1] !== 'string') {\n masked.splice(1, 0, maskCredentials(masked[0].message));\n }\n return method.apply(this, masked as Parameters<typeof method>);\n },\n },\n redact: {\n paths: ['config.env', '*.data'],\n censor: CENSOR,\n },\n };\n // pino-pretty as an in-process stream, not a `transport`: a transport runs\n // in a worker thread that loads the module by name at runtime, which fails\n // in a `bun build --compile` binary and crashed it at startup.\n const logger = isDev ? pino(options, pretty({ colorize: true })) : pino(options);\n\n // Bindings (`logger.child({ ... })`) do not go through formatters.log:\n // scrub them here. A child's own children inherit this child().\n type Child = (this: pino.Logger, bindings: pino.Bindings, options?: object) => pino.Logger;\n const child = logger.child as unknown as Child;\n logger.child = function (this: pino.Logger, bindings: pino.Bindings, childOptions?: object) {\n return child.call(this, scrub(bindings, 0, new WeakMap()) as pino.Bindings, childOptions);\n } as unknown as typeof logger.child;\n return logger;\n};\n\nexport type Logger = pino.Logger;\n","/**\n * The deployment environment, read when called: `NODE_ENV` through a computed\n * key, because `bun build` replaces a literal `process.env.NODE_ENV` with its\n * value at build time (\"development\" when unset), so a compiled binary ignored\n * the NODE_ENV it ran with.\n */\nconst NODE_ENV = 'NODE_ENV';\n\n/** `NODE_ENV` as the process runs with it, or undefined when unset or empty. */\nexport function nodeEnv(): string | undefined {\n return process.env[NODE_ENV] || undefined;\n}\n\n/**\n * Whether development conveniences apply (pretty logs, http auth URLs, sample\n * secrets, `disableCSRFCheck`): only with `NODE_ENV` set to `development` or\n * `test`.\n */\nexport function isDevelopmentEnv(env: string | undefined = nodeEnv()): boolean {\n return env === 'development' || env === 'test';\n}\n\n/**\n * Whether production safeguards apply: in every environment but `development`\n * and `test`, including an unset NODE_ENV and names like `staging`. They used\n * to apply only to NODE_ENV=production exactly, so a deploy that forgot it\n * sent non-Secure cookies and accepted placeholder secrets.\n */\nexport function isProductionEnv(env: string | undefined = nodeEnv()): boolean {\n return !isDevelopmentEnv(env);\n}\n","import { loadConfig } from 'c12';\nimport { AppConfigSchema } from './schema';\nimport type { AppConfig } from '../types';\n\nexport async function loadAppConfig(cwd: string = process.cwd()): Promise<AppConfig> {\n const { config } = await loadConfig<AppConfig>({\n name: 'app',\n configFile: 'app.config',\n cwd,\n dotenv: true,\n // c12 would also merge a `.apprc` file from the working directory (a\n // plain key=value file that could add `processes` to spawn or move\n // `otel.endpoint`), and download and run `extends` layers from\n // github:/gitlab:/https:// sources on every start. Local `extends`\n // paths still work.\n rcFile: false,\n giget: false,\n });\n\n const validConfig = AppConfigSchema.parse(config || {});\n return validConfig as AppConfig;\n}\n","import { z } from 'zod';\n\nconst RestartBackoffSchema = z.object({\n initialMs: z.number().positive().optional(),\n maxMs: z.number().positive().optional(),\n factor: z.number().positive().optional(),\n});\n\n// Sections owned by the kits are validated here for shape only, and every\n// object is `.passthrough()`: AppConfig promises `[key: string]: any`, and a\n// plain z.object() silently strips unknown keys â which used to drop `db`,\n// `kv` and `socket` from app.config.ts entirely.\nexport const AppConfigSchema = z\n .object({\n name: z.string().default('IskraApp'),\n debug: z.boolean().default(false),\n logger: z\n .object({\n level: z.string().default('info'),\n })\n .passthrough()\n .default({}),\n otel: z\n .object({\n enabled: z.boolean().default(true),\n endpoint: z.string().default('http://localhost:4318'),\n serviceName: z.string().optional(),\n serviceVersion: z.string().default('0.1.0'),\n environment: z.string().optional(),\n metricIntervalMs: z.number().default(60_000),\n resourceAttributes: z.record(z.string()).optional(),\n // passthrough: the instrumentations' own options (hooks, redactedQueryParams) were stripped.\n instrumentations: z.record(z.object({ enabled: z.boolean().optional() }).passthrough()).optional(),\n })\n .passthrough()\n .optional(),\n shutdownSignals: z.union([z.array(z.string()), z.literal(false)]).optional(),\n shutdownTimeoutMs: z.number().positive().optional(),\n processes: z\n .record(\n z\n .object({\n command: z.string(),\n args: z.array(z.string()).optional(),\n mode: z.enum(['daemon', 'oneshot', 'stdio']).default('daemon'),\n restartOnCrash: z.boolean().default(false),\n maxRestarts: z.number().int().nonnegative().optional(),\n restartCooldown: z.number().nonnegative().optional(),\n restartBackoff: RestartBackoffSchema.optional(),\n env: z.record(z.string()).optional(),\n inheritEnv: z.union([z.boolean(), z.array(z.string())]).optional(),\n maxPendingStdinBytes: z.number().int().positive().optional(),\n })\n .passthrough(),\n )\n .optional(),\n socket: z\n .object({\n enabled: z.boolean(),\n port: z.number().optional(),\n adapter: z.enum(['bun', 'socket.io']).optional(),\n })\n .passthrough()\n .optional(),\n kv: z\n .object({\n driver: z.enum(['memory', 'redis']),\n connection: z.any().optional(),\n })\n .passthrough()\n .optional(),\n db: z\n .object({\n driver: z.enum(['postgres', 'mysql', 'sqlite', 'libsql']),\n url: z.string(),\n authToken: z.string().optional(),\n })\n .passthrough()\n .optional(),\n })\n .passthrough();\n\nexport type AppConfigInput = z.input<typeof AppConfigSchema>;\nexport type AppConfigOutput = z.output<typeof AppConfigSchema>;\n","export const ErrorCodes = {\n // General\n INTERNAL_ERROR: 'INTERNAL_ERROR',\n NOT_INITIALIZED: 'NOT_INITIALIZED',\n ADAPTER_ERROR: 'ADAPTER_ERROR',\n\n // Config\n CONFIG_INVALID: 'CONFIG_INVALID',\n CONFIG_MISSING: 'CONFIG_MISSING',\n\n // Driver\n DRIVER_INIT_FAILED: 'DRIVER_INIT_FAILED',\n DRIVER_START_FAILED: 'DRIVER_START_FAILED',\n DRIVER_STOP_FAILED: 'DRIVER_STOP_FAILED',\n DRIVER_NOT_FOUND: 'DRIVER_NOT_FOUND',\n\n // Plugin\n PLUGIN_INSTALL_FAILED: 'PLUGIN_INSTALL_FAILED',\n\n // Lifecycle\n LIFECYCLE_START_FAILED: 'LIFECYCLE_START_FAILED',\n LIFECYCLE_STOP_FAILED: 'LIFECYCLE_STOP_FAILED',\n\n // HTTP\n BAD_REQUEST: 'BAD_REQUEST',\n UNAUTHORIZED: 'UNAUTHORIZED',\n FORBIDDEN: 'FORBIDDEN',\n NOT_FOUND: 'NOT_FOUND',\n CONFLICT: 'CONFLICT',\n VALIDATION_ERROR: 'VALIDATION_ERROR',\n\n // Data\n DATABASE_ERROR: 'DATABASE_ERROR',\n CONNECTION_ERROR: 'CONNECTION_ERROR',\n QUERY_ERROR: 'QUERY_ERROR',\n MIGRATION_ERROR: 'MIGRATION_ERROR',\n CACHE_ERROR: 'CACHE_ERROR',\n\n // Storage / Upload\n STORAGE_ERROR: 'STORAGE_ERROR',\n FILE_UPLOAD_ERROR: 'FILE_UPLOAD_ERROR',\n FILE_NOT_FOUND: 'FILE_NOT_FOUND',\n\n // Worker\n QUEUE_ERROR: 'QUEUE_ERROR',\n JOB_ERROR: 'JOB_ERROR',\n\n // Socket\n SOCKET_CONNECTION_ERROR: 'SOCKET_CONNECTION_ERROR',\n SOCKET_MESSAGE_ERROR: 'SOCKET_MESSAGE_ERROR',\n} as const;\n\nexport type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];\n\nexport interface IskraErrorOptions {\n code: ErrorCode;\n cause?: Error;\n context?: Record<string, unknown>;\n}\n\n/**\n * Clase base para todos los errores del framework Iskra.\n * Soporta encadenamiento de errores vĂa `cause` y metadata arbitraria en `context`.\n */\nexport class IskraError extends Error {\n public readonly code: ErrorCode;\n public readonly context: Record<string, unknown>;\n\n constructor(message: string, options: IskraErrorOptions) {\n super(message, { cause: options.cause });\n this.name = 'IskraError';\n this.code = options.code;\n this.context = options.context ?? {};\n }\n\n toJSON() {\n return {\n name: this.name,\n message: this.message,\n code: this.code,\n context: this.context,\n ...(this.cause instanceof Error ? { cause: this.cause.message } : {}),\n };\n }\n}\n\n// âââ Config Errors âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ\n\nexport class ConfigError extends IskraError {\n constructor(message: string, options?: { cause?: Error; context?: Record<string, unknown> }) {\n super(message, { code: ErrorCodes.CONFIG_INVALID, ...options });\n this.name = 'ConfigError';\n }\n}\n\n// âââ Driver Errors âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ\n\nexport class DriverError extends IskraError {\n constructor(message: string, options?: { code?: ErrorCode; cause?: Error; context?: Record<string, unknown> }) {\n super(message, {\n code: options?.code ?? ErrorCodes.DRIVER_INIT_FAILED,\n cause: options?.cause,\n context: options?.context,\n });\n this.name = 'DriverError';\n }\n}\n\n// âââ Plugin Errors âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ\n\nexport class PluginError extends IskraError {\n constructor(message: string, options?: { cause?: Error; context?: Record<string, unknown> }) {\n super(message, { code: ErrorCodes.PLUGIN_INSTALL_FAILED, ...options });\n this.name = 'PluginError';\n }\n}\n\n// âââ Lifecycle Errors ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ\n\nexport class LifecycleError extends IskraError {\n public readonly failures: PromiseRejectedResult[];\n\n constructor(\n message: string,\n options: { failures?: PromiseRejectedResult[]; cause?: Error; context?: Record<string, unknown> },\n ) {\n super(message, { code: ErrorCodes.LIFECYCLE_STOP_FAILED, cause: options.cause, context: options.context });\n this.name = 'LifecycleError';\n this.failures = options.failures ?? [];\n }\n}\n","/**\n * OpenTelemetry SDK initialization for Iskra apps.\n *\n * All OTel packages are dynamic-imported so they remain optional peer deps.\n * Apps that don't set `config.otel` never load this code.\n *\n * The module specifiers are held in variables rather than passed as string\n * literals to `import()`. This stops `tsc` from statically resolving the\n * optional `@opentelemetry/*` packages at compile time, so the module\n * typechecks cleanly even when those peer deps are absent. At runtime the\n * imports still resolve normally, and a missing package degrades to a clear,\n * actionable error instead of an unhandled module-resolution failure.\n */\nimport type { OtelConfig } from './types';\nimport { nodeEnv } from './env';\n\nlet sdkInstance: { shutdown(): Promise<void> } | null = null;\n\n/** Optional OTel package specifiers, indirected so tsc does not resolve them statically. */\nconst OTEL_MODULES = {\n sdkNode: '@opentelemetry/sdk-node',\n autoInstrumentations: '@opentelemetry/auto-instrumentations-node',\n traceExporter: '@opentelemetry/exporter-trace-otlp-http',\n metricExporter: '@opentelemetry/exporter-metrics-otlp-http',\n sdkMetrics: '@opentelemetry/sdk-metrics',\n resources: '@opentelemetry/resources',\n semconv: '@opentelemetry/semantic-conventions',\n} as const;\n\n// The optional OTel packages are loaded by specifier, untyped; these are the\n// parts of them used here.\ntype Ctor<T = unknown> = new (options: Record<string, unknown>) => T;\ninterface OtelSdkNode {\n NodeSDK: Ctor<{ start(): void; shutdown(): Promise<void> }>;\n}\ninterface OtelAutoInstrumentations {\n getNodeAutoInstrumentations(overrides: Record<string, unknown>): unknown;\n}\ninterface OtelTraceExporter {\n OTLPTraceExporter: Ctor;\n}\ninterface OtelMetricExporter {\n OTLPMetricExporter: Ctor;\n}\ninterface OtelSdkMetrics {\n PeriodicExportingMetricReader: Ctor;\n}\ninterface OtelSemconv {\n ATTR_SERVICE_NAME?: string;\n ATTR_SERVICE_VERSION?: string;\n}\n/** `@opentelemetry/resources`: 2.x exports resourceFromAttributes(), 1.x the Resource class. */\nexport interface OtelResourcesModule {\n resourceFromAttributes?: (attributes: Record<string, string>) => unknown;\n Resource?: new (attributes: Record<string, string>) => unknown;\n}\n\n/** Import an optional module by specifier; the indirection keeps tsc from resolving it. */\nfunction importOptional<T>(specifier: string): Promise<T> {\n return import(specifier) as Promise<T>;\n}\n\nconst DEFAULT_ENDPOINT = 'http://localhost:4318';\nconst HTTP_INSTRUMENTATION = '@opentelemetry/instrumentation-http';\n\n/**\n * Query parameters whose values are exported as REDACTED in span URLs (see\n * redactUrl). Names are compared without case, `-` or `_`.\n */\nexport const SECRET_QUERY_PARAMS: readonly string[] = [\n 'token',\n 'access_token',\n 'refresh_token',\n 'id_token',\n 'auth_token',\n 'session_token',\n 'api_key',\n 'key',\n 'secret',\n 'client_secret',\n 'password',\n 'passwd',\n 'code',\n 'state',\n 'ticket',\n 'otp',\n 'jwt',\n 'signature',\n 'sig',\n 'X-Amz-Signature',\n 'X-Amz-Credential',\n 'X-Amz-Security-Token',\n 'AWSAccessKeyId',\n 'X-Goog-Signature',\n 'X-Goog-Credential',\n];\n\nconst normalizeParam = (name: string): string => name.toLowerCase().replace(/[-_]/g, '');\n\nfunction decodeParam(name: string): string {\n try {\n return decodeURIComponent(name.replace(/\\+/g, ' '));\n } catch {\n return name;\n }\n}\n\n/**\n * `url` (absolute, or a path) with the value of each query parameter named in\n * `params` replaced by REDACTED, the rest kept as it was. Telemetry exported\n * URLs as they were requested: `?api_key=`, `?access_token=`, a presigned\n * URL's signature.\n */\nexport function redactUrl(url: string, params: readonly string[] = SECRET_QUERY_PARAMS): string {\n const start = url.indexOf('?');\n if (start === -1 || params.length === 0) return url;\n const names = new Set(params.map(normalizeParam));\n const hash = url.indexOf('#', start);\n const end = hash === -1 ? url.length : hash;\n let redacted = false;\n const query = url\n .slice(start + 1, end)\n .split('&')\n .map((pair) => {\n const eq = pair.indexOf('=');\n if (eq === -1 || !names.has(normalizeParam(decodeParam(pair.slice(0, eq))))) return pair;\n redacted = true;\n return `${pair.slice(0, eq)}=REDACTED`;\n })\n .join('&');\n return redacted ? url.slice(0, start + 1) + query + url.slice(end) : url;\n}\n\n/** The URL attributes of an HTTP span, in the stable and the older semantic conventions. */\nconst URL_ATTRIBUTES = ['url.full', 'url.query', 'http.url', 'http.target'];\n\n/** Rewrites the URL attributes of a recording SDK span (it has `attributes`). */\nfunction redactSpanUrls(span: unknown, params: readonly string[]): void {\n const { attributes, setAttribute } = (span ?? {}) as {\n attributes?: Record<string, unknown>;\n setAttribute?: (key: string, value: string) => unknown;\n };\n if (!attributes || typeof setAttribute !== 'function') return;\n for (const key of URL_ATTRIBUTES) {\n const value = attributes[key];\n if (typeof value !== 'string') continue;\n // url.query is the query string alone, without its \"?\".\n const redacted = key === 'url.query' ? redactUrl(`?${value}`, params).slice(1) : redactUrl(value, params);\n if (redacted !== value) setAttribute.call(span, key, redacted);\n }\n}\n\n/**\n * The options initOtel() passes to getNodeAutoInstrumentations(): the app's\n * `instrumentations` over Iskra's defaults. The HTTP instrumentation redacts\n * SECRET_QUERY_PARAMS unless `redactedQueryParams` / `redactedQueryParamsServer`\n * are set (instrumentation-http 0.204 / 0.222 and later apply them itself;\n * for older releases a requestHook rewrites the span's URL attributes, before\n * the app's own requestHook runs).\n */\nexport function autoInstrumentationOptions(config: OtelConfig): Record<string, unknown> {\n const http: Record<string, unknown> = { ...config.instrumentations?.[HTTP_INSTRUMENTATION] };\n const list = (value: unknown) => (Array.isArray(value) ? (value as string[]) : undefined);\n const client = list(http.redactedQueryParams) ?? SECRET_QUERY_PARAMS;\n const server = list(http.redactedQueryParamsServer) ?? SECRET_QUERY_PARAMS;\n const appHook = http.requestHook;\n return {\n '@opentelemetry/instrumentation-fs': { enabled: false },\n ...config.instrumentations,\n [HTTP_INSTRUMENTATION]: {\n ...http,\n redactedQueryParams: client,\n redactedQueryParamsServer: server,\n requestHook: (span: unknown, request: unknown) => {\n // A ClientRequest (outgoing) has setHeader(); an IncomingMessage does not.\n const outgoing = typeof (request as { setHeader?: unknown } | null)?.setHeader === 'function';\n redactSpanUrls(span, outgoing ? client : server);\n if (typeof appHook === 'function') appHook(span, request);\n },\n },\n };\n}\n\n/** Hosts spans may reach in clear text: this machine, a private network, or a name that only resolves inside one. */\nfunction isPrivateHost(hostname: string): boolean {\n const host = hostname.replace(/^\\[|\\]$/g, '').toLowerCase();\n if (host.includes(':')) return host === '::1' || /^f[cd]/.test(host);\n return (\n !host.includes('.') ||\n /\\.(localhost|local|internal)$/.test(host) ||\n /^(127|10)\\./.test(host) ||\n /^192\\.168\\./.test(host) ||\n /^172\\.(1[6-9]|2\\d|3[01])\\./.test(host)\n );\n}\n\n/**\n * The OTLP endpoint as it is safe to log, its origin (the path, query or\n * userinfo can carry an API key), and whether spans travel to it in clear\n * text beyond this machine and its private network.\n */\nexport function describeOtelEndpoint(config: OtelConfig): { origin: string; plaintext: boolean } {\n try {\n const url = new URL(config.endpoint || DEFAULT_ENDPOINT);\n return { origin: url.origin, plaintext: url.protocol === 'http:' && !isPrivateHost(url.hostname) };\n } catch {\n return { origin: '(invalid URL)', plaintext: false };\n }\n}\n\n/**\n * Builds a Resource with whichever API the installed `@opentelemetry/resources`\n * provides: 2.x only exports `resourceFromAttributes()` (`Resource` is a type\n * there, so `new Resource()` throws), 1.x exports the `Resource` class.\n */\nexport function createResource(resources: OtelResourcesModule, attributes: Record<string, string>): unknown {\n if (typeof resources?.resourceFromAttributes === 'function') {\n return resources.resourceFromAttributes(attributes);\n }\n if (typeof resources?.Resource === 'function') {\n return new resources.Resource(attributes);\n }\n throw new Error(\n '[iskra/otel] Unsupported @opentelemetry/resources: it exports neither resourceFromAttributes() nor Resource',\n );\n}\n\n/**\n * Initialize the OpenTelemetry NodeSDK with OTLP exporters.\n * Must be called before any drivers start so auto-instrumentation can patch libraries.\n */\nexport async function initOtel(config: OtelConfig, appName: string): Promise<void> {\n if (sdkInstance) return; // Already initialized\n\n try {\n const [\n { NodeSDK },\n { getNodeAutoInstrumentations },\n { OTLPTraceExporter },\n { OTLPMetricExporter },\n { PeriodicExportingMetricReader },\n resources,\n semconv,\n ] = await Promise.all([\n importOptional<OtelSdkNode>(OTEL_MODULES.sdkNode),\n importOptional<OtelAutoInstrumentations>(OTEL_MODULES.autoInstrumentations),\n importOptional<OtelTraceExporter>(OTEL_MODULES.traceExporter),\n importOptional<OtelMetricExporter>(OTEL_MODULES.metricExporter),\n importOptional<OtelSdkMetrics>(OTEL_MODULES.sdkMetrics),\n importOptional<OtelResourcesModule>(OTEL_MODULES.resources),\n importOptional<OtelSemconv>(OTEL_MODULES.semconv),\n ]);\n\n const serviceName = config.serviceName || appName;\n const endpoint = config.endpoint || DEFAULT_ENDPOINT;\n\n const resource = createResource(resources, {\n [semconv.ATTR_SERVICE_NAME ?? 'service.name']: serviceName,\n [semconv.ATTR_SERVICE_VERSION ?? 'service.version']: config.serviceVersion || '0.1.0',\n 'deployment.environment': config.environment || nodeEnv() || 'development',\n ...config.resourceAttributes,\n });\n\n const instrumentationOverrides = autoInstrumentationOptions(config);\n\n const sdk = new NodeSDK({\n resource,\n traceExporter: new OTLPTraceExporter({ url: `${endpoint}/v1/traces` }),\n metricReader: new PeriodicExportingMetricReader({\n exporter: new OTLPMetricExporter({ url: `${endpoint}/v1/metrics` }),\n exportIntervalMillis: config.metricIntervalMs ?? 60_000,\n }),\n instrumentations: [getNodeAutoInstrumentations(instrumentationOverrides)],\n });\n\n sdk.start();\n sdkInstance = sdk;\n } catch (err) {\n // Provide a clear error when OTel packages are not installed\n const e = err as { code?: string; message?: string } | null;\n if (\n e?.code === 'ERR_MODULE_NOT_FOUND' ||\n e?.code === 'MODULE_NOT_FOUND' ||\n e?.message?.includes('Cannot find')\n ) {\n throw new Error(\n `[iskra/otel] OpenTelemetry packages are not installed. ` +\n `Install them with: bun add @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node ` +\n `@opentelemetry/exporter-trace-otlp-http @opentelemetry/exporter-metrics-otlp-http ` +\n `@opentelemetry/sdk-metrics @opentelemetry/resources @opentelemetry/semantic-conventions`,\n );\n }\n throw err;\n }\n}\n\n/**\n * Gracefully shut down the OTel SDK, flushing any pending spans/metrics.\n */\nexport async function shutdownOtel(): Promise<void> {\n if (sdkInstance) {\n await sdkInstance.shutdown();\n sdkInstance = null;\n }\n}\n"],"mappings":"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","names":["result"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iskra-bun/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Nucleo del framework Iskra: App, DI, eventos, logger, errores.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"iskra",
|
|
@@ -19,6 +19,9 @@
|
|
|
19
19
|
},
|
|
20
20
|
"homepage": "https://github.com/fearful/iskra/tree/main/packages/core#readme",
|
|
21
21
|
"bugs": "https://github.com/fearful/iskra/issues",
|
|
22
|
+
"engines": {
|
|
23
|
+
"bun": ">=1.3.0"
|
|
24
|
+
},
|
|
22
25
|
"type": "module",
|
|
23
26
|
"main": "./dist/index.js",
|
|
24
27
|
"module": "./dist/index.js",
|
|
@@ -46,7 +49,7 @@
|
|
|
46
49
|
"build": "tsup --config ../../tsup.config.ts"
|
|
47
50
|
},
|
|
48
51
|
"dependencies": {
|
|
49
|
-
"c12": "^
|
|
52
|
+
"c12": "^3.3.4",
|
|
50
53
|
"mitt": "^3.0.1",
|
|
51
54
|
"pino": "^9.6.0",
|
|
52
55
|
"pino-pretty": "^13.0.0",
|
|
@@ -85,6 +88,7 @@
|
|
|
85
88
|
}
|
|
86
89
|
},
|
|
87
90
|
"devDependencies": {
|
|
91
|
+
"@opentelemetry/resources": "^2.11.0",
|
|
88
92
|
"@types/node": "^22.10.2"
|
|
89
93
|
}
|
|
90
94
|
}
|
package/src/app.ts
CHANGED
|
@@ -1,21 +1,50 @@
|
|
|
1
1
|
import mitt, { type Emitter } from 'mitt';
|
|
2
2
|
import { createLogger, type Logger } from './logger';
|
|
3
3
|
import { loadAppConfig } from './config/loader';
|
|
4
|
-
import type { AppConfig, Driver, Plugin, Context } from './types';
|
|
4
|
+
import type { AppConfig, AppContextRegistry, AppEvents, Driver, Plugin, Context } from './types';
|
|
5
5
|
import { LifecycleError } from './errors';
|
|
6
|
-
import { initOtel, shutdownOtel } from './otel';
|
|
6
|
+
import { describeOtelEndpoint, initOtel, shutdownOtel } from './otel';
|
|
7
|
+
import { nodeEnv } from './env';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* `app.context`: a Map whose keys in AppContextRegistry (`'db'`, `'kv'`, âŚ)
|
|
11
|
+
* are typed; any other key holds `unknown`, or the type given as `T`.
|
|
12
|
+
*/
|
|
13
|
+
export class AppContext extends Map<string, unknown> {
|
|
14
|
+
override get<K extends keyof AppContextRegistry>(key: K): AppContextRegistry[K] | undefined;
|
|
15
|
+
override get<T = unknown>(key: string): T | undefined;
|
|
16
|
+
override get(key: string): unknown {
|
|
17
|
+
return super.get(key);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
override set<K extends keyof AppContextRegistry>(key: K, value: AppContextRegistry[K]): this;
|
|
21
|
+
override set(key: string, value: unknown): this;
|
|
22
|
+
override set(key: string, value: unknown): this {
|
|
23
|
+
return super.set(key, value);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const DEFAULT_SHUTDOWN_SIGNALS: NodeJS.Signals[] = ['SIGTERM', 'SIGINT'];
|
|
28
|
+
const DEFAULT_SHUTDOWN_TIMEOUT_MS = 10_000;
|
|
7
29
|
|
|
8
30
|
export class App {
|
|
9
31
|
public config: AppConfig;
|
|
10
32
|
public logger: Logger;
|
|
11
|
-
public events: Emitter<
|
|
33
|
+
public events: Emitter<Record<string, unknown>>;
|
|
12
34
|
private drivers: Driver[] = [];
|
|
13
|
-
|
|
35
|
+
/** Drivers whose start() succeeded, in start order; null until start() runs. */
|
|
36
|
+
private startedDrivers: Driver[] | null = null;
|
|
37
|
+
private pendingInstalls: PromiseLike<void>[] = [];
|
|
38
|
+
/** The shutdown in progress, shared by concurrent stop() calls. */
|
|
39
|
+
private stopping: Promise<void> | null = null;
|
|
40
|
+
private signalHandler?: (signal: NodeJS.Signals) => void;
|
|
41
|
+
private signals: NodeJS.Signals[] = [];
|
|
42
|
+
public context = new AppContext();
|
|
14
43
|
|
|
15
44
|
constructor(config?: AppConfig) {
|
|
16
45
|
this.events = mitt();
|
|
17
46
|
// Temporary config until init() is called (or passed in constructor)
|
|
18
|
-
this.config = config || { name: 'Bootstrapping', logger: { level: 'info' } } as AppConfig;
|
|
47
|
+
this.config = config || ({ name: 'Bootstrapping', logger: { level: 'info' } } as AppConfig);
|
|
19
48
|
this.logger = createLogger(this.config.name, this.config.logger?.level);
|
|
20
49
|
}
|
|
21
50
|
|
|
@@ -29,15 +58,42 @@ export class App {
|
|
|
29
58
|
// Initialize OpenTelemetry SDK before drivers (so auto-instrumentation patches libraries)
|
|
30
59
|
if (this.config.otel && this.config.otel.enabled !== false) {
|
|
31
60
|
await initOtel(this.config.otel, this.config.name);
|
|
32
|
-
|
|
61
|
+
// Its origin only: the endpoint's path, query or userinfo can hold an API key.
|
|
62
|
+
const { origin, plaintext } = describeOtelEndpoint(this.config.otel);
|
|
63
|
+
this.logger.info({ endpoint: origin }, 'OpenTelemetry initialized');
|
|
64
|
+
if (plaintext) {
|
|
65
|
+
this.logger.warn(
|
|
66
|
+
{ endpoint: origin },
|
|
67
|
+
'OpenTelemetry endpoint is plain http:// on a remote host: spans (URLs, captured headers) travel unencrypted; use https://',
|
|
68
|
+
);
|
|
69
|
+
}
|
|
33
70
|
}
|
|
34
71
|
|
|
35
72
|
this.logger.info('Initializing App...');
|
|
36
73
|
|
|
74
|
+
// Async plugin installs are awaited here so a failure surfaces from
|
|
75
|
+
// start() instead of crashing the process as an unhandled rejection.
|
|
76
|
+
await Promise.all(this.pendingInstalls);
|
|
77
|
+
this.pendingInstalls = [];
|
|
78
|
+
|
|
79
|
+
// Only the drivers whose init() ran are rolled back: one without an
|
|
80
|
+
// init hook was never initialized and has nothing to release yet.
|
|
81
|
+
const initialized: Driver[] = [];
|
|
37
82
|
for (const driver of this.drivers) {
|
|
38
|
-
if (driver.init)
|
|
39
|
-
|
|
83
|
+
if (!driver.init) continue;
|
|
84
|
+
this.logger.debug(`Initializing driver: ${driver.name}`);
|
|
85
|
+
initialized.push(driver);
|
|
86
|
+
try {
|
|
40
87
|
await driver.init(this);
|
|
88
|
+
} catch (err) {
|
|
89
|
+
// The failing driver may hold part of what it opened (a pool, a
|
|
90
|
+
// child process), so it is stopped too, with the ones before it.
|
|
91
|
+
this.logger.error(
|
|
92
|
+
{ err, driver: driver.name },
|
|
93
|
+
'Driver failed to initialize; stopping the drivers already initialized',
|
|
94
|
+
);
|
|
95
|
+
await this.rollback(initialized.reverse());
|
|
96
|
+
throw err;
|
|
41
97
|
}
|
|
42
98
|
}
|
|
43
99
|
}
|
|
@@ -48,17 +104,30 @@ export class App {
|
|
|
48
104
|
}
|
|
49
105
|
|
|
50
106
|
use(plugin: Plugin) {
|
|
51
|
-
plugin.install(this);
|
|
107
|
+
const result = plugin.install(this);
|
|
108
|
+
if (result && typeof (result as PromiseLike<void>).then === 'function') {
|
|
109
|
+
const install = Promise.resolve(result);
|
|
110
|
+
// Observed now so an early rejection is not reported as unhandled;
|
|
111
|
+
// init() awaits the original promise and rethrows its error.
|
|
112
|
+
install.catch(() => {});
|
|
113
|
+
this.pendingInstalls.push(install);
|
|
114
|
+
}
|
|
52
115
|
return this;
|
|
53
116
|
}
|
|
54
117
|
|
|
55
|
-
|
|
118
|
+
/**
|
|
119
|
+
* Runs `handler` for every `event`. Events in AppEvents give the handler a
|
|
120
|
+
* typed payload; for others it is `unknown`, or the type given as `T`.
|
|
121
|
+
*/
|
|
122
|
+
on<K extends keyof AppEvents>(event: K, handler: (ctx: Context<AppEvents[K]>) => Promise<void> | void): this;
|
|
123
|
+
on<T = unknown>(event: string, handler: (ctx: Context<T>) => Promise<void> | void): this;
|
|
124
|
+
on(event: string, handler: (ctx: Context<unknown>) => Promise<void> | void): this {
|
|
56
125
|
this.events.on(event, async (payload) => {
|
|
57
|
-
const ctx: Context = {
|
|
126
|
+
const ctx: Context<unknown> = {
|
|
58
127
|
app: this,
|
|
59
128
|
logger: this.logger.child({ event }),
|
|
60
129
|
payload,
|
|
61
|
-
reply: (data) => this.events.emit(`${event}:reply`, data)
|
|
130
|
+
reply: (data) => this.events.emit(`${event}:reply`, data),
|
|
62
131
|
};
|
|
63
132
|
|
|
64
133
|
try {
|
|
@@ -70,33 +139,162 @@ export class App {
|
|
|
70
139
|
return this;
|
|
71
140
|
}
|
|
72
141
|
|
|
73
|
-
|
|
142
|
+
/** Emits `event`; for events in AppEvents the payload must match their type. */
|
|
143
|
+
emit<K extends keyof AppEvents>(event: K, payload: AppEvents[K]): void;
|
|
144
|
+
emit(event: string, payload?: unknown): void;
|
|
145
|
+
emit(event: string, payload?: unknown): void {
|
|
74
146
|
this.events.emit(event, payload);
|
|
75
147
|
}
|
|
76
148
|
|
|
149
|
+
/**
|
|
150
|
+
* Starts drivers one at a time, in registration order, so a driver can rely
|
|
151
|
+
* on the ones registered before it (e.g. the DB before the web server). If
|
|
152
|
+
* one fails, every driver is stopped in reverse order (all were initialized
|
|
153
|
+
* and may hold connections, including the failing one), OpenTelemetry is
|
|
154
|
+
* shut down and the error is rethrown, instead of leaving ports bound and
|
|
155
|
+
* children running.
|
|
156
|
+
*/
|
|
77
157
|
async start() {
|
|
78
158
|
await this.init();
|
|
159
|
+
if (!nodeEnv()) {
|
|
160
|
+
this.logger.warn(
|
|
161
|
+
'NODE_ENV is not set: production defaults apply (Secure cookies, no sample secrets, JSON logs). ' +
|
|
162
|
+
'Set NODE_ENV=development for local development.',
|
|
163
|
+
);
|
|
164
|
+
}
|
|
79
165
|
this.logger.info('Starting drivers...');
|
|
80
|
-
|
|
166
|
+
const started: Driver[] = [];
|
|
167
|
+
for (const driver of this.drivers) {
|
|
168
|
+
try {
|
|
169
|
+
if (driver.start) await driver.start();
|
|
170
|
+
started.push(driver);
|
|
171
|
+
} catch (err) {
|
|
172
|
+
this.logger.error({ err, driver: driver.name }, 'Driver failed to start; stopping every driver');
|
|
173
|
+
await this.rollback([...this.drivers].reverse());
|
|
174
|
+
throw err;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
this.startedDrivers = started;
|
|
178
|
+
this.installSignalHandlers();
|
|
81
179
|
this.logger.info('App started successfully đ');
|
|
82
180
|
}
|
|
83
181
|
|
|
84
|
-
|
|
182
|
+
/**
|
|
183
|
+
* Stops drivers in reverse start order (the web server before the DB it
|
|
184
|
+
* uses), continuing past failures, and always flushes OpenTelemetry.
|
|
185
|
+
*
|
|
186
|
+
* A call made while a stop is in progress (e.g. an app's own signal
|
|
187
|
+
* handler next to the App's) gets that same stop: it resolves only once
|
|
188
|
+
* every driver has stopped, instead of right away with nothing left to do.
|
|
189
|
+
*/
|
|
190
|
+
stop(): Promise<void> {
|
|
191
|
+
this.stopping ??= this.stopOnce().finally(() => {
|
|
192
|
+
this.stopping = null;
|
|
193
|
+
});
|
|
194
|
+
return this.stopping;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private async stopOnce() {
|
|
85
198
|
this.logger.info('Stopping app...');
|
|
86
|
-
const
|
|
199
|
+
const toStop = [...(this.startedDrivers ?? this.drivers)].reverse();
|
|
200
|
+
this.startedDrivers = [];
|
|
201
|
+
|
|
202
|
+
let failures: PromiseRejectedResult[];
|
|
203
|
+
try {
|
|
204
|
+
failures = await this.stopDrivers(toStop);
|
|
205
|
+
} finally {
|
|
206
|
+
// Shutdown OTel SDK after drivers (flush pending spans/metrics)
|
|
207
|
+
await shutdownOtel();
|
|
208
|
+
// Removed only now: while the drivers stop, a second signal must
|
|
209
|
+
// still reach the handler (forced exit) instead of the default action.
|
|
210
|
+
this.removeSignalHandlers();
|
|
211
|
+
}
|
|
87
212
|
|
|
88
|
-
const failures = results.filter((r): r is PromiseRejectedResult => r.status === 'rejected');
|
|
89
213
|
if (failures.length > 0) {
|
|
90
214
|
this.logger.error({ failures }, 'Some drivers failed to stop');
|
|
91
215
|
throw new LifecycleError('Some drivers failed to stop', {
|
|
92
216
|
failures,
|
|
93
|
-
context: { driverCount:
|
|
217
|
+
context: { driverCount: toStop.length, failedCount: failures.length },
|
|
94
218
|
});
|
|
95
219
|
}
|
|
96
220
|
|
|
97
|
-
// Shutdown OTel SDK after drivers (flush pending spans/metrics)
|
|
98
|
-
await shutdownOtel();
|
|
99
|
-
|
|
100
221
|
this.logger.info('App stopped.');
|
|
101
222
|
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Stops `drivers` after a failed init() or start() and shuts down
|
|
226
|
+
* OpenTelemetry. Failures to stop are logged, not thrown: the caller
|
|
227
|
+
* rethrows the original error. A later stop() stops nothing more.
|
|
228
|
+
*/
|
|
229
|
+
private async rollback(drivers: Driver[]) {
|
|
230
|
+
this.startedDrivers = [];
|
|
231
|
+
await this.stopDrivers(drivers);
|
|
232
|
+
try {
|
|
233
|
+
await shutdownOtel();
|
|
234
|
+
} catch (err) {
|
|
235
|
+
this.logger.error({ err }, 'OpenTelemetry failed to shut down');
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private async stopDrivers(drivers: Driver[]): Promise<PromiseRejectedResult[]> {
|
|
240
|
+
const failures: PromiseRejectedResult[] = [];
|
|
241
|
+
for (const driver of drivers) {
|
|
242
|
+
try {
|
|
243
|
+
if (driver.stop) await driver.stop();
|
|
244
|
+
} catch (reason) {
|
|
245
|
+
this.logger.error({ err: reason, driver: driver.name }, 'Driver failed to stop');
|
|
246
|
+
failures.push({ status: 'rejected', reason });
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return failures;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Stops the app gracefully on SIGTERM/SIGINT (configurable with
|
|
254
|
+
* `shutdownSignals`, off by default under NODE_ENV=test). Once a listener
|
|
255
|
+
* exists the runtime no longer exits on its own, so the handler exits with
|
|
256
|
+
* 0 after a clean stop, or 1 on failure / after `shutdownTimeoutMs`. A
|
|
257
|
+
* second signal forces exit.
|
|
258
|
+
*/
|
|
259
|
+
private installSignalHandlers() {
|
|
260
|
+
const configured = this.config.shutdownSignals;
|
|
261
|
+
const signals =
|
|
262
|
+
configured === false ? [] : (configured ?? (nodeEnv() === 'test' ? [] : DEFAULT_SHUTDOWN_SIGNALS));
|
|
263
|
+
if (signals.length === 0 || this.signalHandler) return;
|
|
264
|
+
|
|
265
|
+
const timeoutMs = this.config.shutdownTimeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS;
|
|
266
|
+
let shuttingDown = false;
|
|
267
|
+
this.signalHandler = (signal) => {
|
|
268
|
+
if (shuttingDown) {
|
|
269
|
+
this.logger.warn({ signal }, 'Second shutdown signal; exiting immediately');
|
|
270
|
+
process.exit(1);
|
|
271
|
+
}
|
|
272
|
+
shuttingDown = true;
|
|
273
|
+
this.logger.info({ signal }, 'Shutdown signal received; stopping app');
|
|
274
|
+
const timer = setTimeout(() => {
|
|
275
|
+
this.logger.error({ timeoutMs }, 'Graceful shutdown timed out; exiting');
|
|
276
|
+
process.exit(1);
|
|
277
|
+
}, timeoutMs);
|
|
278
|
+
this.stop().then(
|
|
279
|
+
() => {
|
|
280
|
+
clearTimeout(timer);
|
|
281
|
+
process.exit(0);
|
|
282
|
+
},
|
|
283
|
+
(err) => {
|
|
284
|
+
clearTimeout(timer);
|
|
285
|
+
this.logger.error({ err }, 'Graceful shutdown failed');
|
|
286
|
+
process.exit(1);
|
|
287
|
+
},
|
|
288
|
+
);
|
|
289
|
+
};
|
|
290
|
+
this.signals = signals as NodeJS.Signals[];
|
|
291
|
+
for (const signal of this.signals) process.on(signal, this.signalHandler);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private removeSignalHandlers() {
|
|
295
|
+
if (!this.signalHandler) return;
|
|
296
|
+
for (const signal of this.signals) process.off(signal, this.signalHandler);
|
|
297
|
+
this.signalHandler = undefined;
|
|
298
|
+
this.signals = [];
|
|
299
|
+
}
|
|
102
300
|
}
|
package/src/config/loader.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { loadConfig } from 'c12';
|
|
2
|
-
import { AppConfigSchema
|
|
2
|
+
import { AppConfigSchema } from './schema';
|
|
3
3
|
import type { AppConfig } from '../types';
|
|
4
4
|
|
|
5
5
|
export async function loadAppConfig(cwd: string = process.cwd()): Promise<AppConfig> {
|
|
@@ -7,7 +7,14 @@ export async function loadAppConfig(cwd: string = process.cwd()): Promise<AppCon
|
|
|
7
7
|
name: 'app',
|
|
8
8
|
configFile: 'app.config',
|
|
9
9
|
cwd,
|
|
10
|
-
dotenv: true
|
|
10
|
+
dotenv: true,
|
|
11
|
+
// c12 would also merge a `.apprc` file from the working directory (a
|
|
12
|
+
// plain key=value file that could add `processes` to spawn or move
|
|
13
|
+
// `otel.endpoint`), and download and run `extends` layers from
|
|
14
|
+
// github:/gitlab:/https:// sources on every start. Local `extends`
|
|
15
|
+
// paths still work.
|
|
16
|
+
rcFile: false,
|
|
17
|
+
giget: false,
|
|
11
18
|
});
|
|
12
19
|
|
|
13
20
|
const validConfig = AppConfigSchema.parse(config || {});
|