@gobing-ai/ts-infra 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.
Files changed (99) hide show
  1. package/README.md +389 -0
  2. package/dist/api-client.d.ts +31 -0
  3. package/dist/api-client.d.ts.map +1 -0
  4. package/dist/api-client.js +112 -0
  5. package/dist/event-bus/event-bus.d.ts +33 -0
  6. package/dist/event-bus/event-bus.d.ts.map +1 -0
  7. package/dist/event-bus/event-bus.js +211 -0
  8. package/dist/event-bus/index.d.ts +3 -0
  9. package/dist/event-bus/index.d.ts.map +1 -0
  10. package/dist/event-bus/index.js +1 -0
  11. package/dist/event-bus/types.d.ts +35 -0
  12. package/dist/event-bus/types.d.ts.map +1 -0
  13. package/dist/event-bus/types.js +3 -0
  14. package/dist/events/app-events.d.ts +7 -0
  15. package/dist/events/app-events.d.ts.map +1 -0
  16. package/dist/events/app-events.js +4 -0
  17. package/dist/events/create-system-bus.d.ts +6 -0
  18. package/dist/events/create-system-bus.d.ts.map +1 -0
  19. package/dist/events/create-system-bus.js +7 -0
  20. package/dist/events/index.d.ts +3 -0
  21. package/dist/events/index.d.ts.map +1 -0
  22. package/dist/events/index.js +1 -0
  23. package/dist/index.d.ts +9 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +11 -0
  26. package/dist/index.js.map +9 -0
  27. package/dist/job-queue/index.d.ts +2 -0
  28. package/dist/job-queue/index.d.ts.map +1 -0
  29. package/dist/job-queue/index.js +0 -0
  30. package/dist/job-queue/types.d.ts +57 -0
  31. package/dist/job-queue/types.d.ts.map +1 -0
  32. package/dist/job-queue/types.js +8 -0
  33. package/dist/logger.d.ts +28 -0
  34. package/dist/logger.d.ts.map +1 -0
  35. package/dist/logger.js +100 -0
  36. package/dist/scheduler/action.d.ts +5 -0
  37. package/dist/scheduler/action.d.ts.map +1 -0
  38. package/dist/scheduler/action.js +0 -0
  39. package/dist/scheduler/cloudflare.d.ts +27 -0
  40. package/dist/scheduler/cloudflare.d.ts.map +1 -0
  41. package/dist/scheduler/cloudflare.js +24 -0
  42. package/dist/scheduler/factory.d.ts +19 -0
  43. package/dist/scheduler/factory.d.ts.map +1 -0
  44. package/dist/scheduler/factory.js +45 -0
  45. package/dist/scheduler/index.d.ts +6 -0
  46. package/dist/scheduler/index.d.ts.map +1 -0
  47. package/dist/scheduler/index.js +4 -0
  48. package/dist/scheduler/node.d.ts +16 -0
  49. package/dist/scheduler/node.d.ts.map +1 -0
  50. package/dist/scheduler/node.js +63 -0
  51. package/dist/scheduler/noop.d.ts +11 -0
  52. package/dist/scheduler/noop.d.ts.map +1 -0
  53. package/dist/scheduler/noop.js +12 -0
  54. package/dist/scheduler/types.d.ts +12 -0
  55. package/dist/scheduler/types.d.ts.map +1 -0
  56. package/dist/scheduler/types.js +3 -0
  57. package/dist/telemetry/config.d.ts +42 -0
  58. package/dist/telemetry/config.d.ts.map +1 -0
  59. package/dist/telemetry/config.js +24 -0
  60. package/dist/telemetry/db-sanitize.d.ts +15 -0
  61. package/dist/telemetry/db-sanitize.d.ts.map +1 -0
  62. package/dist/telemetry/db-sanitize.js +72 -0
  63. package/dist/telemetry/index.d.ts +7 -0
  64. package/dist/telemetry/index.d.ts.map +1 -0
  65. package/dist/telemetry/index.js +5 -0
  66. package/dist/telemetry/metrics.d.ts +32 -0
  67. package/dist/telemetry/metrics.d.ts.map +1 -0
  68. package/dist/telemetry/metrics.js +109 -0
  69. package/dist/telemetry/sdk.d.ts +13 -0
  70. package/dist/telemetry/sdk.d.ts.map +1 -0
  71. package/dist/telemetry/sdk.js +54 -0
  72. package/dist/telemetry/tracing.d.ts +13 -0
  73. package/dist/telemetry/tracing.d.ts.map +1 -0
  74. package/dist/telemetry/tracing.js +54 -0
  75. package/package.json +50 -0
  76. package/src/api-client.ts +162 -0
  77. package/src/event-bus/event-bus.ts +236 -0
  78. package/src/event-bus/index.ts +9 -0
  79. package/src/event-bus/types.ts +40 -0
  80. package/src/events/app-events.ts +8 -0
  81. package/src/events/create-system-bus.ts +8 -0
  82. package/src/events/index.ts +2 -0
  83. package/src/index.ts +74 -0
  84. package/src/job-queue/index.ts +9 -0
  85. package/src/job-queue/types.ts +60 -0
  86. package/src/logger.ts +123 -0
  87. package/src/scheduler/action.ts +4 -0
  88. package/src/scheduler/cloudflare.ts +45 -0
  89. package/src/scheduler/factory.ts +57 -0
  90. package/src/scheduler/index.ts +5 -0
  91. package/src/scheduler/node.ts +83 -0
  92. package/src/scheduler/noop.ts +20 -0
  93. package/src/scheduler/types.ts +13 -0
  94. package/src/telemetry/config.ts +63 -0
  95. package/src/telemetry/db-sanitize.ts +79 -0
  96. package/src/telemetry/index.ts +29 -0
  97. package/src/telemetry/metrics.ts +150 -0
  98. package/src/telemetry/sdk.ts +65 -0
  99. package/src/telemetry/tracing.ts +64 -0
package/README.md ADDED
@@ -0,0 +1,389 @@
1
+ # @gobing-ai/ts-infra
2
+
3
+ Infrastructure backbone — typed event bus, job queue (types), cron scheduler, OpenTelemetry telemetry, HTTP API client, and structured logging. Designed to be wired together at bootstrap via `RuntimeContext`.
4
+
5
+ ## Overview
6
+
7
+ `ts-infra` provides seven subsystems that form the application backbone:
8
+
9
+ | Subsystem | Module | Purpose |
10
+ |-----------|--------|---------|
11
+ | **Event Bus** | `event-bus/` | Typed pub/sub with sync + async dispatch, lifecycle self-observability |
12
+ | **Events** | `events/` | Typed event map pattern + system bus factory |
13
+ | **Job Queue** | `job-queue/` | Types for DB-backed job processing (`JobQueue`, `QueueConsumer`, `Job`) |
14
+ | **Scheduler** | `scheduler/` | Cron-like scheduled actions — Node (interval), Cloudflare (Cron Triggers), Noop (test) |
15
+ | **Telemetry** | `telemetry/` | OpenTelemetry SDK wrapper — tracing (`traceAsync`), metrics (17 instruments), SQL sanitizer |
16
+ | **API Client** | `api-client.ts` | Typed HTTP client with OTel tracing, retry, timeout, error handling |
17
+ | **Logger** | `logger.ts` | Structured JSON logger with levels, child loggers, and mute toggle |
18
+
19
+ ## Architecture
20
+
21
+ ```mermaid
22
+ classDiagram
23
+ namespace event-bus {
24
+ class EventBus~TEvents~ {
25
+ +on(event, handler, opts?) void
26
+ +once(event, handler, opts?) void
27
+ +off(event, handler) void
28
+ +emit(event, ...args) Promise~void~
29
+ +removeAllListeners(event?) void
30
+ +listenerCount(event) number
31
+ +eventNames() string[]
32
+ }
33
+ }
34
+
35
+ namespace events {
36
+ class EventFactory {
37
+ +createSystemBus() EventBus
38
+ }
39
+ }
40
+
41
+ namespace job-queue {
42
+ class JobQueue~T~ {
43
+ <<interface>>
44
+ +enqueue(type, payload, opts?) Promise~string~
45
+ +enqueueBatch(jobs) Promise~string[]~
46
+ }
47
+ class QueueConsumer~T~ {
48
+ <<interface>>
49
+ +register(type, handler) void
50
+ +start() Promise~void~
51
+ +stop() Promise~void~
52
+ +stats() Promise~QueueStats~
53
+ }
54
+ class Job~T~ {
55
+ <<interface>>
56
+ }
57
+ class QueueStats {
58
+ <<interface>>
59
+ }
60
+ }
61
+
62
+ namespace scheduler {
63
+ class SchedulerAdapter {
64
+ <<interface>>
65
+ +register(cron, action) void
66
+ +start() Promise~void~
67
+ +stop() Promise~void~
68
+ }
69
+ class NodeSchedulerAdapter {
70
+ }
71
+ class CloudflareSchedulerAdapter {
72
+ }
73
+ class NoopSchedulerAdapter {
74
+ }
75
+ class SchedulerFactory {
76
+ +initScheduler(cronEntries?) SchedulerAdapter
77
+ +setSchedulerAdapter(adapter) void
78
+ +getSchedulerAdapter() SchedulerAdapter
79
+ }
80
+ }
81
+
82
+ namespace telemetry {
83
+ class TelemetrySDK {
84
+ +initTelemetry(config?) void
85
+ +shutdownTelemetry() Promise~void~
86
+ +getTracer() Tracer
87
+ }
88
+ class Tracing {
89
+ +traceAsync~T~(name, fn) Promise~T~
90
+ +traceSync~T~(name, fn) T
91
+ +addSpanAttributes(attrs) void
92
+ +getActiveSpan() Span
93
+ }
94
+ class Metrics {
95
+ +getHttpServerRequestTotal() Counter
96
+ +getHttpServerRequestDuration() Histogram
97
+ +getDbOperationTotal() Counter
98
+ +getQueueJobEnqueuedTotal() Counter
99
+ +getSchedulerJobExecutedTotal() Counter
100
+ }
101
+ class DbSanitize {
102
+ +sanitizeSql(sql) string
103
+ +extractSqlOperation(sql) string
104
+ }
105
+ }
106
+
107
+ namespace api-client {
108
+ class APIClient {
109
+ +get~T~(path, opts?) Promise~T~
110
+ +post~T~(path, body?, opts?) Promise~T~
111
+ +put~T~(path, body?, opts?) Promise~T~
112
+ +delete~T~(path, opts?) Promise~T~
113
+ }
114
+ class APIError {
115
+ +number status
116
+ +string body
117
+ }
118
+ }
119
+
120
+ namespace logger {
121
+ class Logger {
122
+ <<interface>>
123
+ +trace(msg, data?) void
124
+ +debug(msg, data?) void
125
+ +info(msg, data?) void
126
+ +warn(msg, data?) void
127
+ +error(msg, data?) void
128
+ +fatal(msg, data?) void
129
+ +child(context) Logger
130
+ }
131
+ class LoggerFactory {
132
+ +getLogger(category) Logger
133
+ +initializeLogger(level) void
134
+ }
135
+ }
136
+
137
+ SchedulerAdapter <|.. NodeSchedulerAdapter : implements
138
+ SchedulerAdapter <|.. CloudflareSchedulerAdapter : implements
139
+ SchedulerAdapter <|.. NoopSchedulerAdapter : implements
140
+ SchedulerFactory --> NodeSchedulerAdapter
141
+ SchedulerFactory --> CloudflareSchedulerAdapter
142
+ SchedulerFactory --> NoopSchedulerAdapter
143
+ EventBus --> JobQueue : "async dispatch"
144
+ EventBus --> Logger : "self-observability"
145
+ APIClient --> Tracing : "traceAsync"
146
+ APIClient --> Metrics : "counters + histograms"
147
+ ```
148
+
149
+ ## How It Works
150
+
151
+ ### Event Bus — typed pub/sub
152
+
153
+ ```ts
154
+ import { EventBus, type EventMap } from '@gobing-ai/ts-infra';
155
+
156
+ // Define your event map
157
+ type AppEvents = {
158
+ 'user.signed_up': (email: string, plan: string) => void;
159
+ 'order.placed': (orderId: string, total: number) => void;
160
+ };
161
+
162
+ const bus = new EventBus<AppEvents>();
163
+
164
+ // Sync handler (runs in-process immediately)
165
+ bus.on('user.signed_up', (email, plan) => {
166
+ console.log(`Welcome ${email} on ${plan} plan!`);
167
+ });
168
+
169
+ // Async handler (runs through the async handler path)
170
+ bus.on('order.placed', (orderId) => {
171
+ console.log(`Order placed: ${orderId}`);
172
+ }, { async: true });
173
+
174
+ // Emit
175
+ await bus.emit('user.signed_up', 'alice@test.com', 'pro');
176
+ // → "Welcome alice@test.com on pro plan!"
177
+
178
+ // Once (auto-removes after first emit)
179
+ bus.once('user.signed_up', () => console.log('one-time'));
180
+ ```
181
+
182
+ **Lifecycle events** — inject a second `EventBus` to observe bus internals:
183
+
184
+ ```ts
185
+ const lifecycleBus = new EventBus<BusLifecycleEvents>();
186
+ lifecycleBus.on('bus.emit.done', (detail) => {
187
+ // { event, syncCount, asyncCount, emitDurationMs, errors }
188
+ metrics.recordEmit(detail);
189
+ });
190
+
191
+ const bus = new EventBus<AppEvents>({ lifecycleBus });
192
+ ```
193
+
194
+ ### Scheduler — cron-like actions
195
+
196
+ ```ts
197
+ import { NodeSchedulerAdapter, initScheduler } from '@gobing-ai/ts-infra';
198
+
199
+ // Node.js (interval-based)
200
+ const scheduler = new NodeSchedulerAdapter();
201
+ scheduler.register('60000', async () => {
202
+ console.log('Runs every 60 seconds');
203
+ });
204
+ scheduler.register('*/5 * * * *', async () => {
205
+ console.log('Runs every 5 minutes');
206
+ });
207
+ await scheduler.start();
208
+
209
+ // Or use factory
210
+ const sched = initScheduler([
211
+ ['300000', async () => cleanupExpiredSessions()],
212
+ ['3600000', async () => generateReports()],
213
+ ]);
214
+ await sched.start();
215
+
216
+ // Cloudflare Workers
217
+ import { CloudflareSchedulerAdapter } from '@gobing-ai/ts-infra';
218
+ const cfScheduler = new CloudflareSchedulerAdapter();
219
+ cfScheduler.register('* * * * *', async () => { /* ... */ });
220
+
221
+ export default {
222
+ async scheduled(event, env, ctx) {
223
+ cfScheduler.handleScheduledEvent(event, ctx);
224
+ },
225
+ };
226
+ ```
227
+
228
+ ### API Client — typed HTTP with tracing
229
+
230
+ ```ts
231
+ import { APIClient, APIError } from '@gobing-ai/ts-infra';
232
+
233
+ const api = new APIClient({
234
+ baseUrl: 'https://api.example.com',
235
+ defaultHeaders: { Authorization: `Bearer ${token}` },
236
+ timeout: 10_000,
237
+ });
238
+
239
+ // Typed response — spans are auto-created
240
+ const user = await api.get<{ id: string; name: string }>('/users/me');
241
+
242
+ try {
243
+ await api.post('/orders', { productId: 'p1', quantity: 2 });
244
+ } catch (error) {
245
+ if (error instanceof APIError) {
246
+ console.error(`HTTP ${error.status}: ${error.body}`);
247
+ }
248
+ }
249
+
250
+ // Custom operation name for tracing
251
+ const items = await api.get<Item[]>('/items', { operationName: 'inventory.list' });
252
+ ```
253
+
254
+ The client auto-instruments every request: creates a `CLIENT` span, records method/URL/status attributes, emits request count + duration metrics, and records errors.
255
+
256
+ ### Logger — structured JSON
257
+
258
+ ```ts
259
+ import { getLogger, initializeLogger } from '@gobing-ai/ts-infra';
260
+
261
+ initializeLogger('debug'); // set minimum level
262
+
263
+ const log = getLogger('auth');
264
+ log.info('User logged in', { userId: 'u1', method: 'password' });
265
+ // → {"level":"info","message":"User logged in",...,"category":"auth","userId":"u1"}
266
+
267
+ // Child loggers carry context
268
+ const reqLog = log.child({ requestId: 'req-123' });
269
+ reqLog.error('Validation failed', { field: 'email' });
270
+ // → {...,"category":"auth","requestId":"req-123","field":"email"}
271
+
272
+ // Mute during tests
273
+ import { setLoggerMuted } from './logger.js'; // internal import
274
+ setLoggerMuted(true);
275
+ ```
276
+
277
+ ### Telemetry — OpenTelemetry
278
+
279
+ ```ts
280
+ import {
281
+ initTelemetry, shutdownTelemetry,
282
+ traceAsync, addSpanAttributes, getActiveSpan,
283
+ sanitizeSql,
284
+ } from '@gobing-ai/ts-infra';
285
+
286
+ // Initialize at startup
287
+ initTelemetry({
288
+ enabled: true,
289
+ serviceName: 'my-api',
290
+ environment: 'production',
291
+ exporterEndpoint: 'http://otel-collector:4318/v1/traces',
292
+ });
293
+
294
+ // Trace an operation
295
+ const result = await traceAsync('db.query', async (span) => {
296
+ addSpanAttributes({ 'db.system': 'sqlite', 'db.operation': 'SELECT' });
297
+ return db.select().from(users);
298
+ });
299
+
300
+ // Sanitize SQL before export
301
+ const safe = sanitizeSql("SELECT * FROM users WHERE email = 'alice@test.com'");
302
+ // → "SELECT * FROM users WHERE email = ?"
303
+
304
+ // Shutdown gracefully
305
+ process.on('SIGTERM', async () => {
306
+ await shutdownTelemetry();
307
+ });
308
+ ```
309
+
310
+ Metrics are lazy-initialized — no configuration needed beyond `initTelemetry()`:
311
+
312
+ ```ts
313
+ import { getQueueJobEnqueuedTotal, getQueueJobProcessingDuration } from '@gobing-ai/ts-infra';
314
+
315
+ getQueueJobEnqueuedTotal().add(1, { 'queue.job_type': 'send-email' });
316
+ getQueueJobProcessingDuration().record(42, { 'queue.job_type': 'send-email' });
317
+ ```
318
+
319
+ ## Usage
320
+
321
+ ### Install
322
+
323
+ ```bash
324
+ bun add @gobing-ai/ts-infra @gobing-ai/ts-runtime @gobing-ai/ts-db
325
+ ```
326
+
327
+ ### Full bootstrap example
328
+
329
+ ```ts
330
+ import { createRuntimeContext } from '@gobing-ai/ts-runtime';
331
+ import { createDbAdapter, applyMigrations } from '@gobing-ai/ts-db';
332
+ import {
333
+ EventBus,
334
+ NodeSchedulerAdapter,
335
+ initTelemetry,
336
+ APIClient,
337
+ getLogger,
338
+ initializeLogger,
339
+ } from '@gobing-ai/ts-infra';
340
+
341
+ // 1. Runtime
342
+ const ctx = createRuntimeContext({ runtimeName: 'node-bun' });
343
+
344
+ // 2. Database
345
+ const db = await createDbAdapter({ driver: 'bun-sqlite', url: './data/app.db' });
346
+ await applyMigrations(db);
347
+ ctx.register('db', db);
348
+
349
+ // 3. Logging
350
+ initializeLogger('info');
351
+ const log = getLogger('app');
352
+
353
+ // 4. Telemetry
354
+ initTelemetry({ serviceName: 'my-app', environment: 'production' });
355
+
356
+ // 5. Event bus
357
+ const bus = new EventBus<AppEvents>();
358
+
359
+ // 6. Scheduler
360
+ const scheduler = new NodeSchedulerAdapter();
361
+ scheduler.register('3600000', async () => {
362
+ log.info('Hourly cleanup running');
363
+ });
364
+ await scheduler.start();
365
+
366
+ // 7. API client
367
+ const stripeApi = new APIClient({
368
+ baseUrl: 'https://api.stripe.com/v1',
369
+ defaultHeaders: { Authorization: `Bearer ${process.env.STRIPE_KEY}` },
370
+ });
371
+
372
+ log.info('Application started');
373
+ ```
374
+
375
+ ### Graceful shutdown
376
+
377
+ ```ts
378
+ async function shutdown() {
379
+ log.info('Shutting down...');
380
+ await scheduler.stop();
381
+ db.close();
382
+ await shutdownTelemetry();
383
+ await ctx.dispose();
384
+ process.exit(0);
385
+ }
386
+
387
+ process.on('SIGTERM', shutdown);
388
+ process.on('SIGINT', shutdown);
389
+ ```
@@ -0,0 +1,31 @@
1
+ export interface APIClientConfig {
2
+ baseUrl: string;
3
+ defaultHeaders?: Record<string, string>;
4
+ timeout?: number;
5
+ fetch?: typeof globalThis.fetch;
6
+ }
7
+ export interface RequestOptions {
8
+ headers?: Record<string, string>;
9
+ timeout?: number;
10
+ operationName?: string;
11
+ signal?: AbortSignal;
12
+ }
13
+ export declare class APIError extends Error {
14
+ readonly status: number;
15
+ readonly body: string;
16
+ constructor(status: number, body: string);
17
+ }
18
+ export declare class APIClient {
19
+ private readonly baseUrl;
20
+ private readonly defaultHeaders;
21
+ private readonly timeout;
22
+ private readonly fetchFn;
23
+ constructor(config: APIClientConfig);
24
+ private buildUrl;
25
+ private request;
26
+ get<T>(path: string, opts?: RequestOptions): Promise<T>;
27
+ post<T>(path: string, body?: unknown, opts?: RequestOptions): Promise<T>;
28
+ put<T>(path: string, body?: unknown, opts?: RequestOptions): Promise<T>;
29
+ delete<T>(path: string, opts?: RequestOptions): Promise<T>;
30
+ }
31
+ //# sourceMappingURL=api-client.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Typed HTTP client builder wrapping fetch with OTel tracing.
3
+ */
4
+ import { SpanKind } from '@opentelemetry/api';
5
+ import { ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_RESPONSE_STATUS_CODE, ATTR_URL_FULL, } from '@opentelemetry/semantic-conventions';
6
+ import { getHttpClientRequestDuration, getHttpClientRequestErrors, getHttpClientRequestTotal, } from './telemetry/metrics.js';
7
+ import { traceAsync } from './telemetry/tracing.js';
8
+ export class APIError extends Error {
9
+ status;
10
+ body;
11
+ constructor(status, body) {
12
+ super(`HTTP ${status}: ${body.slice(0, 200)}`);
13
+ this.status = status;
14
+ this.body = body;
15
+ this.name = 'APIError';
16
+ }
17
+ }
18
+ // ── Client ──────────────────────────────────────────────────────────
19
+ export class APIClient {
20
+ baseUrl;
21
+ defaultHeaders;
22
+ timeout;
23
+ fetchFn;
24
+ constructor(config) {
25
+ this.baseUrl = config.baseUrl.replace(/\/+$/, '');
26
+ this.defaultHeaders = config.defaultHeaders ?? {};
27
+ this.timeout = config.timeout ?? 30_000;
28
+ this.fetchFn = config.fetch ?? globalThis.fetch;
29
+ }
30
+ buildUrl(path) {
31
+ return `${this.baseUrl}${path.startsWith('/') ? path : `/${path}`}`;
32
+ }
33
+ async request(method, path, body, opts) {
34
+ const url = this.buildUrl(path);
35
+ const headers = {
36
+ 'Content-Type': 'application/json',
37
+ ...this.defaultHeaders,
38
+ ...opts?.headers,
39
+ };
40
+ const operationName = opts?.operationName ?? `HTTP ${method} ${url}`;
41
+ return traceAsync(operationName, async (span) => {
42
+ span.setAttribute(ATTR_HTTP_REQUEST_METHOD, method);
43
+ span.setAttribute(ATTR_URL_FULL, url);
44
+ const controller = new AbortController();
45
+ const timeoutMs = opts?.timeout ?? this.timeout;
46
+ let timer;
47
+ if (timeoutMs > 0) {
48
+ timer = setTimeout(() => controller.abort(), timeoutMs);
49
+ }
50
+ const combinedSignal = opts?.signal
51
+ ? AbortSignal.any([opts.signal, controller.signal])
52
+ : controller.signal;
53
+ try {
54
+ const start = performance.now();
55
+ const response = await this.fetchFn(url, {
56
+ method,
57
+ headers,
58
+ body: body !== undefined ? JSON.stringify(body) : undefined,
59
+ signal: combinedSignal,
60
+ });
61
+ if (timer)
62
+ clearTimeout(timer);
63
+ span.setAttribute(ATTR_HTTP_RESPONSE_STATUS_CODE, response.status);
64
+ getHttpClientRequestTotal().add(1, {
65
+ 'http.request.method': method,
66
+ 'http.response.status_code': response.status,
67
+ });
68
+ const duration = performance.now() - start;
69
+ getHttpClientRequestDuration().record(duration, {
70
+ 'http.request.method': method,
71
+ 'http.response.status_code': response.status,
72
+ });
73
+ if (!response.ok) {
74
+ const text = await response.text();
75
+ getHttpClientRequestErrors().add(1, {
76
+ 'http.request.method': method,
77
+ 'error.type': `HTTP_${response.status}`,
78
+ });
79
+ throw new APIError(response.status, text);
80
+ }
81
+ const contentType = response.headers.get('content-type') ?? '';
82
+ if (contentType.includes('application/json')) {
83
+ return (await response.json());
84
+ }
85
+ return (await response.text());
86
+ }
87
+ catch (error) {
88
+ if (timer)
89
+ clearTimeout(timer);
90
+ if (!(error instanceof APIError)) {
91
+ getHttpClientRequestErrors().add(1, {
92
+ 'http.request.method': method,
93
+ 'error.type': error instanceof Error ? error.name : 'Unknown',
94
+ });
95
+ }
96
+ throw error;
97
+ }
98
+ }, { kind: SpanKind.CLIENT });
99
+ }
100
+ async get(path, opts) {
101
+ return this.request('GET', path, undefined, opts);
102
+ }
103
+ async post(path, body, opts) {
104
+ return this.request('POST', path, body, opts);
105
+ }
106
+ async put(path, body, opts) {
107
+ return this.request('PUT', path, body, opts);
108
+ }
109
+ async delete(path, opts) {
110
+ return this.request('DELETE', path, undefined, opts);
111
+ }
112
+ }
@@ -0,0 +1,33 @@
1
+ import type { JobQueue } from '../job-queue/types';
2
+ import type { BusLifecycleEvents, EventMap, SubscribeOptions } from './types';
3
+ /**
4
+ * Type-safe event bus supporting both synchronous (in-process) and
5
+ * asynchronous handlers with optional job-queue enqueue instrumentation.
6
+ */
7
+ export declare class EventBus<TEvents extends EventMap> {
8
+ private readonly syncHandlers;
9
+ private readonly asyncHandlers;
10
+ private readonly asyncHandlerIds;
11
+ private readonly jobQueue;
12
+ private readonly lifecycleBus;
13
+ private nextAsyncHandlerId;
14
+ constructor(opts?: {
15
+ jobQueue?: JobQueue;
16
+ lifecycleBus?: EventBus<BusLifecycleEvents>;
17
+ });
18
+ on<K extends keyof TEvents>(event: K, handler: TEvents[K], opts?: SubscribeOptions): void;
19
+ once<K extends keyof TEvents>(event: K, handler: TEvents[K], opts?: SubscribeOptions): void;
20
+ off<K extends keyof TEvents>(event: K, handler: TEvents[K]): void;
21
+ removeAllListeners<K extends keyof TEvents>(event?: K): void;
22
+ emit<K extends keyof TEvents>(event: K, ...args: Parameters<TEvents[K]>): Promise<void>;
23
+ listenerCount<K extends keyof TEvents>(event: K, mode?: 'sync' | 'async'): number;
24
+ eventNames(): string[];
25
+ private registerSync;
26
+ private registerAsync;
27
+ private getAsyncHandlerId;
28
+ private publishEmitDone;
29
+ private publishEmitNoop;
30
+ private publishHandlerError;
31
+ private publishAsyncEnqueued;
32
+ }
33
+ //# sourceMappingURL=event-bus.d.ts.map
@@ -0,0 +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;AAEnD,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;IAwE7F,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"}