@gobing-ai/ts-infra 0.4.13 → 0.4.15
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 +4 -0
- package/dist/application-node.d.ts.map +1 -1
- package/dist/application-node.js +10 -3
- package/dist/events.d.ts +78 -25
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +12 -8
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/internals/drain.d.ts +18 -0
- package/dist/internals/drain.d.ts.map +1 -0
- package/dist/internals/drain.js +29 -0
- package/dist/job-queue/db-job-queue.d.ts +1 -0
- package/dist/job-queue/db-job-queue.d.ts.map +1 -1
- package/dist/job-queue/db-job-queue.js +79 -18
- package/dist/job-queue/types.d.ts +13 -3
- package/dist/job-queue/types.d.ts.map +1 -1
- package/dist/scheduler/cloudflare.d.ts +7 -0
- package/dist/scheduler/cloudflare.d.ts.map +1 -1
- package/dist/scheduler/cloudflare.js +7 -0
- package/dist/scheduler/node.d.ts +20 -1
- package/dist/scheduler/node.d.ts.map +1 -1
- package/dist/scheduler/node.js +33 -2
- package/dist/scheduler/types.d.ts +7 -1
- package/dist/scheduler/types.d.ts.map +1 -1
- package/dist/scheduler-node.d.ts +1 -1
- package/dist/scheduler-node.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/application-node.ts +10 -3
- package/src/events.ts +83 -19
- package/src/index.ts +5 -0
- package/src/internals/drain.ts +29 -0
- package/src/job-queue/db-job-queue.ts +86 -17
- package/src/job-queue/types.ts +13 -3
- package/src/scheduler/cloudflare.ts +7 -0
- package/src/scheduler/node.ts +47 -2
- package/src/scheduler/types.ts +7 -1
- package/src/scheduler-node.ts +1 -1
package/README.md
CHANGED
|
@@ -247,6 +247,8 @@ const queue = new DBJobQueue<{ to: string; subject: string }>(dao);
|
|
|
247
247
|
await queue.enqueue(
|
|
248
248
|
'send-email',
|
|
249
249
|
{ to: 'alice@example.com', subject: 'Welcome' },
|
|
250
|
+
// maxRetries is a total attempt budget, not retries after the first:
|
|
251
|
+
// 5 means up to 5 attempts, and `maxRetries: 1` runs the job once with no retry.
|
|
250
252
|
{ maxRetries: 5, delay: 1_000, ttlMs: 86_400_000 },
|
|
251
253
|
);
|
|
252
254
|
|
|
@@ -271,6 +273,8 @@ const processed = await consumer.processOnce();
|
|
|
271
273
|
|
|
272
274
|
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`. Corrupt payloads are failed individually without rejecting the batch. Poll-cycle errors are logged and retried on the next cycle — a single DAO hiccup will not crash the process.
|
|
273
275
|
|
|
276
|
+
`stop()` drains gracefully: it stops polling, then waits for work already in flight — including a poll cycle that has claimed nothing yet — before resolving. The wait is capped by `drainTimeoutMs` (default 30s) so a stuck handler cannot block shutdown; when the cap is hit, `queue.consumer.stopped` reports `drained: false` with the outstanding `inFlightAtStop` count.
|
|
277
|
+
|
|
274
278
|
**Queue lifecycle events** are opt-in through the injected `EventBus`:
|
|
275
279
|
|
|
276
280
|
```ts
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"application-node.d.ts","sourceRoot":"","sources":["../src/application-node.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAWH,OAAO,KAAK,EACR,2BAA2B,EAC3B,uBAAuB,EAEvB,kBAAkB,EAClB,qBAAqB,EAGrB,QAAQ,EACR,aAAa,EACb,WAAW,EACX,cAAc,EACd,gBAAgB,EAChB,gBAAgB,EACnB,MAAM,qBAAqB,CAAC;AAM7B,kGAAkG;AAClG,qBAAa,qBAAsB,SAAQ,KAAK;gBAChC,OAAO,EAAE,MAAM;CAI9B;AAgHD;;;;GAIG;AACH,MAAM,WAAW,6BAA8B,SAAQ,gBAAgB;IACnE,uEAAuE;IACvE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,qEAAqE;IACrE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACpC;AAED,uEAAuE;AACvE,MAAM,WAAW,sBAAsB,CAAC,UAAU,GAAG,OAAO,EAAE,OAAO,SAAS,QAAQ,GAAG,WAAW;IAChG,gEAAgE;IAChE,QAAQ,CAAC,YAAY,CAAC,EAAE,uBAAuB,CAAC,UAAU,CAAC,CAAC;IAC5D,8DAA8D;IAC9D,QAAQ,CAAC,MAAM,CAAC,EAAE;QACd,OAAO,CAAC,EAAE,cAAc,CAAC;QACzB,MAAM,CAAC,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC;QAChC,SAAS,CAAC,EAAE,6BAA6B,CAAC;QAC1C,SAAS,CAAC,EAAE,gBAAgB,CAAC;KAChC,CAAC;IACF,oCAAoC;IACpC,QAAQ,CAAC,QAAQ,CAAC,EAAE,2BAA2B,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,UAAU,CAAC,CAAC;IACjF,wCAAwC;IACxC,QAAQ,CAAC,KAAK,EAAE,CAAC,GAAG,EAAE,kBAAkB,CAAC,UAAU,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACvF,wDAAwD;IACxD,QAAQ,CAAC,IAAI,CAAC,EAAE,CACZ,GAAG,EAAE,kBAAkB,CAAC,UAAU,EAAE,OAAO,CAAC,EAC5C,MAAM,EAAE,qBAAqB,KAC5B,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CAC7B;AAID;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAsB,kBAAkB,CAAC,UAAU,GAAG,OAAO,EAAE,OAAO,SAAS,QAAQ,GAAG,WAAW,EACjG,OAAO,EAAE,sBAAsB,CAAC,UAAU,EAAE,OAAO,CAAC,GACrD,OAAO,CAAC,kBAAkB,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,
|
|
1
|
+
{"version":3,"file":"application-node.d.ts","sourceRoot":"","sources":["../src/application-node.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAWH,OAAO,KAAK,EACR,2BAA2B,EAC3B,uBAAuB,EAEvB,kBAAkB,EAClB,qBAAqB,EAGrB,QAAQ,EACR,aAAa,EACb,WAAW,EACX,cAAc,EACd,gBAAgB,EAChB,gBAAgB,EACnB,MAAM,qBAAqB,CAAC;AAM7B,kGAAkG;AAClG,qBAAa,qBAAsB,SAAQ,KAAK;gBAChC,OAAO,EAAE,MAAM;CAI9B;AAgHD;;;;GAIG;AACH,MAAM,WAAW,6BAA8B,SAAQ,gBAAgB;IACnE,uEAAuE;IACvE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,qEAAqE;IACrE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACpC;AAED,uEAAuE;AACvE,MAAM,WAAW,sBAAsB,CAAC,UAAU,GAAG,OAAO,EAAE,OAAO,SAAS,QAAQ,GAAG,WAAW;IAChG,gEAAgE;IAChE,QAAQ,CAAC,YAAY,CAAC,EAAE,uBAAuB,CAAC,UAAU,CAAC,CAAC;IAC5D,8DAA8D;IAC9D,QAAQ,CAAC,MAAM,CAAC,EAAE;QACd,OAAO,CAAC,EAAE,cAAc,CAAC;QACzB,MAAM,CAAC,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC;QAChC,SAAS,CAAC,EAAE,6BAA6B,CAAC;QAC1C,SAAS,CAAC,EAAE,gBAAgB,CAAC;KAChC,CAAC;IACF,oCAAoC;IACpC,QAAQ,CAAC,QAAQ,CAAC,EAAE,2BAA2B,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,UAAU,CAAC,CAAC;IACjF,wCAAwC;IACxC,QAAQ,CAAC,KAAK,EAAE,CAAC,GAAG,EAAE,kBAAkB,CAAC,UAAU,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACvF,wDAAwD;IACxD,QAAQ,CAAC,IAAI,CAAC,EAAE,CACZ,GAAG,EAAE,kBAAkB,CAAC,UAAU,EAAE,OAAO,CAAC,EAC5C,MAAM,EAAE,qBAAqB,KAC5B,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CAC7B;AAID;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAsB,kBAAkB,CAAC,UAAU,GAAG,OAAO,EAAE,OAAO,SAAS,QAAQ,GAAG,WAAW,EACjG,OAAO,EAAE,sBAAsB,CAAC,UAAU,EAAE,OAAO,CAAC,GACrD,OAAO,CAAC,kBAAkB,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,CAuIlD"}
|
package/dist/application-node.js
CHANGED
|
@@ -169,12 +169,19 @@ export async function runNodeApplication(options) {
|
|
|
169
169
|
const logFilePath = yamlBootstrap.logging?.filePath;
|
|
170
170
|
const loggingConfig = typeof logFilePath === 'string' ? { ...loggingOpts, fileSink: createFileSink(logFilePath) } : loggingOpts;
|
|
171
171
|
// ── Scheduler adapter ───────────────────────────────────────────────
|
|
172
|
+
// Honour a caller-supplied `SchedulerOptions.adapter` (documented injection
|
|
173
|
+
// point, application/types.ts:82) instead of unconditionally overwriting
|
|
174
|
+
// it. Only default-construct a NodeSchedulerAdapter when none was provided.
|
|
175
|
+
// `drainTimeoutMs` (ADR-024) is reachable by passing a pre-built adapter;
|
|
176
|
+
// the auto-wired default keeps the 30000 ms bound (CHANGELOG.md:16).
|
|
172
177
|
const schedulerConfig = {};
|
|
173
|
-
|
|
174
|
-
if (rawSched.enabled === true) {
|
|
178
|
+
if (schedulerOpts.enabled === true) {
|
|
175
179
|
schedulerConfig.enabled = true;
|
|
176
180
|
schedulerConfig.autoStart = schedulerOpts.autoStart;
|
|
177
|
-
schedulerConfig.adapter = new NodeSchedulerAdapter();
|
|
181
|
+
schedulerConfig.adapter = schedulerOpts.adapter ?? new NodeSchedulerAdapter();
|
|
182
|
+
if (schedulerOpts.entries) {
|
|
183
|
+
schedulerConfig.entries = schedulerOpts.entries;
|
|
184
|
+
}
|
|
178
185
|
}
|
|
179
186
|
// ── Node-owned plugins ──────────────────────────────────────────────
|
|
180
187
|
const plugins = [];
|
package/dist/events.d.ts
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Infrastructure-level event definitions for ts-infra observability.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* (
|
|
8
|
-
*
|
|
9
|
-
* (
|
|
10
|
-
*
|
|
11
|
-
*
|
|
4
|
+
* These maps define the infra-level event *contract* for the consumers of
|
|
5
|
+
* `@gobing-ai/ts-infra`. ts-infra emits `queue.*` (DBJobQueue / DBQueueConsumer),
|
|
6
|
+
* `scheduler.job.executed` (`wrapScheduledHandler`), and `db.connection.error`
|
|
7
|
+
* (DB wiring). Application-domain events (history import, HTTP server, …) belong
|
|
8
|
+
* to the consuming app, not this library. Process events belong to
|
|
9
|
+
* `@gobing-ai/ts-runtime` (the owner of `ProcessExecutor`) and are intentionally
|
|
10
|
+
* not re-exported here.
|
|
11
|
+
*
|
|
12
|
+
* **Metadata-only invariant:** every `queue.*` detail object is correlator-grade —
|
|
13
|
+
* it carries job identity, timing, and retry counters only, and NEVER embeds the
|
|
14
|
+
* job business payload `T` (which may contain prompts, tokens, PII, or large
|
|
15
|
+
* blobs). Inspect job bodies via the Jobs DAO when needed.
|
|
12
16
|
*
|
|
13
17
|
* Consume via `EventBus<InfraEvents>` or compose individual maps into a wider
|
|
14
18
|
* app event map.
|
|
@@ -21,21 +25,76 @@ export interface DbConnectionErrorDetail {
|
|
|
21
25
|
/** Adapter type (e.g. 'sqlite', 'd1'). */
|
|
22
26
|
adapter: string;
|
|
23
27
|
}
|
|
24
|
-
/**
|
|
25
|
-
export interface
|
|
28
|
+
/** Shared job identity correlators present on every `queue.job.*` event. */
|
|
29
|
+
export interface QueueJobRef {
|
|
30
|
+
/** Job id. */
|
|
26
31
|
jobId: string;
|
|
32
|
+
/** Job type (the handler registration key). */
|
|
27
33
|
type: string;
|
|
34
|
+
}
|
|
35
|
+
/** Payload for `queue.job.enqueued` — a new job was added to the queue. */
|
|
36
|
+
export interface QueueJobEnqueuedDetail extends QueueJobRef {
|
|
37
|
+
/** Epoch ms when the job was enqueued. */
|
|
38
|
+
enqueuedAt: number;
|
|
39
|
+
/** Max retry count from `EnqueueOptions.maxRetries`, when supplied. */
|
|
40
|
+
maxRetries?: number;
|
|
41
|
+
/** Requested delay before the job becomes ready (ms), from `EnqueueOptions.delay`. */
|
|
42
|
+
delayMs?: number;
|
|
43
|
+
/** Job TTL in ms, from `EnqueueOptions.ttlMs`. */
|
|
44
|
+
ttlMs?: number;
|
|
45
|
+
}
|
|
46
|
+
/** Payload for `queue.consumer.started` — the polling loop began. */
|
|
47
|
+
export interface QueueConsumerStartedDetail {
|
|
48
|
+
/** Epoch ms when the consumer was started. */
|
|
49
|
+
startedAt: number;
|
|
50
|
+
/** Polling interval in ms. */
|
|
51
|
+
pollInterval: number;
|
|
52
|
+
/** Claim batch size per poll cycle. */
|
|
53
|
+
batchSize: number;
|
|
54
|
+
/** Maximum concurrent in-flight handlers. */
|
|
55
|
+
maxConcurrency: number;
|
|
56
|
+
/** Visibility-timeout window in ms. */
|
|
57
|
+
visibilityTimeout: number;
|
|
58
|
+
}
|
|
59
|
+
/** Payload for `queue.consumer.stopped` — the polling loop was halted. */
|
|
60
|
+
export interface QueueConsumerStoppedDetail {
|
|
61
|
+
/** Epoch ms when `stop()` was called. */
|
|
62
|
+
stoppedAt: number;
|
|
63
|
+
/** Drain deadline in ms applied to in-flight handlers at stop time. */
|
|
64
|
+
drainTimeoutMs: number;
|
|
65
|
+
/** In-flight handler count observed at stop time. */
|
|
66
|
+
inFlightAtStop: number;
|
|
67
|
+
/** `true` when all in-flight handlers completed within the drain deadline. */
|
|
68
|
+
drained: boolean;
|
|
69
|
+
}
|
|
70
|
+
/** Payload for `queue.job.completed` — a job ran to success. */
|
|
71
|
+
export interface QueueJobCompletedDetail extends QueueJobRef {
|
|
72
|
+
/** Handler wall-clock duration in ms. */
|
|
73
|
+
durationMs: number;
|
|
74
|
+
/** Attempts counter on the job row at success (0 on the first successful run). */
|
|
75
|
+
attempt: number;
|
|
76
|
+
}
|
|
77
|
+
/** Payload for `queue.job.failed` — a job exhausted retries. */
|
|
78
|
+
export interface QueueJobFailedDetail extends QueueJobRef {
|
|
79
|
+
/** Error message from the final attempt. */
|
|
28
80
|
error: string;
|
|
29
|
-
/**
|
|
81
|
+
/** 1-based attempt number of the failure. */
|
|
30
82
|
attempt: number;
|
|
83
|
+
/** Maximum retry count configured for the job. */
|
|
84
|
+
maxRetries: number;
|
|
85
|
+
/** Handler wall-clock duration in ms, when measured at the failing attempt. */
|
|
86
|
+
durationMs?: number;
|
|
31
87
|
}
|
|
32
88
|
/** Payload for `queue.job.retrying` — a job will be retried after backoff. */
|
|
33
|
-
export interface QueueJobRetryingDetail {
|
|
34
|
-
|
|
35
|
-
type: string;
|
|
89
|
+
export interface QueueJobRetryingDetail extends QueueJobRef {
|
|
90
|
+
/** 1-based attempt number of the failure that triggers this retry. */
|
|
36
91
|
attempt: number;
|
|
92
|
+
/** Maximum retry count configured for the job. */
|
|
93
|
+
maxRetries: number;
|
|
37
94
|
/** When the next retry will fire (epoch ms). */
|
|
38
95
|
nextRetryAt: number;
|
|
96
|
+
/** Error message that caused this retry. */
|
|
97
|
+
error: string;
|
|
39
98
|
}
|
|
40
99
|
/** Payload for `scheduler.job.executed`. */
|
|
41
100
|
export interface SchedulerJobExecutedDetail {
|
|
@@ -64,19 +123,13 @@ export type DbEvents = {
|
|
|
64
123
|
/** Job-queue lifecycle events emitted by the queue and its consumer. */
|
|
65
124
|
export type QueueEvents = {
|
|
66
125
|
/** A new job was enqueued. */
|
|
67
|
-
'queue.job.enqueued': (detail:
|
|
68
|
-
jobId: string;
|
|
69
|
-
type: string;
|
|
70
|
-
}) => void;
|
|
126
|
+
'queue.job.enqueued': (detail: QueueJobEnqueuedDetail) => void;
|
|
71
127
|
/** Consumer polling loop started. */
|
|
72
|
-
'queue.consumer.started': () => void;
|
|
73
|
-
/** Consumer stopped. */
|
|
74
|
-
'queue.consumer.stopped': () => void;
|
|
128
|
+
'queue.consumer.started': (detail: QueueConsumerStartedDetail) => void;
|
|
129
|
+
/** Consumer polling loop stopped. */
|
|
130
|
+
'queue.consumer.stopped': (detail: QueueConsumerStoppedDetail) => void;
|
|
75
131
|
/** A job completed successfully. */
|
|
76
|
-
'queue.job.completed': (detail:
|
|
77
|
-
jobId: string;
|
|
78
|
-
type: string;
|
|
79
|
-
}) => void;
|
|
132
|
+
'queue.job.completed': (detail: QueueJobCompletedDetail) => void;
|
|
80
133
|
/** A job exhausted retries and is permanently failed. */
|
|
81
134
|
'queue.job.failed': (detail: QueueJobFailedDetail) => void;
|
|
82
135
|
/** A job will be retried after backoff. */
|
package/dist/events.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"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"}
|
package/dist/events.js
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Infrastructure-level event definitions for ts-infra observability.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* (
|
|
8
|
-
*
|
|
9
|
-
* (
|
|
10
|
-
*
|
|
11
|
-
*
|
|
4
|
+
* These maps define the infra-level event *contract* for the consumers of
|
|
5
|
+
* `@gobing-ai/ts-infra`. ts-infra emits `queue.*` (DBJobQueue / DBQueueConsumer),
|
|
6
|
+
* `scheduler.job.executed` (`wrapScheduledHandler`), and `db.connection.error`
|
|
7
|
+
* (DB wiring). Application-domain events (history import, HTTP server, …) belong
|
|
8
|
+
* to the consuming app, not this library. Process events belong to
|
|
9
|
+
* `@gobing-ai/ts-runtime` (the owner of `ProcessExecutor`) and are intentionally
|
|
10
|
+
* not re-exported here.
|
|
11
|
+
*
|
|
12
|
+
* **Metadata-only invariant:** every `queue.*` detail object is correlator-grade —
|
|
13
|
+
* it carries job identity, timing, and retry counters only, and NEVER embeds the
|
|
14
|
+
* job business payload `T` (which may contain prompts, tokens, PII, or large
|
|
15
|
+
* blobs). Inspect job bodies via the Jobs DAO when needed.
|
|
12
16
|
*
|
|
13
17
|
* Consume via `EventBus<InfraEvents>` or compose individual maps into a wider
|
|
14
18
|
* app event map.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { APIClient, type APIClientConfig, APIError, type RawHttpResponse, type RawRequestOptions, type RequestOptions, } from './api-client';
|
|
2
2
|
export { type AsyncEventJobPayload, attachDefaultObservers, attachFileObserver, attachLogObserver, attachTelemetryObserver, type BusLifecycleEvents, createLifecycleBus, EventBus, type EventMap, type FileObserverWriter, type SubscribeOptions, } from './event-bus/index';
|
|
3
|
-
export type { ApiClientEvents, ApiRequestErrorDetail, DbConnectionErrorDetail, DbEvents, InfraEvents, QueueEvents, QueueJobFailedDetail, QueueJobRetryingDetail, SchedulerEvents, SchedulerJobExecutedDetail, } from './events';
|
|
3
|
+
export type { ApiClientEvents, ApiRequestErrorDetail, DbConnectionErrorDetail, DbEvents, InfraEvents, QueueConsumerStartedDetail, QueueConsumerStoppedDetail, QueueEvents, QueueJobCompletedDetail, QueueJobEnqueuedDetail, QueueJobFailedDetail, QueueJobRef, QueueJobRetryingDetail, SchedulerEvents, SchedulerJobExecutedDetail, } from './events';
|
|
4
4
|
export type { EnqueueOptions, Job, JobHandler, JobQueue, QueueConsumer, QueueConsumerConfig, QueueStats, } from './job-queue/index';
|
|
5
5
|
export { getLogger, type InitLoggerOptions, initializeLogger, type Logger, type LogLevel, setLoggerMuted, } from './logger';
|
|
6
6
|
export { ActionRegistry, type CreateDefaultRegistryOptions, createDefaultRegistry, HealthPingAction, type HealthPingWriter, initScheduler, LogAction, NoopSchedulerAdapter, QueueStatsAction, type QueueStatsDaoProvider, type ScheduledAction, type SchedulerAction, type SchedulerAdapter, toScheduledAction, wrapScheduledHandler, } from './scheduler/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,EACH,SAAS,EACT,KAAK,eAAe,EACpB,QAAQ,EACR,KAAK,eAAe,EACpB,KAAK,iBAAiB,EACtB,KAAK,cAAc,GACtB,MAAM,cAAc,CAAC;AAGtB,OAAO,EACH,KAAK,oBAAoB,EACzB,sBAAsB,EACtB,kBAAkB,EAClB,iBAAiB,EACjB,uBAAuB,EACvB,KAAK,kBAAkB,EACvB,kBAAkB,EAClB,QAAQ,EACR,KAAK,QAAQ,EACb,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,GACxB,MAAM,mBAAmB,CAAC;AAG3B,YAAY,EACR,eAAe,EACf,qBAAqB,EACrB,uBAAuB,EACvB,QAAQ,EACR,WAAW,EACX,WAAW,EACX,oBAAoB,EACpB,sBAAsB,EACtB,eAAe,EACf,0BAA0B,GAC7B,MAAM,UAAU,CAAC;AAElB,YAAY,EACR,cAAc,EACd,GAAG,EACH,UAAU,EACV,QAAQ,EACR,aAAa,EACb,mBAAmB,EACnB,UAAU,GACb,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACH,SAAS,EACT,KAAK,iBAAiB,EACtB,gBAAgB,EAChB,KAAK,MAAM,EACX,KAAK,QAAQ,EACb,cAAc,GACjB,MAAM,UAAU,CAAC;AAGlB,OAAO,EACH,cAAc,EACd,KAAK,4BAA4B,EACjC,qBAAqB,EACrB,gBAAgB,EAChB,KAAK,gBAAgB,EACrB,aAAa,EACb,SAAS,EACT,oBAAoB,EACpB,gBAAgB,EAChB,KAAK,qBAAqB,EAC1B,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,iBAAiB,EACjB,oBAAoB,GACvB,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EACH,iBAAiB,EACjB,YAAY,EACZ,mBAAmB,EACnB,aAAa,EACb,qBAAqB,EACrB,sBAAsB,EACtB,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"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EACH,SAAS,EACT,KAAK,eAAe,EACpB,QAAQ,EACR,KAAK,eAAe,EACpB,KAAK,iBAAiB,EACtB,KAAK,cAAc,GACtB,MAAM,cAAc,CAAC;AAGtB,OAAO,EACH,KAAK,oBAAoB,EACzB,sBAAsB,EACtB,kBAAkB,EAClB,iBAAiB,EACjB,uBAAuB,EACvB,KAAK,kBAAkB,EACvB,kBAAkB,EAClB,QAAQ,EACR,KAAK,QAAQ,EACb,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,GACxB,MAAM,mBAAmB,CAAC;AAG3B,YAAY,EACR,eAAe,EACf,qBAAqB,EACrB,uBAAuB,EACvB,QAAQ,EACR,WAAW,EACX,0BAA0B,EAC1B,0BAA0B,EAC1B,WAAW,EACX,uBAAuB,EACvB,sBAAsB,EACtB,oBAAoB,EACpB,WAAW,EACX,sBAAsB,EACtB,eAAe,EACf,0BAA0B,GAC7B,MAAM,UAAU,CAAC;AAElB,YAAY,EACR,cAAc,EACd,GAAG,EACH,UAAU,EACV,QAAQ,EACR,aAAa,EACb,mBAAmB,EACnB,UAAU,GACb,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACH,SAAS,EACT,KAAK,iBAAiB,EACtB,gBAAgB,EAChB,KAAK,MAAM,EACX,KAAK,QAAQ,EACb,cAAc,GACjB,MAAM,UAAU,CAAC;AAGlB,OAAO,EACH,cAAc,EACd,KAAK,4BAA4B,EACjC,qBAAqB,EACrB,gBAAgB,EAChB,KAAK,gBAAgB,EACrB,aAAa,EACb,SAAS,EACT,oBAAoB,EACpB,gBAAgB,EAChB,KAAK,qBAAqB,EAC1B,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,iBAAiB,EACjB,oBAAoB,GACvB,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EACH,iBAAiB,EACjB,YAAY,EACZ,mBAAmB,EACnB,aAAa,EACb,qBAAqB,EACrB,sBAAsB,EACtB,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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal drain primitives shared by adapters that bound a shutdown wait on
|
|
3
|
+
* in-flight work (DB queue consumer, Node scheduler).
|
|
4
|
+
*
|
|
5
|
+
* WHY: two adapters had near-identical "await this in-flight thing but not
|
|
6
|
+
* forever" logic. Centralizing the bound keeps the two shapes from drifting
|
|
7
|
+
* and documents the contract once. Not re-exported from the package barrel —
|
|
8
|
+
* consumers depend on adapter behaviour, not this helper.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Resolve when `promise` settles or when `deadline` passes, whichever comes first.
|
|
12
|
+
*
|
|
13
|
+
* Never rejects: a rejected promise ends the wait the same way a fulfilled one
|
|
14
|
+
* does. `deadline` is an absolute epoch-ms timestamp (not a duration) so a single
|
|
15
|
+
* bound can be shared across several awaited promises without compounding.
|
|
16
|
+
*/
|
|
17
|
+
export declare function settleWithin(promise: Promise<void>, deadline: number): Promise<void>;
|
|
18
|
+
//# sourceMappingURL=drain.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"drain.d.ts","sourceRoot":"","sources":["../../src/internals/drain.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAWpF"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal drain primitives shared by adapters that bound a shutdown wait on
|
|
3
|
+
* in-flight work (DB queue consumer, Node scheduler).
|
|
4
|
+
*
|
|
5
|
+
* WHY: two adapters had near-identical "await this in-flight thing but not
|
|
6
|
+
* forever" logic. Centralizing the bound keeps the two shapes from drifting
|
|
7
|
+
* and documents the contract once. Not re-exported from the package barrel —
|
|
8
|
+
* consumers depend on adapter behaviour, not this helper.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Resolve when `promise` settles or when `deadline` passes, whichever comes first.
|
|
12
|
+
*
|
|
13
|
+
* Never rejects: a rejected promise ends the wait the same way a fulfilled one
|
|
14
|
+
* does. `deadline` is an absolute epoch-ms timestamp (not a duration) so a single
|
|
15
|
+
* bound can be shared across several awaited promises without compounding.
|
|
16
|
+
*/
|
|
17
|
+
export function settleWithin(promise, deadline) {
|
|
18
|
+
const remaining = deadline - Date.now();
|
|
19
|
+
if (remaining <= 0)
|
|
20
|
+
return Promise.resolve();
|
|
21
|
+
const { promise: settled, resolve } = Promise.withResolvers();
|
|
22
|
+
const timer = setTimeout(resolve, remaining);
|
|
23
|
+
const done = () => {
|
|
24
|
+
clearTimeout(timer);
|
|
25
|
+
resolve();
|
|
26
|
+
};
|
|
27
|
+
promise.then(done, done);
|
|
28
|
+
return settled;
|
|
29
|
+
}
|
|
@@ -29,6 +29,7 @@ export declare class DBQueueConsumer<T = unknown> implements QueueConsumer<T> {
|
|
|
29
29
|
private timer;
|
|
30
30
|
private running;
|
|
31
31
|
private inFlight;
|
|
32
|
+
private pollPromise;
|
|
32
33
|
constructor(dao: QueueJobDao, config?: QueueConsumerConfig);
|
|
33
34
|
register(type: string, handler: JobHandler<T>): void;
|
|
34
35
|
start(): Promise<void>;
|
|
@@ -1 +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,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,KAAK,
|
|
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,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,KAAK,EAER,WAAW,EAKd,MAAM,WAAW,CAAC;AAUnB,OAAO,KAAK,EAAE,cAAc,EAAO,UAAU,EAAE,QAAQ,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAQ7G,kFAAkF;AAClF,qBAAa,UAAU,CAAC,CAAC,GAAG,OAAO,CAAE,YAAW,QAAQ,CAAC,CAAC,CAAC;IAEnD,QAAQ,CAAC,GAAG,EAAE,WAAW;IACzB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC;gBADf,GAAG,EAAE,WAAW,EACR,MAAM,CAAC,EAAE,QAAQ,CAAC,WAAW,CAAC,YAAA;IAG7C,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC;IAO5E,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;IAY3F,KAAK,IAAI,OAAO,CAAC,UAAU,CAAC;CAGrC;AAcD,qFAAqF;AACrF,qBAAa,eAAe,CAAC,CAAC,GAAG,OAAO,CAAE,YAAW,aAAa,CAAC,CAAC,CAAC;IAgB7D,OAAO,CAAC,QAAQ,CAAC,GAAG;IAfxB,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,QAAQ,CAAC,MAAM,CAAoC;IAC3D,OAAO,CAAC,KAAK,CAA8C;IAC3D,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,QAAQ,CAAK;IACrB,OAAO,CAAC,WAAW,CAA8B;gBAG5B,GAAG,EAAE,WAAW,EACjC,MAAM,GAAE,mBAAwB;IAYpC,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,IAAI;IAI9C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAatB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAkCrB,KAAK,IAAI,OAAO,CAAC,UAAU,CAAC;IAIlC,sFAAsF;IAChF,WAAW,IAAI,OAAO,CAAC,MAAM,CAAC;IA4BpC,OAAO,CAAC,QAAQ;YAWF,IAAI;YAgBJ,UAAU;YA6CV,WAAW;CAmC5B"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { settleWithin } from '../internals/drain.js';
|
|
1
2
|
import { getLogger } from '../logger.js';
|
|
2
3
|
import { getQueueJobCompletedTotal, getQueueJobEnqueuedTotal, getQueueJobFailedTotal, getQueueJobProcessingDuration, } from '../telemetry/metrics.js';
|
|
3
4
|
import { addSpanAttributes, traceAsync } from '../telemetry/tracing.js';
|
|
@@ -18,7 +19,7 @@ export class DBJobQueue {
|
|
|
18
19
|
async enqueue(type, payload, options) {
|
|
19
20
|
const id = await this.dao.enqueue(type, payload, options);
|
|
20
21
|
getQueueJobEnqueuedTotal().add(1, { type });
|
|
21
|
-
await this.events?.emit('queue.job.enqueued',
|
|
22
|
+
await this.events?.emit('queue.job.enqueued', enqueuedDetail(id, type, options));
|
|
22
23
|
return id;
|
|
23
24
|
}
|
|
24
25
|
async enqueueBatch(jobs) {
|
|
@@ -26,7 +27,8 @@ export class DBJobQueue {
|
|
|
26
27
|
getQueueJobEnqueuedTotal().add(jobs.length);
|
|
27
28
|
if (this.events) {
|
|
28
29
|
for (const [index, jobId] of ids.entries()) {
|
|
29
|
-
|
|
30
|
+
const job = jobs[index];
|
|
31
|
+
await this.events.emit('queue.job.enqueued', enqueuedDetail(jobId, job?.type ?? 'unknown', job));
|
|
30
32
|
}
|
|
31
33
|
}
|
|
32
34
|
return ids;
|
|
@@ -35,6 +37,20 @@ export class DBJobQueue {
|
|
|
35
37
|
return this.dao.getStats();
|
|
36
38
|
}
|
|
37
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Build the `queue.job.enqueued` correlator detail, filling optional retry/delay/TTL
|
|
42
|
+
* fields only when supplied. Shared by single and batch enqueue so both emit one shape.
|
|
43
|
+
*/
|
|
44
|
+
function enqueuedDetail(jobId, type, options) {
|
|
45
|
+
const detail = { jobId, type, enqueuedAt: Date.now() };
|
|
46
|
+
if (options && options.maxRetries !== undefined)
|
|
47
|
+
detail.maxRetries = options.maxRetries;
|
|
48
|
+
if (options && options.delay !== undefined)
|
|
49
|
+
detail.delayMs = options.delay;
|
|
50
|
+
if (options && options.ttlMs !== undefined)
|
|
51
|
+
detail.ttlMs = options.ttlMs;
|
|
52
|
+
return detail;
|
|
53
|
+
}
|
|
38
54
|
/** DB-backed queue consumer with polling, retry, and visibility-timeout handling. */
|
|
39
55
|
export class DBQueueConsumer {
|
|
40
56
|
dao;
|
|
@@ -50,6 +66,7 @@ export class DBQueueConsumer {
|
|
|
50
66
|
timer = null;
|
|
51
67
|
running = false;
|
|
52
68
|
inFlight = 0;
|
|
69
|
+
pollPromise = null;
|
|
53
70
|
constructor(dao, config = {}) {
|
|
54
71
|
this.dao = dao;
|
|
55
72
|
this.pollInterval = nonNegativeFiniteConfig('pollInterval', config.pollInterval ?? 1_000);
|
|
@@ -69,7 +86,13 @@ export class DBQueueConsumer {
|
|
|
69
86
|
return;
|
|
70
87
|
this.running = true;
|
|
71
88
|
this.schedule(0);
|
|
72
|
-
await this.events?.emit('queue.consumer.started'
|
|
89
|
+
await this.events?.emit('queue.consumer.started', {
|
|
90
|
+
startedAt: Date.now(),
|
|
91
|
+
pollInterval: this.pollInterval,
|
|
92
|
+
batchSize: this.batchSize,
|
|
93
|
+
maxConcurrency: this.maxConcurrency,
|
|
94
|
+
visibilityTimeout: this.visibilityTimeout,
|
|
95
|
+
});
|
|
73
96
|
}
|
|
74
97
|
async stop() {
|
|
75
98
|
const wasRunning = this.running;
|
|
@@ -79,11 +102,27 @@ export class DBQueueConsumer {
|
|
|
79
102
|
this.timer = null;
|
|
80
103
|
}
|
|
81
104
|
const deadline = Date.now() + this.drainTimeoutMs;
|
|
105
|
+
// Wait for an already-running poll cycle before consulting `inFlight`.
|
|
106
|
+
// `inFlight` is only incremented once claimReady() has resolved, so a stop()
|
|
107
|
+
// landing between the timer firing and that increment would see 0, exit the
|
|
108
|
+
// drain loop immediately, and report `drained: true` while the cycle went on
|
|
109
|
+
// to claim and process jobs after stop() resolved.
|
|
110
|
+
const pending = this.pollPromise;
|
|
111
|
+
if (pending !== null) {
|
|
112
|
+
await settleWithin(pending, deadline);
|
|
113
|
+
}
|
|
82
114
|
while (this.inFlight > 0 && Date.now() < deadline) {
|
|
83
115
|
await sleep(10);
|
|
84
116
|
}
|
|
85
|
-
if (wasRunning)
|
|
86
|
-
|
|
117
|
+
if (wasRunning) {
|
|
118
|
+
const detail = {
|
|
119
|
+
stoppedAt: Date.now(),
|
|
120
|
+
drainTimeoutMs: this.drainTimeoutMs,
|
|
121
|
+
inFlightAtStop: this.inFlight,
|
|
122
|
+
drained: this.inFlight === 0,
|
|
123
|
+
};
|
|
124
|
+
await this.events?.emit('queue.consumer.stopped', detail);
|
|
125
|
+
}
|
|
87
126
|
}
|
|
88
127
|
async stats() {
|
|
89
128
|
return this.dao.getStats();
|
|
@@ -114,7 +153,13 @@ export class DBQueueConsumer {
|
|
|
114
153
|
}
|
|
115
154
|
schedule(delay) {
|
|
116
155
|
this.timer = setTimeout(() => {
|
|
117
|
-
|
|
156
|
+
// Retained so stop() can await the cycle it interrupts; poll() contains its
|
|
157
|
+
// own errors, so this promise settles rather than rejecting.
|
|
158
|
+
const cycle = this.poll().finally(() => {
|
|
159
|
+
if (this.pollPromise === cycle)
|
|
160
|
+
this.pollPromise = null;
|
|
161
|
+
});
|
|
162
|
+
this.pollPromise = cycle;
|
|
118
163
|
}, delay);
|
|
119
164
|
}
|
|
120
165
|
async poll() {
|
|
@@ -144,7 +189,7 @@ export class DBQueueConsumer {
|
|
|
144
189
|
catch (error) {
|
|
145
190
|
// Corrupt payload — the job can never parse; route it through the
|
|
146
191
|
// retry/fail path instead of rejecting the whole batch.
|
|
147
|
-
await this.failOrRetry(record, error);
|
|
192
|
+
await this.failOrRetry(record, error, 0);
|
|
148
193
|
return;
|
|
149
194
|
}
|
|
150
195
|
return traceAsync('queue.job.process', async () => {
|
|
@@ -155,46 +200,60 @@ export class DBQueueConsumer {
|
|
|
155
200
|
});
|
|
156
201
|
const handler = this.handlers.get(job.type);
|
|
157
202
|
if (handler === undefined) {
|
|
158
|
-
await this.failOrRetry(job, new Error(`No handler registered for job type "${job.type}"`));
|
|
203
|
+
await this.failOrRetry(job, new Error(`No handler registered for job type "${job.type}"`), 0);
|
|
159
204
|
return;
|
|
160
205
|
}
|
|
161
206
|
const startMs = performance.now();
|
|
162
207
|
try {
|
|
163
208
|
await handler(job);
|
|
164
209
|
await this.dao.markCompleted(job.id);
|
|
210
|
+
const durationMs = performance.now() - startMs;
|
|
165
211
|
getQueueJobCompletedTotal().add(1, { type: job.type });
|
|
166
|
-
getQueueJobProcessingDuration().record(
|
|
167
|
-
|
|
212
|
+
getQueueJobProcessingDuration().record(durationMs, { type: job.type });
|
|
213
|
+
const completed = {
|
|
214
|
+
jobId: job.id,
|
|
215
|
+
type: job.type,
|
|
216
|
+
durationMs: Number.isFinite(durationMs) ? durationMs : 0,
|
|
217
|
+
attempt: job.attempts,
|
|
218
|
+
};
|
|
219
|
+
await this.events?.emit('queue.job.completed', completed);
|
|
168
220
|
}
|
|
169
221
|
catch (error) {
|
|
170
|
-
|
|
171
|
-
|
|
222
|
+
const durationMs = performance.now() - startMs;
|
|
223
|
+
getQueueJobProcessingDuration().record(durationMs, { type: job.type });
|
|
224
|
+
await this.failOrRetry(job, error, Number.isFinite(durationMs) ? durationMs : 0);
|
|
172
225
|
}
|
|
173
226
|
});
|
|
174
227
|
}
|
|
175
|
-
async failOrRetry(job, error) {
|
|
228
|
+
async failOrRetry(job, error, durationMs) {
|
|
176
229
|
const attempts = job.attempts + 1;
|
|
177
230
|
const message = error instanceof Error ? error.message : String(error);
|
|
178
231
|
if (attempts >= job.maxRetries) {
|
|
179
232
|
await this.dao.markFailed(job.id, attempts, message);
|
|
180
233
|
getQueueJobFailedTotal().add(1, { type: job.type });
|
|
181
|
-
|
|
234
|
+
const failed = {
|
|
182
235
|
jobId: job.id,
|
|
183
236
|
type: job.type,
|
|
184
237
|
error: message,
|
|
185
238
|
attempt: attempts,
|
|
186
|
-
|
|
239
|
+
maxRetries: job.maxRetries,
|
|
240
|
+
durationMs,
|
|
241
|
+
};
|
|
242
|
+
await this.events?.emit('queue.job.failed', failed);
|
|
187
243
|
return;
|
|
188
244
|
}
|
|
189
245
|
const delay = Math.min(this.maxDelay, this.baseDelay * 2 ** Math.max(0, attempts - 1));
|
|
190
246
|
const nextRetryAt = Date.now() + delay;
|
|
191
247
|
await this.dao.markForRetry(job.id, attempts, message, nextRetryAt);
|
|
192
|
-
|
|
248
|
+
const retrying = {
|
|
193
249
|
jobId: job.id,
|
|
194
250
|
type: job.type,
|
|
195
251
|
attempt: attempts,
|
|
252
|
+
maxRetries: job.maxRetries,
|
|
196
253
|
nextRetryAt,
|
|
197
|
-
|
|
254
|
+
error: message,
|
|
255
|
+
};
|
|
256
|
+
await this.events?.emit('queue.job.retrying', retrying);
|
|
198
257
|
}
|
|
199
258
|
}
|
|
200
259
|
function toJob(record) {
|
|
@@ -213,7 +272,9 @@ function toJob(record) {
|
|
|
213
272
|
};
|
|
214
273
|
}
|
|
215
274
|
function sleep(ms) {
|
|
216
|
-
|
|
275
|
+
const { promise, resolve } = Promise.withResolvers();
|
|
276
|
+
setTimeout(resolve, ms);
|
|
277
|
+
return promise;
|
|
217
278
|
}
|
|
218
279
|
function positiveIntegerConfig(name, value) {
|
|
219
280
|
if (!Number.isInteger(value) || value <= 0) {
|
|
@@ -13,6 +13,7 @@ export interface Job<T = unknown> {
|
|
|
13
13
|
payload: T;
|
|
14
14
|
status: 'pending' | 'processing' | 'completed' | 'failed';
|
|
15
15
|
attempts: number;
|
|
16
|
+
/** Total attempts allowed, not retries *after* the first — a job fails once `attempts >= maxRetries`. */
|
|
16
17
|
maxRetries: number;
|
|
17
18
|
createdAt: number;
|
|
18
19
|
updatedAt: number;
|
|
@@ -22,6 +23,7 @@ export interface Job<T = unknown> {
|
|
|
22
23
|
}
|
|
23
24
|
/** Options for enqueuing a job: retry policy, delay, and TTL. */
|
|
24
25
|
export interface EnqueueOptions {
|
|
26
|
+
/** Total attempts allowed (default 3) — `maxRetries: 1` runs the job once with no retry. */
|
|
25
27
|
maxRetries?: number;
|
|
26
28
|
delay?: number;
|
|
27
29
|
ttlMs?: number;
|
|
@@ -52,11 +54,14 @@ export interface QueueConsumerConfig {
|
|
|
52
54
|
visibilityTimeout?: number;
|
|
53
55
|
baseDelay?: number;
|
|
54
56
|
maxDelay?: number;
|
|
57
|
+
/** Upper bound (ms, default 30_000) on how long `stop()` waits for in-flight work to drain. */
|
|
55
58
|
drainTimeoutMs?: number;
|
|
56
59
|
/**
|
|
57
|
-
* Optional bus for queue lifecycle events
|
|
58
|
-
* `queue.consumer.
|
|
59
|
-
* `queue.job.
|
|
60
|
+
* Optional bus for queue lifecycle events with correlator-grade detail payloads:
|
|
61
|
+
* `queue.consumer.started` / `queue.consumer.stopped` (config snapshot + drain
|
|
62
|
+
* outcome), `queue.job.enqueued` / `queue.job.completed` / `queue.job.failed` /
|
|
63
|
+
* `queue.job.retrying`. Details carry job identity, timing, and retry counters
|
|
64
|
+
* only — never the business payload `T`. Omitting it leaves the consumer silent.
|
|
60
65
|
*/
|
|
61
66
|
events?: EventBus<QueueEvents>;
|
|
62
67
|
}
|
|
@@ -64,6 +69,11 @@ export interface QueueConsumerConfig {
|
|
|
64
69
|
export interface QueueConsumer<T = unknown> {
|
|
65
70
|
register(type: string, handler: JobHandler<T>): void;
|
|
66
71
|
start(): Promise<void>;
|
|
72
|
+
/**
|
|
73
|
+
* Stop polling and drain work already in flight, including a poll cycle that has
|
|
74
|
+
* claimed nothing yet. Resolves once the drain completes or `drainTimeoutMs`
|
|
75
|
+
* elapses — whichever comes first, so a hung handler cannot block shutdown.
|
|
76
|
+
*/
|
|
67
77
|
stop(): Promise<void>;
|
|
68
78
|
stats(): Promise<QueueStats>;
|
|
69
79
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/job-queue/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAE7C,yEAAyE;AACzE,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,iEAAiE;AACjE,MAAM,WAAW,cAAc;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,6EAA6E;AAC7E,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;IAC5F,KAAK,IAAI,OAAO,CAAC,UAAU,CAAC,CAAC;CAChC;AAED,iDAAiD;AACjD,MAAM,MAAM,UAAU,CAAC,CAAC,GAAG,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAErE,8DAA8D;AAC9D,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,6EAA6E;AAC7E,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
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/job-queue/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAE7C,yEAAyE;AACzE,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,yGAAyG;IACzG,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,iEAAiE;AACjE,MAAM,WAAW,cAAc;IAC3B,4FAA4F;IAC5F,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,6EAA6E;AAC7E,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;IAC5F,KAAK,IAAI,OAAO,CAAC,UAAU,CAAC,CAAC;CAChC;AAED,iDAAiD;AACjD,MAAM,MAAM,UAAU,CAAC,CAAC,GAAG,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAErE,8DAA8D;AAC9D,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,6EAA6E;AAC7E,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,+FAA+F;IAC/F,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,QAAQ,CAAC,WAAW,CAAC,CAAC;CAClC;AAED,gGAAgG;AAChG,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;;;;OAIG;IACH,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,KAAK,IAAI,OAAO,CAAC,UAAU,CAAC,CAAC;CAChC"}
|
|
@@ -18,6 +18,13 @@ export declare class CloudflareSchedulerAdapter implements SchedulerAdapter {
|
|
|
18
18
|
constructor();
|
|
19
19
|
register(cron: string, action: ScheduledAction): void;
|
|
20
20
|
start(): Promise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* No-op drain. Cloudflare Workers fire Cron Triggers externally — this adapter
|
|
23
|
+
* owns no timer to cancel — and `handleScheduledEvent` already bounds each
|
|
24
|
+
* in-flight action via `ctx.waitUntil()`, the runtime's own drain. So there is
|
|
25
|
+
* nothing for stop() to await here; clearing registrations is the full contract
|
|
26
|
+
* (ADR-024).
|
|
27
|
+
*/
|
|
21
28
|
stop(): Promise<void>;
|
|
22
29
|
/**
|
|
23
30
|
* Handle a Cloudflare Workers Cron Trigger event.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cloudflare.d.ts","sourceRoot":"","sources":["../../src/scheduler/cloudflare.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAEjE,UAAU,gBAAgB;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC;CAC9C;AAED,UAAU,cAAc;IACpB,SAAS,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC;CAC9C;AAED;;;;;GAKG;AACH,qBAAa,0BAA2B,YAAW,gBAAgB;IAC/D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAsC;;IAI9D,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI;IAI/C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"cloudflare.d.ts","sourceRoot":"","sources":["../../src/scheduler/cloudflare.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAEjE,UAAU,gBAAgB;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC;CAC9C;AAED,UAAU,cAAc;IACpB,SAAS,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC;CAC9C;AAED;;;;;GAKG;AACH,qBAAa,0BAA2B,YAAW,gBAAgB;IAC/D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAsC;;IAI9D,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI;IAI/C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAK5B;;;;;;OAMG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAI3B;;;OAGG;IACH,oBAAoB,CAAC,KAAK,EAAE,gBAAgB,EAAE,GAAG,EAAE,cAAc,GAAG,IAAI;CAmB3E"}
|
|
@@ -19,6 +19,13 @@ export class CloudflareSchedulerAdapter {
|
|
|
19
19
|
// Cloudflare Workers use cron triggers defined in wrangler.toml.
|
|
20
20
|
// The `scheduled()` handler should call `handleScheduledEvent()`.
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* No-op drain. Cloudflare Workers fire Cron Triggers externally — this adapter
|
|
24
|
+
* owns no timer to cancel — and `handleScheduledEvent` already bounds each
|
|
25
|
+
* in-flight action via `ctx.waitUntil()`, the runtime's own drain. So there is
|
|
26
|
+
* nothing for stop() to await here; clearing registrations is the full contract
|
|
27
|
+
* (ADR-024).
|
|
28
|
+
*/
|
|
22
29
|
async stop() {
|
|
23
30
|
this.entries.clear();
|
|
24
31
|
}
|