@gobing-ai/ts-infra 0.3.1 → 0.3.3
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/src/event-bus/index.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
export {
|
|
2
|
+
attachDefaultObservers,
|
|
3
|
+
attachLogObserver,
|
|
4
|
+
attachTelemetryObserver,
|
|
5
|
+
createLifecycleBus,
|
|
6
|
+
} from './default-observers';
|
|
1
7
|
export { EventBus } from './event-bus';
|
|
8
|
+
export { attachFileObserver, type FileObserverWriter } from './file-observer';
|
|
2
9
|
export type {
|
|
3
10
|
AsyncEnqueuedDetail,
|
|
4
11
|
BusLifecycleEvents,
|
package/src/event-bus/types.ts
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
* Event bus types — shared constraints for typed pub-sub.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
/** Type constraint for event maps — keys are event names, values are listener signatures. */
|
|
5
6
|
export type EventMap = Record<string, (...args: never[]) => void>;
|
|
6
7
|
|
|
8
|
+
/** Options for subscribing to an event, including async dispatch mode. */
|
|
7
9
|
export interface SubscribeOptions {
|
|
8
10
|
/** When true, the handler is dispatched through the async handler path. */
|
|
9
11
|
async?: boolean;
|
|
@@ -11,6 +13,7 @@ export interface SubscribeOptions {
|
|
|
11
13
|
|
|
12
14
|
// ── Lifecycle events ────────────────────────────────────────────────
|
|
13
15
|
|
|
16
|
+
/** Payload emitted on `bus.emit.done` after synchronous handlers complete. */
|
|
14
17
|
export interface EmitDoneDetail {
|
|
15
18
|
event: string;
|
|
16
19
|
syncCount: number;
|
|
@@ -20,18 +23,21 @@ export interface EmitDoneDetail {
|
|
|
20
23
|
detail?: unknown;
|
|
21
24
|
}
|
|
22
25
|
|
|
26
|
+
/** Payload emitted on `bus.handler.error` when a handler throws. */
|
|
23
27
|
export interface HandlerErrorDetail {
|
|
24
28
|
event: string;
|
|
25
29
|
mode: 'sync' | 'async';
|
|
26
30
|
error: string;
|
|
27
31
|
}
|
|
28
32
|
|
|
33
|
+
/** Payload emitted on `bus.handler.async.enqueued` when async handlers are scheduled. */
|
|
29
34
|
export interface AsyncEnqueuedDetail {
|
|
30
35
|
event: string;
|
|
31
36
|
jobId: string;
|
|
32
37
|
handlerCount: number;
|
|
33
38
|
}
|
|
34
39
|
|
|
40
|
+
/** Strongly-typed lifecycle events emitted by the event bus. */
|
|
35
41
|
export type BusLifecycleEvents = {
|
|
36
42
|
'bus.emit.done': (detail: EmitDoneDetail) => void;
|
|
37
43
|
'bus.emit.noop': (detail: { event: string }) => void;
|
package/src/events.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Infrastructure-level event definitions for ts-infra observability.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the package-local pattern in `@gobing-ai/ts-ai-runner` (`events.ts`):
|
|
5
|
+
* only the events ts-infra components actually emit live here. Application-domain
|
|
6
|
+
* events (history import, HTTP server, …) belong to the consuming app, not this
|
|
7
|
+
* library. Process events belong to `@gobing-ai/ts-runtime` (the owner of
|
|
8
|
+
* `ProcessExecutor`) and are intentionally not re-exported here.
|
|
9
|
+
*
|
|
10
|
+
* Consume via `EventBus<InfraEvents>` or compose individual maps into a wider
|
|
11
|
+
* app event map.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { QueueStats } from './job-queue/types';
|
|
15
|
+
|
|
16
|
+
// ── Detail payloads ────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
/** Payload for `db.connection.error`. */
|
|
19
|
+
export interface DbConnectionErrorDetail {
|
|
20
|
+
/** Error message. */
|
|
21
|
+
error: string;
|
|
22
|
+
/** Adapter type (e.g. 'sqlite', 'd1'). */
|
|
23
|
+
adapter: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Payload for `queue.job.failed` — a job exhausted retries. */
|
|
27
|
+
export interface QueueJobFailedDetail {
|
|
28
|
+
jobId: string;
|
|
29
|
+
type: string;
|
|
30
|
+
error: string;
|
|
31
|
+
/** Attempt number (0-indexed, incremented after each failure). */
|
|
32
|
+
attempt: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Payload for `queue.job.retrying` — a job will be retried after backoff. */
|
|
36
|
+
export interface QueueJobRetryingDetail {
|
|
37
|
+
jobId: string;
|
|
38
|
+
type: string;
|
|
39
|
+
attempt: number;
|
|
40
|
+
/** When the next retry will fire (epoch ms). */
|
|
41
|
+
nextRetryAt: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Payload for `scheduler.job.executed`. */
|
|
45
|
+
export interface SchedulerJobExecutedDetail {
|
|
46
|
+
/** Job name. */
|
|
47
|
+
name: string;
|
|
48
|
+
/** Wall-clock duration in milliseconds. */
|
|
49
|
+
durationMs: number;
|
|
50
|
+
/** Error message if the job threw. */
|
|
51
|
+
error?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Payload for `api.request.error`. */
|
|
55
|
+
export interface ApiRequestErrorDetail {
|
|
56
|
+
url: string;
|
|
57
|
+
method: string;
|
|
58
|
+
/** HTTP status code, if a response was received. */
|
|
59
|
+
status?: number;
|
|
60
|
+
error: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── Event maps ─────────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
/** Database lifecycle events emitted by DB-facing infra wiring. */
|
|
66
|
+
export type DbEvents = {
|
|
67
|
+
/** Database connection established. */
|
|
68
|
+
'db.connected': () => void;
|
|
69
|
+
/** Database connection error. */
|
|
70
|
+
'db.connection.error': (detail: DbConnectionErrorDetail) => void;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/** Job-queue lifecycle events emitted by the queue and its consumer. */
|
|
74
|
+
export type QueueEvents = {
|
|
75
|
+
/** A new job was enqueued. */
|
|
76
|
+
'queue.job.enqueued': (detail: { jobId: string; type: string }) => void;
|
|
77
|
+
/** Consumer polling loop started. */
|
|
78
|
+
'queue.consumer.started': () => void;
|
|
79
|
+
/** Consumer stopped. */
|
|
80
|
+
'queue.consumer.stopped': () => void;
|
|
81
|
+
/** A job completed successfully. */
|
|
82
|
+
'queue.job.completed': (detail: { jobId: string; type: string }) => void;
|
|
83
|
+
/** A job exhausted retries and is permanently failed. */
|
|
84
|
+
'queue.job.failed': (detail: QueueJobFailedDetail) => void;
|
|
85
|
+
/** A job will be retried after backoff. */
|
|
86
|
+
'queue.job.retrying': (detail: QueueJobRetryingDetail) => void;
|
|
87
|
+
/** Queue depth snapshot emitted on a poll cycle. */
|
|
88
|
+
'queue.stats': (detail: QueueStats) => void;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/** Scheduler events emitted by scheduler adapters and the handler wrapper. */
|
|
92
|
+
export type SchedulerEvents = {
|
|
93
|
+
/** A scheduled job was executed (success or failure). */
|
|
94
|
+
'scheduler.job.executed': (detail: SchedulerJobExecutedDetail) => void;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/** API-client events. */
|
|
98
|
+
export type ApiClientEvents = {
|
|
99
|
+
/** An API request failed (non-2xx or network error). */
|
|
100
|
+
'api.request.error': (detail: ApiRequestErrorDetail) => void;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Aggregate of all infrastructure-level event maps ts-infra emits. Use with
|
|
105
|
+
* `EventBus<InfraEvents>`, or pick individual maps when composing a wider app
|
|
106
|
+
* event map.
|
|
107
|
+
*/
|
|
108
|
+
export type InfraEvents = DbEvents & QueueEvents & SchedulerEvents & ApiClientEvents;
|
package/src/index.ts
CHANGED
|
@@ -2,7 +2,31 @@
|
|
|
2
2
|
export { APIClient, type APIClientConfig, APIError, type RequestOptions } from './api-client';
|
|
3
3
|
|
|
4
4
|
// Event Bus
|
|
5
|
-
export {
|
|
5
|
+
export {
|
|
6
|
+
attachDefaultObservers,
|
|
7
|
+
attachFileObserver,
|
|
8
|
+
attachLogObserver,
|
|
9
|
+
attachTelemetryObserver,
|
|
10
|
+
createLifecycleBus,
|
|
11
|
+
EventBus,
|
|
12
|
+
type EventMap,
|
|
13
|
+
type FileObserverWriter,
|
|
14
|
+
type SubscribeOptions,
|
|
15
|
+
} from './event-bus/index';
|
|
16
|
+
|
|
17
|
+
// Events (infrastructure-level event maps)
|
|
18
|
+
export type {
|
|
19
|
+
ApiClientEvents,
|
|
20
|
+
ApiRequestErrorDetail,
|
|
21
|
+
DbConnectionErrorDetail,
|
|
22
|
+
DbEvents,
|
|
23
|
+
InfraEvents,
|
|
24
|
+
QueueEvents,
|
|
25
|
+
QueueJobFailedDetail,
|
|
26
|
+
QueueJobRetryingDetail,
|
|
27
|
+
SchedulerEvents,
|
|
28
|
+
SchedulerJobExecutedDetail,
|
|
29
|
+
} from './events';
|
|
6
30
|
|
|
7
31
|
export type {
|
|
8
32
|
EnqueueOptions,
|
|
@@ -13,22 +37,35 @@ export type {
|
|
|
13
37
|
QueueConsumerConfig,
|
|
14
38
|
QueueStats,
|
|
15
39
|
} from './job-queue/index';
|
|
16
|
-
// Job Queue
|
|
17
|
-
export { DBJobQueue, DBQueueConsumer } from './job-queue/index';
|
|
18
|
-
|
|
19
40
|
// Logger
|
|
20
|
-
export {
|
|
41
|
+
export {
|
|
42
|
+
getLogger,
|
|
43
|
+
type InitLoggerOptions,
|
|
44
|
+
initializeLogger,
|
|
45
|
+
type Logger,
|
|
46
|
+
type LogLevel,
|
|
47
|
+
setLoggerMuted,
|
|
48
|
+
} from './logger';
|
|
21
49
|
|
|
22
50
|
// Scheduler
|
|
23
51
|
export {
|
|
24
|
-
|
|
52
|
+
ActionRegistry,
|
|
53
|
+
type CreateDefaultRegistryOptions,
|
|
54
|
+
createDefaultRegistry,
|
|
25
55
|
getSchedulerAdapter,
|
|
56
|
+
HealthPingAction,
|
|
57
|
+
type HealthPingWriter,
|
|
26
58
|
initScheduler,
|
|
27
|
-
|
|
59
|
+
LogAction,
|
|
28
60
|
NoopSchedulerAdapter,
|
|
61
|
+
QueueStatsAction,
|
|
62
|
+
type QueueStatsDaoProvider,
|
|
29
63
|
type ScheduledAction,
|
|
64
|
+
type SchedulerAction,
|
|
30
65
|
type SchedulerAdapter,
|
|
31
66
|
setSchedulerAdapter,
|
|
67
|
+
toScheduledAction,
|
|
68
|
+
wrapScheduledHandler,
|
|
32
69
|
} from './scheduler/index';
|
|
33
70
|
|
|
34
71
|
// Telemetry
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
getQueueJobFailedTotal,
|
|
6
6
|
getQueueJobProcessingDuration,
|
|
7
7
|
} from '../telemetry/metrics';
|
|
8
|
+
import { addSpanAttributes, traceAsync } from '../telemetry/tracing';
|
|
8
9
|
import type { EnqueueOptions, Job, JobHandler, JobQueue, QueueConsumer, QueueConsumerConfig } from './types';
|
|
9
10
|
|
|
10
11
|
/** DB-backed job queue implementation over `@gobing-ai/ts-db`'s `QueueJobDao`. */
|
|
@@ -84,28 +85,31 @@ export class DBQueueConsumer<T = unknown> implements QueueConsumer<T> {
|
|
|
84
85
|
|
|
85
86
|
/** Process one batch immediately. Useful for tests, schedulers, and manual drains. */
|
|
86
87
|
async processOnce(): Promise<number> {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
88
|
+
return traceAsync('queue.poll', async () => {
|
|
89
|
+
await this.dao.resetStuckJobs(this.visibilityTimeout);
|
|
90
|
+
await this.dao.failExpiredJobs();
|
|
91
|
+
|
|
92
|
+
const jobs = await this.dao.claimReady(this.batchSize);
|
|
93
|
+
let processed = 0;
|
|
94
|
+
|
|
95
|
+
for (let index = 0; index < jobs.length; index += this.maxConcurrency) {
|
|
96
|
+
const batch = jobs.slice(index, index + this.maxConcurrency);
|
|
97
|
+
await Promise.all(
|
|
98
|
+
batch.map(async (job) => {
|
|
99
|
+
this.inFlight += 1;
|
|
100
|
+
try {
|
|
101
|
+
await this.processJob(job);
|
|
102
|
+
processed += 1;
|
|
103
|
+
} finally {
|
|
104
|
+
this.inFlight -= 1;
|
|
105
|
+
}
|
|
106
|
+
}),
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
addSpanAttributes({ 'queue.claimed': jobs.length, 'queue.processed': processed });
|
|
111
|
+
return processed;
|
|
112
|
+
});
|
|
109
113
|
}
|
|
110
114
|
|
|
111
115
|
private schedule(delay: number): void {
|
|
@@ -125,22 +129,30 @@ export class DBQueueConsumer<T = unknown> implements QueueConsumer<T> {
|
|
|
125
129
|
|
|
126
130
|
private async processJob(record: QueueJobRecord): Promise<void> {
|
|
127
131
|
const job = toJob<T>(record);
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
132
|
+
return traceAsync('queue.job.process', async () => {
|
|
133
|
+
addSpanAttributes({
|
|
134
|
+
'queue.job_id': job.id,
|
|
135
|
+
'queue.job_type': job.type,
|
|
136
|
+
'queue.job_attempt': job.attempts,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const handler = this.handlers.get(job.type);
|
|
140
|
+
if (handler === undefined) {
|
|
141
|
+
await this.failOrRetry(job, new Error(`No handler registered for job type "${job.type}"`));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const startMs = performance.now();
|
|
146
|
+
try {
|
|
147
|
+
await handler(job);
|
|
148
|
+
await this.dao.markCompleted(job.id);
|
|
149
|
+
getQueueJobCompletedTotal().add(1, { type: job.type });
|
|
150
|
+
getQueueJobProcessingDuration().record(performance.now() - startMs, { type: job.type });
|
|
151
|
+
} catch (error) {
|
|
152
|
+
getQueueJobProcessingDuration().record(performance.now() - startMs, { type: job.type });
|
|
153
|
+
await this.failOrRetry(job, error);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
144
156
|
}
|
|
145
157
|
|
|
146
158
|
private async failOrRetry(job: Job<T>, error: unknown): Promise<void> {
|
package/src/job-queue/index.ts
CHANGED
package/src/job-queue/types.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* `DBJobQueue` and `DBQueueConsumer`.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
/** A queued job with status tracking, retry metadata, and timestamps. */
|
|
8
9
|
export interface Job<T = unknown> {
|
|
9
10
|
id: string;
|
|
10
11
|
type: string;
|
|
@@ -19,20 +20,24 @@ export interface Job<T = unknown> {
|
|
|
19
20
|
processingAt: number | null;
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
/** Options for enqueuing a job: retry policy, delay, and TTL. */
|
|
22
24
|
export interface EnqueueOptions {
|
|
23
25
|
maxRetries?: number;
|
|
24
26
|
delay?: number;
|
|
25
27
|
ttlMs?: number;
|
|
26
28
|
}
|
|
27
29
|
|
|
30
|
+
/** Producer interface for the job queue — enqueue single jobs or batches. */
|
|
28
31
|
export interface JobQueue<T = unknown> {
|
|
29
32
|
enqueue(type: string, payload: T, options?: EnqueueOptions): Promise<string>;
|
|
30
33
|
enqueueBatch(jobs: Array<{ type: string; payload: T } & EnqueueOptions>): Promise<string[]>;
|
|
31
34
|
stats(): Promise<QueueStats>;
|
|
32
35
|
}
|
|
33
36
|
|
|
37
|
+
/** Async handler that processes a single job. */
|
|
34
38
|
export type JobHandler<T = unknown> = (job: Job<T>) => Promise<void>;
|
|
35
39
|
|
|
40
|
+
/** Aggregate statistics for a job queue: counts by status. */
|
|
36
41
|
export interface QueueStats {
|
|
37
42
|
pending: number;
|
|
38
43
|
processing: number;
|
|
@@ -40,6 +45,7 @@ export interface QueueStats {
|
|
|
40
45
|
failed: number;
|
|
41
46
|
}
|
|
42
47
|
|
|
48
|
+
/** Configuration for a queue consumer: polling, concurrency, and backoff. */
|
|
43
49
|
export interface QueueConsumerConfig {
|
|
44
50
|
pollInterval?: number;
|
|
45
51
|
batchSize?: number;
|
|
@@ -50,6 +56,7 @@ export interface QueueConsumerConfig {
|
|
|
50
56
|
drainTimeoutMs?: number;
|
|
51
57
|
}
|
|
52
58
|
|
|
59
|
+
/** Consumer interface for the job queue — register handlers and control the processing loop. */
|
|
53
60
|
export interface QueueConsumer<T = unknown> {
|
|
54
61
|
register(type: string, handler: JobHandler<T>): void;
|
|
55
62
|
start(): Promise<void>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DBJobQueue, DBQueueConsumer } from './job-queue/db-job-queue';
|
package/src/logger.ts
CHANGED
|
@@ -1,20 +1,45 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Structured
|
|
2
|
+
* Structured logger for ts-infra, backed by LogTape.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* LogTape owns level filtering, sink routing, and formatting; this module
|
|
5
|
+
* adapts LogTape's template-string logger to the ts-infra `Logger` contract
|
|
6
|
+
* (`method(msg, data?)` + `child(context)`) so call-sites stay backend-agnostic.
|
|
7
|
+
*
|
|
8
|
+
* ADR-011: ts-infra must not touch `node:fs`/`process.stderr`. The console sink
|
|
9
|
+
* is LogTape's own (`getConsoleSink`); the file sink is supplied by the caller
|
|
10
|
+
* (typically `@gobing-ai/ts-runtime`, which owns FileSystem) via
|
|
11
|
+
* {@link InitLoggerOptions.fileSink}.
|
|
5
12
|
*/
|
|
6
13
|
|
|
14
|
+
import {
|
|
15
|
+
ConfigError,
|
|
16
|
+
configure,
|
|
17
|
+
getConsoleSink,
|
|
18
|
+
getJsonLinesFormatter,
|
|
19
|
+
getTextFormatter,
|
|
20
|
+
type LogRecord,
|
|
21
|
+
type Logger as LtLogger,
|
|
22
|
+
getLogger as ltGetLogger,
|
|
23
|
+
reset,
|
|
24
|
+
type Sink,
|
|
25
|
+
} from '@logtape/logtape';
|
|
26
|
+
|
|
27
|
+
/** Log severity levels in ascending order: trace → debug → info → warn → error → fatal. */
|
|
7
28
|
export type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';
|
|
8
29
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
};
|
|
30
|
+
/** Root category every ts-infra logger lives under, so one config rule covers them all. */
|
|
31
|
+
const ROOT_CATEGORY = 'app';
|
|
32
|
+
|
|
33
|
+
/** Map the ts-infra level name to LogTape's (`warn` → `warning`). */
|
|
34
|
+
function toLogTapeLevel(level: LogLevel): 'trace' | 'debug' | 'info' | 'warning' | 'error' | 'fatal' {
|
|
35
|
+
return level === 'warn' ? 'warning' : level;
|
|
36
|
+
}
|
|
17
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Structured logger interface with level methods and context inheritance
|
|
40
|
+
* via {@link Logger.child}. `data` is attached as structured properties —
|
|
41
|
+
* `msg` is logged verbatim (no template substitution).
|
|
42
|
+
*/
|
|
18
43
|
export interface Logger {
|
|
19
44
|
trace(msg: string, data?: Record<string, unknown>): void;
|
|
20
45
|
debug(msg: string, data?: Record<string, unknown>): void;
|
|
@@ -25,99 +50,99 @@ export interface Logger {
|
|
|
25
50
|
child(context: Record<string, unknown>): Logger;
|
|
26
51
|
}
|
|
27
52
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
constructor(
|
|
32
|
-
private readonly category: string,
|
|
33
|
-
context: Record<string, unknown> = {},
|
|
34
|
-
) {
|
|
35
|
-
this.context = { category, ...context };
|
|
36
|
-
}
|
|
53
|
+
/** Module-level mute flag — LogTape has no global mute, so the adapter gates on it. */
|
|
54
|
+
let muted = false;
|
|
37
55
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
message: msg,
|
|
46
|
-
timestamp: new Date().toISOString(),
|
|
47
|
-
...this.context,
|
|
48
|
-
...data,
|
|
49
|
-
};
|
|
56
|
+
/**
|
|
57
|
+
* Mute or unmute all logger output. Useful for tests. Gating happens in the
|
|
58
|
+
* adapter before delegating to LogTape, so muting is synchronous and global.
|
|
59
|
+
*/
|
|
60
|
+
export function setLoggerMuted(value: boolean): void {
|
|
61
|
+
muted = value;
|
|
62
|
+
}
|
|
50
63
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
case 'fatal':
|
|
55
|
-
console.error(json);
|
|
56
|
-
break;
|
|
57
|
-
case 'warn':
|
|
58
|
-
console.warn(json);
|
|
59
|
-
break;
|
|
60
|
-
case 'debug':
|
|
61
|
-
case 'trace':
|
|
62
|
-
console.debug(json);
|
|
63
|
-
break;
|
|
64
|
-
default:
|
|
65
|
-
console.log(json);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
64
|
+
/** Adapter: ts-infra `Logger` over a LogTape logger. */
|
|
65
|
+
class LogTapeLogger implements Logger {
|
|
66
|
+
constructor(private readonly inner: LtLogger) {}
|
|
68
67
|
|
|
69
68
|
trace(msg: string, data?: Record<string, unknown>): void {
|
|
70
|
-
this.
|
|
69
|
+
if (!muted) this.inner.trace(msg, data ?? {});
|
|
71
70
|
}
|
|
72
71
|
debug(msg: string, data?: Record<string, unknown>): void {
|
|
73
|
-
this.
|
|
72
|
+
if (!muted) this.inner.debug(msg, data ?? {});
|
|
74
73
|
}
|
|
75
74
|
info(msg: string, data?: Record<string, unknown>): void {
|
|
76
|
-
this.
|
|
75
|
+
if (!muted) this.inner.info(msg, data ?? {});
|
|
77
76
|
}
|
|
78
77
|
warn(msg: string, data?: Record<string, unknown>): void {
|
|
79
|
-
this.
|
|
78
|
+
if (!muted) this.inner.warn(msg, data ?? {});
|
|
80
79
|
}
|
|
81
80
|
error(msg: string, data?: Record<string, unknown>): void {
|
|
82
|
-
this.
|
|
81
|
+
if (!muted) this.inner.error(msg, data ?? {});
|
|
83
82
|
}
|
|
84
83
|
fatal(msg: string, data?: Record<string, unknown>): void {
|
|
85
|
-
this.
|
|
84
|
+
if (!muted) this.inner.fatal(msg, data ?? {});
|
|
86
85
|
}
|
|
87
86
|
|
|
88
87
|
child(context: Record<string, unknown>): Logger {
|
|
89
|
-
return new
|
|
88
|
+
return new LogTapeLogger(this.inner.with(context));
|
|
90
89
|
}
|
|
91
90
|
}
|
|
92
91
|
|
|
93
|
-
const loggers = new Map<string, ConsoleLogger>();
|
|
94
|
-
|
|
95
|
-
let globalLevel: LogLevel = 'info';
|
|
96
|
-
let globalMuted = false;
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Mute or unmute all logger console output. Useful for tests.
|
|
100
|
-
*/
|
|
101
|
-
export function setLoggerMuted(muted: boolean): void {
|
|
102
|
-
globalMuted = muted;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
92
|
/**
|
|
106
|
-
* Get
|
|
93
|
+
* Get a logger for the given category. Categories are nested under the
|
|
94
|
+
* `app` root so a single `initializeLogger` rule configures them all.
|
|
107
95
|
*/
|
|
108
96
|
export function getLogger(category: string): Logger {
|
|
109
|
-
|
|
110
|
-
|
|
97
|
+
return new LogTapeLogger(ltGetLogger([ROOT_CATEGORY, category]));
|
|
98
|
+
}
|
|
111
99
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
100
|
+
/** Options for {@link initializeLogger}. */
|
|
101
|
+
export interface InitLoggerOptions {
|
|
102
|
+
/** Minimum level to emit. Default `info`. */
|
|
103
|
+
level?: LogLevel;
|
|
104
|
+
/** Enable the console sink. Default `true`. */
|
|
105
|
+
console?: boolean;
|
|
106
|
+
/**
|
|
107
|
+
* File sink writer. ts-infra never opens files itself (ADR-011); the
|
|
108
|
+
* caller (e.g. ts-runtime FileSystem) provides a writer that appends one
|
|
109
|
+
* already-formatted line per record. When omitted, no file sink is wired.
|
|
110
|
+
*/
|
|
111
|
+
fileSink?: (line: string) => void;
|
|
112
|
+
/** Format records as JSON Lines (`true`) or human-readable text (`false`). Default `true`. */
|
|
113
|
+
json?: boolean;
|
|
115
114
|
}
|
|
116
115
|
|
|
117
116
|
/**
|
|
118
|
-
*
|
|
117
|
+
* Configure LogTape with console and/or file sinks. Call once at startup;
|
|
118
|
+
* safe to call again to reconfigure — the prior config is reset first so the
|
|
119
|
+
* latest call wins (LogTape throws `ConfigError` on double-configure otherwise).
|
|
119
120
|
*/
|
|
120
|
-
export function initializeLogger(
|
|
121
|
-
|
|
122
|
-
|
|
121
|
+
export async function initializeLogger(options: InitLoggerOptions = {}): Promise<void> {
|
|
122
|
+
const { level = 'info', console: enableConsole = true, fileSink, json = true } = options;
|
|
123
|
+
const lowestLevel = toLogTapeLevel(level);
|
|
124
|
+
const formatter = json ? getJsonLinesFormatter() : getTextFormatter();
|
|
125
|
+
|
|
126
|
+
const sinks: Record<string, Sink> = {};
|
|
127
|
+
if (enableConsole) {
|
|
128
|
+
sinks.console = getConsoleSink({ formatter });
|
|
129
|
+
}
|
|
130
|
+
if (fileSink) {
|
|
131
|
+
sinks.file = (record: LogRecord) => {
|
|
132
|
+
fileSink(formatter(record));
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
await reset();
|
|
137
|
+
try {
|
|
138
|
+
await configure({
|
|
139
|
+
sinks,
|
|
140
|
+
loggers: [
|
|
141
|
+
{ category: [ROOT_CATEGORY], lowestLevel, sinks: Object.keys(sinks) },
|
|
142
|
+
{ category: ['logtape', 'meta'], lowestLevel: 'warning', sinks: [] },
|
|
143
|
+
],
|
|
144
|
+
});
|
|
145
|
+
} catch (error) {
|
|
146
|
+
if (!(error instanceof ConfigError)) throw error;
|
|
147
|
+
}
|
|
123
148
|
}
|