@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.
- 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,277 +0,0 @@
|
|
|
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
|
-
|
|
32
|
-
import type { WorkerTriggerOpts } from "@blokjs/helper";
|
|
33
|
-
import { v4 as uuid } from "uuid";
|
|
34
|
-
import type { WorkerAdapter, WorkerJob, WorkerQueueStats } from "../WorkerTrigger";
|
|
35
|
-
|
|
36
|
-
export interface KafkaConfig {
|
|
37
|
-
brokers: string[];
|
|
38
|
-
clientId: string;
|
|
39
|
-
saslUsername?: string;
|
|
40
|
-
saslPassword?: string;
|
|
41
|
-
ssl: boolean;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
interface KafkaJsHandle {
|
|
45
|
-
producer?: {
|
|
46
|
-
connect: () => Promise<void>;
|
|
47
|
-
disconnect: () => Promise<void>;
|
|
48
|
-
send: (args: unknown) => Promise<unknown>;
|
|
49
|
-
};
|
|
50
|
-
consumers: Map<
|
|
51
|
-
string,
|
|
52
|
-
{
|
|
53
|
-
disconnect: () => Promise<void>;
|
|
54
|
-
stop: () => Promise<void>;
|
|
55
|
-
run: (opts: unknown) => Promise<void>;
|
|
56
|
-
}
|
|
57
|
-
>;
|
|
58
|
-
admin?: {
|
|
59
|
-
connect: () => Promise<void>;
|
|
60
|
-
disconnect: () => Promise<void>;
|
|
61
|
-
fetchTopicOffsets: (topic: string) => Promise<Array<{ partition: number; offset: string }>>;
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
interface QueueStatsCounters {
|
|
66
|
-
completed: number;
|
|
67
|
-
failed: number;
|
|
68
|
-
active: number;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export class KafkaAdapter implements WorkerAdapter {
|
|
72
|
-
readonly provider = "kafka" as const;
|
|
73
|
-
private readonly config: KafkaConfig;
|
|
74
|
-
// biome-ignore lint/suspicious/noExplicitAny: kafkajs's exported `Kafka` constructor is loosely typed.
|
|
75
|
-
private kafka: any = null;
|
|
76
|
-
private handle: KafkaJsHandle = { consumers: new Map() };
|
|
77
|
-
private connected = false;
|
|
78
|
-
private stats: Map<string, QueueStatsCounters> = new Map();
|
|
79
|
-
|
|
80
|
-
constructor(config?: Partial<KafkaConfig>) {
|
|
81
|
-
this.config = {
|
|
82
|
-
brokers: config?.brokers ?? (process.env.KAFKA_BROKERS ?? "localhost:9092").split(",").map((s) => s.trim()),
|
|
83
|
-
clientId: config?.clientId ?? process.env.KAFKA_CLIENT_ID ?? "blok-worker",
|
|
84
|
-
saslUsername: config?.saslUsername ?? process.env.KAFKA_SASL_USERNAME,
|
|
85
|
-
saslPassword: config?.saslPassword ?? process.env.KAFKA_SASL_PASSWORD,
|
|
86
|
-
ssl: config?.ssl ?? process.env.KAFKA_SSL === "true",
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
async connect(): Promise<void> {
|
|
91
|
-
if (this.connected) return;
|
|
92
|
-
try {
|
|
93
|
-
// biome-ignore lint/suspicious/noExplicitAny: kafkajs is a runtime-loaded peer dep.
|
|
94
|
-
const kafkajs: any = await import("kafkajs");
|
|
95
|
-
const sasl =
|
|
96
|
-
this.config.saslUsername && this.config.saslPassword
|
|
97
|
-
? { mechanism: "plain", username: this.config.saslUsername, password: this.config.saslPassword }
|
|
98
|
-
: undefined;
|
|
99
|
-
this.kafka = new kafkajs.Kafka({
|
|
100
|
-
clientId: this.config.clientId,
|
|
101
|
-
brokers: this.config.brokers,
|
|
102
|
-
ssl: this.config.ssl,
|
|
103
|
-
sasl,
|
|
104
|
-
});
|
|
105
|
-
this.handle.producer = this.kafka.producer();
|
|
106
|
-
await this.handle.producer?.connect();
|
|
107
|
-
this.handle.admin = this.kafka.admin();
|
|
108
|
-
await this.handle.admin?.connect();
|
|
109
|
-
this.connected = true;
|
|
110
|
-
} catch (err) {
|
|
111
|
-
throw new Error(
|
|
112
|
-
`[blok][kafka] connect failed: ${(err as Error).message}. Install kafkajs as a peer dependency: bun add kafkajs`,
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
async disconnect(): Promise<void> {
|
|
118
|
-
if (!this.connected) return;
|
|
119
|
-
for (const [, consumer] of this.handle.consumers) {
|
|
120
|
-
try {
|
|
121
|
-
await consumer.disconnect();
|
|
122
|
-
} catch {
|
|
123
|
-
/* ignore */
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
this.handle.consumers.clear();
|
|
127
|
-
try {
|
|
128
|
-
await this.handle.producer?.disconnect();
|
|
129
|
-
} catch {
|
|
130
|
-
/* ignore */
|
|
131
|
-
}
|
|
132
|
-
try {
|
|
133
|
-
await this.handle.admin?.disconnect();
|
|
134
|
-
} catch {
|
|
135
|
-
/* ignore */
|
|
136
|
-
}
|
|
137
|
-
this.connected = false;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
async process(config: WorkerTriggerOpts, handler: (job: WorkerJob) => Promise<void>): Promise<void> {
|
|
141
|
-
if (!this.connected) throw new Error("[blok][kafka] not connected. Call connect() first.");
|
|
142
|
-
const groupId = config.consumerGroup ?? `${config.queue}-group`;
|
|
143
|
-
const consumer = this.kafka.consumer({ groupId });
|
|
144
|
-
await consumer.connect();
|
|
145
|
-
await consumer.subscribe({ topic: config.queue, fromBeginning: config.fromBeginning === true });
|
|
146
|
-
this.handle.consumers.set(config.queue, consumer);
|
|
147
|
-
this.stats.set(config.queue, { completed: 0, failed: 0, active: 0 });
|
|
148
|
-
const stats = this.stats.get(config.queue) as QueueStatsCounters;
|
|
149
|
-
|
|
150
|
-
await consumer.run({
|
|
151
|
-
autoCommit: config.ack !== false,
|
|
152
|
-
eachMessage: async ({
|
|
153
|
-
message,
|
|
154
|
-
}: {
|
|
155
|
-
message: { key?: Buffer; value?: Buffer; offset: string; timestamp: string; headers?: Record<string, Buffer> };
|
|
156
|
-
}) => {
|
|
157
|
-
const payloadString = message.value?.toString("utf8") ?? "";
|
|
158
|
-
let data: unknown;
|
|
159
|
-
try {
|
|
160
|
-
data = payloadString.length > 0 ? JSON.parse(payloadString) : null;
|
|
161
|
-
} catch {
|
|
162
|
-
data = payloadString;
|
|
163
|
-
}
|
|
164
|
-
const headers: Record<string, string> = {};
|
|
165
|
-
if (message.headers) {
|
|
166
|
-
for (const [k, v] of Object.entries(message.headers)) headers[k] = v?.toString("utf8") ?? "";
|
|
167
|
-
}
|
|
168
|
-
const job: WorkerJob = {
|
|
169
|
-
id: message.key?.toString("utf8") ?? `${config.queue}:${message.offset}`,
|
|
170
|
-
data,
|
|
171
|
-
headers,
|
|
172
|
-
queue: config.queue,
|
|
173
|
-
priority: config.priority ?? 0,
|
|
174
|
-
attempts: 0,
|
|
175
|
-
maxRetries: config.retries ?? 0,
|
|
176
|
-
createdAt: new Date(Number.parseInt(message.timestamp, 10)),
|
|
177
|
-
timeout: config.timeout,
|
|
178
|
-
raw: message,
|
|
179
|
-
complete: async () => {
|
|
180
|
-
stats.completed += 1;
|
|
181
|
-
},
|
|
182
|
-
fail: async (_err: Error) => {
|
|
183
|
-
stats.failed += 1;
|
|
184
|
-
throw _err;
|
|
185
|
-
},
|
|
186
|
-
};
|
|
187
|
-
stats.active += 1;
|
|
188
|
-
try {
|
|
189
|
-
await handler(job);
|
|
190
|
-
stats.completed += 1;
|
|
191
|
-
} catch (err) {
|
|
192
|
-
stats.failed += 1;
|
|
193
|
-
throw err;
|
|
194
|
-
} finally {
|
|
195
|
-
stats.active = Math.max(0, stats.active - 1);
|
|
196
|
-
}
|
|
197
|
-
},
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
async addJob(
|
|
202
|
-
queue: string,
|
|
203
|
-
data: unknown,
|
|
204
|
-
opts?: { priority?: number; delay?: number; retries?: number; timeout?: number; jobId?: string },
|
|
205
|
-
): Promise<string> {
|
|
206
|
-
if (!this.connected) throw new Error("[blok][kafka] not connected. Call connect() first.");
|
|
207
|
-
if (!this.handle.producer) throw new Error("[blok][kafka] producer not initialized");
|
|
208
|
-
const key = opts?.jobId ?? uuid();
|
|
209
|
-
const payload = typeof data === "string" ? data : JSON.stringify(data);
|
|
210
|
-
await this.handle.producer.send({
|
|
211
|
-
topic: queue,
|
|
212
|
-
messages: [
|
|
213
|
-
{
|
|
214
|
-
key,
|
|
215
|
-
value: payload,
|
|
216
|
-
headers: opts?.delay ? { "x-blok-delay-ms": String(opts.delay) } : undefined,
|
|
217
|
-
},
|
|
218
|
-
],
|
|
219
|
-
});
|
|
220
|
-
return key;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
async stopProcessing(queue: string): Promise<void> {
|
|
224
|
-
const consumer = this.handle.consumers.get(queue);
|
|
225
|
-
if (consumer) {
|
|
226
|
-
try {
|
|
227
|
-
await consumer.stop();
|
|
228
|
-
} catch {
|
|
229
|
-
/* ignore */
|
|
230
|
-
}
|
|
231
|
-
try {
|
|
232
|
-
await consumer.disconnect();
|
|
233
|
-
} catch {
|
|
234
|
-
/* ignore */
|
|
235
|
-
}
|
|
236
|
-
this.handle.consumers.delete(queue);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
isConnected(): boolean {
|
|
241
|
-
return this.connected;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
async healthCheck(): Promise<boolean> {
|
|
245
|
-
if (!this.connected || !this.handle.admin) return false;
|
|
246
|
-
try {
|
|
247
|
-
await this.handle.admin.fetchTopicOffsets("__consumer_offsets");
|
|
248
|
-
return true;
|
|
249
|
-
} catch {
|
|
250
|
-
return false;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
async getQueueStats(queue: string): Promise<WorkerQueueStats> {
|
|
255
|
-
const counters = this.stats.get(queue) ?? { completed: 0, failed: 0, active: 0 };
|
|
256
|
-
let waiting = 0;
|
|
257
|
-
if (this.handle.admin) {
|
|
258
|
-
try {
|
|
259
|
-
const offsets = await this.handle.admin.fetchTopicOffsets(queue);
|
|
260
|
-
// Approximate: total committed offsets across partitions. Real lag
|
|
261
|
-
// requires admin.fetchOffsets({ groupId }) — skipped here to keep
|
|
262
|
-
// the call cheap; production deployments should use Kafka's
|
|
263
|
-
// dedicated lag metrics anyway.
|
|
264
|
-
waiting = offsets.reduce((sum, p) => sum + Number.parseInt(p.offset, 10), 0);
|
|
265
|
-
} catch {
|
|
266
|
-
waiting = 0;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
return {
|
|
270
|
-
waiting,
|
|
271
|
-
active: counters.active,
|
|
272
|
-
completed: counters.completed,
|
|
273
|
-
failed: counters.failed,
|
|
274
|
-
delayed: 0,
|
|
275
|
-
};
|
|
276
|
-
}
|
|
277
|
-
}
|