@amqp-contract/worker 0.4.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/README.md +14 -0
- package/dist/index.cjs +158 -88
- package/dist/index.d.cts +98 -41
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +98 -41
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +158 -88
- package/dist/index.mjs.map +1 -1
- package/docs/index.md +306 -55
- package/package.json +7 -8
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
|
|
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,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
|
-
* //
|
|
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
|
-
* //
|
|
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(
|
|
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
|
-
*
|
|
311
|
+
* // Batch handler - MUST include batchSize
|
|
312
|
+
* const processBatchOrders = defineHandler(
|
|
258
313
|
* 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,
|
|
314
|
+
* 'processOrders',
|
|
315
|
+
* async (messages) => {
|
|
316
|
+
* // messages is an array - batchSize configuration is REQUIRED
|
|
317
|
+
* await db.insertMany(messages);
|
|
271
318
|
* },
|
|
272
|
-
*
|
|
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>):
|
|
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
|
package/dist/index.d.mts.map
CHANGED
|
@@ -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":";;;;;;;;;;uBAGe,WAAA,SAAoB,KAAA;;;;;AAkBnC;AAaA;cAba,cAAA,SAAuB,WAAA;;;AChB0B;;;;AAM5B,cDuBrB,sBAAA,SAA+B,WAAA,CCvBV;EAK7B,SAAA,YAAkB,EAAA,MAAA;EAAmB,SAAA,MAAA,EAAA,OAAA;EACxC,WAAA,CAAA,YAAA,EAAA,MAAA,EAAA,MAAA,EAAA,OAAA;;;;;;;KAPG,iCAAiC,oBACpC,gBAAgB;;;ADUlB;AAaA,KClBK,kBDkBQ,CAAA,kBClB6B,kBDkBa,CAAA,GClBS,gBDkBT,CCjBrD,SDiBqD,CAAA,SAAA,CAAA,CAAA,SAAA,CAAA,CAAA;;;;AC7BO,KAkBzD,cAbA,CAAA,kBAaiC,kBAbjB,CAAA,GAauC,WAbvC,CAamD,SAbnD,CAAA,WAAA,CAAA,CAAA;;;;KAkBhB,aAjB6B,CAAA,kBAkBd,kBAlBc,EAAA,cAmBlB,kBAnBkB,CAmBC,SAnBD,CAAA,CAAA,GAoB9B,cApB8B,CAoBf,SApBe,CAAA,CAoBJ,KApBI,CAAA;AAAA;;;AAK8B,KAoBpD,wBApBoD,CAAA,kBAqB5C,kBArB4C,EAAA,cAsBhD,kBAtBgD,CAsB7B,SAtB6B,CAAA,CAAA,GAuB5D,kBAvB4D,CAuBzC,aAvByC,CAuB3B,SAvB2B,EAuBhB,KAvBgB,CAAA,CAAA;;AAAgB
|
|
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,31 +191,61 @@ 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.
|
|
194
|
+
const handler = this.actualHandlers[consumerName];
|
|
167
195
|
if (!handler) return Future.value(Result.Error(new TechnicalError(`Handler for "${String(consumerName)}" not provided`)));
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
if (
|
|
171
|
-
|
|
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", {
|
|
172
226
|
consumerName: String(consumerName),
|
|
173
227
|
queueName: consumer.queue.name,
|
|
174
|
-
error
|
|
228
|
+
error
|
|
175
229
|
});
|
|
176
230
|
this.amqpClient.channel.nack(msg, false, false);
|
|
177
|
-
return;
|
|
231
|
+
return Result.Error(void 0);
|
|
178
232
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
233
|
+
return Result.Ok(validationResult.value);
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Consume messages one at a time
|
|
238
|
+
*/
|
|
239
|
+
consumeSingle(consumerName, consumer, handler) {
|
|
240
|
+
return Future.fromPromise(this.amqpClient.channel.consume(consumer.queue.name, async (msg) => {
|
|
241
|
+
if (msg === null) {
|
|
242
|
+
this.logger?.warn("Consumer cancelled by server", {
|
|
185
243
|
consumerName: String(consumerName),
|
|
186
|
-
queueName: consumer.queue.name
|
|
187
|
-
error
|
|
244
|
+
queueName: consumer.queue.name
|
|
188
245
|
});
|
|
189
|
-
|
|
190
|
-
}
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
await this.parseAndValidateMessage(msg, consumer, consumerName).flatMapOk((validatedMessage) => Future.fromPromise(handler(validatedMessage)).tapError((error) => {
|
|
191
249
|
this.logger?.error("Error processing message", {
|
|
192
250
|
consumerName: String(consumerName),
|
|
193
251
|
queueName: consumer.queue.name,
|
|
@@ -203,86 +261,98 @@ var TypedAmqpWorker = class TypedAmqpWorker {
|
|
|
203
261
|
}).toPromise();
|
|
204
262
|
})).mapError((error) => new TechnicalError(`Failed to start consuming for "${String(consumerName)}"`, error)).mapOk(() => void 0);
|
|
205
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);
|
|
282
|
+
}
|
|
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", {
|
|
293
|
+
consumerName: String(consumerName),
|
|
294
|
+
queueName: consumer.queue.name,
|
|
295
|
+
batchSize: currentBatch.length
|
|
296
|
+
});
|
|
297
|
+
} catch (error) {
|
|
298
|
+
this.logger?.error("Error processing batch", {
|
|
299
|
+
consumerName: String(consumerName),
|
|
300
|
+
queueName: consumer.queue.name,
|
|
301
|
+
batchSize: currentBatch.length,
|
|
302
|
+
error
|
|
303
|
+
});
|
|
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", {
|
|
326
|
+
consumerName: String(consumerName),
|
|
327
|
+
queueName: consumer.queue.name
|
|
328
|
+
});
|
|
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();
|
|
342
|
+
})).mapError((error) => new TechnicalError(`Failed to start consuming for "${String(consumerName)}"`, error)).mapOk(() => void 0);
|
|
343
|
+
}
|
|
206
344
|
};
|
|
207
345
|
|
|
208
346
|
//#endregion
|
|
209
347
|
//#region src/handlers.ts
|
|
210
|
-
|
|
211
|
-
* Define a type-safe handler for a specific consumer in a contract.
|
|
212
|
-
*
|
|
213
|
-
* This utility allows you to define handlers outside of the worker creation,
|
|
214
|
-
* providing better code organization and reusability.
|
|
215
|
-
*
|
|
216
|
-
* @template TContract - The contract definition type
|
|
217
|
-
* @template TName - The consumer name from the contract
|
|
218
|
-
* @param contract - The contract definition containing the consumer
|
|
219
|
-
* @param consumerName - The name of the consumer from the contract
|
|
220
|
-
* @param handler - The async handler function that processes messages
|
|
221
|
-
* @returns A type-safe handler that can be used with TypedAmqpWorker
|
|
222
|
-
*
|
|
223
|
-
* @example
|
|
224
|
-
* ```typescript
|
|
225
|
-
* import { defineHandler } from '@amqp-contract/worker';
|
|
226
|
-
* import { orderContract } from './contract';
|
|
227
|
-
*
|
|
228
|
-
* // Define handler outside of worker creation
|
|
229
|
-
* const processOrderHandler = defineHandler(
|
|
230
|
-
* orderContract,
|
|
231
|
-
* 'processOrder',
|
|
232
|
-
* async (message) => {
|
|
233
|
-
* // message is fully typed based on the contract
|
|
234
|
-
* console.log('Processing order:', message.orderId);
|
|
235
|
-
* await processPayment(message);
|
|
236
|
-
* }
|
|
237
|
-
* );
|
|
238
|
-
*
|
|
239
|
-
* // Use the handler in worker
|
|
240
|
-
* const worker = await TypedAmqpWorker.create({
|
|
241
|
-
* contract: orderContract,
|
|
242
|
-
* handlers: {
|
|
243
|
-
* processOrder: processOrderHandler,
|
|
244
|
-
* },
|
|
245
|
-
* connection: 'amqp://localhost',
|
|
246
|
-
* });
|
|
247
|
-
* ```
|
|
248
|
-
*
|
|
249
|
-
* @example
|
|
250
|
-
* ```typescript
|
|
251
|
-
* // Define multiple handlers
|
|
252
|
-
* const processOrderHandler = defineHandler(
|
|
253
|
-
* orderContract,
|
|
254
|
-
* 'processOrder',
|
|
255
|
-
* async (message) => {
|
|
256
|
-
* await processOrder(message);
|
|
257
|
-
* }
|
|
258
|
-
* );
|
|
259
|
-
*
|
|
260
|
-
* const notifyOrderHandler = defineHandler(
|
|
261
|
-
* orderContract,
|
|
262
|
-
* 'notifyOrder',
|
|
263
|
-
* async (message) => {
|
|
264
|
-
* await sendNotification(message);
|
|
265
|
-
* }
|
|
266
|
-
* );
|
|
267
|
-
*
|
|
268
|
-
* // Compose handlers
|
|
269
|
-
* const worker = await TypedAmqpWorker.create({
|
|
270
|
-
* contract: orderContract,
|
|
271
|
-
* handlers: {
|
|
272
|
-
* processOrder: processOrderHandler,
|
|
273
|
-
* notifyOrder: notifyOrderHandler,
|
|
274
|
-
* },
|
|
275
|
-
* connection: 'amqp://localhost',
|
|
276
|
-
* });
|
|
277
|
-
* ```
|
|
278
|
-
*/
|
|
279
|
-
function defineHandler(contract, consumerName, handler) {
|
|
348
|
+
function defineHandler(contract, consumerName, handler, options) {
|
|
280
349
|
const consumers = contract.consumers;
|
|
281
350
|
if (!consumers || !(consumerName in consumers)) {
|
|
282
351
|
const availableConsumers = consumers ? Object.keys(consumers) : [];
|
|
283
352
|
const available = availableConsumers.length > 0 ? availableConsumers.join(", ") : "none";
|
|
284
353
|
throw new Error(`Consumer "${String(consumerName)}" not found in contract. Available consumers: ${available}`);
|
|
285
354
|
}
|
|
355
|
+
if (options) return [handler, options];
|
|
286
356
|
return handler;
|
|
287
357
|
}
|
|
288
358
|
/**
|