@blokjs/trigger-worker 0.2.1 → 0.6.1
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/__tests__/integration/nats-adapter.real-nats.test.ts +116 -0
- package/__tests__/integration/pgboss-adapter.real-pg.test.ts +164 -0
- package/__tests__/integration/rabbitmq-adapter.real-rabbitmq.test.ts +179 -0
- package/__tests__/integration/sqs-adapter.real-sqs.test.ts +228 -0
- package/dist/WorkerTrigger.d.ts +40 -4
- package/dist/WorkerTrigger.js +272 -40
- package/dist/adapters/BullMQAdapter.d.ts +1 -1
- package/dist/adapters/BullMQAdapter.js +5 -42
- package/dist/adapters/InMemoryAdapter.d.ts +1 -1
- package/dist/adapters/InMemoryAdapter.js +13 -12
- package/dist/adapters/KafkaAdapter.d.ts +62 -0
- package/dist/adapters/KafkaAdapter.js +236 -0
- package/dist/adapters/NATSAdapter.d.ts +110 -0
- package/dist/adapters/NATSAdapter.js +394 -0
- package/dist/adapters/PgBossAdapter.d.ts +56 -0
- package/dist/adapters/PgBossAdapter.js +251 -0
- package/dist/adapters/RabbitMQAdapter.d.ts +51 -0
- package/dist/adapters/RabbitMQAdapter.js +241 -0
- package/dist/adapters/RedisStreamsAdapter.d.ts +64 -0
- package/dist/adapters/RedisStreamsAdapter.js +240 -0
- package/dist/adapters/SQSAdapter.d.ts +61 -0
- package/dist/adapters/SQSAdapter.js +269 -0
- package/dist/adapters/factory.d.ts +34 -0
- package/dist/adapters/factory.js +103 -0
- package/dist/index.d.ts +25 -7
- package/dist/index.js +31 -16
- package/package.json +27 -5
- package/src/WorkerTrigger.test.ts +44 -14
- package/src/WorkerTrigger.ts +299 -27
- package/src/adapters/InMemoryAdapter.ts +9 -5
- package/src/adapters/KafkaAdapter.ts +277 -0
- package/src/adapters/NATSAdapter.ts +454 -0
- package/src/adapters/PgBossAdapter.ts +293 -0
- package/src/adapters/RabbitMQAdapter.ts +285 -0
- package/src/adapters/RedisStreamsAdapter.ts +286 -0
- package/src/adapters/SQSAdapter.ts +306 -0
- package/src/adapters/factory.test.ts +89 -0
- package/src/adapters/factory.ts +111 -0
- package/src/adapters/new-adapters.test.ts +130 -0
- package/src/index.ts +31 -4
- package/template/.env.example +13 -0
- package/template/package.json +45 -0
- package/template/src/Nodes.ts +10 -0
- package/template/src/Workflows.ts +8 -0
- package/template/src/index.ts +41 -0
- package/template/src/runner/WorkerServer.ts +34 -0
- package/template/src/runner/types/Workflows.ts +7 -0
- package/template/src/workflows/jobs/process-job.ts +47 -0
- package/template/tsconfig.json +31 -0
- package/template/vitest.config.ts +39 -0
|
@@ -0,0 +1,454 @@
|
|
|
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;
|