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,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bunqueue Simple Mode — DLQ and rate limit helpers.
|
|
3
|
+
* Port of src/client/bunqueue/dlqRateLimit.ts. Delegates to the TCP Queue;
|
|
4
|
+
* the DLQ filter and stats are computed client-side from the fetched entries
|
|
5
|
+
* (the embedded original reads them in-process).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Queue } from '../queue.js';
|
|
9
|
+
import type { BunqueueDlqConfig } from './types.js';
|
|
10
|
+
|
|
11
|
+
type Raw = Record<string, unknown>;
|
|
12
|
+
|
|
13
|
+
/** Filter for DLQ queries (mirrors the official DlqFilter). */
|
|
14
|
+
export interface DlqFilter {
|
|
15
|
+
reason?: string;
|
|
16
|
+
jobName?: string;
|
|
17
|
+
since?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Aggregate DLQ statistics (mirrors the official DlqStats). */
|
|
21
|
+
export interface DlqStats {
|
|
22
|
+
total: number;
|
|
23
|
+
byReason: Record<string, number>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function entryReason(entry: Raw): string {
|
|
27
|
+
return String(entry.reason ?? (entry.entry as Raw | undefined)?.reason ?? 'unknown');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function entryName(entry: Raw): string | undefined {
|
|
31
|
+
const data = (entry.data ?? (entry.entry as Raw | undefined)?.data) as Raw | undefined;
|
|
32
|
+
return data?.name === undefined ? undefined : String(data.name);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class DlqRateLimitManager<T = unknown> {
|
|
36
|
+
constructor(private readonly queue: Queue<T>) {}
|
|
37
|
+
|
|
38
|
+
// ------------------------------------------------------------------- dlq
|
|
39
|
+
|
|
40
|
+
async setDlqConfig(config: BunqueueDlqConfig): Promise<void> {
|
|
41
|
+
await this.queue.setDlqConfig(config as Raw);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
getDlqConfig(): Promise<Raw> {
|
|
45
|
+
return this.queue.getDlqConfig();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async getDlq(filter?: DlqFilter): Promise<Raw[]> {
|
|
49
|
+
const entries = await this.queue.getDlq();
|
|
50
|
+
if (!filter) return entries;
|
|
51
|
+
return entries.filter((entry) => {
|
|
52
|
+
if (filter.reason && entryReason(entry) !== filter.reason) return false;
|
|
53
|
+
if (filter.jobName && entryName(entry) !== filter.jobName) return false;
|
|
54
|
+
if (filter.since !== undefined) {
|
|
55
|
+
const at = Number(entry.enteredAt ?? entry.failedAt ?? 0);
|
|
56
|
+
if (at < filter.since) return false;
|
|
57
|
+
}
|
|
58
|
+
return true;
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async getDlqStats(): Promise<DlqStats> {
|
|
63
|
+
const entries = await this.queue.getDlq();
|
|
64
|
+
const byReason: Record<string, number> = {};
|
|
65
|
+
for (const entry of entries) {
|
|
66
|
+
const reason = entryReason(entry);
|
|
67
|
+
byReason[reason] = (byReason[reason] ?? 0) + 1;
|
|
68
|
+
}
|
|
69
|
+
return { total: entries.length, byReason };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
retryDlq(id?: string): Promise<number> {
|
|
73
|
+
return this.queue.retryDlq(id);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
purgeDlq(): Promise<number> {
|
|
77
|
+
return this.queue.purgeDlq();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ----------------------------------------------------------- rate limiting
|
|
81
|
+
|
|
82
|
+
async setGlobalRateLimit(max: number, duration?: number): Promise<void> {
|
|
83
|
+
await this.queue.setGlobalRateLimit(max, duration);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async removeGlobalRateLimit(): Promise<void> {
|
|
87
|
+
await this.queue.removeGlobalRateLimit();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bunqueue Simple Mode — client-side rate limiter for the worker.
|
|
3
|
+
*
|
|
4
|
+
* The official client passes `rateLimit`/`limiter` to the embedded Worker,
|
|
5
|
+
* which throttles job starts to `max` per `duration` window, optionally per
|
|
6
|
+
* group (`groupKey` names a field of job.data). This SDK reproduces the same
|
|
7
|
+
* semantics as a gate awaited before each job runs: a job whose window is
|
|
8
|
+
* full waits (holding its concurrency slot) until the window frees.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { RateLimiterOptions } from './types.js';
|
|
12
|
+
|
|
13
|
+
const sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
|
|
14
|
+
|
|
15
|
+
export class RateGate {
|
|
16
|
+
private readonly max: number;
|
|
17
|
+
private readonly duration: number;
|
|
18
|
+
private readonly groupKey: string | undefined;
|
|
19
|
+
private readonly windows = new Map<string, number[]>();
|
|
20
|
+
|
|
21
|
+
constructor(options: RateLimiterOptions) {
|
|
22
|
+
this.max = options.max;
|
|
23
|
+
this.duration = options.duration;
|
|
24
|
+
this.groupKey = options.groupKey;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Resolve the rate-limit group for a job's data. */
|
|
28
|
+
groupFor(data: unknown): string {
|
|
29
|
+
if (!this.groupKey) return '';
|
|
30
|
+
if (typeof data === 'object' && data !== null) {
|
|
31
|
+
const value = (data as Record<string, unknown>)[this.groupKey];
|
|
32
|
+
if (value !== undefined && value !== null) return String(value);
|
|
33
|
+
}
|
|
34
|
+
return '';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Wait until the group's window has room, then record the start. */
|
|
38
|
+
async acquire(group: string): Promise<void> {
|
|
39
|
+
for (;;) {
|
|
40
|
+
const now = Date.now();
|
|
41
|
+
const window = this.windows.get(group) ?? [];
|
|
42
|
+
const fresh = window.filter((t) => now - t < this.duration);
|
|
43
|
+
if (fresh.length < this.max) {
|
|
44
|
+
fresh.push(now);
|
|
45
|
+
this.windows.set(group, fresh);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
this.windows.set(group, fresh);
|
|
49
|
+
const oldest = fresh[0];
|
|
50
|
+
await sleep(Math.max(oldest + this.duration - now, 10));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bunqueue Simple Mode — advanced in-process retry with backoff strategies.
|
|
3
|
+
* 1:1 port of src/client/bunqueue/retry.ts (identical formulas).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { RetryConfig, RetryStrategy } from './types.js';
|
|
7
|
+
|
|
8
|
+
/** Calculate backoff delay based on strategy. */
|
|
9
|
+
export function calculateBackoff(
|
|
10
|
+
strategy: RetryStrategy,
|
|
11
|
+
attempt: number,
|
|
12
|
+
baseDelay: number,
|
|
13
|
+
error: Error,
|
|
14
|
+
config: RetryConfig
|
|
15
|
+
): number {
|
|
16
|
+
switch (strategy) {
|
|
17
|
+
case 'fixed':
|
|
18
|
+
return baseDelay;
|
|
19
|
+
|
|
20
|
+
case 'exponential':
|
|
21
|
+
return baseDelay * 2 ** (attempt - 1);
|
|
22
|
+
|
|
23
|
+
case 'jitter': {
|
|
24
|
+
const exp = baseDelay * 2 ** (attempt - 1);
|
|
25
|
+
return Math.floor(exp * (0.5 + Math.random()));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
case 'fibonacci': {
|
|
29
|
+
let a = 1;
|
|
30
|
+
let b = 1;
|
|
31
|
+
for (let i = 0; i < attempt - 1; i++) {
|
|
32
|
+
const next = a + b;
|
|
33
|
+
a = b;
|
|
34
|
+
b = next;
|
|
35
|
+
}
|
|
36
|
+
return baseDelay * b;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
case 'custom':
|
|
40
|
+
if (config.customBackoff) {
|
|
41
|
+
return config.customBackoff(attempt, error);
|
|
42
|
+
}
|
|
43
|
+
return baseDelay;
|
|
44
|
+
|
|
45
|
+
default:
|
|
46
|
+
return baseDelay;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Execute a function with retry logic (the job stays active throughout). */
|
|
51
|
+
export function executeWithRetry<R>(fn: () => Promise<R>, config: RetryConfig): Promise<R> {
|
|
52
|
+
const maxAttempts = config.maxAttempts ?? 3;
|
|
53
|
+
const baseDelay = config.delay ?? 1000;
|
|
54
|
+
const strategy = config.strategy ?? 'exponential';
|
|
55
|
+
|
|
56
|
+
const attempt = (n: number): Promise<R> => {
|
|
57
|
+
return fn().catch((err: unknown) => {
|
|
58
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
59
|
+
if (n >= maxAttempts) throw error;
|
|
60
|
+
if (config.retryIf && !config.retryIf(error, n)) throw error;
|
|
61
|
+
|
|
62
|
+
const delay = calculateBackoff(strategy, n, baseDelay, error, config);
|
|
63
|
+
return new Promise<R>((resolve) => {
|
|
64
|
+
setTimeout(() => {
|
|
65
|
+
resolve(attempt(n + 1));
|
|
66
|
+
}, delay);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
return attempt(1);
|
|
72
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bunqueue Simple Mode — event triggers.
|
|
3
|
+
* 1:1 port of src/client/bunqueue/triggers.ts: when a job completes/fails,
|
|
4
|
+
* automatically create another job.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Job } from '../job.js';
|
|
8
|
+
import type { Queue } from '../queue.js';
|
|
9
|
+
import type { Worker } from '../worker.js';
|
|
10
|
+
import type { TriggerRule } from './types.js';
|
|
11
|
+
|
|
12
|
+
export class TriggerManager<T = unknown, R = unknown> {
|
|
13
|
+
private readonly rules: TriggerRule<T>[] = [];
|
|
14
|
+
private active = false;
|
|
15
|
+
private readonly queue: Queue<T>;
|
|
16
|
+
private readonly worker: Worker<T, R>;
|
|
17
|
+
|
|
18
|
+
constructor(queue: Queue<T>, worker: Worker<T, R>) {
|
|
19
|
+
this.queue = queue;
|
|
20
|
+
this.worker = worker;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
add(rule: TriggerRule<T>): void {
|
|
24
|
+
this.rules.push(rule);
|
|
25
|
+
this.ensureActive();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private ensureActive(): void {
|
|
29
|
+
if (this.active) return;
|
|
30
|
+
this.active = true;
|
|
31
|
+
|
|
32
|
+
this.worker.on('completed', (job, result) => {
|
|
33
|
+
this.fire('completed', job as Job<T>, result);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
this.worker.on('failed', (job, error) => {
|
|
37
|
+
this.fire('failed', job as Job<T>, error);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private fire(event: 'completed' | 'failed', job: Job<T>, resultOrError: unknown): void {
|
|
42
|
+
for (const rule of this.rules) {
|
|
43
|
+
if (rule.on !== job.name) continue;
|
|
44
|
+
if ((rule.event ?? 'completed') !== event) continue;
|
|
45
|
+
if (rule.condition && !rule.condition(resultOrError, job)) continue;
|
|
46
|
+
|
|
47
|
+
const data = rule.data(resultOrError, job);
|
|
48
|
+
void this.queue.add(rule.create, data, rule.opts);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bunqueue Simple Mode — job TTL (time to live).
|
|
3
|
+
* 1:1 port of src/client/bunqueue/ttl.ts: expire unprocessed jobs, checked
|
|
4
|
+
* when the worker picks the job up. Resolution: perName -> defaultTtl -> 0.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { JobTtlConfig } from './types.js';
|
|
8
|
+
|
|
9
|
+
export class TtlChecker {
|
|
10
|
+
private readonly config: JobTtlConfig;
|
|
11
|
+
|
|
12
|
+
constructor(config: JobTtlConfig) {
|
|
13
|
+
this.config = config;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Get TTL for a specific job name. */
|
|
17
|
+
getTtl(jobName: string): number {
|
|
18
|
+
return this.config.perName?.[jobName] ?? this.config.defaultTtl ?? 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Check if a job has expired. */
|
|
22
|
+
isExpired(jobName: string, jobTimestamp: number): boolean {
|
|
23
|
+
const ttl = this.getTtl(jobName);
|
|
24
|
+
if (ttl <= 0) return false;
|
|
25
|
+
return Date.now() - jobTimestamp > ttl;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Set default TTL. */
|
|
29
|
+
setDefaultTtl(ttlMs: number): void {
|
|
30
|
+
this.config.defaultTtl = ttlMs;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Set TTL for a specific job name. */
|
|
34
|
+
setNameTtl(jobName: string, ttlMs: number): void {
|
|
35
|
+
this.config.perName ??= {};
|
|
36
|
+
this.config.perName[jobName] = ttlMs;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bunqueue Simple Mode — type definitions.
|
|
3
|
+
* 1:1 port of src/client/bunqueue/types.ts from the official client, minus
|
|
4
|
+
* the embedded-mode options (this SDK is TCP-only).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { TlsOption } from '../connection.js';
|
|
8
|
+
import type { Job } from '../job.js';
|
|
9
|
+
import type { JobOptions } from '../types.js';
|
|
10
|
+
import type { Processor } from '../worker-types.js';
|
|
11
|
+
|
|
12
|
+
/** Connection options accepted by Simple Mode (mirrors ConnectionOptions). */
|
|
13
|
+
export interface BunqueueConnection {
|
|
14
|
+
host?: string;
|
|
15
|
+
port?: number;
|
|
16
|
+
token?: string;
|
|
17
|
+
tls?: TlsOption;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Middleware function: receives job and next(), returns result. */
|
|
21
|
+
export type BunqueueMiddleware<T = unknown, R = unknown> = (
|
|
22
|
+
job: Job<T>,
|
|
23
|
+
next: () => Promise<R>
|
|
24
|
+
) => Promise<R>;
|
|
25
|
+
|
|
26
|
+
/** Retry strategy for advanced backoff. */
|
|
27
|
+
export type RetryStrategy = 'fixed' | 'exponential' | 'jitter' | 'fibonacci' | 'custom';
|
|
28
|
+
|
|
29
|
+
/** Advanced retry configuration (in-process; the job stays active). */
|
|
30
|
+
export interface RetryConfig {
|
|
31
|
+
/** Max attempts (default: 3). */
|
|
32
|
+
maxAttempts?: number;
|
|
33
|
+
/** Base delay in ms (default: 1000). */
|
|
34
|
+
delay?: number;
|
|
35
|
+
/** Strategy (default: exponential). */
|
|
36
|
+
strategy?: RetryStrategy;
|
|
37
|
+
/** Custom backoff function: attempt, error -> delay in ms. */
|
|
38
|
+
customBackoff?: (attempt: number, error: Error) => number;
|
|
39
|
+
/** Only retry if this returns true. */
|
|
40
|
+
retryIf?: (error: Error, attempt: number) => boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Circuit breaker configuration. */
|
|
44
|
+
export interface CircuitBreakerConfig {
|
|
45
|
+
/** Max consecutive failures before opening (default: 5). */
|
|
46
|
+
threshold?: number;
|
|
47
|
+
/** Time in ms before half-open retry (default: 30000). */
|
|
48
|
+
resetTimeout?: number;
|
|
49
|
+
onOpen?: (failures: number) => void;
|
|
50
|
+
onClose?: () => void;
|
|
51
|
+
onHalfOpen?: () => void;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Circuit breaker state. */
|
|
55
|
+
export type CircuitState = 'closed' | 'open' | 'half-open';
|
|
56
|
+
|
|
57
|
+
/** Event trigger rule. */
|
|
58
|
+
export interface TriggerRule<T = unknown> {
|
|
59
|
+
/** Job name that triggers this rule. */
|
|
60
|
+
on: string;
|
|
61
|
+
/** Event type (default: completed). */
|
|
62
|
+
event?: 'completed' | 'failed';
|
|
63
|
+
/** Job name to create. */
|
|
64
|
+
create: string;
|
|
65
|
+
/** Data builder from the triggering job. */
|
|
66
|
+
data: (result: unknown, job: Job<T>) => T;
|
|
67
|
+
/** Optional job options. */
|
|
68
|
+
opts?: JobOptions;
|
|
69
|
+
/** Optional condition. */
|
|
70
|
+
condition?: (result: unknown, job: Job<T>) => boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Priority aging configuration. */
|
|
74
|
+
export interface PriorityAgingConfig {
|
|
75
|
+
/** Check interval in ms (default: 60000). */
|
|
76
|
+
interval?: number;
|
|
77
|
+
/** Min age in ms before boost (default: 60000). */
|
|
78
|
+
minAge?: number;
|
|
79
|
+
/** Priority boost per interval (default: 1). */
|
|
80
|
+
boost?: number;
|
|
81
|
+
/** Max priority cap (default: 100). */
|
|
82
|
+
maxPriority?: number;
|
|
83
|
+
/** Max jobs to scan per tick (default: 100). */
|
|
84
|
+
maxScan?: number;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Batch processor function. */
|
|
88
|
+
export type BatchProcessor<T = unknown, R = unknown> = (jobs: Array<Job<T>>) => Promise<R[]>;
|
|
89
|
+
|
|
90
|
+
/** Batch processing configuration. */
|
|
91
|
+
export interface BatchConfig<T = unknown, R = unknown> {
|
|
92
|
+
/** Batch size (default: 10). */
|
|
93
|
+
size: number;
|
|
94
|
+
/** Max wait in ms before flushing a partial batch (default: 5000). */
|
|
95
|
+
timeout?: number;
|
|
96
|
+
processor: BatchProcessor<T, R>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Job TTL configuration. */
|
|
100
|
+
export interface JobTtlConfig {
|
|
101
|
+
/** Default TTL in ms (0 = no TTL). */
|
|
102
|
+
defaultTtl?: number;
|
|
103
|
+
/** Per-job-name TTL overrides. */
|
|
104
|
+
perName?: Record<string, number>;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Deduplication defaults for Simple Mode. */
|
|
108
|
+
export interface BunqueueDeduplicationConfig {
|
|
109
|
+
/** Default deduplication TTL in ms (default: 3600000 = 1 hour). */
|
|
110
|
+
ttl?: number;
|
|
111
|
+
/** Extend TTL when a duplicate arrives (default: false). */
|
|
112
|
+
extend?: boolean;
|
|
113
|
+
/** Replace data when a duplicate arrives in delayed state (default: false). */
|
|
114
|
+
replace?: boolean;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Debounce defaults for Simple Mode. */
|
|
118
|
+
export interface BunqueueDebounceConfig {
|
|
119
|
+
/** Debounce TTL in ms. */
|
|
120
|
+
ttl: number;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** DLQ configuration for Simple Mode (forwarded to the server). */
|
|
124
|
+
export interface BunqueueDlqConfig {
|
|
125
|
+
autoRetry?: boolean;
|
|
126
|
+
autoRetryInterval?: number;
|
|
127
|
+
maxAutoRetries?: number;
|
|
128
|
+
maxAge?: number | null;
|
|
129
|
+
maxEntries?: number;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Client-side rate limiter options (max job starts per duration window). */
|
|
133
|
+
export interface RateLimiterOptions {
|
|
134
|
+
max: number;
|
|
135
|
+
duration: number;
|
|
136
|
+
/** Group jobs by this data field (e.g. per-customer limits). */
|
|
137
|
+
groupKey?: string;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Bunqueue Simple Mode options. */
|
|
141
|
+
export interface BunqueueOptions<T = unknown, R = unknown> {
|
|
142
|
+
processor?: Processor<T, R>;
|
|
143
|
+
routes?: Record<string, Processor<T, R>>;
|
|
144
|
+
batch?: BatchConfig<T, R>;
|
|
145
|
+
concurrency?: number;
|
|
146
|
+
connection?: BunqueueConnection;
|
|
147
|
+
defaultJobOptions?: JobOptions;
|
|
148
|
+
autorun?: boolean;
|
|
149
|
+
/** Worker heartbeat interval in ms (official-client unit). */
|
|
150
|
+
heartbeatInterval?: number;
|
|
151
|
+
batchSize?: number;
|
|
152
|
+
/** Long-poll timeout in ms. */
|
|
153
|
+
pollTimeout?: number;
|
|
154
|
+
limiter?: RateLimiterOptions;
|
|
155
|
+
removeOnComplete?: boolean;
|
|
156
|
+
removeOnFail?: boolean;
|
|
157
|
+
retry?: RetryConfig;
|
|
158
|
+
circuitBreaker?: CircuitBreakerConfig;
|
|
159
|
+
ttl?: JobTtlConfig;
|
|
160
|
+
priorityAging?: PriorityAgingConfig;
|
|
161
|
+
/** Job deduplication defaults, merged into every add(). */
|
|
162
|
+
deduplication?: BunqueueDeduplicationConfig;
|
|
163
|
+
/** Job debouncing defaults, merged into every add(). */
|
|
164
|
+
debounce?: BunqueueDebounceConfig;
|
|
165
|
+
/** Rate limiting for the worker (alias of limiter, takes precedence). */
|
|
166
|
+
rateLimit?: RateLimiterOptions;
|
|
167
|
+
/** Dead letter queue auto-management (forwarded to the server). */
|
|
168
|
+
dlq?: BunqueueDlqConfig;
|
|
169
|
+
/** Namespace prefix prepended to the queue name on the server. */
|
|
170
|
+
prefixKey?: string;
|
|
171
|
+
}
|
package/src/connection.ts
CHANGED
|
@@ -6,9 +6,7 @@
|
|
|
6
6
|
* builtins (net/tls), which Node, Bun and Deno all support.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
10
|
-
import { connect as netConnect, type Socket } from 'node:net';
|
|
11
|
-
import { type ConnectionOptions as TlsConnectOptions, connect as tlsConnect } from 'node:tls';
|
|
9
|
+
import type { Socket } from 'node:net';
|
|
12
10
|
import { pack, unpack } from 'msgpackr';
|
|
13
11
|
import type {
|
|
14
12
|
Command,
|
|
@@ -19,6 +17,7 @@ import type {
|
|
|
19
17
|
} from './connection-types.js';
|
|
20
18
|
import { AuthError, CommandError, CommandTimeoutError, ConnectionClosedError } from './errors.js';
|
|
21
19
|
import { compact, FrameParser, frame, PROTOCOL_VERSION } from './frame.js';
|
|
20
|
+
import { openSocket } from './socket-factory.js';
|
|
22
21
|
|
|
23
22
|
export type { Command, ConnectionOptions, Response, TlsOption } from './connection-types.js';
|
|
24
23
|
|
|
@@ -38,6 +37,9 @@ export class Connection {
|
|
|
38
37
|
private pending = new Map<string, Pending>();
|
|
39
38
|
private reqCounter = 0;
|
|
40
39
|
private parser = new FrameParser();
|
|
40
|
+
private connectGeneration = 0;
|
|
41
|
+
private failedAttempts = 0;
|
|
42
|
+
private nextAttemptAt = 0;
|
|
41
43
|
|
|
42
44
|
constructor(options: ConnectionOptions = {}) {
|
|
43
45
|
this.host = options.host ?? 'localhost';
|
|
@@ -52,19 +54,47 @@ export class Connection {
|
|
|
52
54
|
return this.connected;
|
|
53
55
|
}
|
|
54
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Monotonic counter, incremented on every successful (re)connect. Consumers
|
|
59
|
+
* that hold per-connection server state (e.g. a Worker registration) compare
|
|
60
|
+
* it between operations and re-establish that state after a reconnect.
|
|
61
|
+
*/
|
|
62
|
+
get generation(): number {
|
|
63
|
+
return this.connectGeneration;
|
|
64
|
+
}
|
|
65
|
+
|
|
55
66
|
/** Open the socket (and authenticate) if not already connected. */
|
|
56
67
|
async connect(): Promise<void> {
|
|
57
68
|
if (this.connected) return;
|
|
58
69
|
if (this.closed) throw new ConnectionClosedError('connection closed by client');
|
|
70
|
+
// Fast-fail while inside the reconnect backoff window: without this, a
|
|
71
|
+
// producer calling add() against a downed server pays the full connect
|
|
72
|
+
// timeout on EVERY call (reconnect storm).
|
|
73
|
+
if (Date.now() < this.nextAttemptAt) {
|
|
74
|
+
throw new ConnectionClosedError(
|
|
75
|
+
`server unreachable, retry in ${this.nextAttemptAt - Date.now()}ms`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
59
78
|
if (this.connecting) return this.connecting;
|
|
60
|
-
this.connecting = this.doConnect()
|
|
61
|
-
|
|
62
|
-
|
|
79
|
+
this.connecting = this.doConnect()
|
|
80
|
+
.then(() => {
|
|
81
|
+
this.failedAttempts = 0;
|
|
82
|
+
this.nextAttemptAt = 0;
|
|
83
|
+
})
|
|
84
|
+
.catch((err) => {
|
|
85
|
+
this.failedAttempts += 1;
|
|
86
|
+
const backoff = Math.min(500 * 2 ** (this.failedAttempts - 1), 5000);
|
|
87
|
+
this.nextAttemptAt = Date.now() + backoff;
|
|
88
|
+
throw err;
|
|
89
|
+
})
|
|
90
|
+
.finally(() => {
|
|
91
|
+
this.connecting = null;
|
|
92
|
+
});
|
|
63
93
|
return this.connecting;
|
|
64
94
|
}
|
|
65
95
|
|
|
66
96
|
private async doConnect(): Promise<void> {
|
|
67
|
-
const socket = await this.
|
|
97
|
+
const socket = await openSocket(this.host, this.port, this.tls, this.connectTimeoutMs);
|
|
68
98
|
socket.setNoDelay(true);
|
|
69
99
|
this.parser.clear();
|
|
70
100
|
this.socket = socket;
|
|
@@ -74,6 +104,7 @@ export class Connection {
|
|
|
74
104
|
socket.on('close', () => this.teardown());
|
|
75
105
|
|
|
76
106
|
this.connected = true;
|
|
107
|
+
this.connectGeneration += 1;
|
|
77
108
|
|
|
78
109
|
if (this.token) {
|
|
79
110
|
try {
|
|
@@ -86,45 +117,6 @@ export class Connection {
|
|
|
86
117
|
}
|
|
87
118
|
}
|
|
88
119
|
|
|
89
|
-
private openSocket(): Promise<Socket> {
|
|
90
|
-
return new Promise((resolve, reject) => {
|
|
91
|
-
let settled = false;
|
|
92
|
-
const timer = setTimeout(() => {
|
|
93
|
-
if (settled) return;
|
|
94
|
-
settled = true;
|
|
95
|
-
socket.destroy();
|
|
96
|
-
reject(new ConnectionClosedError(`connect timeout to ${this.host}:${this.port}`));
|
|
97
|
-
}, this.connectTimeoutMs);
|
|
98
|
-
|
|
99
|
-
const onError = (err: Error) => {
|
|
100
|
-
if (settled) return;
|
|
101
|
-
settled = true;
|
|
102
|
-
clearTimeout(timer);
|
|
103
|
-
reject(new ConnectionClosedError(`connect failed: ${err.message}`));
|
|
104
|
-
};
|
|
105
|
-
const onReady = () => {
|
|
106
|
-
if (settled) return;
|
|
107
|
-
settled = true;
|
|
108
|
-
clearTimeout(timer);
|
|
109
|
-
socket.off('error', onError);
|
|
110
|
-
resolve(socket);
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
let socket: Socket;
|
|
114
|
-
if (this.tls) {
|
|
115
|
-
const tlsOpts: TlsConnectOptions = { host: this.host, port: this.port };
|
|
116
|
-
if (typeof this.tls === 'object') {
|
|
117
|
-
if (this.tls.caFile) tlsOpts.ca = readFileSync(this.tls.caFile);
|
|
118
|
-
if (this.tls.rejectUnauthorized === false) tlsOpts.rejectUnauthorized = false;
|
|
119
|
-
}
|
|
120
|
-
socket = tlsConnect(tlsOpts, onReady);
|
|
121
|
-
} else {
|
|
122
|
-
socket = netConnect({ host: this.host, port: this.port }, onReady);
|
|
123
|
-
}
|
|
124
|
-
socket.on('error', onError);
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
|
|
128
120
|
/**
|
|
129
121
|
* Send a command and await its response. Rejects with CommandError when
|
|
130
122
|
* the server answers ok=false. Reconnects lazily if the link was lost.
|
package/src/index.ts
CHANGED
|
@@ -3,6 +3,27 @@
|
|
|
3
3
|
* Works on Node.js ≥20, Bun and Deno ≥2 (uses only node: builtins + msgpackr).
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
// Simple Mode (Bunqueue): all-in-one Queue + Worker, 1:1 with the official client
|
|
7
|
+
export { Bunqueue } from './bunqueue/bunqueue.js';
|
|
8
|
+
export type { DlqFilter, DlqStats } from './bunqueue/dlq-rate-limit.js';
|
|
9
|
+
export type {
|
|
10
|
+
BatchConfig,
|
|
11
|
+
BatchProcessor,
|
|
12
|
+
BunqueueConnection,
|
|
13
|
+
BunqueueDebounceConfig,
|
|
14
|
+
BunqueueDeduplicationConfig,
|
|
15
|
+
BunqueueDlqConfig,
|
|
16
|
+
BunqueueMiddleware,
|
|
17
|
+
BunqueueOptions,
|
|
18
|
+
CircuitBreakerConfig,
|
|
19
|
+
CircuitState,
|
|
20
|
+
JobTtlConfig,
|
|
21
|
+
PriorityAgingConfig,
|
|
22
|
+
RateLimiterOptions,
|
|
23
|
+
RetryConfig,
|
|
24
|
+
RetryStrategy,
|
|
25
|
+
TriggerRule,
|
|
26
|
+
} from './bunqueue/types.js';
|
|
6
27
|
export type { Command, ConnectionOptions, Response, TlsOption } from './connection.js';
|
|
7
28
|
export { Connection } from './connection.js';
|
|
8
29
|
export {
|
|
@@ -38,4 +59,4 @@ export type {
|
|
|
38
59
|
export { Worker } from './worker.js';
|
|
39
60
|
export type { Processor, WorkerOptions } from './worker-types.js';
|
|
40
61
|
|
|
41
|
-
export const __version__ = '0.1.
|
|
62
|
+
export const __version__ = '0.1.2';
|
package/src/job.ts
CHANGED
|
@@ -62,6 +62,11 @@ export class Job<T = unknown> {
|
|
|
62
62
|
return this.raw.createdAt === undefined ? undefined : Number(this.raw.createdAt);
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/** Creation time in epoch ms (BullMQ-parity alias of createdAt). */
|
|
66
|
+
get timestamp(): number {
|
|
67
|
+
return this.createdAt ?? Date.now();
|
|
68
|
+
}
|
|
69
|
+
|
|
65
70
|
get startedAt(): number | null {
|
|
66
71
|
return this.raw.startedAt === undefined || this.raw.startedAt === null
|
|
67
72
|
? null
|
package/src/queue-admin.ts
CHANGED
|
@@ -206,7 +206,8 @@ export const adminMethods = {
|
|
|
206
206
|
},
|
|
207
207
|
|
|
208
208
|
async setWebhookEnabled(this: Ctx, webhookId: string, enabled: boolean): Promise<void> {
|
|
209
|
-
|
|
209
|
+
// wire field is `id` (SetWebhookEnabledCommand), unlike RemoveWebhook's `webhookId`
|
|
210
|
+
await this.call({ cmd: 'SetWebhookEnabled', id: webhookId, enabled });
|
|
210
211
|
},
|
|
211
212
|
};
|
|
212
213
|
|