@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 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 '@amqp-contract/worker';
23
- import type { Logger } from '@amqp-contract/core';
24
- import { contract } from './contract';
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('Processing order:', message.orderId);
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: ['amqp://localhost'],
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
- return _swan_io_boxed.Future.fromPromise(this.amqpClient.close()).mapError((error) => new TechnicalError("Failed to close AMQP connection", error)).mapOk(() => void 0);
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.handlers[consumerName];
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
- return _swan_io_boxed.Future.fromPromise(this.amqpClient.channel.consume(consumer.queue.name, async (msg) => {
169
- if (msg === null) {
170
- this.logger?.warn("Consumer cancelled by server", {
171
- consumerName: String(consumerName),
172
- queueName: consumer.queue.name
173
- });
174
- return;
175
- }
176
- const parseResult = _swan_io_boxed.Result.fromExecution(() => JSON.parse(msg.content.toString()));
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
- const rawValidation = consumer.message.payload["~standard"].validate(parseResult.value);
187
- await _swan_io_boxed.Future.fromPromise(rawValidation instanceof Promise ? rawValidation : Promise.resolve(rawValidation)).mapOkToResult((validationResult) => {
188
- if (validationResult.issues) return _swan_io_boxed.Result.Error(new MessageValidationError(String(consumerName), validationResult.issues));
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
- }).tapError((error) => {
191
- this.logger?.error("Message validation failed", {
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
- this.amqpClient.channel.nack(msg, false, false);
197
- }).flatMapOk((validatedMessage) => _swan_io_boxed.Future.fromPromise(handler(validatedMessage)).tapError((error) => {
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
- })).mapError((error) => new TechnicalError(`Failed to start consuming for "${String(consumerName)}"`, error)).mapOk(() => void 0);
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 all consumer handlers for a contract
59
+ * Infer consumer handler type for batch processing.
60
+ * Batch handlers receive an array of messages.
58
61
  */
59
- type WorkerInferConsumerHandlers<TContract extends ContractDefinition> = { [K in InferConsumerNames<TContract>]: WorkerInferConsumerHandler<TContract, K> };
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
- * // Define handler outside of worker creation
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
- * // Use the handler in worker
237
- * const worker = await TypedAmqpWorker.create({
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
- * const notifyOrderHandler = defineHandler(
312
+ * // Batch handler - MUST include batchSize
313
+ * const processBatchOrders = defineHandler(
258
314
  * orderContract,
259
- * 'notifyOrder',
260
- * async (message) => {
261
- * await sendNotification(message);
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
- * connection: 'amqp://localhost',
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>): 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