@gobing-ai/ts-infra 0.3.1 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/README.md +52 -29
  2. package/dist/api-client.d.ts +13 -0
  3. package/dist/api-client.d.ts.map +1 -1
  4. package/dist/api-client.js +15 -4
  5. package/dist/event-bus/default-observers.d.ts +53 -0
  6. package/dist/event-bus/default-observers.d.ts.map +1 -0
  7. package/dist/event-bus/default-observers.js +107 -0
  8. package/dist/event-bus/event-bus.d.ts.map +1 -1
  9. package/dist/event-bus/event-bus.js +1 -0
  10. package/dist/event-bus/file-observer.d.ts +25 -0
  11. package/dist/event-bus/file-observer.d.ts.map +1 -0
  12. package/dist/event-bus/file-observer.js +110 -0
  13. package/dist/event-bus/index.d.ts +2 -0
  14. package/dist/event-bus/index.d.ts.map +1 -1
  15. package/dist/event-bus/index.js +2 -0
  16. package/dist/event-bus/types.d.ts +6 -0
  17. package/dist/event-bus/types.d.ts.map +1 -1
  18. package/dist/events.d.ts +100 -0
  19. package/dist/events.d.ts.map +1 -0
  20. package/dist/events.js +12 -0
  21. package/dist/index.d.ts +4 -4
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +3 -5
  24. package/dist/job-queue/db-job-queue.d.ts.map +1 -1
  25. package/dist/job-queue/db-job-queue.js +45 -34
  26. package/dist/job-queue/index.d.ts +0 -1
  27. package/dist/job-queue/index.d.ts.map +1 -1
  28. package/dist/job-queue/index.js +0 -1
  29. package/dist/job-queue/types.d.ts +7 -0
  30. package/dist/job-queue/types.d.ts.map +1 -1
  31. package/dist/job-queue-db.d.ts +2 -0
  32. package/dist/job-queue-db.d.ts.map +1 -0
  33. package/dist/job-queue-db.js +1 -0
  34. package/dist/logger.d.ts +39 -7
  35. package/dist/logger.d.ts.map +1 -1
  36. package/dist/logger.js +76 -73
  37. package/dist/scheduler/action.d.ts +97 -1
  38. package/dist/scheduler/action.d.ts.map +1 -1
  39. package/dist/scheduler/action.js +111 -0
  40. package/dist/scheduler/cloudflare.d.ts +6 -0
  41. package/dist/scheduler/cloudflare.d.ts.map +1 -1
  42. package/dist/scheduler/cloudflare.js +6 -0
  43. package/dist/scheduler/factory.d.ts +2 -0
  44. package/dist/scheduler/factory.d.ts.map +1 -1
  45. package/dist/scheduler/factory.js +2 -0
  46. package/dist/scheduler/index.d.ts +2 -2
  47. package/dist/scheduler/index.d.ts.map +1 -1
  48. package/dist/scheduler/index.js +2 -2
  49. package/dist/scheduler/node.d.ts +4 -0
  50. package/dist/scheduler/node.d.ts.map +1 -1
  51. package/dist/scheduler/node.js +4 -0
  52. package/dist/scheduler/noop.d.ts +1 -0
  53. package/dist/scheduler/noop.d.ts.map +1 -1
  54. package/dist/scheduler/noop.js +1 -0
  55. package/dist/scheduler/wrap-handler.d.ts +18 -0
  56. package/dist/scheduler/wrap-handler.d.ts.map +1 -0
  57. package/dist/scheduler/wrap-handler.js +41 -0
  58. package/dist/scheduler-cloudflare.d.ts +2 -0
  59. package/dist/scheduler-cloudflare.d.ts.map +1 -0
  60. package/dist/scheduler-cloudflare.js +1 -0
  61. package/dist/scheduler-node.d.ts +2 -0
  62. package/dist/scheduler-node.d.ts.map +1 -0
  63. package/dist/scheduler-node.js +1 -0
  64. package/dist/telemetry/config.d.ts +4 -0
  65. package/dist/telemetry/config.d.ts.map +1 -1
  66. package/dist/telemetry/metrics.d.ts +19 -0
  67. package/dist/telemetry/metrics.d.ts.map +1 -1
  68. package/dist/telemetry/metrics.js +19 -0
  69. package/dist/telemetry/sdk.d.ts +4 -0
  70. package/dist/telemetry/sdk.d.ts.map +1 -1
  71. package/dist/telemetry/sdk.js +4 -0
  72. package/dist/telemetry/tracing.d.ts +12 -0
  73. package/dist/telemetry/tracing.d.ts.map +1 -1
  74. package/dist/telemetry/tracing.js +12 -0
  75. package/package.json +19 -2
  76. package/src/api-client.ts +15 -4
  77. package/src/event-bus/default-observers.ts +117 -0
  78. package/src/event-bus/event-bus.ts +1 -0
  79. package/src/event-bus/file-observer.ts +142 -0
  80. package/src/event-bus/index.ts +7 -0
  81. package/src/event-bus/types.ts +6 -0
  82. package/src/events.ts +108 -0
  83. package/src/index.ts +44 -7
  84. package/src/job-queue/db-job-queue.ts +50 -38
  85. package/src/job-queue/index.ts +0 -1
  86. package/src/job-queue/types.ts +7 -0
  87. package/src/job-queue-db.ts +1 -0
  88. package/src/logger.ts +102 -77
  89. package/src/scheduler/action.ts +164 -1
  90. package/src/scheduler/cloudflare.ts +6 -0
  91. package/src/scheduler/factory.ts +2 -0
  92. package/src/scheduler/index.ts +13 -2
  93. package/src/scheduler/node.ts +4 -0
  94. package/src/scheduler/noop.ts +1 -0
  95. package/src/scheduler/wrap-handler.ts +50 -0
  96. package/src/scheduler-cloudflare.ts +1 -0
  97. package/src/scheduler-node.ts +1 -0
  98. package/src/telemetry/config.ts +4 -0
  99. package/src/telemetry/metrics.ts +19 -0
  100. package/src/telemetry/sdk.ts +4 -0
  101. package/src/telemetry/tracing.ts +12 -0
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @gobing-ai/ts-infra
2
2
 
3
- Infrastructure backbone — typed event bus, DB-backed job queue, cron scheduler, OpenTelemetry telemetry, HTTP API client, and structured logging.
3
+ Infrastructure backbone — typed event bus, queue/scheduler contracts, adapter subpaths, OpenTelemetry instrumentation, HTTP API client, and structured logging.
4
4
 
5
5
  ## Overview
6
6
 
@@ -9,11 +9,11 @@ Infrastructure backbone — typed event bus, DB-backed job queue, cron scheduler
9
9
  | Subsystem | Module | Purpose |
10
10
  |-----------|--------|---------|
11
11
  | **Event Bus** | `event-bus/` | Typed pub/sub with sync + async dispatch, lifecycle self-observability |
12
- | **Job Queue** | `job-queue/` | DB-backed enqueue and consume flow (`DBJobQueue`, `DBQueueConsumer`) plus queue interfaces |
13
- | **Scheduler** | `scheduler/` | Cron-like scheduled actions Node (interval), Cloudflare (Cron Triggers), Noop (test) |
12
+ | **Job Queue** | core + `/job-queue-db` | Queue interfaces in core; DB-backed enqueue/consume flow (`DBJobQueue`, `DBQueueConsumer`) via opt-in subpath |
13
+ | **Scheduler** | core + scheduler subpaths | Scheduler contracts, registry, built-in actions, noop adapter in core; Node and Cloudflare adapters via opt-in subpaths |
14
14
  | **Telemetry** | `telemetry/` | OTel instrumentation — tracing (`traceAsync`), metrics (12 instruments), SQL sanitizer; opt-in OTLP export via the `/otel-node` subpath |
15
15
  | **API Client** | `api-client.ts` | Typed HTTP client with OTel tracing, timeout, and error handling |
16
- | **Logger** | `logger.ts` | Structured JSON logger with levels, child loggers, and mute toggle |
16
+ | **Logger** | `logger.ts` | LogTape-backed structured logger with levels, child loggers, injectable sinks, and mute toggle |
17
17
 
18
18
  ## Architecture
19
19
 
@@ -139,16 +139,15 @@ classDiagram
139
139
  }
140
140
  class LoggerFactory {
141
141
  +getLogger(category) Logger
142
- +initializeLogger(level) void
142
+ +initializeLogger(options?) Promise~void~
143
143
  }
144
144
  }
145
145
 
146
146
  SchedulerAdapter <|.. NodeSchedulerAdapter : implements
147
147
  SchedulerAdapter <|.. CloudflareSchedulerAdapter : implements
148
148
  SchedulerAdapter <|.. NoopSchedulerAdapter : implements
149
- SchedulerFactory --> NodeSchedulerAdapter
150
- SchedulerFactory --> CloudflareSchedulerAdapter
151
- SchedulerFactory --> NoopSchedulerAdapter
149
+ SchedulerFactory --> SchedulerAdapter : "uses injected adapter"
150
+ SchedulerFactory --> NoopSchedulerAdapter : "default when none injected"
152
151
  EventBus --> JobQueue : "async dispatch"
153
152
  DBJobQueue --> JobQueue : "implements"
154
153
  DBQueueConsumer --> QueueConsumer : "implements"
@@ -162,7 +161,7 @@ classDiagram
162
161
  ### Event Bus — typed pub/sub
163
162
 
164
163
  ```ts
165
- import { EventBus, type EventMap } from '@gobing-ai/ts-infra';
164
+ import { attachDefaultObservers, createLifecycleBus, EventBus, type EventMap } from '@gobing-ai/ts-infra';
166
165
 
167
166
  // Define your event map
168
167
  type AppEvents = {
@@ -193,14 +192,11 @@ bus.once('user.signed_up', () => console.log('one-time'));
193
192
  **Lifecycle events** — inject a second `EventBus` to observe bus internals:
194
193
 
195
194
  ```ts
196
- type LifecycleEvents = {
197
- 'bus.emit.done': (detail: { event: string; syncCount: number; asyncCount: number; emitDurationMs: number }) => void;
198
- };
199
-
200
- const lifecycleBus = new EventBus<LifecycleEvents>();
195
+ const lifecycleBus = createLifecycleBus();
196
+ attachDefaultObservers(lifecycleBus); // log + telemetry spans; metrics are emitted by EventBus itself
201
197
  lifecycleBus.on('bus.emit.done', (detail) => {
202
198
  // { event, syncCount, asyncCount, emitDurationMs, errors }
203
- metrics.recordEmit(detail);
199
+ console.debug('EventBus emit complete', detail);
204
200
  });
205
201
 
206
202
  const bus = new EventBus<AppEvents>({ lifecycleBus });
@@ -208,11 +204,11 @@ const bus = new EventBus<AppEvents>({ lifecycleBus });
208
204
 
209
205
  ### Job Queue — DB-backed async work
210
206
 
211
- `DBJobQueue` and `DBQueueConsumer` run over `@gobing-ai/ts-db`'s `QueueJobDao`. Use this when event handlers, schedulers, or API handlers need durable background work with retries.
207
+ `DBJobQueue` and `DBQueueConsumer` live behind the `@gobing-ai/ts-infra/job-queue-db` subpath and run over `@gobing-ai/ts-db`'s `QueueJobDao`. Use this when event handlers, schedulers, or API handlers need durable background work with retries.
212
208
 
213
209
  ```ts
214
210
  import { createDbAdapter, QueueJobDao } from '@gobing-ai/ts-db';
215
- import { DBJobQueue, DBQueueConsumer } from '@gobing-ai/ts-infra';
211
+ import { DBJobQueue, DBQueueConsumer } from '@gobing-ai/ts-infra/job-queue-db';
216
212
 
217
213
  const db = await createDbAdapter({ driver: 'bun-sqlite', url: './jobs.db' });
218
214
  const dao = new QueueJobDao(db);
@@ -248,7 +244,8 @@ The consumer claims ready jobs, resets stuck processing jobs after the visibilit
248
244
  ### Scheduler — cron-like actions
249
245
 
250
246
  ```ts
251
- import { NodeSchedulerAdapter, initScheduler } from '@gobing-ai/ts-infra';
247
+ import { initScheduler, setSchedulerAdapter } from '@gobing-ai/ts-infra';
248
+ import { NodeSchedulerAdapter } from '@gobing-ai/ts-infra/scheduler-node';
252
249
 
253
250
  // Node.js (interval-based)
254
251
  const scheduler = new NodeSchedulerAdapter();
@@ -260,7 +257,8 @@ scheduler.register('*/5 * * * *', async () => {
260
257
  });
261
258
  await scheduler.start();
262
259
 
263
- // Or use factory
260
+ // Or use the core factory after injecting the runtime adapter
261
+ setSchedulerAdapter(new NodeSchedulerAdapter());
264
262
  const sched = initScheduler([
265
263
  ['300000', async () => cleanupExpiredSessions()],
266
264
  ['3600000', async () => generateReports()],
@@ -268,7 +266,7 @@ const sched = initScheduler([
268
266
  await sched.start();
269
267
 
270
268
  // Cloudflare Workers
271
- import { CloudflareSchedulerAdapter } from '@gobing-ai/ts-infra';
269
+ import { CloudflareSchedulerAdapter } from '@gobing-ai/ts-infra/scheduler-cloudflare';
272
270
  const cfScheduler = new CloudflareSchedulerAdapter();
273
271
  cfScheduler.register('* * * * *', async () => { /* ... */ });
274
272
 
@@ -312,7 +310,11 @@ The client auto-instruments every request: creates a `CLIENT` span, records meth
312
310
  ```ts
313
311
  import { getLogger, initializeLogger } from '@gobing-ai/ts-infra';
314
312
 
315
- initializeLogger('debug'); // set minimum level
313
+ await initializeLogger({
314
+ level: 'debug',
315
+ console: true,
316
+ json: true,
317
+ });
316
318
 
317
319
  const log = getLogger('auth');
318
320
  log.info('User logged in', { userId: 'u1', method: 'password' });
@@ -323,7 +325,15 @@ const reqLog = log.child({ requestId: 'req-123' });
323
325
  reqLog.error('Validation failed', { field: 'email' });
324
326
  // → {...,"category":"auth","requestId":"req-123","field":"email"}
325
327
 
326
- // In tests, prefer injecting a logger boundary or initializing at a quiet level.
328
+ // File logging is also injectable; ts-infra never opens files directly.
329
+ await initializeLogger({
330
+ console: false,
331
+ fileSink: (line) => {
332
+ // Append `line` using the host runtime's FileSystem / stream owner.
333
+ },
334
+ });
335
+
336
+ // In tests, prefer injecting a logger boundary or initializing with console: false.
327
337
  ```
328
338
 
329
339
  ### Telemetry — OpenTelemetry
@@ -397,8 +407,10 @@ process.on('SIGTERM', async () => {
397
407
  ```
398
408
 
399
409
  The exporter packages are **optional peers** — only consumers of `/otel-node`
400
- need them installed. The main `@gobing-ai/ts-infra` import never pulls them, so
401
- BYO and browser/edge consumers stay lean. To use the subpath:
410
+ need them installed. `@opentelemetry/api` and semantic conventions are the only
411
+ OTel packages used by the core instrumentation surface. The main
412
+ `@gobing-ai/ts-infra` import never pulls exporter/provider SDKs, so BYO and
413
+ browser/edge consumers stay lean. To use the subpath:
402
414
 
403
415
  ```bash
404
416
  bun add @opentelemetry/sdk-trace-node @opentelemetry/sdk-metrics \
@@ -412,25 +424,35 @@ bun add @opentelemetry/sdk-trace-node @opentelemetry/sdk-metrics \
412
424
  ### Install
413
425
 
414
426
  ```bash
415
- bun add @gobing-ai/ts-infra @gobing-ai/ts-runtime @gobing-ai/ts-db
427
+ bun add @gobing-ai/ts-infra
428
+
429
+ # Only needed when using @gobing-ai/ts-infra/job-queue-db.
430
+ bun add @gobing-ai/ts-db
431
+
432
+ # Only needed when using @gobing-ai/ts-infra/otel-node.
433
+ bun add @opentelemetry/sdk-trace-node @opentelemetry/sdk-metrics \
434
+ @opentelemetry/resources \
435
+ @opentelemetry/exporter-trace-otlp-http \
436
+ @opentelemetry/exporter-metrics-otlp-http
416
437
  ```
417
438
 
418
439
  ### Full bootstrap example
419
440
 
420
441
  ```ts
421
- import { createRuntimeContext } from '@gobing-ai/ts-runtime';
442
+ import { createRuntimeContextFromFactory } from '@gobing-ai/ts-runtime';
422
443
  import { createDbAdapter, applyMigrations } from '@gobing-ai/ts-db';
423
444
  import {
424
445
  EventBus,
425
- NodeSchedulerAdapter,
446
+ setSchedulerAdapter,
426
447
  initTelemetry,
427
448
  APIClient,
428
449
  getLogger,
429
450
  initializeLogger,
430
451
  } from '@gobing-ai/ts-infra';
452
+ import { NodeSchedulerAdapter } from '@gobing-ai/ts-infra/scheduler-node';
431
453
 
432
454
  // 1. Runtime
433
- const ctx = createRuntimeContext({ runtimeName: 'node-bun' });
455
+ const ctx = await createRuntimeContextFromFactory();
434
456
 
435
457
  // 2. Database
436
458
  const db = await createDbAdapter({ driver: 'bun-sqlite', url: './data/app.db' });
@@ -438,7 +460,7 @@ await applyMigrations(db);
438
460
  ctx.register('db', db);
439
461
 
440
462
  // 3. Logging
441
- initializeLogger('info');
463
+ await initializeLogger({ level: 'info', console: true });
442
464
  const log = getLogger('app');
443
465
 
444
466
  // 4. Telemetry
@@ -449,6 +471,7 @@ const bus = new EventBus<AppEvents>();
449
471
 
450
472
  // 6. Scheduler
451
473
  const scheduler = new NodeSchedulerAdapter();
474
+ setSchedulerAdapter(scheduler);
452
475
  scheduler.register('3600000', async () => {
453
476
  log.info('Hourly cleanup running');
454
477
  });
@@ -1,20 +1,33 @@
1
+ /** Configuration for {@link APIClient}: base URL, default headers, timeout, and optional custom fetch. */
1
2
  export interface APIClientConfig {
2
3
  baseUrl: string;
3
4
  defaultHeaders?: Record<string, string>;
4
5
  timeout?: number;
5
6
  fetch?: typeof globalThis.fetch;
6
7
  }
8
+ /** Per-request overrides: headers, timeout, operation name, and abort signal. */
7
9
  export interface RequestOptions {
8
10
  headers?: Record<string, string>;
9
11
  timeout?: number;
10
12
  operationName?: string;
11
13
  signal?: AbortSignal;
12
14
  }
15
+ /** HTTP error with status code and response body text. */
13
16
  export declare class APIError extends Error {
14
17
  readonly status: number;
15
18
  readonly body: string;
16
19
  constructor(status: number, body: string);
17
20
  }
21
+ /**
22
+ * Typed HTTP client builder wrapping fetch with automatic JSON serialization,
23
+ * timeout support, and OpenTelemetry tracing on every request.
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * const client = new APIClient({ baseUrl: 'https://api.example.com' });
28
+ * const data = await client.get<User>('/users/1');
29
+ * ```
30
+ */
18
31
  export declare class APIClient {
19
32
  private readonly baseUrl;
20
33
  private readonly defaultHeaders;
@@ -1 +1 @@
1
- {"version":3,"file":"api-client.d.ts","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"AAkBA,MAAM,WAAW,eAAe;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;CACnC;AAED,MAAM,WAAW,cAAc;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,WAAW,CAAC;CACxB;AAED,qBAAa,QAAS,SAAQ,KAAK;aAEX,MAAM,EAAE,MAAM;aACd,IAAI,EAAE,MAAM;gBADZ,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM;CAKnC;AAID,qBAAa,SAAS;IAClB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAyB;IACxD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA0B;gBAEtC,MAAM,EAAE,eAAe;IAOnC,OAAO,CAAC,QAAQ;YAIF,OAAO;IAqFf,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,CAAC,CAAC;IAIvD,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,CAAC,CAAC;IAIxE,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,CAAC,CAAC;IAIvE,MAAM,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,CAAC,CAAC;CAGnE"}
1
+ {"version":3,"file":"api-client.d.ts","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"AAkBA,0GAA0G;AAC1G,MAAM,WAAW,eAAe;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;CACnC;AAED,iFAAiF;AACjF,MAAM,WAAW,cAAc;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,WAAW,CAAC;CACxB;AAED,0DAA0D;AAC1D,qBAAa,QAAS,SAAQ,KAAK;aAEX,MAAM,EAAE,MAAM;aACd,IAAI,EAAE,MAAM;gBADZ,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM;CAKnC;AAID;;;;;;;;;GASG;AACH,qBAAa,SAAS;IAClB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAyB;IACxD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA0B;gBAEtC,MAAM,EAAE,eAAe;IAOnC,OAAO,CAAC,QAAQ;YAIF,OAAO;IAmFf,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,CAAC,CAAC;IAIvD,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,CAAC,CAAC;IAIxE,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,CAAC,CAAC;IAIvE,MAAM,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,CAAC,CAAC;CAGnE"}
@@ -5,6 +5,7 @@ import { SpanKind } from '@opentelemetry/api';
5
5
  import { ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_RESPONSE_STATUS_CODE, ATTR_URL_FULL, } from '@opentelemetry/semantic-conventions';
6
6
  import { getHttpClientRequestDuration, getHttpClientRequestErrors, getHttpClientRequestTotal, } from './telemetry/metrics.js';
7
7
  import { traceAsync } from './telemetry/tracing.js';
8
+ /** HTTP error with status code and response body text. */
8
9
  export class APIError extends Error {
9
10
  status;
10
11
  body;
@@ -16,6 +17,16 @@ export class APIError extends Error {
16
17
  }
17
18
  }
18
19
  // ── Client ──────────────────────────────────────────────────────────
20
+ /**
21
+ * Typed HTTP client builder wrapping fetch with automatic JSON serialization,
22
+ * timeout support, and OpenTelemetry tracing on every request.
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * const client = new APIClient({ baseUrl: 'https://api.example.com' });
27
+ * const data = await client.get<User>('/users/1');
28
+ * ```
29
+ */
19
30
  export class APIClient {
20
31
  baseUrl;
21
32
  defaultHeaders;
@@ -58,8 +69,6 @@ export class APIClient {
58
69
  body: body !== undefined ? JSON.stringify(body) : undefined,
59
70
  signal: combinedSignal,
60
71
  });
61
- if (timer)
62
- clearTimeout(timer);
63
72
  span.setAttribute(ATTR_HTTP_RESPONSE_STATUS_CODE, response.status);
64
73
  getHttpClientRequestTotal().add(1, {
65
74
  'http.request.method': method,
@@ -85,8 +94,6 @@ export class APIClient {
85
94
  return (await response.text());
86
95
  }
87
96
  catch (error) {
88
- if (timer)
89
- clearTimeout(timer);
90
97
  if (!(error instanceof APIError)) {
91
98
  getHttpClientRequestErrors().add(1, {
92
99
  'http.request.method': method,
@@ -95,6 +102,10 @@ export class APIClient {
95
102
  }
96
103
  throw error;
97
104
  }
105
+ finally {
106
+ if (timer)
107
+ clearTimeout(timer);
108
+ }
98
109
  }, { kind: SpanKind.CLIENT });
99
110
  }
100
111
  async get(path, opts) {
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Default lifecycle observers for EventBus self-observability.
3
+ *
4
+ * Connect `BusLifecycleEvents` to the logging and telemetry infrastructure so
5
+ * operators get visibility without wiring everything by hand. Opt-in: nothing
6
+ * is attached unless you call one of these.
7
+ *
8
+ * **Metrics are intentionally omitted here.** Unlike the original design, the
9
+ * `EventBus` core now increments `eventbus.emits.total` / `eventbus.errors.total`
10
+ * inline on every emit (see `event-bus.ts`). A metrics observer would
11
+ * double-count, so the restored default set is **log + telemetry-trace only**.
12
+ *
13
+ * @example One-liner setup
14
+ * ```ts
15
+ * import { EventBus, createLifecycleBus, attachDefaultObservers } from '@gobing-ai/ts-infra';
16
+ *
17
+ * const lifecycleBus = createLifecycleBus();
18
+ * attachDefaultObservers(lifecycleBus);
19
+ *
20
+ * const appBus = new EventBus<AppEvents>({ lifecycleBus });
21
+ * ```
22
+ */
23
+ import { EventBus } from './event-bus';
24
+ import type { BusLifecycleEvents } from './types';
25
+ /**
26
+ * Create a dedicated `EventBus<BusLifecycleEvents>` for use as the
27
+ * `lifecycleBus` constructor argument on another `EventBus`.
28
+ */
29
+ export declare function createLifecycleBus(): EventBus<BusLifecycleEvents>;
30
+ /**
31
+ * Register handlers that write structured log entries for every lifecycle event.
32
+ *
33
+ * - `bus.emit.done` → debug (warn if errors > 0)
34
+ * - `bus.emit.noop` → debug
35
+ * - `bus.handler.error` → warn
36
+ * - `bus.handler.async.enqueued` → debug
37
+ */
38
+ export declare function attachLogObserver(lifecycleBus: EventBus<BusLifecycleEvents>): void;
39
+ /**
40
+ * Register handlers that create OpenTelemetry spans for emit operations.
41
+ *
42
+ * Each `bus.emit.done` creates a span named `eventbus.emit.{EVENT}` with
43
+ * attributes for sync/async counts, duration, and error count. Degrades
44
+ * gracefully when telemetry is not initialised (`traceAsync` is a pass-through).
45
+ */
46
+ export declare function attachTelemetryObserver(lifecycleBus: EventBus<BusLifecycleEvents>): void;
47
+ /**
48
+ * Attach the built-in observers (log + telemetry) to the lifecycle bus in one
49
+ * call. Recommended default for production deployments. Metrics are emitted by
50
+ * the `EventBus` core inline, so they are not part of this set.
51
+ */
52
+ export declare function attachDefaultObservers(lifecycleBus: EventBus<BusLifecycleEvents>): void;
53
+ //# sourceMappingURL=default-observers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"default-observers.d.ts","sourceRoot":"","sources":["../../src/event-bus/default-observers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAIH,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAQlD;;;GAGG;AACH,wBAAgB,kBAAkB,IAAI,QAAQ,CAAC,kBAAkB,CAAC,CAEjE;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,QAAQ,CAAC,kBAAkB,CAAC,GAAG,IAAI,CAoBlF;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,YAAY,EAAE,QAAQ,CAAC,kBAAkB,CAAC,GAAG,IAAI,CA2BxF;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,YAAY,EAAE,QAAQ,CAAC,kBAAkB,CAAC,GAAG,IAAI,CAGvF"}
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Default lifecycle observers for EventBus self-observability.
3
+ *
4
+ * Connect `BusLifecycleEvents` to the logging and telemetry infrastructure so
5
+ * operators get visibility without wiring everything by hand. Opt-in: nothing
6
+ * is attached unless you call one of these.
7
+ *
8
+ * **Metrics are intentionally omitted here.** Unlike the original design, the
9
+ * `EventBus` core now increments `eventbus.emits.total` / `eventbus.errors.total`
10
+ * inline on every emit (see `event-bus.ts`). A metrics observer would
11
+ * double-count, so the restored default set is **log + telemetry-trace only**.
12
+ *
13
+ * @example One-liner setup
14
+ * ```ts
15
+ * import { EventBus, createLifecycleBus, attachDefaultObservers } from '@gobing-ai/ts-infra';
16
+ *
17
+ * const lifecycleBus = createLifecycleBus();
18
+ * attachDefaultObservers(lifecycleBus);
19
+ *
20
+ * const appBus = new EventBus<AppEvents>({ lifecycleBus });
21
+ * ```
22
+ */
23
+ import { getLogger } from '../logger.js';
24
+ import { addSpanAttributes, addSpanEvent, traceAsync } from '../telemetry/tracing.js';
25
+ import { EventBus } from './event-bus.js';
26
+ let _obsLogger;
27
+ function obsLogger() {
28
+ if (!_obsLogger)
29
+ _obsLogger = getLogger('event-bus.observers');
30
+ return _obsLogger;
31
+ }
32
+ /**
33
+ * Create a dedicated `EventBus<BusLifecycleEvents>` for use as the
34
+ * `lifecycleBus` constructor argument on another `EventBus`.
35
+ */
36
+ export function createLifecycleBus() {
37
+ return new EventBus();
38
+ }
39
+ /**
40
+ * Register handlers that write structured log entries for every lifecycle event.
41
+ *
42
+ * - `bus.emit.done` → debug (warn if errors > 0)
43
+ * - `bus.emit.noop` → debug
44
+ * - `bus.handler.error` → warn
45
+ * - `bus.handler.async.enqueued` → debug
46
+ */
47
+ export function attachLogObserver(lifecycleBus) {
48
+ lifecycleBus.on('bus.emit.done', (detail) => {
49
+ if (detail.errors > 0) {
50
+ obsLogger().warn('emit completed with errors', { ...detail });
51
+ }
52
+ else {
53
+ obsLogger().debug('emit done', { ...detail });
54
+ }
55
+ });
56
+ lifecycleBus.on('bus.emit.noop', (detail) => {
57
+ obsLogger().debug('emit with zero handlers', { ...detail });
58
+ });
59
+ lifecycleBus.on('bus.handler.error', (detail) => {
60
+ obsLogger().warn('handler error', { ...detail });
61
+ });
62
+ lifecycleBus.on('bus.handler.async.enqueued', (detail) => {
63
+ obsLogger().debug('async job enqueued', { ...detail });
64
+ });
65
+ }
66
+ /**
67
+ * Register handlers that create OpenTelemetry spans for emit operations.
68
+ *
69
+ * Each `bus.emit.done` creates a span named `eventbus.emit.{EVENT}` with
70
+ * attributes for sync/async counts, duration, and error count. Degrades
71
+ * gracefully when telemetry is not initialised (`traceAsync` is a pass-through).
72
+ */
73
+ export function attachTelemetryObserver(lifecycleBus) {
74
+ lifecycleBus.on('bus.emit.done', (detail) => {
75
+ void traceAsync(`eventbus.emit.${detail.event}`, async (_span) => {
76
+ addSpanAttributes({
77
+ 'eventbus.event': detail.event,
78
+ 'eventbus.sync_count': detail.syncCount,
79
+ 'eventbus.async_count': detail.asyncCount,
80
+ 'eventbus.duration_ms': detail.emitDurationMs,
81
+ 'eventbus.errors': detail.errors,
82
+ });
83
+ if (detail.errors > 0) {
84
+ addSpanEvent('eventbus.emit.errors', { 'eventbus.error_count': detail.errors });
85
+ }
86
+ });
87
+ });
88
+ lifecycleBus.on('bus.handler.error', (detail) => {
89
+ void traceAsync('eventbus.handler.error', async (span) => {
90
+ addSpanAttributes({
91
+ 'eventbus.event': detail.event,
92
+ 'eventbus.handler_mode': detail.mode,
93
+ 'eventbus.error': detail.error,
94
+ });
95
+ span.setStatus({ code: 2, message: detail.error }); // ERROR
96
+ });
97
+ });
98
+ }
99
+ /**
100
+ * Attach the built-in observers (log + telemetry) to the lifecycle bus in one
101
+ * call. Recommended default for production deployments. Metrics are emitted by
102
+ * the `EventBus` core inline, so they are not part of this set.
103
+ */
104
+ export function attachDefaultObservers(lifecycleBus) {
105
+ attachLogObserver(lifecycleBus);
106
+ attachTelemetryObserver(lifecycleBus);
107
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"event-bus.d.ts","sourceRoot":"","sources":["../../src/event-bus/event-bus.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAGnD,OAAO,KAAK,EAER,kBAAkB,EAElB,QAAQ,EAER,gBAAgB,EACnB,MAAM,SAAS,CAAC;AAQjB;;;GAGG;AACH,qBAAa,QAAQ,CAAC,OAAO,SAAS,QAAQ;IAC1C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAyD;IACtF,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAyD;IACvF,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAiD;IACjF,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAkB;IAC3C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAsC;IACnE,OAAO,CAAC,kBAAkB,CAAK;gBAEnB,IAAI,CAAC,EAAE;QACf,QAAQ,CAAC,EAAE,QAAQ,CAAC;QACpB,YAAY,CAAC,EAAE,QAAQ,CAAC,kBAAkB,CAAC,CAAC;KAC/C;IAKD,EAAE,CAAC,CAAC,SAAS,MAAM,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,EAAE,gBAAgB,GAAG,IAAI;IAQzF,IAAI,CAAC,CAAC,SAAS,MAAM,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,EAAE,gBAAgB,GAAG,IAAI;IAS3F,GAAG,CAAC,CAAC,SAAS,MAAM,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,IAAI;IAkBjE,kBAAkB,CAAC,CAAC,SAAS,MAAM,OAAO,EAAE,KAAK,CAAC,EAAE,CAAC,GAAG,IAAI;IAUtD,IAAI,CAAC,CAAC,SAAS,MAAM,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,GAAG,IAAI,EAAE,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IA2E7F,aAAa,CAAC,CAAC,SAAS,MAAM,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM;IAMjF,UAAU,IAAI,MAAM,EAAE;IAOtB,OAAO,CAAC,YAAY;IASpB,OAAO,CAAC,aAAa;IASrB,OAAO,CAAC,iBAAiB;IASzB,OAAO,CAAC,eAAe;IAUvB,OAAO,CAAC,eAAe;IAUvB,OAAO,CAAC,mBAAmB;IAW3B,OAAO,CAAC,oBAAoB;CAU/B"}
1
+ {"version":3,"file":"event-bus.d.ts","sourceRoot":"","sources":["../../src/event-bus/event-bus.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAGnD,OAAO,KAAK,EAER,kBAAkB,EAElB,QAAQ,EAER,gBAAgB,EACnB,MAAM,SAAS,CAAC;AAQjB;;;GAGG;AACH,qBAAa,QAAQ,CAAC,OAAO,SAAS,QAAQ;IAC1C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAyD;IACtF,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAyD;IACvF,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAiD;IACjF,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAkB;IAC3C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAsC;IACnE,OAAO,CAAC,kBAAkB,CAAK;gBAEnB,IAAI,CAAC,EAAE;QACf,QAAQ,CAAC,EAAE,QAAQ,CAAC;QACpB,YAAY,CAAC,EAAE,QAAQ,CAAC,kBAAkB,CAAC,CAAC;KAC/C;IAKD,EAAE,CAAC,CAAC,SAAS,MAAM,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,EAAE,gBAAgB,GAAG,IAAI;IAQzF,IAAI,CAAC,CAAC,SAAS,MAAM,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,EAAE,gBAAgB,GAAG,IAAI;IAS3F,GAAG,CAAC,CAAC,SAAS,MAAM,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,IAAI;IAkBjE,kBAAkB,CAAC,CAAC,SAAS,MAAM,OAAO,EAAE,KAAK,CAAC,EAAE,CAAC,GAAG,IAAI;IAUtD,IAAI,CAAC,CAAC,SAAS,MAAM,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,GAAG,IAAI,EAAE,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IA4E7F,aAAa,CAAC,CAAC,SAAS,MAAM,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM;IAMjF,UAAU,IAAI,MAAM,EAAE;IAOtB,OAAO,CAAC,YAAY;IASpB,OAAO,CAAC,aAAa;IASrB,OAAO,CAAC,iBAAiB;IASzB,OAAO,CAAC,eAAe;IAUvB,OAAO,CAAC,eAAe;IAUvB,OAAO,CAAC,mBAAmB;IAW3B,OAAO,CAAC,oBAAoB;CAU/B"}
@@ -97,6 +97,7 @@ export class EventBus {
97
97
  });
98
98
  busLogger().debug('async job enqueued', { event: eventName, jobId, handlerCount: 1 });
99
99
  this.publishAsyncEnqueued(eventName, jobId, 1);
100
+ continue;
100
101
  }
101
102
  catch (error) {
102
103
  errors++;
@@ -0,0 +1,25 @@
1
+ import type { EventBus } from './event-bus';
2
+ import type { BusLifecycleEvents } from './types';
3
+ /**
4
+ * Minimal append-only file writer the file observer needs.
5
+ *
6
+ * ADR-011: ts-infra must not import `node:fs`. The caller injects a writer —
7
+ * typically `@gobing-ai/ts-runtime`'s `FileSystem`, which already satisfies
8
+ * this shape (`ensureDir` + `appendFile`).
9
+ */
10
+ export interface FileObserverWriter {
11
+ /** Ensure the directory exists. Called once with the parent dir of the file. */
12
+ ensureDir(dir: string): void | Promise<void>;
13
+ /** Append `content` to `path`, creating the file if absent. */
14
+ appendFile(path: string, content: string): void | Promise<void>;
15
+ }
16
+ /**
17
+ * Register handlers on `lifecycleBus` that append structured JSON lines to
18
+ * `filePath` for every lifecycle event.
19
+ *
20
+ * @param lifecycleBus - The bus to observe.
21
+ * @param filePath - Path to the JSONL output file.
22
+ * @param writer - Injected file writer (e.g. ts-runtime `FileSystem`).
23
+ */
24
+ export declare function attachFileObserver(lifecycleBus: EventBus<BusLifecycleEvents>, filePath: string, writer: FileObserverWriter): void;
25
+ //# sourceMappingURL=file-observer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-observer.d.ts","sourceRoot":"","sources":["../../src/event-bus/file-observer.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAElD;;;;;;GAMG;AACH,MAAM,WAAW,kBAAkB;IAC/B,gFAAgF;IAChF,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,+DAA+D;IAC/D,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnE;AAYD;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAC9B,YAAY,EAAE,QAAQ,CAAC,kBAAkB,CAAC,EAC1C,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,kBAAkB,GAC3B,IAAI,CAqGN"}
@@ -0,0 +1,110 @@
1
+ import { getLogger } from '../logger.js';
2
+ /** Parent directory of a `/`-separated path (no `node:path` dependency). */
3
+ function parentDir(filePath) {
4
+ const idx = filePath.lastIndexOf('/');
5
+ return idx <= 0 ? '.' : filePath.slice(0, idx);
6
+ }
7
+ function isPromiseLike(value) {
8
+ return value !== null && typeof value === 'object' && 'then' in value;
9
+ }
10
+ /**
11
+ * Register handlers on `lifecycleBus` that append structured JSON lines to
12
+ * `filePath` for every lifecycle event.
13
+ *
14
+ * @param lifecycleBus - The bus to observe.
15
+ * @param filePath - Path to the JSONL output file.
16
+ * @param writer - Injected file writer (e.g. ts-runtime `FileSystem`).
17
+ */
18
+ export function attachFileObserver(lifecycleBus, filePath, writer) {
19
+ const logger = getLogger('event-bus.file-observer');
20
+ const ensureResult = writer.ensureDir(parentDir(filePath));
21
+ let ready = !isPromiseLike(ensureResult);
22
+ let pending = isPromiseLike(ensureResult);
23
+ let setupFailed = false;
24
+ let sequence = 0;
25
+ let writeChain = isPromiseLike(ensureResult)
26
+ ? Promise.resolve(ensureResult)
27
+ .then(() => {
28
+ ready = true;
29
+ })
30
+ .catch((error) => {
31
+ setupFailed = true;
32
+ logger.warn('failed to prepare event bus file observer directory', {
33
+ filePath,
34
+ error: error instanceof Error ? error.message : String(error),
35
+ });
36
+ })
37
+ : Promise.resolve();
38
+ function setPending(next) {
39
+ const current = ++sequence;
40
+ pending = true;
41
+ writeChain = next
42
+ .catch((error) => {
43
+ logger.warn('failed to append event bus lifecycle event', {
44
+ filePath,
45
+ error: error instanceof Error ? error.message : String(error),
46
+ });
47
+ })
48
+ .finally(() => {
49
+ if (sequence === current)
50
+ pending = false;
51
+ });
52
+ }
53
+ function writeLine(line) {
54
+ const content = `${JSON.stringify(line)}\n`;
55
+ if (setupFailed)
56
+ return;
57
+ if (ready && !pending) {
58
+ const result = writer.appendFile(filePath, content);
59
+ if (isPromiseLike(result)) {
60
+ setPending(Promise.resolve(result));
61
+ }
62
+ return;
63
+ }
64
+ setPending(writeChain.then(() => {
65
+ if (setupFailed)
66
+ return;
67
+ return writer.appendFile(filePath, content);
68
+ }));
69
+ }
70
+ lifecycleBus.on('bus.emit.done', (d) => {
71
+ const payload = d.detail !== null && typeof d.detail === 'object' && !Array.isArray(d.detail)
72
+ ? d.detail
73
+ : undefined;
74
+ writeLine({
75
+ ts: new Date().toISOString(),
76
+ lifecycle: 'bus.emit.done',
77
+ event: d.event,
78
+ syncCount: d.syncCount,
79
+ asyncCount: d.asyncCount,
80
+ emitDurationMs: d.emitDurationMs,
81
+ errors: d.errors,
82
+ ...(payload && Object.keys(payload).length > 0 ? { payload } : {}),
83
+ });
84
+ });
85
+ lifecycleBus.on('bus.emit.noop', (d) => {
86
+ writeLine({
87
+ ts: new Date().toISOString(),
88
+ lifecycle: 'bus.emit.noop',
89
+ event: d.event,
90
+ });
91
+ });
92
+ lifecycleBus.on('bus.handler.error', (d) => {
93
+ writeLine({
94
+ ts: new Date().toISOString(),
95
+ lifecycle: 'bus.handler.error',
96
+ event: d.event,
97
+ mode: d.mode,
98
+ error: d.error,
99
+ });
100
+ });
101
+ lifecycleBus.on('bus.handler.async.enqueued', (d) => {
102
+ writeLine({
103
+ ts: new Date().toISOString(),
104
+ lifecycle: 'bus.handler.async.enqueued',
105
+ event: d.event,
106
+ jobId: d.jobId,
107
+ handlerCount: d.handlerCount,
108
+ });
109
+ });
110
+ }
@@ -1,3 +1,5 @@
1
+ export { attachDefaultObservers, attachLogObserver, attachTelemetryObserver, createLifecycleBus, } from './default-observers';
1
2
  export { EventBus } from './event-bus';
3
+ export { attachFileObserver, type FileObserverWriter } from './file-observer';
2
4
  export type { AsyncEnqueuedDetail, BusLifecycleEvents, EmitDoneDetail, EventMap, HandlerErrorDetail, SubscribeOptions, } from './types';
3
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/event-bus/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,YAAY,EACR,mBAAmB,EACnB,kBAAkB,EAClB,cAAc,EACd,QAAQ,EACR,kBAAkB,EAClB,gBAAgB,GACnB,MAAM,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/event-bus/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,sBAAsB,EACtB,iBAAiB,EACjB,uBAAuB,EACvB,kBAAkB,GACrB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,kBAAkB,EAAE,KAAK,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAC9E,YAAY,EACR,mBAAmB,EACnB,kBAAkB,EAClB,cAAc,EACd,QAAQ,EACR,kBAAkB,EAClB,gBAAgB,GACnB,MAAM,SAAS,CAAC"}
@@ -1 +1,3 @@
1
+ export { attachDefaultObservers, attachLogObserver, attachTelemetryObserver, createLifecycleBus, } from './default-observers.js';
1
2
  export { EventBus } from './event-bus.js';
3
+ export { attachFileObserver } from './file-observer.js';