@amqp-contract/worker 0.5.0 → 0.7.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.
- package/README.md +20 -6
- package/dist/index.cjs +209 -93
- package/dist/index.d.cts +99 -41
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +99 -41
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +209 -93
- package/dist/index.mjs.map +1 -1
- package/docs/index.md +306 -55
- package/package.json +16 -9
package/README.md
CHANGED
|
@@ -16,12 +16,22 @@
|
|
|
16
16
|
pnpm add @amqp-contract/worker
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
- ✅ **Type-safe message consumption** — Handlers are fully typed based on your contract
|
|
22
|
+
- ✅ **Automatic validation** — Messages are validated before reaching your handlers
|
|
23
|
+
- ✅ **Prefetch configuration** — Control message flow with per-consumer prefetch settings
|
|
24
|
+
- ✅ **Batch processing** — Process multiple messages at once for better throughput
|
|
25
|
+
- ✅ **Automatic reconnection** — Built-in connection management with failover support
|
|
26
|
+
|
|
19
27
|
## Usage
|
|
20
28
|
|
|
29
|
+
### Basic Usage
|
|
30
|
+
|
|
21
31
|
```typescript
|
|
22
|
-
import { TypedAmqpWorker } from
|
|
23
|
-
import type { Logger } from
|
|
24
|
-
import { contract } from
|
|
32
|
+
import { TypedAmqpWorker } from "@amqp-contract/worker";
|
|
33
|
+
import type { Logger } from "@amqp-contract/core";
|
|
34
|
+
import { contract } from "./contract";
|
|
25
35
|
|
|
26
36
|
// Optional: Create a logger implementation
|
|
27
37
|
const logger: Logger = {
|
|
@@ -36,7 +46,7 @@ const worker = await TypedAmqpWorker.create({
|
|
|
36
46
|
contract,
|
|
37
47
|
handlers: {
|
|
38
48
|
processOrder: async (message) => {
|
|
39
|
-
console.log(
|
|
49
|
+
console.log("Processing order:", message.orderId);
|
|
40
50
|
|
|
41
51
|
// Your business logic here
|
|
42
52
|
await processPayment(message);
|
|
@@ -45,7 +55,7 @@ const worker = await TypedAmqpWorker.create({
|
|
|
45
55
|
// If an exception is thrown, the message is automatically requeued
|
|
46
56
|
},
|
|
47
57
|
},
|
|
48
|
-
urls: [
|
|
58
|
+
urls: ["amqp://localhost"],
|
|
49
59
|
logger, // Optional: logs message consumption and errors
|
|
50
60
|
});
|
|
51
61
|
|
|
@@ -55,6 +65,10 @@ const worker = await TypedAmqpWorker.create({
|
|
|
55
65
|
// await worker.close();
|
|
56
66
|
```
|
|
57
67
|
|
|
68
|
+
### Advanced Features
|
|
69
|
+
|
|
70
|
+
For advanced features like prefetch configuration and batch processing, see the [Worker Usage Guide](https://btravers.github.io/amqp-contract/guide/worker-usage).
|
|
71
|
+
|
|
58
72
|
## Defining Handlers Externally
|
|
59
73
|
|
|
60
74
|
You can define handlers outside of the worker creation using `defineHandler` and `defineHandlers` for better code organization. See the [Worker API documentation](https://btravers.github.io/amqp-contract/api/worker) for details.
|
|
@@ -75,7 +89,7 @@ handlers: {
|
|
|
75
89
|
// Message is requeued for retry
|
|
76
90
|
throw error;
|
|
77
91
|
}
|
|
78
|
-
}
|
|
92
|
+
};
|
|
79
93
|
}
|
|
80
94
|
```
|
|
81
95
|
|
package/dist/index.cjs
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
let _amqp_contract_core = require("@amqp-contract/core");
|
|
2
2
|
let _swan_io_boxed = require("@swan-io/boxed");
|
|
3
|
+
let node_zlib = require("node:zlib");
|
|
4
|
+
let node_util = require("node:util");
|
|
3
5
|
|
|
4
6
|
//#region src/errors.ts
|
|
5
7
|
/**
|
|
@@ -36,6 +38,29 @@ var MessageValidationError = class extends WorkerError {
|
|
|
36
38
|
}
|
|
37
39
|
};
|
|
38
40
|
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region src/decompression.ts
|
|
43
|
+
const gunzipAsync = (0, node_util.promisify)(node_zlib.gunzip);
|
|
44
|
+
const inflateAsync = (0, node_util.promisify)(node_zlib.inflate);
|
|
45
|
+
/**
|
|
46
|
+
* Decompress a buffer based on the content-encoding header.
|
|
47
|
+
*
|
|
48
|
+
* @param buffer - The buffer to decompress
|
|
49
|
+
* @param contentEncoding - The content-encoding header value (e.g., 'gzip', 'deflate')
|
|
50
|
+
* @returns A promise that resolves to the decompressed buffer
|
|
51
|
+
* @throws Error if decompression fails or if the encoding is unsupported
|
|
52
|
+
*
|
|
53
|
+
* @internal
|
|
54
|
+
*/
|
|
55
|
+
async function decompressBuffer(buffer, contentEncoding) {
|
|
56
|
+
if (!contentEncoding) return buffer;
|
|
57
|
+
switch (contentEncoding.toLowerCase()) {
|
|
58
|
+
case "gzip": return gunzipAsync(buffer);
|
|
59
|
+
case "deflate": return inflateAsync(buffer);
|
|
60
|
+
default: throw new Error(`Unsupported content-encoding: ${contentEncoding}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
39
64
|
//#endregion
|
|
40
65
|
//#region src/worker.ts
|
|
41
66
|
/**
|
|
@@ -79,11 +104,23 @@ var MessageValidationError = class extends WorkerError {
|
|
|
79
104
|
* ```
|
|
80
105
|
*/
|
|
81
106
|
var TypedAmqpWorker = class TypedAmqpWorker {
|
|
107
|
+
actualHandlers;
|
|
108
|
+
consumerOptions;
|
|
109
|
+
batchTimers = /* @__PURE__ */ new Map();
|
|
110
|
+
consumerTags = /* @__PURE__ */ new Set();
|
|
82
111
|
constructor(contract, amqpClient, handlers, logger) {
|
|
83
112
|
this.contract = contract;
|
|
84
113
|
this.amqpClient = amqpClient;
|
|
85
|
-
this.handlers = handlers;
|
|
86
114
|
this.logger = logger;
|
|
115
|
+
this.actualHandlers = {};
|
|
116
|
+
this.consumerOptions = {};
|
|
117
|
+
for (const consumerName of Object.keys(handlers)) {
|
|
118
|
+
const handlerEntry = handlers[consumerName];
|
|
119
|
+
if (Array.isArray(handlerEntry)) {
|
|
120
|
+
this.actualHandlers[consumerName] = handlerEntry[0];
|
|
121
|
+
this.consumerOptions[consumerName] = handlerEntry[1];
|
|
122
|
+
} else this.actualHandlers[consumerName] = handlerEntry;
|
|
123
|
+
}
|
|
87
124
|
}
|
|
88
125
|
/**
|
|
89
126
|
* Create a type-safe AMQP worker from a contract.
|
|
@@ -138,7 +175,17 @@ var TypedAmqpWorker = class TypedAmqpWorker {
|
|
|
138
175
|
* ```
|
|
139
176
|
*/
|
|
140
177
|
close() {
|
|
141
|
-
|
|
178
|
+
for (const timer of this.batchTimers.values()) clearTimeout(timer);
|
|
179
|
+
this.batchTimers.clear();
|
|
180
|
+
return _swan_io_boxed.Future.all(Array.from(this.consumerTags).map((consumerTag) => _swan_io_boxed.Future.fromPromise(this.amqpClient.channel.cancel(consumerTag)).mapErrorToResult((error) => {
|
|
181
|
+
this.logger?.warn("Failed to cancel consumer during close", {
|
|
182
|
+
consumerTag,
|
|
183
|
+
error
|
|
184
|
+
});
|
|
185
|
+
return _swan_io_boxed.Result.Ok(void 0);
|
|
186
|
+
}))).map(_swan_io_boxed.Result.all).tapOk(() => {
|
|
187
|
+
this.consumerTags.clear();
|
|
188
|
+
}).flatMapOk(() => _swan_io_boxed.Future.fromPromise(this.amqpClient.close())).mapError((error) => new TechnicalError("Failed to close AMQP connection", error)).mapOk(() => void 0);
|
|
142
189
|
}
|
|
143
190
|
/**
|
|
144
191
|
* Start consuming messages for all consumers
|
|
@@ -146,6 +193,21 @@ var TypedAmqpWorker = class TypedAmqpWorker {
|
|
|
146
193
|
consumeAll() {
|
|
147
194
|
if (!this.contract.consumers) return _swan_io_boxed.Future.value(_swan_io_boxed.Result.Error(new TechnicalError("No consumers defined in contract")));
|
|
148
195
|
const consumerNames = Object.keys(this.contract.consumers);
|
|
196
|
+
let maxPrefetch = 0;
|
|
197
|
+
for (const consumerName of consumerNames) {
|
|
198
|
+
const options = this.consumerOptions[consumerName];
|
|
199
|
+
if (options?.prefetch !== void 0) {
|
|
200
|
+
if (options.prefetch <= 0 || !Number.isInteger(options.prefetch)) return _swan_io_boxed.Future.value(_swan_io_boxed.Result.Error(new TechnicalError(`Invalid prefetch value for "${String(consumerName)}": must be a positive integer`)));
|
|
201
|
+
maxPrefetch = Math.max(maxPrefetch, options.prefetch);
|
|
202
|
+
}
|
|
203
|
+
if (options?.batchSize !== void 0) {
|
|
204
|
+
const effectivePrefetch = options.prefetch ?? options.batchSize;
|
|
205
|
+
maxPrefetch = Math.max(maxPrefetch, effectivePrefetch);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (maxPrefetch > 0) this.amqpClient.channel.addSetup(async (channel) => {
|
|
209
|
+
await channel.prefetch(maxPrefetch);
|
|
210
|
+
});
|
|
149
211
|
return _swan_io_boxed.Future.all(consumerNames.map((consumerName) => this.consume(consumerName))).map(_swan_io_boxed.Result.all).mapOk(() => void 0);
|
|
150
212
|
}
|
|
151
213
|
waitForConnectionReady() {
|
|
@@ -163,17 +225,34 @@ var TypedAmqpWorker = class TypedAmqpWorker {
|
|
|
163
225
|
const available = availableConsumers.length > 0 ? availableConsumers.join(", ") : "none";
|
|
164
226
|
return _swan_io_boxed.Future.value(_swan_io_boxed.Result.Error(new TechnicalError(`Consumer not found: "${String(consumerName)}". Available consumers: ${available}`)));
|
|
165
227
|
}
|
|
166
|
-
const handler = this.
|
|
228
|
+
const handler = this.actualHandlers[consumerName];
|
|
167
229
|
if (!handler) return _swan_io_boxed.Future.value(_swan_io_boxed.Result.Error(new TechnicalError(`Handler for "${String(consumerName)}" not provided`)));
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
230
|
+
const options = this.consumerOptions[consumerName] ?? {};
|
|
231
|
+
if (options.batchSize !== void 0) {
|
|
232
|
+
if (options.batchSize <= 0 || !Number.isInteger(options.batchSize)) return _swan_io_boxed.Future.value(_swan_io_boxed.Result.Error(new TechnicalError(`Invalid batchSize for "${String(consumerName)}": must be a positive integer`)));
|
|
233
|
+
}
|
|
234
|
+
if (options.batchTimeout !== void 0) {
|
|
235
|
+
if (typeof options.batchTimeout !== "number" || !Number.isFinite(options.batchTimeout) || options.batchTimeout <= 0) return _swan_io_boxed.Future.value(_swan_io_boxed.Result.Error(new TechnicalError(`Invalid batchTimeout for "${String(consumerName)}": must be a positive number`)));
|
|
236
|
+
}
|
|
237
|
+
if (options.batchSize !== void 0 && options.batchSize > 0) return this.consumeBatch(consumerName, consumer, options, handler);
|
|
238
|
+
else return this.consumeSingle(consumerName, consumer, handler);
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Parse and validate a message from AMQP
|
|
242
|
+
* @returns Future<Result<validated message, void>> - Ok with validated message, or Error (already handled with nack)
|
|
243
|
+
*/
|
|
244
|
+
parseAndValidateMessage(msg, consumer, consumerName) {
|
|
245
|
+
const decompressMessage = _swan_io_boxed.Future.fromPromise(decompressBuffer(msg.content, msg.properties.contentEncoding)).mapError((error) => {
|
|
246
|
+
this.logger?.error("Error decompressing message", {
|
|
247
|
+
consumerName: String(consumerName),
|
|
248
|
+
queueName: consumer.queue.name,
|
|
249
|
+
contentEncoding: msg.properties.contentEncoding,
|
|
250
|
+
error
|
|
251
|
+
});
|
|
252
|
+
this.amqpClient.channel.nack(msg, false, false);
|
|
253
|
+
});
|
|
254
|
+
const parseMessage = (buffer) => {
|
|
255
|
+
const parseResult = _swan_io_boxed.Result.fromExecution(() => JSON.parse(buffer.toString()));
|
|
177
256
|
if (parseResult.isError()) {
|
|
178
257
|
this.logger?.error("Error parsing message", {
|
|
179
258
|
consumerName: String(consumerName),
|
|
@@ -181,20 +260,41 @@ var TypedAmqpWorker = class TypedAmqpWorker {
|
|
|
181
260
|
error: parseResult.error
|
|
182
261
|
});
|
|
183
262
|
this.amqpClient.channel.nack(msg, false, false);
|
|
184
|
-
return;
|
|
263
|
+
return _swan_io_boxed.Future.value(_swan_io_boxed.Result.Error(void 0));
|
|
185
264
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
265
|
+
return _swan_io_boxed.Future.value(_swan_io_boxed.Result.Ok(parseResult.value));
|
|
266
|
+
};
|
|
267
|
+
const validateMessage = (parsedMessage) => {
|
|
268
|
+
const rawValidation = consumer.message.payload["~standard"].validate(parsedMessage);
|
|
269
|
+
return _swan_io_boxed.Future.fromPromise(rawValidation instanceof Promise ? rawValidation : Promise.resolve(rawValidation)).mapOkToResult((validationResult) => {
|
|
270
|
+
if (validationResult.issues) {
|
|
271
|
+
const error = new MessageValidationError(String(consumerName), validationResult.issues);
|
|
272
|
+
this.logger?.error("Message validation failed", {
|
|
273
|
+
consumerName: String(consumerName),
|
|
274
|
+
queueName: consumer.queue.name,
|
|
275
|
+
error
|
|
276
|
+
});
|
|
277
|
+
this.amqpClient.channel.nack(msg, false, false);
|
|
278
|
+
return _swan_io_boxed.Result.Error(void 0);
|
|
279
|
+
}
|
|
189
280
|
return _swan_io_boxed.Result.Ok(validationResult.value);
|
|
190
|
-
})
|
|
191
|
-
|
|
281
|
+
});
|
|
282
|
+
};
|
|
283
|
+
return decompressMessage.flatMapOk(parseMessage).flatMapOk(validateMessage);
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Consume messages one at a time
|
|
287
|
+
*/
|
|
288
|
+
consumeSingle(consumerName, consumer, handler) {
|
|
289
|
+
return _swan_io_boxed.Future.fromPromise(this.amqpClient.channel.consume(consumer.queue.name, async (msg) => {
|
|
290
|
+
if (msg === null) {
|
|
291
|
+
this.logger?.warn("Consumer cancelled by server", {
|
|
192
292
|
consumerName: String(consumerName),
|
|
193
|
-
queueName: consumer.queue.name
|
|
194
|
-
error
|
|
293
|
+
queueName: consumer.queue.name
|
|
195
294
|
});
|
|
196
|
-
|
|
197
|
-
}
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
await this.parseAndValidateMessage(msg, consumer, consumerName).flatMapOk((validatedMessage) => _swan_io_boxed.Future.fromPromise(handler(validatedMessage)).tapError((error) => {
|
|
198
298
|
this.logger?.error("Error processing message", {
|
|
199
299
|
consumerName: String(consumerName),
|
|
200
300
|
queueName: consumer.queue.name,
|
|
@@ -208,88 +308,104 @@ var TypedAmqpWorker = class TypedAmqpWorker {
|
|
|
208
308
|
});
|
|
209
309
|
this.amqpClient.channel.ack(msg);
|
|
210
310
|
}).toPromise();
|
|
211
|
-
})).
|
|
311
|
+
})).tapOk((reply) => {
|
|
312
|
+
this.consumerTags.add(reply.consumerTag);
|
|
313
|
+
}).mapError((error) => new TechnicalError(`Failed to start consuming for "${String(consumerName)}"`, error)).mapOk(() => void 0);
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Consume messages in batches
|
|
317
|
+
*/
|
|
318
|
+
consumeBatch(consumerName, consumer, options, handler) {
|
|
319
|
+
const batchSize = options.batchSize;
|
|
320
|
+
const batchTimeout = options.batchTimeout ?? 1e3;
|
|
321
|
+
const timerKey = String(consumerName);
|
|
322
|
+
let batch = [];
|
|
323
|
+
let isProcessing = false;
|
|
324
|
+
const processBatch = async () => {
|
|
325
|
+
if (isProcessing || batch.length === 0) return;
|
|
326
|
+
isProcessing = true;
|
|
327
|
+
const currentBatch = batch;
|
|
328
|
+
batch = [];
|
|
329
|
+
const timer = this.batchTimers.get(timerKey);
|
|
330
|
+
if (timer) {
|
|
331
|
+
clearTimeout(timer);
|
|
332
|
+
this.batchTimers.delete(timerKey);
|
|
333
|
+
}
|
|
334
|
+
const messages = currentBatch.map((item) => item.message);
|
|
335
|
+
this.logger?.info("Processing batch", {
|
|
336
|
+
consumerName: String(consumerName),
|
|
337
|
+
queueName: consumer.queue.name,
|
|
338
|
+
batchSize: currentBatch.length
|
|
339
|
+
});
|
|
340
|
+
try {
|
|
341
|
+
await handler(messages);
|
|
342
|
+
for (const item of currentBatch) this.amqpClient.channel.ack(item.amqpMessage);
|
|
343
|
+
this.logger?.info("Batch processed successfully", {
|
|
344
|
+
consumerName: String(consumerName),
|
|
345
|
+
queueName: consumer.queue.name,
|
|
346
|
+
batchSize: currentBatch.length
|
|
347
|
+
});
|
|
348
|
+
} catch (error) {
|
|
349
|
+
this.logger?.error("Error processing batch", {
|
|
350
|
+
consumerName: String(consumerName),
|
|
351
|
+
queueName: consumer.queue.name,
|
|
352
|
+
batchSize: currentBatch.length,
|
|
353
|
+
error
|
|
354
|
+
});
|
|
355
|
+
for (const item of currentBatch) this.amqpClient.channel.nack(item.amqpMessage, false, true);
|
|
356
|
+
} finally {
|
|
357
|
+
isProcessing = false;
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
const scheduleBatchProcessing = () => {
|
|
361
|
+
if (isProcessing) return;
|
|
362
|
+
const existingTimer = this.batchTimers.get(timerKey);
|
|
363
|
+
if (existingTimer) clearTimeout(existingTimer);
|
|
364
|
+
const timer = setTimeout(() => {
|
|
365
|
+
processBatch().catch((error) => {
|
|
366
|
+
this.logger?.error("Unexpected error in batch processing", {
|
|
367
|
+
consumerName: String(consumerName),
|
|
368
|
+
error
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
}, batchTimeout);
|
|
372
|
+
this.batchTimers.set(timerKey, timer);
|
|
373
|
+
};
|
|
374
|
+
return _swan_io_boxed.Future.fromPromise(this.amqpClient.channel.consume(consumer.queue.name, async (msg) => {
|
|
375
|
+
if (msg === null) {
|
|
376
|
+
this.logger?.warn("Consumer cancelled by server", {
|
|
377
|
+
consumerName: String(consumerName),
|
|
378
|
+
queueName: consumer.queue.name
|
|
379
|
+
});
|
|
380
|
+
await processBatch();
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
const validationResult = await this.parseAndValidateMessage(msg, consumer, consumerName).toPromise();
|
|
384
|
+
if (validationResult.isError()) return;
|
|
385
|
+
batch.push({
|
|
386
|
+
message: validationResult.value,
|
|
387
|
+
amqpMessage: msg
|
|
388
|
+
});
|
|
389
|
+
if (batch.length >= batchSize) {
|
|
390
|
+
await processBatch();
|
|
391
|
+
if (batch.length > 0 && !this.batchTimers.has(timerKey)) scheduleBatchProcessing();
|
|
392
|
+
} else if (!this.batchTimers.has(timerKey)) scheduleBatchProcessing();
|
|
393
|
+
})).tapOk((reply) => {
|
|
394
|
+
this.consumerTags.add(reply.consumerTag);
|
|
395
|
+
}).mapError((error) => new TechnicalError(`Failed to start consuming for "${String(consumerName)}"`, error)).mapOk(() => void 0);
|
|
212
396
|
}
|
|
213
397
|
};
|
|
214
398
|
|
|
215
399
|
//#endregion
|
|
216
400
|
//#region src/handlers.ts
|
|
217
|
-
|
|
218
|
-
* Define a type-safe handler for a specific consumer in a contract.
|
|
219
|
-
*
|
|
220
|
-
* This utility allows you to define handlers outside of the worker creation,
|
|
221
|
-
* providing better code organization and reusability.
|
|
222
|
-
*
|
|
223
|
-
* @template TContract - The contract definition type
|
|
224
|
-
* @template TName - The consumer name from the contract
|
|
225
|
-
* @param contract - The contract definition containing the consumer
|
|
226
|
-
* @param consumerName - The name of the consumer from the contract
|
|
227
|
-
* @param handler - The async handler function that processes messages
|
|
228
|
-
* @returns A type-safe handler that can be used with TypedAmqpWorker
|
|
229
|
-
*
|
|
230
|
-
* @example
|
|
231
|
-
* ```typescript
|
|
232
|
-
* import { defineHandler } from '@amqp-contract/worker';
|
|
233
|
-
* import { orderContract } from './contract';
|
|
234
|
-
*
|
|
235
|
-
* // Define handler outside of worker creation
|
|
236
|
-
* const processOrderHandler = defineHandler(
|
|
237
|
-
* orderContract,
|
|
238
|
-
* 'processOrder',
|
|
239
|
-
* async (message) => {
|
|
240
|
-
* // message is fully typed based on the contract
|
|
241
|
-
* console.log('Processing order:', message.orderId);
|
|
242
|
-
* await processPayment(message);
|
|
243
|
-
* }
|
|
244
|
-
* );
|
|
245
|
-
*
|
|
246
|
-
* // Use the handler in worker
|
|
247
|
-
* const worker = await TypedAmqpWorker.create({
|
|
248
|
-
* contract: orderContract,
|
|
249
|
-
* handlers: {
|
|
250
|
-
* processOrder: processOrderHandler,
|
|
251
|
-
* },
|
|
252
|
-
* connection: 'amqp://localhost',
|
|
253
|
-
* });
|
|
254
|
-
* ```
|
|
255
|
-
*
|
|
256
|
-
* @example
|
|
257
|
-
* ```typescript
|
|
258
|
-
* // Define multiple handlers
|
|
259
|
-
* const processOrderHandler = defineHandler(
|
|
260
|
-
* orderContract,
|
|
261
|
-
* 'processOrder',
|
|
262
|
-
* async (message) => {
|
|
263
|
-
* await processOrder(message);
|
|
264
|
-
* }
|
|
265
|
-
* );
|
|
266
|
-
*
|
|
267
|
-
* const notifyOrderHandler = defineHandler(
|
|
268
|
-
* orderContract,
|
|
269
|
-
* 'notifyOrder',
|
|
270
|
-
* async (message) => {
|
|
271
|
-
* await sendNotification(message);
|
|
272
|
-
* }
|
|
273
|
-
* );
|
|
274
|
-
*
|
|
275
|
-
* // Compose handlers
|
|
276
|
-
* const worker = await TypedAmqpWorker.create({
|
|
277
|
-
* contract: orderContract,
|
|
278
|
-
* handlers: {
|
|
279
|
-
* processOrder: processOrderHandler,
|
|
280
|
-
* notifyOrder: notifyOrderHandler,
|
|
281
|
-
* },
|
|
282
|
-
* connection: 'amqp://localhost',
|
|
283
|
-
* });
|
|
284
|
-
* ```
|
|
285
|
-
*/
|
|
286
|
-
function defineHandler(contract, consumerName, handler) {
|
|
401
|
+
function defineHandler(contract, consumerName, handler, options) {
|
|
287
402
|
const consumers = contract.consumers;
|
|
288
403
|
if (!consumers || !(consumerName in consumers)) {
|
|
289
404
|
const availableConsumers = consumers ? Object.keys(consumers) : [];
|
|
290
405
|
const available = availableConsumers.length > 0 ? availableConsumers.join(", ") : "none";
|
|
291
406
|
throw new Error(`Consumer "${String(consumerName)}" not found in contract. Available consumers: ${available}`);
|
|
292
407
|
}
|
|
408
|
+
if (options) return [handler, options];
|
|
293
409
|
return handler;
|
|
294
410
|
}
|
|
295
411
|
/**
|
package/dist/index.d.cts
CHANGED
|
@@ -50,13 +50,38 @@ type InferConsumer<TContract extends ContractDefinition, TName extends InferCons
|
|
|
50
50
|
*/
|
|
51
51
|
type WorkerInferConsumerInput<TContract extends ContractDefinition, TName extends InferConsumerNames<TContract>> = ConsumerInferInput<InferConsumer<TContract, TName>>;
|
|
52
52
|
/**
|
|
53
|
-
* Infer consumer handler type for a specific consumer
|
|
53
|
+
* Infer consumer handler type for a specific consumer.
|
|
54
|
+
* Handlers always receive a single message by default.
|
|
55
|
+
* For batch processing, use consumerOptions to configure batch behavior.
|
|
54
56
|
*/
|
|
55
57
|
type WorkerInferConsumerHandler<TContract extends ContractDefinition, TName extends InferConsumerNames<TContract>> = (message: WorkerInferConsumerInput<TContract, TName>) => Promise<void>;
|
|
56
58
|
/**
|
|
57
|
-
* Infer
|
|
59
|
+
* Infer consumer handler type for batch processing.
|
|
60
|
+
* Batch handlers receive an array of messages.
|
|
58
61
|
*/
|
|
59
|
-
type
|
|
62
|
+
type WorkerInferConsumerBatchHandler<TContract extends ContractDefinition, TName extends InferConsumerNames<TContract>> = (messages: Array<WorkerInferConsumerInput<TContract, TName>>) => Promise<void>;
|
|
63
|
+
/**
|
|
64
|
+
* Infer handler entry for a consumer - either a function or a tuple of [handler, options].
|
|
65
|
+
*
|
|
66
|
+
* Three patterns are supported:
|
|
67
|
+
* 1. Simple handler: `async (message) => { ... }`
|
|
68
|
+
* 2. Handler with prefetch: `[async (message) => { ... }, { prefetch: 10 }]`
|
|
69
|
+
* 3. Batch handler: `[async (messages) => { ... }, { batchSize: 5, batchTimeout: 1000 }]`
|
|
70
|
+
*/
|
|
71
|
+
type WorkerInferConsumerHandlerEntry<TContract extends ContractDefinition, TName extends InferConsumerNames<TContract>> = WorkerInferConsumerHandler<TContract, TName> | readonly [WorkerInferConsumerHandler<TContract, TName>, {
|
|
72
|
+
prefetch?: number;
|
|
73
|
+
batchSize?: never;
|
|
74
|
+
batchTimeout?: never;
|
|
75
|
+
}] | readonly [WorkerInferConsumerBatchHandler<TContract, TName>, {
|
|
76
|
+
prefetch?: number;
|
|
77
|
+
batchSize: number;
|
|
78
|
+
batchTimeout?: number;
|
|
79
|
+
}];
|
|
80
|
+
/**
|
|
81
|
+
* Infer all consumer handlers for a contract.
|
|
82
|
+
* Handlers can be either single-message handlers, batch handlers, or a tuple of [handler, options].
|
|
83
|
+
*/
|
|
84
|
+
type WorkerInferConsumerHandlers<TContract extends ContractDefinition> = { [K in InferConsumerNames<TContract>]: WorkerInferConsumerHandlerEntry<TContract, K> };
|
|
60
85
|
//#endregion
|
|
61
86
|
//#region src/worker.d.ts
|
|
62
87
|
/**
|
|
@@ -69,9 +94,24 @@ type WorkerInferConsumerHandlers<TContract extends ContractDefinition> = { [K in
|
|
|
69
94
|
* const options: CreateWorkerOptions<typeof contract> = {
|
|
70
95
|
* contract: myContract,
|
|
71
96
|
* handlers: {
|
|
97
|
+
* // Simple handler
|
|
72
98
|
* processOrder: async (message) => {
|
|
73
99
|
* console.log('Processing order:', message.orderId);
|
|
74
|
-
* }
|
|
100
|
+
* },
|
|
101
|
+
* // Handler with options (prefetch)
|
|
102
|
+
* processPayment: [
|
|
103
|
+
* async (message) => {
|
|
104
|
+
* console.log('Processing payment:', message.paymentId);
|
|
105
|
+
* },
|
|
106
|
+
* { prefetch: 10 }
|
|
107
|
+
* ],
|
|
108
|
+
* // Handler with batch processing
|
|
109
|
+
* processNotifications: [
|
|
110
|
+
* async (messages) => {
|
|
111
|
+
* console.log('Processing batch:', messages.length);
|
|
112
|
+
* },
|
|
113
|
+
* { batchSize: 5, batchTimeout: 1000 }
|
|
114
|
+
* ]
|
|
75
115
|
* },
|
|
76
116
|
* urls: ['amqp://localhost'],
|
|
77
117
|
* connectionOptions: {
|
|
@@ -84,7 +124,7 @@ type WorkerInferConsumerHandlers<TContract extends ContractDefinition> = { [K in
|
|
|
84
124
|
type CreateWorkerOptions<TContract extends ContractDefinition> = {
|
|
85
125
|
/** The AMQP contract definition specifying consumers and their message schemas */
|
|
86
126
|
contract: TContract;
|
|
87
|
-
/** Handlers for each consumer defined in the contract */
|
|
127
|
+
/** Handlers for each consumer defined in the contract. Can be a function or a tuple of [handler, options] */
|
|
88
128
|
handlers: WorkerInferConsumerHandlers<TContract>;
|
|
89
129
|
/** AMQP broker URL(s). Multiple URLs provide failover support */
|
|
90
130
|
urls: ConnectionUrl[];
|
|
@@ -136,8 +176,11 @@ type CreateWorkerOptions<TContract extends ContractDefinition> = {
|
|
|
136
176
|
declare class TypedAmqpWorker<TContract extends ContractDefinition> {
|
|
137
177
|
private readonly contract;
|
|
138
178
|
private readonly amqpClient;
|
|
139
|
-
private readonly handlers;
|
|
140
179
|
private readonly logger?;
|
|
180
|
+
private readonly actualHandlers;
|
|
181
|
+
private readonly consumerOptions;
|
|
182
|
+
private readonly batchTimers;
|
|
183
|
+
private readonly consumerTags;
|
|
141
184
|
private constructor();
|
|
142
185
|
/**
|
|
143
186
|
* Create a type-safe AMQP worker from a contract.
|
|
@@ -201,6 +244,19 @@ declare class TypedAmqpWorker<TContract extends ContractDefinition> {
|
|
|
201
244
|
* Start consuming messages for a specific consumer
|
|
202
245
|
*/
|
|
203
246
|
private consume;
|
|
247
|
+
/**
|
|
248
|
+
* Parse and validate a message from AMQP
|
|
249
|
+
* @returns Future<Result<validated message, void>> - Ok with validated message, or Error (already handled with nack)
|
|
250
|
+
*/
|
|
251
|
+
private parseAndValidateMessage;
|
|
252
|
+
/**
|
|
253
|
+
* Consume messages one at a time
|
|
254
|
+
*/
|
|
255
|
+
private consumeSingle;
|
|
256
|
+
/**
|
|
257
|
+
* Consume messages in batches
|
|
258
|
+
*/
|
|
259
|
+
private consumeBatch;
|
|
204
260
|
}
|
|
205
261
|
//#endregion
|
|
206
262
|
//#region src/handlers.d.ts
|
|
@@ -210,11 +266,22 @@ declare class TypedAmqpWorker<TContract extends ContractDefinition> {
|
|
|
210
266
|
* This utility allows you to define handlers outside of the worker creation,
|
|
211
267
|
* providing better code organization and reusability.
|
|
212
268
|
*
|
|
269
|
+
* Supports three patterns:
|
|
270
|
+
* 1. Simple handler: just the function (single message handler)
|
|
271
|
+
* 2. Handler with prefetch: [handler, { prefetch: 10 }] (single message handler with config)
|
|
272
|
+
* 3. Batch handler: [batchHandler, { batchSize: 5, batchTimeout: 1000 }] (REQUIRES batchSize config)
|
|
273
|
+
*
|
|
274
|
+
* **Important**: Batch handlers (handlers that accept an array of messages) MUST include
|
|
275
|
+
* batchSize configuration. You cannot create a batch handler without specifying batchSize.
|
|
276
|
+
*
|
|
213
277
|
* @template TContract - The contract definition type
|
|
214
278
|
* @template TName - The consumer name from the contract
|
|
215
279
|
* @param contract - The contract definition containing the consumer
|
|
216
280
|
* @param consumerName - The name of the consumer from the contract
|
|
217
|
-
* @param handler - The async handler function that processes messages
|
|
281
|
+
* @param handler - The async handler function that processes messages (single or batch)
|
|
282
|
+
* @param options - Optional consumer options (prefetch, batchSize, batchTimeout)
|
|
283
|
+
* - For single-message handlers: { prefetch?: number } is optional
|
|
284
|
+
* - For batch handlers: { batchSize: number, batchTimeout?: number } is REQUIRED
|
|
218
285
|
* @returns A type-safe handler that can be used with TypedAmqpWorker
|
|
219
286
|
*
|
|
220
287
|
* @example
|
|
@@ -222,58 +289,49 @@ declare class TypedAmqpWorker<TContract extends ContractDefinition> {
|
|
|
222
289
|
* import { defineHandler } from '@amqp-contract/worker';
|
|
223
290
|
* import { orderContract } from './contract';
|
|
224
291
|
*
|
|
225
|
-
* //
|
|
292
|
+
* // Simple single-message handler without options
|
|
226
293
|
* const processOrderHandler = defineHandler(
|
|
227
294
|
* orderContract,
|
|
228
295
|
* 'processOrder',
|
|
229
296
|
* async (message) => {
|
|
230
|
-
* // message is fully typed based on the contract
|
|
231
297
|
* console.log('Processing order:', message.orderId);
|
|
232
298
|
* await processPayment(message);
|
|
233
299
|
* }
|
|
234
300
|
* );
|
|
235
301
|
*
|
|
236
|
-
* //
|
|
237
|
-
* const
|
|
238
|
-
* contract: orderContract,
|
|
239
|
-
* handlers: {
|
|
240
|
-
* processOrder: processOrderHandler,
|
|
241
|
-
* },
|
|
242
|
-
* connection: 'amqp://localhost',
|
|
243
|
-
* });
|
|
244
|
-
* ```
|
|
245
|
-
*
|
|
246
|
-
* @example
|
|
247
|
-
* ```typescript
|
|
248
|
-
* // Define multiple handlers
|
|
249
|
-
* const processOrderHandler = defineHandler(
|
|
302
|
+
* // Single-message handler with prefetch
|
|
303
|
+
* const processOrderWithPrefetch = defineHandler(
|
|
250
304
|
* orderContract,
|
|
251
305
|
* 'processOrder',
|
|
252
306
|
* async (message) => {
|
|
253
307
|
* await processOrder(message);
|
|
254
|
-
* }
|
|
308
|
+
* },
|
|
309
|
+
* { prefetch: 10 }
|
|
255
310
|
* );
|
|
256
311
|
*
|
|
257
|
-
*
|
|
312
|
+
* // Batch handler - MUST include batchSize
|
|
313
|
+
* const processBatchOrders = defineHandler(
|
|
258
314
|
* orderContract,
|
|
259
|
-
* '
|
|
260
|
-
* async (
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
* );
|
|
264
|
-
*
|
|
265
|
-
* // Compose handlers
|
|
266
|
-
* const worker = await TypedAmqpWorker.create({
|
|
267
|
-
* contract: orderContract,
|
|
268
|
-
* handlers: {
|
|
269
|
-
* processOrder: processOrderHandler,
|
|
270
|
-
* notifyOrder: notifyOrderHandler,
|
|
315
|
+
* 'processOrders',
|
|
316
|
+
* async (messages) => {
|
|
317
|
+
* // messages is an array - batchSize configuration is REQUIRED
|
|
318
|
+
* await db.insertMany(messages);
|
|
271
319
|
* },
|
|
272
|
-
*
|
|
273
|
-
*
|
|
320
|
+
* { batchSize: 5, batchTimeout: 1000 }
|
|
321
|
+
* );
|
|
274
322
|
* ```
|
|
275
323
|
*/
|
|
276
|
-
declare function defineHandler<TContract extends ContractDefinition, TName extends InferConsumerNames<TContract>>(contract: TContract, consumerName: TName, handler: WorkerInferConsumerHandler<TContract, TName>):
|
|
324
|
+
declare function defineHandler<TContract extends ContractDefinition, TName extends InferConsumerNames<TContract>>(contract: TContract, consumerName: TName, handler: WorkerInferConsumerHandler<TContract, TName>): WorkerInferConsumerHandlerEntry<TContract, TName>;
|
|
325
|
+
declare function defineHandler<TContract extends ContractDefinition, TName extends InferConsumerNames<TContract>>(contract: TContract, consumerName: TName, handler: WorkerInferConsumerHandler<TContract, TName>, options: {
|
|
326
|
+
prefetch?: number;
|
|
327
|
+
batchSize?: never;
|
|
328
|
+
batchTimeout?: never;
|
|
329
|
+
}): WorkerInferConsumerHandlerEntry<TContract, TName>;
|
|
330
|
+
declare function defineHandler<TContract extends ContractDefinition, TName extends InferConsumerNames<TContract>>(contract: TContract, consumerName: TName, handler: WorkerInferConsumerBatchHandler<TContract, TName>, options: {
|
|
331
|
+
prefetch?: number;
|
|
332
|
+
batchSize: number;
|
|
333
|
+
batchTimeout?: number;
|
|
334
|
+
}): WorkerInferConsumerHandlerEntry<TContract, TName>;
|
|
277
335
|
/**
|
|
278
336
|
* Define multiple type-safe handlers for consumers in a contract.
|
|
279
337
|
*
|
|
@@ -332,5 +390,5 @@ declare function defineHandler<TContract extends ContractDefinition, TName exten
|
|
|
332
390
|
*/
|
|
333
391
|
declare function defineHandlers<TContract extends ContractDefinition>(contract: TContract, handlers: WorkerInferConsumerHandlers<TContract>): WorkerInferConsumerHandlers<TContract>;
|
|
334
392
|
//#endregion
|
|
335
|
-
export { type CreateWorkerOptions, MessageValidationError, TechnicalError, TypedAmqpWorker, type WorkerInferConsumerHandler, type WorkerInferConsumerHandlers, type WorkerInferConsumerInput, defineHandler, defineHandlers };
|
|
393
|
+
export { type CreateWorkerOptions, MessageValidationError, TechnicalError, TypedAmqpWorker, type WorkerInferConsumerBatchHandler, type WorkerInferConsumerHandler, type WorkerInferConsumerHandlerEntry, type WorkerInferConsumerHandlers, type WorkerInferConsumerInput, defineHandler, defineHandlers };
|
|
336
394
|
//# sourceMappingURL=index.d.cts.map
|