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.
Files changed (60) hide show
  1. package/README.md +45 -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 +1 -1
  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,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
+ }
@@ -0,0 +1,42 @@
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
+ export class TriggerManager {
7
+ rules = [];
8
+ active = false;
9
+ queue;
10
+ worker;
11
+ constructor(queue, worker) {
12
+ this.queue = queue;
13
+ this.worker = worker;
14
+ }
15
+ add(rule) {
16
+ this.rules.push(rule);
17
+ this.ensureActive();
18
+ }
19
+ ensureActive() {
20
+ if (this.active)
21
+ return;
22
+ this.active = true;
23
+ this.worker.on('completed', (job, result) => {
24
+ this.fire('completed', job, result);
25
+ });
26
+ this.worker.on('failed', (job, error) => {
27
+ this.fire('failed', job, error);
28
+ });
29
+ }
30
+ fire(event, job, resultOrError) {
31
+ for (const rule of this.rules) {
32
+ if (rule.on !== job.name)
33
+ continue;
34
+ if ((rule.event ?? 'completed') !== event)
35
+ continue;
36
+ if (rule.condition && !rule.condition(resultOrError, job))
37
+ continue;
38
+ const data = rule.data(resultOrError, job);
39
+ void this.queue.add(rule.create, data, rule.opts);
40
+ }
41
+ }
42
+ }
@@ -0,0 +1,18 @@
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
+ import type { JobTtlConfig } from './types.js';
7
+ export declare class TtlChecker {
8
+ private readonly config;
9
+ constructor(config: JobTtlConfig);
10
+ /** Get TTL for a specific job name. */
11
+ getTtl(jobName: string): number;
12
+ /** Check if a job has expired. */
13
+ isExpired(jobName: string, jobTimestamp: number): boolean;
14
+ /** Set default TTL. */
15
+ setDefaultTtl(ttlMs: number): void;
16
+ /** Set TTL for a specific job name. */
17
+ setNameTtl(jobName: string, ttlMs: number): void;
18
+ }
@@ -0,0 +1,31 @@
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
+ export class TtlChecker {
7
+ config;
8
+ constructor(config) {
9
+ this.config = config;
10
+ }
11
+ /** Get TTL for a specific job name. */
12
+ getTtl(jobName) {
13
+ return this.config.perName?.[jobName] ?? this.config.defaultTtl ?? 0;
14
+ }
15
+ /** Check if a job has expired. */
16
+ isExpired(jobName, jobTimestamp) {
17
+ const ttl = this.getTtl(jobName);
18
+ if (ttl <= 0)
19
+ return false;
20
+ return Date.now() - jobTimestamp > ttl;
21
+ }
22
+ /** Set default TTL. */
23
+ setDefaultTtl(ttlMs) {
24
+ this.config.defaultTtl = ttlMs;
25
+ }
26
+ /** Set TTL for a specific job name. */
27
+ setNameTtl(jobName, ttlMs) {
28
+ this.config.perName ??= {};
29
+ this.config.perName[jobName] = ttlMs;
30
+ }
31
+ }