@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.
- package/README.md +389 -0
- package/dist/api-client.d.ts +31 -0
- package/dist/api-client.d.ts.map +1 -0
- package/dist/api-client.js +112 -0
- package/dist/event-bus/event-bus.d.ts +33 -0
- package/dist/event-bus/event-bus.d.ts.map +1 -0
- package/dist/event-bus/event-bus.js +211 -0
- package/dist/event-bus/index.d.ts +3 -0
- package/dist/event-bus/index.d.ts.map +1 -0
- package/dist/event-bus/index.js +1 -0
- package/dist/event-bus/types.d.ts +35 -0
- package/dist/event-bus/types.d.ts.map +1 -0
- package/dist/event-bus/types.js +3 -0
- package/dist/events/app-events.d.ts +7 -0
- package/dist/events/app-events.d.ts.map +1 -0
- package/dist/events/app-events.js +4 -0
- package/dist/events/create-system-bus.d.ts +6 -0
- package/dist/events/create-system-bus.d.ts.map +1 -0
- package/dist/events/create-system-bus.js +7 -0
- package/dist/events/index.d.ts +3 -0
- package/dist/events/index.d.ts.map +1 -0
- package/dist/events/index.js +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +9 -0
- package/dist/job-queue/index.d.ts +2 -0
- package/dist/job-queue/index.d.ts.map +1 -0
- package/dist/job-queue/index.js +0 -0
- package/dist/job-queue/types.d.ts +57 -0
- package/dist/job-queue/types.d.ts.map +1 -0
- package/dist/job-queue/types.js +8 -0
- package/dist/logger.d.ts +28 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +100 -0
- package/dist/scheduler/action.d.ts +5 -0
- package/dist/scheduler/action.d.ts.map +1 -0
- package/dist/scheduler/action.js +0 -0
- package/dist/scheduler/cloudflare.d.ts +27 -0
- package/dist/scheduler/cloudflare.d.ts.map +1 -0
- package/dist/scheduler/cloudflare.js +24 -0
- package/dist/scheduler/factory.d.ts +19 -0
- package/dist/scheduler/factory.d.ts.map +1 -0
- package/dist/scheduler/factory.js +45 -0
- package/dist/scheduler/index.d.ts +6 -0
- package/dist/scheduler/index.d.ts.map +1 -0
- package/dist/scheduler/index.js +4 -0
- package/dist/scheduler/node.d.ts +16 -0
- package/dist/scheduler/node.d.ts.map +1 -0
- package/dist/scheduler/node.js +63 -0
- package/dist/scheduler/noop.d.ts +11 -0
- package/dist/scheduler/noop.d.ts.map +1 -0
- package/dist/scheduler/noop.js +12 -0
- package/dist/scheduler/types.d.ts +12 -0
- package/dist/scheduler/types.d.ts.map +1 -0
- package/dist/scheduler/types.js +3 -0
- package/dist/telemetry/config.d.ts +42 -0
- package/dist/telemetry/config.d.ts.map +1 -0
- package/dist/telemetry/config.js +24 -0
- package/dist/telemetry/db-sanitize.d.ts +15 -0
- package/dist/telemetry/db-sanitize.d.ts.map +1 -0
- package/dist/telemetry/db-sanitize.js +72 -0
- package/dist/telemetry/index.d.ts +7 -0
- package/dist/telemetry/index.d.ts.map +1 -0
- package/dist/telemetry/index.js +5 -0
- package/dist/telemetry/metrics.d.ts +32 -0
- package/dist/telemetry/metrics.d.ts.map +1 -0
- package/dist/telemetry/metrics.js +109 -0
- package/dist/telemetry/sdk.d.ts +13 -0
- package/dist/telemetry/sdk.d.ts.map +1 -0
- package/dist/telemetry/sdk.js +54 -0
- package/dist/telemetry/tracing.d.ts +13 -0
- package/dist/telemetry/tracing.d.ts.map +1 -0
- package/dist/telemetry/tracing.js +54 -0
- package/package.json +50 -0
- package/src/api-client.ts +162 -0
- package/src/event-bus/event-bus.ts +236 -0
- package/src/event-bus/index.ts +9 -0
- package/src/event-bus/types.ts +40 -0
- package/src/events/app-events.ts +8 -0
- package/src/events/create-system-bus.ts +8 -0
- package/src/events/index.ts +2 -0
- package/src/index.ts +74 -0
- package/src/job-queue/index.ts +9 -0
- package/src/job-queue/types.ts +60 -0
- package/src/logger.ts +123 -0
- package/src/scheduler/action.ts +4 -0
- package/src/scheduler/cloudflare.ts +45 -0
- package/src/scheduler/factory.ts +57 -0
- package/src/scheduler/index.ts +5 -0
- package/src/scheduler/node.ts +83 -0
- package/src/scheduler/noop.ts +20 -0
- package/src/scheduler/types.ts +13 -0
- package/src/telemetry/config.ts +63 -0
- package/src/telemetry/db-sanitize.ts +79 -0
- package/src/telemetry/index.ts +29 -0
- package/src/telemetry/metrics.ts +150 -0
- package/src/telemetry/sdk.ts +65 -0
- 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"}
|