@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,454 +0,0 @@
1
- /**
2
- * NATSAdapter - NATS JetStream worker adapter for WorkerTrigger
3
- *
4
- * Uses NATS JetStream for persistent background job processing with:
5
- * - Pull-based consumers with configurable concurrency
6
- * - Server-side retry config (max_deliver)
7
- * - Ack wait for job timeouts
8
- * - Priority via message headers
9
- * - Delayed job scheduling
10
- * - Queue statistics via consumer info
11
- *
12
- * Requires: npm install nats
13
- *
14
- * Environment variables:
15
- * - NATS_SERVERS: Comma-separated NATS server URLs (default: localhost:4222)
16
- * - NATS_TOKEN: Authentication token (optional)
17
- * - NATS_USER: Username for auth (optional)
18
- * - NATS_PASS: Password for auth (optional)
19
- * - NATS_STREAM_NAME: JetStream stream name (default: blok-worker)
20
- */
21
-
22
- import type { WorkerTriggerOpts } from "@blokjs/helper";
23
- import { v4 as uuid } from "uuid";
24
- import type { WorkerAdapter, WorkerJob, WorkerQueueStats } from "../WorkerTrigger";
25
-
26
- /**
27
- * Tier 2 polish — compute the consumer-side hold time for a NATS message
28
- * with an `x-delay` header. NATS JetStream stores `x-delay` as opaque
29
- * metadata; the broker does NOT defer delivery on it. The consumer is
30
- * responsible for honouring the delay between the message's first-publish
31
- * timestamp and `createdMs + delay`.
32
- *
33
- * Returns the milliseconds to wait. Clamps to >= 0; returns 0 when the
34
- * delay has already elapsed (the message was queued for longer than the
35
- * delay) or when no delay was set.
36
- *
37
- * Exported for unit testability — the consumer message handler in
38
- * `NATSWorkerAdapter.process()` mocks the NATS client extensively, so
39
- * isolating the math here keeps the surface easy to verify.
40
- */
41
- export function computeXDelayHoldMs(delay: number, createdMs: number, nowMs: number): number {
42
- if (!delay || delay <= 0) return 0;
43
- const dispatchAt = createdMs + delay;
44
- return Math.max(0, dispatchAt - nowMs);
45
- }
46
-
47
- /**
48
- * NATS worker adapter configuration
49
- */
50
- export interface NATSWorkerConfig {
51
- /** NATS server URLs */
52
- servers: string[];
53
- /** Authentication token */
54
- token?: string;
55
- /** Username */
56
- user?: string;
57
- /** Password */
58
- pass?: string;
59
- /** JetStream stream name (default: "blok-worker") */
60
- streamName?: string;
61
- }
62
-
63
- /**
64
- * NATSWorkerAdapter - NATS JetStream implementation of WorkerAdapter
65
- */
66
- export class NATSWorkerAdapter implements WorkerAdapter {
67
- readonly provider = "nats" as const;
68
-
69
- // biome-ignore lint/suspicious/noExplicitAny: NATS types are dynamically imported (optional peer dependency)
70
- private nc: any = null;
71
- // biome-ignore lint/suspicious/noExplicitAny: NATS types are dynamically imported
72
- private js: any = null;
73
- // biome-ignore lint/suspicious/noExplicitAny: NATS types are dynamically imported
74
- private jsm: any = null;
75
- private connected = false;
76
- private config: NATSWorkerConfig;
77
- // biome-ignore lint/suspicious/noExplicitAny: NATS consumer instances
78
- private consumers: Map<string, any> = new Map();
79
- // biome-ignore lint/suspicious/noExplicitAny: NATS consume iterators
80
- private consumeIterators: Map<string, any> = new Map();
81
-
82
- constructor(config?: Partial<NATSWorkerConfig>) {
83
- this.config = {
84
- servers: config?.servers || (process.env.NATS_SERVERS || "localhost:4222").split(","),
85
- token: config?.token || process.env.NATS_TOKEN,
86
- user: config?.user || process.env.NATS_USER,
87
- pass: config?.pass || process.env.NATS_PASS,
88
- streamName: config?.streamName || process.env.NATS_STREAM_NAME || "blok-worker",
89
- };
90
- }
91
-
92
- /**
93
- * Connect to NATS and initialize JetStream
94
- */
95
- async connect(): Promise<void> {
96
- if (this.connected) return;
97
-
98
- try {
99
- const nats = await import("nats");
100
-
101
- const connectOpts: Record<string, unknown> = {
102
- servers: this.config.servers,
103
- };
104
-
105
- if (this.config.token) connectOpts.token = this.config.token;
106
- if (this.config.user) connectOpts.user = this.config.user;
107
- if (this.config.pass) connectOpts.pass = this.config.pass;
108
-
109
- this.nc = await nats.connect(connectOpts);
110
- this.js = this.nc.jetstream();
111
- this.jsm = await this.nc.jetstreamManager();
112
-
113
- this.connected = true;
114
- console.log(`[NATSWorkerAdapter] Connected to NATS: ${this.config.servers.join(", ")}`);
115
- } catch (error) {
116
- throw new Error(
117
- `Failed to connect to NATS: ${(error as Error).message}. Make sure nats is installed: npm install nats`,
118
- );
119
- }
120
- }
121
-
122
- /**
123
- * Disconnect from NATS
124
- */
125
- async disconnect(): Promise<void> {
126
- if (!this.connected) return;
127
-
128
- try {
129
- // Stop all consume iterators
130
- for (const [, iter] of this.consumeIterators) {
131
- try {
132
- iter.stop();
133
- } catch {
134
- // Iterator may already be stopped
135
- }
136
- }
137
- this.consumeIterators.clear();
138
- this.consumers.clear();
139
-
140
- await this.nc.drain();
141
- this.connected = false;
142
- console.log("[NATSWorkerAdapter] Disconnected from NATS");
143
- } catch (error) {
144
- console.error(`[NATSWorkerAdapter] Disconnect error: ${(error as Error).message}`);
145
- }
146
- }
147
-
148
- /**
149
- * Start processing jobs from a queue
150
- */
151
- async process(config: WorkerTriggerOpts, handler: (job: WorkerJob) => Promise<void>): Promise<void> {
152
- if (!this.connected) {
153
- throw new Error("Not connected. Call connect() first.");
154
- }
155
-
156
- const nats = await import("nats");
157
- const queue = config.queue;
158
- const streamName = this.config.streamName || "blok-worker";
159
- const subject = `worker.${queue}`;
160
- const durableName = `blok-worker-${queue}`;
161
-
162
- // Ensure stream exists with worker subjects
163
- await this.ensureStream(streamName, [subject]);
164
-
165
- // Create or update durable pull consumer with worker semantics
166
- const ackWaitNs = ((config.timeout ?? 30000) + 5000) * 1_000_000; // timeout + 5s buffer, in nanoseconds
167
- await this.jsm.consumers.add(streamName, {
168
- durable_name: durableName,
169
- ack_policy: nats.AckPolicy.Explicit,
170
- max_deliver: (config.retries ?? 3) + 1, // +1 because first attempt counts
171
- ack_wait: ackWaitNs,
172
- filter_subjects: [subject],
173
- });
174
-
175
- // Get consumer handle
176
- const consumer = await this.js.consumers.get(streamName, durableName);
177
- this.consumers.set(queue, consumer);
178
-
179
- // Start consuming
180
- const iter = await consumer.consume();
181
- this.consumeIterators.set(queue, iter);
182
-
183
- // Process jobs in background
184
- (async () => {
185
- const semaphore = new Semaphore(config.concurrency ?? 1);
186
-
187
- for await (const msg of iter) {
188
- await semaphore.acquire();
189
-
190
- // Process each job concurrently up to concurrency limit
191
- (async () => {
192
- try {
193
- // Parse job data
194
- let data: unknown;
195
- try {
196
- const codec = nats.JSONCodec();
197
- data = codec.decode(msg.data);
198
- } catch {
199
- try {
200
- const sc = nats.StringCodec();
201
- data = JSON.parse(sc.decode(msg.data));
202
- } catch {
203
- data = msg.data;
204
- }
205
- }
206
-
207
- // Extract headers
208
- const headers: Record<string, string> = {};
209
- if (msg.headers) {
210
- for (const [key, values] of msg.headers) {
211
- headers[key] = Array.isArray(values) ? values[0] : values;
212
- }
213
- }
214
-
215
- // Extract job metadata from headers
216
- const jobId = headers["x-job-id"] || msg.headers?.get("Nats-Msg-Id") || uuid();
217
- const priority = Number.parseInt(headers["x-priority"] || "0", 10);
218
- const delay = Number.parseInt(headers["x-delay"] || "0", 10);
219
- const timeout = Number.parseInt(headers["x-timeout"] || "0", 10);
220
-
221
- // Get redelivery count (attempts)
222
- const info = msg.info;
223
- const attempts = info.redeliveryCount ?? 0;
224
- const maxRetries = config.retries ?? 3;
225
-
226
- // Create WorkerJob
227
- const workerJob: WorkerJob = {
228
- id: jobId,
229
- data,
230
- headers,
231
- queue,
232
- priority,
233
- attempts,
234
- maxRetries,
235
- createdAt: new Date(
236
- info.timestampNanos ? Math.floor(Number(info.timestampNanos) / 1_000_000) : Date.now(),
237
- ),
238
- delay: delay || undefined,
239
- timeout: timeout || config.timeout || undefined,
240
- raw: msg,
241
- complete: async () => {
242
- msg.ack();
243
- },
244
- fail: async (error: Error, requeue?: boolean) => {
245
- if (requeue) {
246
- // nak() tells the server to redeliver
247
- msg.nak();
248
- } else {
249
- // term() terminates delivery — no more retries
250
- msg.term();
251
- }
252
- },
253
- };
254
-
255
- // Tier 2 polish — enforce `x-delay` header on the consumer side.
256
- // NATS JetStream stores `x-delay` as opaque metadata; the broker
257
- // does NOT defer delivery on it. We implement consumer-side
258
- // holding here. createdMs is the message's first-publish timestamp;
259
- // hold until createdMs + delay. Single-process semantics — for
260
- // long deferrals, prefer trigger-level `delay` (DeferredRunScheduler).
261
- const createdMs = info.timestampNanos ? Math.floor(Number(info.timestampNanos) / 1_000_000) : Date.now();
262
- const waitMs = computeXDelayHoldMs(delay, createdMs, Date.now());
263
- if (waitMs > 0) {
264
- await new Promise<void>((resolve) => setTimeout(resolve, waitMs));
265
- }
266
-
267
- await handler(workerJob);
268
- } catch (error) {
269
- console.error(`[NATSWorkerAdapter] Error processing job from ${queue}: ${(error as Error).message}`);
270
- try {
271
- msg.nak();
272
- } catch {
273
- // Already acked/nacked
274
- }
275
- } finally {
276
- semaphore.release();
277
- }
278
- })();
279
- }
280
- })();
281
-
282
- console.log(
283
- `[NATSWorkerAdapter] Processing queue: ${queue} (concurrency=${config.concurrency ?? 1}, retries=${config.retries ?? 3})`,
284
- );
285
- }
286
-
287
- /**
288
- * Add a job to a worker queue
289
- */
290
- async addJob(
291
- queue: string,
292
- data: unknown,
293
- opts?: {
294
- priority?: number;
295
- delay?: number;
296
- retries?: number;
297
- timeout?: number;
298
- jobId?: string;
299
- },
300
- ): Promise<string> {
301
- if (!this.connected) {
302
- throw new Error("Not connected. Call connect() first.");
303
- }
304
-
305
- const nats = await import("nats");
306
- const subject = `worker.${queue}`;
307
- const streamName = this.config.streamName || "blok-worker";
308
-
309
- // Ensure stream has this subject
310
- await this.ensureStream(streamName, [subject]);
311
-
312
- // Build headers with job metadata
313
- const hdrs = nats.headers();
314
- const jobId = opts?.jobId || uuid();
315
- hdrs.set("x-job-id", jobId);
316
- hdrs.set("Nats-Msg-Id", jobId); // Deduplication
317
- if (opts?.priority) hdrs.set("x-priority", String(opts.priority));
318
- if (opts?.delay) hdrs.set("x-delay", String(opts.delay));
319
- if (opts?.timeout) hdrs.set("x-timeout", String(opts.timeout));
320
-
321
- // Encode and publish
322
- const codec = nats.JSONCodec();
323
- await this.js.publish(subject, codec.encode(data), { headers: hdrs });
324
-
325
- return jobId;
326
- }
327
-
328
- /**
329
- * Stop processing a specific queue
330
- */
331
- async stopProcessing(queue: string): Promise<void> {
332
- const iter = this.consumeIterators.get(queue);
333
- if (iter) {
334
- try {
335
- iter.stop();
336
- } catch {
337
- // Already stopped
338
- }
339
- this.consumeIterators.delete(queue);
340
- }
341
- this.consumers.delete(queue);
342
- console.log(`[NATSWorkerAdapter] Stopped processing queue: ${queue}`);
343
- }
344
-
345
- /**
346
- * Check if connected
347
- */
348
- isConnected(): boolean {
349
- return this.connected;
350
- }
351
-
352
- /**
353
- * Health check
354
- */
355
- async healthCheck(): Promise<boolean> {
356
- if (!this.connected || !this.nc) return false;
357
- try {
358
- const info = this.nc.info;
359
- return info !== undefined;
360
- } catch {
361
- return false;
362
- }
363
- }
364
-
365
- /**
366
- * Get queue statistics from JetStream consumer info
367
- */
368
- async getQueueStats(queue: string): Promise<WorkerQueueStats> {
369
- if (!this.connected) {
370
- return { waiting: 0, active: 0, completed: 0, failed: 0, delayed: 0 };
371
- }
372
-
373
- try {
374
- const streamName = this.config.streamName || "blok-worker";
375
- const durableName = `blok-worker-${queue}`;
376
-
377
- const info = await this.jsm.consumers.info(streamName, durableName);
378
-
379
- return {
380
- waiting: info.num_pending ?? 0,
381
- active: info.num_ack_pending ?? 0,
382
- completed: info.delivered?.consumer_seq ?? 0,
383
- failed: info.num_redelivered ?? 0,
384
- delayed: 0, // NATS doesn't have a native delayed count
385
- };
386
- } catch {
387
- return { waiting: 0, active: 0, completed: 0, failed: 0, delayed: 0 };
388
- }
389
- }
390
-
391
- /**
392
- * Ensure a JetStream stream exists with the given subjects
393
- */
394
- private async ensureStream(name: string, subjects: string[]): Promise<void> {
395
- try {
396
- const info = await this.jsm.streams.info(name);
397
-
398
- // Merge new subjects with existing
399
- const existingSubjects = info.config.subjects || [];
400
- const allSubjects = [...new Set([...existingSubjects, ...subjects])];
401
-
402
- if (allSubjects.length !== existingSubjects.length) {
403
- await this.jsm.streams.update(name, {
404
- ...info.config,
405
- subjects: allSubjects,
406
- });
407
- }
408
- } catch {
409
- // Stream doesn't exist, create it
410
- await this.jsm.streams.add({
411
- name,
412
- subjects,
413
- // biome-ignore lint/suspicious/noExplicitAny: nats JetStream retention policy enum
414
- retention: "workqueue" as any,
415
- max_deliver: 4, // default: 3 retries + 1 initial attempt
416
- // biome-ignore lint/suspicious/noExplicitAny: nats JetStream storage type enum
417
- storage: "file" as any,
418
- });
419
- }
420
- }
421
- }
422
-
423
- /**
424
- * Simple semaphore for concurrency control
425
- */
426
- class Semaphore {
427
- private permits: number;
428
- private waiting: Array<() => void> = [];
429
-
430
- constructor(permits: number) {
431
- this.permits = permits;
432
- }
433
-
434
- async acquire(): Promise<void> {
435
- if (this.permits > 0) {
436
- this.permits--;
437
- return;
438
- }
439
- return new Promise<void>((resolve) => {
440
- this.waiting.push(resolve);
441
- });
442
- }
443
-
444
- release(): void {
445
- const next = this.waiting.shift();
446
- if (next) {
447
- next();
448
- } else {
449
- this.permits++;
450
- }
451
- }
452
- }
453
-
454
- export default NATSWorkerAdapter;