@amqp-contract/worker 0.0.5 → 0.1.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 +81 -9
- package/dist/index.cjs +75 -14
- package/dist/index.d.cts +29 -4
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +29 -4
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +75 -15
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -1,35 +1,36 @@
|
|
|
1
1
|
# @amqp-contract/worker
|
|
2
2
|
|
|
3
|
-
Type-safe AMQP worker for consuming messages using amqp-contract.
|
|
3
|
+
Type-safe AMQP worker for consuming messages using amqp-contract with standard async/await error handling.
|
|
4
4
|
|
|
5
5
|
📖 **[Full documentation →](https://btravers.github.io/amqp-contract/api/worker)**
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
pnpm add @amqp-contract/worker
|
|
10
|
+
pnpm add @amqp-contract/worker
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
## Usage
|
|
14
14
|
|
|
15
15
|
```typescript
|
|
16
16
|
import { TypedAmqpWorker } from '@amqp-contract/worker';
|
|
17
|
-
import { connect } from 'amqplib';
|
|
18
17
|
import { contract } from './contract';
|
|
19
18
|
|
|
20
|
-
// Connect to RabbitMQ
|
|
21
|
-
const connection = await connect('amqp://localhost');
|
|
22
|
-
|
|
23
19
|
// Create worker from contract with handlers (automatically connects and starts consuming)
|
|
24
20
|
const worker = await TypedAmqpWorker.create({
|
|
25
21
|
contract,
|
|
26
22
|
handlers: {
|
|
27
23
|
processOrder: async (message) => {
|
|
28
24
|
console.log('Processing order:', message.orderId);
|
|
25
|
+
|
|
29
26
|
// Your business logic here
|
|
27
|
+
await processPayment(message);
|
|
28
|
+
await updateInventory(message);
|
|
29
|
+
|
|
30
|
+
// If an exception is thrown, the message is automatically requeued
|
|
30
31
|
},
|
|
31
32
|
},
|
|
32
|
-
connection,
|
|
33
|
+
connection: 'amqp://localhost',
|
|
33
34
|
});
|
|
34
35
|
|
|
35
36
|
// Worker is already consuming messages
|
|
@@ -38,6 +39,35 @@ const worker = await TypedAmqpWorker.create({
|
|
|
38
39
|
// await worker.close();
|
|
39
40
|
```
|
|
40
41
|
|
|
42
|
+
## Error Handling
|
|
43
|
+
|
|
44
|
+
Worker handlers use standard Promise-based async/await pattern:
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
handlers: {
|
|
48
|
+
processOrder: async (message) => {
|
|
49
|
+
// Standard async/await - no Result wrapping needed
|
|
50
|
+
try {
|
|
51
|
+
await process(message);
|
|
52
|
+
// Message acknowledged automatically on success
|
|
53
|
+
} catch (error) {
|
|
54
|
+
// Exception automatically caught by worker
|
|
55
|
+
// Message is requeued for retry
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**Error Types:**
|
|
63
|
+
|
|
64
|
+
Worker defines error classes for internal use:
|
|
65
|
+
|
|
66
|
+
- `TechnicalError` - Runtime failures (parsing, processing)
|
|
67
|
+
- `MessageValidationError` - Message fails schema validation
|
|
68
|
+
|
|
69
|
+
These errors are logged but **handlers don't need to use them** - just throw standard exceptions.
|
|
70
|
+
|
|
41
71
|
## API
|
|
42
72
|
|
|
43
73
|
### `TypedAmqpWorker.create(options)`
|
|
@@ -47,13 +77,55 @@ Create a type-safe AMQP worker from a contract with message handlers. Automatica
|
|
|
47
77
|
**Parameters:**
|
|
48
78
|
|
|
49
79
|
- `options.contract` - Contract definition
|
|
50
|
-
- `options.handlers` - Object with handler functions for each consumer
|
|
51
|
-
- `options.connection` -
|
|
80
|
+
- `options.handlers` - Object with async handler functions for each consumer
|
|
81
|
+
- `options.connection` - AMQP connection URL (string) or connection options (Options.Connect)
|
|
82
|
+
|
|
83
|
+
**Returns:** `Promise<TypedAmqpWorker>`
|
|
84
|
+
|
|
85
|
+
**Example:**
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
const worker = await TypedAmqpWorker.create({
|
|
89
|
+
contract,
|
|
90
|
+
handlers: {
|
|
91
|
+
// Each handler receives type-checked message
|
|
92
|
+
processOrder: async (message) => {
|
|
93
|
+
// message.orderId is type-checked
|
|
94
|
+
console.log(message.orderId);
|
|
95
|
+
},
|
|
96
|
+
processPayment: async (message) => {
|
|
97
|
+
// Different message type for this consumer
|
|
98
|
+
await handlePayment(message);
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
connection: {
|
|
102
|
+
hostname: 'localhost',
|
|
103
|
+
port: 5672,
|
|
104
|
+
username: 'guest',
|
|
105
|
+
password: 'guest',
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Handler Signature
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
type Handler<T> = (message: T) => Promise<void>
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Handlers are simple async functions that:
|
|
117
|
+
|
|
118
|
+
- Receive type-checked message as parameter
|
|
119
|
+
- Return `Promise<void>`
|
|
120
|
+
- Can throw exceptions (message will be requeued)
|
|
121
|
+
- Message is acknowledged automatically on success
|
|
52
122
|
|
|
53
123
|
### `TypedAmqpWorker.close()`
|
|
54
124
|
|
|
55
125
|
Stop consuming and close the channel and connection.
|
|
56
126
|
|
|
127
|
+
**Returns:** `Promise<void>`
|
|
128
|
+
|
|
57
129
|
## License
|
|
58
130
|
|
|
59
131
|
MIT
|
package/dist/index.cjs
CHANGED
|
@@ -1,15 +1,54 @@
|
|
|
1
|
+
let amqplib = require("amqplib");
|
|
2
|
+
let _swan_io_boxed = require("@swan-io/boxed");
|
|
1
3
|
|
|
4
|
+
//#region src/errors.ts
|
|
5
|
+
/**
|
|
6
|
+
* Base error class for worker errors
|
|
7
|
+
*/
|
|
8
|
+
var WorkerError = class extends Error {
|
|
9
|
+
constructor(message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = "WorkerError";
|
|
12
|
+
const ErrorConstructor = Error;
|
|
13
|
+
if (typeof ErrorConstructor.captureStackTrace === "function") ErrorConstructor.captureStackTrace(this, this.constructor);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Error for technical/runtime failures in worker operations
|
|
18
|
+
* This includes validation failures, parsing failures, and processing failures
|
|
19
|
+
*/
|
|
20
|
+
var TechnicalError = class extends WorkerError {
|
|
21
|
+
constructor(message, cause) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.cause = cause;
|
|
24
|
+
this.name = "TechnicalError";
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Error thrown when message validation fails
|
|
29
|
+
*/
|
|
30
|
+
var MessageValidationError = class extends WorkerError {
|
|
31
|
+
constructor(consumerName, issues) {
|
|
32
|
+
super(`Message validation failed for consumer "${consumerName}"`);
|
|
33
|
+
this.consumerName = consumerName;
|
|
34
|
+
this.issues = issues;
|
|
35
|
+
this.name = "MessageValidationError";
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
//#endregion
|
|
2
40
|
//#region src/worker.ts
|
|
3
41
|
/**
|
|
4
42
|
* Type-safe AMQP worker for consuming messages
|
|
5
43
|
*/
|
|
6
44
|
var TypedAmqpWorker = class TypedAmqpWorker {
|
|
7
45
|
channel = null;
|
|
46
|
+
connection = null;
|
|
8
47
|
consumerTags = [];
|
|
9
|
-
constructor(contract, handlers,
|
|
48
|
+
constructor(contract, handlers, connectionOptions) {
|
|
10
49
|
this.contract = contract;
|
|
11
50
|
this.handlers = handlers;
|
|
12
|
-
this.
|
|
51
|
+
this.connectionOptions = connectionOptions;
|
|
13
52
|
}
|
|
14
53
|
/**
|
|
15
54
|
* Create a type-safe AMQP worker from a contract
|
|
@@ -26,13 +65,20 @@ var TypedAmqpWorker = class TypedAmqpWorker {
|
|
|
26
65
|
*/
|
|
27
66
|
async close() {
|
|
28
67
|
await this.stopConsuming();
|
|
29
|
-
if (this.channel)
|
|
30
|
-
|
|
68
|
+
if (this.channel) {
|
|
69
|
+
await this.channel.close();
|
|
70
|
+
this.channel = null;
|
|
71
|
+
}
|
|
72
|
+
if (this.connection) {
|
|
73
|
+
await this.connection.close();
|
|
74
|
+
this.connection = null;
|
|
75
|
+
}
|
|
31
76
|
}
|
|
32
77
|
/**
|
|
33
78
|
* Connect to AMQP broker
|
|
34
79
|
*/
|
|
35
80
|
async init() {
|
|
81
|
+
this.connection = await (0, amqplib.connect)(this.connectionOptions);
|
|
36
82
|
this.channel = await this.connection.createChannel();
|
|
37
83
|
if (this.contract.exchanges) for (const exchange of Object.values(this.contract.exchanges)) await this.channel.assertExchange(exchange.name, exchange.type, {
|
|
38
84
|
durable: exchange.durable,
|
|
@@ -64,25 +110,38 @@ var TypedAmqpWorker = class TypedAmqpWorker {
|
|
|
64
110
|
const consumers = this.contract.consumers;
|
|
65
111
|
if (!consumers) throw new Error("No consumers defined in contract");
|
|
66
112
|
const consumer = consumers[consumerName];
|
|
67
|
-
if (!consumer || typeof consumer !== "object")
|
|
113
|
+
if (!consumer || typeof consumer !== "object") {
|
|
114
|
+
const availableConsumers = Object.keys(consumers);
|
|
115
|
+
const available = availableConsumers.length > 0 ? availableConsumers.join(", ") : "none";
|
|
116
|
+
throw new Error(`Consumer not found: "${String(consumerName)}". Available consumers: ${available}`);
|
|
117
|
+
}
|
|
68
118
|
const consumerDef = consumer;
|
|
69
119
|
const handler = this.handlers[consumerName];
|
|
70
120
|
if (!handler) throw new Error(`Handler for "${String(consumerName)}" not provided`);
|
|
71
121
|
if (consumerDef.prefetch !== void 0) await this.channel.prefetch(consumerDef.prefetch);
|
|
72
122
|
const result = await this.channel.consume(consumerDef.queue, async (msg) => {
|
|
73
123
|
if (!msg) return;
|
|
124
|
+
const parseResult = _swan_io_boxed.Result.fromExecution(() => JSON.parse(msg.content.toString()));
|
|
125
|
+
if (parseResult.isError()) {
|
|
126
|
+
console.error(new TechnicalError(`Error parsing message for consumer "${String(consumerName)}"`, parseResult.error));
|
|
127
|
+
this.channel?.nack(msg, false, false);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const content = parseResult.value;
|
|
131
|
+
const rawValidation = consumerDef.message["~standard"].validate(content);
|
|
132
|
+
const resolvedValidation = rawValidation instanceof Promise ? await rawValidation : rawValidation;
|
|
133
|
+
const validationResult = typeof resolvedValidation === "object" && resolvedValidation !== null && "issues" in resolvedValidation && resolvedValidation.issues ? _swan_io_boxed.Result.Error(new MessageValidationError(String(consumerName), resolvedValidation.issues)) : _swan_io_boxed.Result.Ok(typeof resolvedValidation === "object" && resolvedValidation !== null && "value" in resolvedValidation ? resolvedValidation.value : content);
|
|
134
|
+
if (validationResult.isError()) {
|
|
135
|
+
console.error(validationResult.error);
|
|
136
|
+
this.channel?.nack(msg, false, false);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const validatedMessage = validationResult.value;
|
|
74
140
|
try {
|
|
75
|
-
|
|
76
|
-
const validation = consumerDef.message["~standard"].validate(content);
|
|
77
|
-
if (typeof validation === "object" && validation !== null && "issues" in validation && validation.issues) {
|
|
78
|
-
console.error("Message validation failed:", validation.issues);
|
|
79
|
-
this.channel?.nack(msg, false, false);
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
await handler(typeof validation === "object" && validation !== null && "value" in validation ? validation.value : content);
|
|
141
|
+
await handler(validatedMessage);
|
|
83
142
|
if (!consumerDef.noAck) this.channel?.ack(msg);
|
|
84
143
|
} catch (error) {
|
|
85
|
-
console.error(
|
|
144
|
+
console.error(new TechnicalError(`Error processing message for consumer "${String(consumerName)}"`, error));
|
|
86
145
|
this.channel?.nack(msg, false, true);
|
|
87
146
|
}
|
|
88
147
|
}, { noAck: consumerDef.noAck ?? false });
|
|
@@ -99,4 +158,6 @@ var TypedAmqpWorker = class TypedAmqpWorker {
|
|
|
99
158
|
};
|
|
100
159
|
|
|
101
160
|
//#endregion
|
|
161
|
+
exports.MessageValidationError = MessageValidationError;
|
|
162
|
+
exports.TechnicalError = TechnicalError;
|
|
102
163
|
exports.TypedAmqpWorker = TypedAmqpWorker;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Options } from "amqplib";
|
|
2
2
|
import { ContractDefinition, WorkerInferConsumerHandlers } from "@amqp-contract/contract";
|
|
3
3
|
|
|
4
4
|
//#region src/worker.d.ts
|
|
@@ -9,7 +9,7 @@ import { ContractDefinition, WorkerInferConsumerHandlers } from "@amqp-contract/
|
|
|
9
9
|
interface CreateWorkerOptions<TContract extends ContractDefinition> {
|
|
10
10
|
contract: TContract;
|
|
11
11
|
handlers: WorkerInferConsumerHandlers<TContract>;
|
|
12
|
-
connection:
|
|
12
|
+
connection: string | Options.Connect;
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
15
|
* Type-safe AMQP worker for consuming messages
|
|
@@ -17,8 +17,9 @@ interface CreateWorkerOptions<TContract extends ContractDefinition> {
|
|
|
17
17
|
declare class TypedAmqpWorker<TContract extends ContractDefinition> {
|
|
18
18
|
private readonly contract;
|
|
19
19
|
private readonly handlers;
|
|
20
|
-
private readonly
|
|
20
|
+
private readonly connectionOptions;
|
|
21
21
|
private channel;
|
|
22
|
+
private connection;
|
|
22
23
|
private consumerTags;
|
|
23
24
|
private constructor();
|
|
24
25
|
/**
|
|
@@ -48,5 +49,29 @@ declare class TypedAmqpWorker<TContract extends ContractDefinition> {
|
|
|
48
49
|
private stopConsuming;
|
|
49
50
|
}
|
|
50
51
|
//#endregion
|
|
51
|
-
|
|
52
|
+
//#region src/errors.d.ts
|
|
53
|
+
/**
|
|
54
|
+
* Base error class for worker errors
|
|
55
|
+
*/
|
|
56
|
+
declare abstract class WorkerError extends Error {
|
|
57
|
+
protected constructor(message: string);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Error for technical/runtime failures in worker operations
|
|
61
|
+
* This includes validation failures, parsing failures, and processing failures
|
|
62
|
+
*/
|
|
63
|
+
declare class TechnicalError extends WorkerError {
|
|
64
|
+
readonly cause?: unknown | undefined;
|
|
65
|
+
constructor(message: string, cause?: unknown | undefined);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Error thrown when message validation fails
|
|
69
|
+
*/
|
|
70
|
+
declare class MessageValidationError extends WorkerError {
|
|
71
|
+
readonly consumerName: string;
|
|
72
|
+
readonly issues: unknown;
|
|
73
|
+
constructor(consumerName: string, issues: unknown);
|
|
74
|
+
}
|
|
75
|
+
//#endregion
|
|
76
|
+
export { type CreateWorkerOptions, MessageValidationError, TechnicalError, TypedAmqpWorker };
|
|
52
77
|
//# sourceMappingURL=index.d.cts.map
|
package/dist/index.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/worker.ts"],"sourcesContent":[],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/worker.ts","../src/errors.ts"],"sourcesContent":[],"mappings":";;;;;;;AAcA;AAAuD,UAAtC,mBAAsC,CAAA,kBAAA,kBAAA,CAAA,CAAA;EAC3C,QAAA,EAAA,SAAA;EAC4B,QAAA,EAA5B,2BAA4B,CAAA,SAAA,CAAA;EAA5B,UAAA,EAAA,MAAA,GACW,OAAA,CAAQ,OADnB;;;AAOZ;;AAewC,cAf3B,eAe2B,CAAA,kBAfO,kBAeP,CAAA,CAAA;EACP,iBAAA,QAAA;EAApB,iBAAA,QAAA;EACgB,iBAAA,iBAAA;EAAhB,QAAA,OAAA;EAAR,QAAA,UAAA;EAUY,QAAA,YAAA;EAAO,QAAA,WAAA,CAAA;;;;;EC7BX,OAAA,MAAA,CAAA,kBDiB2B,kBCjBO,CAAA,CAAA,OAAA,EDkBlC,mBClBkC,CDkBd,SClBc,CAAA,CAAA,EDmB1C,OCnB0C,CDmBlC,eCnBkC,CDmBlB,SCnBkB,CAAA,CAAA;EAalC;;;WDgBI;;;;;;;;;;;;;;;;;;;;;;;uBC/CF,WAAA,SAAoB,KAAA;EDWlB,UAAA,WAAA,CAAmB,OAAA,EAAA,MAAA;;;;;;AAGE,cCIzB,cAAA,SAAuB,WAAA,CDJE;EAMzB,SAAA,KAAA,CAAA,EAAA,OAAe,GAAA,SAAA;EAAmB,WAAA,CAAA,OAAA,EAAA,MAAA,EAAA,KAAA,CAAA,EAAA,OAAA,GAAA,SAAA;;;;;AAiBlC,cCNA,sBAAA,SAA+B,WAAA,CDM/B;EAAR,SAAA,YAAA,EAAA,MAAA;EAUY,SAAA,MAAA,EAAA,OAAA;EAAO,WAAA,CAAA,YAAA,EAAA,MAAA,EAAA,MAAA,EAAA,OAAA"}
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Options } from "amqplib";
|
|
2
2
|
import { ContractDefinition, WorkerInferConsumerHandlers } from "@amqp-contract/contract";
|
|
3
3
|
|
|
4
4
|
//#region src/worker.d.ts
|
|
@@ -9,7 +9,7 @@ import { ContractDefinition, WorkerInferConsumerHandlers } from "@amqp-contract/
|
|
|
9
9
|
interface CreateWorkerOptions<TContract extends ContractDefinition> {
|
|
10
10
|
contract: TContract;
|
|
11
11
|
handlers: WorkerInferConsumerHandlers<TContract>;
|
|
12
|
-
connection:
|
|
12
|
+
connection: string | Options.Connect;
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
15
|
* Type-safe AMQP worker for consuming messages
|
|
@@ -17,8 +17,9 @@ interface CreateWorkerOptions<TContract extends ContractDefinition> {
|
|
|
17
17
|
declare class TypedAmqpWorker<TContract extends ContractDefinition> {
|
|
18
18
|
private readonly contract;
|
|
19
19
|
private readonly handlers;
|
|
20
|
-
private readonly
|
|
20
|
+
private readonly connectionOptions;
|
|
21
21
|
private channel;
|
|
22
|
+
private connection;
|
|
22
23
|
private consumerTags;
|
|
23
24
|
private constructor();
|
|
24
25
|
/**
|
|
@@ -48,5 +49,29 @@ declare class TypedAmqpWorker<TContract extends ContractDefinition> {
|
|
|
48
49
|
private stopConsuming;
|
|
49
50
|
}
|
|
50
51
|
//#endregion
|
|
51
|
-
|
|
52
|
+
//#region src/errors.d.ts
|
|
53
|
+
/**
|
|
54
|
+
* Base error class for worker errors
|
|
55
|
+
*/
|
|
56
|
+
declare abstract class WorkerError extends Error {
|
|
57
|
+
protected constructor(message: string);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Error for technical/runtime failures in worker operations
|
|
61
|
+
* This includes validation failures, parsing failures, and processing failures
|
|
62
|
+
*/
|
|
63
|
+
declare class TechnicalError extends WorkerError {
|
|
64
|
+
readonly cause?: unknown | undefined;
|
|
65
|
+
constructor(message: string, cause?: unknown | undefined);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Error thrown when message validation fails
|
|
69
|
+
*/
|
|
70
|
+
declare class MessageValidationError extends WorkerError {
|
|
71
|
+
readonly consumerName: string;
|
|
72
|
+
readonly issues: unknown;
|
|
73
|
+
constructor(consumerName: string, issues: unknown);
|
|
74
|
+
}
|
|
75
|
+
//#endregion
|
|
76
|
+
export { type CreateWorkerOptions, MessageValidationError, TechnicalError, TypedAmqpWorker };
|
|
52
77
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/worker.ts"],"sourcesContent":[],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/worker.ts","../src/errors.ts"],"sourcesContent":[],"mappings":";;;;;;;AAcA;AAAuD,UAAtC,mBAAsC,CAAA,kBAAA,kBAAA,CAAA,CAAA;EAC3C,QAAA,EAAA,SAAA;EAC4B,QAAA,EAA5B,2BAA4B,CAAA,SAAA,CAAA;EAA5B,UAAA,EAAA,MAAA,GACW,OAAA,CAAQ,OADnB;;;AAOZ;;AAewC,cAf3B,eAe2B,CAAA,kBAfO,kBAeP,CAAA,CAAA;EACP,iBAAA,QAAA;EAApB,iBAAA,QAAA;EACgB,iBAAA,iBAAA;EAAhB,QAAA,OAAA;EAAR,QAAA,UAAA;EAUY,QAAA,YAAA;EAAO,QAAA,WAAA,CAAA;;;;;EC7BX,OAAA,MAAA,CAAA,kBDiB2B,kBCjBO,CAAA,CAAA,OAAA,EDkBlC,mBClBkC,CDkBd,SClBc,CAAA,CAAA,EDmB1C,OCnB0C,CDmBlC,eCnBkC,CDmBlB,SCnBkB,CAAA,CAAA;EAalC;;;WDgBI;;;;;;;;;;;;;;;;;;;;;;;uBC/CF,WAAA,SAAoB,KAAA;EDWlB,UAAA,WAAA,CAAmB,OAAA,EAAA,MAAA;;;;;;AAGE,cCIzB,cAAA,SAAuB,WAAA,CDJE;EAMzB,SAAA,KAAA,CAAA,EAAA,OAAe,GAAA,SAAA;EAAmB,WAAA,CAAA,OAAA,EAAA,MAAA,EAAA,KAAA,CAAA,EAAA,OAAA,GAAA,SAAA;;;;;AAiBlC,cCNA,sBAAA,SAA+B,WAAA,CDM/B;EAAR,SAAA,YAAA,EAAA,MAAA;EAUY,SAAA,MAAA,EAAA,OAAA;EAAO,WAAA,CAAA,YAAA,EAAA,MAAA,EAAA,MAAA,EAAA,OAAA"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,14 +1,54 @@
|
|
|
1
|
+
import { connect } from "amqplib";
|
|
2
|
+
import { Result } from "@swan-io/boxed";
|
|
3
|
+
|
|
4
|
+
//#region src/errors.ts
|
|
5
|
+
/**
|
|
6
|
+
* Base error class for worker errors
|
|
7
|
+
*/
|
|
8
|
+
var WorkerError = class extends Error {
|
|
9
|
+
constructor(message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = "WorkerError";
|
|
12
|
+
const ErrorConstructor = Error;
|
|
13
|
+
if (typeof ErrorConstructor.captureStackTrace === "function") ErrorConstructor.captureStackTrace(this, this.constructor);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Error for technical/runtime failures in worker operations
|
|
18
|
+
* This includes validation failures, parsing failures, and processing failures
|
|
19
|
+
*/
|
|
20
|
+
var TechnicalError = class extends WorkerError {
|
|
21
|
+
constructor(message, cause) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.cause = cause;
|
|
24
|
+
this.name = "TechnicalError";
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Error thrown when message validation fails
|
|
29
|
+
*/
|
|
30
|
+
var MessageValidationError = class extends WorkerError {
|
|
31
|
+
constructor(consumerName, issues) {
|
|
32
|
+
super(`Message validation failed for consumer "${consumerName}"`);
|
|
33
|
+
this.consumerName = consumerName;
|
|
34
|
+
this.issues = issues;
|
|
35
|
+
this.name = "MessageValidationError";
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
//#endregion
|
|
1
40
|
//#region src/worker.ts
|
|
2
41
|
/**
|
|
3
42
|
* Type-safe AMQP worker for consuming messages
|
|
4
43
|
*/
|
|
5
44
|
var TypedAmqpWorker = class TypedAmqpWorker {
|
|
6
45
|
channel = null;
|
|
46
|
+
connection = null;
|
|
7
47
|
consumerTags = [];
|
|
8
|
-
constructor(contract, handlers,
|
|
48
|
+
constructor(contract, handlers, connectionOptions) {
|
|
9
49
|
this.contract = contract;
|
|
10
50
|
this.handlers = handlers;
|
|
11
|
-
this.
|
|
51
|
+
this.connectionOptions = connectionOptions;
|
|
12
52
|
}
|
|
13
53
|
/**
|
|
14
54
|
* Create a type-safe AMQP worker from a contract
|
|
@@ -25,13 +65,20 @@ var TypedAmqpWorker = class TypedAmqpWorker {
|
|
|
25
65
|
*/
|
|
26
66
|
async close() {
|
|
27
67
|
await this.stopConsuming();
|
|
28
|
-
if (this.channel)
|
|
29
|
-
|
|
68
|
+
if (this.channel) {
|
|
69
|
+
await this.channel.close();
|
|
70
|
+
this.channel = null;
|
|
71
|
+
}
|
|
72
|
+
if (this.connection) {
|
|
73
|
+
await this.connection.close();
|
|
74
|
+
this.connection = null;
|
|
75
|
+
}
|
|
30
76
|
}
|
|
31
77
|
/**
|
|
32
78
|
* Connect to AMQP broker
|
|
33
79
|
*/
|
|
34
80
|
async init() {
|
|
81
|
+
this.connection = await connect(this.connectionOptions);
|
|
35
82
|
this.channel = await this.connection.createChannel();
|
|
36
83
|
if (this.contract.exchanges) for (const exchange of Object.values(this.contract.exchanges)) await this.channel.assertExchange(exchange.name, exchange.type, {
|
|
37
84
|
durable: exchange.durable,
|
|
@@ -63,25 +110,38 @@ var TypedAmqpWorker = class TypedAmqpWorker {
|
|
|
63
110
|
const consumers = this.contract.consumers;
|
|
64
111
|
if (!consumers) throw new Error("No consumers defined in contract");
|
|
65
112
|
const consumer = consumers[consumerName];
|
|
66
|
-
if (!consumer || typeof consumer !== "object")
|
|
113
|
+
if (!consumer || typeof consumer !== "object") {
|
|
114
|
+
const availableConsumers = Object.keys(consumers);
|
|
115
|
+
const available = availableConsumers.length > 0 ? availableConsumers.join(", ") : "none";
|
|
116
|
+
throw new Error(`Consumer not found: "${String(consumerName)}". Available consumers: ${available}`);
|
|
117
|
+
}
|
|
67
118
|
const consumerDef = consumer;
|
|
68
119
|
const handler = this.handlers[consumerName];
|
|
69
120
|
if (!handler) throw new Error(`Handler for "${String(consumerName)}" not provided`);
|
|
70
121
|
if (consumerDef.prefetch !== void 0) await this.channel.prefetch(consumerDef.prefetch);
|
|
71
122
|
const result = await this.channel.consume(consumerDef.queue, async (msg) => {
|
|
72
123
|
if (!msg) return;
|
|
124
|
+
const parseResult = Result.fromExecution(() => JSON.parse(msg.content.toString()));
|
|
125
|
+
if (parseResult.isError()) {
|
|
126
|
+
console.error(new TechnicalError(`Error parsing message for consumer "${String(consumerName)}"`, parseResult.error));
|
|
127
|
+
this.channel?.nack(msg, false, false);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const content = parseResult.value;
|
|
131
|
+
const rawValidation = consumerDef.message["~standard"].validate(content);
|
|
132
|
+
const resolvedValidation = rawValidation instanceof Promise ? await rawValidation : rawValidation;
|
|
133
|
+
const validationResult = typeof resolvedValidation === "object" && resolvedValidation !== null && "issues" in resolvedValidation && resolvedValidation.issues ? Result.Error(new MessageValidationError(String(consumerName), resolvedValidation.issues)) : Result.Ok(typeof resolvedValidation === "object" && resolvedValidation !== null && "value" in resolvedValidation ? resolvedValidation.value : content);
|
|
134
|
+
if (validationResult.isError()) {
|
|
135
|
+
console.error(validationResult.error);
|
|
136
|
+
this.channel?.nack(msg, false, false);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const validatedMessage = validationResult.value;
|
|
73
140
|
try {
|
|
74
|
-
|
|
75
|
-
const validation = consumerDef.message["~standard"].validate(content);
|
|
76
|
-
if (typeof validation === "object" && validation !== null && "issues" in validation && validation.issues) {
|
|
77
|
-
console.error("Message validation failed:", validation.issues);
|
|
78
|
-
this.channel?.nack(msg, false, false);
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
await handler(typeof validation === "object" && validation !== null && "value" in validation ? validation.value : content);
|
|
141
|
+
await handler(validatedMessage);
|
|
82
142
|
if (!consumerDef.noAck) this.channel?.ack(msg);
|
|
83
143
|
} catch (error) {
|
|
84
|
-
console.error(
|
|
144
|
+
console.error(new TechnicalError(`Error processing message for consumer "${String(consumerName)}"`, error));
|
|
85
145
|
this.channel?.nack(msg, false, true);
|
|
86
146
|
}
|
|
87
147
|
}, { noAck: consumerDef.noAck ?? false });
|
|
@@ -98,5 +158,5 @@ var TypedAmqpWorker = class TypedAmqpWorker {
|
|
|
98
158
|
};
|
|
99
159
|
|
|
100
160
|
//#endregion
|
|
101
|
-
export { TypedAmqpWorker };
|
|
161
|
+
export { MessageValidationError, TechnicalError, TypedAmqpWorker };
|
|
102
162
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["contract: TContract","handlers: WorkerInferConsumerHandlers<TContract>","connection: ChannelModel"],"sources":["../src/worker.ts"],"sourcesContent":["import type { Channel, ChannelModel, ConsumeMessage } from \"amqplib\";\nimport type {\n ContractDefinition,\n InferConsumerNames,\n WorkerInferConsumerHandlers,\n} from \"@amqp-contract/contract\";\n\n/**\n * Options for creating a worker\n */\nexport interface CreateWorkerOptions<TContract extends ContractDefinition> {\n contract: TContract;\n handlers: WorkerInferConsumerHandlers<TContract>;\n connection: ChannelModel;\n}\n\n/**\n * Type-safe AMQP worker for consuming messages\n */\nexport class TypedAmqpWorker<TContract extends ContractDefinition> {\n private channel: Channel | null = null;\n private consumerTags: string[] = [];\n\n private constructor(\n private readonly contract: TContract,\n private readonly handlers: WorkerInferConsumerHandlers<TContract>,\n private readonly connection: ChannelModel,\n ) {}\n\n /**\n * Create a type-safe AMQP worker from a contract\n * The worker will automatically connect and start consuming all messages\n */\n static async create<TContract extends ContractDefinition>(\n options: CreateWorkerOptions<TContract>,\n ): Promise<TypedAmqpWorker<TContract>> {\n const worker = new TypedAmqpWorker(options.contract, options.handlers, options.connection);\n await worker.init();\n await worker.consumeAll();\n return worker;\n }\n\n /**\n * Close the connection\n */\n async close(): Promise<void> {\n await this.stopConsuming();\n\n if (this.channel) {\n await this.channel.close();\n }\n\n await this.connection.close();\n }\n\n /**\n * Connect to AMQP broker\n */\n private async init(): Promise<void> {\n this.channel = await this.connection.createChannel();\n\n // Setup exchanges\n if (this.contract.exchanges) {\n for (const exchange of Object.values(this.contract.exchanges)) {\n await this.channel.assertExchange(exchange.name, exchange.type, {\n durable: exchange.durable,\n autoDelete: exchange.autoDelete,\n internal: exchange.internal,\n arguments: exchange.arguments,\n });\n }\n }\n\n // Setup queues\n if (this.contract.queues) {\n for (const queue of Object.values(this.contract.queues)) {\n await this.channel.assertQueue(queue.name, {\n durable: queue.durable,\n exclusive: queue.exclusive,\n autoDelete: queue.autoDelete,\n arguments: queue.arguments,\n });\n }\n }\n\n // Setup bindings\n if (this.contract.bindings) {\n for (const binding of Object.values(this.contract.bindings)) {\n await this.channel.bindQueue(\n binding.queue,\n binding.exchange,\n binding.routingKey ?? \"\",\n binding.arguments,\n );\n }\n }\n }\n\n /**\n * Start consuming messages for all consumers\n */\n private async consumeAll(): Promise<void> {\n if (!this.contract.consumers) {\n throw new Error(\"No consumers defined in contract\");\n }\n\n const consumerNames = Object.keys(this.contract.consumers) as InferConsumerNames<TContract>[];\n\n for (const consumerName of consumerNames) {\n await this.consume(consumerName);\n }\n }\n\n /**\n * Start consuming messages for a specific consumer\n */\n private async consume<TName extends InferConsumerNames<TContract>>(\n consumerName: TName,\n ): Promise<void> {\n if (!this.channel) {\n throw new Error(\n \"Worker not initialized. Use TypedAmqpWorker.create() to obtain an initialized worker instance.\",\n );\n }\n\n const consumers = this.contract.consumers as Record<string, unknown>;\n if (!consumers) {\n throw new Error(\"No consumers defined in contract\");\n }\n\n const consumer = consumers[consumerName as string];\n if (!consumer || typeof consumer !== \"object\") {\n throw new Error(`Consumer \"${String(consumerName)}\" not found in contract`);\n }\n\n const consumerDef = consumer as {\n queue: string;\n message: { \"~standard\": { validate: (value: unknown) => unknown } };\n prefetch?: number;\n noAck?: boolean;\n };\n\n const handler = this.handlers[consumerName];\n if (!handler) {\n throw new Error(`Handler for \"${String(consumerName)}\" not provided`);\n }\n\n // Set prefetch if specified\n if (consumerDef.prefetch !== undefined) {\n await this.channel.prefetch(consumerDef.prefetch);\n }\n\n // Start consuming\n const result = await this.channel.consume(\n consumerDef.queue,\n async (msg: ConsumeMessage | null) => {\n if (!msg) {\n return;\n }\n\n try {\n // Parse message\n const content = JSON.parse(msg.content.toString());\n\n // Validate message using schema\n const validation = consumerDef.message[\"~standard\"].validate(content);\n if (\n typeof validation === \"object\" &&\n validation !== null &&\n \"issues\" in validation &&\n validation.issues\n ) {\n console.error(\"Message validation failed:\", validation.issues);\n // Reject message with no requeue\n this.channel?.nack(msg, false, false);\n return;\n }\n\n const validatedMessage =\n typeof validation === \"object\" && validation !== null && \"value\" in validation\n ? validation.value\n : content;\n\n // Call handler\n await handler(validatedMessage);\n\n // Acknowledge message if not in noAck mode\n if (!consumerDef.noAck) {\n this.channel?.ack(msg);\n }\n } catch (error) {\n console.error(\"Error processing message:\", error);\n // Reject message and requeue\n this.channel?.nack(msg, false, true);\n }\n },\n {\n noAck: consumerDef.noAck ?? false,\n },\n );\n\n this.consumerTags.push(result.consumerTag);\n }\n\n /**\n * Stop consuming messages\n */\n private async stopConsuming(): Promise<void> {\n if (!this.channel) {\n return;\n }\n\n for (const tag of this.consumerTags) {\n await this.channel.cancel(tag);\n }\n\n this.consumerTags = [];\n }\n}\n"],"mappings":";;;;AAmBA,IAAa,kBAAb,MAAa,gBAAsD;CACjE,AAAQ,UAA0B;CAClC,AAAQ,eAAyB,EAAE;CAEnC,AAAQ,YACN,AAAiBA,UACjB,AAAiBC,UACjB,AAAiBC,YACjB;EAHiB;EACA;EACA;;;;;;CAOnB,aAAa,OACX,SACqC;EACrC,MAAM,SAAS,IAAI,gBAAgB,QAAQ,UAAU,QAAQ,UAAU,QAAQ,WAAW;AAC1F,QAAM,OAAO,MAAM;AACnB,QAAM,OAAO,YAAY;AACzB,SAAO;;;;;CAMT,MAAM,QAAuB;AAC3B,QAAM,KAAK,eAAe;AAE1B,MAAI,KAAK,QACP,OAAM,KAAK,QAAQ,OAAO;AAG5B,QAAM,KAAK,WAAW,OAAO;;;;;CAM/B,MAAc,OAAsB;AAClC,OAAK,UAAU,MAAM,KAAK,WAAW,eAAe;AAGpD,MAAI,KAAK,SAAS,UAChB,MAAK,MAAM,YAAY,OAAO,OAAO,KAAK,SAAS,UAAU,CAC3D,OAAM,KAAK,QAAQ,eAAe,SAAS,MAAM,SAAS,MAAM;GAC9D,SAAS,SAAS;GAClB,YAAY,SAAS;GACrB,UAAU,SAAS;GACnB,WAAW,SAAS;GACrB,CAAC;AAKN,MAAI,KAAK,SAAS,OAChB,MAAK,MAAM,SAAS,OAAO,OAAO,KAAK,SAAS,OAAO,CACrD,OAAM,KAAK,QAAQ,YAAY,MAAM,MAAM;GACzC,SAAS,MAAM;GACf,WAAW,MAAM;GACjB,YAAY,MAAM;GAClB,WAAW,MAAM;GAClB,CAAC;AAKN,MAAI,KAAK,SAAS,SAChB,MAAK,MAAM,WAAW,OAAO,OAAO,KAAK,SAAS,SAAS,CACzD,OAAM,KAAK,QAAQ,UACjB,QAAQ,OACR,QAAQ,UACR,QAAQ,cAAc,IACtB,QAAQ,UACT;;;;;CAQP,MAAc,aAA4B;AACxC,MAAI,CAAC,KAAK,SAAS,UACjB,OAAM,IAAI,MAAM,mCAAmC;EAGrD,MAAM,gBAAgB,OAAO,KAAK,KAAK,SAAS,UAAU;AAE1D,OAAK,MAAM,gBAAgB,cACzB,OAAM,KAAK,QAAQ,aAAa;;;;;CAOpC,MAAc,QACZ,cACe;AACf,MAAI,CAAC,KAAK,QACR,OAAM,IAAI,MACR,iGACD;EAGH,MAAM,YAAY,KAAK,SAAS;AAChC,MAAI,CAAC,UACH,OAAM,IAAI,MAAM,mCAAmC;EAGrD,MAAM,WAAW,UAAU;AAC3B,MAAI,CAAC,YAAY,OAAO,aAAa,SACnC,OAAM,IAAI,MAAM,aAAa,OAAO,aAAa,CAAC,yBAAyB;EAG7E,MAAM,cAAc;EAOpB,MAAM,UAAU,KAAK,SAAS;AAC9B,MAAI,CAAC,QACH,OAAM,IAAI,MAAM,gBAAgB,OAAO,aAAa,CAAC,gBAAgB;AAIvE,MAAI,YAAY,aAAa,OAC3B,OAAM,KAAK,QAAQ,SAAS,YAAY,SAAS;EAInD,MAAM,SAAS,MAAM,KAAK,QAAQ,QAChC,YAAY,OACZ,OAAO,QAA+B;AACpC,OAAI,CAAC,IACH;AAGF,OAAI;IAEF,MAAM,UAAU,KAAK,MAAM,IAAI,QAAQ,UAAU,CAAC;IAGlD,MAAM,aAAa,YAAY,QAAQ,aAAa,SAAS,QAAQ;AACrE,QACE,OAAO,eAAe,YACtB,eAAe,QACf,YAAY,cACZ,WAAW,QACX;AACA,aAAQ,MAAM,8BAA8B,WAAW,OAAO;AAE9D,UAAK,SAAS,KAAK,KAAK,OAAO,MAAM;AACrC;;AASF,UAAM,QALJ,OAAO,eAAe,YAAY,eAAe,QAAQ,WAAW,aAChE,WAAW,QACX,QAGyB;AAG/B,QAAI,CAAC,YAAY,MACf,MAAK,SAAS,IAAI,IAAI;YAEjB,OAAO;AACd,YAAQ,MAAM,6BAA6B,MAAM;AAEjD,SAAK,SAAS,KAAK,KAAK,OAAO,KAAK;;KAGxC,EACE,OAAO,YAAY,SAAS,OAC7B,CACF;AAED,OAAK,aAAa,KAAK,OAAO,YAAY;;;;;CAM5C,MAAc,gBAA+B;AAC3C,MAAI,CAAC,KAAK,QACR;AAGF,OAAK,MAAM,OAAO,KAAK,aACrB,OAAM,KAAK,QAAQ,OAAO,IAAI;AAGhC,OAAK,eAAe,EAAE"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["cause?: unknown","consumerName: string","issues: unknown","contract: TContract","handlers: WorkerInferConsumerHandlers<TContract>","connectionOptions: string | Options.Connect","validationResult: Result<unknown, MessageValidationError>"],"sources":["../src/errors.ts","../src/worker.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 { connect } from \"amqplib\";\nimport type { Channel, ChannelModel, ConsumeMessage, Options } from \"amqplib\";\nimport type {\n ContractDefinition,\n InferConsumerNames,\n WorkerInferConsumerHandlers,\n WorkerInferConsumerInput,\n} from \"@amqp-contract/contract\";\nimport { Result } from \"@swan-io/boxed\";\nimport { MessageValidationError, TechnicalError } from \"./errors.js\";\n\n/**\n * Options for creating a worker\n */\nexport interface CreateWorkerOptions<TContract extends ContractDefinition> {\n contract: TContract;\n handlers: WorkerInferConsumerHandlers<TContract>;\n connection: string | Options.Connect;\n}\n\n/**\n * Type-safe AMQP worker for consuming messages\n */\nexport class TypedAmqpWorker<TContract extends ContractDefinition> {\n private channel: Channel | null = null;\n private connection: ChannelModel | null = null;\n private consumerTags: string[] = [];\n\n private constructor(\n private readonly contract: TContract,\n private readonly handlers: WorkerInferConsumerHandlers<TContract>,\n private readonly connectionOptions: string | Options.Connect,\n ) {}\n\n /**\n * Create a type-safe AMQP worker from a contract\n * The worker will automatically connect and start consuming all messages\n */\n static async create<TContract extends ContractDefinition>(\n options: CreateWorkerOptions<TContract>,\n ): Promise<TypedAmqpWorker<TContract>> {\n const worker = new TypedAmqpWorker(options.contract, options.handlers, options.connection);\n await worker.init();\n await worker.consumeAll();\n return worker;\n }\n\n /**\n * Close the connection\n */\n async close(): Promise<void> {\n await this.stopConsuming();\n\n if (this.channel) {\n await this.channel.close();\n this.channel = null;\n }\n\n if (this.connection) {\n await this.connection.close();\n this.connection = null;\n }\n }\n\n /**\n * Connect to AMQP broker\n */\n private async init(): Promise<void> {\n this.connection = await connect(this.connectionOptions);\n this.channel = await this.connection.createChannel();\n\n // Setup exchanges\n if (this.contract.exchanges) {\n for (const exchange of Object.values(this.contract.exchanges)) {\n await this.channel.assertExchange(exchange.name, exchange.type, {\n durable: exchange.durable,\n autoDelete: exchange.autoDelete,\n internal: exchange.internal,\n arguments: exchange.arguments,\n });\n }\n }\n\n // Setup queues\n if (this.contract.queues) {\n for (const queue of Object.values(this.contract.queues)) {\n await this.channel.assertQueue(queue.name, {\n durable: queue.durable,\n exclusive: queue.exclusive,\n autoDelete: queue.autoDelete,\n arguments: queue.arguments,\n });\n }\n }\n\n // Setup bindings\n if (this.contract.bindings) {\n for (const binding of Object.values(this.contract.bindings)) {\n await this.channel.bindQueue(\n binding.queue,\n binding.exchange,\n binding.routingKey ?? \"\",\n binding.arguments,\n );\n }\n }\n }\n\n /**\n * Start consuming messages for all consumers\n */\n private async consumeAll(): Promise<void> {\n if (!this.contract.consumers) {\n throw new Error(\"No consumers defined in contract\");\n }\n\n const consumerNames = Object.keys(this.contract.consumers) as InferConsumerNames<TContract>[];\n\n for (const consumerName of consumerNames) {\n await this.consume(consumerName);\n }\n }\n\n /**\n * Start consuming messages for a specific consumer\n */\n private async consume<TName extends InferConsumerNames<TContract>>(\n consumerName: TName,\n ): Promise<void> {\n if (!this.channel) {\n throw new Error(\n \"Worker not initialized. Use TypedAmqpWorker.create() to obtain an initialized worker instance.\",\n );\n }\n\n const consumers = this.contract.consumers as Record<string, unknown>;\n if (!consumers) {\n throw new Error(\"No consumers defined in contract\");\n }\n\n const consumer = consumers[consumerName as string];\n if (!consumer || typeof consumer !== \"object\") {\n const availableConsumers = Object.keys(consumers);\n const available = availableConsumers.length > 0 ? availableConsumers.join(\", \") : \"none\";\n throw new Error(\n `Consumer not found: \"${String(consumerName)}\". Available consumers: ${available}`,\n );\n }\n\n const consumerDef = consumer as {\n queue: string;\n message: { \"~standard\": { validate: (value: unknown) => unknown } };\n prefetch?: number;\n noAck?: boolean;\n };\n\n const handler = this.handlers[consumerName];\n if (!handler) {\n throw new Error(`Handler for \"${String(consumerName)}\" not provided`);\n }\n\n // Set prefetch if specified\n if (consumerDef.prefetch !== undefined) {\n await this.channel.prefetch(consumerDef.prefetch);\n }\n\n // Start consuming\n const result = await this.channel.consume(\n consumerDef.queue,\n async (msg: ConsumeMessage | null) => {\n if (!msg) {\n return;\n }\n\n // Parse message\n const parseResult = Result.fromExecution(() => JSON.parse(msg.content.toString()));\n\n if (parseResult.isError()) {\n console.error(\n new TechnicalError(\n `Error parsing message for consumer \"${String(consumerName)}\"`,\n parseResult.error,\n ),\n );\n // Reject message with no requeue (malformed JSON)\n this.channel?.nack(msg, false, false);\n return;\n }\n\n const content = parseResult.value;\n\n // Validate message using schema (supports sync and async validators)\n const rawValidation = consumerDef.message[\"~standard\"].validate(content);\n const resolvedValidation =\n rawValidation instanceof Promise ? await rawValidation : rawValidation;\n const validationResult: Result<unknown, MessageValidationError> =\n typeof resolvedValidation === \"object\" &&\n resolvedValidation !== null &&\n \"issues\" in resolvedValidation &&\n resolvedValidation.issues\n ? Result.Error(\n new MessageValidationError(String(consumerName), resolvedValidation.issues),\n )\n : Result.Ok(\n typeof resolvedValidation === \"object\" &&\n resolvedValidation !== null &&\n \"value\" in resolvedValidation\n ? resolvedValidation.value\n : content,\n );\n\n if (validationResult.isError()) {\n console.error(validationResult.error);\n // Reject message with no requeue (validation failed)\n this.channel?.nack(msg, false, false);\n return;\n }\n\n const validatedMessage = validationResult.value as WorkerInferConsumerInput<\n TContract,\n TName\n >;\n\n // Call handler and wait for Promise to resolve\n try {\n await handler(validatedMessage);\n\n // Acknowledge message if not in noAck mode\n if (!consumerDef.noAck) {\n this.channel?.ack(msg);\n }\n } catch (error) {\n console.error(\n new TechnicalError(\n `Error processing message for consumer \"${String(consumerName)}\"`,\n error,\n ),\n );\n // Reject message and requeue (handler failed)\n this.channel?.nack(msg, false, true);\n }\n },\n {\n noAck: consumerDef.noAck ?? false,\n },\n );\n\n this.consumerTags.push(result.consumerTag);\n }\n\n /**\n * Stop consuming messages\n */\n private async stopConsuming(): Promise<void> {\n if (!this.channel) {\n return;\n }\n\n for (const tag of this.consumerTags) {\n await this.channel.cancel(tag);\n }\n\n this.consumerTags = [];\n }\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amqp-contract/worker",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Worker utilities for consuming messages using amqp-contract",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"amqp",
|
|
@@ -41,7 +41,8 @@
|
|
|
41
41
|
"dist"
|
|
42
42
|
],
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@
|
|
44
|
+
"@swan-io/boxed": "3.2.1",
|
|
45
|
+
"@amqp-contract/contract": "0.1.0"
|
|
45
46
|
},
|
|
46
47
|
"devDependencies": {
|
|
47
48
|
"@types/amqplib": "0.10.8",
|
|
@@ -52,8 +53,8 @@
|
|
|
52
53
|
"typescript": "5.9.3",
|
|
53
54
|
"vitest": "4.0.16",
|
|
54
55
|
"zod": "4.2.1",
|
|
55
|
-
"@amqp-contract/client": "0.0
|
|
56
|
-
"@amqp-contract/testing": "0.0
|
|
56
|
+
"@amqp-contract/client": "0.1.0",
|
|
57
|
+
"@amqp-contract/testing": "0.1.0",
|
|
57
58
|
"@amqp-contract/tsconfig": "0.0.0"
|
|
58
59
|
},
|
|
59
60
|
"peerDependencies": {
|