@blokjs/trigger-worker 0.6.18 → 0.6.19

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,540 +0,0 @@
1
- /**
2
- * WorkerTrigger Tests
3
- *
4
- * Tests the WorkerTrigger base class, WorkerAdapter interface,
5
- * InMemoryAdapter, and BullMQAdapter configuration.
6
- */
7
-
8
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
9
- import type { WorkerAdapter, WorkerJob, WorkerQueueStats } from "./WorkerTrigger";
10
- import { InMemoryAdapter } from "./adapters/InMemoryAdapter";
11
- import { computeXDelayHoldMs } from "./adapters/NATSAdapter";
12
-
13
- // ============================================================================
14
- // WorkerJob Interface Tests
15
- // ============================================================================
16
-
17
- describe("WorkerTrigger", () => {
18
- describe("WorkerJob Interface", () => {
19
- it("should accept valid worker job structure", () => {
20
- const job: WorkerJob = {
21
- id: "job-123",
22
- data: { userId: "user-1", action: "send-email" },
23
- headers: { "content-type": "application/json" },
24
- queue: "background-jobs",
25
- priority: 5,
26
- attempts: 0,
27
- maxRetries: 3,
28
- createdAt: new Date(),
29
- delay: 0,
30
- timeout: 30000,
31
- raw: {},
32
- complete: async () => {},
33
- fail: async () => {},
34
- };
35
-
36
- expect(job.id).toBe("job-123");
37
- expect(job.data).toEqual({ userId: "user-1", action: "send-email" });
38
- expect(job.queue).toBe("background-jobs");
39
- expect(job.priority).toBe(5);
40
- expect(job.maxRetries).toBe(3);
41
- });
42
-
43
- it("should handle minimal required fields", () => {
44
- const job: WorkerJob = {
45
- id: "job-min",
46
- data: null,
47
- headers: {},
48
- queue: "default",
49
- priority: 0,
50
- attempts: 0,
51
- maxRetries: 0,
52
- createdAt: new Date(),
53
- raw: null,
54
- complete: async () => {},
55
- fail: async () => {},
56
- };
57
-
58
- expect(job.id).toBeDefined();
59
- expect(job.queue).toBeDefined();
60
- expect(job.complete).toBeDefined();
61
- expect(job.fail).toBeDefined();
62
- });
63
-
64
- it("should support optional delay and timeout", () => {
65
- const job: WorkerJob = {
66
- id: "job-delayed",
67
- data: { type: "scheduled-report" },
68
- headers: {},
69
- queue: "reports",
70
- priority: 1,
71
- attempts: 0,
72
- maxRetries: 2,
73
- createdAt: new Date(),
74
- delay: 60000,
75
- timeout: 120000,
76
- raw: {},
77
- complete: async () => {},
78
- fail: async () => {},
79
- };
80
-
81
- expect(job.delay).toBe(60000);
82
- expect(job.timeout).toBe(120000);
83
- });
84
- });
85
-
86
- describe("WorkerAdapter Interface", () => {
87
- it("should validate adapter interface methods", () => {
88
- const mockAdapter: WorkerAdapter = {
89
- provider: "mock",
90
- connect: vi.fn().mockResolvedValue(undefined),
91
- disconnect: vi.fn().mockResolvedValue(undefined),
92
- process: vi.fn().mockResolvedValue(undefined),
93
- addJob: vi.fn().mockResolvedValue("job-1"),
94
- stopProcessing: vi.fn().mockResolvedValue(undefined),
95
- isConnected: vi.fn().mockReturnValue(true),
96
- healthCheck: vi.fn().mockResolvedValue(true),
97
- getQueueStats: vi.fn().mockResolvedValue({
98
- waiting: 5,
99
- active: 2,
100
- completed: 100,
101
- failed: 3,
102
- delayed: 1,
103
- }),
104
- };
105
-
106
- expect(mockAdapter.provider).toBe("mock");
107
- expect(typeof mockAdapter.connect).toBe("function");
108
- expect(typeof mockAdapter.disconnect).toBe("function");
109
- expect(typeof mockAdapter.process).toBe("function");
110
- expect(typeof mockAdapter.addJob).toBe("function");
111
- expect(typeof mockAdapter.stopProcessing).toBe("function");
112
- expect(typeof mockAdapter.isConnected).toBe("function");
113
- expect(typeof mockAdapter.healthCheck).toBe("function");
114
- expect(typeof mockAdapter.getQueueStats).toBe("function");
115
- });
116
-
117
- it("should return correct queue stats structure", async () => {
118
- const stats: WorkerQueueStats = {
119
- waiting: 10,
120
- active: 3,
121
- completed: 500,
122
- failed: 12,
123
- delayed: 5,
124
- };
125
-
126
- expect(stats.waiting).toBe(10);
127
- expect(stats.active).toBe(3);
128
- expect(stats.completed).toBe(500);
129
- expect(stats.failed).toBe(12);
130
- expect(stats.delayed).toBe(5);
131
- });
132
- });
133
- });
134
-
135
- // ============================================================================
136
- // InMemoryAdapter Tests
137
- // ============================================================================
138
-
139
- describe("InMemoryAdapter", () => {
140
- let adapter: InMemoryAdapter;
141
-
142
- beforeEach(() => {
143
- adapter = new InMemoryAdapter();
144
- });
145
-
146
- afterEach(async () => {
147
- await adapter.disconnect();
148
- });
149
-
150
- describe("Connection Lifecycle", () => {
151
- it("should connect successfully", async () => {
152
- expect(adapter.isConnected()).toBe(false);
153
- await adapter.connect();
154
- expect(adapter.isConnected()).toBe(true);
155
- });
156
-
157
- it("should disconnect successfully", async () => {
158
- await adapter.connect();
159
- expect(adapter.isConnected()).toBe(true);
160
- await adapter.disconnect();
161
- expect(adapter.isConnected()).toBe(false);
162
- });
163
-
164
- it("should report healthy when connected", async () => {
165
- await adapter.connect();
166
- expect(await adapter.healthCheck()).toBe(true);
167
- });
168
-
169
- it("should report unhealthy when disconnected", async () => {
170
- expect(await adapter.healthCheck()).toBe(false);
171
- });
172
-
173
- it("should have provider name 'in-memory'", () => {
174
- expect(adapter.provider).toBe("in-memory");
175
- });
176
- });
177
-
178
- describe("Job Dispatching", () => {
179
- it("should add a job and return its ID", async () => {
180
- await adapter.connect();
181
- const jobId = await adapter.addJob("test-queue", { action: "test" });
182
- expect(jobId).toBeDefined();
183
- expect(typeof jobId).toBe("string");
184
- });
185
-
186
- it("should accept custom job ID", async () => {
187
- await adapter.connect();
188
- const jobId = await adapter.addJob(
189
- "test-queue",
190
- { data: 1 },
191
- {
192
- jobId: "custom-id-123",
193
- },
194
- );
195
- expect(jobId).toBe("custom-id-123");
196
- });
197
-
198
- it("should add jobs with priority ordering", async () => {
199
- await adapter.connect();
200
- await adapter.addJob("priority-queue", { order: "low" }, { priority: 1 });
201
- await adapter.addJob("priority-queue", { order: "high" }, { priority: 10 });
202
- await adapter.addJob("priority-queue", { order: "medium" }, { priority: 5 });
203
-
204
- const stats = await adapter.getQueueStats("priority-queue");
205
- expect(stats.waiting).toBe(3);
206
- });
207
-
208
- it("should add delayed jobs", async () => {
209
- await adapter.connect();
210
- await adapter.addJob(
211
- "delayed-queue",
212
- { data: 1 },
213
- {
214
- delay: 5000,
215
- },
216
- );
217
-
218
- const stats = await adapter.getQueueStats("delayed-queue");
219
- expect(stats.delayed).toBe(1);
220
- expect(stats.waiting).toBe(0);
221
- });
222
-
223
- it("should throw when not connected", async () => {
224
- await expect(adapter.addJob("test-queue", { data: 1 })).rejects.toThrow("Not connected");
225
- });
226
- });
227
-
228
- describe("Job Processing", () => {
229
- it("should process jobs from a queue", async () => {
230
- await adapter.connect();
231
-
232
- const processedJobs: WorkerJob[] = [];
233
-
234
- await adapter.process({ queue: "process-queue", concurrency: 1, retries: 3, priority: 0 }, async (job) => {
235
- processedJobs.push(job);
236
- await job.complete();
237
- });
238
-
239
- await adapter.addJob("process-queue", { item: "test-1" });
240
-
241
- // Wait for processing
242
- await new Promise((resolve) => setTimeout(resolve, 200));
243
-
244
- expect(processedJobs).toHaveLength(1);
245
- expect(processedJobs[0].data).toEqual({ item: "test-1" });
246
- });
247
-
248
- it("should process multiple jobs sequentially", async () => {
249
- await adapter.connect();
250
-
251
- const processedOrder: number[] = [];
252
-
253
- await adapter.process({ queue: "seq-queue", concurrency: 1, retries: 3, priority: 0 }, async (job) => {
254
- processedOrder.push(job.data as number);
255
- await job.complete();
256
- });
257
-
258
- await adapter.addJob("seq-queue", 1);
259
- await adapter.addJob("seq-queue", 2);
260
- await adapter.addJob("seq-queue", 3);
261
-
262
- await new Promise((resolve) => setTimeout(resolve, 500));
263
-
264
- expect(processedOrder).toEqual([1, 2, 3]);
265
- });
266
-
267
- it("should track queue stats correctly", async () => {
268
- await adapter.connect();
269
-
270
- await adapter.process({ queue: "stats-queue", concurrency: 1, retries: 3, priority: 0 }, async (job) => {
271
- await job.complete();
272
- });
273
-
274
- await adapter.addJob("stats-queue", { a: 1 });
275
- await adapter.addJob("stats-queue", { a: 2 });
276
-
277
- await new Promise((resolve) => setTimeout(resolve, 300));
278
-
279
- const stats = await adapter.getQueueStats("stats-queue");
280
- expect(stats.completed).toBe(2);
281
- expect(stats.waiting).toBe(0);
282
- });
283
-
284
- it("should handle job failures", async () => {
285
- await adapter.connect();
286
-
287
- await adapter.process({ queue: "fail-queue", concurrency: 1, retries: 0, priority: 0 }, async (job) => {
288
- await job.fail(new Error("test failure"), false);
289
- });
290
-
291
- await adapter.addJob("fail-queue", { data: "will-fail" }, { retries: 0 });
292
-
293
- await new Promise((resolve) => setTimeout(resolve, 200));
294
-
295
- const stats = await adapter.getQueueStats("fail-queue");
296
- expect(stats.failed).toBe(1);
297
- });
298
-
299
- it("should requeue failed jobs for retry", async () => {
300
- await adapter.connect();
301
-
302
- let attemptCount = 0;
303
-
304
- await adapter.process({ queue: "retry-queue", concurrency: 1, retries: 3, priority: 0 }, async (job) => {
305
- attemptCount++;
306
- if (attemptCount < 2) {
307
- await job.fail(new Error("temporary failure"), true);
308
- } else {
309
- await job.complete();
310
- }
311
- });
312
-
313
- await adapter.addJob("retry-queue", { data: "retry-me" }, { retries: 3 });
314
-
315
- // Wait long enough for retry backoff + processing
316
- await new Promise((resolve) => setTimeout(resolve, 3000));
317
-
318
- expect(attemptCount).toBeGreaterThanOrEqual(2);
319
- }, 5000);
320
-
321
- it("should stop processing a queue", async () => {
322
- await adapter.connect();
323
-
324
- const processed: string[] = [];
325
-
326
- await adapter.process({ queue: "stop-queue", concurrency: 1, retries: 3, priority: 0 }, async (job) => {
327
- processed.push(job.id);
328
- await job.complete();
329
- });
330
-
331
- await adapter.addJob("stop-queue", { first: true });
332
- await new Promise((resolve) => setTimeout(resolve, 200));
333
-
334
- await adapter.stopProcessing("stop-queue");
335
-
336
- await adapter.addJob("stop-queue", { second: true });
337
- await new Promise((resolve) => setTimeout(resolve, 200));
338
-
339
- // Only first job should have been processed
340
- expect(processed).toHaveLength(1);
341
- });
342
-
343
- it("should throw when processing without connection", async () => {
344
- await expect(
345
- adapter.process({ queue: "q", concurrency: 1, retries: 0, priority: 0 }, async () => {}),
346
- ).rejects.toThrow("Not connected");
347
- });
348
- });
349
-
350
- describe("Queue Stats", () => {
351
- it("should return zeros for unknown queue", async () => {
352
- await adapter.connect();
353
- const stats = await adapter.getQueueStats("nonexistent");
354
- expect(stats).toEqual({
355
- waiting: 0,
356
- active: 0,
357
- completed: 0,
358
- failed: 0,
359
- delayed: 0,
360
- });
361
- });
362
-
363
- it("should track waiting count", async () => {
364
- await adapter.connect();
365
- await adapter.addJob("count-queue", { a: 1 });
366
- await adapter.addJob("count-queue", { a: 2 });
367
- await adapter.addJob("count-queue", { a: 3 });
368
-
369
- const stats = await adapter.getQueueStats("count-queue");
370
- expect(stats.waiting).toBe(3);
371
- });
372
- });
373
- });
374
-
375
- // ============================================================================
376
- // BullMQAdapter Config Tests
377
- // ============================================================================
378
-
379
- describe("BullMQAdapter", () => {
380
- it("should read config from environment variables", () => {
381
- const originalHost = process.env.REDIS_HOST;
382
- const originalPort = process.env.REDIS_PORT;
383
- const originalPassword = process.env.REDIS_PASSWORD;
384
- const originalDb = process.env.REDIS_DB;
385
-
386
- process.env.REDIS_HOST = "redis.example.com";
387
- process.env.REDIS_PORT = "6380";
388
- process.env.REDIS_PASSWORD = "secret123";
389
- process.env.REDIS_DB = "2";
390
-
391
- const config = {
392
- host: process.env.REDIS_HOST || "localhost",
393
- port: Number.parseInt(process.env.REDIS_PORT || "6379", 10),
394
- password: process.env.REDIS_PASSWORD,
395
- db: Number.parseInt(process.env.REDIS_DB || "0", 10),
396
- };
397
-
398
- expect(config.host).toBe("redis.example.com");
399
- expect(config.port).toBe(6380);
400
- expect(config.password).toBe("secret123");
401
- expect(config.db).toBe(2);
402
-
403
- // Restore
404
- process.env.REDIS_HOST = originalHost;
405
- process.env.REDIS_PORT = originalPort;
406
- process.env.REDIS_PASSWORD = originalPassword;
407
- process.env.REDIS_DB = originalDb;
408
- });
409
-
410
- it("should use default values when env vars not set", () => {
411
- // Pure: don't mutate process.env (races with other parallel workers
412
- // when running via `nx run-many -t test` and pollutes other tests).
413
- // Simulate "env unset" by reading from an explicit snapshot rather
414
- // than the live process.env.
415
- const fakeEnv: Record<string, string | undefined> = {
416
- REDIS_HOST: undefined,
417
- REDIS_PORT: undefined,
418
- };
419
-
420
- const config = {
421
- host: fakeEnv.REDIS_HOST || "localhost",
422
- port: Number.parseInt(fakeEnv.REDIS_PORT || "6379", 10),
423
- };
424
-
425
- expect(config.host).toBe("localhost");
426
- expect(config.port).toBe(6379);
427
- });
428
- });
429
-
430
- // ============================================================================
431
- // WorkerTriggerOpts Schema Tests
432
- // ============================================================================
433
-
434
- describe("WorkerTriggerOpts Schema", () => {
435
- it("should validate worker trigger configuration", () => {
436
- const validConfig = {
437
- queue: "background-jobs",
438
- concurrency: 5,
439
- timeout: 30000,
440
- retries: 3,
441
- priority: 10,
442
- delay: 1000,
443
- };
444
-
445
- expect(validConfig.queue).toBe("background-jobs");
446
- expect(validConfig.concurrency).toBe(5);
447
- expect(validConfig.timeout).toBe(30000);
448
- expect(validConfig.retries).toBe(3);
449
- expect(validConfig.priority).toBe(10);
450
- expect(validConfig.delay).toBe(1000);
451
- });
452
-
453
- it("should support minimal configuration", () => {
454
- const minConfig = {
455
- queue: "default",
456
- };
457
-
458
- expect(minConfig.queue).toBe("default");
459
- });
460
-
461
- it("should support high-concurrency configuration", () => {
462
- const config = {
463
- queue: "high-throughput",
464
- concurrency: 50,
465
- retries: 5,
466
- timeout: 60000,
467
- priority: 0,
468
- };
469
-
470
- expect(config.concurrency).toBe(50);
471
- expect(config.retries).toBe(5);
472
- });
473
- });
474
-
475
- // ============================================================================
476
- // Exponential Backoff Tests
477
- // ============================================================================
478
-
479
- describe("Exponential Backoff", () => {
480
- it("should calculate increasing delays", () => {
481
- const base = 1000;
482
- const maxDelay = 30000;
483
-
484
- const delays = [0, 1, 2, 3, 4, 5].map((attempt) => {
485
- const exponential = Math.min(base * 2 ** attempt, maxDelay);
486
- return exponential;
487
- });
488
-
489
- expect(delays[0]).toBe(1000); // 1s
490
- expect(delays[1]).toBe(2000); // 2s
491
- expect(delays[2]).toBe(4000); // 4s
492
- expect(delays[3]).toBe(8000); // 8s
493
- expect(delays[4]).toBe(16000); // 16s
494
- expect(delays[5]).toBe(30000); // capped at 30s
495
- });
496
-
497
- it("should cap at maximum delay", () => {
498
- const base = 1000;
499
- const maxDelay = 30000;
500
-
501
- const delay = Math.min(base * 2 ** 10, maxDelay);
502
- expect(delay).toBe(30000);
503
- });
504
-
505
- it("should support custom base delay", () => {
506
- const base = 500;
507
- const exponential = base * 2 ** 2;
508
- expect(exponential).toBe(2000);
509
- });
510
- });
511
-
512
- // ============================================================================
513
- // NATSAdapter — computeXDelayHoldMs (Tier 2 polish: x-delay enforcement)
514
- // ============================================================================
515
-
516
- describe("NATSAdapter — computeXDelayHoldMs", () => {
517
- it("returns 0 when no delay was set", () => {
518
- expect(computeXDelayHoldMs(0, 1_000_000, 1_000_000)).toBe(0);
519
- expect(computeXDelayHoldMs(-50, 1_000_000, 1_000_000)).toBe(0);
520
- });
521
-
522
- it("returns the full delay when the message just arrived", () => {
523
- // createdMs == nowMs (just published), delay 5s → wait 5s.
524
- expect(computeXDelayHoldMs(5000, 2_000_000, 2_000_000)).toBe(5000);
525
- });
526
-
527
- it("returns the remaining delay when partially elapsed", () => {
528
- // Published 2s ago, delay 5s → wait 3s remaining.
529
- expect(computeXDelayHoldMs(5000, 1_000_000, 1_002_000)).toBe(3000);
530
- });
531
-
532
- it("returns 0 when the delay has already elapsed", () => {
533
- // Published 10s ago, delay 5s → fire immediately.
534
- expect(computeXDelayHoldMs(5000, 1_000_000, 1_010_000)).toBe(0);
535
- });
536
-
537
- it("clamps to 0 when nowMs is far in the future", () => {
538
- expect(computeXDelayHoldMs(5000, 1_000_000, 9_999_999)).toBe(0);
539
- });
540
- });