@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.
Files changed (50) hide show
  1. package/__tests__/integration/nats-adapter.real-nats.test.ts +116 -0
  2. package/__tests__/integration/pgboss-adapter.real-pg.test.ts +164 -0
  3. package/__tests__/integration/rabbitmq-adapter.real-rabbitmq.test.ts +179 -0
  4. package/__tests__/integration/sqs-adapter.real-sqs.test.ts +228 -0
  5. package/dist/WorkerTrigger.d.ts +40 -4
  6. package/dist/WorkerTrigger.js +272 -40
  7. package/dist/adapters/BullMQAdapter.d.ts +1 -1
  8. package/dist/adapters/BullMQAdapter.js +5 -42
  9. package/dist/adapters/InMemoryAdapter.d.ts +1 -1
  10. package/dist/adapters/InMemoryAdapter.js +13 -12
  11. package/dist/adapters/KafkaAdapter.d.ts +62 -0
  12. package/dist/adapters/KafkaAdapter.js +236 -0
  13. package/dist/adapters/NATSAdapter.d.ts +110 -0
  14. package/dist/adapters/NATSAdapter.js +394 -0
  15. package/dist/adapters/PgBossAdapter.d.ts +56 -0
  16. package/dist/adapters/PgBossAdapter.js +251 -0
  17. package/dist/adapters/RabbitMQAdapter.d.ts +51 -0
  18. package/dist/adapters/RabbitMQAdapter.js +241 -0
  19. package/dist/adapters/RedisStreamsAdapter.d.ts +64 -0
  20. package/dist/adapters/RedisStreamsAdapter.js +240 -0
  21. package/dist/adapters/SQSAdapter.d.ts +61 -0
  22. package/dist/adapters/SQSAdapter.js +269 -0
  23. package/dist/adapters/factory.d.ts +34 -0
  24. package/dist/adapters/factory.js +103 -0
  25. package/dist/index.d.ts +25 -7
  26. package/dist/index.js +31 -16
  27. package/package.json +27 -5
  28. package/src/WorkerTrigger.test.ts +44 -14
  29. package/src/WorkerTrigger.ts +299 -27
  30. package/src/adapters/InMemoryAdapter.ts +9 -5
  31. package/src/adapters/KafkaAdapter.ts +277 -0
  32. package/src/adapters/NATSAdapter.ts +454 -0
  33. package/src/adapters/PgBossAdapter.ts +293 -0
  34. package/src/adapters/RabbitMQAdapter.ts +285 -0
  35. package/src/adapters/RedisStreamsAdapter.ts +286 -0
  36. package/src/adapters/SQSAdapter.ts +306 -0
  37. package/src/adapters/factory.test.ts +89 -0
  38. package/src/adapters/factory.ts +111 -0
  39. package/src/adapters/new-adapters.test.ts +130 -0
  40. package/src/index.ts +31 -4
  41. package/template/.env.example +13 -0
  42. package/template/package.json +45 -0
  43. package/template/src/Nodes.ts +10 -0
  44. package/template/src/Workflows.ts +8 -0
  45. package/template/src/index.ts +41 -0
  46. package/template/src/runner/WorkerServer.ts +34 -0
  47. package/template/src/runner/types/Workflows.ts +7 -0
  48. package/template/src/workflows/jobs/process-job.ts +47 -0
  49. package/template/tsconfig.json +31 -0
  50. package/template/vitest.config.ts +39 -0
@@ -0,0 +1,236 @@
1
+ /**
2
+ * KafkaAdapter — v0.7 PR 5 — Worker adapter backed by Apache Kafka via
3
+ * `kafkajs`. Consumes from a topic (the `queue` field) with a
4
+ * consumer-group identifier; produces via the same client.
5
+ *
6
+ * Kafka is fundamentally a streaming platform — not a queue — so a
7
+ * few semantics differ from BullMQ/SQS/RabbitMQ:
8
+ *
9
+ * - **Ordering**: per-partition, not per-topic. Set the partition
10
+ * key via the `dedupId` field on `addJob` to keep related
11
+ * messages on the same partition.
12
+ * - **Retries**: Kafka doesn't have a broker-side retry concept.
13
+ * The adapter re-throws on handler failure; offset commit is
14
+ * suppressed so the consumer re-polls the message on the next
15
+ * cycle. For real retry semantics, layer a dead-letter topic.
16
+ * - **Stats**: KafkaJS exposes consumer-group lag via its admin
17
+ * client; the lag count is reported as `waiting`. Other stats
18
+ * are tracked locally per consumer.
19
+ *
20
+ * Requires `kafkajs` as a peer dependency:
21
+ *
22
+ * bun add kafkajs
23
+ *
24
+ * Environment variables (read at adapter construction):
25
+ * - `KAFKA_BROKERS` — comma-separated list (default `localhost:9092`).
26
+ * - `KAFKA_CLIENT_ID` — client.id (default `"blok-worker"`).
27
+ * - `KAFKA_SASL_USERNAME` — SASL/PLAIN username (optional).
28
+ * - `KAFKA_SASL_PASSWORD` — SASL/PLAIN password (optional).
29
+ * - `KAFKA_SSL` — when `"true"`, enable TLS.
30
+ */
31
+ import { v4 as uuid } from "uuid";
32
+ export class KafkaAdapter {
33
+ provider = "kafka";
34
+ config;
35
+ // biome-ignore lint/suspicious/noExplicitAny: kafkajs's exported `Kafka` constructor is loosely typed.
36
+ kafka = null;
37
+ handle = { consumers: new Map() };
38
+ connected = false;
39
+ stats = new Map();
40
+ constructor(config) {
41
+ this.config = {
42
+ brokers: config?.brokers ?? (process.env.KAFKA_BROKERS ?? "localhost:9092").split(",").map((s) => s.trim()),
43
+ clientId: config?.clientId ?? process.env.KAFKA_CLIENT_ID ?? "blok-worker",
44
+ saslUsername: config?.saslUsername ?? process.env.KAFKA_SASL_USERNAME,
45
+ saslPassword: config?.saslPassword ?? process.env.KAFKA_SASL_PASSWORD,
46
+ ssl: config?.ssl ?? process.env.KAFKA_SSL === "true",
47
+ };
48
+ }
49
+ async connect() {
50
+ if (this.connected)
51
+ return;
52
+ try {
53
+ // biome-ignore lint/suspicious/noExplicitAny: kafkajs is a runtime-loaded peer dep.
54
+ const kafkajs = await import("kafkajs");
55
+ const sasl = this.config.saslUsername && this.config.saslPassword
56
+ ? { mechanism: "plain", username: this.config.saslUsername, password: this.config.saslPassword }
57
+ : undefined;
58
+ this.kafka = new kafkajs.Kafka({
59
+ clientId: this.config.clientId,
60
+ brokers: this.config.brokers,
61
+ ssl: this.config.ssl,
62
+ sasl,
63
+ });
64
+ this.handle.producer = this.kafka.producer();
65
+ await this.handle.producer?.connect();
66
+ this.handle.admin = this.kafka.admin();
67
+ await this.handle.admin?.connect();
68
+ this.connected = true;
69
+ }
70
+ catch (err) {
71
+ throw new Error(`[blok][kafka] connect failed: ${err.message}. Install kafkajs as a peer dependency: bun add kafkajs`);
72
+ }
73
+ }
74
+ async disconnect() {
75
+ if (!this.connected)
76
+ return;
77
+ for (const [, consumer] of this.handle.consumers) {
78
+ try {
79
+ await consumer.disconnect();
80
+ }
81
+ catch {
82
+ /* ignore */
83
+ }
84
+ }
85
+ this.handle.consumers.clear();
86
+ try {
87
+ await this.handle.producer?.disconnect();
88
+ }
89
+ catch {
90
+ /* ignore */
91
+ }
92
+ try {
93
+ await this.handle.admin?.disconnect();
94
+ }
95
+ catch {
96
+ /* ignore */
97
+ }
98
+ this.connected = false;
99
+ }
100
+ async process(config, handler) {
101
+ if (!this.connected)
102
+ throw new Error("[blok][kafka] not connected. Call connect() first.");
103
+ const groupId = config.consumerGroup ?? `${config.queue}-group`;
104
+ const consumer = this.kafka.consumer({ groupId });
105
+ await consumer.connect();
106
+ await consumer.subscribe({ topic: config.queue, fromBeginning: config.fromBeginning === true });
107
+ this.handle.consumers.set(config.queue, consumer);
108
+ this.stats.set(config.queue, { completed: 0, failed: 0, active: 0 });
109
+ const stats = this.stats.get(config.queue);
110
+ await consumer.run({
111
+ autoCommit: config.ack !== false,
112
+ eachMessage: async ({ message, }) => {
113
+ const payloadString = message.value?.toString("utf8") ?? "";
114
+ let data;
115
+ try {
116
+ data = payloadString.length > 0 ? JSON.parse(payloadString) : null;
117
+ }
118
+ catch {
119
+ data = payloadString;
120
+ }
121
+ const headers = {};
122
+ if (message.headers) {
123
+ for (const [k, v] of Object.entries(message.headers))
124
+ headers[k] = v?.toString("utf8") ?? "";
125
+ }
126
+ const job = {
127
+ id: message.key?.toString("utf8") ?? `${config.queue}:${message.offset}`,
128
+ data,
129
+ headers,
130
+ queue: config.queue,
131
+ priority: config.priority ?? 0,
132
+ attempts: 0,
133
+ maxRetries: config.retries ?? 0,
134
+ createdAt: new Date(Number.parseInt(message.timestamp, 10)),
135
+ timeout: config.timeout,
136
+ raw: message,
137
+ complete: async () => {
138
+ stats.completed += 1;
139
+ },
140
+ fail: async (_err) => {
141
+ stats.failed += 1;
142
+ throw _err;
143
+ },
144
+ };
145
+ stats.active += 1;
146
+ try {
147
+ await handler(job);
148
+ stats.completed += 1;
149
+ }
150
+ catch (err) {
151
+ stats.failed += 1;
152
+ throw err;
153
+ }
154
+ finally {
155
+ stats.active = Math.max(0, stats.active - 1);
156
+ }
157
+ },
158
+ });
159
+ }
160
+ async addJob(queue, data, opts) {
161
+ if (!this.connected)
162
+ throw new Error("[blok][kafka] not connected. Call connect() first.");
163
+ if (!this.handle.producer)
164
+ throw new Error("[blok][kafka] producer not initialized");
165
+ const key = opts?.jobId ?? uuid();
166
+ const payload = typeof data === "string" ? data : JSON.stringify(data);
167
+ await this.handle.producer.send({
168
+ topic: queue,
169
+ messages: [
170
+ {
171
+ key,
172
+ value: payload,
173
+ headers: opts?.delay ? { "x-blok-delay-ms": String(opts.delay) } : undefined,
174
+ },
175
+ ],
176
+ });
177
+ return key;
178
+ }
179
+ async stopProcessing(queue) {
180
+ const consumer = this.handle.consumers.get(queue);
181
+ if (consumer) {
182
+ try {
183
+ await consumer.stop();
184
+ }
185
+ catch {
186
+ /* ignore */
187
+ }
188
+ try {
189
+ await consumer.disconnect();
190
+ }
191
+ catch {
192
+ /* ignore */
193
+ }
194
+ this.handle.consumers.delete(queue);
195
+ }
196
+ }
197
+ isConnected() {
198
+ return this.connected;
199
+ }
200
+ async healthCheck() {
201
+ if (!this.connected || !this.handle.admin)
202
+ return false;
203
+ try {
204
+ await this.handle.admin.fetchTopicOffsets("__consumer_offsets");
205
+ return true;
206
+ }
207
+ catch {
208
+ return false;
209
+ }
210
+ }
211
+ async getQueueStats(queue) {
212
+ const counters = this.stats.get(queue) ?? { completed: 0, failed: 0, active: 0 };
213
+ let waiting = 0;
214
+ if (this.handle.admin) {
215
+ try {
216
+ const offsets = await this.handle.admin.fetchTopicOffsets(queue);
217
+ // Approximate: total committed offsets across partitions. Real lag
218
+ // requires admin.fetchOffsets({ groupId }) — skipped here to keep
219
+ // the call cheap; production deployments should use Kafka's
220
+ // dedicated lag metrics anyway.
221
+ waiting = offsets.reduce((sum, p) => sum + Number.parseInt(p.offset, 10), 0);
222
+ }
223
+ catch {
224
+ waiting = 0;
225
+ }
226
+ }
227
+ return {
228
+ waiting,
229
+ active: counters.active,
230
+ completed: counters.completed,
231
+ failed: counters.failed,
232
+ delayed: 0,
233
+ };
234
+ }
235
+ }
236
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiS2Fma2FBZGFwdGVyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2FkYXB0ZXJzL0thZmthQWRhcHRlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7R0E2Qkc7QUFHSCxPQUFPLEVBQUUsRUFBRSxJQUFJLElBQUksRUFBRSxNQUFNLE1BQU0sQ0FBQztBQXNDbEMsTUFBTSxPQUFPLFlBQVk7SUFDZixRQUFRLEdBQUcsT0FBZ0IsQ0FBQztJQUNwQixNQUFNLENBQWM7SUFDckMsdUdBQXVHO0lBQy9GLEtBQUssR0FBUSxJQUFJLENBQUM7SUFDbEIsTUFBTSxHQUFrQixFQUFFLFNBQVMsRUFBRSxJQUFJLEdBQUcsRUFBRSxFQUFFLENBQUM7SUFDakQsU0FBUyxHQUFHLEtBQUssQ0FBQztJQUNsQixLQUFLLEdBQW9DLElBQUksR0FBRyxFQUFFLENBQUM7SUFFM0QsWUFBWSxNQUE2QjtRQUN4QyxJQUFJLENBQUMsTUFBTSxHQUFHO1lBQ2IsT0FBTyxFQUFFLE1BQU0sRUFBRSxPQUFPLElBQUksQ0FBQyxPQUFPLENBQUMsR0FBRyxDQUFDLGFBQWEsSUFBSSxnQkFBZ0IsQ0FBQyxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLEVBQUUsRUFBRSxDQUFDLENBQUMsQ0FBQyxJQUFJLEVBQUUsQ0FBQztZQUMzRyxRQUFRLEVBQUUsTUFBTSxFQUFFLFFBQVEsSUFBSSxPQUFPLENBQUMsR0FBRyxDQUFDLGVBQWUsSUFBSSxhQUFhO1lBQzFFLFlBQVksRUFBRSxNQUFNLEVBQUUsWUFBWSxJQUFJLE9BQU8sQ0FBQyxHQUFHLENBQUMsbUJBQW1CO1lBQ3JFLFlBQVksRUFBRSxNQUFNLEVBQUUsWUFBWSxJQUFJLE9BQU8sQ0FBQyxHQUFHLENBQUMsbUJBQW1CO1lBQ3JFLEdBQUcsRUFBRSxNQUFNLEVBQUUsR0FBRyxJQUFJLE9BQU8sQ0FBQyxHQUFH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QUFhO1FBQ2hDLE1BQU0sUUFBUSxHQUFHLElBQUksQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLEtBQUssQ0FBQyxJQUFJLEVBQUUsU0FBUyxFQUFFLENBQUMsRUFBRSxNQUFNLEVBQUUsQ0FBQyxFQUFFLE1BQU0sRUFBRSxDQUFDLEVBQUUsQ0FBQztRQUNqRixJQUFJLE9BQU8sR0FBRyxDQUFDLENBQUM7UUFDaEIsSUFBSSxJQUFJLENBQUMsTUFBTSxDQUFDLEtBQUssRUFBRSxDQUFDO1lBQ3ZCLElBQUksQ0FBQztnQkFDSixNQUFNLE9BQU8sR0FBRyxNQUFNLElBQUksQ0FBQyxNQUFNLENBQUMsS0FBSyxDQUFDLGlCQUFpQixDQUFDLEtBQUssQ0FBQyxDQUFDO2dCQUNqRSxtRUFBbUU7Z0JBQ25FLGtFQUFrRTtnQkFDbEUsNERBQTREO2dCQUM1RCxnQ0FBZ0M7Z0JBQ2hDLE9BQU8sR0FBRyxPQUFPLENBQUMsTUFBTSxDQUFDLENBQUMsR0FBRyxFQUFFLENBQUMsRUFBRSxFQUFFLENBQUMsR0FBRyxHQUFHLE1BQU0sQ0FBQyxRQUFRLENBQUMsQ0FBQyxDQUFDLE1BQU0sRUFBRSxFQUFFLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQztZQUM5RSxDQUFDO1lBQUMsTUFBTSxDQUFDO2dCQUNSLE9BQU8sR0FBRyxDQUFDLENBQUM7WUFDYixDQUFDO1FBQ0YsQ0FBQztRQUNELE9BQU87WUFDTixPQUFPO1lBQ1AsTUFBTSxFQUFFLFFBQVEsQ0FBQyxNQUFNO1lBQ3ZCLFNBQVMsRUFBRSxRQUFRLENBQUMsU0FBUztZQUM3QixNQUFNLEVBQUUsUUFBUSxDQUFDLE1BQU07WUFDdkIsT0FBTyxFQUFFLENBQUM7U0FDVixDQUFDO0lBQ0gsQ0FBQztDQUNEIiwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiBLYWZrYUFkYXB0ZXIg4oCUIHYwLjcgUFIgNSDigJQgV29ya2VyIGFkYXB0ZXIgYmFja2VkIGJ5IEFwYWNoZSBLYWZrYSB2aWFcbiAqIGBrYWZrYWpzYC4gQ29uc3VtZXMgZnJvbSBhIHRvcGljICh0aGUgYHF1ZXVlYCBmaWVsZCkgd2l0aCBhXG4gKiBjb25zdW1lci1ncm91cCBpZGVudGlmaWVyOyBwcm9kdWNlcyB2aWEgdGhlIHNhbWUgY2xpZW50LlxuICpcbiAqIEthZmthIGlzIGZ1bmRhbWVudGFsbHkgYSBzdHJlYW1pbmcgcGxhdGZvcm0g4oCUIG5vdCBhIHF1ZXVlIOKAlCBzbyBhXG4gKiBmZXcgc2VtYW50aWNzIGRpZmZlciBmcm9tIEJ1bGxNUS9TUVMvUmFiYml0TVE6XG4gKlxuICogICAtICoqT3JkZXJpbmcqKjogcGVyLXBhcnRpdGlvbiwgbm90IHBlci10b3BpYy4gU2V0IHRoZSBwYXJ0aXRpb25cbiAqICAgICBrZXkgdmlhIHRoZSBgZGVkdXBJZGAgZmllbGQgb24gYGFkZEpvYmAgdG8ga2VlcCByZWxhdGVkXG4gKiAgICAgbWVzc2FnZXMgb24gdGhlIHNhbWUgcGFydGl0aW9uLlxuICogICAtICoqUmV0cmllcyoqOiBLYWZrYSBkb2Vzbid0IGhhdmUgYSBicm9rZXItc2lkZSByZXRyeSBjb25jZXB0LlxuICogICAgIFRoZSBhZGFwdGVyIHJlLXRocm93cyBvbiBoYW5kbGVyIGZhaWx1cmU7IG9mZnNldCBjb21taXQgaXNcbiAqICAgICBzdXBwcmVzc2VkIHNvIHRoZSBjb25zdW1lciByZS1wb2xscyB0aGUgbWVzc2FnZSBvbiB0aGUgbmV4dFxuICogICAgIGN5Y2xlLiBGb3IgcmVhbCByZXRyeSBzZW1hbnRpY3MsIGxheWVyIGEgZGVhZC1sZXR0ZXIgdG9waWMuXG4gKiAgIC0gKipTdGF0cyoqOiBLYWZrYUpTIGV4cG9zZXMgY29uc3VtZXItZ3JvdXAgbGFnIHZpYSBpdHMgYWRtaW5cbiAqICAgICBjbGllbnQ7IHRoZSBsYWcgY291bnQgaXMgcmVwb3J0ZWQgYXMgYHdhaXRpbmdgLiBPdGhlciBzdGF0c1xuICogICAgIGFyZSB0cmFja2VkIGxvY2FsbHkgcGVyIGNvbnN1bWVyLlxuICpcbiAqIFJlcXVpcmVzIGBrYWZrYWpzYCBhcyBhIHBlZXIgZGVwZW5kZW5jeTpcbiAqXG4gKiAgICAgYnVuIGFkZCBrYWZrYWpzXG4gKlxuICogRW52aXJvbm1lbnQgdmFyaWFibGVzIChyZWFkIGF0IGFkYXB0ZXIgY29uc3RydWN0aW9uKTpcbiAqICAgLSBgS0FGS0FfQlJPS0VSU2AgICAgICAgICAgIOKAlCBjb21tYS1zZXBhcmF0ZWQgbGlzdCAoZGVmYXVsdCBgbG9jYWxob3N0OjkwOTJgKS5cbiAqICAgLSBgS0FGS0FfQ0xJRU5UX0lEYCAgICAgICAgIOKAlCBjbGllbnQuaWQgKGRlZmF1bHQgYFwiYmxvay13b3JrZXJcImApLlxuICogICAtIGBLQUZLQV9TQVNMX1VTRVJOQU1FYCAgICAg4oCUIFNBU0wvUExBSU4gdXNlcm5hbWUgKG9wdGlvbmFsKS5cbiAqICAgLSBgS0FGS0FfU0FTTF9QQVNTV09SRGAgICAgIOKAlCBTQVNML1BMQUlOIHBhc3N3b3JkIChvcHRpb25hbCkuXG4gKiAgIC0gYEtBRktBX1NTTGAgICAgICAgICAgICAgICDigJQgd2hlbiBgXCJ0cnVlXCJgLCBlbmFibGUgVExTLlxuICovXG5cbmltcG9ydCB0eXBlIHsgV29ya2VyVHJpZ2dlck9wdHMgfSBmcm9tIFwiQGJsb2tqcy9oZWxwZXJcIjtcbmltcG9ydCB7IHY0IGFzIHV1aWQgfSBmcm9tIFwidXVpZFwiO1xuaW1wb3J0IHR5cGUgeyBXb3JrZXJBZGFwdGVyLCBXb3JrZXJKb2IsIFdvcmtlclF1ZXVlU3RhdHMgfSBmcm9tIFwiLi4vV29ya2VyVHJpZ2dlclwiO1xuXG5leHBvcnQgaW50ZXJmYWNlIEthZmthQ29uZmlnIHtcblx0YnJva2Vyczogc3RyaW5nW107XG5cdGNsaWVudElkOiBzdHJpbmc7XG5cdHNhc2xVc2VybmFtZT86IHN0cmluZztcblx0c2FzbFBhc3N3b3JkPzogc3RyaW5nO1xuXHRzc2w6IGJvb2xlYW47XG59XG5cbmludGVyZmFjZSBLYWZrYUpzSGFuZGxlIHtcblx0cHJvZHVjZXI/OiB7XG5cdFx0Y29ubmVjdDogKCkgPT4gUHJvbWlzZTx2b2lkPjtcblx0XHRkaXNjb25uZWN0OiAoKSA9PiBQcm9taXNlPHZvaWQ+O1xuXHRcdHNlbmQ6IChhcmdzOiB1bmtub3duKSA9PiBQcm9taXNlPHVua25vd24+O1xuXHR9O1xuXHRjb25zdW1lcnM6IE1hcDxcblx0XHRzdHJpbmcsXG5cdFx0e1xuXHRcdFx0ZGlzY29ubmVjdDogKCkgPT4gUHJvbWlzZTx2b2lkPjtcblx0XHRcdHN0b3A6ICgpID0+IFByb21pc2U8dm9pZD47XG5cdFx0XHRydW46IChvcHRzOiB1bmtub3duKSA9PiBQcm9taXNlPHZvaWQ+O1xuXHRcdH1cblx0Pjtcblx0YWRtaW4/OiB7XG5cdFx0Y29ubmVjdDogKCkgPT4gUHJvbWlzZTx2b2lkPjtcblx0XHRkaXNjb25uZWN0OiAoKSA9PiBQcm9taXNlPHZvaWQ+O1xuXHRcdGZldGNoVG9waWNPZmZzZXRzOiAodG9waWM6IHN0cmluZykgPT4gUHJvbWlzZTxBcnJheTx7IHBhcnRpdGlvbjogbnVtYmVyOyBvZmZzZXQ6IHN0cmluZyB9Pj47XG5cdH07XG59XG5cbmludGVyZmFjZSBRdWV1ZVN0YXRzQ291bnRlcnMge1xuXHRjb21wbGV0ZWQ6IG51bWJlcjtcblx0ZmFpbGVkOiBudW1iZXI7XG5cdGFjdGl2ZTogbnVtYmVyO1xufVxuXG5leHBvcnQgY2xhc3MgS2Fma2FBZGFwdGVyIGltcGxlbWVudHMgV29ya2VyQWRhcHRlciB7XG5cdHJlYWRvbmx5IHByb3ZpZGVyID0gXCJrYWZrYVwiIGFzIGNvbnN0O1xuXHRwcml2YXRlIHJlYWRvbmx5IGNvbmZpZzogS2Fma2FDb25maWc7XG5cdC8vIGJpb21lLWlnbm9yZSBsaW50L3N1c3BpY2lvdXMvbm9FeHBsaWNpdEFueToga2Fma2FqcydzIGV4cG9ydGVkIGBLYWZrYWAgY29uc3RydWN0b3IgaXMgbG9vc2VseSB0eXBlZC5cblx0cHJpdmF0ZSBrYWZrYTogYW55ID0gbnVsbDtcblx0cHJpdmF0ZSBoYW5kbGU6IEthZmthSnNIYW5kbGUgPSB7IGNvbnN1bWVyczogbmV3IE1hcCgpIH07XG5cdHByaXZhdGUgY29ubmVjdGVkID0gZmFsc2U7XG5cdHByaXZhdGUgc3RhdHM6IE1hcDxzdHJpbmcsIFF1ZXVlU3RhdHNDb3VudGVycz4gPSBuZXcgTWFwKCk7XG5cblx0Y29uc3RydWN0b3IoY29uZmlnPzogUGFydGlhbDxLYWZrYUNvbmZpZz4pIHtcblx0XHR0aGlzLmNvbmZpZyA9IHtcblx0XHRcdGJyb2tlcnM6IGNvbmZpZz8uYnJva2VycyA/PyAocHJvY2Vzcy5lbnYuS0FGS0FfQlJPS0VSUyA/PyBcImxvY2FsaG9zdDo5MDkyXCIpLnNwbGl0KFwiLFwiKS5tYXAoKHMpID0+IHMudHJpbSgpKSxcblx0XHRcdGNsaWVudElkOiBjb25maWc/LmNsaWVudElkID8/IHByb2Nlc3MuZW52LktBRktBX0NMSUVOVF9JRCA/PyBcImJsb2std29ya2VyXCIsXG5cdFx0XHRzYXNsVXNlcm5hbWU6IGNvbmZpZz8uc2FzbFVzZXJuYW1lID8/IHByb2Nlc3MuZW52LktBRktBX1NBU0xfVVNFUk5BTUUsXG5cdFx0XHRzYXNsUGFzc3dvcmQ6IGNvbmZpZz8uc2FzbFBhc3N3b3JkID8/IHByb2Nlc3MuZW52LktBRktBX1NBU0xfUEFTU1dPUkQsXG5cdFx0XHRzc2w6IGNvbmZpZz8uc3NsID8/IHByb2Nlc3MuZW52LktBRktBX1NTTCA9PT0gXCJ0cnVlXCIsXG5cdFx0fTtcblx0fVxuXG5cdGFzeW5jIGNvbm5lY3QoKTogUHJvbWlzZTx2b2lkPiB7XG5cdFx0aWYgKHRoaXMuY29ubmVjdGVkKSByZXR1cm47XG5cdFx0dHJ5IHtcblx0XHRcdC8vIGJpb21lLWlnbm9yZSBsaW50L3N1c3BpY2lvdXMvbm9FeHBsaWNpdEFueToga2Fma2FqcyBpcyBhIHJ1bnRpbWUtbG9hZGVkIHBlZXIgZGVwLlxuXHRcdFx0Y29uc3Qga2Fma2FqczogYW55ID0gYXdhaXQgaW1wb3J0KFwia2Fma2Fqc1wiKTtcblx0XHRcdGNvbnN0IHNhc2wgPVxuXHRcdFx0XHR0aGlzLmNvbmZpZy5zYXNsVXNlcm5hbWUgJiYgdGhpcy5jb25maWcuc2FzbFBhc3N3b3JkXG5cdFx0XHRcdFx0PyB7IG1lY2hhbmlzbTogXCJwbGFpblwiLCB1c2VybmFtZTogdGhpcy5jb25maWcuc2FzbFVzZXJuYW1lLCBwYXNzd29yZDogdGhpcy5jb25maWcuc2FzbFBhc3N3b3JkIH1cblx0XHRcdFx0XHQ6IHVuZGVmaW5lZDtcblx0XHRcdHRoaXMua2Fma2EgPSBuZXcga2Fma2Fqcy5LYWZrYSh7XG5cdFx0XHRcdGNsaWVudElkOiB0aGlzLmNvbmZpZy5jbGllbnRJZCxcblx0XHRcdFx0YnJva2VyczogdGhpcy5jb25maWcuYnJva2Vycyxcblx0XHRcdFx0c3NsOiB0aGlzLmNvbmZpZy5zc2wsXG5cdFx0XHRcdHNhc2wsXG5cdFx0XHR9KTtcblx0XHRcdHRoaXMuaGFuZGxlLnByb2R1Y2VyID0gdGhpcy5rYWZrYS5wcm9kdWNlcigpO1xuXHRcdFx0YXdhaXQgdGhpcy5oYW5kbGUucHJvZHVjZXI/LmNvbm5lY3QoKTtcblx0XHRcdHRoaXMuaGFuZGxlLmFkbWluID0gdGhpcy5rYWZrYS5hZG1pbigpO1xuXHRcdFx0YXdhaXQgdGhpcy5oYW5kbGUuYWRtaW4/LmNvbm5lY3QoKTtcblx0XHRcdHRoaXMuY29ubmVjdGVkID0gdHJ1ZTtcblx0XHR9IGNhdGNoIChlcnIpIHtcblx0XHRcdHRocm93IG5ldyBFcnJvcihcblx0XHRcdFx0YFtibG9rXVtrYWZrYV0gY29ubmVjdCBmYWlsZWQ6ICR7KGVyciBhcyBFcnJvcikubWVzc2FnZX0uIEluc3RhbGwga2Fma2FqcyBhcyBhIHBlZXIgZGVwZW5kZW5jeTogYnVuIGFkZCBrYWZrYWpzYCxcblx0XHRcdCk7XG5cdFx0fVxuXHR9XG5cblx0YXN5bmMgZGlzY29ubmVjdCgpOiBQcm9taXNlPHZvaWQ+IHtcblx0XHRpZiAoIXRoaXMuY29ubmVjdGVkKSByZXR1cm47XG5cdFx0Zm9yIChjb25zdCBbLCBjb25zdW1lcl0gb2YgdGhpcy5oYW5kbGUuY29uc3VtZXJzKSB7XG5cdFx0XHR0cnkge1xuXHRcdFx0XHRhd2FpdCBjb25zdW1lci5kaXNjb25uZWN0KCk7XG5cdFx0XHR9IGNhdGNoIHtcblx0XHRcdFx0LyogaWdub3JlICovXG5cdFx0XHR9XG5cdFx0fVxuXHRcdHRoaXMuaGFuZGxlLmNvbnN1bWVycy5jbGVhcigpO1xuXHRcdHRyeSB7XG5cdFx0XHRhd2FpdCB0aGlzLmhhbmRsZS5wcm9kdWNlcj8uZGlzY29ubmVjdCgpO1xuXHRcdH0gY2F0Y2gge1xuXHRcdFx0LyogaWdub3JlICovXG5cdFx0fVxuXHRcdHRyeSB7XG5cdFx0XHRhd2FpdCB0aGlzLmhhbmRsZS5hZG1pbj8uZGlzY29ubmVjdCgpO1xuXHRcdH0gY2F0Y2gge1xuXHRcdFx0LyogaWdub3JlICovXG5cdFx0fVxuXHRcdHRoaXMuY29ubmVjdGVkID0gZmFsc2U7XG5cdH1cblxuXHRhc3luYyBwcm9jZXNzKGNvbmZpZzogV29ya2VyVHJpZ2dlck9wdHMsIGhhbmRsZXI6IChqb2I6IFdvcmtlckpvYikgPT4gUHJvbWlzZTx2b2lkPik6IFByb21pc2U8dm9pZD4ge1xuXHRcdGlmICghdGhpcy5jb25uZWN0ZWQpIHRocm93IG5ldyBFcnJvcihcIltibG9rXVtrYWZrYV0gbm90IGNvbm5lY3RlZC4gQ2FsbCBjb25uZWN0KCkgZmlyc3QuXCIpO1xuXHRcdGNvbnN0IGdyb3VwSWQgPSBjb25maWcuY29uc3VtZXJHcm91cCA/PyBgJHtjb25maWcucXVldWV9LWdyb3VwYDtcblx0XHRjb25zdCBjb25zdW1lciA9IHRoaXMua2Fma2EuY29uc3VtZXIoeyBncm91cElkIH0pO1xuXHRcdGF3YWl0IGNvbnN1bWVyLmNvbm5lY3QoKTtcblx0XHRhd2FpdCBjb25zdW1lci5zdWJzY3JpYmUoeyB0b3BpYzogY29uZmlnLnF1ZXVlLCBmcm9tQmVnaW5uaW5nOiBjb25maWcuZnJvbUJlZ2lubmluZyA9PT0gdHJ1ZSB9KTtcblx0XHR0aGlzLmhhbmRsZS5jb25zdW1lcnMuc2V0KGNvbmZpZy5xdWV1ZSwgY29uc3VtZXIpO1xuXHRcdHRoaXMuc3RhdHMuc2V0KGNvbmZpZy5xdWV1ZSwgeyBjb21wbGV0ZWQ6IDAsIGZhaWxlZDogMCwgYWN0aXZlOiAwIH0pO1xuXHRcdGNvbnN0IHN0YXRzID0gdGhpcy5zdGF0cy5nZXQoY29uZmlnLnF1ZXVlKSBhcyBRdWV1ZVN0YXRzQ291bnRlcnM7XG5cblx0XHRhd2FpdCBjb25zdW1lci5ydW4oe1xuXHRcdFx0YXV0b0NvbW1pdDogY29uZmlnLmFjayAhPT0gZmFsc2UsXG5cdFx0XHRlYWNoTWVzc2FnZTogYXN5bmMgKHtcblx0XHRcdFx0bWVzc2FnZSxcblx0XHRcdH06IHtcblx0XHRcdFx0bWVzc2FnZTogeyBrZXk/OiBCdWZmZXI7IHZhbHVlPzogQnVmZmVyOyBvZmZzZXQ6IHN0cmluZzsgdGltZXN0YW1wOiBzdHJpbmc7IGhlYWRlcnM/OiBSZWNvcmQ8c3RyaW5nLCBCdWZmZXI+IH07XG5cdFx0XHR9KSA9PiB7XG5cdFx0XHRcdGNvbnN0IHBheWxvYWRTdHJpbmcgPSBtZXNzYWdlLnZhbHVlPy50b1N0cmluZyhcInV0ZjhcIikgPz8gXCJcIjtcblx0XHRcdFx0bGV0IGRhdGE6IHVua25vd247XG5cdFx0XHRcdHRyeSB7XG5cdFx0XHRcdFx0ZGF0YSA9IHBheWxvYWRTdHJpbmcubGVuZ3RoID4gMCA/IEpTT04ucGFyc2UocGF5bG9hZFN0cmluZykgOiBudWxsO1xuXHRcdFx0XHR9IGNhdGNoIHtcblx0XHRcdFx0XHRkYXRhID0gcGF5bG9hZFN0cmluZztcblx0XHRcdFx0fVxuXHRcdFx0XHRjb25zdCBoZWFkZXJzOiBSZWNvcmQ8c3RyaW5nLCBzdHJpbmc+ID0ge307XG5cdFx0XHRcdGlmIChtZXNzYWdlLmhlYWRlcnMpIHtcblx0XHRcdFx0XHRmb3IgKGNvbnN0IFtrLCB2XSBvZiBPYmplY3QuZW50cmllcyhtZXNzYWdlLmhlYWRlcnMpKSBoZWFkZXJzW2tdID0gdj8udG9TdHJpbmcoXCJ1dGY4XCIpID8/IFwiXCI7XG5cdFx0XHRcdH1cblx0XHRcdFx0Y29uc3Qgam9iOiBXb3JrZXJKb2IgPSB7XG5cdFx0XHRcdFx0aWQ6IG1lc3NhZ2Uua2V5Py50b1N0cmluZyhcInV0ZjhcIikgPz8gYCR7Y29uZmlnLnF1ZXVlfToke21lc3NhZ2Uub2Zmc2V0fWAsXG5cdFx0XHRcdFx0ZGF0YSxcblx0XHRcdFx0XHRoZWFkZXJzLFxuXHRcdFx0XHRcdHF1ZXVlOiBjb25maWcucXVldWUsXG5cdFx0XHRcdFx0cHJpb3JpdHk6IGNvbmZpZy5wcmlvcml0eSA/PyAwLFxuXHRcdFx0XHRcdGF0dGVtcHRzOiAwLFxuXHRcdFx0XHRcdG1heFJldHJpZXM6IGNvbmZpZy5yZXRyaWVzID8/IDAsXG5cdFx0XHRcdFx0Y3JlYXRlZEF0OiBuZXcgRGF0ZShOdW1iZXIucGFyc2VJbnQobWVzc2FnZS50aW1lc3RhbXAsIDEwKSksXG5cdFx0XHRcdFx0dGltZW91dDogY29uZmlnLnRpbWVvdXQsXG5cdFx0XHRcdFx0cmF3OiBtZXNzYWdlLFxuXHRcdFx0XHRcdGNvbXBsZXRlOiBhc3luYyAoKSA9PiB7XG5cdFx0XHRcdFx0XHRzdGF0cy5jb21wbGV0ZWQgKz0gMTtcblx0XHRcdFx0XHR9LFxuXHRcdFx0XHRcdGZhaWw6IGFzeW5jIChfZXJyOiBFcnJvcikgPT4ge1xuXHRcdFx0XHRcdFx0c3RhdHMuZmFpbGVkICs9IDE7XG5cdFx0XHRcdFx0XHR0aHJvdyBfZXJyO1xuXHRcdFx0XHRcdH0sXG5cdFx0XHRcdH07XG5cdFx0XHRcdHN0YXRzLmFjdGl2ZSArPSAxO1xuXHRcdFx0XHR0cnkge1xuXHRcdFx0XHRcdGF3YWl0IGhhbmRsZXIoam9iKTtcblx0XHRcdFx0XHRzdGF0cy5jb21wbGV0ZWQgKz0gMTtcblx0XHRcdFx0fSBjYXRjaCAoZXJyKSB7XG5cdFx0XHRcdFx0c3RhdHMuZmFpbGVkICs9IDE7XG5cdFx0XHRcdFx0dGhyb3cgZXJyO1xuXHRcdFx0XHR9IGZpbmFsbHkge1xuXHRcdFx0XHRcdHN0YXRzLmFjdGl2ZSA9IE1hdGgubWF4KDAsIHN0YXRzLmFjdGl2ZSAtIDEpO1xuXHRcdFx0XHR9XG5cdFx0XHR9LFxuXHRcdH0pO1xuXHR9XG5cblx0YXN5bmMgYWRkSm9iKFxuXHRcdHF1ZXVlOiBzdHJpbmcsXG5cdFx0ZGF0YTogdW5rbm93bixcblx0XHRvcHRzPzogeyBwcmlvcml0eT86IG51bWJlcjsgZGVsYXk/OiBudW1iZXI7IHJldHJpZXM/OiBudW1iZXI7IHRpbWVvdXQ/OiBudW1iZXI7IGpvYklkPzogc3RyaW5nIH0sXG5cdCk6IFByb21pc2U8c3RyaW5nPiB7XG5cdFx0aWYgKCF0aGlzLmNvbm5lY3RlZCkgdGhyb3cgbmV3IEVycm9yKFwiW2Jsb2tdW2thZmthXSBub3QgY29ubmVjdGVkLiBDYWxsIGNvbm5lY3QoKSBmaXJzdC5cIik7XG5cdFx0aWYgKCF0aGlzLmhhbmRsZS5wcm9kdWNlcikgdGhyb3cgbmV3IEVycm9yKFwiW2Jsb2tdW2thZmthXSBwcm9kdWNlciBub3QgaW5pdGlhbGl6ZWRcIik7XG5cdFx0Y29uc3Qga2V5ID0gb3B0cz8uam9iSWQgPz8gdXVpZCgpO1xuXHRcdGNvbnN0IHBheWxvYWQgPSB0eXBlb2YgZGF0YSA9PT0gXCJzdHJpbmdcIiA/IGRhdGEgOiBKU09OLnN0cmluZ2lmeShkYXRhKTtcblx0XHRhd2FpdCB0aGlzLmhhbmRsZS5wcm9kdWNlci5zZW5kKHtcblx0XHRcdHRvcGljOiBxdWV1ZSxcblx0XHRcdG1lc3NhZ2VzOiBbXG5cdFx0XHRcdHtcblx0XHRcdFx0XHRrZXksXG5cdFx0XHRcdFx0dmFsdWU6IHBheWxvYWQsXG5cdFx0XHRcdFx0aGVhZGVyczogb3B0cz8uZGVsYXkgPyB7IFwieC1ibG9rLWRlbGF5LW1zXCI6IFN0cmluZyhvcHRzLmRlbGF5KSB9IDogdW5kZWZpbmVkLFxuXHRcdFx0XHR9LFxuXHRcdFx0XSxcblx0XHR9KTtcblx0XHRyZXR1cm4ga2V5O1xuXHR9XG5cblx0YXN5bmMgc3RvcFByb2Nlc3NpbmcocXVldWU6IHN0cmluZyk6IFByb21pc2U8dm9pZD4ge1xuXHRcdGNvbnN0IGNvbnN1bWVyID0gdGhpcy5oYW5kbGUuY29uc3VtZXJzLmdldChxdWV1ZSk7XG5cdFx0aWYgKGNvbnN1bWVyKSB7XG5cdFx0XHR0cnkge1xuXHRcdFx0XHRhd2FpdCBjb25zdW1lci5zdG9wKCk7XG5cdFx0XHR9IGNhdGNoIHtcblx0XHRcdFx0LyogaWdub3JlICovXG5cdFx0XHR9XG5cdFx0XHR0cnkge1xuXHRcdFx0XHRhd2FpdCBjb25zdW1lci5kaXNjb25uZWN0KCk7XG5cdFx0XHR9IGNhdGNoIHtcblx0XHRcdFx0LyogaWdub3JlICovXG5cdFx0XHR9XG5cdFx0XHR0aGlzLmhhbmRsZS5jb25zdW1lcnMuZGVsZXRlKHF1ZXVlKTtcblx0XHR9XG5cdH1cblxuXHRpc0Nvbm5lY3RlZCgpOiBib29sZWFuIHtcblx0XHRyZXR1cm4gdGhpcy5jb25uZWN0ZWQ7XG5cdH1cblxuXHRhc3luYyBoZWFsdGhDaGVjaygpOiBQcm9taXNlPGJvb2xlYW4+IHtcblx0XHRpZiAoIXRoaXMuY29ubmVjdGVkIHx8ICF0aGlzLmhhbmRsZS5hZG1pbikgcmV0dXJuIGZhbHNlO1xuXHRcdHRyeSB7XG5cdFx0XHRhd2FpdCB0aGlzLmhhbmRsZS5hZG1pbi5mZXRjaFRvcGljT2Zmc2V0cyhcIl9fY29uc3VtZXJfb2Zmc2V0c1wiKTtcblx0XHRcdHJldHVybiB0cnVlO1xuXHRcdH0gY2F0Y2gge1xuXHRcdFx0cmV0dXJuIGZhbHNlO1xuXHRcdH1cblx0fVxuXG5cdGFzeW5jIGdldFF1ZXVlU3RhdHMocXVldWU6IHN0cmluZyk6IFByb21pc2U8V29ya2VyUXVldWVTdGF0cz4ge1xuXHRcdGNvbnN0IGNvdW50ZXJzID0gdGhpcy5zdGF0cy5nZXQocXVldWUpID8/IHsgY29tcGxldGVkOiAwLCBmYWlsZWQ6IDAsIGFjdGl2ZTogMCB9O1xuXHRcdGxldCB3YWl0aW5nID0gMDtcblx0XHRpZiAodGhpcy5oYW5kbGUuYWRtaW4pIHtcblx0XHRcdHRyeSB7XG5cdFx0XHRcdGNvbnN0IG9mZnNldHMgPSBhd2FpdCB0aGlzLmhhbmRsZS5hZG1pbi5mZXRjaFRvcGljT2Zmc2V0cyhxdWV1ZSk7XG5cdFx0XHRcdC8vIEFwcHJveGltYXRlOiB0b3RhbCBjb21taXR0ZWQgb2Zmc2V0cyBhY3Jvc3MgcGFydGl0aW9ucy4gUmVhbCBsYWdcblx0XHRcdFx0Ly8gcmVxdWlyZXMgYWRtaW4uZmV0Y2hPZmZzZXRzKHsgZ3JvdXBJZCB9KSDigJQgc2tpcHBlZCBoZXJlIHRvIGtlZXBcblx0XHRcdFx0Ly8gdGhlIGNhbGwgY2hlYXA7IHByb2R1Y3Rpb24gZGVwbG95bWVudHMgc2hvdWxkIHVzZSBLYWZrYSdzXG5cdFx0XHRcdC8vIGRlZGljYXRlZCBsYWcgbWV0cmljcyBhbnl3YXkuXG5cdFx0XHRcdHdhaXRpbmcgPSBvZmZzZXRzLnJlZHVjZSgoc3VtLCBwKSA9PiBzdW0gKyBOdW1iZXIucGFyc2VJbnQocC5vZmZzZXQsIDEwKSwgMCk7XG5cdFx0XHR9IGNhdGNoIHtcblx0XHRcdFx0d2FpdGluZyA9IDA7XG5cdFx0XHR9XG5cdFx0fVxuXHRcdHJldHVybiB7XG5cdFx0XHR3YWl0aW5nLFxuXHRcdFx0YWN0aXZlOiBjb3VudGVycy5hY3RpdmUsXG5cdFx0XHRjb21wbGV0ZWQ6IGNvdW50ZXJzLmNvbXBsZXRlZCxcblx0XHRcdGZhaWxlZDogY291bnRlcnMuZmFpbGVkLFxuXHRcdFx0ZGVsYXllZDogMCxcblx0XHR9O1xuXHR9XG59XG4iXX0=
@@ -0,0 +1,110 @@
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
+ import type { WorkerTriggerOpts } from "@blokjs/helper";
22
+ import type { WorkerAdapter, WorkerJob, WorkerQueueStats } from "../WorkerTrigger";
23
+ /**
24
+ * Tier 2 polish — compute the consumer-side hold time for a NATS message
25
+ * with an `x-delay` header. NATS JetStream stores `x-delay` as opaque
26
+ * metadata; the broker does NOT defer delivery on it. The consumer is
27
+ * responsible for honouring the delay between the message's first-publish
28
+ * timestamp and `createdMs + delay`.
29
+ *
30
+ * Returns the milliseconds to wait. Clamps to >= 0; returns 0 when the
31
+ * delay has already elapsed (the message was queued for longer than the
32
+ * delay) or when no delay was set.
33
+ *
34
+ * Exported for unit testability — the consumer message handler in
35
+ * `NATSWorkerAdapter.process()` mocks the NATS client extensively, so
36
+ * isolating the math here keeps the surface easy to verify.
37
+ */
38
+ export declare function computeXDelayHoldMs(delay: number, createdMs: number, nowMs: number): number;
39
+ /**
40
+ * NATS worker adapter configuration
41
+ */
42
+ export interface NATSWorkerConfig {
43
+ /** NATS server URLs */
44
+ servers: string[];
45
+ /** Authentication token */
46
+ token?: string;
47
+ /** Username */
48
+ user?: string;
49
+ /** Password */
50
+ pass?: string;
51
+ /** JetStream stream name (default: "blok-worker") */
52
+ streamName?: string;
53
+ }
54
+ /**
55
+ * NATSWorkerAdapter - NATS JetStream implementation of WorkerAdapter
56
+ */
57
+ export declare class NATSWorkerAdapter implements WorkerAdapter {
58
+ readonly provider: "nats";
59
+ private nc;
60
+ private js;
61
+ private jsm;
62
+ private connected;
63
+ private config;
64
+ private consumers;
65
+ private consumeIterators;
66
+ constructor(config?: Partial<NATSWorkerConfig>);
67
+ /**
68
+ * Connect to NATS and initialize JetStream
69
+ */
70
+ connect(): Promise<void>;
71
+ /**
72
+ * Disconnect from NATS
73
+ */
74
+ disconnect(): Promise<void>;
75
+ /**
76
+ * Start processing jobs from a queue
77
+ */
78
+ process(config: WorkerTriggerOpts, handler: (job: WorkerJob) => Promise<void>): Promise<void>;
79
+ /**
80
+ * Add a job to a worker queue
81
+ */
82
+ addJob(queue: string, data: unknown, opts?: {
83
+ priority?: number;
84
+ delay?: number;
85
+ retries?: number;
86
+ timeout?: number;
87
+ jobId?: string;
88
+ }): Promise<string>;
89
+ /**
90
+ * Stop processing a specific queue
91
+ */
92
+ stopProcessing(queue: string): Promise<void>;
93
+ /**
94
+ * Check if connected
95
+ */
96
+ isConnected(): boolean;
97
+ /**
98
+ * Health check
99
+ */
100
+ healthCheck(): Promise<boolean>;
101
+ /**
102
+ * Get queue statistics from JetStream consumer info
103
+ */
104
+ getQueueStats(queue: string): Promise<WorkerQueueStats>;
105
+ /**
106
+ * Ensure a JetStream stream exists with the given subjects
107
+ */
108
+ private ensureStream;
109
+ }
110
+ export default NATSWorkerAdapter;