@gobing-ai/ts-infra 0.3.0 → 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.
- package/README.md +52 -29
- package/dist/api-client.d.ts +13 -0
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js +15 -4
- package/dist/event-bus/default-observers.d.ts +53 -0
- package/dist/event-bus/default-observers.d.ts.map +1 -0
- package/dist/event-bus/default-observers.js +107 -0
- package/dist/event-bus/event-bus.d.ts.map +1 -1
- package/dist/event-bus/event-bus.js +1 -0
- package/dist/event-bus/file-observer.d.ts +25 -0
- package/dist/event-bus/file-observer.d.ts.map +1 -0
- package/dist/event-bus/file-observer.js +110 -0
- package/dist/event-bus/index.d.ts +2 -0
- package/dist/event-bus/index.d.ts.map +1 -1
- package/dist/event-bus/index.js +2 -0
- package/dist/event-bus/types.d.ts +6 -0
- package/dist/event-bus/types.d.ts.map +1 -1
- package/dist/events.d.ts +100 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +12 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -5
- package/dist/job-queue/db-job-queue.d.ts.map +1 -1
- package/dist/job-queue/db-job-queue.js +45 -34
- package/dist/job-queue/index.d.ts +0 -1
- package/dist/job-queue/index.d.ts.map +1 -1
- package/dist/job-queue/index.js +0 -1
- package/dist/job-queue/types.d.ts +7 -0
- package/dist/job-queue/types.d.ts.map +1 -1
- package/dist/job-queue-db.d.ts +2 -0
- package/dist/job-queue-db.d.ts.map +1 -0
- package/dist/job-queue-db.js +1 -0
- package/dist/logger.d.ts +39 -7
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +76 -73
- package/dist/scheduler/action.d.ts +97 -1
- package/dist/scheduler/action.d.ts.map +1 -1
- package/dist/scheduler/action.js +111 -0
- package/dist/scheduler/cloudflare.d.ts +6 -0
- package/dist/scheduler/cloudflare.d.ts.map +1 -1
- package/dist/scheduler/cloudflare.js +6 -0
- package/dist/scheduler/factory.d.ts +2 -0
- package/dist/scheduler/factory.d.ts.map +1 -1
- package/dist/scheduler/factory.js +2 -0
- package/dist/scheduler/index.d.ts +2 -2
- package/dist/scheduler/index.d.ts.map +1 -1
- package/dist/scheduler/index.js +2 -2
- package/dist/scheduler/node.d.ts +4 -0
- package/dist/scheduler/node.d.ts.map +1 -1
- package/dist/scheduler/node.js +4 -0
- package/dist/scheduler/noop.d.ts +1 -0
- package/dist/scheduler/noop.d.ts.map +1 -1
- package/dist/scheduler/noop.js +1 -0
- package/dist/scheduler/wrap-handler.d.ts +18 -0
- package/dist/scheduler/wrap-handler.d.ts.map +1 -0
- package/dist/scheduler/wrap-handler.js +41 -0
- package/dist/scheduler-cloudflare.d.ts +2 -0
- package/dist/scheduler-cloudflare.d.ts.map +1 -0
- package/dist/scheduler-cloudflare.js +1 -0
- package/dist/scheduler-node.d.ts +2 -0
- package/dist/scheduler-node.d.ts.map +1 -0
- package/dist/scheduler-node.js +1 -0
- package/dist/telemetry/config.d.ts +4 -0
- package/dist/telemetry/config.d.ts.map +1 -1
- package/dist/telemetry/metrics.d.ts +19 -0
- package/dist/telemetry/metrics.d.ts.map +1 -1
- package/dist/telemetry/metrics.js +19 -0
- package/dist/telemetry/sdk.d.ts +4 -0
- package/dist/telemetry/sdk.d.ts.map +1 -1
- package/dist/telemetry/sdk.js +4 -0
- package/dist/telemetry/tracing.d.ts +12 -0
- package/dist/telemetry/tracing.d.ts.map +1 -1
- package/dist/telemetry/tracing.js +12 -0
- package/package.json +19 -2
- package/src/api-client.ts +15 -4
- package/src/event-bus/default-observers.ts +117 -0
- package/src/event-bus/event-bus.ts +1 -0
- package/src/event-bus/file-observer.ts +142 -0
- package/src/event-bus/index.ts +7 -0
- package/src/event-bus/types.ts +6 -0
- package/src/events.ts +108 -0
- package/src/index.ts +44 -7
- package/src/job-queue/db-job-queue.ts +50 -38
- package/src/job-queue/index.ts +0 -1
- package/src/job-queue/types.ts +7 -0
- package/src/job-queue-db.ts +1 -0
- package/src/logger.ts +102 -77
- package/src/scheduler/action.ts +164 -1
- package/src/scheduler/cloudflare.ts +6 -0
- package/src/scheduler/factory.ts +2 -0
- package/src/scheduler/index.ts +13 -2
- package/src/scheduler/node.ts +4 -0
- package/src/scheduler/noop.ts +1 -0
- package/src/scheduler/wrap-handler.ts +50 -0
- package/src/scheduler-cloudflare.ts +1 -0
- package/src/scheduler-node.ts +1 -0
- package/src/telemetry/config.ts +4 -0
- package/src/telemetry/metrics.ts +19 -0
- package/src/telemetry/sdk.ts +4 -0
- 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,
|
|
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** |
|
|
13
|
-
| **Scheduler** |
|
|
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` |
|
|
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(
|
|
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 -->
|
|
150
|
-
SchedulerFactory -->
|
|
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
|
-
|
|
197
|
-
|
|
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
|
-
|
|
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 {
|
|
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(
|
|
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
|
-
//
|
|
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.
|
|
401
|
-
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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 =
|
|
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
|
});
|
package/dist/api-client.d.ts
CHANGED
|
@@ -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;
|
package/dist/api-client.d.ts.map
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/api-client.js
CHANGED
|
@@ -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;
|
|
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"}
|
|
@@ -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"}
|
package/dist/event-bus/index.js
CHANGED