@amqp-contract/worker 0.6.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 +6 -6
- package/dist/index.cjs +71 -18
- package/dist/index.d.cts +1 -0
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +1 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +71 -18
- package/dist/index.mjs.map +1 -1
- package/docs/index.md +25 -25
- package/package.json +16 -9
package/README.md
CHANGED
|
@@ -29,9 +29,9 @@ pnpm add @amqp-contract/worker
|
|
|
29
29
|
### Basic Usage
|
|
30
30
|
|
|
31
31
|
```typescript
|
|
32
|
-
import { TypedAmqpWorker } from
|
|
33
|
-
import type { Logger } from
|
|
34
|
-
import { contract } from
|
|
32
|
+
import { TypedAmqpWorker } from "@amqp-contract/worker";
|
|
33
|
+
import type { Logger } from "@amqp-contract/core";
|
|
34
|
+
import { contract } from "./contract";
|
|
35
35
|
|
|
36
36
|
// Optional: Create a logger implementation
|
|
37
37
|
const logger: Logger = {
|
|
@@ -46,7 +46,7 @@ const worker = await TypedAmqpWorker.create({
|
|
|
46
46
|
contract,
|
|
47
47
|
handlers: {
|
|
48
48
|
processOrder: async (message) => {
|
|
49
|
-
console.log(
|
|
49
|
+
console.log("Processing order:", message.orderId);
|
|
50
50
|
|
|
51
51
|
// Your business logic here
|
|
52
52
|
await processPayment(message);
|
|
@@ -55,7 +55,7 @@ const worker = await TypedAmqpWorker.create({
|
|
|
55
55
|
// If an exception is thrown, the message is automatically requeued
|
|
56
56
|
},
|
|
57
57
|
},
|
|
58
|
-
urls: [
|
|
58
|
+
urls: ["amqp://localhost"],
|
|
59
59
|
logger, // Optional: logs message consumption and errors
|
|
60
60
|
});
|
|
61
61
|
|
|
@@ -89,7 +89,7 @@ handlers: {
|
|
|
89
89
|
// Message is requeued for retry
|
|
90
90
|
throw error;
|
|
91
91
|
}
|
|
92
|
-
}
|
|
92
|
+
};
|
|
93
93
|
}
|
|
94
94
|
```
|
|
95
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
|
/**
|
|
@@ -82,6 +107,7 @@ var TypedAmqpWorker = class TypedAmqpWorker {
|
|
|
82
107
|
actualHandlers;
|
|
83
108
|
consumerOptions;
|
|
84
109
|
batchTimers = /* @__PURE__ */ new Map();
|
|
110
|
+
consumerTags = /* @__PURE__ */ new Set();
|
|
85
111
|
constructor(contract, amqpClient, handlers, logger) {
|
|
86
112
|
this.contract = contract;
|
|
87
113
|
this.amqpClient = amqpClient;
|
|
@@ -151,7 +177,15 @@ var TypedAmqpWorker = class TypedAmqpWorker {
|
|
|
151
177
|
close() {
|
|
152
178
|
for (const timer of this.batchTimers.values()) clearTimeout(timer);
|
|
153
179
|
this.batchTimers.clear();
|
|
154
|
-
return _swan_io_boxed.Future.
|
|
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);
|
|
155
189
|
}
|
|
156
190
|
/**
|
|
157
191
|
* Start consuming messages for all consumers
|
|
@@ -208,30 +242,45 @@ var TypedAmqpWorker = class TypedAmqpWorker {
|
|
|
208
242
|
* @returns Future<Result<validated message, void>> - Ok with validated message, or Error (already handled with nack)
|
|
209
243
|
*/
|
|
210
244
|
parseAndValidateMessage(msg, consumer, consumerName) {
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
this.logger?.error("Error parsing message", {
|
|
245
|
+
const decompressMessage = _swan_io_boxed.Future.fromPromise(decompressBuffer(msg.content, msg.properties.contentEncoding)).mapError((error) => {
|
|
246
|
+
this.logger?.error("Error decompressing message", {
|
|
214
247
|
consumerName: String(consumerName),
|
|
215
248
|
queueName: consumer.queue.name,
|
|
216
|
-
|
|
249
|
+
contentEncoding: msg.properties.contentEncoding,
|
|
250
|
+
error
|
|
217
251
|
});
|
|
218
252
|
this.amqpClient.channel.nack(msg, false, false);
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
const error = new MessageValidationError(String(consumerName), validationResult.issues);
|
|
225
|
-
this.logger?.error("Message validation failed", {
|
|
253
|
+
});
|
|
254
|
+
const parseMessage = (buffer) => {
|
|
255
|
+
const parseResult = _swan_io_boxed.Result.fromExecution(() => JSON.parse(buffer.toString()));
|
|
256
|
+
if (parseResult.isError()) {
|
|
257
|
+
this.logger?.error("Error parsing message", {
|
|
226
258
|
consumerName: String(consumerName),
|
|
227
259
|
queueName: consumer.queue.name,
|
|
228
|
-
error
|
|
260
|
+
error: parseResult.error
|
|
229
261
|
});
|
|
230
262
|
this.amqpClient.channel.nack(msg, false, false);
|
|
231
|
-
return _swan_io_boxed.Result.Error(void 0);
|
|
263
|
+
return _swan_io_boxed.Future.value(_swan_io_boxed.Result.Error(void 0));
|
|
232
264
|
}
|
|
233
|
-
return _swan_io_boxed.Result.Ok(
|
|
234
|
-
}
|
|
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
|
+
}
|
|
280
|
+
return _swan_io_boxed.Result.Ok(validationResult.value);
|
|
281
|
+
});
|
|
282
|
+
};
|
|
283
|
+
return decompressMessage.flatMapOk(parseMessage).flatMapOk(validateMessage);
|
|
235
284
|
}
|
|
236
285
|
/**
|
|
237
286
|
* Consume messages one at a time
|
|
@@ -259,7 +308,9 @@ var TypedAmqpWorker = class TypedAmqpWorker {
|
|
|
259
308
|
});
|
|
260
309
|
this.amqpClient.channel.ack(msg);
|
|
261
310
|
}).toPromise();
|
|
262
|
-
})).
|
|
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);
|
|
263
314
|
}
|
|
264
315
|
/**
|
|
265
316
|
* Consume messages in batches
|
|
@@ -339,7 +390,9 @@ var TypedAmqpWorker = class TypedAmqpWorker {
|
|
|
339
390
|
await processBatch();
|
|
340
391
|
if (batch.length > 0 && !this.batchTimers.has(timerKey)) scheduleBatchProcessing();
|
|
341
392
|
} else if (!this.batchTimers.has(timerKey)) scheduleBatchProcessing();
|
|
342
|
-
})).
|
|
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);
|
|
343
396
|
}
|
|
344
397
|
};
|
|
345
398
|
|
package/dist/index.d.cts
CHANGED
|
@@ -180,6 +180,7 @@ declare class TypedAmqpWorker<TContract extends ContractDefinition> {
|
|
|
180
180
|
private readonly actualHandlers;
|
|
181
181
|
private readonly consumerOptions;
|
|
182
182
|
private readonly batchTimers;
|
|
183
|
+
private readonly consumerTags;
|
|
183
184
|
private constructor();
|
|
184
185
|
/**
|
|
185
186
|
* Create a type-safe AMQP worker from a contract.
|
package/dist/index.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","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;;;;AAOT,KAuB3D,0BAvB2D,CAAA,kBAwBnD,kBAxBmD,EAAA,cAyBvD,kBAzBuD,CAyBpC,SAzBoC,CAAA,CAAA,GAAA,CAAA,OAAA,EA0BzD,wBA1ByD,CA0BhC,SA1BgC,EA0BrB,KA1BqB,CAAA,EAAA,GA0BV,OA1BU,CAAA,IAAA,CAAA;AAAA;;;;AAQpD,KAwBP,+BAxBO,CAAA,kBAyBC,kBAzBD,EAAA,cA0BH,kBA1BG,CA0BgB,SA1BhB,CAAA,CAAA,GAAA,CAAA,QAAA,EA2BJ,KA3BI,CA2BE,wBA3BF,CA2B2B,SA3B3B,EA2BsC,KA3BtC,CAAA,CAAA,EAAA,GA2BkD,OA3BlD,CAAA,IAAA,CAAA;;;;AAKnB;;;;;AAGgD,KA6BpC,+BA7BoC,CAAA,kBA8B5B,kBA9B4B,EAAA,cA+BhC,kBA/BgC,CA+Bb,SA/Ba,CAAA,CAAA,GAiC5C,0BAjC4C,CAiCjB,SAjCiB,EAiCN,KAjCM,CAAA,GAAA,SAAA,CAmC1C,0BAnCiB,CAmCU,SAnCV,EAmCqB,KAnCrB,CAAA,EAAnB;EAAkB,QAAA,CAAA,EAAA,MAAA;EAOV,SAAA,CAAA,EAAA,KAAA;EACQ,YAAA,CAAA,EAAA,KAAA;AACe,CAAA,CAAnB,GAAA,SAAA,CA8BV,+BA7BiC,CA6BD,SA7BC,EA6BU,KA7BV,CAAA,EAAW;EAApC,QAAA,CAAA,EAAA,MAAA;EAA+C,SAAA,EAAA,MAAA;EAAO,YAAA,CAAA,EAAA,MAAA;AAMxD,CAAA,CACQ;;;;;AAEC,KA4BT,2BA5BS,CAAA,kBA4BqC,kBA5BrC,CAAA,GAAA,QA6Bb,kBA7BO,CA6BY,SA7BZ,CAAA,GA6ByB,+BA7BzB,CA6ByD,SA7BzD,EA6BoE,CA7BpE,CAAA,EAAsD;;;;;ADrCrE;AAaA;;;;AC7B8D;;;;;AAM5B;;;;;AAK8C;;;;;AAOT;;;;;;;;AAavE;;;;;;;AAGI,
|
|
1
|
+
{"version":3,"file":"index.d.cts","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;;;;AAOT,KAuB3D,0BAvB2D,CAAA,kBAwBnD,kBAxBmD,EAAA,cAyBvD,kBAzBuD,CAyBpC,SAzBoC,CAAA,CAAA,GAAA,CAAA,OAAA,EA0BzD,wBA1ByD,CA0BhC,SA1BgC,EA0BrB,KA1BqB,CAAA,EAAA,GA0BV,OA1BU,CAAA,IAAA,CAAA;AAAA;;;;AAQpD,KAwBP,+BAxBO,CAAA,kBAyBC,kBAzBD,EAAA,cA0BH,kBA1BG,CA0BgB,SA1BhB,CAAA,CAAA,GAAA,CAAA,QAAA,EA2BJ,KA3BI,CA2BE,wBA3BF,CA2B2B,SA3B3B,EA2BsC,KA3BtC,CAAA,CAAA,EAAA,GA2BkD,OA3BlD,CAAA,IAAA,CAAA;;;;AAKnB;;;;;AAGgD,KA6BpC,+BA7BoC,CAAA,kBA8B5B,kBA9B4B,EAAA,cA+BhC,kBA/BgC,CA+Bb,SA/Ba,CAAA,CAAA,GAiC5C,0BAjC4C,CAiCjB,SAjCiB,EAiCN,KAjCM,CAAA,GAAA,SAAA,CAmC1C,0BAnCiB,CAmCU,SAnCV,EAmCqB,KAnCrB,CAAA,EAAnB;EAAkB,QAAA,CAAA,EAAA,MAAA;EAOV,SAAA,CAAA,EAAA,KAAA;EACQ,YAAA,CAAA,EAAA,KAAA;AACe,CAAA,CAAnB,GAAA,SAAA,CA8BV,+BA7BiC,CA6BD,SA7BC,EA6BU,KA7BV,CAAA,EAAW;EAApC,QAAA,CAAA,EAAA,MAAA;EAA+C,SAAA,EAAA,MAAA;EAAO,YAAA,CAAA,EAAA,MAAA;AAMxD,CAAA,CACQ;;;;;AAEC,KA4BT,2BA5BS,CAAA,kBA4BqC,kBA5BrC,CAAA,GAAA,QA6Bb,kBA7BO,CA6BY,SA7BZ,CAAA,GA6ByB,+BA7BzB,CA6ByD,SA7BzD,EA6BoE,CA7BpE,CAAA,EAAsD;;;;;ADrCrE;AAaA;;;;AC7B8D;;;;;AAM5B;;;;;AAK8C;;;;;AAOT;;;;;;;;AAavE;;;;;;;AAGI,KCqCQ,mBDrCR,CAAA,kBCqC8C,kBDrC9C,CAAA,GAAA;EAAkB;EAOV,QAAA,ECgCA,SDhCA;EACQ;EACe,QAAA,ECgCvB,2BDhCuB,CCgCK,SDhCL,CAAA;EAAnB;EACuB,IAAA,ECiC/B,aDjC+B,EAAA;EAAW;EAApC,iBAAA,CAAA,ECmCQ,4BDnCR,GAAA,SAAA;EAA+C;EAAO,MAAA,CAAA,ECqCzD,MDrCyD,GAAA,SAAA;AAMpE,CAAA;;;;;;;;;;AAaA;;;;;;;;;;;;;;AAkBA;;;;;;;;;;;ACVA;;;;;;AAQsB,cA6CT,eA7CS,CAAA,kBA6CyB,kBA7CzB,CAAA,CAAA;EAEX,iBAAA,QAAA;EAAM,iBAAA,UAAA;EA2CJ,iBAAA,MAAe;EAAmB,iBAAA,cAAA;EA2Eb,iBAAA,eAAA;EAC9B,iBAAA,WAAA;EACA,iBAAA,YAAA;EACA,QAAA,WAAA,CAAA;EACA;;;;;;;;;;;;;;;;AC3IJ;;;;;;;;;;;;;EAQgB,OAAA,MAAA,CAAA,kBD+HkB,kBC/HL,CAAA,CAAA;IAAA,QAAA;IAAA,QAAA;IAAA,IAAA;IAAA,iBAAA;IAAA;EAAA,CAAA,EDqIxB,mBCrIwB,CDqIJ,SCrII,CAAA,CAAA,EDqIS,MCrIT,CDqIgB,MCrIhB,CDqIuB,eCrIvB,CDqIuC,SCrIvC,CAAA,EDqImD,cCrInD,CAAA,CAAA;EACT;;;;;;;;;;;;AAQpB;;;;EAIY,KAAA,CAAA,CAAA,EDyJD,MCzJC,CDyJM,MCzJN,CAAA,IAAA,EDyJmB,cCzJnB,CAAA,CAAA;EACI;;;EACL,QAAA,UAAA;EAEwB,QAAA,sBAAA;EAAW;;;EAsF9B,QAAA,OAAA;EAAiC;;;;EAGlB,QAAA,uBAAA;EAA5B;;;;;;;;;;;;;;;;;AHlKH;AAaA;;;;AC7B8D;;;;;AAM5B;;;;;AAK8C;;;;;AAOT;;;;;;;;AAavE;;;;;;;;;AAUA;;;;;;;;;AASA;;;;;;;;;AAG4E,iBEW5D,aFX4D,CAAA,kBEYxD,kBFZwD,EAAA,cEa5D,kBFb4D,CEazC,SFbyC,CAAA,CAAA,CAAA,QAAA,EEehE,SFfgE,EAAA,YAAA,EEgB5D,KFhB4D,EAAA,OAAA,EEiBjE,0BFjBiE,CEiBtC,SFjBsC,EEiB3B,KFjB2B,CAAA,CAAA,EEkBzE,+BFlByE,CEkBzC,SFlByC,EEkB9B,KFlB8B,CAAA;AAUhE,iBESI,aFTJ,CAA+B,kBEUvB,kBFVuB,EAAA,cEW3B,kBFX2B,CEWR,SFXQ,CAAA,CAAA,CAAA,QAAA,EEa/B,SFb+B,EAAA,YAAA,EEc3B,KFd2B,EAAA,OAAA,EEehC,0BFfgC,CEeL,SFfK,EEeM,KFfN,CAAA,EAAA,OAAA,EAAA;EACvB,QAAA,CAAA,EAAA,MAAA;EACe,SAAA,CAAA,EAAA,KAAA;EAAnB,YAAA,CAAA,EAAA,KAAA;CAEe,CAAA,EEa5B,+BFb4B,CEaI,SFbJ,EEae,KFbf,CAAA;AAAW,iBEc1B,aFd0B,CAAA,kBEetB,kBFfsB,EAAA,cEgB1B,kBFhB0B,CEgBP,SFhBO,CAAA,CAAA,CAAA,QAAA,EEkB9B,SFlB8B,EAAA,YAAA,EEmB1B,KFnB0B,EAAA,OAAA,EEoB/B,+BFpB+B,CEoBC,SFpBD,EEoBY,KFpBZ,CAAA,EAAA,OAAA,EAAA;EAAtC,QAAA,CAAA,EAAA,MAAA;EAE6B,SAAA,EAAA,MAAA;EAAW,YAAA,CAAA,EAAA,MAAA;CAAtC,CAAA,EEoBH,+BFpBG,CEoB6B,SFpB7B,EEoBwC,KFpBxC,CAAA;;;;;AAYN;;;;;;;;;;;ACVA;;;;;;;;;AAqDA;;;;;;;;;;;;;;;;;;;;;;AC5DA;;;;;;;;;;AAO8C,iBAwG9B,cAxG8B,CAAA,kBAwGG,kBAxGH,CAAA,CAAA,QAAA,EAyGlC,SAzGkC,EAAA,QAAA,EA0GlC,2BA1GkC,CA0GN,SA1GM,CAAA,CAAA,EA2G3C,2BA3G2C,CA2Gf,SA3Ge,CAAA"}
|
package/dist/index.d.mts
CHANGED
|
@@ -180,6 +180,7 @@ declare class TypedAmqpWorker<TContract extends ContractDefinition> {
|
|
|
180
180
|
private readonly actualHandlers;
|
|
181
181
|
private readonly consumerOptions;
|
|
182
182
|
private readonly batchTimers;
|
|
183
|
+
private readonly consumerTags;
|
|
183
184
|
private constructor();
|
|
184
185
|
/**
|
|
185
186
|
* Create a type-safe AMQP worker from a contract.
|
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;;;;AAOT,KAuB3D,0BAvB2D,CAAA,kBAwBnD,kBAxBmD,EAAA,cAyBvD,kBAzBuD,CAyBpC,SAzBoC,CAAA,CAAA,GAAA,CAAA,OAAA,EA0BzD,wBA1ByD,CA0BhC,SA1BgC,EA0BrB,KA1BqB,CAAA,EAAA,GA0BV,OA1BU,CAAA,IAAA,CAAA;AAAA;;;;AAQpD,KAwBP,+BAxBO,CAAA,kBAyBC,kBAzBD,EAAA,cA0BH,kBA1BG,CA0BgB,SA1BhB,CAAA,CAAA,GAAA,CAAA,QAAA,EA2BJ,KA3BI,CA2BE,wBA3BF,CA2B2B,SA3B3B,EA2BsC,KA3BtC,CAAA,CAAA,EAAA,GA2BkD,OA3BlD,CAAA,IAAA,CAAA;;;;AAKnB;;;;;AAGgD,KA6BpC,+BA7BoC,CAAA,kBA8B5B,kBA9B4B,EAAA,cA+BhC,kBA/BgC,CA+Bb,SA/Ba,CAAA,CAAA,GAiC5C,0BAjC4C,CAiCjB,SAjCiB,EAiCN,KAjCM,CAAA,GAAA,SAAA,CAmC1C,0BAnCiB,CAmCU,SAnCV,EAmCqB,KAnCrB,CAAA,EAAnB;EAAkB,QAAA,CAAA,EAAA,MAAA;EAOV,SAAA,CAAA,EAAA,KAAA;EACQ,YAAA,CAAA,EAAA,KAAA;AACe,CAAA,CAAnB,GAAA,SAAA,CA8BV,+BA7BiC,CA6BD,SA7BC,EA6BU,KA7BV,CAAA,EAAW;EAApC,QAAA,CAAA,EAAA,MAAA;EAA+C,SAAA,EAAA,MAAA;EAAO,YAAA,CAAA,EAAA,MAAA;AAMxD,CAAA,CACQ;;;;;AAEC,KA4BT,2BA5BS,CAAA,kBA4BqC,kBA5BrC,CAAA,GAAA,QA6Bb,kBA7BO,CA6BY,SA7BZ,CAAA,GA6ByB,+BA7BzB,CA6ByD,SA7BzD,EA6BoE,CA7BpE,CAAA,EAAsD;;;;;ADrCrE;AAaA;;;;AC7B8D;;;;;AAM5B;;;;;AAK8C;;;;;AAOT;;;;;;;;AAavE;;;;;;;AAGI,
|
|
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;;;;AAOT,KAuB3D,0BAvB2D,CAAA,kBAwBnD,kBAxBmD,EAAA,cAyBvD,kBAzBuD,CAyBpC,SAzBoC,CAAA,CAAA,GAAA,CAAA,OAAA,EA0BzD,wBA1ByD,CA0BhC,SA1BgC,EA0BrB,KA1BqB,CAAA,EAAA,GA0BV,OA1BU,CAAA,IAAA,CAAA;AAAA;;;;AAQpD,KAwBP,+BAxBO,CAAA,kBAyBC,kBAzBD,EAAA,cA0BH,kBA1BG,CA0BgB,SA1BhB,CAAA,CAAA,GAAA,CAAA,QAAA,EA2BJ,KA3BI,CA2BE,wBA3BF,CA2B2B,SA3B3B,EA2BsC,KA3BtC,CAAA,CAAA,EAAA,GA2BkD,OA3BlD,CAAA,IAAA,CAAA;;;;AAKnB;;;;;AAGgD,KA6BpC,+BA7BoC,CAAA,kBA8B5B,kBA9B4B,EAAA,cA+BhC,kBA/BgC,CA+Bb,SA/Ba,CAAA,CAAA,GAiC5C,0BAjC4C,CAiCjB,SAjCiB,EAiCN,KAjCM,CAAA,GAAA,SAAA,CAmC1C,0BAnCiB,CAmCU,SAnCV,EAmCqB,KAnCrB,CAAA,EAAnB;EAAkB,QAAA,CAAA,EAAA,MAAA;EAOV,SAAA,CAAA,EAAA,KAAA;EACQ,YAAA,CAAA,EAAA,KAAA;AACe,CAAA,CAAnB,GAAA,SAAA,CA8BV,+BA7BiC,CA6BD,SA7BC,EA6BU,KA7BV,CAAA,EAAW;EAApC,QAAA,CAAA,EAAA,MAAA;EAA+C,SAAA,EAAA,MAAA;EAAO,YAAA,CAAA,EAAA,MAAA;AAMxD,CAAA,CACQ;;;;;AAEC,KA4BT,2BA5BS,CAAA,kBA4BqC,kBA5BrC,CAAA,GAAA,QA6Bb,kBA7BO,CA6BY,SA7BZ,CAAA,GA6ByB,+BA7BzB,CA6ByD,SA7BzD,EA6BoE,CA7BpE,CAAA,EAAsD;;;;;ADrCrE;AAaA;;;;AC7B8D;;;;;AAM5B;;;;;AAK8C;;;;;AAOT;;;;;;;;AAavE;;;;;;;AAGI,KCqCQ,mBDrCR,CAAA,kBCqC8C,kBDrC9C,CAAA,GAAA;EAAkB;EAOV,QAAA,ECgCA,SDhCA;EACQ;EACe,QAAA,ECgCvB,2BDhCuB,CCgCK,SDhCL,CAAA;EAAnB;EACuB,IAAA,ECiC/B,aDjC+B,EAAA;EAAW;EAApC,iBAAA,CAAA,ECmCQ,4BDnCR,GAAA,SAAA;EAA+C;EAAO,MAAA,CAAA,ECqCzD,MDrCyD,GAAA,SAAA;AAMpE,CAAA;;;;;;;;;;AAaA;;;;;;;;;;;;;;AAkBA;;;;;;;;;;;ACVA;;;;;;AAQsB,cA6CT,eA7CS,CAAA,kBA6CyB,kBA7CzB,CAAA,CAAA;EAEX,iBAAA,QAAA;EAAM,iBAAA,UAAA;EA2CJ,iBAAA,MAAe;EAAmB,iBAAA,cAAA;EA2Eb,iBAAA,eAAA;EAC9B,iBAAA,WAAA;EACA,iBAAA,YAAA;EACA,QAAA,WAAA,CAAA;EACA;;;;;;;;;;;;;;;;AC3IJ;;;;;;;;;;;;;EAQgB,OAAA,MAAA,CAAA,kBD+HkB,kBC/HL,CAAA,CAAA;IAAA,QAAA;IAAA,QAAA;IAAA,IAAA;IAAA,iBAAA;IAAA;EAAA,CAAA,EDqIxB,mBCrIwB,CDqIJ,SCrII,CAAA,CAAA,EDqIS,MCrIT,CDqIgB,MCrIhB,CDqIuB,eCrIvB,CDqIuC,SCrIvC,CAAA,EDqImD,cCrInD,CAAA,CAAA;EACT;;;;;;;;;;;;AAQpB;;;;EAIY,KAAA,CAAA,CAAA,EDyJD,MCzJC,CDyJM,MCzJN,CAAA,IAAA,EDyJmB,cCzJnB,CAAA,CAAA;EACI;;;EACL,QAAA,UAAA;EAEwB,QAAA,sBAAA;EAAW;;;EAsF9B,QAAA,OAAA;EAAiC;;;;EAGlB,QAAA,uBAAA;EAA5B;;;;;;;;;;;;;;;;;AHlKH;AAaA;;;;AC7B8D;;;;;AAM5B;;;;;AAK8C;;;;;AAOT;;;;;;;;AAavE;;;;;;;;;AAUA;;;;;;;;;AASA;;;;;;;;;AAG4E,iBEW5D,aFX4D,CAAA,kBEYxD,kBFZwD,EAAA,cEa5D,kBFb4D,CEazC,SFbyC,CAAA,CAAA,CAAA,QAAA,EEehE,SFfgE,EAAA,YAAA,EEgB5D,KFhB4D,EAAA,OAAA,EEiBjE,0BFjBiE,CEiBtC,SFjBsC,EEiB3B,KFjB2B,CAAA,CAAA,EEkBzE,+BFlByE,CEkBzC,SFlByC,EEkB9B,KFlB8B,CAAA;AAUhE,iBESI,aFTJ,CAA+B,kBEUvB,kBFVuB,EAAA,cEW3B,kBFX2B,CEWR,SFXQ,CAAA,CAAA,CAAA,QAAA,EEa/B,SFb+B,EAAA,YAAA,EEc3B,KFd2B,EAAA,OAAA,EEehC,0BFfgC,CEeL,SFfK,EEeM,KFfN,CAAA,EAAA,OAAA,EAAA;EACvB,QAAA,CAAA,EAAA,MAAA;EACe,SAAA,CAAA,EAAA,KAAA;EAAnB,YAAA,CAAA,EAAA,KAAA;CAEe,CAAA,EEa5B,+BFb4B,CEaI,SFbJ,EEae,KFbf,CAAA;AAAW,iBEc1B,aFd0B,CAAA,kBEetB,kBFfsB,EAAA,cEgB1B,kBFhB0B,CEgBP,SFhBO,CAAA,CAAA,CAAA,QAAA,EEkB9B,SFlB8B,EAAA,YAAA,EEmB1B,KFnB0B,EAAA,OAAA,EEoB/B,+BFpB+B,CEoBC,SFpBD,EEoBY,KFpBZ,CAAA,EAAA,OAAA,EAAA;EAAtC,QAAA,CAAA,EAAA,MAAA;EAE6B,SAAA,EAAA,MAAA;EAAW,YAAA,CAAA,EAAA,MAAA;CAAtC,CAAA,EEoBH,+BFpBG,CEoB6B,SFpB7B,EEoBwC,KFpBxC,CAAA;;;;;AAYN;;;;;;;;;;;ACVA;;;;;;;;;AAqDA;;;;;;;;;;;;;;;;;;;;;;AC5DA;;;;;;;;;;AAO8C,iBAwG9B,cAxG8B,CAAA,kBAwGG,kBAxGH,CAAA,CAAA,QAAA,EAyGlC,SAzGkC,EAAA,QAAA,EA0GlC,2BA1GkC,CA0GN,SA1GM,CAAA,CAAA,EA2G3C,2BA3G2C,CA2Gf,SA3Ge,CAAA"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { AmqpClient } from "@amqp-contract/core";
|
|
2
2
|
import { Future, Result } from "@swan-io/boxed";
|
|
3
|
+
import { gunzip, inflate } from "node:zlib";
|
|
4
|
+
import { promisify } from "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 = promisify(gunzip);
|
|
44
|
+
const inflateAsync = promisify(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
|
/**
|
|
@@ -82,6 +107,7 @@ var TypedAmqpWorker = class TypedAmqpWorker {
|
|
|
82
107
|
actualHandlers;
|
|
83
108
|
consumerOptions;
|
|
84
109
|
batchTimers = /* @__PURE__ */ new Map();
|
|
110
|
+
consumerTags = /* @__PURE__ */ new Set();
|
|
85
111
|
constructor(contract, amqpClient, handlers, logger) {
|
|
86
112
|
this.contract = contract;
|
|
87
113
|
this.amqpClient = amqpClient;
|
|
@@ -151,7 +177,15 @@ var TypedAmqpWorker = class TypedAmqpWorker {
|
|
|
151
177
|
close() {
|
|
152
178
|
for (const timer of this.batchTimers.values()) clearTimeout(timer);
|
|
153
179
|
this.batchTimers.clear();
|
|
154
|
-
return Future.
|
|
180
|
+
return Future.all(Array.from(this.consumerTags).map((consumerTag) => 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 Result.Ok(void 0);
|
|
186
|
+
}))).map(Result.all).tapOk(() => {
|
|
187
|
+
this.consumerTags.clear();
|
|
188
|
+
}).flatMapOk(() => Future.fromPromise(this.amqpClient.close())).mapError((error) => new TechnicalError("Failed to close AMQP connection", error)).mapOk(() => void 0);
|
|
155
189
|
}
|
|
156
190
|
/**
|
|
157
191
|
* Start consuming messages for all consumers
|
|
@@ -208,30 +242,45 @@ var TypedAmqpWorker = class TypedAmqpWorker {
|
|
|
208
242
|
* @returns Future<Result<validated message, void>> - Ok with validated message, or Error (already handled with nack)
|
|
209
243
|
*/
|
|
210
244
|
parseAndValidateMessage(msg, consumer, consumerName) {
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
this.logger?.error("Error parsing message", {
|
|
245
|
+
const decompressMessage = Future.fromPromise(decompressBuffer(msg.content, msg.properties.contentEncoding)).mapError((error) => {
|
|
246
|
+
this.logger?.error("Error decompressing message", {
|
|
214
247
|
consumerName: String(consumerName),
|
|
215
248
|
queueName: consumer.queue.name,
|
|
216
|
-
|
|
249
|
+
contentEncoding: msg.properties.contentEncoding,
|
|
250
|
+
error
|
|
217
251
|
});
|
|
218
252
|
this.amqpClient.channel.nack(msg, false, false);
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
const error = new MessageValidationError(String(consumerName), validationResult.issues);
|
|
225
|
-
this.logger?.error("Message validation failed", {
|
|
253
|
+
});
|
|
254
|
+
const parseMessage = (buffer) => {
|
|
255
|
+
const parseResult = Result.fromExecution(() => JSON.parse(buffer.toString()));
|
|
256
|
+
if (parseResult.isError()) {
|
|
257
|
+
this.logger?.error("Error parsing message", {
|
|
226
258
|
consumerName: String(consumerName),
|
|
227
259
|
queueName: consumer.queue.name,
|
|
228
|
-
error
|
|
260
|
+
error: parseResult.error
|
|
229
261
|
});
|
|
230
262
|
this.amqpClient.channel.nack(msg, false, false);
|
|
231
|
-
return Result.Error(void 0);
|
|
263
|
+
return Future.value(Result.Error(void 0));
|
|
232
264
|
}
|
|
233
|
-
return Result.Ok(
|
|
234
|
-
}
|
|
265
|
+
return Future.value(Result.Ok(parseResult.value));
|
|
266
|
+
};
|
|
267
|
+
const validateMessage = (parsedMessage) => {
|
|
268
|
+
const rawValidation = consumer.message.payload["~standard"].validate(parsedMessage);
|
|
269
|
+
return 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 Result.Error(void 0);
|
|
279
|
+
}
|
|
280
|
+
return Result.Ok(validationResult.value);
|
|
281
|
+
});
|
|
282
|
+
};
|
|
283
|
+
return decompressMessage.flatMapOk(parseMessage).flatMapOk(validateMessage);
|
|
235
284
|
}
|
|
236
285
|
/**
|
|
237
286
|
* Consume messages one at a time
|
|
@@ -259,7 +308,9 @@ var TypedAmqpWorker = class TypedAmqpWorker {
|
|
|
259
308
|
});
|
|
260
309
|
this.amqpClient.channel.ack(msg);
|
|
261
310
|
}).toPromise();
|
|
262
|
-
})).
|
|
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);
|
|
263
314
|
}
|
|
264
315
|
/**
|
|
265
316
|
* Consume messages in batches
|
|
@@ -339,7 +390,9 @@ var TypedAmqpWorker = class TypedAmqpWorker {
|
|
|
339
390
|
await processBatch();
|
|
340
391
|
if (batch.length > 0 && !this.batchTimers.has(timerKey)) scheduleBatchProcessing();
|
|
341
392
|
} else if (!this.batchTimers.has(timerKey)) scheduleBatchProcessing();
|
|
342
|
-
})).
|
|
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);
|
|
343
396
|
}
|
|
344
397
|
};
|
|
345
398
|
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["cause?: unknown","consumerName: string","issues: unknown","contract: TContract","amqpClient: AmqpClient","logger?: Logger","batch: BatchItem[]"],"sources":["../src/errors.ts","../src/worker.ts","../src/handlers.ts"],"sourcesContent":["/**\n * Base error class for worker errors\n */\nabstract class WorkerError extends Error {\n protected constructor(message: string) {\n super(message);\n this.name = \"WorkerError\";\n // Node.js specific stack trace capture\n const ErrorConstructor = Error as unknown as {\n captureStackTrace?: (target: object, constructor: Function) => void;\n };\n if (typeof ErrorConstructor.captureStackTrace === \"function\") {\n ErrorConstructor.captureStackTrace(this, this.constructor);\n }\n }\n}\n\n/**\n * Error for technical/runtime failures in worker operations\n * This includes validation failures, parsing failures, and processing failures\n */\nexport class TechnicalError extends WorkerError {\n constructor(\n message: string,\n public override readonly cause?: unknown,\n ) {\n super(message);\n this.name = \"TechnicalError\";\n }\n}\n\n/**\n * Error thrown when message validation fails\n */\nexport class MessageValidationError extends WorkerError {\n constructor(\n public readonly consumerName: string,\n public readonly issues: unknown,\n ) {\n super(`Message validation failed for consumer \"${consumerName}\"`);\n this.name = \"MessageValidationError\";\n }\n}\n","import { AmqpClient, type Logger } from \"@amqp-contract/core\";\nimport type { AmqpConnectionManagerOptions, ConnectionUrl } from \"amqp-connection-manager\";\nimport type { Channel, Message } from \"amqplib\";\nimport type {\n ConsumerDefinition,\n ContractDefinition,\n InferConsumerNames,\n} from \"@amqp-contract/contract\";\nimport { Future, Result } from \"@swan-io/boxed\";\nimport { MessageValidationError, TechnicalError } from \"./errors.js\";\nimport type {\n WorkerInferConsumerBatchHandler,\n WorkerInferConsumerHandler,\n WorkerInferConsumerHandlers,\n WorkerInferConsumerInput,\n} from \"./types.js\";\n\n/**\n * Internal type for consumer options extracted from handler tuples.\n * Not exported - options are specified inline in the handler tuple types.\n * Uses discriminated union to enforce mutual exclusivity:\n * - Prefetch-only mode: Cannot have batchSize or batchTimeout\n * - Batch mode: Requires batchSize, allows batchTimeout and prefetch\n */\ntype ConsumerOptions =\n | {\n /** Prefetch-based processing (no batching) */\n prefetch?: number;\n batchSize?: never;\n batchTimeout?: never;\n }\n | {\n /** Batch-based processing */\n prefetch?: number;\n batchSize: number;\n batchTimeout?: number;\n };\n\n/**\n * Options for creating a type-safe AMQP worker.\n *\n * @typeParam TContract - The contract definition type\n *\n * @example\n * ```typescript\n * const options: CreateWorkerOptions<typeof contract> = {\n * contract: myContract,\n * handlers: {\n * // Simple handler\n * processOrder: async (message) => {\n * console.log('Processing order:', message.orderId);\n * },\n * // Handler with options (prefetch)\n * processPayment: [\n * async (message) => {\n * console.log('Processing payment:', message.paymentId);\n * },\n * { prefetch: 10 }\n * ],\n * // Handler with batch processing\n * processNotifications: [\n * async (messages) => {\n * console.log('Processing batch:', messages.length);\n * },\n * { batchSize: 5, batchTimeout: 1000 }\n * ]\n * },\n * urls: ['amqp://localhost'],\n * connectionOptions: {\n * heartbeatIntervalInSeconds: 30\n * },\n * logger: myLogger\n * };\n * ```\n */\nexport type CreateWorkerOptions<TContract extends ContractDefinition> = {\n /** The AMQP contract definition specifying consumers and their message schemas */\n contract: TContract;\n /** Handlers for each consumer defined in the contract. Can be a function or a tuple of [handler, options] */\n handlers: WorkerInferConsumerHandlers<TContract>;\n /** AMQP broker URL(s). Multiple URLs provide failover support */\n urls: ConnectionUrl[];\n /** Optional connection configuration (heartbeat, reconnect settings, etc.) */\n connectionOptions?: AmqpConnectionManagerOptions | undefined;\n /** Optional logger for logging message consumption and errors */\n logger?: Logger | undefined;\n};\n\n/**\n * Type-safe AMQP worker for consuming messages from RabbitMQ.\n *\n * This class provides automatic message validation, connection management,\n * and error handling for consuming messages based on a contract definition.\n *\n * @typeParam TContract - The contract definition type\n *\n * @example\n * ```typescript\n * import { TypedAmqpWorker } from '@amqp-contract/worker';\n * import { z } from 'zod';\n *\n * const contract = defineContract({\n * queues: {\n * orderProcessing: defineQueue('order-processing', { durable: true })\n * },\n * consumers: {\n * processOrder: defineConsumer('order-processing', z.object({\n * orderId: z.string(),\n * amount: z.number()\n * }))\n * }\n * });\n *\n * const worker = await TypedAmqpWorker.create({\n * contract,\n * handlers: {\n * processOrder: async (message) => {\n * console.log('Processing order', message.orderId);\n * // Process the order...\n * }\n * },\n * urls: ['amqp://localhost']\n * }).resultToPromise();\n *\n * // Close when done\n * await worker.close().resultToPromise();\n * ```\n */\nexport class TypedAmqpWorker<TContract extends ContractDefinition> {\n private readonly actualHandlers: Partial<\n Record<\n InferConsumerNames<TContract>,\n | WorkerInferConsumerHandler<TContract, InferConsumerNames<TContract>>\n | WorkerInferConsumerBatchHandler<TContract, InferConsumerNames<TContract>>\n >\n >;\n private readonly consumerOptions: Partial<Record<InferConsumerNames<TContract>, ConsumerOptions>>;\n private readonly batchTimers: Map<string, NodeJS.Timeout> = new Map();\n\n private constructor(\n private readonly contract: TContract,\n private readonly amqpClient: AmqpClient,\n handlers: WorkerInferConsumerHandlers<TContract>,\n private readonly logger?: Logger,\n ) {\n // Extract handlers and options from the handlers object\n this.actualHandlers = {};\n this.consumerOptions = {};\n\n for (const consumerName of Object.keys(handlers) as InferConsumerNames<TContract>[]) {\n const handlerEntry = handlers[consumerName];\n\n if (Array.isArray(handlerEntry)) {\n // Tuple format: [handler, options]\n // Type assertion is safe: The discriminated union in WorkerInferConsumerHandlerEntry\n // ensures the handler matches the options (single-message or batch handler).\n // TypeScript loses this type relationship during runtime extraction, but it's\n // guaranteed by the type system at compile time.\n this.actualHandlers[consumerName] = handlerEntry[0] as unknown as\n | WorkerInferConsumerHandler<TContract, InferConsumerNames<TContract>>\n | WorkerInferConsumerBatchHandler<TContract, InferConsumerNames<TContract>>;\n this.consumerOptions[consumerName] = handlerEntry[1];\n } else {\n // Direct function format\n // Type assertion is safe: handlerEntry is guaranteed to be a function type\n // by the discriminated union, but TypeScript needs help with the union narrowing.\n this.actualHandlers[consumerName] = handlerEntry as unknown as\n | WorkerInferConsumerHandler<TContract, InferConsumerNames<TContract>>\n | WorkerInferConsumerBatchHandler<TContract, InferConsumerNames<TContract>>;\n }\n }\n }\n\n /**\n * Create a type-safe AMQP worker from a contract.\n *\n * Connection management (including automatic reconnection) is handled internally\n * by amqp-connection-manager via the {@link AmqpClient}. The worker will set up\n * consumers for all contract-defined handlers asynchronously in the background\n * once the underlying connection and channels are ready.\n *\n * Connections are automatically shared across clients and workers with the same\n * URLs and connection options, following RabbitMQ best practices.\n *\n * @param options - Configuration options for the worker\n * @returns A Future that resolves to a Result containing the worker or an error\n *\n * @example\n * ```typescript\n * const workerResult = await TypedAmqpWorker.create({\n * contract: myContract,\n * handlers: {\n * processOrder: async (msg) => console.log('Order:', msg.orderId)\n * },\n * urls: ['amqp://localhost']\n * }).resultToPromise();\n *\n * if (workerResult.isError()) {\n * console.error('Failed to create worker:', workerResult.error);\n * }\n * ```\n */\n static create<TContract extends ContractDefinition>({\n contract,\n handlers,\n urls,\n connectionOptions,\n logger,\n }: CreateWorkerOptions<TContract>): Future<Result<TypedAmqpWorker<TContract>, TechnicalError>> {\n const worker = new TypedAmqpWorker(\n contract,\n new AmqpClient(contract, {\n urls,\n connectionOptions,\n }),\n handlers,\n logger,\n );\n\n return worker\n .waitForConnectionReady()\n .flatMapOk(() => worker.consumeAll())\n .mapOk(() => worker);\n }\n\n /**\n * Close the AMQP channel and connection.\n *\n * This gracefully closes the connection to the AMQP broker,\n * stopping all message consumption and cleaning up resources.\n *\n * @returns A Future that resolves to a Result indicating success or failure\n *\n * @example\n * ```typescript\n * const closeResult = await worker.close().resultToPromise();\n * if (closeResult.isOk()) {\n * console.log('Worker closed successfully');\n * }\n * ```\n */\n close(): Future<Result<void, TechnicalError>> {\n // Clear all pending batch timers\n for (const timer of this.batchTimers.values()) {\n clearTimeout(timer);\n }\n this.batchTimers.clear();\n\n return Future.fromPromise(this.amqpClient.close())\n .mapError((error) => new TechnicalError(\"Failed to close AMQP connection\", error))\n .mapOk(() => undefined);\n }\n\n /**\n * Start consuming messages for all consumers\n */\n private consumeAll(): Future<Result<void, TechnicalError>> {\n if (!this.contract.consumers) {\n return Future.value(Result.Error(new TechnicalError(\"No consumers defined in contract\")));\n }\n\n // Calculate the maximum prefetch value among all consumers\n // Since prefetch is per-channel in AMQP 0.9.1, we use the maximum value\n const consumerNames = Object.keys(this.contract.consumers) as InferConsumerNames<TContract>[];\n let maxPrefetch = 0;\n\n for (const consumerName of consumerNames) {\n const options = this.consumerOptions[consumerName];\n if (options?.prefetch !== undefined) {\n if (options.prefetch <= 0 || !Number.isInteger(options.prefetch)) {\n return Future.value(\n Result.Error(\n new TechnicalError(\n `Invalid prefetch value for \"${String(consumerName)}\": must be a positive integer`,\n ),\n ),\n );\n }\n maxPrefetch = Math.max(maxPrefetch, options.prefetch);\n }\n if (options?.batchSize !== undefined) {\n // Batch consumers need prefetch at least equal to batch size\n const effectivePrefetch = options.prefetch ?? options.batchSize;\n maxPrefetch = Math.max(maxPrefetch, effectivePrefetch);\n }\n }\n\n // Apply the maximum prefetch if any consumer specified it\n if (maxPrefetch > 0) {\n this.amqpClient.channel.addSetup(async (channel: Channel) => {\n await channel.prefetch(maxPrefetch);\n });\n }\n\n return Future.all(consumerNames.map((consumerName) => this.consume(consumerName)))\n .map(Result.all)\n .mapOk(() => undefined);\n }\n\n private waitForConnectionReady(): Future<Result<void, TechnicalError>> {\n return Future.fromPromise(this.amqpClient.channel.waitForConnect()).mapError(\n (error) => new TechnicalError(\"Failed to wait for connection ready\", error),\n );\n }\n\n /**\n * Start consuming messages for a specific consumer\n */\n private consume<TName extends InferConsumerNames<TContract>>(\n consumerName: TName,\n ): Future<Result<void, TechnicalError>> {\n const consumers = this.contract.consumers;\n if (!consumers) {\n return Future.value(Result.Error(new TechnicalError(\"No consumers defined in contract\")));\n }\n\n const consumer = consumers[consumerName as string];\n if (!consumer) {\n const availableConsumers = Object.keys(consumers);\n const available = availableConsumers.length > 0 ? availableConsumers.join(\", \") : \"none\";\n return Future.value(\n Result.Error(\n new TechnicalError(\n `Consumer not found: \"${String(consumerName)}\". Available consumers: ${available}`,\n ),\n ),\n );\n }\n\n const handler = this.actualHandlers[consumerName];\n if (!handler) {\n return Future.value(\n Result.Error(new TechnicalError(`Handler for \"${String(consumerName)}\" not provided`)),\n );\n }\n\n // Get consumer-specific options\n const options = this.consumerOptions[consumerName] ?? {};\n\n // Validate batch size if specified\n if (options.batchSize !== undefined) {\n if (options.batchSize <= 0 || !Number.isInteger(options.batchSize)) {\n return Future.value(\n Result.Error(\n new TechnicalError(\n `Invalid batchSize for \"${String(consumerName)}\": must be a positive integer`,\n ),\n ),\n );\n }\n }\n\n // Validate batch timeout if specified\n if (options.batchTimeout !== undefined) {\n if (\n typeof options.batchTimeout !== \"number\" ||\n !Number.isFinite(options.batchTimeout) ||\n options.batchTimeout <= 0\n ) {\n return Future.value(\n Result.Error(\n new TechnicalError(\n `Invalid batchTimeout for \"${String(consumerName)}\": must be a positive number`,\n ),\n ),\n );\n }\n }\n\n // Check if this is a batch consumer\n const isBatchConsumer = options.batchSize !== undefined && options.batchSize > 0;\n\n if (isBatchConsumer) {\n return this.consumeBatch(\n consumerName,\n consumer,\n options,\n handler as unknown as (\n messages: Array<WorkerInferConsumerInput<TContract, TName>>,\n ) => Promise<void>,\n );\n } else {\n return this.consumeSingle(\n consumerName,\n consumer,\n handler as unknown as (\n message: WorkerInferConsumerInput<TContract, TName>,\n ) => Promise<void>,\n );\n }\n }\n\n /**\n * Parse and validate a message from AMQP\n * @returns Future<Result<validated message, void>> - Ok with validated message, or Error (already handled with nack)\n */\n private parseAndValidateMessage<TName extends InferConsumerNames<TContract>>(\n msg: Message,\n consumer: ConsumerDefinition,\n consumerName: TName,\n ): Future<Result<WorkerInferConsumerInput<TContract, TName>, void>> {\n // Parse message\n const parseResult = Result.fromExecution(() => JSON.parse(msg.content.toString()));\n if (parseResult.isError()) {\n this.logger?.error(\"Error parsing message\", {\n consumerName: String(consumerName),\n queueName: consumer.queue.name,\n error: parseResult.error,\n });\n\n // fixme proper error handling strategy\n // Reject message with no requeue (malformed JSON)\n this.amqpClient.channel.nack(msg, false, false);\n return Future.value(Result.Error(undefined));\n }\n\n const rawValidation = consumer.message.payload[\"~standard\"].validate(parseResult.value);\n return Future.fromPromise(\n rawValidation instanceof Promise ? rawValidation : Promise.resolve(rawValidation),\n ).mapOkToResult((validationResult) => {\n if (validationResult.issues) {\n const error = new MessageValidationError(String(consumerName), validationResult.issues);\n this.logger?.error(\"Message validation failed\", {\n consumerName: String(consumerName),\n queueName: consumer.queue.name,\n error,\n });\n\n // fixme proper error handling strategy\n // Reject message with no requeue (validation failed)\n this.amqpClient.channel.nack(msg, false, false);\n return Result.Error(undefined) as Result<WorkerInferConsumerInput<TContract, TName>, void>;\n }\n\n return Result.Ok(validationResult.value as WorkerInferConsumerInput<TContract, TName>);\n }) as Future<Result<WorkerInferConsumerInput<TContract, TName>, void>>;\n }\n\n /**\n * Consume messages one at a time\n */\n private consumeSingle<TName extends InferConsumerNames<TContract>>(\n consumerName: TName,\n consumer: ConsumerDefinition,\n handler: (message: WorkerInferConsumerInput<TContract, TName>) => Promise<void>,\n ): Future<Result<void, TechnicalError>> {\n // Start consuming\n return Future.fromPromise(\n this.amqpClient.channel.consume(consumer.queue.name, async (msg) => {\n // Handle null messages (consumer cancellation)\n if (msg === null) {\n this.logger?.warn(\"Consumer cancelled by server\", {\n consumerName: String(consumerName),\n queueName: consumer.queue.name,\n });\n return;\n }\n\n // Parse and validate message\n await this.parseAndValidateMessage(msg, consumer, consumerName)\n .flatMapOk((validatedMessage) =>\n Future.fromPromise(handler(validatedMessage)).tapError((error) => {\n this.logger?.error(\"Error processing message\", {\n consumerName: String(consumerName),\n queueName: consumer.queue.name,\n error,\n });\n\n // fixme proper error handling strategy\n // Reject message and requeue (handler failed)\n this.amqpClient.channel.nack(msg, false, true);\n }),\n )\n .tapOk(() => {\n this.logger?.info(\"Message consumed successfully\", {\n consumerName: String(consumerName),\n queueName: consumer.queue.name,\n });\n\n // Acknowledge message\n this.amqpClient.channel.ack(msg);\n })\n .toPromise();\n }),\n )\n .mapError(\n (error) =>\n new TechnicalError(`Failed to start consuming for \"${String(consumerName)}\"`, error),\n )\n .mapOk(() => undefined);\n }\n\n /**\n * Consume messages in batches\n */\n private consumeBatch<TName extends InferConsumerNames<TContract>>(\n consumerName: TName,\n consumer: ConsumerDefinition,\n options: ConsumerOptions,\n handler: (messages: Array<WorkerInferConsumerInput<TContract, TName>>) => Promise<void>,\n ): Future<Result<void, TechnicalError>> {\n const batchSize = options.batchSize!;\n const batchTimeout = options.batchTimeout ?? 1000;\n const timerKey = String(consumerName);\n\n // Note: Prefetch is handled globally in consumeAll()\n // Batch accumulation state\n type BatchItem = {\n message: WorkerInferConsumerInput<TContract, TName>;\n amqpMessage: Message;\n };\n let batch: BatchItem[] = [];\n // Track if batch processing is currently in progress to avoid race conditions\n let isProcessing = false;\n\n const processBatch = async () => {\n // Prevent concurrent batch processing\n if (isProcessing || batch.length === 0) {\n return;\n }\n\n isProcessing = true;\n\n const currentBatch = batch;\n batch = [];\n\n // Clear timer from tracking\n const timer = this.batchTimers.get(timerKey);\n if (timer) {\n clearTimeout(timer);\n this.batchTimers.delete(timerKey);\n }\n\n const messages = currentBatch.map((item) => item.message);\n\n this.logger?.info(\"Processing batch\", {\n consumerName: String(consumerName),\n queueName: consumer.queue.name,\n batchSize: currentBatch.length,\n });\n\n try {\n await handler(messages);\n\n // Acknowledge all messages in the batch\n for (const item of currentBatch) {\n this.amqpClient.channel.ack(item.amqpMessage);\n }\n\n this.logger?.info(\"Batch processed successfully\", {\n consumerName: String(consumerName),\n queueName: consumer.queue.name,\n batchSize: currentBatch.length,\n });\n } catch (error) {\n this.logger?.error(\"Error processing batch\", {\n consumerName: String(consumerName),\n queueName: consumer.queue.name,\n batchSize: currentBatch.length,\n error,\n });\n\n // fixme proper error handling strategy\n // Reject all messages and requeue (handler failed)\n for (const item of currentBatch) {\n this.amqpClient.channel.nack(item.amqpMessage, false, true);\n }\n } finally {\n isProcessing = false;\n }\n };\n\n const scheduleBatchProcessing = () => {\n // Don't schedule if batch is currently being processed\n if (isProcessing) {\n return;\n }\n\n // Clear existing timer\n const existingTimer = this.batchTimers.get(timerKey);\n if (existingTimer) {\n clearTimeout(existingTimer);\n }\n\n // Schedule new timer and track it\n const timer = setTimeout(() => {\n processBatch().catch((error) => {\n this.logger?.error(\"Unexpected error in batch processing\", {\n consumerName: String(consumerName),\n error,\n });\n });\n }, batchTimeout);\n\n this.batchTimers.set(timerKey, timer);\n };\n\n // Start consuming\n return Future.fromPromise(\n this.amqpClient.channel.consume(consumer.queue.name, async (msg) => {\n // Handle null messages (consumer cancellation)\n if (msg === null) {\n this.logger?.warn(\"Consumer cancelled by server\", {\n consumerName: String(consumerName),\n queueName: consumer.queue.name,\n });\n // Process any remaining messages in the batch\n await processBatch();\n return;\n }\n\n // Parse and validate message\n const validationResult = await this.parseAndValidateMessage(\n msg,\n consumer,\n consumerName,\n ).toPromise();\n\n if (validationResult.isError()) {\n // Error already handled in parseAndValidateMessage (nacked)\n return;\n }\n\n // Add to batch\n batch.push({\n message: validationResult.value,\n amqpMessage: msg,\n });\n\n // Process batch if full\n if (batch.length >= batchSize) {\n await processBatch();\n // After processing a full batch, schedule timer for any subsequent messages\n // This ensures that if more messages arrive at a slow rate, they won't be held indefinitely\n if (batch.length > 0 && !this.batchTimers.has(timerKey)) {\n scheduleBatchProcessing();\n }\n } else {\n // Schedule batch processing if not already scheduled\n if (!this.batchTimers.has(timerKey)) {\n scheduleBatchProcessing();\n }\n }\n }),\n )\n .mapError(\n (error) =>\n new TechnicalError(`Failed to start consuming for \"${String(consumerName)}\"`, error),\n )\n .mapOk(() => undefined);\n }\n}\n","import type { ContractDefinition, InferConsumerNames } from \"@amqp-contract/contract\";\nimport type {\n WorkerInferConsumerBatchHandler,\n WorkerInferConsumerHandler,\n WorkerInferConsumerHandlerEntry,\n WorkerInferConsumerHandlers,\n} from \"./types.js\";\n\n/**\n * Define a type-safe handler for a specific consumer in a contract.\n *\n * This utility allows you to define handlers outside of the worker creation,\n * providing better code organization and reusability.\n *\n * Supports three patterns:\n * 1. Simple handler: just the function (single message handler)\n * 2. Handler with prefetch: [handler, { prefetch: 10 }] (single message handler with config)\n * 3. Batch handler: [batchHandler, { batchSize: 5, batchTimeout: 1000 }] (REQUIRES batchSize config)\n *\n * **Important**: Batch handlers (handlers that accept an array of messages) MUST include\n * batchSize configuration. You cannot create a batch handler without specifying batchSize.\n *\n * @template TContract - The contract definition type\n * @template TName - The consumer name from the contract\n * @param contract - The contract definition containing the consumer\n * @param consumerName - The name of the consumer from the contract\n * @param handler - The async handler function that processes messages (single or batch)\n * @param options - Optional consumer options (prefetch, batchSize, batchTimeout)\n * - For single-message handlers: { prefetch?: number } is optional\n * - For batch handlers: { batchSize: number, batchTimeout?: number } is REQUIRED\n * @returns A type-safe handler that can be used with TypedAmqpWorker\n *\n * @example\n * ```typescript\n * import { defineHandler } from '@amqp-contract/worker';\n * import { orderContract } from './contract';\n *\n * // Simple single-message handler without options\n * const processOrderHandler = defineHandler(\n * orderContract,\n * 'processOrder',\n * async (message) => {\n * console.log('Processing order:', message.orderId);\n * await processPayment(message);\n * }\n * );\n *\n * // Single-message handler with prefetch\n * const processOrderWithPrefetch = defineHandler(\n * orderContract,\n * 'processOrder',\n * async (message) => {\n * await processOrder(message);\n * },\n * { prefetch: 10 }\n * );\n *\n * // Batch handler - MUST include batchSize\n * const processBatchOrders = defineHandler(\n * orderContract,\n * 'processOrders',\n * async (messages) => {\n * // messages is an array - batchSize configuration is REQUIRED\n * await db.insertMany(messages);\n * },\n * { batchSize: 5, batchTimeout: 1000 }\n * );\n * ```\n */\nexport function defineHandler<\n TContract extends ContractDefinition,\n TName extends InferConsumerNames<TContract>,\n>(\n contract: TContract,\n consumerName: TName,\n handler: WorkerInferConsumerHandler<TContract, TName>,\n): WorkerInferConsumerHandlerEntry<TContract, TName>;\nexport function defineHandler<\n TContract extends ContractDefinition,\n TName extends InferConsumerNames<TContract>,\n>(\n contract: TContract,\n consumerName: TName,\n handler: WorkerInferConsumerHandler<TContract, TName>,\n options: { prefetch?: number; batchSize?: never; batchTimeout?: never },\n): WorkerInferConsumerHandlerEntry<TContract, TName>;\nexport function defineHandler<\n TContract extends ContractDefinition,\n TName extends InferConsumerNames<TContract>,\n>(\n contract: TContract,\n consumerName: TName,\n handler: WorkerInferConsumerBatchHandler<TContract, TName>,\n options: { prefetch?: number; batchSize: number; batchTimeout?: number },\n): WorkerInferConsumerHandlerEntry<TContract, TName>;\nexport function defineHandler<\n TContract extends ContractDefinition,\n TName extends InferConsumerNames<TContract>,\n>(\n contract: TContract,\n consumerName: TName,\n handler:\n | WorkerInferConsumerHandler<TContract, TName>\n | WorkerInferConsumerBatchHandler<TContract, TName>,\n options?: { prefetch?: number; batchSize?: number; batchTimeout?: number },\n): WorkerInferConsumerHandlerEntry<TContract, TName> {\n // Validate that the consumer exists in the contract\n const consumers = contract.consumers;\n\n if (!consumers || !(consumerName in consumers)) {\n const availableConsumers = consumers ? Object.keys(consumers) : [];\n const available = availableConsumers.length > 0 ? availableConsumers.join(\", \") : \"none\";\n throw new Error(\n `Consumer \"${String(consumerName)}\" not found in contract. Available consumers: ${available}`,\n );\n }\n\n // Return the handler with options if provided, otherwise just the handler\n if (options) {\n return [handler, options] as WorkerInferConsumerHandlerEntry<TContract, TName>;\n }\n return handler as WorkerInferConsumerHandlerEntry<TContract, TName>;\n}\n\n/**\n * Define multiple type-safe handlers for consumers in a contract.\n *\n * This utility allows you to define all handlers at once outside of the worker creation,\n * ensuring type safety and providing better code organization.\n *\n * @template TContract - The contract definition type\n * @param contract - The contract definition containing the consumers\n * @param handlers - An object with async handler functions for each consumer\n * @returns A type-safe handlers object that can be used with TypedAmqpWorker\n *\n * @example\n * ```typescript\n * import { defineHandlers } from '@amqp-contract/worker';\n * import { orderContract } from './contract';\n *\n * // Define all handlers at once\n * const handlers = defineHandlers(orderContract, {\n * processOrder: async (message) => {\n * // message is fully typed based on the contract\n * console.log('Processing order:', message.orderId);\n * await processPayment(message);\n * },\n * notifyOrder: async (message) => {\n * await sendNotification(message);\n * },\n * shipOrder: async (message) => {\n * await prepareShipment(message);\n * },\n * });\n *\n * // Use the handlers in worker\n * const worker = await TypedAmqpWorker.create({\n * contract: orderContract,\n * handlers,\n * connection: 'amqp://localhost',\n * });\n * ```\n *\n * @example\n * ```typescript\n * // Separate handler definitions for better organization\n * async function handleProcessOrder(message: WorkerInferConsumerInput<typeof orderContract, 'processOrder'>) {\n * await processOrder(message);\n * }\n *\n * async function handleNotifyOrder(message: WorkerInferConsumerInput<typeof orderContract, 'notifyOrder'>) {\n * await sendNotification(message);\n * }\n *\n * const handlers = defineHandlers(orderContract, {\n * processOrder: handleProcessOrder,\n * notifyOrder: handleNotifyOrder,\n * });\n * ```\n */\nexport function defineHandlers<TContract extends ContractDefinition>(\n contract: TContract,\n handlers: WorkerInferConsumerHandlers<TContract>,\n): WorkerInferConsumerHandlers<TContract> {\n // Validate that all consumers in handlers exist in the contract\n const consumers = contract.consumers;\n const availableConsumers = Object.keys(consumers ?? {});\n const availableConsumerNames =\n availableConsumers.length > 0 ? availableConsumers.join(\", \") : \"none\";\n\n for (const handlerName of Object.keys(handlers)) {\n if (!consumers || !(handlerName in consumers)) {\n throw new Error(\n `Consumer \"${handlerName}\" not found in contract. Available consumers: ${availableConsumerNames}`,\n );\n }\n }\n\n // Return the handlers as-is, with type checking enforced\n return handlers;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["cause?: unknown","consumerName: string","issues: unknown","contract: TContract","amqpClient: AmqpClient","logger?: Logger","batch: BatchItem[]"],"sources":["../src/errors.ts","../src/decompression.ts","../src/worker.ts","../src/handlers.ts"],"sourcesContent":["/**\n * Base error class for worker errors\n */\nabstract class WorkerError extends Error {\n protected constructor(message: string) {\n super(message);\n this.name = \"WorkerError\";\n // Node.js specific stack trace capture\n const ErrorConstructor = Error as unknown as {\n captureStackTrace?: (target: object, constructor: Function) => void;\n };\n if (typeof ErrorConstructor.captureStackTrace === \"function\") {\n ErrorConstructor.captureStackTrace(this, this.constructor);\n }\n }\n}\n\n/**\n * Error for technical/runtime failures in worker operations\n * This includes validation failures, parsing failures, and processing failures\n */\nexport class TechnicalError extends WorkerError {\n constructor(\n message: string,\n public override readonly cause?: unknown,\n ) {\n super(message);\n this.name = \"TechnicalError\";\n }\n}\n\n/**\n * Error thrown when message validation fails\n */\nexport class MessageValidationError extends WorkerError {\n constructor(\n public readonly consumerName: string,\n public readonly issues: unknown,\n ) {\n super(`Message validation failed for consumer \"${consumerName}\"`);\n this.name = \"MessageValidationError\";\n }\n}\n","import { gunzip, inflate } from \"node:zlib\";\nimport { promisify } from \"node:util\";\n\nconst gunzipAsync = promisify(gunzip);\nconst inflateAsync = promisify(inflate);\n\n/**\n * Decompress a buffer based on the content-encoding header.\n *\n * @param buffer - The buffer to decompress\n * @param contentEncoding - The content-encoding header value (e.g., 'gzip', 'deflate')\n * @returns A promise that resolves to the decompressed buffer\n * @throws Error if decompression fails or if the encoding is unsupported\n *\n * @internal\n */\nexport async function decompressBuffer(\n buffer: Buffer,\n contentEncoding: string | undefined,\n): Promise<Buffer> {\n if (!contentEncoding) {\n return buffer; // No compression\n }\n\n switch (contentEncoding.toLowerCase()) {\n case \"gzip\":\n return gunzipAsync(buffer);\n case \"deflate\":\n return inflateAsync(buffer);\n default:\n throw new Error(`Unsupported content-encoding: ${contentEncoding}`);\n }\n}\n","import { AmqpClient, type Logger } from \"@amqp-contract/core\";\nimport type { AmqpConnectionManagerOptions, ConnectionUrl } from \"amqp-connection-manager\";\nimport type { Channel, Message } from \"amqplib\";\nimport type {\n ConsumerDefinition,\n ContractDefinition,\n InferConsumerNames,\n} from \"@amqp-contract/contract\";\nimport { Future, Result } from \"@swan-io/boxed\";\nimport { MessageValidationError, TechnicalError } from \"./errors.js\";\nimport type {\n WorkerInferConsumerBatchHandler,\n WorkerInferConsumerHandler,\n WorkerInferConsumerHandlers,\n WorkerInferConsumerInput,\n} from \"./types.js\";\nimport { decompressBuffer } from \"./decompression.js\";\n\n/**\n * Internal type for consumer options extracted from handler tuples.\n * Not exported - options are specified inline in the handler tuple types.\n * Uses discriminated union to enforce mutual exclusivity:\n * - Prefetch-only mode: Cannot have batchSize or batchTimeout\n * - Batch mode: Requires batchSize, allows batchTimeout and prefetch\n */\ntype ConsumerOptions =\n | {\n /** Prefetch-based processing (no batching) */\n prefetch?: number;\n batchSize?: never;\n batchTimeout?: never;\n }\n | {\n /** Batch-based processing */\n prefetch?: number;\n batchSize: number;\n batchTimeout?: number;\n };\n\n/**\n * Options for creating a type-safe AMQP worker.\n *\n * @typeParam TContract - The contract definition type\n *\n * @example\n * ```typescript\n * const options: CreateWorkerOptions<typeof contract> = {\n * contract: myContract,\n * handlers: {\n * // Simple handler\n * processOrder: async (message) => {\n * console.log('Processing order:', message.orderId);\n * },\n * // Handler with options (prefetch)\n * processPayment: [\n * async (message) => {\n * console.log('Processing payment:', message.paymentId);\n * },\n * { prefetch: 10 }\n * ],\n * // Handler with batch processing\n * processNotifications: [\n * async (messages) => {\n * console.log('Processing batch:', messages.length);\n * },\n * { batchSize: 5, batchTimeout: 1000 }\n * ]\n * },\n * urls: ['amqp://localhost'],\n * connectionOptions: {\n * heartbeatIntervalInSeconds: 30\n * },\n * logger: myLogger\n * };\n * ```\n */\nexport type CreateWorkerOptions<TContract extends ContractDefinition> = {\n /** The AMQP contract definition specifying consumers and their message schemas */\n contract: TContract;\n /** Handlers for each consumer defined in the contract. Can be a function or a tuple of [handler, options] */\n handlers: WorkerInferConsumerHandlers<TContract>;\n /** AMQP broker URL(s). Multiple URLs provide failover support */\n urls: ConnectionUrl[];\n /** Optional connection configuration (heartbeat, reconnect settings, etc.) */\n connectionOptions?: AmqpConnectionManagerOptions | undefined;\n /** Optional logger for logging message consumption and errors */\n logger?: Logger | undefined;\n};\n\n/**\n * Type-safe AMQP worker for consuming messages from RabbitMQ.\n *\n * This class provides automatic message validation, connection management,\n * and error handling for consuming messages based on a contract definition.\n *\n * @typeParam TContract - The contract definition type\n *\n * @example\n * ```typescript\n * import { TypedAmqpWorker } from '@amqp-contract/worker';\n * import { z } from 'zod';\n *\n * const contract = defineContract({\n * queues: {\n * orderProcessing: defineQueue('order-processing', { durable: true })\n * },\n * consumers: {\n * processOrder: defineConsumer('order-processing', z.object({\n * orderId: z.string(),\n * amount: z.number()\n * }))\n * }\n * });\n *\n * const worker = await TypedAmqpWorker.create({\n * contract,\n * handlers: {\n * processOrder: async (message) => {\n * console.log('Processing order', message.orderId);\n * // Process the order...\n * }\n * },\n * urls: ['amqp://localhost']\n * }).resultToPromise();\n *\n * // Close when done\n * await worker.close().resultToPromise();\n * ```\n */\nexport class TypedAmqpWorker<TContract extends ContractDefinition> {\n private readonly actualHandlers: Partial<\n Record<\n InferConsumerNames<TContract>,\n | WorkerInferConsumerHandler<TContract, InferConsumerNames<TContract>>\n | WorkerInferConsumerBatchHandler<TContract, InferConsumerNames<TContract>>\n >\n >;\n private readonly consumerOptions: Partial<Record<InferConsumerNames<TContract>, ConsumerOptions>>;\n private readonly batchTimers: Map<string, NodeJS.Timeout> = new Map();\n private readonly consumerTags: Set<string> = new Set();\n\n private constructor(\n private readonly contract: TContract,\n private readonly amqpClient: AmqpClient,\n handlers: WorkerInferConsumerHandlers<TContract>,\n private readonly logger?: Logger,\n ) {\n // Extract handlers and options from the handlers object\n this.actualHandlers = {};\n this.consumerOptions = {};\n\n for (const consumerName of Object.keys(handlers) as InferConsumerNames<TContract>[]) {\n const handlerEntry = handlers[consumerName];\n\n if (Array.isArray(handlerEntry)) {\n // Tuple format: [handler, options]\n // Type assertion is safe: The discriminated union in WorkerInferConsumerHandlerEntry\n // ensures the handler matches the options (single-message or batch handler).\n // TypeScript loses this type relationship during runtime extraction, but it's\n // guaranteed by the type system at compile time.\n this.actualHandlers[consumerName] = handlerEntry[0] as unknown as\n | WorkerInferConsumerHandler<TContract, InferConsumerNames<TContract>>\n | WorkerInferConsumerBatchHandler<TContract, InferConsumerNames<TContract>>;\n this.consumerOptions[consumerName] = handlerEntry[1];\n } else {\n // Direct function format\n // Type assertion is safe: handlerEntry is guaranteed to be a function type\n // by the discriminated union, but TypeScript needs help with the union narrowing.\n this.actualHandlers[consumerName] = handlerEntry as unknown as\n | WorkerInferConsumerHandler<TContract, InferConsumerNames<TContract>>\n | WorkerInferConsumerBatchHandler<TContract, InferConsumerNames<TContract>>;\n }\n }\n }\n\n /**\n * Create a type-safe AMQP worker from a contract.\n *\n * Connection management (including automatic reconnection) is handled internally\n * by amqp-connection-manager via the {@link AmqpClient}. The worker will set up\n * consumers for all contract-defined handlers asynchronously in the background\n * once the underlying connection and channels are ready.\n *\n * Connections are automatically shared across clients and workers with the same\n * URLs and connection options, following RabbitMQ best practices.\n *\n * @param options - Configuration options for the worker\n * @returns A Future that resolves to a Result containing the worker or an error\n *\n * @example\n * ```typescript\n * const workerResult = await TypedAmqpWorker.create({\n * contract: myContract,\n * handlers: {\n * processOrder: async (msg) => console.log('Order:', msg.orderId)\n * },\n * urls: ['amqp://localhost']\n * }).resultToPromise();\n *\n * if (workerResult.isError()) {\n * console.error('Failed to create worker:', workerResult.error);\n * }\n * ```\n */\n static create<TContract extends ContractDefinition>({\n contract,\n handlers,\n urls,\n connectionOptions,\n logger,\n }: CreateWorkerOptions<TContract>): Future<Result<TypedAmqpWorker<TContract>, TechnicalError>> {\n const worker = new TypedAmqpWorker(\n contract,\n new AmqpClient(contract, {\n urls,\n connectionOptions,\n }),\n handlers,\n logger,\n );\n\n return worker\n .waitForConnectionReady()\n .flatMapOk(() => worker.consumeAll())\n .mapOk(() => worker);\n }\n\n /**\n * Close the AMQP channel and connection.\n *\n * This gracefully closes the connection to the AMQP broker,\n * stopping all message consumption and cleaning up resources.\n *\n * @returns A Future that resolves to a Result indicating success or failure\n *\n * @example\n * ```typescript\n * const closeResult = await worker.close().resultToPromise();\n * if (closeResult.isOk()) {\n * console.log('Worker closed successfully');\n * }\n * ```\n */\n close(): Future<Result<void, TechnicalError>> {\n // Clear all pending batch timers\n for (const timer of this.batchTimers.values()) {\n clearTimeout(timer);\n }\n this.batchTimers.clear();\n\n return Future.all(\n Array.from(this.consumerTags).map((consumerTag) =>\n Future.fromPromise(this.amqpClient.channel.cancel(consumerTag)).mapErrorToResult(\n (error) => {\n this.logger?.warn(\"Failed to cancel consumer during close\", { consumerTag, error });\n return Result.Ok(undefined);\n },\n ),\n ),\n )\n .map(Result.all)\n .tapOk(() => {\n // Clear consumer tags after successful cancellation\n this.consumerTags.clear();\n })\n .flatMapOk(() => Future.fromPromise(this.amqpClient.close()))\n .mapError((error) => new TechnicalError(\"Failed to close AMQP connection\", error))\n .mapOk(() => undefined);\n }\n\n /**\n * Start consuming messages for all consumers\n */\n private consumeAll(): Future<Result<void, TechnicalError>> {\n if (!this.contract.consumers) {\n return Future.value(Result.Error(new TechnicalError(\"No consumers defined in contract\")));\n }\n\n // Calculate the maximum prefetch value among all consumers\n // Since prefetch is per-channel in AMQP 0.9.1, we use the maximum value\n const consumerNames = Object.keys(this.contract.consumers) as InferConsumerNames<TContract>[];\n let maxPrefetch = 0;\n\n for (const consumerName of consumerNames) {\n const options = this.consumerOptions[consumerName];\n if (options?.prefetch !== undefined) {\n if (options.prefetch <= 0 || !Number.isInteger(options.prefetch)) {\n return Future.value(\n Result.Error(\n new TechnicalError(\n `Invalid prefetch value for \"${String(consumerName)}\": must be a positive integer`,\n ),\n ),\n );\n }\n maxPrefetch = Math.max(maxPrefetch, options.prefetch);\n }\n if (options?.batchSize !== undefined) {\n // Batch consumers need prefetch at least equal to batch size\n const effectivePrefetch = options.prefetch ?? options.batchSize;\n maxPrefetch = Math.max(maxPrefetch, effectivePrefetch);\n }\n }\n\n // Apply the maximum prefetch if any consumer specified it\n if (maxPrefetch > 0) {\n this.amqpClient.channel.addSetup(async (channel: Channel) => {\n await channel.prefetch(maxPrefetch);\n });\n }\n\n return Future.all(consumerNames.map((consumerName) => this.consume(consumerName)))\n .map(Result.all)\n .mapOk(() => undefined);\n }\n\n private waitForConnectionReady(): Future<Result<void, TechnicalError>> {\n return Future.fromPromise(this.amqpClient.channel.waitForConnect()).mapError(\n (error) => new TechnicalError(\"Failed to wait for connection ready\", error),\n );\n }\n\n /**\n * Start consuming messages for a specific consumer\n */\n private consume<TName extends InferConsumerNames<TContract>>(\n consumerName: TName,\n ): Future<Result<void, TechnicalError>> {\n const consumers = this.contract.consumers;\n if (!consumers) {\n return Future.value(Result.Error(new TechnicalError(\"No consumers defined in contract\")));\n }\n\n const consumer = consumers[consumerName as string];\n if (!consumer) {\n const availableConsumers = Object.keys(consumers);\n const available = availableConsumers.length > 0 ? availableConsumers.join(\", \") : \"none\";\n return Future.value(\n Result.Error(\n new TechnicalError(\n `Consumer not found: \"${String(consumerName)}\". Available consumers: ${available}`,\n ),\n ),\n );\n }\n\n const handler = this.actualHandlers[consumerName];\n if (!handler) {\n return Future.value(\n Result.Error(new TechnicalError(`Handler for \"${String(consumerName)}\" not provided`)),\n );\n }\n\n // Get consumer-specific options\n const options = this.consumerOptions[consumerName] ?? {};\n\n // Validate batch size if specified\n if (options.batchSize !== undefined) {\n if (options.batchSize <= 0 || !Number.isInteger(options.batchSize)) {\n return Future.value(\n Result.Error(\n new TechnicalError(\n `Invalid batchSize for \"${String(consumerName)}\": must be a positive integer`,\n ),\n ),\n );\n }\n }\n\n // Validate batch timeout if specified\n if (options.batchTimeout !== undefined) {\n if (\n typeof options.batchTimeout !== \"number\" ||\n !Number.isFinite(options.batchTimeout) ||\n options.batchTimeout <= 0\n ) {\n return Future.value(\n Result.Error(\n new TechnicalError(\n `Invalid batchTimeout for \"${String(consumerName)}\": must be a positive number`,\n ),\n ),\n );\n }\n }\n\n // Check if this is a batch consumer\n const isBatchConsumer = options.batchSize !== undefined && options.batchSize > 0;\n\n if (isBatchConsumer) {\n return this.consumeBatch(\n consumerName,\n consumer,\n options,\n handler as unknown as (\n messages: Array<WorkerInferConsumerInput<TContract, TName>>,\n ) => Promise<void>,\n );\n } else {\n return this.consumeSingle(\n consumerName,\n consumer,\n handler as unknown as (\n message: WorkerInferConsumerInput<TContract, TName>,\n ) => Promise<void>,\n );\n }\n }\n\n /**\n * Parse and validate a message from AMQP\n * @returns Future<Result<validated message, void>> - Ok with validated message, or Error (already handled with nack)\n */\n private parseAndValidateMessage<TName extends InferConsumerNames<TContract>>(\n msg: Message,\n consumer: ConsumerDefinition,\n consumerName: TName,\n ): Future<Result<WorkerInferConsumerInput<TContract, TName>, void>> {\n // Decompress message if needed\n const decompressMessage = Future.fromPromise(\n decompressBuffer(msg.content, msg.properties.contentEncoding),\n ).mapError((error) => {\n this.logger?.error(\"Error decompressing message\", {\n consumerName: String(consumerName),\n queueName: consumer.queue.name,\n contentEncoding: msg.properties.contentEncoding,\n error,\n });\n\n // Reject message with no requeue (decompression failed)\n this.amqpClient.channel.nack(msg, false, false);\n return undefined;\n });\n\n // Parse message\n const parseMessage = (buffer: Buffer) => {\n const parseResult = Result.fromExecution(() => JSON.parse(buffer.toString()));\n if (parseResult.isError()) {\n this.logger?.error(\"Error parsing message\", {\n consumerName: String(consumerName),\n queueName: consumer.queue.name,\n error: parseResult.error,\n });\n\n // Reject message with no requeue (malformed JSON)\n this.amqpClient.channel.nack(msg, false, false);\n return Future.value(Result.Error(undefined));\n }\n return Future.value(Result.Ok(parseResult.value));\n };\n\n // Validate message\n const validateMessage = (parsedMessage: unknown) => {\n const rawValidation = consumer.message.payload[\"~standard\"].validate(parsedMessage);\n return Future.fromPromise(\n rawValidation instanceof Promise ? rawValidation : Promise.resolve(rawValidation),\n ).mapOkToResult((validationResult) => {\n if (validationResult.issues) {\n const error = new MessageValidationError(String(consumerName), validationResult.issues);\n this.logger?.error(\"Message validation failed\", {\n consumerName: String(consumerName),\n queueName: consumer.queue.name,\n error,\n });\n\n // Reject message with no requeue (validation failed)\n this.amqpClient.channel.nack(msg, false, false);\n return Result.Error(undefined) as Result<\n WorkerInferConsumerInput<TContract, TName>,\n void\n >;\n }\n\n return Result.Ok(validationResult.value as WorkerInferConsumerInput<TContract, TName>);\n }) as Future<Result<WorkerInferConsumerInput<TContract, TName>, void>>;\n };\n\n return decompressMessage.flatMapOk(parseMessage).flatMapOk(validateMessage) as Future<\n Result<WorkerInferConsumerInput<TContract, TName>, void>\n >;\n }\n\n /**\n * Consume messages one at a time\n */\n private consumeSingle<TName extends InferConsumerNames<TContract>>(\n consumerName: TName,\n consumer: ConsumerDefinition,\n handler: (message: WorkerInferConsumerInput<TContract, TName>) => Promise<void>,\n ): Future<Result<void, TechnicalError>> {\n // Start consuming\n return Future.fromPromise(\n this.amqpClient.channel.consume(consumer.queue.name, async (msg) => {\n // Handle null messages (consumer cancellation)\n if (msg === null) {\n this.logger?.warn(\"Consumer cancelled by server\", {\n consumerName: String(consumerName),\n queueName: consumer.queue.name,\n });\n return;\n }\n\n // Parse and validate message\n await this.parseAndValidateMessage(msg, consumer, consumerName)\n .flatMapOk((validatedMessage) =>\n Future.fromPromise(handler(validatedMessage)).tapError((error) => {\n this.logger?.error(\"Error processing message\", {\n consumerName: String(consumerName),\n queueName: consumer.queue.name,\n error,\n });\n\n // fixme proper error handling strategy\n // Reject message and requeue (handler failed)\n this.amqpClient.channel.nack(msg, false, true);\n }),\n )\n .tapOk(() => {\n this.logger?.info(\"Message consumed successfully\", {\n consumerName: String(consumerName),\n queueName: consumer.queue.name,\n });\n\n // Acknowledge message\n this.amqpClient.channel.ack(msg);\n })\n .toPromise();\n }),\n )\n .tapOk((reply) => {\n // Store consumer tag for later cancellation\n this.consumerTags.add(reply.consumerTag);\n })\n .mapError(\n (error) =>\n new TechnicalError(`Failed to start consuming for \"${String(consumerName)}\"`, error),\n )\n .mapOk(() => undefined);\n }\n\n /**\n * Consume messages in batches\n */\n private consumeBatch<TName extends InferConsumerNames<TContract>>(\n consumerName: TName,\n consumer: ConsumerDefinition,\n options: ConsumerOptions,\n handler: (messages: Array<WorkerInferConsumerInput<TContract, TName>>) => Promise<void>,\n ): Future<Result<void, TechnicalError>> {\n const batchSize = options.batchSize!;\n const batchTimeout = options.batchTimeout ?? 1000;\n const timerKey = String(consumerName);\n\n // Note: Prefetch is handled globally in consumeAll()\n // Batch accumulation state\n type BatchItem = {\n message: WorkerInferConsumerInput<TContract, TName>;\n amqpMessage: Message;\n };\n let batch: BatchItem[] = [];\n // Track if batch processing is currently in progress to avoid race conditions\n let isProcessing = false;\n\n const processBatch = async () => {\n // Prevent concurrent batch processing\n if (isProcessing || batch.length === 0) {\n return;\n }\n\n isProcessing = true;\n\n const currentBatch = batch;\n batch = [];\n\n // Clear timer from tracking\n const timer = this.batchTimers.get(timerKey);\n if (timer) {\n clearTimeout(timer);\n this.batchTimers.delete(timerKey);\n }\n\n const messages = currentBatch.map((item) => item.message);\n\n this.logger?.info(\"Processing batch\", {\n consumerName: String(consumerName),\n queueName: consumer.queue.name,\n batchSize: currentBatch.length,\n });\n\n try {\n await handler(messages);\n\n // Acknowledge all messages in the batch\n for (const item of currentBatch) {\n this.amqpClient.channel.ack(item.amqpMessage);\n }\n\n this.logger?.info(\"Batch processed successfully\", {\n consumerName: String(consumerName),\n queueName: consumer.queue.name,\n batchSize: currentBatch.length,\n });\n } catch (error) {\n this.logger?.error(\"Error processing batch\", {\n consumerName: String(consumerName),\n queueName: consumer.queue.name,\n batchSize: currentBatch.length,\n error,\n });\n\n // fixme proper error handling strategy\n // Reject all messages and requeue (handler failed)\n for (const item of currentBatch) {\n this.amqpClient.channel.nack(item.amqpMessage, false, true);\n }\n } finally {\n isProcessing = false;\n }\n };\n\n const scheduleBatchProcessing = () => {\n // Don't schedule if batch is currently being processed\n if (isProcessing) {\n return;\n }\n\n // Clear existing timer\n const existingTimer = this.batchTimers.get(timerKey);\n if (existingTimer) {\n clearTimeout(existingTimer);\n }\n\n // Schedule new timer and track it\n const timer = setTimeout(() => {\n processBatch().catch((error) => {\n this.logger?.error(\"Unexpected error in batch processing\", {\n consumerName: String(consumerName),\n error,\n });\n });\n }, batchTimeout);\n\n this.batchTimers.set(timerKey, timer);\n };\n\n // Start consuming\n return Future.fromPromise(\n this.amqpClient.channel.consume(consumer.queue.name, async (msg) => {\n // Handle null messages (consumer cancellation)\n if (msg === null) {\n this.logger?.warn(\"Consumer cancelled by server\", {\n consumerName: String(consumerName),\n queueName: consumer.queue.name,\n });\n // Process any remaining messages in the batch\n await processBatch();\n return;\n }\n\n // Parse and validate message\n const validationResult = await this.parseAndValidateMessage(\n msg,\n consumer,\n consumerName,\n ).toPromise();\n\n if (validationResult.isError()) {\n // Error already handled in parseAndValidateMessage (nacked)\n return;\n }\n\n // Add to batch\n batch.push({\n message: validationResult.value,\n amqpMessage: msg,\n });\n\n // Process batch if full\n if (batch.length >= batchSize) {\n await processBatch();\n // After processing a full batch, schedule timer for any subsequent messages\n // This ensures that if more messages arrive at a slow rate, they won't be held indefinitely\n if (batch.length > 0 && !this.batchTimers.has(timerKey)) {\n scheduleBatchProcessing();\n }\n } else {\n // Schedule batch processing if not already scheduled\n if (!this.batchTimers.has(timerKey)) {\n scheduleBatchProcessing();\n }\n }\n }),\n )\n .tapOk((reply) => {\n // Store consumer tag for later cancellation\n this.consumerTags.add(reply.consumerTag);\n })\n .mapError(\n (error) =>\n new TechnicalError(`Failed to start consuming for \"${String(consumerName)}\"`, error),\n )\n .mapOk(() => undefined);\n }\n}\n","import type { ContractDefinition, InferConsumerNames } from \"@amqp-contract/contract\";\nimport type {\n WorkerInferConsumerBatchHandler,\n WorkerInferConsumerHandler,\n WorkerInferConsumerHandlerEntry,\n WorkerInferConsumerHandlers,\n} from \"./types.js\";\n\n/**\n * Define a type-safe handler for a specific consumer in a contract.\n *\n * This utility allows you to define handlers outside of the worker creation,\n * providing better code organization and reusability.\n *\n * Supports three patterns:\n * 1. Simple handler: just the function (single message handler)\n * 2. Handler with prefetch: [handler, { prefetch: 10 }] (single message handler with config)\n * 3. Batch handler: [batchHandler, { batchSize: 5, batchTimeout: 1000 }] (REQUIRES batchSize config)\n *\n * **Important**: Batch handlers (handlers that accept an array of messages) MUST include\n * batchSize configuration. You cannot create a batch handler without specifying batchSize.\n *\n * @template TContract - The contract definition type\n * @template TName - The consumer name from the contract\n * @param contract - The contract definition containing the consumer\n * @param consumerName - The name of the consumer from the contract\n * @param handler - The async handler function that processes messages (single or batch)\n * @param options - Optional consumer options (prefetch, batchSize, batchTimeout)\n * - For single-message handlers: { prefetch?: number } is optional\n * - For batch handlers: { batchSize: number, batchTimeout?: number } is REQUIRED\n * @returns A type-safe handler that can be used with TypedAmqpWorker\n *\n * @example\n * ```typescript\n * import { defineHandler } from '@amqp-contract/worker';\n * import { orderContract } from './contract';\n *\n * // Simple single-message handler without options\n * const processOrderHandler = defineHandler(\n * orderContract,\n * 'processOrder',\n * async (message) => {\n * console.log('Processing order:', message.orderId);\n * await processPayment(message);\n * }\n * );\n *\n * // Single-message handler with prefetch\n * const processOrderWithPrefetch = defineHandler(\n * orderContract,\n * 'processOrder',\n * async (message) => {\n * await processOrder(message);\n * },\n * { prefetch: 10 }\n * );\n *\n * // Batch handler - MUST include batchSize\n * const processBatchOrders = defineHandler(\n * orderContract,\n * 'processOrders',\n * async (messages) => {\n * // messages is an array - batchSize configuration is REQUIRED\n * await db.insertMany(messages);\n * },\n * { batchSize: 5, batchTimeout: 1000 }\n * );\n * ```\n */\nexport function defineHandler<\n TContract extends ContractDefinition,\n TName extends InferConsumerNames<TContract>,\n>(\n contract: TContract,\n consumerName: TName,\n handler: WorkerInferConsumerHandler<TContract, TName>,\n): WorkerInferConsumerHandlerEntry<TContract, TName>;\nexport function defineHandler<\n TContract extends ContractDefinition,\n TName extends InferConsumerNames<TContract>,\n>(\n contract: TContract,\n consumerName: TName,\n handler: WorkerInferConsumerHandler<TContract, TName>,\n options: { prefetch?: number; batchSize?: never; batchTimeout?: never },\n): WorkerInferConsumerHandlerEntry<TContract, TName>;\nexport function defineHandler<\n TContract extends ContractDefinition,\n TName extends InferConsumerNames<TContract>,\n>(\n contract: TContract,\n consumerName: TName,\n handler: WorkerInferConsumerBatchHandler<TContract, TName>,\n options: { prefetch?: number; batchSize: number; batchTimeout?: number },\n): WorkerInferConsumerHandlerEntry<TContract, TName>;\nexport function defineHandler<\n TContract extends ContractDefinition,\n TName extends InferConsumerNames<TContract>,\n>(\n contract: TContract,\n consumerName: TName,\n handler:\n | WorkerInferConsumerHandler<TContract, TName>\n | WorkerInferConsumerBatchHandler<TContract, TName>,\n options?: { prefetch?: number; batchSize?: number; batchTimeout?: number },\n): WorkerInferConsumerHandlerEntry<TContract, TName> {\n // Validate that the consumer exists in the contract\n const consumers = contract.consumers;\n\n if (!consumers || !(consumerName in consumers)) {\n const availableConsumers = consumers ? Object.keys(consumers) : [];\n const available = availableConsumers.length > 0 ? availableConsumers.join(\", \") : \"none\";\n throw new Error(\n `Consumer \"${String(consumerName)}\" not found in contract. Available consumers: ${available}`,\n );\n }\n\n // Return the handler with options if provided, otherwise just the handler\n if (options) {\n return [handler, options] as WorkerInferConsumerHandlerEntry<TContract, TName>;\n }\n return handler as WorkerInferConsumerHandlerEntry<TContract, TName>;\n}\n\n/**\n * Define multiple type-safe handlers for consumers in a contract.\n *\n * This utility allows you to define all handlers at once outside of the worker creation,\n * ensuring type safety and providing better code organization.\n *\n * @template TContract - The contract definition type\n * @param contract - The contract definition containing the consumers\n * @param handlers - An object with async handler functions for each consumer\n * @returns A type-safe handlers object that can be used with TypedAmqpWorker\n *\n * @example\n * ```typescript\n * import { defineHandlers } from '@amqp-contract/worker';\n * import { orderContract } from './contract';\n *\n * // Define all handlers at once\n * const handlers = defineHandlers(orderContract, {\n * processOrder: async (message) => {\n * // message is fully typed based on the contract\n * console.log('Processing order:', message.orderId);\n * await processPayment(message);\n * },\n * notifyOrder: async (message) => {\n * await sendNotification(message);\n * },\n * shipOrder: async (message) => {\n * await prepareShipment(message);\n * },\n * });\n *\n * // Use the handlers in worker\n * const worker = await TypedAmqpWorker.create({\n * contract: orderContract,\n * handlers,\n * connection: 'amqp://localhost',\n * });\n * ```\n *\n * @example\n * ```typescript\n * // Separate handler definitions for better organization\n * async function handleProcessOrder(message: WorkerInferConsumerInput<typeof orderContract, 'processOrder'>) {\n * await processOrder(message);\n * }\n *\n * async function handleNotifyOrder(message: WorkerInferConsumerInput<typeof orderContract, 'notifyOrder'>) {\n * await sendNotification(message);\n * }\n *\n * const handlers = defineHandlers(orderContract, {\n * processOrder: handleProcessOrder,\n * notifyOrder: handleNotifyOrder,\n * });\n * ```\n */\nexport function defineHandlers<TContract extends ContractDefinition>(\n contract: TContract,\n handlers: WorkerInferConsumerHandlers<TContract>,\n): WorkerInferConsumerHandlers<TContract> {\n // Validate that all consumers in handlers exist in the contract\n const consumers = contract.consumers;\n const availableConsumers = Object.keys(consumers ?? {});\n const availableConsumerNames =\n availableConsumers.length > 0 ? availableConsumers.join(\", \") : \"none\";\n\n for (const handlerName of Object.keys(handlers)) {\n if (!consumers || !(handlerName in consumers)) {\n throw new Error(\n `Consumer \"${handlerName}\" not found in contract. Available consumers: ${availableConsumerNames}`,\n );\n }\n }\n\n // Return the handlers as-is, with type checking enforced\n return handlers;\n}\n"],"mappings":"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"}
|
package/docs/index.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
### MessageValidationError
|
|
10
10
|
|
|
11
|
-
Defined in: [packages/worker/src/errors.ts:35](https://github.com/btravers/amqp-contract/blob/
|
|
11
|
+
Defined in: [packages/worker/src/errors.ts:35](https://github.com/btravers/amqp-contract/blob/382c6d2fbfc563c9f6f16cb71cb33fd4a3d41d77/packages/worker/src/errors.ts#L35)
|
|
12
12
|
|
|
13
13
|
Error thrown when message validation fails
|
|
14
14
|
|
|
@@ -24,7 +24,7 @@ Error thrown when message validation fails
|
|
|
24
24
|
new MessageValidationError(consumerName, issues): MessageValidationError;
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
Defined in: [packages/worker/src/errors.ts:36](https://github.com/btravers/amqp-contract/blob/
|
|
27
|
+
Defined in: [packages/worker/src/errors.ts:36](https://github.com/btravers/amqp-contract/blob/382c6d2fbfc563c9f6f16cb71cb33fd4a3d41d77/packages/worker/src/errors.ts#L36)
|
|
28
28
|
|
|
29
29
|
###### Parameters
|
|
30
30
|
|
|
@@ -48,8 +48,8 @@ WorkerError.constructor
|
|
|
48
48
|
| Property | Modifier | Type | Description | Inherited from | Defined in |
|
|
49
49
|
| ------ | ------ | ------ | ------ | ------ | ------ |
|
|
50
50
|
| <a id="cause"></a> `cause?` | `public` | `unknown` | - | `WorkerError.cause` | node\_modules/.pnpm/typescript@5.9.3/node\_modules/typescript/lib/lib.es2022.error.d.ts:26 |
|
|
51
|
-
| <a id="consumername"></a> `consumerName` | `readonly` | `string` | - | - | [packages/worker/src/errors.ts:37](https://github.com/btravers/amqp-contract/blob/
|
|
52
|
-
| <a id="issues"></a> `issues` | `readonly` | `unknown` | - | - | [packages/worker/src/errors.ts:38](https://github.com/btravers/amqp-contract/blob/
|
|
51
|
+
| <a id="consumername"></a> `consumerName` | `readonly` | `string` | - | - | [packages/worker/src/errors.ts:37](https://github.com/btravers/amqp-contract/blob/382c6d2fbfc563c9f6f16cb71cb33fd4a3d41d77/packages/worker/src/errors.ts#L37) |
|
|
52
|
+
| <a id="issues"></a> `issues` | `readonly` | `unknown` | - | - | [packages/worker/src/errors.ts:38](https://github.com/btravers/amqp-contract/blob/382c6d2fbfc563c9f6f16cb71cb33fd4a3d41d77/packages/worker/src/errors.ts#L38) |
|
|
53
53
|
| <a id="message"></a> `message` | `public` | `string` | - | `WorkerError.message` | node\_modules/.pnpm/typescript@5.9.3/node\_modules/typescript/lib/lib.es5.d.ts:1077 |
|
|
54
54
|
| <a id="name"></a> `name` | `public` | `string` | - | `WorkerError.name` | node\_modules/.pnpm/typescript@5.9.3/node\_modules/typescript/lib/lib.es5.d.ts:1076 |
|
|
55
55
|
| <a id="stack"></a> `stack?` | `public` | `string` | - | `WorkerError.stack` | node\_modules/.pnpm/typescript@5.9.3/node\_modules/typescript/lib/lib.es5.d.ts:1078 |
|
|
@@ -159,7 +159,7 @@ WorkerError.prepareStackTrace
|
|
|
159
159
|
|
|
160
160
|
### TechnicalError
|
|
161
161
|
|
|
162
|
-
Defined in: [packages/worker/src/errors.ts:22](https://github.com/btravers/amqp-contract/blob/
|
|
162
|
+
Defined in: [packages/worker/src/errors.ts:22](https://github.com/btravers/amqp-contract/blob/382c6d2fbfc563c9f6f16cb71cb33fd4a3d41d77/packages/worker/src/errors.ts#L22)
|
|
163
163
|
|
|
164
164
|
Error for technical/runtime failures in worker operations
|
|
165
165
|
This includes validation failures, parsing failures, and processing failures
|
|
@@ -176,7 +176,7 @@ This includes validation failures, parsing failures, and processing failures
|
|
|
176
176
|
new TechnicalError(message, cause?): TechnicalError;
|
|
177
177
|
```
|
|
178
178
|
|
|
179
|
-
Defined in: [packages/worker/src/errors.ts:23](https://github.com/btravers/amqp-contract/blob/
|
|
179
|
+
Defined in: [packages/worker/src/errors.ts:23](https://github.com/btravers/amqp-contract/blob/382c6d2fbfc563c9f6f16cb71cb33fd4a3d41d77/packages/worker/src/errors.ts#L23)
|
|
180
180
|
|
|
181
181
|
###### Parameters
|
|
182
182
|
|
|
@@ -199,7 +199,7 @@ WorkerError.constructor
|
|
|
199
199
|
|
|
200
200
|
| Property | Modifier | Type | Description | Inherited from | Defined in |
|
|
201
201
|
| ------ | ------ | ------ | ------ | ------ | ------ |
|
|
202
|
-
| <a id="cause-1"></a> `cause?` | `readonly` | `unknown` | - | `WorkerError.cause` | [packages/worker/src/errors.ts:25](https://github.com/btravers/amqp-contract/blob/
|
|
202
|
+
| <a id="cause-1"></a> `cause?` | `readonly` | `unknown` | - | `WorkerError.cause` | [packages/worker/src/errors.ts:25](https://github.com/btravers/amqp-contract/blob/382c6d2fbfc563c9f6f16cb71cb33fd4a3d41d77/packages/worker/src/errors.ts#L25) |
|
|
203
203
|
| <a id="message-1"></a> `message` | `public` | `string` | - | `WorkerError.message` | node\_modules/.pnpm/typescript@5.9.3/node\_modules/typescript/lib/lib.es5.d.ts:1077 |
|
|
204
204
|
| <a id="name-1"></a> `name` | `public` | `string` | - | `WorkerError.name` | node\_modules/.pnpm/typescript@5.9.3/node\_modules/typescript/lib/lib.es5.d.ts:1076 |
|
|
205
205
|
| <a id="stack-1"></a> `stack?` | `public` | `string` | - | `WorkerError.stack` | node\_modules/.pnpm/typescript@5.9.3/node\_modules/typescript/lib/lib.es5.d.ts:1078 |
|
|
@@ -309,7 +309,7 @@ WorkerError.prepareStackTrace
|
|
|
309
309
|
|
|
310
310
|
### TypedAmqpWorker
|
|
311
311
|
|
|
312
|
-
Defined in: [packages/worker/src/worker.ts:
|
|
312
|
+
Defined in: [packages/worker/src/worker.ts:130](https://github.com/btravers/amqp-contract/blob/382c6d2fbfc563c9f6f16cb71cb33fd4a3d41d77/packages/worker/src/worker.ts#L130)
|
|
313
313
|
|
|
314
314
|
Type-safe AMQP worker for consuming messages from RabbitMQ.
|
|
315
315
|
|
|
@@ -363,7 +363,7 @@ await worker.close().resultToPromise();
|
|
|
363
363
|
close(): Future<Result<void, TechnicalError>>;
|
|
364
364
|
```
|
|
365
365
|
|
|
366
|
-
Defined in: [packages/worker/src/worker.ts:
|
|
366
|
+
Defined in: [packages/worker/src/worker.ts:244](https://github.com/btravers/amqp-contract/blob/382c6d2fbfc563c9f6f16cb71cb33fd4a3d41d77/packages/worker/src/worker.ts#L244)
|
|
367
367
|
|
|
368
368
|
Close the AMQP channel and connection.
|
|
369
369
|
|
|
@@ -391,7 +391,7 @@ if (closeResult.isOk()) {
|
|
|
391
391
|
static create<TContract>(options): Future<Result<TypedAmqpWorker<TContract>, TechnicalError>>;
|
|
392
392
|
```
|
|
393
393
|
|
|
394
|
-
Defined in: [packages/worker/src/worker.ts:
|
|
394
|
+
Defined in: [packages/worker/src/worker.ts:205](https://github.com/btravers/amqp-contract/blob/382c6d2fbfc563c9f6f16cb71cb33fd4a3d41d77/packages/worker/src/worker.ts#L205)
|
|
395
395
|
|
|
396
396
|
Create a type-safe AMQP worker from a contract.
|
|
397
397
|
|
|
@@ -445,7 +445,7 @@ if (workerResult.isError()) {
|
|
|
445
445
|
type CreateWorkerOptions<TContract> = object;
|
|
446
446
|
```
|
|
447
447
|
|
|
448
|
-
Defined in: [packages/worker/src/worker.ts:
|
|
448
|
+
Defined in: [packages/worker/src/worker.ts:77](https://github.com/btravers/amqp-contract/blob/382c6d2fbfc563c9f6f16cb71cb33fd4a3d41d77/packages/worker/src/worker.ts#L77)
|
|
449
449
|
|
|
450
450
|
Options for creating a type-safe AMQP worker.
|
|
451
451
|
|
|
@@ -492,11 +492,11 @@ const options: CreateWorkerOptions<typeof contract> = {
|
|
|
492
492
|
|
|
493
493
|
| Property | Type | Description | Defined in |
|
|
494
494
|
| ------ | ------ | ------ | ------ |
|
|
495
|
-
| <a id="connectionoptions"></a> `connectionOptions?` | `AmqpConnectionManagerOptions` | Optional connection configuration (heartbeat, reconnect settings, etc.) | [packages/worker/src/worker.ts:
|
|
496
|
-
| <a id="contract"></a> `contract` | `TContract` | The AMQP contract definition specifying consumers and their message schemas | [packages/worker/src/worker.ts:
|
|
497
|
-
| <a id="handlers"></a> `handlers` | [`WorkerInferConsumerHandlers`](#workerinferconsumerhandlers)\<`TContract`\> | Handlers for each consumer defined in the contract. Can be a function or a tuple of [handler, options] | [packages/worker/src/worker.ts:
|
|
498
|
-
| <a id="logger"></a> `logger?` | `Logger` | Optional logger for logging message consumption and errors | [packages/worker/src/worker.ts:
|
|
499
|
-
| <a id="urls"></a> `urls` | `ConnectionUrl`[] | AMQP broker URL(s). Multiple URLs provide failover support | [packages/worker/src/worker.ts:
|
|
495
|
+
| <a id="connectionoptions"></a> `connectionOptions?` | `AmqpConnectionManagerOptions` | Optional connection configuration (heartbeat, reconnect settings, etc.) | [packages/worker/src/worker.ts:85](https://github.com/btravers/amqp-contract/blob/382c6d2fbfc563c9f6f16cb71cb33fd4a3d41d77/packages/worker/src/worker.ts#L85) |
|
|
496
|
+
| <a id="contract"></a> `contract` | `TContract` | The AMQP contract definition specifying consumers and their message schemas | [packages/worker/src/worker.ts:79](https://github.com/btravers/amqp-contract/blob/382c6d2fbfc563c9f6f16cb71cb33fd4a3d41d77/packages/worker/src/worker.ts#L79) |
|
|
497
|
+
| <a id="handlers"></a> `handlers` | [`WorkerInferConsumerHandlers`](#workerinferconsumerhandlers)\<`TContract`\> | Handlers for each consumer defined in the contract. Can be a function or a tuple of [handler, options] | [packages/worker/src/worker.ts:81](https://github.com/btravers/amqp-contract/blob/382c6d2fbfc563c9f6f16cb71cb33fd4a3d41d77/packages/worker/src/worker.ts#L81) |
|
|
498
|
+
| <a id="logger"></a> `logger?` | `Logger` | Optional logger for logging message consumption and errors | [packages/worker/src/worker.ts:87](https://github.com/btravers/amqp-contract/blob/382c6d2fbfc563c9f6f16cb71cb33fd4a3d41d77/packages/worker/src/worker.ts#L87) |
|
|
499
|
+
| <a id="urls"></a> `urls` | `ConnectionUrl`[] | AMQP broker URL(s). Multiple URLs provide failover support | [packages/worker/src/worker.ts:83](https://github.com/btravers/amqp-contract/blob/382c6d2fbfc563c9f6f16cb71cb33fd4a3d41d77/packages/worker/src/worker.ts#L83) |
|
|
500
500
|
|
|
501
501
|
***
|
|
502
502
|
|
|
@@ -506,7 +506,7 @@ const options: CreateWorkerOptions<typeof contract> = {
|
|
|
506
506
|
type WorkerInferConsumerBatchHandler<TContract, TName> = (messages) => Promise<void>;
|
|
507
507
|
```
|
|
508
508
|
|
|
509
|
-
Defined in: [packages/worker/src/types.ts:56](https://github.com/btravers/amqp-contract/blob/
|
|
509
|
+
Defined in: [packages/worker/src/types.ts:56](https://github.com/btravers/amqp-contract/blob/382c6d2fbfc563c9f6f16cb71cb33fd4a3d41d77/packages/worker/src/types.ts#L56)
|
|
510
510
|
|
|
511
511
|
Infer consumer handler type for batch processing.
|
|
512
512
|
Batch handlers receive an array of messages.
|
|
@@ -536,7 +536,7 @@ Batch handlers receive an array of messages.
|
|
|
536
536
|
type WorkerInferConsumerHandler<TContract, TName> = (message) => Promise<void>;
|
|
537
537
|
```
|
|
538
538
|
|
|
539
|
-
Defined in: [packages/worker/src/types.ts:47](https://github.com/btravers/amqp-contract/blob/
|
|
539
|
+
Defined in: [packages/worker/src/types.ts:47](https://github.com/btravers/amqp-contract/blob/382c6d2fbfc563c9f6f16cb71cb33fd4a3d41d77/packages/worker/src/types.ts#L47)
|
|
540
540
|
|
|
541
541
|
Infer consumer handler type for a specific consumer.
|
|
542
542
|
Handlers always receive a single message by default.
|
|
@@ -578,7 +578,7 @@ type WorkerInferConsumerHandlerEntry<TContract, TName> =
|
|
|
578
578
|
}];
|
|
579
579
|
```
|
|
580
580
|
|
|
581
|
-
Defined in: [packages/worker/src/types.ts:69](https://github.com/btravers/amqp-contract/blob/
|
|
581
|
+
Defined in: [packages/worker/src/types.ts:69](https://github.com/btravers/amqp-contract/blob/382c6d2fbfc563c9f6f16cb71cb33fd4a3d41d77/packages/worker/src/types.ts#L69)
|
|
582
582
|
|
|
583
583
|
Infer handler entry for a consumer - either a function or a tuple of [handler, options].
|
|
584
584
|
|
|
@@ -602,7 +602,7 @@ Three patterns are supported:
|
|
|
602
602
|
type WorkerInferConsumerHandlers<TContract> = { [K in InferConsumerNames<TContract>]: WorkerInferConsumerHandlerEntry<TContract, K> };
|
|
603
603
|
```
|
|
604
604
|
|
|
605
|
-
Defined in: [packages/worker/src/types.ts:87](https://github.com/btravers/amqp-contract/blob/
|
|
605
|
+
Defined in: [packages/worker/src/types.ts:87](https://github.com/btravers/amqp-contract/blob/382c6d2fbfc563c9f6f16cb71cb33fd4a3d41d77/packages/worker/src/types.ts#L87)
|
|
606
606
|
|
|
607
607
|
Infer all consumer handlers for a contract.
|
|
608
608
|
Handlers can be either single-message handlers, batch handlers, or a tuple of [handler, options].
|
|
@@ -621,7 +621,7 @@ Handlers can be either single-message handlers, batch handlers, or a tuple of [h
|
|
|
621
621
|
type WorkerInferConsumerInput<TContract, TName> = ConsumerInferInput<InferConsumer<TContract, TName>>;
|
|
622
622
|
```
|
|
623
623
|
|
|
624
|
-
Defined in: [packages/worker/src/types.ts:37](https://github.com/btravers/amqp-contract/blob/
|
|
624
|
+
Defined in: [packages/worker/src/types.ts:37](https://github.com/btravers/amqp-contract/blob/382c6d2fbfc563c9f6f16cb71cb33fd4a3d41d77/packages/worker/src/types.ts#L37)
|
|
625
625
|
|
|
626
626
|
Worker perspective types - for consuming messages
|
|
627
627
|
|
|
@@ -645,7 +645,7 @@ function defineHandler<TContract, TName>(
|
|
|
645
645
|
handler): WorkerInferConsumerHandlerEntry<TContract, TName>;
|
|
646
646
|
```
|
|
647
647
|
|
|
648
|
-
Defined in: [packages/worker/src/handlers.ts:70](https://github.com/btravers/amqp-contract/blob/
|
|
648
|
+
Defined in: [packages/worker/src/handlers.ts:70](https://github.com/btravers/amqp-contract/blob/382c6d2fbfc563c9f6f16cb71cb33fd4a3d41d77/packages/worker/src/handlers.ts#L70)
|
|
649
649
|
|
|
650
650
|
Define a type-safe handler for a specific consumer in a contract.
|
|
651
651
|
|
|
@@ -729,7 +729,7 @@ function defineHandler<TContract, TName>(
|
|
|
729
729
|
options): WorkerInferConsumerHandlerEntry<TContract, TName>;
|
|
730
730
|
```
|
|
731
731
|
|
|
732
|
-
Defined in: [packages/worker/src/handlers.ts:78](https://github.com/btravers/amqp-contract/blob/
|
|
732
|
+
Defined in: [packages/worker/src/handlers.ts:78](https://github.com/btravers/amqp-contract/blob/382c6d2fbfc563c9f6f16cb71cb33fd4a3d41d77/packages/worker/src/handlers.ts#L78)
|
|
733
733
|
|
|
734
734
|
Define a type-safe handler for a specific consumer in a contract.
|
|
735
735
|
|
|
@@ -817,7 +817,7 @@ function defineHandler<TContract, TName>(
|
|
|
817
817
|
options): WorkerInferConsumerHandlerEntry<TContract, TName>;
|
|
818
818
|
```
|
|
819
819
|
|
|
820
|
-
Defined in: [packages/worker/src/handlers.ts:87](https://github.com/btravers/amqp-contract/blob/
|
|
820
|
+
Defined in: [packages/worker/src/handlers.ts:87](https://github.com/btravers/amqp-contract/blob/382c6d2fbfc563c9f6f16cb71cb33fd4a3d41d77/packages/worker/src/handlers.ts#L87)
|
|
821
821
|
|
|
822
822
|
Define a type-safe handler for a specific consumer in a contract.
|
|
823
823
|
|
|
@@ -903,7 +903,7 @@ const processBatchOrders = defineHandler(
|
|
|
903
903
|
function defineHandlers<TContract>(contract, handlers): WorkerInferConsumerHandlers<TContract>;
|
|
904
904
|
```
|
|
905
905
|
|
|
906
|
-
Defined in: [packages/worker/src/handlers.ts:181](https://github.com/btravers/amqp-contract/blob/
|
|
906
|
+
Defined in: [packages/worker/src/handlers.ts:181](https://github.com/btravers/amqp-contract/blob/382c6d2fbfc563c9f6f16cb71cb33fd4a3d41d77/packages/worker/src/handlers.ts#L181)
|
|
907
907
|
|
|
908
908
|
Define multiple type-safe handlers for consumers in a contract.
|
|
909
909
|
|
package/package.json
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amqp-contract/worker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Worker utilities for consuming messages using amqp-contract",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"amqp",
|
|
7
7
|
"contract",
|
|
8
8
|
"rabbitmq",
|
|
9
9
|
"typescript",
|
|
10
|
-
"worker"
|
|
10
|
+
"worker",
|
|
11
|
+
"nodejs",
|
|
12
|
+
"messaging",
|
|
13
|
+
"type-safe",
|
|
14
|
+
"schema",
|
|
15
|
+
"validation",
|
|
16
|
+
"message-broker",
|
|
17
|
+
"message-queue",
|
|
18
|
+
"consumer"
|
|
11
19
|
],
|
|
12
20
|
"homepage": "https://github.com/btravers/amqp-contract#readme",
|
|
13
21
|
"bugs": {
|
|
@@ -44,8 +52,8 @@
|
|
|
44
52
|
"dependencies": {
|
|
45
53
|
"@standard-schema/spec": "1.1.0",
|
|
46
54
|
"@swan-io/boxed": "3.2.1",
|
|
47
|
-
"@amqp-contract/contract": "0.
|
|
48
|
-
"@amqp-contract/core": "0.
|
|
55
|
+
"@amqp-contract/contract": "0.7.0",
|
|
56
|
+
"@amqp-contract/core": "0.7.0"
|
|
49
57
|
},
|
|
50
58
|
"devDependencies": {
|
|
51
59
|
"@types/amqplib": "0.10.8",
|
|
@@ -53,13 +61,13 @@
|
|
|
53
61
|
"@vitest/coverage-v8": "4.0.16",
|
|
54
62
|
"amqp-connection-manager": "5.0.0",
|
|
55
63
|
"amqplib": "0.10.9",
|
|
56
|
-
"tsdown": "0.18.
|
|
64
|
+
"tsdown": "0.18.4",
|
|
57
65
|
"typedoc": "0.28.15",
|
|
58
66
|
"typedoc-plugin-markdown": "4.9.0",
|
|
59
67
|
"typescript": "5.9.3",
|
|
60
68
|
"vitest": "4.0.16",
|
|
61
|
-
"zod": "4.
|
|
62
|
-
"@amqp-contract/testing": "0.
|
|
69
|
+
"zod": "4.3.5",
|
|
70
|
+
"@amqp-contract/testing": "0.7.0",
|
|
63
71
|
"@amqp-contract/tsconfig": "0.0.0",
|
|
64
72
|
"@amqp-contract/typedoc": "0.0.1"
|
|
65
73
|
},
|
|
@@ -67,9 +75,8 @@
|
|
|
67
75
|
"build": "tsdown src/index.ts --format cjs,esm --dts --clean",
|
|
68
76
|
"build:docs": "typedoc",
|
|
69
77
|
"dev": "tsdown src/index.ts --format cjs,esm --dts --watch",
|
|
70
|
-
"test": "vitest run --project
|
|
78
|
+
"test": "vitest run --project unit",
|
|
71
79
|
"test:integration": "vitest run --project integration",
|
|
72
|
-
"test:watch": "vitest --project integration",
|
|
73
80
|
"typecheck": "tsc --noEmit"
|
|
74
81
|
}
|
|
75
82
|
}
|