@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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tracing.d.ts","sourceRoot":"","sources":["../../src/telemetry/tracing.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAW,KAAK,IAAI,EAAE,KAAK,WAAW,EAAE,KAAK,MAAM,EAAS,MAAM,oBAAoB,CAAC;AAG9F,wBAAsB,UAAU,CAAC,CAAC,EAC9B,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,EAC9B,OAAO,CAAC,EAAE,WAAW,EACrB,MAAM,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,CAAC,CAAC,CAYZ;AAED,wBAAgB,SAAS,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,CAAC,EAAE,OAAO,CAAC,EAAE,WAAW,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,CAAC,CAY3G;AAED,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,GAAG,IAAI,CAK7F;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,GAAG,IAAI,CAKvG;AAED,wBAAgB,aAAa,IAAI,IAAI,GAAG,SAAS,CAEhD;AAED,wBAAgB,QAAQ,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAEtD;AAED,YAAY,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AACpE,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* High-level tracing helpers for application code.
|
|
3
|
+
*/
|
|
4
|
+
import { context, trace } from '@opentelemetry/api';
|
|
5
|
+
import { getTracer } from './sdk.js';
|
|
6
|
+
export async function traceAsync(name, fn, options, tracer) {
|
|
7
|
+
const resolvedTracer = tracer ?? getTracer();
|
|
8
|
+
return resolvedTracer.startActiveSpan(name, options ?? {}, async (span) => {
|
|
9
|
+
try {
|
|
10
|
+
return await fn(span);
|
|
11
|
+
}
|
|
12
|
+
catch (err) {
|
|
13
|
+
span.setStatus({ code: 2, message: err instanceof Error ? err.message : String(err) });
|
|
14
|
+
throw err;
|
|
15
|
+
}
|
|
16
|
+
finally {
|
|
17
|
+
span.end();
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
export function traceSync(name, fn, options, tracer) {
|
|
22
|
+
const resolvedTracer = tracer ?? getTracer();
|
|
23
|
+
return resolvedTracer.startActiveSpan(name, options ?? {}, (span) => {
|
|
24
|
+
try {
|
|
25
|
+
return fn(span);
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
span.setStatus({ code: 2, message: err instanceof Error ? err.message : String(err) });
|
|
29
|
+
throw err;
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
span.end();
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
export function addSpanAttributes(attributes) {
|
|
37
|
+
const span = trace.getActiveSpan();
|
|
38
|
+
if (span?.isRecording()) {
|
|
39
|
+
span.setAttributes(attributes);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export function addSpanEvent(name, attributes) {
|
|
43
|
+
const span = trace.getActiveSpan();
|
|
44
|
+
if (span?.isRecording()) {
|
|
45
|
+
span.addEvent(name, attributes);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function getActiveSpan() {
|
|
49
|
+
return trace.getActiveSpan() ?? undefined;
|
|
50
|
+
}
|
|
51
|
+
export function withSpan(span, fn) {
|
|
52
|
+
return context.with(trace.setSpan(context.active(), span), fn);
|
|
53
|
+
}
|
|
54
|
+
export { context, propagation, trace } from './sdk.js';
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gobing-ai/ts-infra",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "@gobing-ai/ts-infra — Infrastructure backbone: event bus, job queue, scheduler, telemetry, API client, and logging.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"private": false,
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"license": "Apache-2.0",
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"src",
|
|
20
|
+
"README.md"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsc -p tsconfig.build.json && bun ../../scripts/fix-dist-esm-extensions.ts dist",
|
|
24
|
+
"test": "NODE_ENV=test bun test --coverage --coverage-dir=.coverage --reporter=dots",
|
|
25
|
+
"test:full": "NODE_ENV=test bun test --update-snapshots --coverage --coverage-dir=.coverage",
|
|
26
|
+
"typecheck": "tsc --noEmit",
|
|
27
|
+
"lint": "biome check . && bun run typecheck",
|
|
28
|
+
"format": "biome check . --write",
|
|
29
|
+
"check": "bun run lint && bun run test",
|
|
30
|
+
"prepublishOnly": "bun run build",
|
|
31
|
+
"release": "npm publish --access public"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@gobing-ai/ts-db": "^0.1.0",
|
|
35
|
+
"@gobing-ai/ts-runtime": "^0.1.0"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"@opentelemetry/api": "^1.9.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/bun": "1.3.14",
|
|
42
|
+
"@opentelemetry/api": "^1.9.0",
|
|
43
|
+
"@opentelemetry/sdk-trace-node": "^1.30.0",
|
|
44
|
+
"@opentelemetry/sdk-metrics": "^1.30.0",
|
|
45
|
+
"@opentelemetry/semantic-conventions": "^1.30.0"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed HTTP client builder wrapping fetch with OTel tracing.
|
|
3
|
+
*/
|
|
4
|
+
import { type Span, SpanKind } from '@opentelemetry/api';
|
|
5
|
+
import {
|
|
6
|
+
ATTR_HTTP_REQUEST_METHOD,
|
|
7
|
+
ATTR_HTTP_RESPONSE_STATUS_CODE,
|
|
8
|
+
ATTR_URL_FULL,
|
|
9
|
+
} from '@opentelemetry/semantic-conventions';
|
|
10
|
+
import {
|
|
11
|
+
getHttpClientRequestDuration,
|
|
12
|
+
getHttpClientRequestErrors,
|
|
13
|
+
getHttpClientRequestTotal,
|
|
14
|
+
} from './telemetry/metrics';
|
|
15
|
+
import { traceAsync } from './telemetry/tracing';
|
|
16
|
+
|
|
17
|
+
// ── Types ───────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
export interface APIClientConfig {
|
|
20
|
+
baseUrl: string;
|
|
21
|
+
defaultHeaders?: Record<string, string>;
|
|
22
|
+
timeout?: number;
|
|
23
|
+
fetch?: typeof globalThis.fetch;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface RequestOptions {
|
|
27
|
+
headers?: Record<string, string>;
|
|
28
|
+
timeout?: number;
|
|
29
|
+
operationName?: string;
|
|
30
|
+
signal?: AbortSignal;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class APIError extends Error {
|
|
34
|
+
constructor(
|
|
35
|
+
public readonly status: number,
|
|
36
|
+
public readonly body: string,
|
|
37
|
+
) {
|
|
38
|
+
super(`HTTP ${status}: ${body.slice(0, 200)}`);
|
|
39
|
+
this.name = 'APIError';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ── Client ──────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
export class APIClient {
|
|
46
|
+
private readonly baseUrl: string;
|
|
47
|
+
private readonly defaultHeaders: Record<string, string>;
|
|
48
|
+
private readonly timeout: number;
|
|
49
|
+
private readonly fetchFn: typeof globalThis.fetch;
|
|
50
|
+
|
|
51
|
+
constructor(config: APIClientConfig) {
|
|
52
|
+
this.baseUrl = config.baseUrl.replace(/\/+$/, '');
|
|
53
|
+
this.defaultHeaders = config.defaultHeaders ?? {};
|
|
54
|
+
this.timeout = config.timeout ?? 30_000;
|
|
55
|
+
this.fetchFn = config.fetch ?? globalThis.fetch;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private buildUrl(path: string): string {
|
|
59
|
+
return `${this.baseUrl}${path.startsWith('/') ? path : `/${path}`}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private async request<T>(method: string, path: string, body?: unknown, opts?: RequestOptions): Promise<T> {
|
|
63
|
+
const url = this.buildUrl(path);
|
|
64
|
+
const headers: Record<string, string> = {
|
|
65
|
+
'Content-Type': 'application/json',
|
|
66
|
+
...this.defaultHeaders,
|
|
67
|
+
...opts?.headers,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const operationName = opts?.operationName ?? `HTTP ${method} ${url}`;
|
|
71
|
+
|
|
72
|
+
return traceAsync(
|
|
73
|
+
operationName,
|
|
74
|
+
async (span: Span) => {
|
|
75
|
+
span.setAttribute(ATTR_HTTP_REQUEST_METHOD, method);
|
|
76
|
+
span.setAttribute(ATTR_URL_FULL, url);
|
|
77
|
+
|
|
78
|
+
const controller = new AbortController();
|
|
79
|
+
const timeoutMs = opts?.timeout ?? this.timeout;
|
|
80
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
81
|
+
|
|
82
|
+
if (timeoutMs > 0) {
|
|
83
|
+
timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const combinedSignal = opts?.signal
|
|
87
|
+
? AbortSignal.any([opts.signal, controller.signal])
|
|
88
|
+
: controller.signal;
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const start = performance.now();
|
|
92
|
+
|
|
93
|
+
const response = await this.fetchFn(url, {
|
|
94
|
+
method,
|
|
95
|
+
headers,
|
|
96
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
97
|
+
signal: combinedSignal,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
if (timer) clearTimeout(timer);
|
|
101
|
+
|
|
102
|
+
span.setAttribute(ATTR_HTTP_RESPONSE_STATUS_CODE, response.status);
|
|
103
|
+
|
|
104
|
+
getHttpClientRequestTotal().add(1, {
|
|
105
|
+
'http.request.method': method,
|
|
106
|
+
'http.response.status_code': response.status,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const duration = performance.now() - start;
|
|
110
|
+
getHttpClientRequestDuration().record(duration, {
|
|
111
|
+
'http.request.method': method,
|
|
112
|
+
'http.response.status_code': response.status,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
if (!response.ok) {
|
|
116
|
+
const text = await response.text();
|
|
117
|
+
getHttpClientRequestErrors().add(1, {
|
|
118
|
+
'http.request.method': method,
|
|
119
|
+
'error.type': `HTTP_${response.status}`,
|
|
120
|
+
});
|
|
121
|
+
throw new APIError(response.status, text);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const contentType = response.headers.get('content-type') ?? '';
|
|
125
|
+
if (contentType.includes('application/json')) {
|
|
126
|
+
return (await response.json()) as T;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return (await response.text()) as unknown as T;
|
|
130
|
+
} catch (error) {
|
|
131
|
+
if (timer) clearTimeout(timer);
|
|
132
|
+
|
|
133
|
+
if (!(error instanceof APIError)) {
|
|
134
|
+
getHttpClientRequestErrors().add(1, {
|
|
135
|
+
'http.request.method': method,
|
|
136
|
+
'error.type': error instanceof Error ? error.name : 'Unknown',
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
{ kind: SpanKind.CLIENT },
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async get<T>(path: string, opts?: RequestOptions): Promise<T> {
|
|
148
|
+
return this.request<T>('GET', path, undefined, opts);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async post<T>(path: string, body?: unknown, opts?: RequestOptions): Promise<T> {
|
|
152
|
+
return this.request<T>('POST', path, body, opts);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async put<T>(path: string, body?: unknown, opts?: RequestOptions): Promise<T> {
|
|
156
|
+
return this.request<T>('PUT', path, body, opts);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async delete<T>(path: string, opts?: RequestOptions): Promise<T> {
|
|
160
|
+
return this.request<T>('DELETE', path, undefined, opts);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import type { JobQueue } from '../job-queue/types';
|
|
2
|
+
import { getLogger, type Logger } from '../logger';
|
|
3
|
+
import type {
|
|
4
|
+
AsyncEnqueuedDetail,
|
|
5
|
+
BusLifecycleEvents,
|
|
6
|
+
EmitDoneDetail,
|
|
7
|
+
EventMap,
|
|
8
|
+
HandlerErrorDetail,
|
|
9
|
+
SubscribeOptions,
|
|
10
|
+
} from './types';
|
|
11
|
+
|
|
12
|
+
let _busLogger: Logger | undefined;
|
|
13
|
+
function busLogger(): Logger {
|
|
14
|
+
if (!_busLogger) _busLogger = getLogger('event-bus');
|
|
15
|
+
return _busLogger;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Type-safe event bus supporting both synchronous (in-process) and
|
|
20
|
+
* asynchronous handlers with optional job-queue enqueue instrumentation.
|
|
21
|
+
*/
|
|
22
|
+
export class EventBus<TEvents extends EventMap> {
|
|
23
|
+
private readonly syncHandlers = new Map<keyof TEvents, Set<TEvents[keyof TEvents]>>();
|
|
24
|
+
private readonly asyncHandlers = new Map<keyof TEvents, Set<TEvents[keyof TEvents]>>();
|
|
25
|
+
private readonly asyncHandlerIds = new WeakMap<TEvents[keyof TEvents], string>();
|
|
26
|
+
private readonly jobQueue: JobQueue | null;
|
|
27
|
+
private readonly lifecycleBus: EventBus<BusLifecycleEvents> | null;
|
|
28
|
+
private nextAsyncHandlerId = 0;
|
|
29
|
+
|
|
30
|
+
constructor(opts?: {
|
|
31
|
+
jobQueue?: JobQueue;
|
|
32
|
+
lifecycleBus?: EventBus<BusLifecycleEvents>;
|
|
33
|
+
}) {
|
|
34
|
+
this.jobQueue = opts?.jobQueue ?? null;
|
|
35
|
+
this.lifecycleBus = opts?.lifecycleBus ?? null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
on<K extends keyof TEvents>(event: K, handler: TEvents[K], opts?: SubscribeOptions): void {
|
|
39
|
+
if (opts?.async) {
|
|
40
|
+
this.registerAsync(event, handler);
|
|
41
|
+
} else {
|
|
42
|
+
this.registerSync(event, handler);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
once<K extends keyof TEvents>(event: K, handler: TEvents[K], opts?: SubscribeOptions): void {
|
|
47
|
+
const wrapped = ((...args: Parameters<TEvents[K]>) => {
|
|
48
|
+
this.off(event, wrapped as TEvents[K]);
|
|
49
|
+
handler(...args);
|
|
50
|
+
}) as TEvents[K];
|
|
51
|
+
|
|
52
|
+
this.on(event, wrapped, opts);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
off<K extends keyof TEvents>(event: K, handler: TEvents[K]): void {
|
|
56
|
+
const syncSet = this.syncHandlers.get(event);
|
|
57
|
+
if (syncSet) {
|
|
58
|
+
syncSet.delete(handler);
|
|
59
|
+
if (syncSet.size === 0) {
|
|
60
|
+
this.syncHandlers.delete(event);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const asyncSet = this.asyncHandlers.get(event);
|
|
65
|
+
if (asyncSet) {
|
|
66
|
+
asyncSet.delete(handler);
|
|
67
|
+
if (asyncSet.size === 0) {
|
|
68
|
+
this.asyncHandlers.delete(event);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
removeAllListeners<K extends keyof TEvents>(event?: K): void {
|
|
74
|
+
if (event !== undefined) {
|
|
75
|
+
this.syncHandlers.delete(event);
|
|
76
|
+
this.asyncHandlers.delete(event);
|
|
77
|
+
} else {
|
|
78
|
+
this.syncHandlers.clear();
|
|
79
|
+
this.asyncHandlers.clear();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async emit<K extends keyof TEvents>(event: K, ...args: Parameters<TEvents[K]>): Promise<void> {
|
|
84
|
+
const eventName = String(event);
|
|
85
|
+
const startMs = performance.now();
|
|
86
|
+
let syncCount = 0;
|
|
87
|
+
let asyncCount = 0;
|
|
88
|
+
let errors = 0;
|
|
89
|
+
|
|
90
|
+
const syncSet = this.syncHandlers.get(event);
|
|
91
|
+
if (syncSet) {
|
|
92
|
+
syncCount = syncSet.size;
|
|
93
|
+
for (const handler of syncSet) {
|
|
94
|
+
try {
|
|
95
|
+
handler(...args);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
errors++;
|
|
98
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
99
|
+
busLogger().error('sync handler threw', { event: eventName, error: message });
|
|
100
|
+
this.publishHandlerError(eventName, 'sync', message);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const asyncSet = this.asyncHandlers.get(event);
|
|
106
|
+
if (asyncSet && asyncSet.size > 0) {
|
|
107
|
+
const handlers = [...asyncSet];
|
|
108
|
+
asyncCount = handlers.length;
|
|
109
|
+
|
|
110
|
+
for (const handler of handlers) {
|
|
111
|
+
if (this.jobQueue) {
|
|
112
|
+
try {
|
|
113
|
+
const jobId = await this.jobQueue.enqueue(eventName, {
|
|
114
|
+
event: eventName,
|
|
115
|
+
args,
|
|
116
|
+
handlerId: this.getAsyncHandlerId(handler),
|
|
117
|
+
});
|
|
118
|
+
busLogger().debug('async job enqueued', { event: eventName, jobId, handlerCount: 1 });
|
|
119
|
+
this.publishAsyncEnqueued(eventName, jobId, 1);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
errors++;
|
|
122
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
123
|
+
busLogger().error('async enqueue failed', { event: eventName, error: message });
|
|
124
|
+
this.publishHandlerError(eventName, 'async', message);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
busLogger().warn('async handlers registered but no JobQueue injected', {
|
|
129
|
+
event: eventName,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
await Promise.resolve().then(() => handler(...args));
|
|
135
|
+
} catch (error) {
|
|
136
|
+
errors++;
|
|
137
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
138
|
+
busLogger().error('async handler threw', { event: eventName, error: message });
|
|
139
|
+
this.publishHandlerError(eventName, 'async', message);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const durationMs = performance.now() - startMs;
|
|
145
|
+
const detail = args.length === 1 ? args[0] : args.length > 1 ? args : undefined;
|
|
146
|
+
|
|
147
|
+
this.publishEmitDone({ event: eventName, syncCount, asyncCount, emitDurationMs: durationMs, errors, detail });
|
|
148
|
+
|
|
149
|
+
if (syncCount === 0 && asyncCount === 0) {
|
|
150
|
+
busLogger().debug('emit with zero handlers', { event: eventName });
|
|
151
|
+
this.publishEmitNoop(eventName);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
listenerCount<K extends keyof TEvents>(event: K, mode?: 'sync' | 'async'): number {
|
|
156
|
+
const sync = mode !== 'async' ? (this.syncHandlers.get(event)?.size ?? 0) : 0;
|
|
157
|
+
const async = mode !== 'sync' ? (this.asyncHandlers.get(event)?.size ?? 0) : 0;
|
|
158
|
+
return sync + async;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
eventNames(): string[] {
|
|
162
|
+
const names = new Set<string>();
|
|
163
|
+
for (const key of this.syncHandlers.keys()) names.add(String(key));
|
|
164
|
+
for (const key of this.asyncHandlers.keys()) names.add(String(key));
|
|
165
|
+
return [...names];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private registerSync<K extends keyof TEvents>(event: K, handler: TEvents[K]): void {
|
|
169
|
+
let set = this.syncHandlers.get(event);
|
|
170
|
+
if (!set) {
|
|
171
|
+
set = new Set();
|
|
172
|
+
this.syncHandlers.set(event, set);
|
|
173
|
+
}
|
|
174
|
+
set.add(handler);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private registerAsync<K extends keyof TEvents>(event: K, handler: TEvents[K]): void {
|
|
178
|
+
let set = this.asyncHandlers.get(event);
|
|
179
|
+
if (!set) {
|
|
180
|
+
set = new Set();
|
|
181
|
+
this.asyncHandlers.set(event, set);
|
|
182
|
+
}
|
|
183
|
+
set.add(handler);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private getAsyncHandlerId(handler: TEvents[keyof TEvents]): string {
|
|
187
|
+
const existing = this.asyncHandlerIds.get(handler);
|
|
188
|
+
if (existing) return existing;
|
|
189
|
+
|
|
190
|
+
const id = `handler-${++this.nextAsyncHandlerId}`;
|
|
191
|
+
this.asyncHandlerIds.set(handler, id);
|
|
192
|
+
return id;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private publishEmitDone(detail: EmitDoneDetail): void {
|
|
196
|
+
if (this.lifecycleBus) {
|
|
197
|
+
try {
|
|
198
|
+
void this.lifecycleBus.emit('bus.emit.done', detail);
|
|
199
|
+
} catch {
|
|
200
|
+
// Lifecycle bus failures must never affect the primary bus.
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private publishEmitNoop(event: string): void {
|
|
206
|
+
if (this.lifecycleBus) {
|
|
207
|
+
try {
|
|
208
|
+
void this.lifecycleBus.emit('bus.emit.noop', { event });
|
|
209
|
+
} catch {
|
|
210
|
+
// Swallow.
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private publishHandlerError(event: string, mode: 'sync' | 'async', error: string): void {
|
|
216
|
+
if (this.lifecycleBus) {
|
|
217
|
+
const detail: HandlerErrorDetail = { event, mode, error };
|
|
218
|
+
try {
|
|
219
|
+
void this.lifecycleBus.emit('bus.handler.error', detail);
|
|
220
|
+
} catch {
|
|
221
|
+
// Swallow.
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private publishAsyncEnqueued(event: string, jobId: string, handlerCount: number): void {
|
|
227
|
+
if (this.lifecycleBus) {
|
|
228
|
+
const detail: AsyncEnqueuedDetail = { event, jobId, handlerCount };
|
|
229
|
+
try {
|
|
230
|
+
void this.lifecycleBus.emit('bus.handler.async.enqueued', detail);
|
|
231
|
+
} catch {
|
|
232
|
+
// Swallow.
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event bus types — shared constraints for typed pub-sub.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type EventMap = Record<string, (...args: never[]) => void>;
|
|
6
|
+
|
|
7
|
+
export interface SubscribeOptions {
|
|
8
|
+
/** When true, the handler is dispatched through the async handler path. */
|
|
9
|
+
async?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// ── Lifecycle events ────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
export interface EmitDoneDetail {
|
|
15
|
+
event: string;
|
|
16
|
+
syncCount: number;
|
|
17
|
+
asyncCount: number;
|
|
18
|
+
emitDurationMs: number;
|
|
19
|
+
errors: number;
|
|
20
|
+
detail?: unknown;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface HandlerErrorDetail {
|
|
24
|
+
event: string;
|
|
25
|
+
mode: 'sync' | 'async';
|
|
26
|
+
error: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface AsyncEnqueuedDetail {
|
|
30
|
+
event: string;
|
|
31
|
+
jobId: string;
|
|
32
|
+
handlerCount: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type BusLifecycleEvents = {
|
|
36
|
+
'bus.emit.done': (detail: EmitDoneDetail) => void;
|
|
37
|
+
'bus.emit.noop': (detail: { event: string }) => void;
|
|
38
|
+
'bus.handler.error': (detail: HandlerErrorDetail) => void;
|
|
39
|
+
'bus.handler.async.enqueued': (detail: AsyncEnqueuedDetail) => void;
|
|
40
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Application event definitions — generic typed event map pattern.
|
|
3
|
+
* Apps extend this with their own event types.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type AppEvents = Record<string, (...args: never[]) => void>;
|
|
7
|
+
|
|
8
|
+
export type AppInternalEvents = Record<string, (...args: never[]) => void>;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// API Client
|
|
2
|
+
export { APIClient, type APIClientConfig, APIError, type RequestOptions } from './api-client';
|
|
3
|
+
|
|
4
|
+
// Event Bus
|
|
5
|
+
export { EventBus, type EventMap, type SubscribeOptions } from './event-bus/index';
|
|
6
|
+
|
|
7
|
+
// Events
|
|
8
|
+
export type { AppEvents, AppInternalEvents } from './events/index';
|
|
9
|
+
export { createSystemBus } from './events/index';
|
|
10
|
+
|
|
11
|
+
// Job Queue
|
|
12
|
+
export type {
|
|
13
|
+
EnqueueOptions,
|
|
14
|
+
Job,
|
|
15
|
+
JobHandler,
|
|
16
|
+
JobQueue,
|
|
17
|
+
QueueConsumer,
|
|
18
|
+
QueueConsumerConfig,
|
|
19
|
+
QueueStats,
|
|
20
|
+
} from './job-queue/index';
|
|
21
|
+
|
|
22
|
+
// Logger
|
|
23
|
+
export { getLogger, initializeLogger, type Logger, type LogLevel } from './logger';
|
|
24
|
+
|
|
25
|
+
// Scheduler
|
|
26
|
+
export {
|
|
27
|
+
CloudflareSchedulerAdapter,
|
|
28
|
+
getSchedulerAdapter,
|
|
29
|
+
initScheduler,
|
|
30
|
+
NodeSchedulerAdapter,
|
|
31
|
+
NoopSchedulerAdapter,
|
|
32
|
+
type ScheduledAction,
|
|
33
|
+
type SchedulerAdapter,
|
|
34
|
+
setSchedulerAdapter,
|
|
35
|
+
} from './scheduler/index';
|
|
36
|
+
|
|
37
|
+
// Telemetry
|
|
38
|
+
export {
|
|
39
|
+
addSpanAttributes,
|
|
40
|
+
addSpanEvent,
|
|
41
|
+
extractSqlOperation,
|
|
42
|
+
getActiveSpan,
|
|
43
|
+
getDbOperationDuration,
|
|
44
|
+
getDbOperationErrors,
|
|
45
|
+
getDbOperationTotal,
|
|
46
|
+
getEventbusEmitsTotal,
|
|
47
|
+
getEventbusErrorsTotal,
|
|
48
|
+
getHttpClientRequestDuration,
|
|
49
|
+
getHttpClientRequestErrors,
|
|
50
|
+
getHttpClientRequestTotal,
|
|
51
|
+
getHttpServerRequestDuration,
|
|
52
|
+
getHttpServerRequestErrors,
|
|
53
|
+
getHttpServerRequestTotal,
|
|
54
|
+
getQueueJobCompletedTotal,
|
|
55
|
+
getQueueJobEnqueuedTotal,
|
|
56
|
+
getQueueJobFailedTotal,
|
|
57
|
+
getQueueJobProcessingDuration,
|
|
58
|
+
getSchedulerJobDuration,
|
|
59
|
+
getSchedulerJobExecutedTotal,
|
|
60
|
+
getSchedulerJobFailedTotal,
|
|
61
|
+
getTelemetryConfig,
|
|
62
|
+
getTracer,
|
|
63
|
+
initMetrics,
|
|
64
|
+
initTelemetry,
|
|
65
|
+
isTelemetryEnabled,
|
|
66
|
+
sanitizeSql,
|
|
67
|
+
shutdownMetrics,
|
|
68
|
+
shutdownTelemetry,
|
|
69
|
+
type TelemetryConfig,
|
|
70
|
+
type TelemetryConfigPartial,
|
|
71
|
+
traceAsync,
|
|
72
|
+
traceSync,
|
|
73
|
+
withSpan,
|
|
74
|
+
} from './telemetry/index';
|