@amqp-contract/worker 0.5.0 → 0.6.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/dist/index.d.mts 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,10 @@ 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;
141
183
  private constructor();
142
184
  /**
143
185
  * Create a type-safe AMQP worker from a contract.
@@ -201,6 +243,19 @@ declare class TypedAmqpWorker<TContract extends ContractDefinition> {
201
243
  * Start consuming messages for a specific consumer
202
244
  */
203
245
  private consume;
246
+ /**
247
+ * Parse and validate a message from AMQP
248
+ * @returns Future<Result<validated message, void>> - Ok with validated message, or Error (already handled with nack)
249
+ */
250
+ private parseAndValidateMessage;
251
+ /**
252
+ * Consume messages one at a time
253
+ */
254
+ private consumeSingle;
255
+ /**
256
+ * Consume messages in batches
257
+ */
258
+ private consumeBatch;
204
259
  }
205
260
  //#endregion
206
261
  //#region src/handlers.d.ts
@@ -210,11 +265,22 @@ declare class TypedAmqpWorker<TContract extends ContractDefinition> {
210
265
  * This utility allows you to define handlers outside of the worker creation,
211
266
  * providing better code organization and reusability.
212
267
  *
268
+ * Supports three patterns:
269
+ * 1. Simple handler: just the function (single message handler)
270
+ * 2. Handler with prefetch: [handler, { prefetch: 10 }] (single message handler with config)
271
+ * 3. Batch handler: [batchHandler, { batchSize: 5, batchTimeout: 1000 }] (REQUIRES batchSize config)
272
+ *
273
+ * **Important**: Batch handlers (handlers that accept an array of messages) MUST include
274
+ * batchSize configuration. You cannot create a batch handler without specifying batchSize.
275
+ *
213
276
  * @template TContract - The contract definition type
214
277
  * @template TName - The consumer name from the contract
215
278
  * @param contract - The contract definition containing the consumer
216
279
  * @param consumerName - The name of the consumer from the contract
217
- * @param handler - The async handler function that processes messages
280
+ * @param handler - The async handler function that processes messages (single or batch)
281
+ * @param options - Optional consumer options (prefetch, batchSize, batchTimeout)
282
+ * - For single-message handlers: { prefetch?: number } is optional
283
+ * - For batch handlers: { batchSize: number, batchTimeout?: number } is REQUIRED
218
284
  * @returns A type-safe handler that can be used with TypedAmqpWorker
219
285
  *
220
286
  * @example
@@ -222,58 +288,49 @@ declare class TypedAmqpWorker<TContract extends ContractDefinition> {
222
288
  * import { defineHandler } from '@amqp-contract/worker';
223
289
  * import { orderContract } from './contract';
224
290
  *
225
- * // Define handler outside of worker creation
291
+ * // Simple single-message handler without options
226
292
  * const processOrderHandler = defineHandler(
227
293
  * orderContract,
228
294
  * 'processOrder',
229
295
  * async (message) => {
230
- * // message is fully typed based on the contract
231
296
  * console.log('Processing order:', message.orderId);
232
297
  * await processPayment(message);
233
298
  * }
234
299
  * );
235
300
  *
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(
301
+ * // Single-message handler with prefetch
302
+ * const processOrderWithPrefetch = defineHandler(
250
303
  * orderContract,
251
304
  * 'processOrder',
252
305
  * async (message) => {
253
306
  * await processOrder(message);
254
- * }
307
+ * },
308
+ * { prefetch: 10 }
255
309
  * );
256
310
  *
257
- * const notifyOrderHandler = defineHandler(
311
+ * // Batch handler - MUST include batchSize
312
+ * const processBatchOrders = defineHandler(
258
313
  * 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,
314
+ * 'processOrders',
315
+ * async (messages) => {
316
+ * // messages is an array - batchSize configuration is REQUIRED
317
+ * await db.insertMany(messages);
271
318
  * },
272
- * connection: 'amqp://localhost',
273
- * });
319
+ * { batchSize: 5, batchTimeout: 1000 }
320
+ * );
274
321
  * ```
275
322
  */
276
- declare function defineHandler<TContract extends ContractDefinition, TName extends InferConsumerNames<TContract>>(contract: TContract, consumerName: TName, handler: WorkerInferConsumerHandler<TContract, TName>): WorkerInferConsumerHandler<TContract, TName>;
323
+ declare function defineHandler<TContract extends ContractDefinition, TName extends InferConsumerNames<TContract>>(contract: TContract, consumerName: TName, handler: WorkerInferConsumerHandler<TContract, TName>): WorkerInferConsumerHandlerEntry<TContract, TName>;
324
+ declare function defineHandler<TContract extends ContractDefinition, TName extends InferConsumerNames<TContract>>(contract: TContract, consumerName: TName, handler: WorkerInferConsumerHandler<TContract, TName>, options: {
325
+ prefetch?: number;
326
+ batchSize?: never;
327
+ batchTimeout?: never;
328
+ }): WorkerInferConsumerHandlerEntry<TContract, TName>;
329
+ declare function defineHandler<TContract extends ContractDefinition, TName extends InferConsumerNames<TContract>>(contract: TContract, consumerName: TName, handler: WorkerInferConsumerBatchHandler<TContract, TName>, options: {
330
+ prefetch?: number;
331
+ batchSize: number;
332
+ batchTimeout?: number;
333
+ }): WorkerInferConsumerHandlerEntry<TContract, TName>;
277
334
  /**
278
335
  * Define multiple type-safe handlers for consumers in a contract.
279
336
  *
@@ -332,5 +389,5 @@ declare function defineHandler<TContract extends ContractDefinition, TName exten
332
389
  */
333
390
  declare function defineHandlers<TContract extends ContractDefinition>(contract: TContract, handlers: WorkerInferConsumerHandlers<TContract>): WorkerInferConsumerHandlers<TContract>;
334
391
  //#endregion
335
- export { type CreateWorkerOptions, MessageValidationError, TechnicalError, TypedAmqpWorker, type WorkerInferConsumerHandler, type WorkerInferConsumerHandlers, type WorkerInferConsumerInput, defineHandler, defineHandlers };
392
+ export { type CreateWorkerOptions, MessageValidationError, TechnicalError, TypedAmqpWorker, type WorkerInferConsumerBatchHandler, type WorkerInferConsumerHandler, type WorkerInferConsumerHandlerEntry, type WorkerInferConsumerHandlers, type WorkerInferConsumerInput, defineHandler, defineHandlers };
336
393
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/errors.ts","../src/types.ts","../src/worker.ts","../src/handlers.ts"],"sourcesContent":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/errors.ts","../src/types.ts","../src/worker.ts","../src/handlers.ts"],"sourcesContent":[],"mappings":"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"}
package/dist/index.mjs CHANGED
@@ -79,11 +79,22 @@ var MessageValidationError = class extends WorkerError {
79
79
  * ```
80
80
  */
81
81
  var TypedAmqpWorker = class TypedAmqpWorker {
82
+ actualHandlers;
83
+ consumerOptions;
84
+ batchTimers = /* @__PURE__ */ new Map();
82
85
  constructor(contract, amqpClient, handlers, logger) {
83
86
  this.contract = contract;
84
87
  this.amqpClient = amqpClient;
85
- this.handlers = handlers;
86
88
  this.logger = logger;
89
+ this.actualHandlers = {};
90
+ this.consumerOptions = {};
91
+ for (const consumerName of Object.keys(handlers)) {
92
+ const handlerEntry = handlers[consumerName];
93
+ if (Array.isArray(handlerEntry)) {
94
+ this.actualHandlers[consumerName] = handlerEntry[0];
95
+ this.consumerOptions[consumerName] = handlerEntry[1];
96
+ } else this.actualHandlers[consumerName] = handlerEntry;
97
+ }
87
98
  }
88
99
  /**
89
100
  * Create a type-safe AMQP worker from a contract.
@@ -138,6 +149,8 @@ var TypedAmqpWorker = class TypedAmqpWorker {
138
149
  * ```
139
150
  */
140
151
  close() {
152
+ for (const timer of this.batchTimers.values()) clearTimeout(timer);
153
+ this.batchTimers.clear();
141
154
  return Future.fromPromise(this.amqpClient.close()).mapError((error) => new TechnicalError("Failed to close AMQP connection", error)).mapOk(() => void 0);
142
155
  }
143
156
  /**
@@ -146,6 +159,21 @@ var TypedAmqpWorker = class TypedAmqpWorker {
146
159
  consumeAll() {
147
160
  if (!this.contract.consumers) return Future.value(Result.Error(new TechnicalError("No consumers defined in contract")));
148
161
  const consumerNames = Object.keys(this.contract.consumers);
162
+ let maxPrefetch = 0;
163
+ for (const consumerName of consumerNames) {
164
+ const options = this.consumerOptions[consumerName];
165
+ if (options?.prefetch !== void 0) {
166
+ if (options.prefetch <= 0 || !Number.isInteger(options.prefetch)) return Future.value(Result.Error(new TechnicalError(`Invalid prefetch value for "${String(consumerName)}": must be a positive integer`)));
167
+ maxPrefetch = Math.max(maxPrefetch, options.prefetch);
168
+ }
169
+ if (options?.batchSize !== void 0) {
170
+ const effectivePrefetch = options.prefetch ?? options.batchSize;
171
+ maxPrefetch = Math.max(maxPrefetch, effectivePrefetch);
172
+ }
173
+ }
174
+ if (maxPrefetch > 0) this.amqpClient.channel.addSetup(async (channel) => {
175
+ await channel.prefetch(maxPrefetch);
176
+ });
149
177
  return Future.all(consumerNames.map((consumerName) => this.consume(consumerName))).map(Result.all).mapOk(() => void 0);
150
178
  }
151
179
  waitForConnectionReady() {
@@ -163,8 +191,52 @@ var TypedAmqpWorker = class TypedAmqpWorker {
163
191
  const available = availableConsumers.length > 0 ? availableConsumers.join(", ") : "none";
164
192
  return Future.value(Result.Error(new TechnicalError(`Consumer not found: "${String(consumerName)}". Available consumers: ${available}`)));
165
193
  }
166
- const handler = this.handlers[consumerName];
194
+ const handler = this.actualHandlers[consumerName];
167
195
  if (!handler) return Future.value(Result.Error(new TechnicalError(`Handler for "${String(consumerName)}" not provided`)));
196
+ const options = this.consumerOptions[consumerName] ?? {};
197
+ if (options.batchSize !== void 0) {
198
+ if (options.batchSize <= 0 || !Number.isInteger(options.batchSize)) return Future.value(Result.Error(new TechnicalError(`Invalid batchSize for "${String(consumerName)}": must be a positive integer`)));
199
+ }
200
+ if (options.batchTimeout !== void 0) {
201
+ if (typeof options.batchTimeout !== "number" || !Number.isFinite(options.batchTimeout) || options.batchTimeout <= 0) return Future.value(Result.Error(new TechnicalError(`Invalid batchTimeout for "${String(consumerName)}": must be a positive number`)));
202
+ }
203
+ if (options.batchSize !== void 0 && options.batchSize > 0) return this.consumeBatch(consumerName, consumer, options, handler);
204
+ else return this.consumeSingle(consumerName, consumer, handler);
205
+ }
206
+ /**
207
+ * Parse and validate a message from AMQP
208
+ * @returns Future<Result<validated message, void>> - Ok with validated message, or Error (already handled with nack)
209
+ */
210
+ parseAndValidateMessage(msg, consumer, consumerName) {
211
+ const parseResult = Result.fromExecution(() => JSON.parse(msg.content.toString()));
212
+ if (parseResult.isError()) {
213
+ this.logger?.error("Error parsing message", {
214
+ consumerName: String(consumerName),
215
+ queueName: consumer.queue.name,
216
+ error: parseResult.error
217
+ });
218
+ this.amqpClient.channel.nack(msg, false, false);
219
+ return Future.value(Result.Error(void 0));
220
+ }
221
+ const rawValidation = consumer.message.payload["~standard"].validate(parseResult.value);
222
+ return Future.fromPromise(rawValidation instanceof Promise ? rawValidation : Promise.resolve(rawValidation)).mapOkToResult((validationResult) => {
223
+ if (validationResult.issues) {
224
+ const error = new MessageValidationError(String(consumerName), validationResult.issues);
225
+ this.logger?.error("Message validation failed", {
226
+ consumerName: String(consumerName),
227
+ queueName: consumer.queue.name,
228
+ error
229
+ });
230
+ this.amqpClient.channel.nack(msg, false, false);
231
+ return Result.Error(void 0);
232
+ }
233
+ return Result.Ok(validationResult.value);
234
+ });
235
+ }
236
+ /**
237
+ * Consume messages one at a time
238
+ */
239
+ consumeSingle(consumerName, consumer, handler) {
168
240
  return Future.fromPromise(this.amqpClient.channel.consume(consumer.queue.name, async (msg) => {
169
241
  if (msg === null) {
170
242
  this.logger?.warn("Consumer cancelled by server", {
@@ -173,123 +245,114 @@ var TypedAmqpWorker = class TypedAmqpWorker {
173
245
  });
174
246
  return;
175
247
  }
176
- const parseResult = Result.fromExecution(() => JSON.parse(msg.content.toString()));
177
- if (parseResult.isError()) {
178
- this.logger?.error("Error parsing message", {
248
+ await this.parseAndValidateMessage(msg, consumer, consumerName).flatMapOk((validatedMessage) => Future.fromPromise(handler(validatedMessage)).tapError((error) => {
249
+ this.logger?.error("Error processing message", {
179
250
  consumerName: String(consumerName),
180
251
  queueName: consumer.queue.name,
181
- error: parseResult.error
252
+ error
182
253
  });
183
- this.amqpClient.channel.nack(msg, false, false);
184
- return;
254
+ this.amqpClient.channel.nack(msg, false, true);
255
+ })).tapOk(() => {
256
+ this.logger?.info("Message consumed successfully", {
257
+ consumerName: String(consumerName),
258
+ queueName: consumer.queue.name
259
+ });
260
+ this.amqpClient.channel.ack(msg);
261
+ }).toPromise();
262
+ })).mapError((error) => new TechnicalError(`Failed to start consuming for "${String(consumerName)}"`, error)).mapOk(() => void 0);
263
+ }
264
+ /**
265
+ * Consume messages in batches
266
+ */
267
+ consumeBatch(consumerName, consumer, options, handler) {
268
+ const batchSize = options.batchSize;
269
+ const batchTimeout = options.batchTimeout ?? 1e3;
270
+ const timerKey = String(consumerName);
271
+ let batch = [];
272
+ let isProcessing = false;
273
+ const processBatch = async () => {
274
+ if (isProcessing || batch.length === 0) return;
275
+ isProcessing = true;
276
+ const currentBatch = batch;
277
+ batch = [];
278
+ const timer = this.batchTimers.get(timerKey);
279
+ if (timer) {
280
+ clearTimeout(timer);
281
+ this.batchTimers.delete(timerKey);
185
282
  }
186
- const rawValidation = consumer.message.payload["~standard"].validate(parseResult.value);
187
- await Future.fromPromise(rawValidation instanceof Promise ? rawValidation : Promise.resolve(rawValidation)).mapOkToResult((validationResult) => {
188
- if (validationResult.issues) return Result.Error(new MessageValidationError(String(consumerName), validationResult.issues));
189
- return Result.Ok(validationResult.value);
190
- }).tapError((error) => {
191
- this.logger?.error("Message validation failed", {
283
+ const messages = currentBatch.map((item) => item.message);
284
+ this.logger?.info("Processing batch", {
285
+ consumerName: String(consumerName),
286
+ queueName: consumer.queue.name,
287
+ batchSize: currentBatch.length
288
+ });
289
+ try {
290
+ await handler(messages);
291
+ for (const item of currentBatch) this.amqpClient.channel.ack(item.amqpMessage);
292
+ this.logger?.info("Batch processed successfully", {
192
293
  consumerName: String(consumerName),
193
294
  queueName: consumer.queue.name,
194
- error
295
+ batchSize: currentBatch.length
195
296
  });
196
- this.amqpClient.channel.nack(msg, false, false);
197
- }).flatMapOk((validatedMessage) => Future.fromPromise(handler(validatedMessage)).tapError((error) => {
198
- this.logger?.error("Error processing message", {
297
+ } catch (error) {
298
+ this.logger?.error("Error processing batch", {
199
299
  consumerName: String(consumerName),
200
300
  queueName: consumer.queue.name,
301
+ batchSize: currentBatch.length,
201
302
  error
202
303
  });
203
- this.amqpClient.channel.nack(msg, false, true);
204
- })).tapOk(() => {
205
- this.logger?.info("Message consumed successfully", {
304
+ for (const item of currentBatch) this.amqpClient.channel.nack(item.amqpMessage, false, true);
305
+ } finally {
306
+ isProcessing = false;
307
+ }
308
+ };
309
+ const scheduleBatchProcessing = () => {
310
+ if (isProcessing) return;
311
+ const existingTimer = this.batchTimers.get(timerKey);
312
+ if (existingTimer) clearTimeout(existingTimer);
313
+ const timer = setTimeout(() => {
314
+ processBatch().catch((error) => {
315
+ this.logger?.error("Unexpected error in batch processing", {
316
+ consumerName: String(consumerName),
317
+ error
318
+ });
319
+ });
320
+ }, batchTimeout);
321
+ this.batchTimers.set(timerKey, timer);
322
+ };
323
+ return Future.fromPromise(this.amqpClient.channel.consume(consumer.queue.name, async (msg) => {
324
+ if (msg === null) {
325
+ this.logger?.warn("Consumer cancelled by server", {
206
326
  consumerName: String(consumerName),
207
327
  queueName: consumer.queue.name
208
328
  });
209
- this.amqpClient.channel.ack(msg);
210
- }).toPromise();
329
+ await processBatch();
330
+ return;
331
+ }
332
+ const validationResult = await this.parseAndValidateMessage(msg, consumer, consumerName).toPromise();
333
+ if (validationResult.isError()) return;
334
+ batch.push({
335
+ message: validationResult.value,
336
+ amqpMessage: msg
337
+ });
338
+ if (batch.length >= batchSize) {
339
+ await processBatch();
340
+ if (batch.length > 0 && !this.batchTimers.has(timerKey)) scheduleBatchProcessing();
341
+ } else if (!this.batchTimers.has(timerKey)) scheduleBatchProcessing();
211
342
  })).mapError((error) => new TechnicalError(`Failed to start consuming for "${String(consumerName)}"`, error)).mapOk(() => void 0);
212
343
  }
213
344
  };
214
345
 
215
346
  //#endregion
216
347
  //#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) {
348
+ function defineHandler(contract, consumerName, handler, options) {
287
349
  const consumers = contract.consumers;
288
350
  if (!consumers || !(consumerName in consumers)) {
289
351
  const availableConsumers = consumers ? Object.keys(consumers) : [];
290
352
  const available = availableConsumers.length > 0 ? availableConsumers.join(", ") : "none";
291
353
  throw new Error(`Consumer "${String(consumerName)}" not found in contract. Available consumers: ${available}`);
292
354
  }
355
+ if (options) return [handler, options];
293
356
  return handler;
294
357
  }
295
358
  /**