@blokjs/trigger-worker 0.2.1 → 0.4.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.
@@ -0,0 +1,452 @@
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(info.timestampNanos ? Number(info.timestampNanos / BigInt(1_000_000)) : Date.now()),
236
+ delay: delay || undefined,
237
+ timeout: timeout || config.timeout || undefined,
238
+ raw: msg,
239
+ complete: async () => {
240
+ msg.ack();
241
+ },
242
+ fail: async (error: Error, requeue?: boolean) => {
243
+ if (requeue) {
244
+ // nak() tells the server to redeliver
245
+ msg.nak();
246
+ } else {
247
+ // term() terminates delivery — no more retries
248
+ msg.term();
249
+ }
250
+ },
251
+ };
252
+
253
+ // Tier 2 polish — enforce `x-delay` header on the consumer side.
254
+ // NATS JetStream stores `x-delay` as opaque metadata; the broker
255
+ // does NOT defer delivery on it. We implement consumer-side
256
+ // holding here. createdMs is the message's first-publish timestamp;
257
+ // hold until createdMs + delay. Single-process semantics — for
258
+ // long deferrals, prefer trigger-level `delay` (DeferredRunScheduler).
259
+ const createdMs = info.timestampNanos ? Number(info.timestampNanos / BigInt(1_000_000)) : Date.now();
260
+ const waitMs = computeXDelayHoldMs(delay, createdMs, Date.now());
261
+ if (waitMs > 0) {
262
+ await new Promise<void>((resolve) => setTimeout(resolve, waitMs));
263
+ }
264
+
265
+ await handler(workerJob);
266
+ } catch (error) {
267
+ console.error(`[NATSWorkerAdapter] Error processing job from ${queue}: ${(error as Error).message}`);
268
+ try {
269
+ msg.nak();
270
+ } catch {
271
+ // Already acked/nacked
272
+ }
273
+ } finally {
274
+ semaphore.release();
275
+ }
276
+ })();
277
+ }
278
+ })();
279
+
280
+ console.log(
281
+ `[NATSWorkerAdapter] Processing queue: ${queue} (concurrency=${config.concurrency ?? 1}, retries=${config.retries ?? 3})`,
282
+ );
283
+ }
284
+
285
+ /**
286
+ * Add a job to a worker queue
287
+ */
288
+ async addJob(
289
+ queue: string,
290
+ data: unknown,
291
+ opts?: {
292
+ priority?: number;
293
+ delay?: number;
294
+ retries?: number;
295
+ timeout?: number;
296
+ jobId?: string;
297
+ },
298
+ ): Promise<string> {
299
+ if (!this.connected) {
300
+ throw new Error("Not connected. Call connect() first.");
301
+ }
302
+
303
+ const nats = await import("nats");
304
+ const subject = `worker.${queue}`;
305
+ const streamName = this.config.streamName || "blok-worker";
306
+
307
+ // Ensure stream has this subject
308
+ await this.ensureStream(streamName, [subject]);
309
+
310
+ // Build headers with job metadata
311
+ const hdrs = nats.headers();
312
+ const jobId = opts?.jobId || uuid();
313
+ hdrs.set("x-job-id", jobId);
314
+ hdrs.set("Nats-Msg-Id", jobId); // Deduplication
315
+ if (opts?.priority) hdrs.set("x-priority", String(opts.priority));
316
+ if (opts?.delay) hdrs.set("x-delay", String(opts.delay));
317
+ if (opts?.timeout) hdrs.set("x-timeout", String(opts.timeout));
318
+
319
+ // Encode and publish
320
+ const codec = nats.JSONCodec();
321
+ await this.js.publish(subject, codec.encode(data), { headers: hdrs });
322
+
323
+ return jobId;
324
+ }
325
+
326
+ /**
327
+ * Stop processing a specific queue
328
+ */
329
+ async stopProcessing(queue: string): Promise<void> {
330
+ const iter = this.consumeIterators.get(queue);
331
+ if (iter) {
332
+ try {
333
+ iter.stop();
334
+ } catch {
335
+ // Already stopped
336
+ }
337
+ this.consumeIterators.delete(queue);
338
+ }
339
+ this.consumers.delete(queue);
340
+ console.log(`[NATSWorkerAdapter] Stopped processing queue: ${queue}`);
341
+ }
342
+
343
+ /**
344
+ * Check if connected
345
+ */
346
+ isConnected(): boolean {
347
+ return this.connected;
348
+ }
349
+
350
+ /**
351
+ * Health check
352
+ */
353
+ async healthCheck(): Promise<boolean> {
354
+ if (!this.connected || !this.nc) return false;
355
+ try {
356
+ const info = this.nc.info;
357
+ return info !== undefined;
358
+ } catch {
359
+ return false;
360
+ }
361
+ }
362
+
363
+ /**
364
+ * Get queue statistics from JetStream consumer info
365
+ */
366
+ async getQueueStats(queue: string): Promise<WorkerQueueStats> {
367
+ if (!this.connected) {
368
+ return { waiting: 0, active: 0, completed: 0, failed: 0, delayed: 0 };
369
+ }
370
+
371
+ try {
372
+ const streamName = this.config.streamName || "blok-worker";
373
+ const durableName = `blok-worker-${queue}`;
374
+
375
+ const info = await this.jsm.consumers.info(streamName, durableName);
376
+
377
+ return {
378
+ waiting: info.num_pending ?? 0,
379
+ active: info.num_ack_pending ?? 0,
380
+ completed: info.delivered?.consumer_seq ?? 0,
381
+ failed: info.num_redelivered ?? 0,
382
+ delayed: 0, // NATS doesn't have a native delayed count
383
+ };
384
+ } catch {
385
+ return { waiting: 0, active: 0, completed: 0, failed: 0, delayed: 0 };
386
+ }
387
+ }
388
+
389
+ /**
390
+ * Ensure a JetStream stream exists with the given subjects
391
+ */
392
+ private async ensureStream(name: string, subjects: string[]): Promise<void> {
393
+ try {
394
+ const info = await this.jsm.streams.info(name);
395
+
396
+ // Merge new subjects with existing
397
+ const existingSubjects = info.config.subjects || [];
398
+ const allSubjects = [...new Set([...existingSubjects, ...subjects])];
399
+
400
+ if (allSubjects.length !== existingSubjects.length) {
401
+ await this.jsm.streams.update(name, {
402
+ ...info.config,
403
+ subjects: allSubjects,
404
+ });
405
+ }
406
+ } catch {
407
+ // Stream doesn't exist, create it
408
+ await this.jsm.streams.add({
409
+ name,
410
+ subjects,
411
+ // biome-ignore lint/suspicious/noExplicitAny: nats JetStream retention policy enum
412
+ retention: "workqueue" as any,
413
+ max_deliver: 4, // default: 3 retries + 1 initial attempt
414
+ // biome-ignore lint/suspicious/noExplicitAny: nats JetStream storage type enum
415
+ storage: "file" as any,
416
+ });
417
+ }
418
+ }
419
+ }
420
+
421
+ /**
422
+ * Simple semaphore for concurrency control
423
+ */
424
+ class Semaphore {
425
+ private permits: number;
426
+ private waiting: Array<() => void> = [];
427
+
428
+ constructor(permits: number) {
429
+ this.permits = permits;
430
+ }
431
+
432
+ async acquire(): Promise<void> {
433
+ if (this.permits > 0) {
434
+ this.permits--;
435
+ return;
436
+ }
437
+ return new Promise<void>((resolve) => {
438
+ this.waiting.push(resolve);
439
+ });
440
+ }
441
+
442
+ release(): void {
443
+ const next = this.waiting.shift();
444
+ if (next) {
445
+ next();
446
+ } else {
447
+ this.permits++;
448
+ }
449
+ }
450
+ }
451
+
452
+ export default NATSWorkerAdapter;
package/src/index.ts CHANGED
@@ -62,6 +62,7 @@ export {
62
62
  // Adapters
63
63
  export { BullMQAdapter, type BullMQConfig } from "./adapters/BullMQAdapter";
64
64
  export { InMemoryAdapter } from "./adapters/InMemoryAdapter";
65
+ export { NATSWorkerAdapter, type NATSWorkerConfig } from "./adapters/NATSAdapter";
65
66
 
66
67
  // Re-export types from helper for convenience
67
68
  export type { WorkerTriggerOpts } from "@blokjs/helper";
@@ -0,0 +1,13 @@
1
+ PROJECT_NAME=trigger-worker-server
2
+ PROJECT_VERSION=0.0.1
3
+ PORT=4008
4
+ WORKFLOWS_PATH=PROJECT_PATH/workflows
5
+ NODES_PATH=PROJECT_PATH/src/nodes
6
+ CONSOLE_LOG_ACTIVE=true
7
+ APP_NAME=blok-worker
8
+ DISABLE_TRIGGER_RUN=false # Set to true to disable trigger run and use this project as a module
9
+
10
+ # NATS JetStream Configuration
11
+ NATS_SERVERS=localhost:4222
12
+ NATS_STREAM_NAME=blok-worker
13
+ # NATS_TOKEN=your-auth-token
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "blok-worker-trigger",
3
+ "version": "0.1.0",
4
+ "description": "Worker trigger for Blok background job workflows",
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=18.0.0"
8
+ },
9
+ "main": "dist/index.js",
10
+ "types": "dist/index.d.ts",
11
+ "author": "",
12
+ "license": "MIT",
13
+ "scripts": {
14
+ "dev": "bun --watch run src/index.ts",
15
+ "start": "bun run dist/index.js",
16
+ "build": "rimraf ./dist && tsc",
17
+ "test": "vitest run",
18
+ "test:dev": "vitest"
19
+ },
20
+ "devDependencies": {
21
+ "@types/node": "^22.15.21",
22
+ "@types/uuid": "^11.0.0",
23
+ "rimraf": "^6.1.2",
24
+ "typescript": "^5.8.3",
25
+ "vitest": "^4.0.18"
26
+ },
27
+ "dependencies": {
28
+ "@blokjs/api-call": "^0.4.0",
29
+ "@blokjs/helper": "^0.4.0",
30
+ "@blokjs/if-else": "^0.4.0",
31
+ "@blokjs/runner": "^0.4.0",
32
+ "@blokjs/shared": "^0.4.0",
33
+ "@blokjs/trigger-worker": "^0.4.0",
34
+ "@opentelemetry/api": "^1.9.0",
35
+ "@opentelemetry/exporter-prometheus": "^0.57.2",
36
+ "@opentelemetry/resources": "^1.30.1",
37
+ "@opentelemetry/sdk-metrics": "^1.30.1",
38
+ "@opentelemetry/sdk-trace-base": "^1.30.1",
39
+ "@opentelemetry/semantic-conventions": "^1.39.0",
40
+ "nats": "^2.28.0",
41
+ "uuid": "^11.1.0",
42
+ "zod": "^3.24.2"
43
+ },
44
+ "private": true
45
+ }
@@ -0,0 +1,10 @@
1
+ import ApiCall from "@blokjs/api-call";
2
+ import IfElse from "@blokjs/if-else";
3
+ import type { BlokService } from "@blokjs/runner";
4
+
5
+ const nodes: Record<string, BlokService<unknown>> = {
6
+ "@blokjs/api-call": ApiCall,
7
+ "@blokjs/if-else": IfElse,
8
+ };
9
+
10
+ export default nodes;
@@ -0,0 +1,8 @@
1
+ import type Workflows from "./runner/types/Workflows";
2
+ import processJob from "./workflows/jobs/process-job";
3
+
4
+ const workflows: Workflows = {
5
+ "process-job": processJob,
6
+ };
7
+
8
+ export default workflows;
@@ -0,0 +1,41 @@
1
+ import { DefaultLogger } from "@blokjs/runner";
2
+ import { type Span, metrics, trace } from "@opentelemetry/api";
3
+ import WorkerServer from "./runner/WorkerServer";
4
+
5
+ export default class App {
6
+ private workerServer: WorkerServer = <WorkerServer>{};
7
+ protected trigger_initializer = 0;
8
+ protected initializer = 0;
9
+ protected tracer = trace.getTracer(
10
+ process.env.PROJECT_NAME || "trigger-worker-server",
11
+ process.env.PROJECT_VERSION || "0.0.1",
12
+ );
13
+ private logger = new DefaultLogger();
14
+ protected app_cold_start = metrics.getMeter("default").createGauge("initialization", {
15
+ description: "Application cold start",
16
+ });
17
+
18
+ constructor() {
19
+ this.initializer = performance.now();
20
+ this.workerServer = new WorkerServer();
21
+ }
22
+
23
+ async run() {
24
+ this.tracer.startActiveSpan("initialization", async (span: Span) => {
25
+ await this.workerServer.listen();
26
+ this.initializer = performance.now() - this.initializer;
27
+
28
+ this.logger.log(`Worker trigger initialized in ${this.initializer.toFixed(2)}ms`);
29
+ this.app_cold_start.record(this.initializer, {
30
+ pid: process.pid,
31
+ env: process.env.NODE_ENV,
32
+ app: process.env.APP_NAME,
33
+ });
34
+ span.end();
35
+ });
36
+ }
37
+ }
38
+
39
+ if (process.env.DISABLE_TRIGGER_RUN !== "true") {
40
+ new App().run();
41
+ }
@@ -0,0 +1,34 @@
1
+ import { NATSWorkerAdapter, WorkerTrigger } from "@blokjs/trigger-worker";
2
+ import nodes from "../Nodes";
3
+ import workflows from "../Workflows";
4
+
5
+ /**
6
+ * WorkerServer - Concrete Worker trigger implementation using NATS JetStream
7
+ *
8
+ * This server extends the abstract WorkerTrigger and provides:
9
+ * - NATS JetStream adapter for persistent job queues
10
+ * - Node and workflow registries
11
+ * - Configurable concurrency, retries, and timeouts
12
+ *
13
+ * Environment variables:
14
+ * - NATS_SERVERS: Comma-separated NATS server URLs (default: localhost:4222)
15
+ * - NATS_STREAM_NAME: JetStream stream name (default: blok-worker)
16
+ * - NATS_TOKEN: Authentication token (optional)
17
+ *
18
+ * @example BullMQ (Redis) alternative
19
+ * ```typescript
20
+ * import { BullMQAdapter } from "@blokjs/trigger-worker";
21
+ * protected adapter = new BullMQAdapter({
22
+ * host: process.env.REDIS_HOST || "localhost",
23
+ * port: Number(process.env.REDIS_PORT) || 6379,
24
+ * });
25
+ * ```
26
+ */
27
+ export default class WorkerServer extends WorkerTrigger {
28
+ protected adapter = new NATSWorkerAdapter({
29
+ servers: (process.env.NATS_SERVERS || "localhost:4222").split(","),
30
+ });
31
+
32
+ protected nodes: Record<string, import("@blokjs/runner").BlokService<unknown>> = nodes;
33
+ protected workflows: Record<string, import("@blokjs/helper").HelperResponse> = workflows;
34
+ }
@@ -0,0 +1,7 @@
1
+ import type { HelperResponse } from "@blokjs/helper";
2
+
3
+ type Workflows = {
4
+ [key: string]: HelperResponse;
5
+ };
6
+
7
+ export default Workflows;
@@ -0,0 +1,45 @@
1
+ import { type Step, Workflow } from "@blokjs/helper";
2
+
3
+ /**
4
+ * Example Worker workflow - triggered when a job is received from the queue
5
+ *
6
+ * The job data is available in ctx.request:
7
+ * - ctx.request.body: The job payload
8
+ * - ctx.request.headers: Job headers/metadata
9
+ * - ctx.request.params.queue: The queue name
10
+ * - ctx.request.params.jobId: Unique job ID
11
+ * - ctx.request.params.attempt: Current attempt number (0-based)
12
+ *
13
+ * Additional metadata is available in ctx.vars._worker_job:
14
+ * - id: Unique job ID
15
+ * - queue: Queue name
16
+ * - attempts: Current attempt number
17
+ * - maxRetries: Maximum retry count
18
+ * - priority: Job priority (if set)
19
+ * - createdAt: When the job was created (ISO string)
20
+ */
21
+ const step: Step = Workflow({
22
+ name: "Process Background Job",
23
+ version: "1.0.0",
24
+ description: "Handles incoming worker jobs from the queue",
25
+ })
26
+ .addTrigger("worker", {
27
+ queue: "background-jobs",
28
+ })
29
+ .addStep({
30
+ name: "process-job",
31
+ node: "@blokjs/api-call",
32
+ type: "module",
33
+ inputs: {
34
+ url: "https://httpbin.org/post",
35
+ method: "POST",
36
+ body: {
37
+ job: "js/ctx.request.body",
38
+ queue: "js/ctx.request.params.queue",
39
+ jobId: "js/ctx.request.params.jobId",
40
+ attempt: "js/ctx.request.params.attempt",
41
+ },
42
+ },
43
+ });
44
+
45
+ export default step;