@iskra-bun/core 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ # @iskra-bun/core
2
+
3
+ ## 0.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Initial public release.
package/README.md ADDED
@@ -0,0 +1,31 @@
1
+ # @iskra-bun/core
2
+
3
+ Nucleo del framework Iskra: clase `App`, inyeccion de dependencias, bus de eventos, logger y manejo de errores. Todos los kits se registran sobre el Core como Drivers o Plugins.
4
+
5
+ ## Instalacion
6
+
7
+ ```bash
8
+ bun add @iskra-bun/core
9
+ ```
10
+
11
+ ## Uso rapido
12
+
13
+ ```typescript
14
+ import { App } from '@iskra-bun/core'
15
+
16
+ const app = new App({ name: 'mi-app' })
17
+
18
+ app.on('app:started', () => app.logger.info('Listo'))
19
+
20
+ await app.start()
21
+ ```
22
+
23
+ Registra capacidades adicionales con `app.register(driver)` (Drivers) o `app.use(plugin)` (Plugins).
24
+
25
+ ## Documentacion
26
+
27
+ Guia completa: [docs/core.md](../../docs/core.md) · Arquitectura: [docs/arquitectura.md](../../docs/arquitectura.md)
28
+
29
+ ## Licencia
30
+
31
+ AGPL-3.0-or-later
@@ -0,0 +1,323 @@
1
+ import { Emitter } from 'mitt';
2
+ import pino, { Logger as Logger$1 } from 'pino';
3
+ import { z } from 'zod';
4
+
5
+ declare const createLogger: (name: string, level?: string) => pino.Logger<never, boolean>;
6
+ type Logger = pino.Logger;
7
+
8
+ interface Driver {
9
+ name: string;
10
+ init(app: App): Promise<void> | void;
11
+ start?(): Promise<void> | void;
12
+ stop?(): Promise<void> | void;
13
+ }
14
+ interface Plugin {
15
+ name: string;
16
+ install(app: App): Promise<void> | void;
17
+ }
18
+ interface Context<T = any> {
19
+ app: App;
20
+ logger: Logger$1;
21
+ payload: T;
22
+ reply(data: any): void;
23
+ }
24
+ interface OtelConfig {
25
+ /** Defaults to true when otel config is present */
26
+ enabled?: boolean;
27
+ /** OTLP endpoint URL. Defaults to http://localhost:4318 */
28
+ endpoint?: string;
29
+ /** Overrides app name for the service.name resource attribute */
30
+ serviceName?: string;
31
+ /** Service version resource attribute */
32
+ serviceVersion?: string;
33
+ /** Deployment environment resource attribute. Defaults to NODE_ENV */
34
+ environment?: string;
35
+ /** Metric export interval in ms. Defaults to 60000 */
36
+ metricIntervalMs?: number;
37
+ /** Additional resource attributes */
38
+ resourceAttributes?: Record<string, string>;
39
+ /** Auto-instrumentation overrides (passed to getNodeAutoInstrumentations) */
40
+ instrumentations?: Record<string, {
41
+ enabled?: boolean;
42
+ }>;
43
+ }
44
+ interface AppConfig {
45
+ name: string;
46
+ debug?: boolean;
47
+ logger?: {
48
+ level?: string;
49
+ };
50
+ otel?: OtelConfig;
51
+ processes?: Record<string, ProcessConfig>;
52
+ socket?: {
53
+ enabled: boolean;
54
+ port?: number;
55
+ adapter?: 'bun' | 'socket.io';
56
+ };
57
+ kv?: {
58
+ driver: 'memory' | 'redis' | 'libsql';
59
+ connection?: any;
60
+ };
61
+ db?: {
62
+ driver: 'postgres' | 'mysql' | 'sqlite' | 'libsql';
63
+ url: string;
64
+ authToken?: string;
65
+ };
66
+ [key: string]: any;
67
+ }
68
+ interface ProcessConfig {
69
+ command: string;
70
+ args?: string[];
71
+ mode?: 'daemon' | 'oneshot' | 'stdio';
72
+ restartOnCrash?: boolean;
73
+ maxRestarts?: number;
74
+ restartCooldown?: number;
75
+ env?: Record<string, string>;
76
+ }
77
+
78
+ declare class App {
79
+ config: AppConfig;
80
+ logger: Logger;
81
+ events: Emitter<any>;
82
+ private drivers;
83
+ context: Map<string, any>;
84
+ constructor(config?: AppConfig);
85
+ init(): Promise<void>;
86
+ register(driver: Driver): this;
87
+ use(plugin: Plugin): this;
88
+ on(event: string, handler: (ctx: Context) => Promise<void> | void): this;
89
+ emit(event: string, payload: any): void;
90
+ start(): Promise<void>;
91
+ stop(): Promise<void>;
92
+ }
93
+
94
+ declare const ErrorCodes: {
95
+ readonly INTERNAL_ERROR: "INTERNAL_ERROR";
96
+ readonly NOT_INITIALIZED: "NOT_INITIALIZED";
97
+ readonly ADAPTER_ERROR: "ADAPTER_ERROR";
98
+ readonly CONFIG_INVALID: "CONFIG_INVALID";
99
+ readonly CONFIG_MISSING: "CONFIG_MISSING";
100
+ readonly DRIVER_INIT_FAILED: "DRIVER_INIT_FAILED";
101
+ readonly DRIVER_START_FAILED: "DRIVER_START_FAILED";
102
+ readonly DRIVER_STOP_FAILED: "DRIVER_STOP_FAILED";
103
+ readonly DRIVER_NOT_FOUND: "DRIVER_NOT_FOUND";
104
+ readonly PLUGIN_INSTALL_FAILED: "PLUGIN_INSTALL_FAILED";
105
+ readonly LIFECYCLE_START_FAILED: "LIFECYCLE_START_FAILED";
106
+ readonly LIFECYCLE_STOP_FAILED: "LIFECYCLE_STOP_FAILED";
107
+ readonly BAD_REQUEST: "BAD_REQUEST";
108
+ readonly UNAUTHORIZED: "UNAUTHORIZED";
109
+ readonly FORBIDDEN: "FORBIDDEN";
110
+ readonly NOT_FOUND: "NOT_FOUND";
111
+ readonly CONFLICT: "CONFLICT";
112
+ readonly VALIDATION_ERROR: "VALIDATION_ERROR";
113
+ readonly DATABASE_ERROR: "DATABASE_ERROR";
114
+ readonly CONNECTION_ERROR: "CONNECTION_ERROR";
115
+ readonly QUERY_ERROR: "QUERY_ERROR";
116
+ readonly MIGRATION_ERROR: "MIGRATION_ERROR";
117
+ readonly CACHE_ERROR: "CACHE_ERROR";
118
+ readonly STORAGE_ERROR: "STORAGE_ERROR";
119
+ readonly FILE_UPLOAD_ERROR: "FILE_UPLOAD_ERROR";
120
+ readonly FILE_NOT_FOUND: "FILE_NOT_FOUND";
121
+ readonly QUEUE_ERROR: "QUEUE_ERROR";
122
+ readonly JOB_ERROR: "JOB_ERROR";
123
+ readonly SOCKET_CONNECTION_ERROR: "SOCKET_CONNECTION_ERROR";
124
+ readonly SOCKET_MESSAGE_ERROR: "SOCKET_MESSAGE_ERROR";
125
+ };
126
+ type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];
127
+ interface IskraErrorOptions {
128
+ code: ErrorCode;
129
+ cause?: Error;
130
+ context?: Record<string, unknown>;
131
+ }
132
+ /**
133
+ * Clase base para todos los errores del framework Iskra.
134
+ * Soporta encadenamiento de errores vía `cause` y metadata arbitraria en `context`.
135
+ */
136
+ declare class IskraError extends Error {
137
+ readonly code: ErrorCode;
138
+ readonly context: Record<string, unknown>;
139
+ constructor(message: string, options: IskraErrorOptions);
140
+ toJSON(): {
141
+ cause?: string | undefined;
142
+ name: string;
143
+ message: string;
144
+ code: ErrorCode;
145
+ context: Record<string, unknown>;
146
+ };
147
+ }
148
+ declare class ConfigError extends IskraError {
149
+ constructor(message: string, options?: {
150
+ cause?: Error;
151
+ context?: Record<string, unknown>;
152
+ });
153
+ }
154
+ declare class DriverError extends IskraError {
155
+ constructor(message: string, options?: {
156
+ code?: ErrorCode;
157
+ cause?: Error;
158
+ context?: Record<string, unknown>;
159
+ });
160
+ }
161
+ declare class PluginError extends IskraError {
162
+ constructor(message: string, options?: {
163
+ cause?: Error;
164
+ context?: Record<string, unknown>;
165
+ });
166
+ }
167
+ declare class LifecycleError extends IskraError {
168
+ readonly failures: PromiseRejectedResult[];
169
+ constructor(message: string, options: {
170
+ failures?: PromiseRejectedResult[];
171
+ cause?: Error;
172
+ context?: Record<string, unknown>;
173
+ });
174
+ }
175
+
176
+ /**
177
+ * OpenTelemetry SDK initialization for Iskra apps.
178
+ *
179
+ * All OTel packages are dynamic-imported so they remain optional peer deps.
180
+ * Apps that don't set `config.otel` never load this code.
181
+ *
182
+ * The module specifiers are held in variables rather than passed as string
183
+ * literals to `import()`. This stops `tsc` from statically resolving the
184
+ * optional `@opentelemetry/*` packages at compile time, so the module
185
+ * typechecks cleanly even when those peer deps are absent. At runtime the
186
+ * imports still resolve normally, and a missing package degrades to a clear,
187
+ * actionable error instead of an unhandled module-resolution failure.
188
+ */
189
+
190
+ /**
191
+ * Initialize the OpenTelemetry NodeSDK with OTLP exporters.
192
+ * Must be called before any drivers start so auto-instrumentation can patch libraries.
193
+ */
194
+ declare function initOtel(config: OtelConfig, appName: string): Promise<void>;
195
+ /**
196
+ * Gracefully shut down the OTel SDK, flushing any pending spans/metrics.
197
+ */
198
+ declare function shutdownOtel(): Promise<void>;
199
+
200
+ declare function loadAppConfig(cwd?: string): Promise<AppConfig>;
201
+
202
+ declare const AppConfigSchema: z.ZodObject<{
203
+ name: z.ZodDefault<z.ZodString>;
204
+ debug: z.ZodDefault<z.ZodBoolean>;
205
+ logger: z.ZodDefault<z.ZodObject<{
206
+ level: z.ZodDefault<z.ZodString>;
207
+ }, "strip", z.ZodTypeAny, {
208
+ level: string;
209
+ }, {
210
+ level?: string | undefined;
211
+ }>>;
212
+ otel: z.ZodOptional<z.ZodObject<{
213
+ enabled: z.ZodDefault<z.ZodBoolean>;
214
+ endpoint: z.ZodDefault<z.ZodString>;
215
+ serviceName: z.ZodOptional<z.ZodString>;
216
+ serviceVersion: z.ZodDefault<z.ZodString>;
217
+ environment: z.ZodOptional<z.ZodString>;
218
+ metricIntervalMs: z.ZodDefault<z.ZodNumber>;
219
+ resourceAttributes: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
220
+ instrumentations: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
221
+ enabled: z.ZodOptional<z.ZodBoolean>;
222
+ }, "strip", z.ZodTypeAny, {
223
+ enabled?: boolean | undefined;
224
+ }, {
225
+ enabled?: boolean | undefined;
226
+ }>>>;
227
+ }, "strip", z.ZodTypeAny, {
228
+ enabled: boolean;
229
+ endpoint: string;
230
+ serviceVersion: string;
231
+ metricIntervalMs: number;
232
+ serviceName?: string | undefined;
233
+ environment?: string | undefined;
234
+ resourceAttributes?: Record<string, string> | undefined;
235
+ instrumentations?: Record<string, {
236
+ enabled?: boolean | undefined;
237
+ }> | undefined;
238
+ }, {
239
+ enabled?: boolean | undefined;
240
+ endpoint?: string | undefined;
241
+ serviceName?: string | undefined;
242
+ serviceVersion?: string | undefined;
243
+ environment?: string | undefined;
244
+ metricIntervalMs?: number | undefined;
245
+ resourceAttributes?: Record<string, string> | undefined;
246
+ instrumentations?: Record<string, {
247
+ enabled?: boolean | undefined;
248
+ }> | undefined;
249
+ }>>;
250
+ processes: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
251
+ command: z.ZodString;
252
+ args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
253
+ mode: z.ZodDefault<z.ZodEnum<["daemon", "oneshot", "stdio"]>>;
254
+ restartOnCrash: z.ZodDefault<z.ZodBoolean>;
255
+ env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
256
+ }, "strip", z.ZodTypeAny, {
257
+ command: string;
258
+ mode: "daemon" | "oneshot" | "stdio";
259
+ restartOnCrash: boolean;
260
+ args?: string[] | undefined;
261
+ env?: Record<string, string> | undefined;
262
+ }, {
263
+ command: string;
264
+ args?: string[] | undefined;
265
+ mode?: "daemon" | "oneshot" | "stdio" | undefined;
266
+ restartOnCrash?: boolean | undefined;
267
+ env?: Record<string, string> | undefined;
268
+ }>>>;
269
+ }, "strip", z.ZodTypeAny, {
270
+ debug: boolean;
271
+ name: string;
272
+ logger: {
273
+ level: string;
274
+ };
275
+ otel?: {
276
+ enabled: boolean;
277
+ endpoint: string;
278
+ serviceVersion: string;
279
+ metricIntervalMs: number;
280
+ serviceName?: string | undefined;
281
+ environment?: string | undefined;
282
+ resourceAttributes?: Record<string, string> | undefined;
283
+ instrumentations?: Record<string, {
284
+ enabled?: boolean | undefined;
285
+ }> | undefined;
286
+ } | undefined;
287
+ processes?: Record<string, {
288
+ command: string;
289
+ mode: "daemon" | "oneshot" | "stdio";
290
+ restartOnCrash: boolean;
291
+ args?: string[] | undefined;
292
+ env?: Record<string, string> | undefined;
293
+ }> | undefined;
294
+ }, {
295
+ debug?: boolean | undefined;
296
+ name?: string | undefined;
297
+ logger?: {
298
+ level?: string | undefined;
299
+ } | undefined;
300
+ otel?: {
301
+ enabled?: boolean | undefined;
302
+ endpoint?: string | undefined;
303
+ serviceName?: string | undefined;
304
+ serviceVersion?: string | undefined;
305
+ environment?: string | undefined;
306
+ metricIntervalMs?: number | undefined;
307
+ resourceAttributes?: Record<string, string> | undefined;
308
+ instrumentations?: Record<string, {
309
+ enabled?: boolean | undefined;
310
+ }> | undefined;
311
+ } | undefined;
312
+ processes?: Record<string, {
313
+ command: string;
314
+ args?: string[] | undefined;
315
+ mode?: "daemon" | "oneshot" | "stdio" | undefined;
316
+ restartOnCrash?: boolean | undefined;
317
+ env?: Record<string, string> | undefined;
318
+ }> | undefined;
319
+ }>;
320
+ type AppConfigInput = z.input<typeof AppConfigSchema>;
321
+ type AppConfigOutput = z.output<typeof AppConfigSchema>;
322
+
323
+ export { App, type AppConfig, type AppConfigInput, type AppConfigOutput, AppConfigSchema, ConfigError, type Context, type Driver, DriverError, type ErrorCode, ErrorCodes, IskraError, type IskraErrorOptions, LifecycleError, type Logger, type OtelConfig, type Plugin, PluginError, type ProcessConfig, createLogger, initOtel, loadAppConfig, shutdownOtel };
package/dist/index.js ADDED
@@ -0,0 +1,314 @@
1
+ // src/app.ts
2
+ import mitt from "mitt";
3
+
4
+ // src/logger/index.ts
5
+ import pino from "pino";
6
+ var createLogger = (name, level = "info") => {
7
+ return pino({
8
+ name,
9
+ level,
10
+ transport: {
11
+ target: "pino-pretty",
12
+ options: {
13
+ colorize: true
14
+ }
15
+ }
16
+ });
17
+ };
18
+
19
+ // src/config/loader.ts
20
+ import { loadConfig } from "c12";
21
+
22
+ // src/config/schema.ts
23
+ import { z } from "zod";
24
+ var AppConfigSchema = z.object({
25
+ name: z.string().default("IskraApp"),
26
+ debug: z.boolean().default(false),
27
+ logger: z.object({
28
+ level: z.string().default("info")
29
+ }).default({}),
30
+ otel: z.object({
31
+ enabled: z.boolean().default(true),
32
+ endpoint: z.string().default("http://localhost:4318"),
33
+ serviceName: z.string().optional(),
34
+ serviceVersion: z.string().default("0.1.0"),
35
+ environment: z.string().optional(),
36
+ metricIntervalMs: z.number().default(6e4),
37
+ resourceAttributes: z.record(z.string()).optional(),
38
+ instrumentations: z.record(z.object({ enabled: z.boolean().optional() })).optional()
39
+ }).optional(),
40
+ processes: z.record(z.object({
41
+ command: z.string(),
42
+ args: z.array(z.string()).optional(),
43
+ mode: z.enum(["daemon", "oneshot", "stdio"]).default("daemon"),
44
+ restartOnCrash: z.boolean().default(false),
45
+ env: z.record(z.string()).optional()
46
+ })).optional()
47
+ });
48
+
49
+ // src/config/loader.ts
50
+ async function loadAppConfig(cwd = process.cwd()) {
51
+ const { config } = await loadConfig({
52
+ name: "app",
53
+ configFile: "app.config",
54
+ cwd,
55
+ dotenv: true
56
+ });
57
+ const validConfig = AppConfigSchema.parse(config || {});
58
+ return validConfig;
59
+ }
60
+
61
+ // src/errors.ts
62
+ var ErrorCodes = {
63
+ // General
64
+ INTERNAL_ERROR: "INTERNAL_ERROR",
65
+ NOT_INITIALIZED: "NOT_INITIALIZED",
66
+ ADAPTER_ERROR: "ADAPTER_ERROR",
67
+ // Config
68
+ CONFIG_INVALID: "CONFIG_INVALID",
69
+ CONFIG_MISSING: "CONFIG_MISSING",
70
+ // Driver
71
+ DRIVER_INIT_FAILED: "DRIVER_INIT_FAILED",
72
+ DRIVER_START_FAILED: "DRIVER_START_FAILED",
73
+ DRIVER_STOP_FAILED: "DRIVER_STOP_FAILED",
74
+ DRIVER_NOT_FOUND: "DRIVER_NOT_FOUND",
75
+ // Plugin
76
+ PLUGIN_INSTALL_FAILED: "PLUGIN_INSTALL_FAILED",
77
+ // Lifecycle
78
+ LIFECYCLE_START_FAILED: "LIFECYCLE_START_FAILED",
79
+ LIFECYCLE_STOP_FAILED: "LIFECYCLE_STOP_FAILED",
80
+ // HTTP
81
+ BAD_REQUEST: "BAD_REQUEST",
82
+ UNAUTHORIZED: "UNAUTHORIZED",
83
+ FORBIDDEN: "FORBIDDEN",
84
+ NOT_FOUND: "NOT_FOUND",
85
+ CONFLICT: "CONFLICT",
86
+ VALIDATION_ERROR: "VALIDATION_ERROR",
87
+ // Data
88
+ DATABASE_ERROR: "DATABASE_ERROR",
89
+ CONNECTION_ERROR: "CONNECTION_ERROR",
90
+ QUERY_ERROR: "QUERY_ERROR",
91
+ MIGRATION_ERROR: "MIGRATION_ERROR",
92
+ CACHE_ERROR: "CACHE_ERROR",
93
+ // Storage / Upload
94
+ STORAGE_ERROR: "STORAGE_ERROR",
95
+ FILE_UPLOAD_ERROR: "FILE_UPLOAD_ERROR",
96
+ FILE_NOT_FOUND: "FILE_NOT_FOUND",
97
+ // Worker
98
+ QUEUE_ERROR: "QUEUE_ERROR",
99
+ JOB_ERROR: "JOB_ERROR",
100
+ // Socket
101
+ SOCKET_CONNECTION_ERROR: "SOCKET_CONNECTION_ERROR",
102
+ SOCKET_MESSAGE_ERROR: "SOCKET_MESSAGE_ERROR"
103
+ };
104
+ var IskraError = class extends Error {
105
+ code;
106
+ context;
107
+ constructor(message, options) {
108
+ super(message, { cause: options.cause });
109
+ this.name = "IskraError";
110
+ this.code = options.code;
111
+ this.context = options.context ?? {};
112
+ }
113
+ toJSON() {
114
+ return {
115
+ name: this.name,
116
+ message: this.message,
117
+ code: this.code,
118
+ context: this.context,
119
+ ...this.cause instanceof Error ? { cause: this.cause.message } : {}
120
+ };
121
+ }
122
+ };
123
+ var ConfigError = class extends IskraError {
124
+ constructor(message, options) {
125
+ super(message, { code: ErrorCodes.CONFIG_INVALID, ...options });
126
+ this.name = "ConfigError";
127
+ }
128
+ };
129
+ var DriverError = class extends IskraError {
130
+ constructor(message, options) {
131
+ super(message, { code: options?.code ?? ErrorCodes.DRIVER_INIT_FAILED, cause: options?.cause, context: options?.context });
132
+ this.name = "DriverError";
133
+ }
134
+ };
135
+ var PluginError = class extends IskraError {
136
+ constructor(message, options) {
137
+ super(message, { code: ErrorCodes.PLUGIN_INSTALL_FAILED, ...options });
138
+ this.name = "PluginError";
139
+ }
140
+ };
141
+ var LifecycleError = class extends IskraError {
142
+ failures;
143
+ constructor(message, options) {
144
+ super(message, { code: ErrorCodes.LIFECYCLE_STOP_FAILED, cause: options.cause, context: options.context });
145
+ this.name = "LifecycleError";
146
+ this.failures = options.failures ?? [];
147
+ }
148
+ };
149
+
150
+ // src/otel.ts
151
+ var sdkInstance = null;
152
+ var OTEL_MODULES = {
153
+ sdkNode: "@opentelemetry/sdk-node",
154
+ autoInstrumentations: "@opentelemetry/auto-instrumentations-node",
155
+ traceExporter: "@opentelemetry/exporter-trace-otlp-http",
156
+ metricExporter: "@opentelemetry/exporter-metrics-otlp-http",
157
+ sdkMetrics: "@opentelemetry/sdk-metrics",
158
+ resources: "@opentelemetry/resources",
159
+ semconv: "@opentelemetry/semantic-conventions"
160
+ };
161
+ function importOptional(specifier) {
162
+ return import(specifier);
163
+ }
164
+ async function initOtel(config, appName) {
165
+ if (sdkInstance) return;
166
+ try {
167
+ const [
168
+ { NodeSDK },
169
+ { getNodeAutoInstrumentations },
170
+ { OTLPTraceExporter },
171
+ { OTLPMetricExporter },
172
+ { PeriodicExportingMetricReader },
173
+ { Resource },
174
+ semconv
175
+ ] = await Promise.all([
176
+ importOptional(OTEL_MODULES.sdkNode),
177
+ importOptional(OTEL_MODULES.autoInstrumentations),
178
+ importOptional(OTEL_MODULES.traceExporter),
179
+ importOptional(OTEL_MODULES.metricExporter),
180
+ importOptional(OTEL_MODULES.sdkMetrics),
181
+ importOptional(OTEL_MODULES.resources),
182
+ importOptional(OTEL_MODULES.semconv)
183
+ ]);
184
+ const serviceName = config.serviceName || appName;
185
+ const endpoint = config.endpoint || "http://localhost:4318";
186
+ const resource = new Resource({
187
+ [semconv.ATTR_SERVICE_NAME]: serviceName,
188
+ [semconv.ATTR_SERVICE_VERSION]: config.serviceVersion || "0.1.0",
189
+ "deployment.environment": config.environment || process.env.NODE_ENV || "development",
190
+ ...config.resourceAttributes
191
+ });
192
+ const instrumentationOverrides = {
193
+ "@opentelemetry/instrumentation-fs": { enabled: false },
194
+ ...config.instrumentations
195
+ };
196
+ const sdk = new NodeSDK({
197
+ resource,
198
+ traceExporter: new OTLPTraceExporter({ url: `${endpoint}/v1/traces` }),
199
+ metricReader: new PeriodicExportingMetricReader({
200
+ exporter: new OTLPMetricExporter({ url: `${endpoint}/v1/metrics` }),
201
+ exportIntervalMillis: config.metricIntervalMs ?? 6e4
202
+ }),
203
+ instrumentations: [getNodeAutoInstrumentations(instrumentationOverrides)]
204
+ });
205
+ sdk.start();
206
+ sdkInstance = sdk;
207
+ } catch (err) {
208
+ if (err?.code === "ERR_MODULE_NOT_FOUND" || err?.code === "MODULE_NOT_FOUND" || err?.message?.includes("Cannot find")) {
209
+ throw new Error(
210
+ `[iskra/otel] OpenTelemetry packages are not installed. Install them with: bun add @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node @opentelemetry/exporter-trace-otlp-http @opentelemetry/exporter-metrics-otlp-http @opentelemetry/sdk-metrics @opentelemetry/resources @opentelemetry/semantic-conventions`
211
+ );
212
+ }
213
+ throw err;
214
+ }
215
+ }
216
+ async function shutdownOtel() {
217
+ if (sdkInstance) {
218
+ await sdkInstance.shutdown();
219
+ sdkInstance = null;
220
+ }
221
+ }
222
+
223
+ // src/app.ts
224
+ var App = class {
225
+ config;
226
+ logger;
227
+ events;
228
+ drivers = [];
229
+ context = /* @__PURE__ */ new Map();
230
+ constructor(config) {
231
+ this.events = mitt();
232
+ this.config = config || { name: "Bootstrapping", logger: { level: "info" } };
233
+ this.logger = createLogger(this.config.name, this.config.logger?.level);
234
+ }
235
+ async init() {
236
+ if (this.config.name === "Bootstrapping") {
237
+ this.config = await loadAppConfig();
238
+ this.logger = createLogger(this.config.name, this.config.logger?.level);
239
+ }
240
+ if (this.config.otel && this.config.otel.enabled !== false) {
241
+ await initOtel(this.config.otel, this.config.name);
242
+ this.logger.info({ endpoint: this.config.otel.endpoint }, "OpenTelemetry initialized");
243
+ }
244
+ this.logger.info("Initializing App...");
245
+ for (const driver of this.drivers) {
246
+ if (driver.init) {
247
+ this.logger.debug(`Initializing driver: ${driver.name}`);
248
+ await driver.init(this);
249
+ }
250
+ }
251
+ }
252
+ register(driver) {
253
+ this.drivers.push(driver);
254
+ return this;
255
+ }
256
+ use(plugin) {
257
+ plugin.install(this);
258
+ return this;
259
+ }
260
+ on(event, handler) {
261
+ this.events.on(event, async (payload) => {
262
+ const ctx = {
263
+ app: this,
264
+ logger: this.logger.child({ event }),
265
+ payload,
266
+ reply: (data) => this.events.emit(`${event}:reply`, data)
267
+ };
268
+ try {
269
+ await handler(ctx);
270
+ } catch (err) {
271
+ this.logger.error({ err, event }, "Error handling event");
272
+ }
273
+ });
274
+ return this;
275
+ }
276
+ emit(event, payload) {
277
+ this.events.emit(event, payload);
278
+ }
279
+ async start() {
280
+ await this.init();
281
+ this.logger.info("Starting drivers...");
282
+ await Promise.all(this.drivers.map((d) => d.start ? d.start() : Promise.resolve()));
283
+ this.logger.info("App started successfully \u{1F680}");
284
+ }
285
+ async stop() {
286
+ this.logger.info("Stopping app...");
287
+ const results = await Promise.allSettled(this.drivers.map((d) => d.stop ? d.stop() : Promise.resolve()));
288
+ const failures = results.filter((r) => r.status === "rejected");
289
+ if (failures.length > 0) {
290
+ this.logger.error({ failures }, "Some drivers failed to stop");
291
+ throw new LifecycleError("Some drivers failed to stop", {
292
+ failures,
293
+ context: { driverCount: this.drivers.length, failedCount: failures.length }
294
+ });
295
+ }
296
+ await shutdownOtel();
297
+ this.logger.info("App stopped.");
298
+ }
299
+ };
300
+ export {
301
+ App,
302
+ AppConfigSchema,
303
+ ConfigError,
304
+ DriverError,
305
+ ErrorCodes,
306
+ IskraError,
307
+ LifecycleError,
308
+ PluginError,
309
+ createLogger,
310
+ initOtel,
311
+ loadAppConfig,
312
+ shutdownOtel
313
+ };
314
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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":[]}
package/package.json ADDED
@@ -0,0 +1,90 @@
1
+ {
2
+ "name": "@iskra-bun/core",
3
+ "version": "0.1.0",
4
+ "description": "Nucleo del framework Iskra: App, DI, eventos, logger, errores.",
5
+ "keywords": [
6
+ "iskra",
7
+ "bun",
8
+ "typescript",
9
+ "framework",
10
+ "dependency-injection",
11
+ "hexagonal"
12
+ ],
13
+ "author": "Joan Lascano",
14
+ "license": "AGPL-3.0-or-later",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/fearful/iskra.git",
18
+ "directory": "packages/core"
19
+ },
20
+ "homepage": "https://github.com/fearful/iskra/tree/main/packages/core#readme",
21
+ "bugs": "https://github.com/fearful/iskra/issues",
22
+ "type": "module",
23
+ "main": "./dist/index.js",
24
+ "module": "./dist/index.js",
25
+ "types": "./dist/index.d.ts",
26
+ "exports": {
27
+ ".": {
28
+ "source": "./src/index.ts",
29
+ "bun": "./src/index.ts",
30
+ "types": "./dist/index.d.ts",
31
+ "import": "./dist/index.js",
32
+ "default": "./dist/index.js"
33
+ }
34
+ },
35
+ "files": [
36
+ "dist",
37
+ "src",
38
+ "README.md",
39
+ "CHANGELOG.md"
40
+ ],
41
+ "publishConfig": {
42
+ "access": "public"
43
+ },
44
+ "scripts": {
45
+ "test": "bun test",
46
+ "build": "tsup --config ../../tsup.config.ts"
47
+ },
48
+ "dependencies": {
49
+ "c12": "^2.0.1",
50
+ "mitt": "^3.0.1",
51
+ "pino": "^9.6.0",
52
+ "pino-pretty": "^13.0.0",
53
+ "zod": "^3.24.1"
54
+ },
55
+ "peerDependencies": {
56
+ "@opentelemetry/sdk-node": ">=0.50.0",
57
+ "@opentelemetry/auto-instrumentations-node": ">=0.40.0",
58
+ "@opentelemetry/exporter-trace-otlp-http": ">=0.50.0",
59
+ "@opentelemetry/exporter-metrics-otlp-http": ">=0.50.0",
60
+ "@opentelemetry/sdk-metrics": ">=1.20.0",
61
+ "@opentelemetry/resources": ">=1.20.0",
62
+ "@opentelemetry/semantic-conventions": ">=1.20.0"
63
+ },
64
+ "peerDependenciesMeta": {
65
+ "@opentelemetry/sdk-node": {
66
+ "optional": true
67
+ },
68
+ "@opentelemetry/auto-instrumentations-node": {
69
+ "optional": true
70
+ },
71
+ "@opentelemetry/exporter-trace-otlp-http": {
72
+ "optional": true
73
+ },
74
+ "@opentelemetry/exporter-metrics-otlp-http": {
75
+ "optional": true
76
+ },
77
+ "@opentelemetry/sdk-metrics": {
78
+ "optional": true
79
+ },
80
+ "@opentelemetry/resources": {
81
+ "optional": true
82
+ },
83
+ "@opentelemetry/semantic-conventions": {
84
+ "optional": true
85
+ }
86
+ },
87
+ "devDependencies": {
88
+ "@types/node": "^22.10.2"
89
+ }
90
+ }
package/src/app.ts ADDED
@@ -0,0 +1,102 @@
1
+ import mitt, { type Emitter } from 'mitt';
2
+ import { createLogger, type Logger } from './logger';
3
+ import { loadAppConfig } from './config/loader';
4
+ import type { AppConfig, Driver, Plugin, Context } from './types';
5
+ import { LifecycleError } from './errors';
6
+ import { initOtel, shutdownOtel } from './otel';
7
+
8
+ export class App {
9
+ public config: AppConfig;
10
+ public logger: Logger;
11
+ public events: Emitter<any>;
12
+ private drivers: Driver[] = [];
13
+ public context: Map<string, any> = new Map();
14
+
15
+ constructor(config?: AppConfig) {
16
+ this.events = mitt();
17
+ // Temporary config until init() is called (or passed in constructor)
18
+ this.config = config || { name: 'Bootstrapping', logger: { level: 'info' } } as AppConfig;
19
+ this.logger = createLogger(this.config.name, this.config.logger?.level);
20
+ }
21
+
22
+ async init() {
23
+ // If config was not passed, load it
24
+ if (this.config.name === 'Bootstrapping') {
25
+ this.config = await loadAppConfig();
26
+ this.logger = createLogger(this.config.name, this.config.logger?.level);
27
+ }
28
+
29
+ // Initialize OpenTelemetry SDK before drivers (so auto-instrumentation patches libraries)
30
+ if (this.config.otel && this.config.otel.enabled !== false) {
31
+ await initOtel(this.config.otel, this.config.name);
32
+ this.logger.info({ endpoint: this.config.otel.endpoint }, 'OpenTelemetry initialized');
33
+ }
34
+
35
+ this.logger.info('Initializing App...');
36
+
37
+ for (const driver of this.drivers) {
38
+ if (driver.init) {
39
+ this.logger.debug(`Initializing driver: ${driver.name}`);
40
+ await driver.init(this);
41
+ }
42
+ }
43
+ }
44
+
45
+ register(driver: Driver) {
46
+ this.drivers.push(driver);
47
+ return this;
48
+ }
49
+
50
+ use(plugin: Plugin) {
51
+ plugin.install(this);
52
+ return this;
53
+ }
54
+
55
+ on(event: string, handler: (ctx: Context) => Promise<void> | void) {
56
+ this.events.on(event, async (payload) => {
57
+ const ctx: Context = {
58
+ app: this,
59
+ logger: this.logger.child({ event }),
60
+ payload,
61
+ reply: (data) => this.events.emit(`${event}:reply`, data)
62
+ };
63
+
64
+ try {
65
+ await handler(ctx);
66
+ } catch (err) {
67
+ this.logger.error({ err, event }, 'Error handling event');
68
+ }
69
+ });
70
+ return this;
71
+ }
72
+
73
+ emit(event: string, payload: any) {
74
+ this.events.emit(event, payload);
75
+ }
76
+
77
+ async start() {
78
+ await this.init();
79
+ this.logger.info('Starting drivers...');
80
+ await Promise.all(this.drivers.map(d => d.start ? d.start() : Promise.resolve()));
81
+ this.logger.info('App started successfully 🚀');
82
+ }
83
+
84
+ async stop() {
85
+ this.logger.info('Stopping app...');
86
+ const results = await Promise.allSettled(this.drivers.map(d => d.stop ? d.stop() : Promise.resolve()));
87
+
88
+ const failures = results.filter((r): r is PromiseRejectedResult => r.status === 'rejected');
89
+ if (failures.length > 0) {
90
+ this.logger.error({ failures }, 'Some drivers failed to stop');
91
+ throw new LifecycleError('Some drivers failed to stop', {
92
+ failures,
93
+ context: { driverCount: this.drivers.length, failedCount: failures.length },
94
+ });
95
+ }
96
+
97
+ // Shutdown OTel SDK after drivers (flush pending spans/metrics)
98
+ await shutdownOtel();
99
+
100
+ this.logger.info('App stopped.');
101
+ }
102
+ }
@@ -0,0 +1,15 @@
1
+ import { loadConfig } from 'c12';
2
+ import { AppConfigSchema, type AppConfigOutput } from './schema';
3
+ import type { AppConfig } from '../types';
4
+
5
+ export async function loadAppConfig(cwd: string = process.cwd()): Promise<AppConfig> {
6
+ const { config } = await loadConfig<AppConfig>({
7
+ name: 'app',
8
+ configFile: 'app.config',
9
+ cwd,
10
+ dotenv: true
11
+ });
12
+
13
+ const validConfig = AppConfigSchema.parse(config || {});
14
+ return validConfig as AppConfig;
15
+ }
@@ -0,0 +1,29 @@
1
+ import { z } from 'zod';
2
+
3
+ export const AppConfigSchema = z.object({
4
+ name: z.string().default('IskraApp'),
5
+ debug: z.boolean().default(false),
6
+ logger: z.object({
7
+ level: z.string().default('info')
8
+ }).default({}),
9
+ otel: z.object({
10
+ enabled: z.boolean().default(true),
11
+ endpoint: z.string().default('http://localhost:4318'),
12
+ serviceName: z.string().optional(),
13
+ serviceVersion: z.string().default('0.1.0'),
14
+ environment: z.string().optional(),
15
+ metricIntervalMs: z.number().default(60_000),
16
+ resourceAttributes: z.record(z.string()).optional(),
17
+ instrumentations: z.record(z.object({ enabled: z.boolean().optional() })).optional(),
18
+ }).optional(),
19
+ processes: z.record(z.object({
20
+ command: z.string(),
21
+ args: z.array(z.string()).optional(),
22
+ mode: z.enum(['daemon', 'oneshot', 'stdio']).default('daemon'),
23
+ restartOnCrash: z.boolean().default(false),
24
+ env: z.record(z.string()).optional()
25
+ })).optional()
26
+ });
27
+
28
+ export type AppConfigInput = z.input<typeof AppConfigSchema>;
29
+ export type AppConfigOutput = z.output<typeof AppConfigSchema>;
package/src/errors.ts ADDED
@@ -0,0 +1,124 @@
1
+ export const ErrorCodes = {
2
+ // General
3
+ INTERNAL_ERROR: 'INTERNAL_ERROR',
4
+ NOT_INITIALIZED: 'NOT_INITIALIZED',
5
+ ADAPTER_ERROR: 'ADAPTER_ERROR',
6
+
7
+ // Config
8
+ CONFIG_INVALID: 'CONFIG_INVALID',
9
+ CONFIG_MISSING: 'CONFIG_MISSING',
10
+
11
+ // Driver
12
+ DRIVER_INIT_FAILED: 'DRIVER_INIT_FAILED',
13
+ DRIVER_START_FAILED: 'DRIVER_START_FAILED',
14
+ DRIVER_STOP_FAILED: 'DRIVER_STOP_FAILED',
15
+ DRIVER_NOT_FOUND: 'DRIVER_NOT_FOUND',
16
+
17
+ // Plugin
18
+ PLUGIN_INSTALL_FAILED: 'PLUGIN_INSTALL_FAILED',
19
+
20
+ // Lifecycle
21
+ LIFECYCLE_START_FAILED: 'LIFECYCLE_START_FAILED',
22
+ LIFECYCLE_STOP_FAILED: 'LIFECYCLE_STOP_FAILED',
23
+
24
+ // HTTP
25
+ BAD_REQUEST: 'BAD_REQUEST',
26
+ UNAUTHORIZED: 'UNAUTHORIZED',
27
+ FORBIDDEN: 'FORBIDDEN',
28
+ NOT_FOUND: 'NOT_FOUND',
29
+ CONFLICT: 'CONFLICT',
30
+ VALIDATION_ERROR: 'VALIDATION_ERROR',
31
+
32
+ // Data
33
+ DATABASE_ERROR: 'DATABASE_ERROR',
34
+ CONNECTION_ERROR: 'CONNECTION_ERROR',
35
+ QUERY_ERROR: 'QUERY_ERROR',
36
+ MIGRATION_ERROR: 'MIGRATION_ERROR',
37
+ CACHE_ERROR: 'CACHE_ERROR',
38
+
39
+ // Storage / Upload
40
+ STORAGE_ERROR: 'STORAGE_ERROR',
41
+ FILE_UPLOAD_ERROR: 'FILE_UPLOAD_ERROR',
42
+ FILE_NOT_FOUND: 'FILE_NOT_FOUND',
43
+
44
+ // Worker
45
+ QUEUE_ERROR: 'QUEUE_ERROR',
46
+ JOB_ERROR: 'JOB_ERROR',
47
+
48
+ // Socket
49
+ SOCKET_CONNECTION_ERROR: 'SOCKET_CONNECTION_ERROR',
50
+ SOCKET_MESSAGE_ERROR: 'SOCKET_MESSAGE_ERROR',
51
+ } as const;
52
+
53
+ export type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];
54
+
55
+ export interface IskraErrorOptions {
56
+ code: ErrorCode;
57
+ cause?: Error;
58
+ context?: Record<string, unknown>;
59
+ }
60
+
61
+ /**
62
+ * Clase base para todos los errores del framework Iskra.
63
+ * Soporta encadenamiento de errores vía `cause` y metadata arbitraria en `context`.
64
+ */
65
+ export class IskraError extends Error {
66
+ public readonly code: ErrorCode;
67
+ public readonly context: Record<string, unknown>;
68
+
69
+ constructor(message: string, options: IskraErrorOptions) {
70
+ super(message, { cause: options.cause });
71
+ this.name = 'IskraError';
72
+ this.code = options.code;
73
+ this.context = options.context ?? {};
74
+ }
75
+
76
+ toJSON() {
77
+ return {
78
+ name: this.name,
79
+ message: this.message,
80
+ code: this.code,
81
+ context: this.context,
82
+ ...(this.cause instanceof Error ? { cause: this.cause.message } : {}),
83
+ };
84
+ }
85
+ }
86
+
87
+ // ─── Config Errors ───────────────────────────────────────────────────────────
88
+
89
+ export class ConfigError extends IskraError {
90
+ constructor(message: string, options?: { cause?: Error; context?: Record<string, unknown> }) {
91
+ super(message, { code: ErrorCodes.CONFIG_INVALID, ...options });
92
+ this.name = 'ConfigError';
93
+ }
94
+ }
95
+
96
+ // ─── Driver Errors ───────────────────────────────────────────────────────────
97
+
98
+ export class DriverError extends IskraError {
99
+ constructor(message: string, options?: { code?: ErrorCode; cause?: Error; context?: Record<string, unknown> }) {
100
+ super(message, { code: options?.code ?? ErrorCodes.DRIVER_INIT_FAILED, cause: options?.cause, context: options?.context });
101
+ this.name = 'DriverError';
102
+ }
103
+ }
104
+
105
+ // ─── Plugin Errors ───────────────────────────────────────────────────────────
106
+
107
+ export class PluginError extends IskraError {
108
+ constructor(message: string, options?: { cause?: Error; context?: Record<string, unknown> }) {
109
+ super(message, { code: ErrorCodes.PLUGIN_INSTALL_FAILED, ...options });
110
+ this.name = 'PluginError';
111
+ }
112
+ }
113
+
114
+ // ─── Lifecycle Errors ────────────────────────────────────────────────────────
115
+
116
+ export class LifecycleError extends IskraError {
117
+ public readonly failures: PromiseRejectedResult[];
118
+
119
+ constructor(message: string, options: { failures?: PromiseRejectedResult[]; cause?: Error; context?: Record<string, unknown> }) {
120
+ super(message, { code: ErrorCodes.LIFECYCLE_STOP_FAILED, cause: options.cause, context: options.context });
121
+ this.name = 'LifecycleError';
122
+ this.failures = options.failures ?? [];
123
+ }
124
+ }
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ export * from './app';
2
+ export * from './types';
3
+ export * from './logger';
4
+ export * from './errors';
5
+ export * from './otel';
6
+ export * from './config/loader';
7
+ export * from './config/schema';
@@ -0,0 +1,16 @@
1
+ import pino from 'pino';
2
+
3
+ export const createLogger = (name: string, level: string = 'info') => {
4
+ return pino({
5
+ name,
6
+ level,
7
+ transport: {
8
+ target: 'pino-pretty',
9
+ options: {
10
+ colorize: true
11
+ }
12
+ }
13
+ });
14
+ };
15
+
16
+ export type Logger = pino.Logger;
package/src/otel.ts ADDED
@@ -0,0 +1,109 @@
1
+ /**
2
+ * OpenTelemetry SDK initialization for Iskra apps.
3
+ *
4
+ * All OTel packages are dynamic-imported so they remain optional peer deps.
5
+ * Apps that don't set `config.otel` never load this code.
6
+ *
7
+ * The module specifiers are held in variables rather than passed as string
8
+ * literals to `import()`. This stops `tsc` from statically resolving the
9
+ * optional `@opentelemetry/*` packages at compile time, so the module
10
+ * typechecks cleanly even when those peer deps are absent. At runtime the
11
+ * imports still resolve normally, and a missing package degrades to a clear,
12
+ * actionable error instead of an unhandled module-resolution failure.
13
+ */
14
+ import type { OtelConfig } from './types';
15
+
16
+ let sdkInstance: any = null;
17
+
18
+ /** Optional OTel package specifiers, indirected so tsc does not resolve them statically. */
19
+ const OTEL_MODULES = {
20
+ sdkNode: '@opentelemetry/sdk-node',
21
+ autoInstrumentations: '@opentelemetry/auto-instrumentations-node',
22
+ traceExporter: '@opentelemetry/exporter-trace-otlp-http',
23
+ metricExporter: '@opentelemetry/exporter-metrics-otlp-http',
24
+ sdkMetrics: '@opentelemetry/sdk-metrics',
25
+ resources: '@opentelemetry/resources',
26
+ semconv: '@opentelemetry/semantic-conventions',
27
+ } as const;
28
+
29
+ /** Import an optional module by specifier; the indirection keeps tsc from resolving it. */
30
+ function importOptional(specifier: string): Promise<any> {
31
+ return import(specifier);
32
+ }
33
+
34
+ /**
35
+ * Initialize the OpenTelemetry NodeSDK with OTLP exporters.
36
+ * Must be called before any drivers start so auto-instrumentation can patch libraries.
37
+ */
38
+ export async function initOtel(config: OtelConfig, appName: string): Promise<void> {
39
+ if (sdkInstance) return; // Already initialized
40
+
41
+ try {
42
+ const [
43
+ { NodeSDK },
44
+ { getNodeAutoInstrumentations },
45
+ { OTLPTraceExporter },
46
+ { OTLPMetricExporter },
47
+ { PeriodicExportingMetricReader },
48
+ { Resource },
49
+ semconv,
50
+ ] = await Promise.all([
51
+ importOptional(OTEL_MODULES.sdkNode),
52
+ importOptional(OTEL_MODULES.autoInstrumentations),
53
+ importOptional(OTEL_MODULES.traceExporter),
54
+ importOptional(OTEL_MODULES.metricExporter),
55
+ importOptional(OTEL_MODULES.sdkMetrics),
56
+ importOptional(OTEL_MODULES.resources),
57
+ importOptional(OTEL_MODULES.semconv),
58
+ ]);
59
+
60
+ const serviceName = config.serviceName || appName;
61
+ const endpoint = config.endpoint || 'http://localhost:4318';
62
+
63
+ const resource = new Resource({
64
+ [semconv.ATTR_SERVICE_NAME]: serviceName,
65
+ [semconv.ATTR_SERVICE_VERSION]: config.serviceVersion || '0.1.0',
66
+ 'deployment.environment': config.environment || process.env.NODE_ENV || 'development',
67
+ ...config.resourceAttributes,
68
+ });
69
+
70
+ const instrumentationOverrides: Record<string, any> = {
71
+ '@opentelemetry/instrumentation-fs': { enabled: false },
72
+ ...config.instrumentations,
73
+ };
74
+
75
+ const sdk = new NodeSDK({
76
+ resource,
77
+ traceExporter: new OTLPTraceExporter({ url: `${endpoint}/v1/traces` }),
78
+ metricReader: new PeriodicExportingMetricReader({
79
+ exporter: new OTLPMetricExporter({ url: `${endpoint}/v1/metrics` }),
80
+ exportIntervalMillis: config.metricIntervalMs ?? 60_000,
81
+ }),
82
+ instrumentations: [getNodeAutoInstrumentations(instrumentationOverrides)],
83
+ });
84
+
85
+ sdk.start();
86
+ sdkInstance = sdk;
87
+ } catch (err: any) {
88
+ // Provide a clear error when OTel packages are not installed
89
+ if (err?.code === 'ERR_MODULE_NOT_FOUND' || err?.code === 'MODULE_NOT_FOUND' || err?.message?.includes('Cannot find')) {
90
+ throw new Error(
91
+ `[iskra/otel] OpenTelemetry packages are not installed. ` +
92
+ `Install them with: bun add @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node ` +
93
+ `@opentelemetry/exporter-trace-otlp-http @opentelemetry/exporter-metrics-otlp-http ` +
94
+ `@opentelemetry/sdk-metrics @opentelemetry/resources @opentelemetry/semantic-conventions`,
95
+ );
96
+ }
97
+ throw err;
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Gracefully shut down the OTel SDK, flushing any pending spans/metrics.
103
+ */
104
+ export async function shutdownOtel(): Promise<void> {
105
+ if (sdkInstance) {
106
+ await sdkInstance.shutdown();
107
+ sdkInstance = null;
108
+ }
109
+ }
package/src/types.ts ADDED
@@ -0,0 +1,75 @@
1
+ import type { App } from './app';
2
+ import type { Logger } from 'pino';
3
+
4
+ export interface Driver {
5
+ name: string;
6
+ init(app: App): Promise<void> | void;
7
+ start?(): Promise<void> | void;
8
+ stop?(): Promise<void> | void;
9
+ }
10
+
11
+ export interface Plugin {
12
+ name: string;
13
+ install(app: App): Promise<void> | void;
14
+ }
15
+
16
+ export interface Context<T = any> {
17
+ app: App;
18
+ logger: Logger;
19
+ payload: T;
20
+ reply(data: any): void;
21
+ }
22
+
23
+ export interface OtelConfig {
24
+ /** Defaults to true when otel config is present */
25
+ enabled?: boolean;
26
+ /** OTLP endpoint URL. Defaults to http://localhost:4318 */
27
+ endpoint?: string;
28
+ /** Overrides app name for the service.name resource attribute */
29
+ serviceName?: string;
30
+ /** Service version resource attribute */
31
+ serviceVersion?: string;
32
+ /** Deployment environment resource attribute. Defaults to NODE_ENV */
33
+ environment?: string;
34
+ /** Metric export interval in ms. Defaults to 60000 */
35
+ metricIntervalMs?: number;
36
+ /** Additional resource attributes */
37
+ resourceAttributes?: Record<string, string>;
38
+ /** Auto-instrumentation overrides (passed to getNodeAutoInstrumentations) */
39
+ instrumentations?: Record<string, { enabled?: boolean }>;
40
+ }
41
+
42
+ export interface AppConfig {
43
+ name: string;
44
+ debug?: boolean;
45
+ logger?: {
46
+ level?: string;
47
+ };
48
+ otel?: OtelConfig;
49
+ processes?: Record<string, ProcessConfig>;
50
+ socket?: {
51
+ enabled: boolean;
52
+ port?: number; // If different from web
53
+ adapter?: 'bun' | 'socket.io';
54
+ };
55
+ kv?: {
56
+ driver: 'memory' | 'redis' | 'libsql';
57
+ connection?: any;
58
+ };
59
+ db?: {
60
+ driver: 'postgres' | 'mysql' | 'sqlite' | 'libsql';
61
+ url: string;
62
+ authToken?: string;
63
+ };
64
+ [key: string]: any;
65
+ }
66
+
67
+ export interface ProcessConfig {
68
+ command: string;
69
+ args?: string[];
70
+ mode?: 'daemon' | 'oneshot' | 'stdio';
71
+ restartOnCrash?: boolean;
72
+ maxRestarts?: number;
73
+ restartCooldown?: number;
74
+ env?: Record<string, string>;
75
+ }