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,151 @@
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
+ import type { TlsOption } from '../connection.js';
7
+ import type { Job } from '../job.js';
8
+ import type { JobOptions } from '../types.js';
9
+ import type { Processor } from '../worker-types.js';
10
+ /** Connection options accepted by Simple Mode (mirrors ConnectionOptions). */
11
+ export interface BunqueueConnection {
12
+ host?: string;
13
+ port?: number;
14
+ token?: string;
15
+ tls?: TlsOption;
16
+ }
17
+ /** Middleware function: receives job and next(), returns result. */
18
+ export type BunqueueMiddleware<T = unknown, R = unknown> = (job: Job<T>, next: () => Promise<R>) => Promise<R>;
19
+ /** Retry strategy for advanced backoff. */
20
+ export type RetryStrategy = 'fixed' | 'exponential' | 'jitter' | 'fibonacci' | 'custom';
21
+ /** Advanced retry configuration (in-process; the job stays active). */
22
+ export interface RetryConfig {
23
+ /** Max attempts (default: 3). */
24
+ maxAttempts?: number;
25
+ /** Base delay in ms (default: 1000). */
26
+ delay?: number;
27
+ /** Strategy (default: exponential). */
28
+ strategy?: RetryStrategy;
29
+ /** Custom backoff function: attempt, error -> delay in ms. */
30
+ customBackoff?: (attempt: number, error: Error) => number;
31
+ /** Only retry if this returns true. */
32
+ retryIf?: (error: Error, attempt: number) => boolean;
33
+ }
34
+ /** Circuit breaker configuration. */
35
+ export interface CircuitBreakerConfig {
36
+ /** Max consecutive failures before opening (default: 5). */
37
+ threshold?: number;
38
+ /** Time in ms before half-open retry (default: 30000). */
39
+ resetTimeout?: number;
40
+ onOpen?: (failures: number) => void;
41
+ onClose?: () => void;
42
+ onHalfOpen?: () => void;
43
+ }
44
+ /** Circuit breaker state. */
45
+ export type CircuitState = 'closed' | 'open' | 'half-open';
46
+ /** Event trigger rule. */
47
+ export interface TriggerRule<T = unknown> {
48
+ /** Job name that triggers this rule. */
49
+ on: string;
50
+ /** Event type (default: completed). */
51
+ event?: 'completed' | 'failed';
52
+ /** Job name to create. */
53
+ create: string;
54
+ /** Data builder from the triggering job. */
55
+ data: (result: unknown, job: Job<T>) => T;
56
+ /** Optional job options. */
57
+ opts?: JobOptions;
58
+ /** Optional condition. */
59
+ condition?: (result: unknown, job: Job<T>) => boolean;
60
+ }
61
+ /** Priority aging configuration. */
62
+ export interface PriorityAgingConfig {
63
+ /** Check interval in ms (default: 60000). */
64
+ interval?: number;
65
+ /** Min age in ms before boost (default: 60000). */
66
+ minAge?: number;
67
+ /** Priority boost per interval (default: 1). */
68
+ boost?: number;
69
+ /** Max priority cap (default: 100). */
70
+ maxPriority?: number;
71
+ /** Max jobs to scan per tick (default: 100). */
72
+ maxScan?: number;
73
+ }
74
+ /** Batch processor function. */
75
+ export type BatchProcessor<T = unknown, R = unknown> = (jobs: Array<Job<T>>) => Promise<R[]>;
76
+ /** Batch processing configuration. */
77
+ export interface BatchConfig<T = unknown, R = unknown> {
78
+ /** Batch size (default: 10). */
79
+ size: number;
80
+ /** Max wait in ms before flushing a partial batch (default: 5000). */
81
+ timeout?: number;
82
+ processor: BatchProcessor<T, R>;
83
+ }
84
+ /** Job TTL configuration. */
85
+ export interface JobTtlConfig {
86
+ /** Default TTL in ms (0 = no TTL). */
87
+ defaultTtl?: number;
88
+ /** Per-job-name TTL overrides. */
89
+ perName?: Record<string, number>;
90
+ }
91
+ /** Deduplication defaults for Simple Mode. */
92
+ export interface BunqueueDeduplicationConfig {
93
+ /** Default deduplication TTL in ms (default: 3600000 = 1 hour). */
94
+ ttl?: number;
95
+ /** Extend TTL when a duplicate arrives (default: false). */
96
+ extend?: boolean;
97
+ /** Replace data when a duplicate arrives in delayed state (default: false). */
98
+ replace?: boolean;
99
+ }
100
+ /** Debounce defaults for Simple Mode. */
101
+ export interface BunqueueDebounceConfig {
102
+ /** Debounce TTL in ms. */
103
+ ttl: number;
104
+ }
105
+ /** DLQ configuration for Simple Mode (forwarded to the server). */
106
+ export interface BunqueueDlqConfig {
107
+ autoRetry?: boolean;
108
+ autoRetryInterval?: number;
109
+ maxAutoRetries?: number;
110
+ maxAge?: number | null;
111
+ maxEntries?: number;
112
+ }
113
+ /** Client-side rate limiter options (max job starts per duration window). */
114
+ export interface RateLimiterOptions {
115
+ max: number;
116
+ duration: number;
117
+ /** Group jobs by this data field (e.g. per-customer limits). */
118
+ groupKey?: string;
119
+ }
120
+ /** Bunqueue Simple Mode options. */
121
+ export interface BunqueueOptions<T = unknown, R = unknown> {
122
+ processor?: Processor<T, R>;
123
+ routes?: Record<string, Processor<T, R>>;
124
+ batch?: BatchConfig<T, R>;
125
+ concurrency?: number;
126
+ connection?: BunqueueConnection;
127
+ defaultJobOptions?: JobOptions;
128
+ autorun?: boolean;
129
+ /** Worker heartbeat interval in ms (official-client unit). */
130
+ heartbeatInterval?: number;
131
+ batchSize?: number;
132
+ /** Long-poll timeout in ms. */
133
+ pollTimeout?: number;
134
+ limiter?: RateLimiterOptions;
135
+ removeOnComplete?: boolean;
136
+ removeOnFail?: boolean;
137
+ retry?: RetryConfig;
138
+ circuitBreaker?: CircuitBreakerConfig;
139
+ ttl?: JobTtlConfig;
140
+ priorityAging?: PriorityAgingConfig;
141
+ /** Job deduplication defaults, merged into every add(). */
142
+ deduplication?: BunqueueDeduplicationConfig;
143
+ /** Job debouncing defaults, merged into every add(). */
144
+ debounce?: BunqueueDebounceConfig;
145
+ /** Rate limiting for the worker (alias of limiter, takes precedence). */
146
+ rateLimit?: RateLimiterOptions;
147
+ /** Dead letter queue auto-management (forwarded to the server). */
148
+ dlq?: BunqueueDlqConfig;
149
+ /** Namespace prefix prepended to the queue name on the server. */
150
+ prefixKey?: string;
151
+ }
@@ -0,0 +1,6 @@
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
+ export {};
@@ -22,12 +22,20 @@ export declare class Connection {
22
22
  private pending;
23
23
  private reqCounter;
24
24
  private parser;
25
+ private connectGeneration;
26
+ private failedAttempts;
27
+ private nextAttemptAt;
25
28
  constructor(options?: ConnectionOptions);
26
29
  get isConnected(): boolean;
30
+ /**
31
+ * Monotonic counter, incremented on every successful (re)connect. Consumers
32
+ * that hold per-connection server state (e.g. a Worker registration) compare
33
+ * it between operations and re-establish that state after a reconnect.
34
+ */
35
+ get generation(): number;
27
36
  /** Open the socket (and authenticate) if not already connected. */
28
37
  connect(): Promise<void>;
29
38
  private doConnect;
30
- private openSocket;
31
39
  /**
32
40
  * Send a command and await its response. Rejects with CommandError when
33
41
  * the server answers ok=false. Reconnects lazily if the link was lost.
@@ -5,12 +5,10 @@
5
5
  * pipelining (many in-flight commands per socket). Uses only `node:`
6
6
  * builtins (net/tls), which Node, Bun and Deno all support.
7
7
  */
8
- import { readFileSync } from 'node:fs';
9
- import { connect as netConnect } from 'node:net';
10
- import { connect as tlsConnect } from 'node:tls';
11
8
  import { pack, unpack } from 'msgpackr';
12
9
  import { AuthError, CommandError, CommandTimeoutError, ConnectionClosedError } from './errors.js';
13
10
  import { compact, FrameParser, frame, PROTOCOL_VERSION } from './frame.js';
11
+ import { openSocket } from './socket-factory.js';
14
12
  /** A single pipelined TCP connection to a bunqueue server. */
15
13
  export class Connection {
16
14
  host;
@@ -26,6 +24,9 @@ export class Connection {
26
24
  pending = new Map();
27
25
  reqCounter = 0;
28
26
  parser = new FrameParser();
27
+ connectGeneration = 0;
28
+ failedAttempts = 0;
29
+ nextAttemptAt = 0;
29
30
  constructor(options = {}) {
30
31
  this.host = options.host ?? 'localhost';
31
32
  this.port = options.port ?? 6789;
@@ -37,21 +38,46 @@ export class Connection {
37
38
  get isConnected() {
38
39
  return this.connected;
39
40
  }
41
+ /**
42
+ * Monotonic counter, incremented on every successful (re)connect. Consumers
43
+ * that hold per-connection server state (e.g. a Worker registration) compare
44
+ * it between operations and re-establish that state after a reconnect.
45
+ */
46
+ get generation() {
47
+ return this.connectGeneration;
48
+ }
40
49
  /** Open the socket (and authenticate) if not already connected. */
41
50
  async connect() {
42
51
  if (this.connected)
43
52
  return;
44
53
  if (this.closed)
45
54
  throw new ConnectionClosedError('connection closed by client');
55
+ // Fast-fail while inside the reconnect backoff window: without this, a
56
+ // producer calling add() against a downed server pays the full connect
57
+ // timeout on EVERY call (reconnect storm).
58
+ if (Date.now() < this.nextAttemptAt) {
59
+ throw new ConnectionClosedError(`server unreachable, retry in ${this.nextAttemptAt - Date.now()}ms`);
60
+ }
46
61
  if (this.connecting)
47
62
  return this.connecting;
48
- this.connecting = this.doConnect().finally(() => {
63
+ this.connecting = this.doConnect()
64
+ .then(() => {
65
+ this.failedAttempts = 0;
66
+ this.nextAttemptAt = 0;
67
+ })
68
+ .catch((err) => {
69
+ this.failedAttempts += 1;
70
+ const backoff = Math.min(500 * 2 ** (this.failedAttempts - 1), 5000);
71
+ this.nextAttemptAt = Date.now() + backoff;
72
+ throw err;
73
+ })
74
+ .finally(() => {
49
75
  this.connecting = null;
50
76
  });
51
77
  return this.connecting;
52
78
  }
53
79
  async doConnect() {
54
- const socket = await this.openSocket();
80
+ const socket = await openSocket(this.host, this.port, this.tls, this.connectTimeoutMs);
55
81
  socket.setNoDelay(true);
56
82
  this.parser.clear();
57
83
  this.socket = socket;
@@ -59,6 +85,7 @@ export class Connection {
59
85
  socket.on('error', () => this.teardown());
60
86
  socket.on('close', () => this.teardown());
61
87
  this.connected = true;
88
+ this.connectGeneration += 1;
62
89
  if (this.token) {
63
90
  try {
64
91
  await this.call({ cmd: 'Auth', token: this.token });
@@ -71,48 +98,6 @@ export class Connection {
71
98
  }
72
99
  }
73
100
  }
74
- openSocket() {
75
- return new Promise((resolve, reject) => {
76
- let settled = false;
77
- const timer = setTimeout(() => {
78
- if (settled)
79
- return;
80
- settled = true;
81
- socket.destroy();
82
- reject(new ConnectionClosedError(`connect timeout to ${this.host}:${this.port}`));
83
- }, this.connectTimeoutMs);
84
- const onError = (err) => {
85
- if (settled)
86
- return;
87
- settled = true;
88
- clearTimeout(timer);
89
- reject(new ConnectionClosedError(`connect failed: ${err.message}`));
90
- };
91
- const onReady = () => {
92
- if (settled)
93
- return;
94
- settled = true;
95
- clearTimeout(timer);
96
- socket.off('error', onError);
97
- resolve(socket);
98
- };
99
- let socket;
100
- if (this.tls) {
101
- const tlsOpts = { host: this.host, port: this.port };
102
- if (typeof this.tls === 'object') {
103
- if (this.tls.caFile)
104
- tlsOpts.ca = readFileSync(this.tls.caFile);
105
- if (this.tls.rejectUnauthorized === false)
106
- tlsOpts.rejectUnauthorized = false;
107
- }
108
- socket = tlsConnect(tlsOpts, onReady);
109
- }
110
- else {
111
- socket = netConnect({ host: this.host, port: this.port }, onReady);
112
- }
113
- socket.on('error', onError);
114
- });
115
- }
116
101
  /**
117
102
  * Send a command and await its response. Rejects with CommandError when
118
103
  * the server answers ok=false. Reconnects lazily if the link was lost.
package/dist/index.d.ts CHANGED
@@ -2,6 +2,9 @@
2
2
  * bunqueue-client — cross-runtime TypeScript SDK for bunqueue (TCP mode).
3
3
  * Works on Node.js ≥20, Bun and Deno ≥2 (uses only node: builtins + msgpackr).
4
4
  */
5
+ export { Bunqueue } from './bunqueue/bunqueue.js';
6
+ export type { DlqFilter, DlqStats } from './bunqueue/dlq-rate-limit.js';
7
+ export type { BatchConfig, BatchProcessor, BunqueueConnection, BunqueueDebounceConfig, BunqueueDeduplicationConfig, BunqueueDlqConfig, BunqueueMiddleware, BunqueueOptions, CircuitBreakerConfig, CircuitState, JobTtlConfig, PriorityAgingConfig, RateLimiterOptions, RetryConfig, RetryStrategy, TriggerRule, } from './bunqueue/types.js';
5
8
  export type { Command, ConnectionOptions, Response, TlsOption } from './connection.js';
6
9
  export { Connection } from './connection.js';
7
10
  export { AuthError, BunqueueError, CommandError, CommandTimeoutError, ConnectionClosedError, UnrecoverableError, } from './errors.js';
@@ -16,4 +19,4 @@ export type { SchedulerOptions } from './queue-admin.js';
16
19
  export type { BackoffOptions, DeduplicationOptions, JobCounts, JobOptions, JobStateName, RepeatOptions, } from './types.js';
17
20
  export { Worker } from './worker.js';
18
21
  export type { Processor, WorkerOptions } from './worker-types.js';
19
- export declare const __version__ = "0.1.0";
22
+ export declare const __version__ = "0.1.2";
package/dist/index.js CHANGED
@@ -2,6 +2,8 @@
2
2
  * bunqueue-client — cross-runtime TypeScript SDK for bunqueue (TCP mode).
3
3
  * Works on Node.js ≥20, Bun and Deno ≥2 (uses only node: builtins + msgpackr).
4
4
  */
5
+ // Simple Mode (Bunqueue): all-in-one Queue + Worker, 1:1 with the official client
6
+ export { Bunqueue } from './bunqueue/bunqueue.js';
5
7
  export { Connection } from './connection.js';
6
8
  export { AuthError, BunqueueError, CommandError, CommandTimeoutError, ConnectionClosedError, UnrecoverableError, } from './errors.js';
7
9
  export { FlowProducer } from './flow.js';
@@ -9,4 +11,4 @@ export { MAX_FRAME_SIZE, PROTOCOL_VERSION } from './frame.js';
9
11
  export { Job } from './job.js';
10
12
  export { Queue } from './queue.js';
11
13
  export { Worker } from './worker.js';
12
- export const __version__ = '0.1.0';
14
+ export const __version__ = '0.1.2';
package/dist/job.d.ts CHANGED
@@ -20,6 +20,8 @@ export declare class Job<T = unknown> {
20
20
  get attempts(): number;
21
21
  get maxAttempts(): number;
22
22
  get createdAt(): number | undefined;
23
+ /** Creation time in epoch ms (BullMQ-parity alias of createdAt). */
24
+ get timestamp(): number;
23
25
  get startedAt(): number | null;
24
26
  get completedAt(): number | null;
25
27
  get customId(): string | null;
package/dist/job.js CHANGED
@@ -44,6 +44,10 @@ export class Job {
44
44
  get createdAt() {
45
45
  return this.raw.createdAt === undefined ? undefined : Number(this.raw.createdAt);
46
46
  }
47
+ /** Creation time in epoch ms (BullMQ-parity alias of createdAt). */
48
+ get timestamp() {
49
+ return this.createdAt ?? Date.now();
50
+ }
47
51
  get startedAt() {
48
52
  return this.raw.startedAt === undefined || this.raw.startedAt === null
49
53
  ? null
@@ -138,6 +138,7 @@ export const adminMethods = {
138
138
  return (data.webhooks ?? response.webhooks ?? []);
139
139
  },
140
140
  async setWebhookEnabled(webhookId, enabled) {
141
- await this.call({ cmd: 'SetWebhookEnabled', webhookId, enabled });
141
+ // wire field is `id` (SetWebhookEnabledCommand), unlike RemoveWebhook's `webhookId`
142
+ await this.call({ cmd: 'SetWebhookEnabled', id: webhookId, enabled });
142
143
  },
143
144
  };
@@ -0,0 +1,4 @@
1
+ /** Socket factory: opens a plain or TLS socket with a connect timeout. */
2
+ import { type Socket } from 'node:net';
3
+ import type { TlsOption } from './connection-types.js';
4
+ export declare function openSocket(host: string, port: number, tls: TlsOption, connectTimeoutMs: number): Promise<Socket>;
@@ -0,0 +1,47 @@
1
+ /** Socket factory: opens a plain or TLS socket with a connect timeout. */
2
+ import { readFileSync } from 'node:fs';
3
+ import { connect as netConnect } from 'node:net';
4
+ import { connect as tlsConnect } from 'node:tls';
5
+ import { ConnectionClosedError } from './errors.js';
6
+ export function openSocket(host, port, tls, connectTimeoutMs) {
7
+ return new Promise((resolve, reject) => {
8
+ let settled = false;
9
+ const timer = setTimeout(() => {
10
+ if (settled)
11
+ return;
12
+ settled = true;
13
+ socket.destroy();
14
+ reject(new ConnectionClosedError(`connect timeout to ${host}:${port}`));
15
+ }, connectTimeoutMs);
16
+ const onError = (err) => {
17
+ if (settled)
18
+ return;
19
+ settled = true;
20
+ clearTimeout(timer);
21
+ reject(new ConnectionClosedError(`connect failed: ${err.message}`));
22
+ };
23
+ const onReady = () => {
24
+ if (settled)
25
+ return;
26
+ settled = true;
27
+ clearTimeout(timer);
28
+ socket.off('error', onError);
29
+ resolve(socket);
30
+ };
31
+ let socket;
32
+ if (tls) {
33
+ const tlsOpts = { host: host, port: port };
34
+ if (typeof tls === 'object') {
35
+ if (tls.caFile)
36
+ tlsOpts.ca = readFileSync(tls.caFile);
37
+ if (tls.rejectUnauthorized === false)
38
+ tlsOpts.rejectUnauthorized = false;
39
+ }
40
+ socket = tlsConnect(tlsOpts, onReady);
41
+ }
42
+ else {
43
+ socket = netConnect({ host: host, port: port }, onReady);
44
+ }
45
+ socket.on('error', onError);
46
+ });
47
+ }
@@ -26,8 +26,10 @@ export declare class WorkerBase extends EventEmitter {
26
26
  protected failedCount: number;
27
27
  protected readyPromise: Promise<void>;
28
28
  protected readyResolve: (() => void) | null;
29
+ protected readyFired: boolean;
29
30
  protected loopPromise: Promise<void> | null;
30
31
  protected heartbeatTimer: ReturnType<typeof setInterval> | null;
32
+ protected registeredGeneration: number;
31
33
  constructor(queue: string, opts?: WorkerOptions);
32
34
  pause(): void;
33
35
  resume(): void;
@@ -35,6 +37,13 @@ export declare class WorkerBase extends EventEmitter {
35
37
  isPaused(): boolean;
36
38
  isClosed(): boolean;
37
39
  waitUntilReady(): Promise<void>;
40
+ /**
41
+ * 'ready' is replayed to listeners attached after it fired: with autorun the
42
+ * loop starts inside the constructor, so a plain once-only event could be
43
+ * missed by `new Worker(...).on('ready', ...)` patterns.
44
+ */
45
+ on(event: string | symbol, listener: (...args: unknown[]) => void): this;
46
+ once(event: string | symbol, listener: (...args: unknown[]) => void): this;
38
47
  /**
39
48
  * Cooperative cancel of a locally active job (mirrors the official client):
40
49
  * marks the job and emits 'cancelled'; the processor is expected to check
@@ -43,8 +52,10 @@ export declare class WorkerBase extends EventEmitter {
43
52
  cancelJob(jobId: string, reason?: string): boolean;
44
53
  cancelAllJobs(reason?: string): void;
45
54
  isJobCancelled(jobId: string): boolean;
46
- /** Graceful shutdown: stop pulling, wait for in-flight jobs. */
47
- close(): Promise<void>;
55
+ /** Graceful shutdown: stop pulling, wait for in-flight jobs.
56
+ * With `force` the wait for in-flight jobs is skipped (parity with the
57
+ * official client's `close(force)`). */
58
+ close(force?: boolean): Promise<void>;
48
59
  protected safeCall(command: Record<string, unknown> & {
49
60
  cmd: string;
50
61
  }): Promise<void>;
@@ -28,8 +28,10 @@ export class WorkerBase extends EventEmitter {
28
28
  failedCount = 0;
29
29
  readyPromise;
30
30
  readyResolve = null;
31
+ readyFired = false;
31
32
  loopPromise = null;
32
33
  heartbeatTimer = null;
34
+ registeredGeneration = -1;
33
35
  constructor(queue, opts = {}) {
34
36
  super();
35
37
  if ((opts.concurrency ?? 4) < 1)
@@ -70,6 +72,23 @@ export class WorkerBase extends EventEmitter {
70
72
  async waitUntilReady() {
71
73
  await this.readyPromise;
72
74
  }
75
+ /**
76
+ * 'ready' is replayed to listeners attached after it fired: with autorun the
77
+ * loop starts inside the constructor, so a plain once-only event could be
78
+ * missed by `new Worker(...).on('ready', ...)` patterns.
79
+ */
80
+ on(event, listener) {
81
+ if (event === 'ready' && this.readyFired)
82
+ listener();
83
+ return super.on(event, listener);
84
+ }
85
+ once(event, listener) {
86
+ if (event === 'ready' && this.readyFired) {
87
+ listener();
88
+ return this;
89
+ }
90
+ return super.once(event, listener);
91
+ }
73
92
  /**
74
93
  * Cooperative cancel of a locally active job (mirrors the official client):
75
94
  * marks the job and emits 'cancelled'; the processor is expected to check
@@ -92,20 +111,26 @@ export class WorkerBase extends EventEmitter {
92
111
  isJobCancelled(jobId) {
93
112
  return this.cancelledJobs.has(jobId);
94
113
  }
95
- /** Graceful shutdown: stop pulling, wait for in-flight jobs. */
96
- async close() {
114
+ /** Graceful shutdown: stop pulling, wait for in-flight jobs.
115
+ * With `force` the wait for in-flight jobs is skipped (parity with the
116
+ * official client's `close(force)`). */
117
+ async close(force = false) {
97
118
  if (this.closedFlag)
98
119
  return;
99
120
  this.stopped = true;
100
121
  if (this.loopPromise)
101
122
  await this.loopPromise;
102
- while (this.active.size > 0)
123
+ while (!force && this.active.size > 0)
103
124
  await sleep(20);
104
125
  if (this.heartbeatTimer) {
105
126
  clearInterval(this.heartbeatTimer);
106
127
  this.heartbeatTimer = null;
107
128
  }
108
- await this.safeCall({ cmd: 'UnregisterWorker', workerId: this.workerId });
129
+ // Only unregister when the loop actually registered (autorun: false and
130
+ // never run() means the server does not know this worker).
131
+ if (this.registeredGeneration >= 0) {
132
+ await this.safeCall({ cmd: 'UnregisterWorker', workerId: this.workerId });
133
+ }
109
134
  this.connection.close();
110
135
  this.closedFlag = true;
111
136
  this.running = false;
package/dist/worker.js CHANGED
@@ -32,6 +32,7 @@ export class Worker extends WorkerBase {
32
32
  async loop() {
33
33
  await this.register();
34
34
  this.readyResolve?.();
35
+ this.readyFired = true;
35
36
  this.emit('ready');
36
37
  this.startHeartbeat();
37
38
  let backoffIdx = 0;
@@ -63,6 +64,12 @@ export class Worker extends WorkerBase {
63
64
  await sleep(20);
64
65
  return;
65
66
  }
67
+ // The registration is per-connection server state: after a reconnect the
68
+ // server no longer knows this worker (ListWorkers, skipIfNoWorker crons).
69
+ // Detect the new connection generation and re-register before pulling.
70
+ if (this.connection.isConnected && this.connection.generation !== this.registeredGeneration) {
71
+ await this.register();
72
+ }
66
73
  const response = await this.connection.call({
67
74
  cmd: 'PULLB',
68
75
  queue: this.queue,
@@ -147,5 +154,6 @@ export class Worker extends WorkerBase {
147
154
  pid: process.pid,
148
155
  startedAt: Date.now(),
149
156
  });
157
+ this.registeredGeneration = this.connection.generation;
150
158
  }
151
159
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bunqueue-client",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Cross-runtime TypeScript client for the bunqueue job queue server — Node.js, Bun, Deno and Cloudflare Workers",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Bunqueue Simple Mode — priority aging.
3
+ * Port of src/client/bunqueue/aging.ts: automatically boosts the priority of
4
+ * old waiting jobs. Same semantics; the job scan goes over TCP (GetJobs on
5
+ * the waiting and prioritized states) instead of the embedded manager.
6
+ */
7
+
8
+ import type { Queue } from '../queue.js';
9
+ import type { PriorityAgingConfig } from './types.js';
10
+
11
+ export class PriorityAger<T = unknown> {
12
+ private timer: ReturnType<typeof setInterval> | null = null;
13
+ private readonly config: PriorityAgingConfig;
14
+ private readonly queue: Queue<T>;
15
+
16
+ constructor(config: PriorityAgingConfig, queue: Queue<T>) {
17
+ this.config = config;
18
+ this.queue = queue;
19
+ }
20
+
21
+ start(): void {
22
+ const interval = this.config.interval ?? 60000;
23
+ this.timer = setInterval(() => {
24
+ void this.tick();
25
+ }, interval);
26
+ this.timer.unref?.();
27
+ }
28
+
29
+ private async tick(): Promise<void> {
30
+ const minAge = this.config.minAge ?? 60000;
31
+ const boost = this.config.boost ?? 1;
32
+ const maxPriority = this.config.maxPriority ?? 100;
33
+ const maxScan = this.config.maxScan ?? 100;
34
+
35
+ // Both waiting and prioritized jobs (priority > 0 means "prioritized")
36
+ const [waiting, prioritized] = await Promise.all([
37
+ this.queue.getJobs({ state: 'waiting', start: 0, end: maxScan }),
38
+ this.queue.getJobs({ state: 'prioritized', start: 0, end: maxScan }),
39
+ ]);
40
+ const jobs = [...waiting, ...prioritized];
41
+ const now = Date.now();
42
+
43
+ for (const job of jobs) {
44
+ const age = now - job.timestamp;
45
+ if (age >= minAge && job.priority < maxPriority) {
46
+ const newPriority = Math.min(job.priority + boost, maxPriority);
47
+ try {
48
+ await this.queue.changeJobPriority(job.id, { priority: newPriority });
49
+ } catch {
50
+ // Best-effort — the job may have been processed meanwhile
51
+ }
52
+ }
53
+ }
54
+ }
55
+
56
+ destroy(): void {
57
+ if (this.timer) {
58
+ clearInterval(this.timer);
59
+ this.timer = null;
60
+ }
61
+ }
62
+ }