@gobing-ai/ts-infra 0.2.7 → 0.2.8
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 +62 -7
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/job-queue/db-job-queue.d.ts +40 -0
- package/dist/job-queue/db-job-queue.d.ts.map +1 -0
- package/dist/job-queue/db-job-queue.js +144 -0
- package/dist/job-queue/index.d.ts +1 -0
- package/dist/job-queue/index.d.ts.map +1 -1
- package/dist/job-queue/index.js +1 -0
- package/dist/job-queue/types.d.ts +2 -4
- package/dist/job-queue/types.d.ts.map +1 -1
- package/dist/job-queue/types.js +2 -4
- package/package.json +3 -3
- package/src/index.ts +2 -2
- package/src/job-queue/db-job-queue.ts +163 -0
- package/src/job-queue/index.ts +1 -0
- package/src/job-queue/types.ts +2 -4
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @gobing-ai/ts-infra
|
|
2
2
|
|
|
3
|
-
Infrastructure backbone — typed event bus, job queue
|
|
3
|
+
Infrastructure backbone — typed event bus, DB-backed job queue, cron scheduler, OpenTelemetry telemetry, HTTP API client, and structured logging.
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
@@ -10,10 +10,10 @@ Infrastructure backbone — typed event bus, job queue (types), cron scheduler,
|
|
|
10
10
|
|-----------|--------|---------|
|
|
11
11
|
| **Event Bus** | `event-bus/` | Typed pub/sub with sync + async dispatch, lifecycle self-observability |
|
|
12
12
|
| **Events** | `events/` | Typed event map pattern + system bus factory |
|
|
13
|
-
| **Job Queue** | `job-queue/` |
|
|
13
|
+
| **Job Queue** | `job-queue/` | DB-backed enqueue and consume flow (`DBJobQueue`, `DBQueueConsumer`) plus queue interfaces |
|
|
14
14
|
| **Scheduler** | `scheduler/` | Cron-like scheduled actions — Node (interval), Cloudflare (Cron Triggers), Noop (test) |
|
|
15
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,
|
|
16
|
+
| **API Client** | `api-client.ts` | Typed HTTP client with OTel tracing, timeout, and error handling |
|
|
17
17
|
| **Logger** | `logger.ts` | Structured JSON logger with levels, child loggers, and mute toggle |
|
|
18
18
|
|
|
19
19
|
## Architecture
|
|
@@ -51,6 +51,18 @@ classDiagram
|
|
|
51
51
|
+stop() Promise~void~
|
|
52
52
|
+stats() Promise~QueueStats~
|
|
53
53
|
}
|
|
54
|
+
class DBJobQueue~T~ {
|
|
55
|
+
+enqueue(type, payload, opts?) Promise~string~
|
|
56
|
+
+enqueueBatch(jobs) Promise~string[]~
|
|
57
|
+
+stats() Promise~QueueStats~
|
|
58
|
+
}
|
|
59
|
+
class DBQueueConsumer~T~ {
|
|
60
|
+
+register(type, handler) void
|
|
61
|
+
+start() Promise~void~
|
|
62
|
+
+stop() Promise~void~
|
|
63
|
+
+processOnce() Promise~number~
|
|
64
|
+
+stats() Promise~QueueStats~
|
|
65
|
+
}
|
|
54
66
|
class Job~T~ {
|
|
55
67
|
<<interface>>
|
|
56
68
|
}
|
|
@@ -141,6 +153,8 @@ classDiagram
|
|
|
141
153
|
SchedulerFactory --> CloudflareSchedulerAdapter
|
|
142
154
|
SchedulerFactory --> NoopSchedulerAdapter
|
|
143
155
|
EventBus --> JobQueue : "async dispatch"
|
|
156
|
+
DBJobQueue --> JobQueue : "implements"
|
|
157
|
+
DBQueueConsumer --> QueueConsumer : "implements"
|
|
144
158
|
EventBus --> Logger : "self-observability"
|
|
145
159
|
APIClient --> Tracing : "traceAsync"
|
|
146
160
|
APIClient --> Metrics : "counters + histograms"
|
|
@@ -182,7 +196,11 @@ bus.once('user.signed_up', () => console.log('one-time'));
|
|
|
182
196
|
**Lifecycle events** — inject a second `EventBus` to observe bus internals:
|
|
183
197
|
|
|
184
198
|
```ts
|
|
185
|
-
|
|
199
|
+
type LifecycleEvents = {
|
|
200
|
+
'bus.emit.done': (detail: { event: string; syncCount: number; asyncCount: number; emitDurationMs: number }) => void;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const lifecycleBus = new EventBus<LifecycleEvents>();
|
|
186
204
|
lifecycleBus.on('bus.emit.done', (detail) => {
|
|
187
205
|
// { event, syncCount, asyncCount, emitDurationMs, errors }
|
|
188
206
|
metrics.recordEmit(detail);
|
|
@@ -191,6 +209,45 @@ lifecycleBus.on('bus.emit.done', (detail) => {
|
|
|
191
209
|
const bus = new EventBus<AppEvents>({ lifecycleBus });
|
|
192
210
|
```
|
|
193
211
|
|
|
212
|
+
### Job Queue — DB-backed async work
|
|
213
|
+
|
|
214
|
+
`DBJobQueue` and `DBQueueConsumer` run over `@gobing-ai/ts-db`'s `QueueJobDao`. Use this when event handlers, schedulers, or API handlers need durable background work with retries.
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
import { createDbAdapter, QueueJobDao } from '@gobing-ai/ts-db';
|
|
218
|
+
import { DBJobQueue, DBQueueConsumer } from '@gobing-ai/ts-infra';
|
|
219
|
+
|
|
220
|
+
const db = await createDbAdapter({ driver: 'bun-sqlite', url: './jobs.db' });
|
|
221
|
+
const dao = new QueueJobDao(db);
|
|
222
|
+
|
|
223
|
+
const queue = new DBJobQueue<{ to: string; subject: string }>(dao);
|
|
224
|
+
await queue.enqueue(
|
|
225
|
+
'send-email',
|
|
226
|
+
{ to: 'alice@example.com', subject: 'Welcome' },
|
|
227
|
+
{ maxRetries: 5, delay: 1_000, ttlMs: 86_400_000 },
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
const consumer = new DBQueueConsumer<{ to: string; subject: string }>(dao, {
|
|
231
|
+
batchSize: 10,
|
|
232
|
+
maxConcurrency: 4,
|
|
233
|
+
visibilityTimeout: 30_000,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
consumer.register('send-email', async (job) => {
|
|
237
|
+
await sendEmail(job.payload.to, job.payload.subject);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
await consumer.start();
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
For scheduled drains and tests, call `processOnce()` instead of starting the polling loop:
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
const processed = await consumer.processOnce();
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
The consumer claims ready jobs, resets stuck processing jobs after the visibility timeout, retries failed jobs with exponential backoff, and marks expired jobs failed through `QueueJobDao`.
|
|
250
|
+
|
|
194
251
|
### Scheduler — cron-like actions
|
|
195
252
|
|
|
196
253
|
```ts
|
|
@@ -269,9 +326,7 @@ const reqLog = log.child({ requestId: 'req-123' });
|
|
|
269
326
|
reqLog.error('Validation failed', { field: 'email' });
|
|
270
327
|
// → {...,"category":"auth","requestId":"req-123","field":"email"}
|
|
271
328
|
|
|
272
|
-
//
|
|
273
|
-
import { setLoggerMuted } from './logger.js'; // internal import
|
|
274
|
-
setLoggerMuted(true);
|
|
329
|
+
// In tests, prefer injecting a logger boundary or initializing at a quiet level.
|
|
275
330
|
```
|
|
276
331
|
|
|
277
332
|
### Telemetry — OpenTelemetry
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export { EventBus, type EventMap, type SubscribeOptions } from './event-bus/inde
|
|
|
3
3
|
export type { AppEvents, AppInternalEvents } from './events/index';
|
|
4
4
|
export { createSystemBus } from './events/index';
|
|
5
5
|
export type { EnqueueOptions, Job, JobHandler, JobQueue, QueueConsumer, QueueConsumerConfig, QueueStats, } from './job-queue/index';
|
|
6
|
+
export { DBJobQueue, DBQueueConsumer } from './job-queue/index';
|
|
6
7
|
export { getLogger, initializeLogger, type Logger, type LogLevel } from './logger';
|
|
7
8
|
export { CloudflareSchedulerAdapter, getSchedulerAdapter, initScheduler, NodeSchedulerAdapter, NoopSchedulerAdapter, type ScheduledAction, type SchedulerAdapter, setSchedulerAdapter, } from './scheduler/index';
|
|
8
9
|
export { addSpanAttributes, addSpanEvent, extractSqlOperation, getActiveSpan, getDbOperationDuration, getDbOperationErrors, getDbOperationTotal, getEventbusEmitsTotal, getEventbusErrorsTotal, getHttpClientRequestDuration, getHttpClientRequestErrors, getHttpClientRequestTotal, getHttpServerRequestDuration, getHttpServerRequestErrors, getHttpServerRequestTotal, getQueueJobCompletedTotal, getQueueJobEnqueuedTotal, getQueueJobFailedTotal, getQueueJobProcessingDuration, getSchedulerJobDuration, getSchedulerJobExecutedTotal, getSchedulerJobFailedTotal, getTelemetryConfig, getTracer, initMetrics, initTelemetry, isTelemetryEnabled, sanitizeSql, shutdownMetrics, shutdownTelemetry, type TelemetryConfig, type TelemetryConfigPartial, traceAsync, traceSync, withSpan, } from './telemetry/index';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,KAAK,eAAe,EAAE,QAAQ,EAAE,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AAG9F,OAAO,EAAE,QAAQ,EAAE,KAAK,QAAQ,EAAE,KAAK,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAGnF,YAAY,EAAE,SAAS,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACnE,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,KAAK,eAAe,EAAE,QAAQ,EAAE,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AAG9F,OAAO,EAAE,QAAQ,EAAE,KAAK,QAAQ,EAAE,KAAK,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAGnF,YAAY,EAAE,SAAS,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACnE,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACjD,YAAY,EACR,cAAc,EACd,GAAG,EACH,UAAU,EACV,QAAQ,EACR,aAAa,EACb,mBAAmB,EACnB,UAAU,GACb,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAGhE,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,KAAK,MAAM,EAAE,KAAK,QAAQ,EAAE,MAAM,UAAU,CAAC;AAGnF,OAAO,EACH,0BAA0B,EAC1B,mBAAmB,EACnB,aAAa,EACb,oBAAoB,EACpB,oBAAoB,EACpB,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,mBAAmB,GACtB,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EACH,iBAAiB,EACjB,YAAY,EACZ,mBAAmB,EACnB,aAAa,EACb,sBAAsB,EACtB,oBAAoB,EACpB,mBAAmB,EACnB,qBAAqB,EACrB,sBAAsB,EACtB,4BAA4B,EAC5B,0BAA0B,EAC1B,yBAAyB,EACzB,4BAA4B,EAC5B,0BAA0B,EAC1B,yBAAyB,EACzB,yBAAyB,EACzB,wBAAwB,EACxB,sBAAsB,EACtB,6BAA6B,EAC7B,uBAAuB,EACvB,4BAA4B,EAC5B,0BAA0B,EAC1B,kBAAkB,EAClB,SAAS,EACT,WAAW,EACX,aAAa,EACb,kBAAkB,EAClB,WAAW,EACX,eAAe,EACf,iBAAiB,EACjB,KAAK,eAAe,EACpB,KAAK,sBAAsB,EAC3B,UAAU,EACV,SAAS,EACT,QAAQ,GACX,MAAM,mBAAmB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,8 @@ export { APIClient, APIError } from './api-client.js';
|
|
|
3
3
|
// Event Bus
|
|
4
4
|
export { EventBus } from './event-bus/index.js';
|
|
5
5
|
export { createSystemBus } from './events/index.js';
|
|
6
|
+
// Job Queue
|
|
7
|
+
export { DBJobQueue, DBQueueConsumer } from './job-queue/index.js';
|
|
6
8
|
// Logger
|
|
7
9
|
export { getLogger, initializeLogger } from './logger.js';
|
|
8
10
|
// Scheduler
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { QueueJobDao, QueueStats } from '@gobing-ai/ts-db';
|
|
2
|
+
import type { EnqueueOptions, JobHandler, JobQueue, QueueConsumer, QueueConsumerConfig } from './types';
|
|
3
|
+
/** DB-backed job queue implementation over `@gobing-ai/ts-db`'s `QueueJobDao`. */
|
|
4
|
+
export declare class DBJobQueue<T = unknown> implements JobQueue<T> {
|
|
5
|
+
readonly dao: QueueJobDao;
|
|
6
|
+
constructor(dao: QueueJobDao);
|
|
7
|
+
enqueue(type: string, payload: T, options?: EnqueueOptions): Promise<string>;
|
|
8
|
+
enqueueBatch(jobs: Array<{
|
|
9
|
+
type: string;
|
|
10
|
+
payload: T;
|
|
11
|
+
} & EnqueueOptions>): Promise<string[]>;
|
|
12
|
+
stats(): Promise<QueueStats>;
|
|
13
|
+
}
|
|
14
|
+
/** DB-backed queue consumer with polling, retry, and visibility-timeout handling. */
|
|
15
|
+
export declare class DBQueueConsumer<T = unknown> implements QueueConsumer<T> {
|
|
16
|
+
private readonly dao;
|
|
17
|
+
private readonly handlers;
|
|
18
|
+
private readonly pollInterval;
|
|
19
|
+
private readonly batchSize;
|
|
20
|
+
private readonly maxConcurrency;
|
|
21
|
+
private readonly visibilityTimeout;
|
|
22
|
+
private readonly baseDelay;
|
|
23
|
+
private readonly maxDelay;
|
|
24
|
+
private readonly drainTimeoutMs;
|
|
25
|
+
private timer;
|
|
26
|
+
private running;
|
|
27
|
+
private inFlight;
|
|
28
|
+
constructor(dao: QueueJobDao, config?: QueueConsumerConfig);
|
|
29
|
+
register(type: string, handler: JobHandler<T>): void;
|
|
30
|
+
start(): Promise<void>;
|
|
31
|
+
stop(): Promise<void>;
|
|
32
|
+
stats(): Promise<QueueStats>;
|
|
33
|
+
/** Process one batch immediately. Useful for tests, schedulers, and manual drains. */
|
|
34
|
+
processOnce(): Promise<number>;
|
|
35
|
+
private schedule;
|
|
36
|
+
private poll;
|
|
37
|
+
private processJob;
|
|
38
|
+
private failOrRetry;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=db-job-queue.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"db-job-queue.d.ts","sourceRoot":"","sources":["../../src/job-queue/db-job-queue.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAkB,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAChF,OAAO,KAAK,EAAE,cAAc,EAAO,UAAU,EAAE,QAAQ,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAE7G,kFAAkF;AAClF,qBAAa,UAAU,CAAC,CAAC,GAAG,OAAO,CAAE,YAAW,QAAQ,CAAC,CAAC,CAAC;IAC3C,QAAQ,CAAC,GAAG,EAAE,WAAW;gBAAhB,GAAG,EAAE,WAAW;IAE/B,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC;IAI5E,YAAY,CAAC,IAAI,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,CAAC,CAAA;KAAE,GAAG,cAAc,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAI3F,KAAK,IAAI,OAAO,CAAC,UAAU,CAAC;CAGrC;AAED,qFAAqF;AACrF,qBAAa,eAAe,CAAC,CAAC,GAAG,OAAO,CAAE,YAAW,aAAa,CAAC,CAAC,CAAC;IAc7D,OAAO,CAAC,QAAQ,CAAC,GAAG;IAbxB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAoC;IAC7D,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAS;IAC3C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,KAAK,CAA8C;IAC3D,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,QAAQ,CAAK;gBAGA,GAAG,EAAE,WAAW,EACjC,MAAM,GAAE,mBAAwB;IAWpC,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,IAAI;IAI9C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAMtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAarB,KAAK,IAAI,OAAO,CAAC,UAAU,CAAC;IAIlC,sFAAsF;IAChF,WAAW,IAAI,OAAO,CAAC,MAAM,CAAC;IAyBpC,OAAO,CAAC,QAAQ;YAMF,IAAI;YASJ,UAAU;YAgBV,WAAW;CAW5B"}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/** DB-backed job queue implementation over `@gobing-ai/ts-db`'s `QueueJobDao`. */
|
|
2
|
+
export class DBJobQueue {
|
|
3
|
+
dao;
|
|
4
|
+
constructor(dao) {
|
|
5
|
+
this.dao = dao;
|
|
6
|
+
}
|
|
7
|
+
async enqueue(type, payload, options) {
|
|
8
|
+
return this.dao.enqueue(type, payload, options);
|
|
9
|
+
}
|
|
10
|
+
async enqueueBatch(jobs) {
|
|
11
|
+
return this.dao.enqueueBatch(jobs);
|
|
12
|
+
}
|
|
13
|
+
async stats() {
|
|
14
|
+
return this.dao.getStats();
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** DB-backed queue consumer with polling, retry, and visibility-timeout handling. */
|
|
18
|
+
export class DBQueueConsumer {
|
|
19
|
+
dao;
|
|
20
|
+
handlers = new Map();
|
|
21
|
+
pollInterval;
|
|
22
|
+
batchSize;
|
|
23
|
+
maxConcurrency;
|
|
24
|
+
visibilityTimeout;
|
|
25
|
+
baseDelay;
|
|
26
|
+
maxDelay;
|
|
27
|
+
drainTimeoutMs;
|
|
28
|
+
timer = null;
|
|
29
|
+
running = false;
|
|
30
|
+
inFlight = 0;
|
|
31
|
+
constructor(dao, config = {}) {
|
|
32
|
+
this.dao = dao;
|
|
33
|
+
this.pollInterval = config.pollInterval ?? 1_000;
|
|
34
|
+
this.batchSize = config.batchSize ?? 10;
|
|
35
|
+
this.maxConcurrency = config.maxConcurrency ?? this.batchSize;
|
|
36
|
+
this.visibilityTimeout = config.visibilityTimeout ?? 30_000;
|
|
37
|
+
this.baseDelay = config.baseDelay ?? 1_000;
|
|
38
|
+
this.maxDelay = config.maxDelay ?? 60_000;
|
|
39
|
+
this.drainTimeoutMs = config.drainTimeoutMs ?? 30_000;
|
|
40
|
+
}
|
|
41
|
+
register(type, handler) {
|
|
42
|
+
this.handlers.set(type, handler);
|
|
43
|
+
}
|
|
44
|
+
async start() {
|
|
45
|
+
if (this.running)
|
|
46
|
+
return;
|
|
47
|
+
this.running = true;
|
|
48
|
+
this.schedule(0);
|
|
49
|
+
}
|
|
50
|
+
async stop() {
|
|
51
|
+
this.running = false;
|
|
52
|
+
if (this.timer !== null) {
|
|
53
|
+
clearTimeout(this.timer);
|
|
54
|
+
this.timer = null;
|
|
55
|
+
}
|
|
56
|
+
const deadline = Date.now() + this.drainTimeoutMs;
|
|
57
|
+
while (this.inFlight > 0 && Date.now() < deadline) {
|
|
58
|
+
await sleep(10);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async stats() {
|
|
62
|
+
return this.dao.getStats();
|
|
63
|
+
}
|
|
64
|
+
/** Process one batch immediately. Useful for tests, schedulers, and manual drains. */
|
|
65
|
+
async processOnce() {
|
|
66
|
+
await this.dao.resetStuckJobs(this.visibilityTimeout);
|
|
67
|
+
await this.dao.failExpiredJobs();
|
|
68
|
+
const jobs = await this.dao.claimReady(this.batchSize);
|
|
69
|
+
let processed = 0;
|
|
70
|
+
for (let index = 0; index < jobs.length; index += this.maxConcurrency) {
|
|
71
|
+
const batch = jobs.slice(index, index + this.maxConcurrency);
|
|
72
|
+
await Promise.all(batch.map(async (job) => {
|
|
73
|
+
this.inFlight += 1;
|
|
74
|
+
try {
|
|
75
|
+
await this.processJob(job);
|
|
76
|
+
processed += 1;
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
this.inFlight -= 1;
|
|
80
|
+
}
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
return processed;
|
|
84
|
+
}
|
|
85
|
+
schedule(delay) {
|
|
86
|
+
this.timer = setTimeout(() => {
|
|
87
|
+
void this.poll();
|
|
88
|
+
}, delay);
|
|
89
|
+
}
|
|
90
|
+
async poll() {
|
|
91
|
+
if (!this.running)
|
|
92
|
+
return;
|
|
93
|
+
try {
|
|
94
|
+
await this.processOnce();
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
if (this.running)
|
|
98
|
+
this.schedule(this.pollInterval);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async processJob(record) {
|
|
102
|
+
const job = toJob(record);
|
|
103
|
+
const handler = this.handlers.get(job.type);
|
|
104
|
+
if (handler === undefined) {
|
|
105
|
+
await this.failOrRetry(job, new Error(`No handler registered for job type "${job.type}"`));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
await handler(job);
|
|
110
|
+
await this.dao.markCompleted(job.id);
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
await this.failOrRetry(job, error);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async failOrRetry(job, error) {
|
|
117
|
+
const attempts = job.attempts + 1;
|
|
118
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
119
|
+
if (attempts >= job.maxRetries) {
|
|
120
|
+
await this.dao.markFailed(job.id, attempts, message);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const delay = Math.min(this.maxDelay, this.baseDelay * 2 ** Math.max(0, attempts - 1));
|
|
124
|
+
await this.dao.markForRetry(job.id, attempts, message, Date.now() + delay);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function toJob(record) {
|
|
128
|
+
return {
|
|
129
|
+
id: record.id,
|
|
130
|
+
type: record.type,
|
|
131
|
+
payload: JSON.parse(record.payload),
|
|
132
|
+
status: record.status,
|
|
133
|
+
attempts: record.attempts,
|
|
134
|
+
maxRetries: record.maxRetries,
|
|
135
|
+
createdAt: record.createdAt,
|
|
136
|
+
updatedAt: record.updatedAt,
|
|
137
|
+
nextRetryAt: record.nextRetryAt,
|
|
138
|
+
lastError: record.lastError,
|
|
139
|
+
processingAt: record.processingAt,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function sleep(ms) {
|
|
143
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
144
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/job-queue/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACR,cAAc,EACd,GAAG,EACH,UAAU,EACV,QAAQ,EACR,aAAa,EACb,mBAAmB,EACnB,UAAU,GACb,MAAM,SAAS,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/job-queue/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAC7D,YAAY,EACR,cAAc,EACd,GAAG,EACH,UAAU,EACV,QAAQ,EACR,aAAa,EACb,mBAAmB,EACnB,UAAU,GACb,MAAM,SAAS,CAAC"}
|
package/dist/job-queue/index.js
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DBJobQueue, DBQueueConsumer } from './db-job-queue.js';
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Job queue types for async work processing with retry.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* are provided here so `EventBus` and other infra components can reference
|
|
7
|
-
* the queue contract without a circular dependency on the DB layer.
|
|
4
|
+
* The concrete DB-backed implementations live beside these interfaces in
|
|
5
|
+
* `DBJobQueue` and `DBQueueConsumer`.
|
|
8
6
|
*/
|
|
9
7
|
export interface Job<T = unknown> {
|
|
10
8
|
id: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/job-queue/types.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/job-queue/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,WAAW,GAAG,CAAC,CAAC,GAAG,OAAO;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,CAAC,CAAC;IACX,MAAM,EAAE,SAAS,GAAG,YAAY,GAAG,WAAW,GAAG,QAAQ,CAAC;IAC1D,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED,MAAM,WAAW,cAAc;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,QAAQ,CAAC,CAAC,GAAG,OAAO;IACjC,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC7E,YAAY,CAAC,IAAI,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,CAAC,CAAA;KAAE,GAAG,cAAc,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;CAC/F;AAED,MAAM,MAAM,UAAU,CAAC,CAAC,GAAG,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAErE,MAAM,WAAW,UAAU;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,mBAAmB;IAChC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,aAAa,CAAC,CAAC,GAAG,OAAO;IACtC,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;IACrD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,KAAK,IAAI,OAAO,CAAC,UAAU,CAAC,CAAC;CAChC"}
|
package/dist/job-queue/types.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Job queue types for async work processing with retry.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* are provided here so `EventBus` and other infra components can reference
|
|
7
|
-
* the queue contract without a circular dependency on the DB layer.
|
|
4
|
+
* The concrete DB-backed implementations live beside these interfaces in
|
|
5
|
+
* `DBJobQueue` and `DBQueueConsumer`.
|
|
8
6
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gobing-ai/ts-infra",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.8",
|
|
4
4
|
"description": "@gobing-ai/ts-infra — Infrastructure backbone: event bus, job queue, scheduler, telemetry, API client, and logging.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript",
|
|
@@ -50,8 +50,8 @@
|
|
|
50
50
|
"release": "echo 'Manual publish is disabled. Releases go through GitHub Actions via Trusted Publishing — push a tag: git tag @gobing-ai/ts-infra-v<version> && git push --tags' && exit 1"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@gobing-ai/ts-db": "^0.2.
|
|
54
|
-
"@gobing-ai/ts-runtime": "^0.2.
|
|
53
|
+
"@gobing-ai/ts-db": "^0.2.8",
|
|
54
|
+
"@gobing-ai/ts-runtime": "^0.2.8"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
57
|
"@opentelemetry/api": "^1.9.0"
|
package/src/index.ts
CHANGED
|
@@ -7,8 +7,6 @@ export { EventBus, type EventMap, type SubscribeOptions } from './event-bus/inde
|
|
|
7
7
|
// Events
|
|
8
8
|
export type { AppEvents, AppInternalEvents } from './events/index';
|
|
9
9
|
export { createSystemBus } from './events/index';
|
|
10
|
-
|
|
11
|
-
// Job Queue
|
|
12
10
|
export type {
|
|
13
11
|
EnqueueOptions,
|
|
14
12
|
Job,
|
|
@@ -18,6 +16,8 @@ export type {
|
|
|
18
16
|
QueueConsumerConfig,
|
|
19
17
|
QueueStats,
|
|
20
18
|
} from './job-queue/index';
|
|
19
|
+
// Job Queue
|
|
20
|
+
export { DBJobQueue, DBQueueConsumer } from './job-queue/index';
|
|
21
21
|
|
|
22
22
|
// Logger
|
|
23
23
|
export { getLogger, initializeLogger, type Logger, type LogLevel } from './logger';
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type { QueueJobDao, QueueJobRecord, QueueStats } from '@gobing-ai/ts-db';
|
|
2
|
+
import type { EnqueueOptions, Job, JobHandler, JobQueue, QueueConsumer, QueueConsumerConfig } from './types';
|
|
3
|
+
|
|
4
|
+
/** DB-backed job queue implementation over `@gobing-ai/ts-db`'s `QueueJobDao`. */
|
|
5
|
+
export class DBJobQueue<T = unknown> implements JobQueue<T> {
|
|
6
|
+
constructor(readonly dao: QueueJobDao) {}
|
|
7
|
+
|
|
8
|
+
async enqueue(type: string, payload: T, options?: EnqueueOptions): Promise<string> {
|
|
9
|
+
return this.dao.enqueue(type, payload, options);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async enqueueBatch(jobs: Array<{ type: string; payload: T } & EnqueueOptions>): Promise<string[]> {
|
|
13
|
+
return this.dao.enqueueBatch(jobs);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async stats(): Promise<QueueStats> {
|
|
17
|
+
return this.dao.getStats();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** DB-backed queue consumer with polling, retry, and visibility-timeout handling. */
|
|
22
|
+
export class DBQueueConsumer<T = unknown> implements QueueConsumer<T> {
|
|
23
|
+
private readonly handlers = new Map<string, JobHandler<T>>();
|
|
24
|
+
private readonly pollInterval: number;
|
|
25
|
+
private readonly batchSize: number;
|
|
26
|
+
private readonly maxConcurrency: number;
|
|
27
|
+
private readonly visibilityTimeout: number;
|
|
28
|
+
private readonly baseDelay: number;
|
|
29
|
+
private readonly maxDelay: number;
|
|
30
|
+
private readonly drainTimeoutMs: number;
|
|
31
|
+
private timer: ReturnType<typeof setTimeout> | null = null;
|
|
32
|
+
private running = false;
|
|
33
|
+
private inFlight = 0;
|
|
34
|
+
|
|
35
|
+
constructor(
|
|
36
|
+
private readonly dao: QueueJobDao,
|
|
37
|
+
config: QueueConsumerConfig = {},
|
|
38
|
+
) {
|
|
39
|
+
this.pollInterval = config.pollInterval ?? 1_000;
|
|
40
|
+
this.batchSize = config.batchSize ?? 10;
|
|
41
|
+
this.maxConcurrency = config.maxConcurrency ?? this.batchSize;
|
|
42
|
+
this.visibilityTimeout = config.visibilityTimeout ?? 30_000;
|
|
43
|
+
this.baseDelay = config.baseDelay ?? 1_000;
|
|
44
|
+
this.maxDelay = config.maxDelay ?? 60_000;
|
|
45
|
+
this.drainTimeoutMs = config.drainTimeoutMs ?? 30_000;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
register(type: string, handler: JobHandler<T>): void {
|
|
49
|
+
this.handlers.set(type, handler);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async start(): Promise<void> {
|
|
53
|
+
if (this.running) return;
|
|
54
|
+
this.running = true;
|
|
55
|
+
this.schedule(0);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async stop(): Promise<void> {
|
|
59
|
+
this.running = false;
|
|
60
|
+
if (this.timer !== null) {
|
|
61
|
+
clearTimeout(this.timer);
|
|
62
|
+
this.timer = null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const deadline = Date.now() + this.drainTimeoutMs;
|
|
66
|
+
while (this.inFlight > 0 && Date.now() < deadline) {
|
|
67
|
+
await sleep(10);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async stats(): Promise<QueueStats> {
|
|
72
|
+
return this.dao.getStats();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Process one batch immediately. Useful for tests, schedulers, and manual drains. */
|
|
76
|
+
async processOnce(): Promise<number> {
|
|
77
|
+
await this.dao.resetStuckJobs(this.visibilityTimeout);
|
|
78
|
+
await this.dao.failExpiredJobs();
|
|
79
|
+
|
|
80
|
+
const jobs = await this.dao.claimReady(this.batchSize);
|
|
81
|
+
let processed = 0;
|
|
82
|
+
|
|
83
|
+
for (let index = 0; index < jobs.length; index += this.maxConcurrency) {
|
|
84
|
+
const batch = jobs.slice(index, index + this.maxConcurrency);
|
|
85
|
+
await Promise.all(
|
|
86
|
+
batch.map(async (job) => {
|
|
87
|
+
this.inFlight += 1;
|
|
88
|
+
try {
|
|
89
|
+
await this.processJob(job);
|
|
90
|
+
processed += 1;
|
|
91
|
+
} finally {
|
|
92
|
+
this.inFlight -= 1;
|
|
93
|
+
}
|
|
94
|
+
}),
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return processed;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private schedule(delay: number): void {
|
|
102
|
+
this.timer = setTimeout(() => {
|
|
103
|
+
void this.poll();
|
|
104
|
+
}, delay);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private async poll(): Promise<void> {
|
|
108
|
+
if (!this.running) return;
|
|
109
|
+
try {
|
|
110
|
+
await this.processOnce();
|
|
111
|
+
} finally {
|
|
112
|
+
if (this.running) this.schedule(this.pollInterval);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private async processJob(record: QueueJobRecord): Promise<void> {
|
|
117
|
+
const job = toJob<T>(record);
|
|
118
|
+
const handler = this.handlers.get(job.type);
|
|
119
|
+
if (handler === undefined) {
|
|
120
|
+
await this.failOrRetry(job, new Error(`No handler registered for job type "${job.type}"`));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
await handler(job);
|
|
126
|
+
await this.dao.markCompleted(job.id);
|
|
127
|
+
} catch (error) {
|
|
128
|
+
await this.failOrRetry(job, error);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private async failOrRetry(job: Job<T>, error: unknown): Promise<void> {
|
|
133
|
+
const attempts = job.attempts + 1;
|
|
134
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
135
|
+
if (attempts >= job.maxRetries) {
|
|
136
|
+
await this.dao.markFailed(job.id, attempts, message);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const delay = Math.min(this.maxDelay, this.baseDelay * 2 ** Math.max(0, attempts - 1));
|
|
141
|
+
await this.dao.markForRetry(job.id, attempts, message, Date.now() + delay);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function toJob<T>(record: QueueJobRecord): Job<T> {
|
|
146
|
+
return {
|
|
147
|
+
id: record.id,
|
|
148
|
+
type: record.type,
|
|
149
|
+
payload: JSON.parse(record.payload) as T,
|
|
150
|
+
status: record.status as Job<T>['status'],
|
|
151
|
+
attempts: record.attempts,
|
|
152
|
+
maxRetries: record.maxRetries,
|
|
153
|
+
createdAt: record.createdAt,
|
|
154
|
+
updatedAt: record.updatedAt,
|
|
155
|
+
nextRetryAt: record.nextRetryAt,
|
|
156
|
+
lastError: record.lastError,
|
|
157
|
+
processingAt: record.processingAt,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function sleep(ms: number): Promise<void> {
|
|
162
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
163
|
+
}
|
package/src/job-queue/index.ts
CHANGED
package/src/job-queue/types.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Job queue types for async work processing with retry.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* are provided here so `EventBus` and other infra components can reference
|
|
7
|
-
* the queue contract without a circular dependency on the DB layer.
|
|
4
|
+
* The concrete DB-backed implementations live beside these interfaces in
|
|
5
|
+
* `DBJobQueue` and `DBQueueConsumer`.
|
|
8
6
|
*/
|
|
9
7
|
|
|
10
8
|
export interface Job<T = unknown> {
|