@blokjs/trigger-worker 0.6.18 → 0.6.20

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 (37) hide show
  1. package/dist/WorkerTrigger.d.ts +27 -3
  2. package/dist/WorkerTrigger.js +168 -26
  3. package/dist/adapters/KafkaAdapter.d.ts +5 -0
  4. package/dist/adapters/KafkaAdapter.js +12 -1
  5. package/dist/index.d.ts +1 -1
  6. package/dist/index.js +2 -2
  7. package/package.json +5 -4
  8. package/CHANGELOG.md +0 -22
  9. package/__tests__/integration/nats-adapter.real-nats.test.ts +0 -116
  10. package/__tests__/integration/pgboss-adapter.real-pg.test.ts +0 -164
  11. package/__tests__/integration/rabbitmq-adapter.real-rabbitmq.test.ts +0 -179
  12. package/__tests__/integration/sqs-adapter.real-sqs.test.ts +0 -228
  13. package/src/WorkerTrigger.test.ts +0 -540
  14. package/src/WorkerTrigger.ts +0 -784
  15. package/src/adapters/BullMQAdapter.ts +0 -296
  16. package/src/adapters/InMemoryAdapter.ts +0 -280
  17. package/src/adapters/KafkaAdapter.ts +0 -277
  18. package/src/adapters/NATSAdapter.ts +0 -454
  19. package/src/adapters/PgBossAdapter.ts +0 -293
  20. package/src/adapters/RabbitMQAdapter.ts +0 -285
  21. package/src/adapters/RedisStreamsAdapter.ts +0 -286
  22. package/src/adapters/SQSAdapter.ts +0 -306
  23. package/src/adapters/factory.test.ts +0 -89
  24. package/src/adapters/factory.ts +0 -111
  25. package/src/adapters/new-adapters.test.ts +0 -130
  26. package/src/index.ts +0 -94
  27. package/template/.env.example +0 -13
  28. package/template/package.json +0 -45
  29. package/template/src/Nodes.ts +0 -10
  30. package/template/src/Workflows.ts +0 -8
  31. package/template/src/index.ts +0 -41
  32. package/template/src/runner/WorkerServer.ts +0 -34
  33. package/template/src/runner/types/Workflows.ts +0 -7
  34. package/template/src/workflows/jobs/process-job.ts +0 -47
  35. package/template/tsconfig.json +0 -31
  36. package/template/vitest.config.ts +0 -39
  37. package/tsconfig.json +0 -32
@@ -1,296 +0,0 @@
1
- /**
2
- * BullMQAdapter - Worker adapter using BullMQ (Redis-backed)
3
- *
4
- * Features:
5
- * - Redis-backed persistent job queues
6
- * - Configurable concurrency per queue
7
- * - Job priority support
8
- * - Delayed job scheduling
9
- * - Automatic retries with configurable backoff
10
- * - Queue statistics
11
- *
12
- * Requires: bullmq and ioredis as peer dependencies
13
- *
14
- * @example
15
- * ```typescript
16
- * const adapter = new BullMQAdapter({
17
- * host: "localhost",
18
- * port: 6379,
19
- * });
20
- * ```
21
- */
22
-
23
- import type { WorkerAdapter, WorkerJob, WorkerQueueStats } from "../WorkerTrigger";
24
-
25
- import type { WorkerTriggerOpts } from "@blokjs/helper";
26
-
27
- /**
28
- * BullMQ adapter configuration
29
- */
30
- export interface BullMQConfig {
31
- /** Redis host (default: from REDIS_HOST env or "localhost") */
32
- host: string;
33
- /** Redis port (default: from REDIS_PORT env or 6379) */
34
- port: number;
35
- /** Redis password (default: from REDIS_PASSWORD env) */
36
- password?: string;
37
- /** Redis database (default: from REDIS_DB env or 0) */
38
- db?: number;
39
- /** Key prefix for all BullMQ keys */
40
- prefix?: string;
41
- /** Max stalled count before job fails */
42
- maxStalledCount?: number;
43
- /** Stalled interval in ms */
44
- stalledInterval?: number;
45
- }
46
-
47
- /**
48
- * BullMQ Worker Adapter
49
- *
50
- * Uses BullMQ for robust, Redis-backed job processing with support for
51
- * priority queues, delayed jobs, retries, and dead letter handling.
52
- */
53
- export class BullMQAdapter implements WorkerAdapter {
54
- readonly provider = "bullmq" as const;
55
- private connection: unknown = null;
56
- private workers: Map<string, unknown> = new Map();
57
- private queues: Map<string, unknown> = new Map();
58
- private connected = false;
59
- private readonly config: BullMQConfig;
60
-
61
- constructor(config?: Partial<BullMQConfig>) {
62
- this.config = {
63
- host: config?.host || process.env.REDIS_HOST || "localhost",
64
- port: config?.port ?? Number.parseInt(process.env.REDIS_PORT || "6379", 10),
65
- password: config?.password || process.env.REDIS_PASSWORD,
66
- db: config?.db ?? Number.parseInt(process.env.REDIS_DB || "0", 10),
67
- prefix: config?.prefix || "blok-worker",
68
- maxStalledCount: config?.maxStalledCount ?? 2,
69
- stalledInterval: config?.stalledInterval ?? 5000,
70
- };
71
- }
72
-
73
- async connect(): Promise<void> {
74
- if (this.connected) return;
75
-
76
- try {
77
- const { default: IORedis } = await import("ioredis");
78
- this.connection = new IORedis({
79
- host: this.config.host,
80
- port: this.config.port,
81
- password: this.config.password,
82
- db: this.config.db,
83
- maxRetriesPerRequest: null, // Required for BullMQ
84
- });
85
-
86
- // Verify connection
87
- await (this.connection as { ping: () => Promise<string> }).ping();
88
- this.connected = true;
89
- console.log(`[BullMQAdapter] Connected to Redis at ${this.config.host}:${this.config.port}`);
90
- } catch (error) {
91
- throw new Error(
92
- `Failed to connect to Redis: ${(error as Error).message}. Ensure ioredis and bullmq are installed: npm install ioredis bullmq`,
93
- );
94
- }
95
- }
96
-
97
- async disconnect(): Promise<void> {
98
- if (!this.connected) return;
99
-
100
- try {
101
- // Close all workers
102
- for (const [, worker] of this.workers) {
103
- await (worker as { close: () => Promise<void> }).close();
104
- }
105
- this.workers.clear();
106
-
107
- // Close all queues
108
- for (const [, queue] of this.queues) {
109
- await (queue as { close: () => Promise<void> }).close();
110
- }
111
- this.queues.clear();
112
-
113
- // Close Redis connection
114
- if (this.connection) {
115
- await (this.connection as { quit: () => Promise<string> }).quit();
116
- }
117
-
118
- this.connected = false;
119
- console.log("[BullMQAdapter] Disconnected from Redis");
120
- } catch (error) {
121
- console.error(`[BullMQAdapter] Disconnect error: ${(error as Error).message}`);
122
- }
123
- }
124
-
125
- async process(config: WorkerTriggerOpts, handler: (job: WorkerJob) => Promise<void>): Promise<void> {
126
- if (!this.connected) {
127
- throw new Error("Not connected. Call connect() first.");
128
- }
129
-
130
- try {
131
- // Dynamic import to avoid hard dependency on bullmq
132
- const bullmq = await import("bullmq");
133
- const BullWorker = bullmq.Worker;
134
- const BullQueue = bullmq.Queue;
135
-
136
- // Build worker options
137
- const workerOpts = {
138
- connection: this.connection,
139
- concurrency: config.concurrency ?? 1,
140
- prefix: this.config.prefix,
141
- stalledInterval: this.config.stalledInterval ?? 5000,
142
- maxStalledCount: this.config.maxStalledCount ?? 2,
143
- };
144
-
145
- const worker = new BullWorker(
146
- config.queue,
147
- // BullMQ processor callback
148
- ((bullJob: unknown) => {
149
- const job = bullJob as {
150
- id?: string;
151
- data: unknown;
152
- opts?: { priority?: number; delay?: number };
153
- attemptsMade: number;
154
- timestamp: number;
155
- token?: string;
156
- moveToFailed: (err: Error, token: string, fetchNext: boolean) => Promise<void>;
157
- };
158
- const workerJob: WorkerJob = {
159
- id: job.id || `job-${Date.now()}`,
160
- data: job.data,
161
- headers: ((job.data as Record<string, unknown>)?._headers as Record<string, string>) || {},
162
- queue: config.queue,
163
- priority: job.opts?.priority ?? config.priority ?? 0,
164
- attempts: job.attemptsMade,
165
- maxRetries: config.retries ?? 3,
166
- createdAt: new Date(job.timestamp),
167
- delay: job.opts?.delay,
168
- timeout: config.timeout,
169
- raw: job,
170
- complete: async () => {
171
- // BullMQ auto-completes when processor resolves
172
- },
173
- fail: async (error: Error, requeue?: boolean) => {
174
- if (!requeue) {
175
- await job.moveToFailed(error, job.token || "", true);
176
- } else {
177
- throw error; // BullMQ will auto-retry
178
- }
179
- },
180
- };
181
- return handler(workerJob);
182
- }) as never,
183
- workerOpts as never,
184
- );
185
-
186
- this.workers.set(config.queue, worker);
187
-
188
- // Ensure queue object exists for job dispatching
189
- if (!this.queues.has(config.queue)) {
190
- const queue = new BullQueue(config.queue, {
191
- connection: this.connection as { host: string; port: number },
192
- prefix: this.config.prefix,
193
- } as never);
194
- this.queues.set(config.queue, queue);
195
- }
196
-
197
- console.log(`[BullMQAdapter] Processing queue: ${config.queue} (concurrency=${config.concurrency ?? 1})`);
198
- } catch (error) {
199
- throw new Error(`Failed to start processing: ${(error as Error).message}`);
200
- }
201
- }
202
-
203
- async addJob(
204
- queue: string,
205
- data: unknown,
206
- opts?: {
207
- priority?: number;
208
- delay?: number;
209
- retries?: number;
210
- timeout?: number;
211
- jobId?: string;
212
- },
213
- ): Promise<string> {
214
- if (!this.connected) {
215
- throw new Error("Not connected. Call connect() first.");
216
- }
217
-
218
- try {
219
- // Ensure queue exists
220
- if (!this.queues.has(queue)) {
221
- const { Queue } = await import("bullmq");
222
- const q = new Queue(queue, {
223
- connection: this.connection as { host: string; port: number },
224
- prefix: this.config.prefix,
225
- });
226
- this.queues.set(queue, q);
227
- }
228
-
229
- const q = this.queues.get(queue) as {
230
- add: (name: string, data: unknown, opts: Record<string, unknown>) => Promise<{ id: string }>;
231
- };
232
-
233
- const job = await q.add("process", data, {
234
- priority: opts?.priority,
235
- delay: opts?.delay,
236
- attempts: (opts?.retries ?? 3) + 1,
237
- jobId: opts?.jobId,
238
- backoff: {
239
- type: "exponential",
240
- delay: 1000,
241
- },
242
- });
243
-
244
- return job.id;
245
- } catch (error) {
246
- throw new Error(`Failed to add job: ${(error as Error).message}`);
247
- }
248
- }
249
-
250
- async stopProcessing(queue: string): Promise<void> {
251
- const worker = this.workers.get(queue);
252
- if (worker) {
253
- await (worker as { close: () => Promise<void> }).close();
254
- this.workers.delete(queue);
255
- console.log(`[BullMQAdapter] Stopped processing queue: ${queue}`);
256
- }
257
- }
258
-
259
- isConnected(): boolean {
260
- return this.connected;
261
- }
262
-
263
- async healthCheck(): Promise<boolean> {
264
- if (!this.connected || !this.connection) return false;
265
- try {
266
- await (this.connection as { ping: () => Promise<string> }).ping();
267
- return true;
268
- } catch {
269
- return false;
270
- }
271
- }
272
-
273
- async getQueueStats(queue: string): Promise<WorkerQueueStats> {
274
- if (!this.queues.has(queue)) {
275
- return { waiting: 0, active: 0, completed: 0, failed: 0, delayed: 0 };
276
- }
277
-
278
- const q = this.queues.get(queue) as {
279
- getWaitingCount: () => Promise<number>;
280
- getActiveCount: () => Promise<number>;
281
- getCompletedCount: () => Promise<number>;
282
- getFailedCount: () => Promise<number>;
283
- getDelayedCount: () => Promise<number>;
284
- };
285
-
286
- const [waiting, active, completed, failed, delayed] = await Promise.all([
287
- q.getWaitingCount(),
288
- q.getActiveCount(),
289
- q.getCompletedCount(),
290
- q.getFailedCount(),
291
- q.getDelayedCount(),
292
- ]);
293
-
294
- return { waiting, active, completed, failed, delayed };
295
- }
296
- }
@@ -1,280 +0,0 @@
1
- /**
2
- * InMemoryAdapter - Worker adapter using in-process queues
3
- *
4
- * Ideal for:
5
- * - Development and testing
6
- * - Simple background job processing
7
- * - Single-instance deployments
8
- *
9
- * Limitations:
10
- * - Jobs are lost on process restart
11
- * - No distributed processing
12
- * - No persistence
13
- *
14
- * @example
15
- * ```typescript
16
- * const adapter = new InMemoryAdapter();
17
- * ```
18
- */
19
-
20
- import type { WorkerTriggerOpts } from "@blokjs/helper";
21
- import { v4 as uuid } from "uuid";
22
- import type { WorkerAdapter, WorkerJob, WorkerQueueStats } from "../WorkerTrigger";
23
-
24
- /**
25
- * Internal job representation
26
- */
27
- interface InternalJob {
28
- id: string;
29
- data: unknown;
30
- queue: string;
31
- priority: number;
32
- attempts: number;
33
- maxRetries: number;
34
- createdAt: Date;
35
- delay: number;
36
- timeout: number;
37
- status: "waiting" | "active" | "completed" | "failed" | "delayed";
38
- scheduledAt?: Date;
39
- error?: Error;
40
- }
41
-
42
- /**
43
- * Queue processor entry
44
- */
45
- interface QueueProcessor {
46
- config: WorkerTriggerOpts;
47
- handler: (job: WorkerJob) => Promise<void>;
48
- active: number;
49
- running: boolean;
50
- timer?: ReturnType<typeof setInterval>;
51
- }
52
-
53
- /**
54
- * InMemoryAdapter - Simple in-process worker queue
55
- */
56
- export class InMemoryAdapter implements WorkerAdapter {
57
- readonly provider = "in-memory" as const;
58
- private connected = false;
59
- private jobs: Map<string, InternalJob[]> = new Map();
60
- private processors: Map<string, QueueProcessor> = new Map();
61
- private stats: Map<string, { completed: number; failed: number }> = new Map();
62
-
63
- async connect(): Promise<void> {
64
- this.connected = true;
65
- }
66
-
67
- async disconnect(): Promise<void> {
68
- // Stop all processors
69
- for (const [queue, processor] of this.processors) {
70
- processor.running = false;
71
- if (processor.timer) {
72
- clearInterval(processor.timer);
73
- }
74
- }
75
- this.processors.clear();
76
- this.jobs.clear();
77
- this.stats.clear();
78
- this.connected = false;
79
- }
80
-
81
- async process(config: WorkerTriggerOpts, handler: (job: WorkerJob) => Promise<void>): Promise<void> {
82
- if (!this.connected) {
83
- throw new Error("Not connected. Call connect() first.");
84
- }
85
-
86
- if (!this.jobs.has(config.queue)) {
87
- this.jobs.set(config.queue, []);
88
- }
89
- if (!this.stats.has(config.queue)) {
90
- this.stats.set(config.queue, { completed: 0, failed: 0 });
91
- }
92
-
93
- const processor: QueueProcessor = {
94
- config,
95
- handler,
96
- active: 0,
97
- running: true,
98
- };
99
-
100
- this.processors.set(config.queue, processor);
101
-
102
- // Start polling for jobs
103
- processor.timer = setInterval(() => {
104
- this.processNext(config.queue).catch((err) => {
105
- console.error(`[InMemoryAdapter] Error processing ${config.queue}: ${(err as Error).message}`);
106
- });
107
- }, 50); // Poll every 50ms
108
- }
109
-
110
- async addJob(
111
- queue: string,
112
- data: unknown,
113
- opts?: {
114
- priority?: number;
115
- delay?: number;
116
- retries?: number;
117
- timeout?: number;
118
- jobId?: string;
119
- },
120
- ): Promise<string> {
121
- if (!this.connected) {
122
- throw new Error("Not connected. Call connect() first.");
123
- }
124
-
125
- // Get-or-init the per-queue job list. Same pattern as `stats`
126
- // below — the previous code used `set-if-absent` then `.get()!`,
127
- // but that non-null assertion is what biome flags. Pulling the
128
- // reference once and seeding when missing keeps the type exact.
129
- let jobs = this.jobs.get(queue);
130
- if (!jobs) {
131
- jobs = [];
132
- this.jobs.set(queue, jobs);
133
- }
134
- if (!this.stats.has(queue)) {
135
- this.stats.set(queue, { completed: 0, failed: 0 });
136
- }
137
-
138
- const job: InternalJob = {
139
- id: opts?.jobId || uuid(),
140
- data,
141
- queue,
142
- priority: opts?.priority ?? 0,
143
- attempts: 0,
144
- maxRetries: opts?.retries ?? 3,
145
- createdAt: new Date(),
146
- delay: opts?.delay ?? 0,
147
- timeout: opts?.timeout ?? 0,
148
- status: opts?.delay && opts.delay > 0 ? "delayed" : "waiting",
149
- };
150
-
151
- if (job.status === "delayed") {
152
- job.scheduledAt = new Date(Date.now() + job.delay);
153
- }
154
-
155
- // Insert sorted by priority (higher first)
156
- const insertIdx = jobs.findIndex((j) => j.status === "waiting" && j.priority < job.priority);
157
- if (insertIdx >= 0) {
158
- jobs.splice(insertIdx, 0, job);
159
- } else {
160
- jobs.push(job);
161
- }
162
-
163
- return job.id;
164
- }
165
-
166
- async stopProcessing(queue: string): Promise<void> {
167
- const processor = this.processors.get(queue);
168
- if (processor) {
169
- processor.running = false;
170
- if (processor.timer) {
171
- clearInterval(processor.timer);
172
- }
173
- this.processors.delete(queue);
174
- }
175
- }
176
-
177
- isConnected(): boolean {
178
- return this.connected;
179
- }
180
-
181
- async healthCheck(): Promise<boolean> {
182
- return this.connected;
183
- }
184
-
185
- async getQueueStats(queue: string): Promise<WorkerQueueStats> {
186
- const jobs = this.jobs.get(queue) || [];
187
- const queueStats = this.stats.get(queue) || { completed: 0, failed: 0 };
188
-
189
- return {
190
- waiting: jobs.filter((j) => j.status === "waiting").length,
191
- active: jobs.filter((j) => j.status === "active").length,
192
- completed: queueStats.completed,
193
- failed: queueStats.failed,
194
- delayed: jobs.filter((j) => j.status === "delayed").length,
195
- };
196
- }
197
-
198
- /**
199
- * Process the next available job from a queue
200
- */
201
- private async processNext(queue: string): Promise<void> {
202
- const processor = this.processors.get(queue);
203
- if (!processor || !processor.running) return;
204
-
205
- const concurrency = processor.config.concurrency ?? 1;
206
- if (processor.active >= concurrency) return;
207
-
208
- const jobs = this.jobs.get(queue);
209
- if (!jobs || jobs.length === 0) return;
210
-
211
- // Check for delayed jobs that are ready
212
- const now = Date.now();
213
- for (const job of jobs) {
214
- if (job.status === "delayed" && job.scheduledAt && job.scheduledAt.getTime() <= now) {
215
- job.status = "waiting";
216
- }
217
- }
218
-
219
- // Find next waiting job
220
- const jobIdx = jobs.findIndex((j) => j.status === "waiting");
221
- if (jobIdx < 0) return;
222
-
223
- const internalJob = jobs[jobIdx];
224
- internalJob.status = "active";
225
- processor.active++;
226
-
227
- const workerJob: WorkerJob = {
228
- id: internalJob.id,
229
- data: internalJob.data,
230
- headers: {},
231
- queue: internalJob.queue,
232
- priority: internalJob.priority,
233
- attempts: internalJob.attempts,
234
- maxRetries: internalJob.maxRetries,
235
- createdAt: internalJob.createdAt,
236
- delay: internalJob.delay,
237
- timeout: internalJob.timeout,
238
- raw: internalJob,
239
- complete: async () => {
240
- internalJob.status = "completed";
241
- const idx = jobs.indexOf(internalJob);
242
- if (idx >= 0) jobs.splice(idx, 1);
243
- const s = this.stats.get(queue);
244
- if (s) s.completed++;
245
- },
246
- fail: async (error: Error, requeue?: boolean) => {
247
- internalJob.attempts++;
248
- internalJob.error = error;
249
-
250
- if (requeue && internalJob.attempts < internalJob.maxRetries) {
251
- // Requeue with backoff
252
- const backoff = Math.min(1000 * 2 ** internalJob.attempts, 30000);
253
- internalJob.status = "delayed";
254
- internalJob.scheduledAt = new Date(Date.now() + backoff);
255
- } else {
256
- internalJob.status = "failed";
257
- const idx = jobs.indexOf(internalJob);
258
- if (idx >= 0) jobs.splice(idx, 1);
259
- const s = this.stats.get(queue);
260
- if (s) s.failed++;
261
- }
262
- },
263
- };
264
-
265
- try {
266
- await processor.handler(workerJob);
267
- } catch {
268
- // Handler threw - treat as failure
269
- if (internalJob.status === "active") {
270
- internalJob.status = "failed";
271
- const idx = jobs.indexOf(internalJob);
272
- if (idx >= 0) jobs.splice(idx, 1);
273
- const s = this.stats.get(queue);
274
- if (s) s.failed++;
275
- }
276
- } finally {
277
- processor.active--;
278
- }
279
- }
280
- }