bunqueue-client 0.1.1 → 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.
Files changed (60) hide show
  1. package/README.md +91 -3
  2. package/dist/bunqueue/aging.d.ts +17 -0
  3. package/dist/bunqueue/aging.js +53 -0
  4. package/dist/bunqueue/batch.d.ts +17 -0
  5. package/dist/bunqueue/batch.js +60 -0
  6. package/dist/bunqueue/bunqueue-api.d.ts +87 -0
  7. package/dist/bunqueue/bunqueue-api.js +116 -0
  8. package/dist/bunqueue/bunqueue.d.ts +57 -0
  9. package/dist/bunqueue/bunqueue.js +182 -0
  10. package/dist/bunqueue/cancellation.d.ts +20 -0
  11. package/dist/bunqueue/cancellation.js +48 -0
  12. package/dist/bunqueue/circuit-breaker.d.ts +21 -0
  13. package/dist/bunqueue/circuit-breaker.js +69 -0
  14. package/dist/bunqueue/dedup-debounce.d.ts +14 -0
  15. package/dist/bunqueue/dedup-debounce.js +37 -0
  16. package/dist/bunqueue/dlq-rate-limit.d.ts +33 -0
  17. package/dist/bunqueue/dlq-rate-limit.js +65 -0
  18. package/dist/bunqueue/rate-gate.d.ts +21 -0
  19. package/dist/bunqueue/rate-gate.js +48 -0
  20. package/dist/bunqueue/retry.d.ts +9 -0
  21. package/dist/bunqueue/retry.js +56 -0
  22. package/dist/bunqueue/triggers.d.ts +18 -0
  23. package/dist/bunqueue/triggers.js +42 -0
  24. package/dist/bunqueue/ttl.d.ts +18 -0
  25. package/dist/bunqueue/ttl.js +31 -0
  26. package/dist/bunqueue/types.d.ts +151 -0
  27. package/dist/bunqueue/types.js +6 -0
  28. package/dist/connection.d.ts +9 -1
  29. package/dist/connection.js +32 -47
  30. package/dist/index.d.ts +4 -1
  31. package/dist/index.js +3 -1
  32. package/dist/job.d.ts +2 -0
  33. package/dist/job.js +4 -0
  34. package/dist/queue-admin.js +2 -1
  35. package/dist/socket-factory.d.ts +4 -0
  36. package/dist/socket-factory.js +47 -0
  37. package/dist/worker-base.d.ts +13 -2
  38. package/dist/worker-base.js +29 -4
  39. package/dist/worker.js +8 -0
  40. package/package.json +2 -2
  41. package/src/bunqueue/aging.ts +62 -0
  42. package/src/bunqueue/batch.ts +79 -0
  43. package/src/bunqueue/bunqueue-api.ts +212 -0
  44. package/src/bunqueue/bunqueue.ts +210 -0
  45. package/src/bunqueue/cancellation.ts +54 -0
  46. package/src/bunqueue/circuit-breaker.ts +81 -0
  47. package/src/bunqueue/dedup-debounce.ts +43 -0
  48. package/src/bunqueue/dlq-rate-limit.ts +89 -0
  49. package/src/bunqueue/rate-gate.ts +53 -0
  50. package/src/bunqueue/retry.ts +72 -0
  51. package/src/bunqueue/triggers.ts +51 -0
  52. package/src/bunqueue/ttl.ts +38 -0
  53. package/src/bunqueue/types.ts +171 -0
  54. package/src/connection.ts +38 -46
  55. package/src/index.ts +22 -1
  56. package/src/job.ts +5 -0
  57. package/src/queue-admin.ts +2 -1
  58. package/src/socket-factory.ts +51 -0
  59. package/src/worker-base.ts +30 -4
  60. package/src/worker.ts +9 -0
@@ -0,0 +1,182 @@
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
+ import { Queue } from '../queue.js';
9
+ import { Worker } from '../worker.js';
10
+ import { PriorityAger } from './aging.js';
11
+ import { BatchAccumulator } from './batch.js';
12
+ import { bunqueueApi } from './bunqueue-api.js';
13
+ import { CancellationManager } from './cancellation.js';
14
+ import { WorkerCircuitBreaker } from './circuit-breaker.js';
15
+ import { DedupDebounceMerger } from './dedup-debounce.js';
16
+ import { DlqRateLimitManager } from './dlq-rate-limit.js';
17
+ import { RateGate } from './rate-gate.js';
18
+ import { executeWithRetry } from './retry.js';
19
+ import { TriggerManager } from './triggers.js';
20
+ import { TtlChecker } from './ttl.js';
21
+ // biome-ignore lint/suspicious/noUnsafeDeclarationMerging: prototype-mixin composition — Object.assign below installs exactly the methods BunqueueApi declares
22
+ export class Bunqueue {
23
+ name;
24
+ queue;
25
+ worker;
26
+ /** @internal */ cancellation = new CancellationManager();
27
+ /** @internal */ cb;
28
+ /** @internal */ triggerMgr;
29
+ /** @internal */ ager;
30
+ /** @internal */ ttlChecker;
31
+ /** @internal */ batchAcc;
32
+ /** @internal */ dlqrl;
33
+ middlewares = [];
34
+ baseProcessor;
35
+ retryConfig;
36
+ merger;
37
+ rateGate;
38
+ defaultJobOptions;
39
+ constructor(name, opts) {
40
+ if (opts.embedded || opts.dataPath) {
41
+ throw new Error('Bunqueue (bunqueue-client) is TCP-only: embedded mode requires the Bun runtime — use the official "bunqueue" package on Bun instead');
42
+ }
43
+ const modes = [opts.processor, opts.routes, opts.batch].filter(Boolean).length;
44
+ if (modes === 0)
45
+ throw new Error('Bunqueue requires "processor", "routes", or "batch"');
46
+ if (modes > 1)
47
+ throw new Error('Bunqueue: use only one of "processor", "routes", or "batch"');
48
+ this.name = (opts.prefixKey ?? '') + name;
49
+ this.retryConfig = opts.retry ?? null;
50
+ this.ttlChecker = opts.ttl ? new TtlChecker(opts.ttl) : null;
51
+ this.merger = new DedupDebounceMerger(opts.deduplication ?? null, opts.debounce ?? null);
52
+ const limiter = opts.rateLimit ?? opts.limiter;
53
+ this.rateGate = limiter ? new RateGate(limiter) : null;
54
+ this.defaultJobOptions = this.buildDefaultJobOptions(opts);
55
+ // Build base processor
56
+ if (opts.batch) {
57
+ this.batchAcc = new BatchAccumulator(opts.batch);
58
+ this.baseProcessor = this.batchAcc.buildProcessor();
59
+ }
60
+ else {
61
+ this.batchAcc = null;
62
+ this.baseProcessor = opts.routes
63
+ ? this.buildRouteProcessor(opts.routes)
64
+ : opts.processor;
65
+ }
66
+ const wrappedProcessor = (job) => this.processJob(job);
67
+ const conn = opts.connection ?? {};
68
+ this.queue = new Queue(this.name, conn);
69
+ this.worker = new Worker(this.name, wrappedProcessor, {
70
+ ...conn,
71
+ concurrency: opts.concurrency,
72
+ autorun: opts.autorun,
73
+ heartbeatIntervalS: opts.heartbeatInterval !== undefined ? opts.heartbeatInterval / 1000 : undefined,
74
+ batchSize: opts.batchSize,
75
+ pollTimeoutMs: opts.pollTimeout,
76
+ });
77
+ // DLQ & rate limit manager
78
+ this.dlqrl = new DlqRateLimitManager(this.queue);
79
+ if (opts.dlq)
80
+ void this.dlqrl.setDlqConfig(opts.dlq);
81
+ // Subsystems
82
+ this.cb = opts.circuitBreaker
83
+ ? new WorkerCircuitBreaker(opts.circuitBreaker, this.worker)
84
+ : null;
85
+ this.triggerMgr = new TriggerManager(this.queue, this.worker);
86
+ this.ager = opts.priorityAging ? new PriorityAger(opts.priorityAging, this.queue) : null;
87
+ this.ager?.start();
88
+ }
89
+ buildRouteProcessor(routes) {
90
+ const routeMap = routes;
91
+ return (job) => {
92
+ const handler = routeMap[job.name ?? ''];
93
+ if (!handler)
94
+ throw new Error(`No route for job "${job.name}" in queue "${this.name}"`);
95
+ return handler(job);
96
+ };
97
+ }
98
+ buildDefaultJobOptions(opts) {
99
+ const base = { ...opts.defaultJobOptions };
100
+ if (opts.removeOnComplete !== undefined)
101
+ base.removeOnComplete ??= opts.removeOnComplete;
102
+ if (opts.removeOnFail !== undefined)
103
+ base.removeOnFail ??= opts.removeOnFail;
104
+ return Object.keys(base).length > 0 ? base : undefined;
105
+ }
106
+ // ------------------------------------------------- core processing pipeline
107
+ async processJob(job) {
108
+ if (this.rateGate)
109
+ await this.rateGate.acquire(this.rateGate.groupFor(job.data));
110
+ // Circuit breaker check
111
+ if (this.cb?.isOpen()) {
112
+ throw new Error('Circuit breaker is open');
113
+ }
114
+ // TTL check
115
+ if (this.ttlChecker?.isExpired(job.name ?? '', job.timestamp)) {
116
+ throw new Error(`Job expired (age: ${Date.now() - job.timestamp}ms)`);
117
+ }
118
+ // Register cancellation
119
+ const ac = this.cancellation.register(job.id);
120
+ const runChain = () => this.runMiddlewareChain(job, ac);
121
+ const execute = this.retryConfig ? executeWithRetry(runChain, this.retryConfig) : runChain();
122
+ return execute.then((result) => {
123
+ this.cb?.onSuccess();
124
+ this.cancellation.unregister(job.id);
125
+ return result;
126
+ }, (err) => {
127
+ this.cb?.onFailure();
128
+ this.cancellation.unregister(job.id);
129
+ throw err;
130
+ });
131
+ }
132
+ runMiddlewareChain(job, ac) {
133
+ if (this.middlewares.length === 0) {
134
+ const result = this.baseProcessor(job);
135
+ return result instanceof Promise ? result : Promise.resolve(result);
136
+ }
137
+ let index = 0;
138
+ const mws = this.middlewares;
139
+ const base = this.baseProcessor;
140
+ const next = () => {
141
+ if (ac.signal.aborted)
142
+ return Promise.reject(new Error('Job cancelled'));
143
+ if (index < mws.length)
144
+ return mws[index++](job, next);
145
+ const result = base(job);
146
+ return result instanceof Promise ? result : Promise.resolve(result);
147
+ };
148
+ return next();
149
+ }
150
+ // --------------------------------------------------------------- middleware
151
+ use(middleware) {
152
+ this.middlewares.push(middleware);
153
+ return this;
154
+ }
155
+ // --------------------------------------------------------- queue operations
156
+ add(name, data, opts) {
157
+ const merged = { ...this.defaultJobOptions, ...opts };
158
+ return this.queue.add(name, data, this.merger.merge(name, merged, data));
159
+ }
160
+ addBulk(jobs) {
161
+ return this.queue.addBulk(jobs.map((j) => ({
162
+ ...j,
163
+ opts: this.merger.merge(j.name, { ...this.defaultJobOptions, ...j.opts }, j.data),
164
+ })));
165
+ }
166
+ getJob(id) {
167
+ return this.queue.getJob(id);
168
+ }
169
+ getJobCounts() {
170
+ return this.queue.getJobCounts();
171
+ }
172
+ getJobCountsAsync() {
173
+ return this.queue.getJobCounts();
174
+ }
175
+ count() {
176
+ return this.queue.count();
177
+ }
178
+ countAsync() {
179
+ return this.queue.count();
180
+ }
181
+ }
182
+ Object.assign(Bunqueue.prototype, bunqueueApi);
@@ -0,0 +1,20 @@
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
+ export declare class CancellationManager {
7
+ private readonly controllers;
8
+ /** Register a new AbortController for a job. */
9
+ register(jobId: string): AbortController;
10
+ /** Remove a job's controller (on completion). */
11
+ unregister(jobId: string): void;
12
+ /** Cancel a job with optional grace period. */
13
+ cancel(jobId: string, gracePeriodMs?: number): void;
14
+ /** Check if a job is cancelled. */
15
+ isCancelled(jobId: string): boolean;
16
+ /** Get the AbortSignal for a job. */
17
+ getSignal(jobId: string): AbortSignal | null;
18
+ /** Cancel all and clear. */
19
+ destroyAll(): void;
20
+ }
@@ -0,0 +1,48 @@
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
+ export class CancellationManager {
7
+ controllers = new Map();
8
+ /** Register a new AbortController for a job. */
9
+ register(jobId) {
10
+ const ac = new AbortController();
11
+ this.controllers.set(jobId, ac);
12
+ return ac;
13
+ }
14
+ /** Remove a job's controller (on completion). */
15
+ unregister(jobId) {
16
+ this.controllers.delete(jobId);
17
+ }
18
+ /** Cancel a job with optional grace period. */
19
+ cancel(jobId, gracePeriodMs = 0) {
20
+ const ac = this.controllers.get(jobId);
21
+ if (!ac)
22
+ return;
23
+ if (gracePeriodMs > 0) {
24
+ setTimeout(() => {
25
+ ac.abort();
26
+ }, gracePeriodMs);
27
+ }
28
+ else {
29
+ ac.abort();
30
+ }
31
+ }
32
+ /** Check if a job is cancelled. */
33
+ isCancelled(jobId) {
34
+ const ac = this.controllers.get(jobId);
35
+ return ac ? ac.signal.aborted : false;
36
+ }
37
+ /** Get the AbortSignal for a job. */
38
+ getSignal(jobId) {
39
+ return this.controllers.get(jobId)?.signal ?? null;
40
+ }
41
+ /** Cancel all and clear. */
42
+ destroyAll() {
43
+ for (const ac of this.controllers.values()) {
44
+ ac.abort();
45
+ }
46
+ this.controllers.clear();
47
+ }
48
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Bunqueue Simple Mode — circuit breaker for worker protection.
3
+ * 1:1 port of src/client/bunqueue/circuitBreaker.ts.
4
+ */
5
+ import type { WorkerBase } from '../worker-base.js';
6
+ import type { CircuitBreakerConfig, CircuitState } from './types.js';
7
+ export declare class WorkerCircuitBreaker {
8
+ private state;
9
+ private failures;
10
+ private timer;
11
+ private readonly config;
12
+ private readonly worker;
13
+ constructor(config: CircuitBreakerConfig, worker: WorkerBase);
14
+ get currentState(): CircuitState;
15
+ isOpen(): boolean;
16
+ onSuccess(): void;
17
+ onFailure(): void;
18
+ private open;
19
+ reset(): void;
20
+ destroy(): void;
21
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Bunqueue Simple Mode — circuit breaker for worker protection.
3
+ * 1:1 port of src/client/bunqueue/circuitBreaker.ts.
4
+ */
5
+ export class WorkerCircuitBreaker {
6
+ state = 'closed';
7
+ failures = 0;
8
+ timer = null;
9
+ config;
10
+ worker;
11
+ constructor(config, worker) {
12
+ this.config = config;
13
+ this.worker = worker;
14
+ }
15
+ get currentState() {
16
+ return this.state;
17
+ }
18
+ isOpen() {
19
+ return this.state === 'open';
20
+ }
21
+ onSuccess() {
22
+ if (this.state === 'half-open') {
23
+ this.state = 'closed';
24
+ this.failures = 0;
25
+ this.config.onClose?.();
26
+ }
27
+ else if (this.state === 'closed') {
28
+ this.failures = 0;
29
+ }
30
+ }
31
+ onFailure() {
32
+ this.failures++;
33
+ const threshold = this.config.threshold ?? 5;
34
+ if (this.state === 'half-open' || this.failures >= threshold) {
35
+ this.open();
36
+ }
37
+ }
38
+ open() {
39
+ this.state = 'open';
40
+ this.config.onOpen?.(this.failures);
41
+ this.worker.pause();
42
+ const resetTimeout = this.config.resetTimeout ?? 30000;
43
+ if (this.timer)
44
+ clearTimeout(this.timer);
45
+ this.timer = setTimeout(() => {
46
+ this.state = 'half-open';
47
+ this.config.onHalfOpen?.();
48
+ this.worker.resume();
49
+ }, resetTimeout);
50
+ this.timer.unref?.();
51
+ }
52
+ reset() {
53
+ this.state = 'closed';
54
+ this.failures = 0;
55
+ if (this.timer) {
56
+ clearTimeout(this.timer);
57
+ this.timer = null;
58
+ }
59
+ if (this.worker.isPaused()) {
60
+ this.worker.resume();
61
+ }
62
+ }
63
+ destroy() {
64
+ if (this.timer) {
65
+ clearTimeout(this.timer);
66
+ this.timer = null;
67
+ }
68
+ }
69
+ }
@@ -0,0 +1,14 @@
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
+ import type { JobOptions } from '../types.js';
7
+ import type { BunqueueDebounceConfig, BunqueueDeduplicationConfig } from './types.js';
8
+ export declare class DedupDebounceMerger {
9
+ private readonly dedup;
10
+ private readonly debounce;
11
+ constructor(dedup: BunqueueDeduplicationConfig | null, debounce: BunqueueDebounceConfig | null);
12
+ get active(): boolean;
13
+ merge(name: string, opts?: JobOptions, data?: unknown): JobOptions | undefined;
14
+ }
@@ -0,0 +1,37 @@
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
+ export class DedupDebounceMerger {
7
+ dedup;
8
+ debounce;
9
+ constructor(dedup, debounce) {
10
+ this.dedup = dedup;
11
+ this.debounce = debounce;
12
+ }
13
+ get active() {
14
+ return this.dedup !== null || this.debounce !== null;
15
+ }
16
+ merge(name, opts, data) {
17
+ if (!this.active)
18
+ return opts;
19
+ const merged = { ...opts };
20
+ if (this.dedup && !merged.deduplication) {
21
+ const dataKey = data !== undefined ? JSON.stringify(data) : '';
22
+ merged.deduplication = {
23
+ id: `${name}:${dataKey}`,
24
+ ttl: this.dedup.ttl ?? 3600000,
25
+ extend: this.dedup.extend,
26
+ replace: this.dedup.replace,
27
+ };
28
+ }
29
+ if (this.debounce && !merged.debounce) {
30
+ merged.debounce = {
31
+ id: name,
32
+ ttl: this.debounce.ttl,
33
+ };
34
+ }
35
+ return merged;
36
+ }
37
+ }
@@ -0,0 +1,33 @@
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
+ import type { Queue } from '../queue.js';
8
+ import type { BunqueueDlqConfig } from './types.js';
9
+ type Raw = Record<string, unknown>;
10
+ /** Filter for DLQ queries (mirrors the official DlqFilter). */
11
+ export interface DlqFilter {
12
+ reason?: string;
13
+ jobName?: string;
14
+ since?: number;
15
+ }
16
+ /** Aggregate DLQ statistics (mirrors the official DlqStats). */
17
+ export interface DlqStats {
18
+ total: number;
19
+ byReason: Record<string, number>;
20
+ }
21
+ export declare class DlqRateLimitManager<T = unknown> {
22
+ private readonly queue;
23
+ constructor(queue: Queue<T>);
24
+ setDlqConfig(config: BunqueueDlqConfig): Promise<void>;
25
+ getDlqConfig(): Promise<Raw>;
26
+ getDlq(filter?: DlqFilter): Promise<Raw[]>;
27
+ getDlqStats(): Promise<DlqStats>;
28
+ retryDlq(id?: string): Promise<number>;
29
+ purgeDlq(): Promise<number>;
30
+ setGlobalRateLimit(max: number, duration?: number): Promise<void>;
31
+ removeGlobalRateLimit(): Promise<void>;
32
+ }
33
+ export {};
@@ -0,0 +1,65 @@
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
+ function entryReason(entry) {
8
+ return String(entry.reason ?? entry.entry?.reason ?? 'unknown');
9
+ }
10
+ function entryName(entry) {
11
+ const data = (entry.data ?? entry.entry?.data);
12
+ return data?.name === undefined ? undefined : String(data.name);
13
+ }
14
+ export class DlqRateLimitManager {
15
+ queue;
16
+ constructor(queue) {
17
+ this.queue = queue;
18
+ }
19
+ // ------------------------------------------------------------------- dlq
20
+ async setDlqConfig(config) {
21
+ await this.queue.setDlqConfig(config);
22
+ }
23
+ getDlqConfig() {
24
+ return this.queue.getDlqConfig();
25
+ }
26
+ async getDlq(filter) {
27
+ const entries = await this.queue.getDlq();
28
+ if (!filter)
29
+ return entries;
30
+ return entries.filter((entry) => {
31
+ if (filter.reason && entryReason(entry) !== filter.reason)
32
+ return false;
33
+ if (filter.jobName && entryName(entry) !== filter.jobName)
34
+ return false;
35
+ if (filter.since !== undefined) {
36
+ const at = Number(entry.enteredAt ?? entry.failedAt ?? 0);
37
+ if (at < filter.since)
38
+ return false;
39
+ }
40
+ return true;
41
+ });
42
+ }
43
+ async getDlqStats() {
44
+ const entries = await this.queue.getDlq();
45
+ const byReason = {};
46
+ for (const entry of entries) {
47
+ const reason = entryReason(entry);
48
+ byReason[reason] = (byReason[reason] ?? 0) + 1;
49
+ }
50
+ return { total: entries.length, byReason };
51
+ }
52
+ retryDlq(id) {
53
+ return this.queue.retryDlq(id);
54
+ }
55
+ purgeDlq() {
56
+ return this.queue.purgeDlq();
57
+ }
58
+ // ----------------------------------------------------------- rate limiting
59
+ async setGlobalRateLimit(max, duration) {
60
+ await this.queue.setGlobalRateLimit(max, duration);
61
+ }
62
+ async removeGlobalRateLimit() {
63
+ await this.queue.removeGlobalRateLimit();
64
+ }
65
+ }
@@ -0,0 +1,21 @@
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
+ import type { RateLimiterOptions } from './types.js';
11
+ export declare class RateGate {
12
+ private readonly max;
13
+ private readonly duration;
14
+ private readonly groupKey;
15
+ private readonly windows;
16
+ constructor(options: RateLimiterOptions);
17
+ /** Resolve the rate-limit group for a job's data. */
18
+ groupFor(data: unknown): string;
19
+ /** Wait until the group's window has room, then record the start. */
20
+ acquire(group: string): Promise<void>;
21
+ }
@@ -0,0 +1,48 @@
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
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
11
+ export class RateGate {
12
+ max;
13
+ duration;
14
+ groupKey;
15
+ windows = new Map();
16
+ constructor(options) {
17
+ this.max = options.max;
18
+ this.duration = options.duration;
19
+ this.groupKey = options.groupKey;
20
+ }
21
+ /** Resolve the rate-limit group for a job's data. */
22
+ groupFor(data) {
23
+ if (!this.groupKey)
24
+ return '';
25
+ if (typeof data === 'object' && data !== null) {
26
+ const value = data[this.groupKey];
27
+ if (value !== undefined && value !== null)
28
+ return String(value);
29
+ }
30
+ return '';
31
+ }
32
+ /** Wait until the group's window has room, then record the start. */
33
+ async acquire(group) {
34
+ for (;;) {
35
+ const now = Date.now();
36
+ const window = this.windows.get(group) ?? [];
37
+ const fresh = window.filter((t) => now - t < this.duration);
38
+ if (fresh.length < this.max) {
39
+ fresh.push(now);
40
+ this.windows.set(group, fresh);
41
+ return;
42
+ }
43
+ this.windows.set(group, fresh);
44
+ const oldest = fresh[0];
45
+ await sleep(Math.max(oldest + this.duration - now, 10));
46
+ }
47
+ }
48
+ }
@@ -0,0 +1,9 @@
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
+ import type { RetryConfig, RetryStrategy } from './types.js';
6
+ /** Calculate backoff delay based on strategy. */
7
+ export declare function calculateBackoff(strategy: RetryStrategy, attempt: number, baseDelay: number, error: Error, config: RetryConfig): number;
8
+ /** Execute a function with retry logic (the job stays active throughout). */
9
+ export declare function executeWithRetry<R>(fn: () => Promise<R>, config: RetryConfig): Promise<R>;
@@ -0,0 +1,56 @@
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
+ /** Calculate backoff delay based on strategy. */
6
+ export function calculateBackoff(strategy, attempt, baseDelay, error, config) {
7
+ switch (strategy) {
8
+ case 'fixed':
9
+ return baseDelay;
10
+ case 'exponential':
11
+ return baseDelay * 2 ** (attempt - 1);
12
+ case 'jitter': {
13
+ const exp = baseDelay * 2 ** (attempt - 1);
14
+ return Math.floor(exp * (0.5 + Math.random()));
15
+ }
16
+ case 'fibonacci': {
17
+ let a = 1;
18
+ let b = 1;
19
+ for (let i = 0; i < attempt - 1; i++) {
20
+ const next = a + b;
21
+ a = b;
22
+ b = next;
23
+ }
24
+ return baseDelay * b;
25
+ }
26
+ case 'custom':
27
+ if (config.customBackoff) {
28
+ return config.customBackoff(attempt, error);
29
+ }
30
+ return baseDelay;
31
+ default:
32
+ return baseDelay;
33
+ }
34
+ }
35
+ /** Execute a function with retry logic (the job stays active throughout). */
36
+ export function executeWithRetry(fn, config) {
37
+ const maxAttempts = config.maxAttempts ?? 3;
38
+ const baseDelay = config.delay ?? 1000;
39
+ const strategy = config.strategy ?? 'exponential';
40
+ const attempt = (n) => {
41
+ return fn().catch((err) => {
42
+ const error = err instanceof Error ? err : new Error(String(err));
43
+ if (n >= maxAttempts)
44
+ throw error;
45
+ if (config.retryIf && !config.retryIf(error, n))
46
+ throw error;
47
+ const delay = calculateBackoff(strategy, n, baseDelay, error, config);
48
+ return new Promise((resolve) => {
49
+ setTimeout(() => {
50
+ resolve(attempt(n + 1));
51
+ }, delay);
52
+ });
53
+ });
54
+ };
55
+ return attempt(1);
56
+ }
@@ -0,0 +1,18 @@
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
+ import type { Queue } from '../queue.js';
7
+ import type { Worker } from '../worker.js';
8
+ import type { TriggerRule } from './types.js';
9
+ export declare class TriggerManager<T = unknown, R = unknown> {
10
+ private readonly rules;
11
+ private active;
12
+ private readonly queue;
13
+ private readonly worker;
14
+ constructor(queue: Queue<T>, worker: Worker<T, R>);
15
+ add(rule: TriggerRule<T>): void;
16
+ private ensureActive;
17
+ private fire;
18
+ }