bunqueue-client 0.1.2 → 0.1.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 +45 -3
- package/dist/bunqueue/aging.d.ts +17 -0
- package/dist/bunqueue/aging.js +53 -0
- package/dist/bunqueue/batch.d.ts +17 -0
- package/dist/bunqueue/batch.js +60 -0
- package/dist/bunqueue/bunqueue-api.d.ts +87 -0
- package/dist/bunqueue/bunqueue-api.js +116 -0
- package/dist/bunqueue/bunqueue.d.ts +57 -0
- package/dist/bunqueue/bunqueue.js +182 -0
- package/dist/bunqueue/cancellation.d.ts +20 -0
- package/dist/bunqueue/cancellation.js +48 -0
- package/dist/bunqueue/circuit-breaker.d.ts +21 -0
- package/dist/bunqueue/circuit-breaker.js +69 -0
- package/dist/bunqueue/dedup-debounce.d.ts +14 -0
- package/dist/bunqueue/dedup-debounce.js +37 -0
- package/dist/bunqueue/dlq-rate-limit.d.ts +33 -0
- package/dist/bunqueue/dlq-rate-limit.js +65 -0
- package/dist/bunqueue/rate-gate.d.ts +21 -0
- package/dist/bunqueue/rate-gate.js +48 -0
- package/dist/bunqueue/retry.d.ts +9 -0
- package/dist/bunqueue/retry.js +56 -0
- package/dist/bunqueue/triggers.d.ts +18 -0
- package/dist/bunqueue/triggers.js +42 -0
- package/dist/bunqueue/ttl.d.ts +18 -0
- package/dist/bunqueue/ttl.js +31 -0
- package/dist/bunqueue/types.d.ts +151 -0
- package/dist/bunqueue/types.js +6 -0
- package/dist/connection.d.ts +9 -1
- package/dist/connection.js +32 -47
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3 -1
- package/dist/job.d.ts +2 -0
- package/dist/job.js +4 -0
- package/dist/queue-admin.js +2 -1
- package/dist/socket-factory.d.ts +4 -0
- package/dist/socket-factory.js +47 -0
- package/dist/worker-base.d.ts +13 -2
- package/dist/worker-base.js +29 -4
- package/dist/worker.js +8 -0
- package/package.json +1 -1
- package/src/bunqueue/aging.ts +62 -0
- package/src/bunqueue/batch.ts +79 -0
- package/src/bunqueue/bunqueue-api.ts +212 -0
- package/src/bunqueue/bunqueue.ts +210 -0
- package/src/bunqueue/cancellation.ts +54 -0
- package/src/bunqueue/circuit-breaker.ts +81 -0
- package/src/bunqueue/dedup-debounce.ts +43 -0
- package/src/bunqueue/dlq-rate-limit.ts +89 -0
- package/src/bunqueue/rate-gate.ts +53 -0
- package/src/bunqueue/retry.ts +72 -0
- package/src/bunqueue/triggers.ts +51 -0
- package/src/bunqueue/ttl.ts +38 -0
- package/src/bunqueue/types.ts +171 -0
- package/src/connection.ts +38 -46
- package/src/index.ts +22 -1
- package/src/job.ts +5 -0
- package/src/queue-admin.ts +2 -1
- package/src/socket-factory.ts +51 -0
- package/src/worker-base.ts +30 -4
- package/src/worker.ts +9 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bunqueue Simple Mode — batch processing.
|
|
3
|
+
* 1:1 port of src/client/bunqueue/batch.ts: accumulates jobs and processes
|
|
4
|
+
* them in groups; flushes on size, on timeout, and on destroy (close).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Job } from '../job.js';
|
|
8
|
+
import type { Processor } from '../worker-types.js';
|
|
9
|
+
import type { BatchConfig } from './types.js';
|
|
10
|
+
|
|
11
|
+
interface BufferEntry<T, R> {
|
|
12
|
+
job: Job<T>;
|
|
13
|
+
resolve: (value: R) => void;
|
|
14
|
+
reject: (err: Error) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class BatchAccumulator<T = unknown, R = unknown> {
|
|
18
|
+
private readonly buffer: BufferEntry<T, R>[] = [];
|
|
19
|
+
private timer: ReturnType<typeof setTimeout> | null = null;
|
|
20
|
+
private readonly config: BatchConfig<T, R>;
|
|
21
|
+
|
|
22
|
+
constructor(config: BatchConfig<T, R>) {
|
|
23
|
+
this.config = config;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Build a Processor that buffers jobs into batches. */
|
|
27
|
+
buildProcessor(): Processor<T, R> {
|
|
28
|
+
return (job: Job<T>): Promise<R> => {
|
|
29
|
+
return new Promise<R>((resolve, reject) => {
|
|
30
|
+
this.buffer.push({ job, resolve, reject });
|
|
31
|
+
|
|
32
|
+
if (this.buffer.length >= this.config.size) {
|
|
33
|
+
this.flush();
|
|
34
|
+
} else if (!this.timer) {
|
|
35
|
+
const timeout = this.config.timeout ?? 5000;
|
|
36
|
+
this.timer = setTimeout(() => {
|
|
37
|
+
this.flush();
|
|
38
|
+
}, timeout);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
flush(): void {
|
|
45
|
+
if (this.timer) {
|
|
46
|
+
clearTimeout(this.timer);
|
|
47
|
+
this.timer = null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const batch = this.buffer.splice(0);
|
|
51
|
+
if (batch.length === 0) return;
|
|
52
|
+
|
|
53
|
+
const jobs = batch.map((b) => b.job);
|
|
54
|
+
this.config.processor(jobs).then(
|
|
55
|
+
(results) => {
|
|
56
|
+
for (let i = 0; i < batch.length; i++) {
|
|
57
|
+
batch[i].resolve(results[i] ?? (undefined as unknown as R));
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
(err: unknown) => {
|
|
61
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
62
|
+
for (const b of batch) {
|
|
63
|
+
b.reject(error);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
destroy(): void {
|
|
70
|
+
if (this.timer) {
|
|
71
|
+
clearTimeout(this.timer);
|
|
72
|
+
this.timer = null;
|
|
73
|
+
}
|
|
74
|
+
// Flush remaining
|
|
75
|
+
if (this.buffer.length > 0) {
|
|
76
|
+
this.flush();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bunqueue Simple Mode — delegation API surface (cron, cancellation, circuit
|
|
3
|
+
* breaker, TTL, DLQ, rate limit, events, control). Merged onto the Bunqueue
|
|
4
|
+
* prototype by bunqueue.ts; split out to keep files small.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Job } from '../job.js';
|
|
8
|
+
import type { JobOptions } from '../types.js';
|
|
9
|
+
import type { Bunqueue } from './bunqueue.js';
|
|
10
|
+
import type { DlqFilter, DlqStats } from './dlq-rate-limit.js';
|
|
11
|
+
import type { CircuitState, TriggerRule } from './types.js';
|
|
12
|
+
|
|
13
|
+
type Raw = Record<string, unknown>;
|
|
14
|
+
// biome-ignore lint/suspicious/noExplicitAny: mixin context needs the erased generic instance
|
|
15
|
+
type Ctx = Bunqueue<any, any>;
|
|
16
|
+
|
|
17
|
+
export const bunqueueApi = {
|
|
18
|
+
// --------------------------------------------------------------------- cron
|
|
19
|
+
|
|
20
|
+
async cron(
|
|
21
|
+
this: Ctx,
|
|
22
|
+
id: string,
|
|
23
|
+
pattern: string,
|
|
24
|
+
data?: unknown,
|
|
25
|
+
opts?: { timezone?: string; jobOpts?: JobOptions }
|
|
26
|
+
): Promise<Raw | null> {
|
|
27
|
+
await this.queue.upsertJobScheduler(
|
|
28
|
+
id,
|
|
29
|
+
{ pattern, tz: opts?.timezone },
|
|
30
|
+
{ name: id, data, opts: opts?.jobOpts }
|
|
31
|
+
);
|
|
32
|
+
return this.queue.getJobScheduler(id);
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
async every(
|
|
36
|
+
this: Ctx,
|
|
37
|
+
id: string,
|
|
38
|
+
intervalMs: number,
|
|
39
|
+
data?: unknown,
|
|
40
|
+
opts?: { jobOpts?: JobOptions }
|
|
41
|
+
): Promise<Raw | null> {
|
|
42
|
+
await this.queue.upsertJobScheduler(
|
|
43
|
+
id,
|
|
44
|
+
{ every: intervalMs },
|
|
45
|
+
{ name: id, data, opts: opts?.jobOpts }
|
|
46
|
+
);
|
|
47
|
+
return this.queue.getJobScheduler(id);
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
removeCron(this: Ctx, id: string): Promise<void> {
|
|
51
|
+
return this.queue.removeJobScheduler(id);
|
|
52
|
+
},
|
|
53
|
+
listCrons(this: Ctx): Promise<Raw[]> {
|
|
54
|
+
return this.queue.getJobSchedulers();
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
// ------------------------------------------------------------- cancellation
|
|
58
|
+
|
|
59
|
+
cancel(this: Ctx, jobId: string, gracePeriodMs = 0): void {
|
|
60
|
+
this.cancellation.cancel(jobId, gracePeriodMs);
|
|
61
|
+
},
|
|
62
|
+
isCancelled(this: Ctx, jobId: string): boolean {
|
|
63
|
+
return this.cancellation.isCancelled(jobId);
|
|
64
|
+
},
|
|
65
|
+
getSignal(this: Ctx, jobId: string): AbortSignal | null {
|
|
66
|
+
return this.cancellation.getSignal(jobId);
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
// ---------------------------------------------------------- circuit breaker
|
|
70
|
+
|
|
71
|
+
getCircuitState(this: Ctx): CircuitState {
|
|
72
|
+
return this.cb?.currentState ?? 'closed';
|
|
73
|
+
},
|
|
74
|
+
resetCircuit(this: Ctx): void {
|
|
75
|
+
this.cb?.reset();
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
// ----------------------------------------------------------------- triggers
|
|
79
|
+
|
|
80
|
+
trigger(this: Ctx, rule: TriggerRule): Ctx {
|
|
81
|
+
this.triggerMgr.add(rule);
|
|
82
|
+
return this;
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
// ---------------------------------------------------------------------- ttl
|
|
86
|
+
|
|
87
|
+
setDefaultTtl(this: Ctx, ttlMs: number): void {
|
|
88
|
+
this.ttlChecker?.setDefaultTtl(ttlMs);
|
|
89
|
+
},
|
|
90
|
+
setNameTtl(this: Ctx, name: string, ttlMs: number): void {
|
|
91
|
+
this.ttlChecker?.setNameTtl(name, ttlMs);
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------- dlq
|
|
95
|
+
|
|
96
|
+
setDlqConfig(this: Ctx, config: Raw): Promise<void> {
|
|
97
|
+
return this.dlqrl.setDlqConfig(config);
|
|
98
|
+
},
|
|
99
|
+
getDlqConfig(this: Ctx): Promise<Raw> {
|
|
100
|
+
return this.dlqrl.getDlqConfig();
|
|
101
|
+
},
|
|
102
|
+
getDlq(this: Ctx, filter?: DlqFilter): Promise<Raw[]> {
|
|
103
|
+
return this.dlqrl.getDlq(filter);
|
|
104
|
+
},
|
|
105
|
+
getDlqStats(this: Ctx): Promise<DlqStats> {
|
|
106
|
+
return this.dlqrl.getDlqStats();
|
|
107
|
+
},
|
|
108
|
+
retryDlq(this: Ctx, id?: string): Promise<number> {
|
|
109
|
+
return this.dlqrl.retryDlq(id);
|
|
110
|
+
},
|
|
111
|
+
purgeDlq(this: Ctx): Promise<number> {
|
|
112
|
+
return this.dlqrl.purgeDlq();
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
// ------------------------------------------------------------ rate limiting
|
|
116
|
+
|
|
117
|
+
setGlobalRateLimit(this: Ctx, max: number, duration?: number): Promise<void> {
|
|
118
|
+
return this.dlqrl.setGlobalRateLimit(max, duration);
|
|
119
|
+
},
|
|
120
|
+
removeGlobalRateLimit(this: Ctx): Promise<void> {
|
|
121
|
+
return this.dlqrl.removeGlobalRateLimit();
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
// ------------------------------------------------------------------- events
|
|
125
|
+
|
|
126
|
+
on(this: Ctx, event: string, listener: (...args: never[]) => void): Ctx {
|
|
127
|
+
this.worker.on(event, listener as (...args: unknown[]) => void);
|
|
128
|
+
return this;
|
|
129
|
+
},
|
|
130
|
+
once(this: Ctx, event: string, listener: (...args: never[]) => void): Ctx {
|
|
131
|
+
this.worker.once(event, listener as (...args: unknown[]) => void);
|
|
132
|
+
return this;
|
|
133
|
+
},
|
|
134
|
+
off(this: Ctx, event: string, listener: (...args: never[]) => void): Ctx {
|
|
135
|
+
this.worker.off(event, listener as (...args: unknown[]) => void);
|
|
136
|
+
return this;
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
// ------------------------------------------------------------------ control
|
|
140
|
+
|
|
141
|
+
pause(this: Ctx): void {
|
|
142
|
+
void this.queue.pause().catch(() => {});
|
|
143
|
+
this.worker.pause();
|
|
144
|
+
},
|
|
145
|
+
resume(this: Ctx): void {
|
|
146
|
+
void this.queue.resume().catch(() => {});
|
|
147
|
+
this.worker.resume();
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
async close(this: Ctx, force = false): Promise<void> {
|
|
151
|
+
this.ager?.destroy();
|
|
152
|
+
this.cb?.destroy();
|
|
153
|
+
this.batchAcc?.destroy();
|
|
154
|
+
this.cancellation.destroyAll();
|
|
155
|
+
await this.worker.close(force);
|
|
156
|
+
this.queue.close();
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
isRunning(this: Ctx): boolean {
|
|
160
|
+
return this.worker.isRunning();
|
|
161
|
+
},
|
|
162
|
+
isPaused(this: Ctx): boolean {
|
|
163
|
+
return this.worker.isPaused();
|
|
164
|
+
},
|
|
165
|
+
isClosed(this: Ctx): boolean {
|
|
166
|
+
return this.worker.isClosed();
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/** Declaration-merged API installed on the Bunqueue prototype. */
|
|
171
|
+
export interface BunqueueApi<T = unknown, R = unknown> {
|
|
172
|
+
cron(
|
|
173
|
+
id: string,
|
|
174
|
+
pattern: string,
|
|
175
|
+
data?: T,
|
|
176
|
+
opts?: { timezone?: string; jobOpts?: JobOptions }
|
|
177
|
+
): Promise<Raw | null>;
|
|
178
|
+
every(
|
|
179
|
+
id: string,
|
|
180
|
+
intervalMs: number,
|
|
181
|
+
data?: T,
|
|
182
|
+
opts?: { jobOpts?: JobOptions }
|
|
183
|
+
): Promise<Raw | null>;
|
|
184
|
+
removeCron(id: string): Promise<void>;
|
|
185
|
+
listCrons(): Promise<Raw[]>;
|
|
186
|
+
cancel(jobId: string, gracePeriodMs?: number): void;
|
|
187
|
+
isCancelled(jobId: string): boolean;
|
|
188
|
+
getSignal(jobId: string): AbortSignal | null;
|
|
189
|
+
getCircuitState(): CircuitState;
|
|
190
|
+
resetCircuit(): void;
|
|
191
|
+
trigger(rule: TriggerRule<T>): Bunqueue<T, R>;
|
|
192
|
+
setDefaultTtl(ttlMs: number): void;
|
|
193
|
+
setNameTtl(name: string, ttlMs: number): void;
|
|
194
|
+
setDlqConfig(config: Raw): Promise<void>;
|
|
195
|
+
getDlqConfig(): Promise<Raw>;
|
|
196
|
+
getDlq(filter?: DlqFilter): Promise<Raw[]>;
|
|
197
|
+
getDlqStats(): Promise<DlqStats>;
|
|
198
|
+
retryDlq(id?: string): Promise<number>;
|
|
199
|
+
purgeDlq(): Promise<number>;
|
|
200
|
+
setGlobalRateLimit(max: number, duration?: number): Promise<void>;
|
|
201
|
+
removeGlobalRateLimit(): Promise<void>;
|
|
202
|
+
on(event: string, listener: (...args: never[]) => void): Bunqueue<T, R>;
|
|
203
|
+
once(event: string, listener: (...args: never[]) => void): Bunqueue<T, R>;
|
|
204
|
+
off(event: string, listener: (...args: never[]) => void): Bunqueue<T, R>;
|
|
205
|
+
pause(): void;
|
|
206
|
+
resume(): void;
|
|
207
|
+
close(force?: boolean): Promise<void>;
|
|
208
|
+
isRunning(): boolean;
|
|
209
|
+
isPaused(): boolean;
|
|
210
|
+
isClosed(): boolean;
|
|
211
|
+
getJob(id: string): Promise<Job<T> | null>;
|
|
212
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bunqueue — simplified all-in-one Queue + Worker (Simple Mode).
|
|
3
|
+
* 1:1 port of src/client/bunqueue.ts from the official client, TCP mode only
|
|
4
|
+
* (the embedded mode requires the in-process Bun runtime). The delegation
|
|
5
|
+
* API (cron, DLQ, events, control, …) lives in bunqueue-api.ts and is merged
|
|
6
|
+
* onto the prototype below.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Job } from '../job.js';
|
|
10
|
+
import { Queue } from '../queue.js';
|
|
11
|
+
import type { JobOptions } from '../types.js';
|
|
12
|
+
import { Worker } from '../worker.js';
|
|
13
|
+
import type { Processor } from '../worker-types.js';
|
|
14
|
+
import { PriorityAger } from './aging.js';
|
|
15
|
+
import { BatchAccumulator } from './batch.js';
|
|
16
|
+
import { type BunqueueApi, bunqueueApi } from './bunqueue-api.js';
|
|
17
|
+
import { CancellationManager } from './cancellation.js';
|
|
18
|
+
import { WorkerCircuitBreaker } from './circuit-breaker.js';
|
|
19
|
+
import { DedupDebounceMerger } from './dedup-debounce.js';
|
|
20
|
+
import { DlqRateLimitManager } from './dlq-rate-limit.js';
|
|
21
|
+
import { RateGate } from './rate-gate.js';
|
|
22
|
+
import { executeWithRetry } from './retry.js';
|
|
23
|
+
import { TriggerManager } from './triggers.js';
|
|
24
|
+
import { TtlChecker } from './ttl.js';
|
|
25
|
+
import type { BunqueueMiddleware, BunqueueOptions } from './types.js';
|
|
26
|
+
|
|
27
|
+
type Raw = Record<string, unknown>;
|
|
28
|
+
|
|
29
|
+
// biome-ignore lint/suspicious/noUnsafeDeclarationMerging: prototype-mixin composition — Object.assign below installs exactly the methods BunqueueApi declares
|
|
30
|
+
export class Bunqueue<T = unknown, R = unknown> {
|
|
31
|
+
readonly name: string;
|
|
32
|
+
readonly queue: Queue<T>;
|
|
33
|
+
readonly worker: Worker<T, R>;
|
|
34
|
+
/** @internal */ readonly cancellation = new CancellationManager();
|
|
35
|
+
/** @internal */ readonly cb: WorkerCircuitBreaker | null;
|
|
36
|
+
/** @internal */ readonly triggerMgr: TriggerManager<T, R>;
|
|
37
|
+
/** @internal */ readonly ager: PriorityAger<T> | null;
|
|
38
|
+
/** @internal */ readonly ttlChecker: TtlChecker | null;
|
|
39
|
+
/** @internal */ readonly batchAcc: BatchAccumulator<T, R> | null;
|
|
40
|
+
/** @internal */ readonly dlqrl: DlqRateLimitManager<T>;
|
|
41
|
+
private readonly middlewares: BunqueueMiddleware<T, R>[] = [];
|
|
42
|
+
private readonly baseProcessor: Processor<T, R>;
|
|
43
|
+
private readonly retryConfig: BunqueueOptions<T, R>['retry'] | null;
|
|
44
|
+
private readonly merger: DedupDebounceMerger;
|
|
45
|
+
private readonly rateGate: RateGate | null;
|
|
46
|
+
private readonly defaultJobOptions: JobOptions | undefined;
|
|
47
|
+
|
|
48
|
+
constructor(name: string, opts: BunqueueOptions<T, R>) {
|
|
49
|
+
if ((opts as Raw).embedded || (opts as Raw).dataPath) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
'Bunqueue (bunqueue-client) is TCP-only: embedded mode requires the Bun runtime — use the official "bunqueue" package on Bun instead'
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
const modes = [opts.processor, opts.routes, opts.batch].filter(Boolean).length;
|
|
55
|
+
if (modes === 0) throw new Error('Bunqueue requires "processor", "routes", or "batch"');
|
|
56
|
+
if (modes > 1) throw new Error('Bunqueue: use only one of "processor", "routes", or "batch"');
|
|
57
|
+
|
|
58
|
+
this.name = (opts.prefixKey ?? '') + name;
|
|
59
|
+
this.retryConfig = opts.retry ?? null;
|
|
60
|
+
this.ttlChecker = opts.ttl ? new TtlChecker(opts.ttl) : null;
|
|
61
|
+
this.merger = new DedupDebounceMerger(opts.deduplication ?? null, opts.debounce ?? null);
|
|
62
|
+
const limiter = opts.rateLimit ?? opts.limiter;
|
|
63
|
+
this.rateGate = limiter ? new RateGate(limiter) : null;
|
|
64
|
+
this.defaultJobOptions = this.buildDefaultJobOptions(opts);
|
|
65
|
+
|
|
66
|
+
// Build base processor
|
|
67
|
+
if (opts.batch) {
|
|
68
|
+
this.batchAcc = new BatchAccumulator<T, R>(opts.batch);
|
|
69
|
+
this.baseProcessor = this.batchAcc.buildProcessor();
|
|
70
|
+
} else {
|
|
71
|
+
this.batchAcc = null;
|
|
72
|
+
this.baseProcessor = opts.routes
|
|
73
|
+
? this.buildRouteProcessor(opts.routes)
|
|
74
|
+
: (opts.processor as Processor<T, R>);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const wrappedProcessor: Processor<T, R> = (job: Job<T>) => this.processJob(job);
|
|
78
|
+
|
|
79
|
+
const conn = opts.connection ?? {};
|
|
80
|
+
this.queue = new Queue<T>(this.name, conn);
|
|
81
|
+
this.worker = new Worker<T, R>(this.name, wrappedProcessor, {
|
|
82
|
+
...conn,
|
|
83
|
+
concurrency: opts.concurrency,
|
|
84
|
+
autorun: opts.autorun,
|
|
85
|
+
heartbeatIntervalS:
|
|
86
|
+
opts.heartbeatInterval !== undefined ? opts.heartbeatInterval / 1000 : undefined,
|
|
87
|
+
batchSize: opts.batchSize,
|
|
88
|
+
pollTimeoutMs: opts.pollTimeout,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// DLQ & rate limit manager
|
|
92
|
+
this.dlqrl = new DlqRateLimitManager<T>(this.queue);
|
|
93
|
+
if (opts.dlq) void this.dlqrl.setDlqConfig(opts.dlq);
|
|
94
|
+
|
|
95
|
+
// Subsystems
|
|
96
|
+
this.cb = opts.circuitBreaker
|
|
97
|
+
? new WorkerCircuitBreaker(opts.circuitBreaker, this.worker)
|
|
98
|
+
: null;
|
|
99
|
+
this.triggerMgr = new TriggerManager<T, R>(this.queue, this.worker);
|
|
100
|
+
this.ager = opts.priorityAging ? new PriorityAger<T>(opts.priorityAging, this.queue) : null;
|
|
101
|
+
this.ager?.start();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private buildRouteProcessor(routes: Record<string, Processor<T, R>>): Processor<T, R> {
|
|
105
|
+
const routeMap: Partial<Record<string, Processor<T, R>>> = routes;
|
|
106
|
+
return (job: Job<T>): Promise<R> | R => {
|
|
107
|
+
const handler = routeMap[job.name ?? ''];
|
|
108
|
+
if (!handler) throw new Error(`No route for job "${job.name}" in queue "${this.name}"`);
|
|
109
|
+
return handler(job);
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private buildDefaultJobOptions(opts: BunqueueOptions<T, R>): JobOptions | undefined {
|
|
114
|
+
const base: JobOptions = { ...opts.defaultJobOptions };
|
|
115
|
+
if (opts.removeOnComplete !== undefined) base.removeOnComplete ??= opts.removeOnComplete;
|
|
116
|
+
if (opts.removeOnFail !== undefined) base.removeOnFail ??= opts.removeOnFail;
|
|
117
|
+
return Object.keys(base).length > 0 ? base : undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ------------------------------------------------- core processing pipeline
|
|
121
|
+
|
|
122
|
+
private async processJob(job: Job<T>): Promise<R> {
|
|
123
|
+
if (this.rateGate) await this.rateGate.acquire(this.rateGate.groupFor(job.data));
|
|
124
|
+
// Circuit breaker check
|
|
125
|
+
if (this.cb?.isOpen()) {
|
|
126
|
+
throw new Error('Circuit breaker is open');
|
|
127
|
+
}
|
|
128
|
+
// TTL check
|
|
129
|
+
if (this.ttlChecker?.isExpired(job.name ?? '', job.timestamp)) {
|
|
130
|
+
throw new Error(`Job expired (age: ${Date.now() - job.timestamp}ms)`);
|
|
131
|
+
}
|
|
132
|
+
// Register cancellation
|
|
133
|
+
const ac = this.cancellation.register(job.id);
|
|
134
|
+
const runChain = () => this.runMiddlewareChain(job, ac);
|
|
135
|
+
const execute = this.retryConfig ? executeWithRetry(runChain, this.retryConfig) : runChain();
|
|
136
|
+
|
|
137
|
+
return execute.then(
|
|
138
|
+
(result) => {
|
|
139
|
+
this.cb?.onSuccess();
|
|
140
|
+
this.cancellation.unregister(job.id);
|
|
141
|
+
return result;
|
|
142
|
+
},
|
|
143
|
+
(err: unknown) => {
|
|
144
|
+
this.cb?.onFailure();
|
|
145
|
+
this.cancellation.unregister(job.id);
|
|
146
|
+
throw err;
|
|
147
|
+
}
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private runMiddlewareChain(job: Job<T>, ac: AbortController): Promise<R> {
|
|
152
|
+
if (this.middlewares.length === 0) {
|
|
153
|
+
const result = this.baseProcessor(job);
|
|
154
|
+
return result instanceof Promise ? result : Promise.resolve(result);
|
|
155
|
+
}
|
|
156
|
+
let index = 0;
|
|
157
|
+
const mws = this.middlewares;
|
|
158
|
+
const base = this.baseProcessor;
|
|
159
|
+
const next = (): Promise<R> => {
|
|
160
|
+
if (ac.signal.aborted) return Promise.reject(new Error('Job cancelled'));
|
|
161
|
+
if (index < mws.length) return mws[index++](job, next);
|
|
162
|
+
const result = base(job);
|
|
163
|
+
return result instanceof Promise ? result : Promise.resolve(result);
|
|
164
|
+
};
|
|
165
|
+
return next();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// --------------------------------------------------------------- middleware
|
|
169
|
+
|
|
170
|
+
use(middleware: BunqueueMiddleware<T, R>): this {
|
|
171
|
+
this.middlewares.push(middleware);
|
|
172
|
+
return this;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// --------------------------------------------------------- queue operations
|
|
176
|
+
|
|
177
|
+
add(name: string, data: T, opts?: JobOptions): Promise<Job<T>> {
|
|
178
|
+
const merged = { ...this.defaultJobOptions, ...opts };
|
|
179
|
+
return this.queue.add(name, data, this.merger.merge(name, merged, data));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
addBulk(jobs: Array<{ name: string; data: T; opts?: JobOptions }>): Promise<Job<T>[]> {
|
|
183
|
+
return this.queue.addBulk(
|
|
184
|
+
jobs.map((j) => ({
|
|
185
|
+
...j,
|
|
186
|
+
opts: this.merger.merge(j.name, { ...this.defaultJobOptions, ...j.opts }, j.data),
|
|
187
|
+
}))
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
getJob(id: string): Promise<Job<T> | null> {
|
|
192
|
+
return this.queue.getJob(id);
|
|
193
|
+
}
|
|
194
|
+
getJobCounts() {
|
|
195
|
+
return this.queue.getJobCounts();
|
|
196
|
+
}
|
|
197
|
+
getJobCountsAsync() {
|
|
198
|
+
return this.queue.getJobCounts();
|
|
199
|
+
}
|
|
200
|
+
count() {
|
|
201
|
+
return this.queue.count();
|
|
202
|
+
}
|
|
203
|
+
countAsync() {
|
|
204
|
+
return this.queue.count();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export interface Bunqueue<T = unknown, R = unknown> extends BunqueueApi<T, R> {}
|
|
209
|
+
|
|
210
|
+
Object.assign(Bunqueue.prototype, bunqueueApi);
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bunqueue Simple Mode — graceful job cancellation.
|
|
3
|
+
* 1:1 port of src/client/bunqueue/cancellation.ts: AbortController-based
|
|
4
|
+
* cancellation with optional grace period.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export class CancellationManager {
|
|
8
|
+
private readonly controllers = new Map<string, AbortController>();
|
|
9
|
+
|
|
10
|
+
/** Register a new AbortController for a job. */
|
|
11
|
+
register(jobId: string): AbortController {
|
|
12
|
+
const ac = new AbortController();
|
|
13
|
+
this.controllers.set(jobId, ac);
|
|
14
|
+
return ac;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Remove a job's controller (on completion). */
|
|
18
|
+
unregister(jobId: string): void {
|
|
19
|
+
this.controllers.delete(jobId);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Cancel a job with optional grace period. */
|
|
23
|
+
cancel(jobId: string, gracePeriodMs = 0): void {
|
|
24
|
+
const ac = this.controllers.get(jobId);
|
|
25
|
+
if (!ac) return;
|
|
26
|
+
|
|
27
|
+
if (gracePeriodMs > 0) {
|
|
28
|
+
setTimeout(() => {
|
|
29
|
+
ac.abort();
|
|
30
|
+
}, gracePeriodMs);
|
|
31
|
+
} else {
|
|
32
|
+
ac.abort();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Check if a job is cancelled. */
|
|
37
|
+
isCancelled(jobId: string): boolean {
|
|
38
|
+
const ac = this.controllers.get(jobId);
|
|
39
|
+
return ac ? ac.signal.aborted : false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Get the AbortSignal for a job. */
|
|
43
|
+
getSignal(jobId: string): AbortSignal | null {
|
|
44
|
+
return this.controllers.get(jobId)?.signal ?? null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Cancel all and clear. */
|
|
48
|
+
destroyAll(): void {
|
|
49
|
+
for (const ac of this.controllers.values()) {
|
|
50
|
+
ac.abort();
|
|
51
|
+
}
|
|
52
|
+
this.controllers.clear();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bunqueue Simple Mode — circuit breaker for worker protection.
|
|
3
|
+
* 1:1 port of src/client/bunqueue/circuitBreaker.ts.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { WorkerBase } from '../worker-base.js';
|
|
7
|
+
import type { CircuitBreakerConfig, CircuitState } from './types.js';
|
|
8
|
+
|
|
9
|
+
export class WorkerCircuitBreaker {
|
|
10
|
+
private state: CircuitState = 'closed';
|
|
11
|
+
private failures = 0;
|
|
12
|
+
private timer: ReturnType<typeof setTimeout> | null = null;
|
|
13
|
+
private readonly config: CircuitBreakerConfig;
|
|
14
|
+
private readonly worker: WorkerBase;
|
|
15
|
+
|
|
16
|
+
constructor(config: CircuitBreakerConfig, worker: WorkerBase) {
|
|
17
|
+
this.config = config;
|
|
18
|
+
this.worker = worker;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
get currentState(): CircuitState {
|
|
22
|
+
return this.state;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
isOpen(): boolean {
|
|
26
|
+
return this.state === 'open';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
onSuccess(): void {
|
|
30
|
+
if (this.state === 'half-open') {
|
|
31
|
+
this.state = 'closed';
|
|
32
|
+
this.failures = 0;
|
|
33
|
+
this.config.onClose?.();
|
|
34
|
+
} else if (this.state === 'closed') {
|
|
35
|
+
this.failures = 0;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
onFailure(): void {
|
|
40
|
+
this.failures++;
|
|
41
|
+
const threshold = this.config.threshold ?? 5;
|
|
42
|
+
|
|
43
|
+
if (this.state === 'half-open' || this.failures >= threshold) {
|
|
44
|
+
this.open();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private open(): void {
|
|
49
|
+
this.state = 'open';
|
|
50
|
+
this.config.onOpen?.(this.failures);
|
|
51
|
+
this.worker.pause();
|
|
52
|
+
|
|
53
|
+
const resetTimeout = this.config.resetTimeout ?? 30000;
|
|
54
|
+
if (this.timer) clearTimeout(this.timer);
|
|
55
|
+
this.timer = setTimeout(() => {
|
|
56
|
+
this.state = 'half-open';
|
|
57
|
+
this.config.onHalfOpen?.();
|
|
58
|
+
this.worker.resume();
|
|
59
|
+
}, resetTimeout);
|
|
60
|
+
this.timer.unref?.();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
reset(): void {
|
|
64
|
+
this.state = 'closed';
|
|
65
|
+
this.failures = 0;
|
|
66
|
+
if (this.timer) {
|
|
67
|
+
clearTimeout(this.timer);
|
|
68
|
+
this.timer = null;
|
|
69
|
+
}
|
|
70
|
+
if (this.worker.isPaused()) {
|
|
71
|
+
this.worker.resume();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
destroy(): void {
|
|
76
|
+
if (this.timer) {
|
|
77
|
+
clearTimeout(this.timer);
|
|
78
|
+
this.timer = null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bunqueue Simple Mode — deduplication and debounce defaults merger.
|
|
3
|
+
* 1:1 port of src/client/bunqueue/dedupDebounce.ts, including the dedup id
|
|
4
|
+
* derivation algorithm: `${name}:${JSON.stringify(data)}`.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { JobOptions } from '../types.js';
|
|
8
|
+
import type { BunqueueDebounceConfig, BunqueueDeduplicationConfig } from './types.js';
|
|
9
|
+
|
|
10
|
+
export class DedupDebounceMerger {
|
|
11
|
+
private readonly dedup: BunqueueDeduplicationConfig | null;
|
|
12
|
+
private readonly debounce: BunqueueDebounceConfig | null;
|
|
13
|
+
|
|
14
|
+
constructor(dedup: BunqueueDeduplicationConfig | null, debounce: BunqueueDebounceConfig | null) {
|
|
15
|
+
this.dedup = dedup;
|
|
16
|
+
this.debounce = debounce;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
get active(): boolean {
|
|
20
|
+
return this.dedup !== null || this.debounce !== null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
merge(name: string, opts?: JobOptions, data?: unknown): JobOptions | undefined {
|
|
24
|
+
if (!this.active) return opts;
|
|
25
|
+
const merged: JobOptions = { ...opts };
|
|
26
|
+
if (this.dedup && !merged.deduplication) {
|
|
27
|
+
const dataKey = data !== undefined ? JSON.stringify(data) : '';
|
|
28
|
+
merged.deduplication = {
|
|
29
|
+
id: `${name}:${dataKey}`,
|
|
30
|
+
ttl: this.dedup.ttl ?? 3600000,
|
|
31
|
+
extend: this.dedup.extend,
|
|
32
|
+
replace: this.dedup.replace,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
if (this.debounce && !merged.debounce) {
|
|
36
|
+
merged.debounce = {
|
|
37
|
+
id: name,
|
|
38
|
+
ttl: this.debounce.ttl,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return merged;
|
|
42
|
+
}
|
|
43
|
+
}
|