bunqueue-client 0.1.0

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 (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +165 -0
  3. package/dist/connection-types.d.ts +24 -0
  4. package/dist/connection-types.js +2 -0
  5. package/dist/connection.d.ts +44 -0
  6. package/dist/connection.js +229 -0
  7. package/dist/errors.d.ts +22 -0
  8. package/dist/errors.js +25 -0
  9. package/dist/flow-types.d.ts +34 -0
  10. package/dist/flow-types.js +2 -0
  11. package/dist/flow.d.ts +31 -0
  12. package/dist/flow.js +186 -0
  13. package/dist/frame.d.ts +16 -0
  14. package/dist/frame.js +57 -0
  15. package/dist/index.d.ts +19 -0
  16. package/dist/index.js +12 -0
  17. package/dist/job.d.ts +56 -0
  18. package/dist/job.js +151 -0
  19. package/dist/queue-admin.d.ts +63 -0
  20. package/dist/queue-admin.js +143 -0
  21. package/dist/queue-control.d.ts +44 -0
  22. package/dist/queue-control.js +85 -0
  23. package/dist/queue-query.d.ts +58 -0
  24. package/dist/queue-query.js +165 -0
  25. package/dist/queue.d.ts +50 -0
  26. package/dist/queue.js +77 -0
  27. package/dist/types.d.ts +74 -0
  28. package/dist/types.js +50 -0
  29. package/dist/worker-base.d.ts +51 -0
  30. package/dist/worker-base.js +122 -0
  31. package/dist/worker-types.d.ts +27 -0
  32. package/dist/worker-types.js +5 -0
  33. package/dist/worker.d.ts +20 -0
  34. package/dist/worker.js +151 -0
  35. package/package.json +64 -0
  36. package/src/connection-types.ts +21 -0
  37. package/src/connection.ts +242 -0
  38. package/src/errors.ts +26 -0
  39. package/src/flow-types.ts +40 -0
  40. package/src/flow.ts +236 -0
  41. package/src/frame.ts +62 -0
  42. package/src/index.ts +41 -0
  43. package/src/job.ts +194 -0
  44. package/src/queue-admin.ts +213 -0
  45. package/src/queue-control.ts +145 -0
  46. package/src/queue-query.ts +220 -0
  47. package/src/queue.ts +112 -0
  48. package/src/types.ts +133 -0
  49. package/src/worker-base.ts +132 -0
  50. package/src/worker-types.ts +32 -0
  51. package/src/worker.ts +174 -0
package/src/worker.ts ADDED
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Worker: pulls jobs over TCP and runs a processor with bounded concurrency.
3
+ *
4
+ * All queue semantics (retry, backoff, DLQ, stall detection, priorities)
5
+ * live in the bunqueue server — the worker pulls, heartbeats and acks/fails.
6
+ * Lifecycle state and cooperative cancel live in WorkerBase.
7
+ */
8
+
9
+ import { hostname } from 'node:os';
10
+ import { CommandTimeoutError, ConnectionClosedError, UnrecoverableError } from './errors.js';
11
+ import { compact } from './frame.js';
12
+ import { Job } from './job.js';
13
+ import { WorkerBase } from './worker-base.js';
14
+ import {
15
+ MAX_STACK_LINES,
16
+ type Processor,
17
+ RECONNECT_BACKOFF_MS,
18
+ sleep,
19
+ type WorkerOptions,
20
+ } from './worker-types.js';
21
+
22
+ export class Worker<T = unknown, R = unknown> extends WorkerBase {
23
+ private readonly processor: Processor<T, R>;
24
+
25
+ constructor(queue: string, processor: Processor<T, R>, opts: WorkerOptions = {}) {
26
+ super(queue, opts);
27
+ this.processor = processor;
28
+ if (opts.autorun !== false) this.run();
29
+ }
30
+
31
+ /** Start the pull loop (no-op if already running). */
32
+ run(): void {
33
+ if (this.running || this.closedFlag) return;
34
+ this.running = true;
35
+ this.loopPromise = this.loop().catch((err) => {
36
+ this.emit('error', err);
37
+ });
38
+ }
39
+
40
+ // -------------------------------------------------------------------- loop
41
+
42
+ private async loop(): Promise<void> {
43
+ await this.register();
44
+ this.readyResolve?.();
45
+ this.emit('ready');
46
+ this.startHeartbeat();
47
+
48
+ let backoffIdx = 0;
49
+ while (!this.stopped) {
50
+ if (this.paused) {
51
+ await sleep(50);
52
+ continue;
53
+ }
54
+ try {
55
+ await this.pollOnce();
56
+ backoffIdx = 0;
57
+ } catch (err) {
58
+ this.emit('error', err);
59
+ if (err instanceof ConnectionClosedError || err instanceof CommandTimeoutError) {
60
+ const delay = RECONNECT_BACKOFF_MS[Math.min(backoffIdx, RECONNECT_BACKOFF_MS.length - 1)];
61
+ backoffIdx += 1;
62
+ await sleep(delay);
63
+ } else {
64
+ await sleep(200);
65
+ }
66
+ }
67
+ }
68
+ }
69
+
70
+ private async pollOnce(): Promise<void> {
71
+ const free = this.concurrency - this.active.size;
72
+ if (free <= 0) {
73
+ await sleep(20);
74
+ return;
75
+ }
76
+
77
+ const response = await this.connection.call(
78
+ {
79
+ cmd: 'PULLB',
80
+ queue: this.queue,
81
+ count: Math.min(free, this.batchSize),
82
+ timeout: this.pollTimeoutMs,
83
+ owner: this.workerId,
84
+ lockTtl: this.lockTtlMs,
85
+ },
86
+ this.pollTimeoutMs + 10_000
87
+ );
88
+
89
+ const jobs = (response.jobs ?? []) as Record<string, unknown>[];
90
+ const tokens = (response.tokens ?? []) as string[];
91
+
92
+ if (jobs.length === 0) {
93
+ if (this.wasBusy && this.active.size === 0) {
94
+ this.wasBusy = false;
95
+ this.emit('drained');
96
+ }
97
+ return;
98
+ }
99
+
100
+ this.wasBusy = true;
101
+ for (let i = 0; i < jobs.length; i++) {
102
+ const jobId = String(jobs[i].id);
103
+ this.active.set(jobId, tokens[i]);
104
+ // fire-and-forget: bounded by the free-slot accounting above
105
+ void this.runJob(jobs[i], tokens[i]);
106
+ }
107
+ }
108
+
109
+ private async runJob(raw: Record<string, unknown>, token: string): Promise<void> {
110
+ const job = new Job<T>(raw, this.connection, token, (j, p) => this.emit('progress', j, p));
111
+ this.emit('active', job);
112
+ try {
113
+ const result = await this.processor(job);
114
+ this.processed += 1;
115
+ await this.safeCall(
116
+ compact({ cmd: 'ACK', id: job.id, token, result: result ?? undefined }) as { cmd: string }
117
+ );
118
+ this.emit('completed', job, result);
119
+ } catch (err) {
120
+ this.failedCount += 1;
121
+ const error = err instanceof Error ? err : new Error(String(err));
122
+ const stack = (error.stack ?? error.message).split('\n').slice(-MAX_STACK_LINES);
123
+ await this.safeCall(
124
+ compact({
125
+ cmd: 'FAIL',
126
+ id: job.id,
127
+ token,
128
+ error: error.message || error.name,
129
+ stack,
130
+ unrecoverable: err instanceof UnrecoverableError ? true : undefined,
131
+ }) as { cmd: string }
132
+ );
133
+ this.emit('failed', job, error);
134
+ } finally {
135
+ this.active.delete(job.id);
136
+ this.cancelledJobs.delete(job.id);
137
+ }
138
+ }
139
+
140
+ // --------------------------------------------------------------- heartbeat
141
+
142
+ private startHeartbeat(): void {
143
+ this.heartbeatTimer = setInterval(() => {
144
+ void (async () => {
145
+ await this.safeCall({
146
+ cmd: 'Heartbeat',
147
+ id: this.workerId,
148
+ activeJobs: this.active.size,
149
+ processed: this.processed,
150
+ failed: this.failedCount,
151
+ });
152
+ if (this.active.size > 0) {
153
+ const ids = [...this.active.keys()];
154
+ const tokens = ids.map((id) => this.active.get(id) as string);
155
+ await this.safeCall({ cmd: 'JobHeartbeatB', ids, tokens });
156
+ }
157
+ })();
158
+ }, this.heartbeatIntervalS * 1000);
159
+ this.heartbeatTimer.unref?.(); // don't keep the process alive for heartbeats
160
+ }
161
+
162
+ private async register(): Promise<void> {
163
+ await this.safeCall({
164
+ cmd: 'RegisterWorker',
165
+ name: this.name,
166
+ queues: [this.queue],
167
+ concurrency: this.concurrency,
168
+ workerId: this.workerId,
169
+ hostname: hostname(),
170
+ pid: process.pid,
171
+ startedAt: Date.now(),
172
+ });
173
+ }
174
+ }