@blokjs/trigger-worker 0.6.17 → 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.
- package/dist/WorkerTrigger.d.ts +27 -3
- package/dist/WorkerTrigger.js +168 -26
- package/dist/adapters/KafkaAdapter.d.ts +5 -0
- package/dist/adapters/KafkaAdapter.js +12 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/package.json +5 -4
- package/CHANGELOG.md +0 -22
- package/__tests__/integration/nats-adapter.real-nats.test.ts +0 -116
- package/__tests__/integration/pgboss-adapter.real-pg.test.ts +0 -164
- package/__tests__/integration/rabbitmq-adapter.real-rabbitmq.test.ts +0 -179
- package/__tests__/integration/sqs-adapter.real-sqs.test.ts +0 -228
- package/src/WorkerTrigger.test.ts +0 -540
- package/src/WorkerTrigger.ts +0 -784
- package/src/adapters/BullMQAdapter.ts +0 -296
- package/src/adapters/InMemoryAdapter.ts +0 -280
- package/src/adapters/KafkaAdapter.ts +0 -277
- package/src/adapters/NATSAdapter.ts +0 -454
- package/src/adapters/PgBossAdapter.ts +0 -293
- package/src/adapters/RabbitMQAdapter.ts +0 -285
- package/src/adapters/RedisStreamsAdapter.ts +0 -286
- package/src/adapters/SQSAdapter.ts +0 -306
- package/src/adapters/factory.test.ts +0 -89
- package/src/adapters/factory.ts +0 -111
- package/src/adapters/new-adapters.test.ts +0 -130
- package/src/index.ts +0 -94
- package/template/.env.example +0 -13
- package/template/package.json +0 -45
- package/template/src/Nodes.ts +0 -10
- package/template/src/Workflows.ts +0 -8
- package/template/src/index.ts +0 -41
- package/template/src/runner/WorkerServer.ts +0 -34
- package/template/src/runner/types/Workflows.ts +0 -7
- package/template/src/workflows/jobs/process-job.ts +0 -47
- package/template/tsconfig.json +0 -31
- package/template/vitest.config.ts +0 -39
- 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
|
-
});
|