@amqp-contract/worker 0.0.1
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/LICENSE +21 -0
- package/README.md +72 -0
- package/dist/index.cjs +106 -0
- package/dist/index.d.cts +43 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +43 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +105 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +66 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Benoit Travers
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# @amqp-contract/worker
|
|
2
|
+
|
|
3
|
+
Type-safe AMQP worker for consuming messages using amqp-contract.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @amqp-contract/worker amqplib
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { createWorker } from '@amqp-contract/worker';
|
|
15
|
+
import { connect } from 'amqplib';
|
|
16
|
+
import { contract } from './contract';
|
|
17
|
+
|
|
18
|
+
// Connect to RabbitMQ
|
|
19
|
+
const connection = await connect('amqp://localhost');
|
|
20
|
+
|
|
21
|
+
// Create worker from contract with handlers
|
|
22
|
+
const worker = createWorker(contract, {
|
|
23
|
+
processOrder: async (message) => {
|
|
24
|
+
console.log('Processing order:', message.orderId);
|
|
25
|
+
// Your business logic here
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
await worker.connect(connection);
|
|
30
|
+
|
|
31
|
+
// Start consuming all consumers
|
|
32
|
+
await worker.consumeAll();
|
|
33
|
+
|
|
34
|
+
// Or start consuming a specific consumer
|
|
35
|
+
// await worker.consume('processOrder');
|
|
36
|
+
|
|
37
|
+
// Stop consuming when needed
|
|
38
|
+
// await worker.stopConsuming();
|
|
39
|
+
|
|
40
|
+
// Clean up
|
|
41
|
+
// await worker.close();
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## API
|
|
45
|
+
|
|
46
|
+
### `createWorker(contract, handlers)`
|
|
47
|
+
|
|
48
|
+
Create a type-safe AMQP worker from a contract with message handlers.
|
|
49
|
+
|
|
50
|
+
### `AmqpWorker.connect(connection)`
|
|
51
|
+
|
|
52
|
+
Connect to an AMQP broker and set up all exchanges, queues, and bindings defined in the contract.
|
|
53
|
+
|
|
54
|
+
### `AmqpWorker.consume(consumerName)`
|
|
55
|
+
|
|
56
|
+
Start consuming messages for a specific consumer.
|
|
57
|
+
|
|
58
|
+
### `AmqpWorker.consumeAll()`
|
|
59
|
+
|
|
60
|
+
Start consuming messages for all consumers defined in the contract.
|
|
61
|
+
|
|
62
|
+
### `AmqpWorker.stopConsuming()`
|
|
63
|
+
|
|
64
|
+
Stop consuming messages from all consumers.
|
|
65
|
+
|
|
66
|
+
### `AmqpWorker.close()`
|
|
67
|
+
|
|
68
|
+
Stop consuming and close the channel and connection.
|
|
69
|
+
|
|
70
|
+
## License
|
|
71
|
+
|
|
72
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
|
|
2
|
+
//#region src/worker.ts
|
|
3
|
+
/**
|
|
4
|
+
* Type-safe AMQP worker for consuming messages
|
|
5
|
+
*/
|
|
6
|
+
var AmqpWorker = class {
|
|
7
|
+
channel = null;
|
|
8
|
+
connection = null;
|
|
9
|
+
consumerTags = [];
|
|
10
|
+
constructor(contract, handlers) {
|
|
11
|
+
this.contract = contract;
|
|
12
|
+
this.handlers = handlers;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Connect to AMQP broker
|
|
16
|
+
*/
|
|
17
|
+
async connect(connection) {
|
|
18
|
+
this.connection = connection;
|
|
19
|
+
this.channel = await connection.createChannel();
|
|
20
|
+
if (this.contract.exchanges && this.channel) for (const exchange of Object.values(this.contract.exchanges)) await this.channel.assertExchange(exchange.name, exchange.type, {
|
|
21
|
+
durable: exchange.durable,
|
|
22
|
+
autoDelete: exchange.autoDelete,
|
|
23
|
+
internal: exchange.internal,
|
|
24
|
+
arguments: exchange.arguments
|
|
25
|
+
});
|
|
26
|
+
if (this.contract.queues && this.channel) for (const queue of Object.values(this.contract.queues)) await this.channel.assertQueue(queue.name, {
|
|
27
|
+
durable: queue.durable,
|
|
28
|
+
exclusive: queue.exclusive,
|
|
29
|
+
autoDelete: queue.autoDelete,
|
|
30
|
+
arguments: queue.arguments
|
|
31
|
+
});
|
|
32
|
+
if (this.contract.bindings && this.channel) for (const binding of Object.values(this.contract.bindings)) await this.channel.bindQueue(binding.queue, binding.exchange, binding.routingKey ?? "", binding.arguments);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Start consuming messages for a specific consumer
|
|
36
|
+
*/
|
|
37
|
+
async consume(consumerName) {
|
|
38
|
+
if (!this.channel) throw new Error("Worker not connected. Call connect() first.");
|
|
39
|
+
const consumers = this.contract.consumers;
|
|
40
|
+
if (!consumers) throw new Error("No consumers defined in contract");
|
|
41
|
+
const consumer = consumers[consumerName];
|
|
42
|
+
if (!consumer || typeof consumer !== "object") throw new Error(`Consumer "${String(consumerName)}" not found in contract`);
|
|
43
|
+
const consumerDef = consumer;
|
|
44
|
+
const handler = this.handlers[consumerName];
|
|
45
|
+
if (!handler) throw new Error(`Handler for "${String(consumerName)}" not provided`);
|
|
46
|
+
if (consumerDef.prefetch !== void 0) await this.channel.prefetch(consumerDef.prefetch);
|
|
47
|
+
const result = await this.channel.consume(consumerDef.queue, async (msg) => {
|
|
48
|
+
if (!msg) return;
|
|
49
|
+
try {
|
|
50
|
+
const content = JSON.parse(msg.content.toString());
|
|
51
|
+
const validation = consumerDef.message["~standard"].validate(content);
|
|
52
|
+
if (typeof validation === "object" && validation !== null && "issues" in validation && validation.issues) {
|
|
53
|
+
console.error("Message validation failed:", validation.issues);
|
|
54
|
+
this.channel?.nack(msg, false, false);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
await handler(typeof validation === "object" && validation !== null && "value" in validation ? validation.value : content);
|
|
58
|
+
if (!consumerDef.noAck) this.channel?.ack(msg);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error("Error processing message:", error);
|
|
61
|
+
this.channel?.nack(msg, false, true);
|
|
62
|
+
}
|
|
63
|
+
}, { noAck: consumerDef.noAck ?? false });
|
|
64
|
+
this.consumerTags.push(result.consumerTag);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Start consuming messages for all consumers
|
|
68
|
+
*/
|
|
69
|
+
async consumeAll() {
|
|
70
|
+
if (!this.contract.consumers) throw new Error("No consumers defined in contract");
|
|
71
|
+
const consumerNames = Object.keys(this.contract.consumers);
|
|
72
|
+
for (const consumerName of consumerNames) await this.consume(consumerName);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Stop consuming messages
|
|
76
|
+
*/
|
|
77
|
+
async stopConsuming() {
|
|
78
|
+
if (!this.channel) return;
|
|
79
|
+
for (const tag of this.consumerTags) await this.channel.cancel(tag);
|
|
80
|
+
this.consumerTags = [];
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Close the connection
|
|
84
|
+
*/
|
|
85
|
+
async close() {
|
|
86
|
+
await this.stopConsuming();
|
|
87
|
+
if (this.channel) {
|
|
88
|
+
await this.channel.close();
|
|
89
|
+
this.channel = null;
|
|
90
|
+
}
|
|
91
|
+
if (this.connection) {
|
|
92
|
+
await this.connection.close();
|
|
93
|
+
this.connection = null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* Create a type-safe AMQP worker from a contract
|
|
99
|
+
*/
|
|
100
|
+
function createWorker(contract, handlers) {
|
|
101
|
+
return new AmqpWorker(contract, handlers);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
//#endregion
|
|
105
|
+
exports.AmqpWorker = AmqpWorker;
|
|
106
|
+
exports.createWorker = createWorker;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Connection } from "amqplib";
|
|
2
|
+
import { ContractDefinition, InferConsumerNames, WorkerInferConsumerHandlers } from "@amqp-contract/contract";
|
|
3
|
+
|
|
4
|
+
//#region src/worker.d.ts
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Type-safe AMQP worker for consuming messages
|
|
8
|
+
*/
|
|
9
|
+
declare class AmqpWorker<TContract extends ContractDefinition> {
|
|
10
|
+
private readonly contract;
|
|
11
|
+
private readonly handlers;
|
|
12
|
+
private channel;
|
|
13
|
+
private connection;
|
|
14
|
+
private consumerTags;
|
|
15
|
+
constructor(contract: TContract, handlers: WorkerInferConsumerHandlers<TContract>);
|
|
16
|
+
/**
|
|
17
|
+
* Connect to AMQP broker
|
|
18
|
+
*/
|
|
19
|
+
connect(connection: Connection): Promise<void>;
|
|
20
|
+
/**
|
|
21
|
+
* Start consuming messages for a specific consumer
|
|
22
|
+
*/
|
|
23
|
+
consume<TName extends InferConsumerNames<TContract>>(consumerName: TName): Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Start consuming messages for all consumers
|
|
26
|
+
*/
|
|
27
|
+
consumeAll(): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Stop consuming messages
|
|
30
|
+
*/
|
|
31
|
+
stopConsuming(): Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* Close the connection
|
|
34
|
+
*/
|
|
35
|
+
close(): Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Create a type-safe AMQP worker from a contract
|
|
39
|
+
*/
|
|
40
|
+
declare function createWorker<TContract extends ContractDefinition>(contract: TContract, handlers: WorkerInferConsumerHandlers<TContract>): AmqpWorker<TContract>;
|
|
41
|
+
//#endregion
|
|
42
|
+
export { AmqpWorker, createWorker };
|
|
43
|
+
//# sourceMappingURL=index.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/worker.ts"],"sourcesContent":[],"mappings":";;;;;;;AAUA;AAA0C,cAA7B,UAA6B,CAAA,kBAAA,kBAAA,CAAA,CAAA;EAMX,iBAAA,QAAA;EAC4B,iBAAA,QAAA;EAA5B,QAAA,OAAA;EAMH,QAAA,UAAA;EAAiB,QAAA,YAAA;EA8CI,WAAA,CAAA,QAAA,EArDlB,SAqDkB,EAAA,QAAA,EApDlB,2BAoDkB,CApDU,SAoDV,CAAA;EAAnB;;;EAuFR,OAAA,CAAA,UAAA,EArIM,UAqIN,CAAA,EArIuB,OAqIvB,CAAA,IAAA,CAAA;EAeG;;;EAiCT,OAAA,CAAA,cAvIc,kBAuIF,CAvIqB,SAuIrB,CAAA,CAAA,CAAA,YAAA,EAvI+C,KAuI/C,CAAA,EAvIuD,OAuIvD,CAAA,IAAA,CAAA;EAAmB;;;EAEnC,UAAA,CAAA,CAAA,EAlDU,OAkDV,CAAA,IAAA,CAAA;EACE;;;mBApCW;;;;WAeR;;;;;iBAkBD,+BAA+B,8BACnC,qBACA,4BAA4B,aACrC,WAAW"}
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Connection } from "amqplib";
|
|
2
|
+
import { ContractDefinition, InferConsumerNames, WorkerInferConsumerHandlers } from "@amqp-contract/contract";
|
|
3
|
+
|
|
4
|
+
//#region src/worker.d.ts
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Type-safe AMQP worker for consuming messages
|
|
8
|
+
*/
|
|
9
|
+
declare class AmqpWorker<TContract extends ContractDefinition> {
|
|
10
|
+
private readonly contract;
|
|
11
|
+
private readonly handlers;
|
|
12
|
+
private channel;
|
|
13
|
+
private connection;
|
|
14
|
+
private consumerTags;
|
|
15
|
+
constructor(contract: TContract, handlers: WorkerInferConsumerHandlers<TContract>);
|
|
16
|
+
/**
|
|
17
|
+
* Connect to AMQP broker
|
|
18
|
+
*/
|
|
19
|
+
connect(connection: Connection): Promise<void>;
|
|
20
|
+
/**
|
|
21
|
+
* Start consuming messages for a specific consumer
|
|
22
|
+
*/
|
|
23
|
+
consume<TName extends InferConsumerNames<TContract>>(consumerName: TName): Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Start consuming messages for all consumers
|
|
26
|
+
*/
|
|
27
|
+
consumeAll(): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Stop consuming messages
|
|
30
|
+
*/
|
|
31
|
+
stopConsuming(): Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* Close the connection
|
|
34
|
+
*/
|
|
35
|
+
close(): Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Create a type-safe AMQP worker from a contract
|
|
39
|
+
*/
|
|
40
|
+
declare function createWorker<TContract extends ContractDefinition>(contract: TContract, handlers: WorkerInferConsumerHandlers<TContract>): AmqpWorker<TContract>;
|
|
41
|
+
//#endregion
|
|
42
|
+
export { AmqpWorker, createWorker };
|
|
43
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/worker.ts"],"sourcesContent":[],"mappings":";;;;;;;AAUA;AAA0C,cAA7B,UAA6B,CAAA,kBAAA,kBAAA,CAAA,CAAA;EAMX,iBAAA,QAAA;EAC4B,iBAAA,QAAA;EAA5B,QAAA,OAAA;EAMH,QAAA,UAAA;EAAiB,QAAA,YAAA;EA8CI,WAAA,CAAA,QAAA,EArDlB,SAqDkB,EAAA,QAAA,EApDlB,2BAoDkB,CApDU,SAoDV,CAAA;EAAnB;;;EAuFR,OAAA,CAAA,UAAA,EArIM,UAqIN,CAAA,EArIuB,OAqIvB,CAAA,IAAA,CAAA;EAeG;;;EAiCT,OAAA,CAAA,cAvIc,kBAuIF,CAvIqB,SAuIrB,CAAA,CAAA,CAAA,YAAA,EAvI+C,KAuI/C,CAAA,EAvIuD,OAuIvD,CAAA,IAAA,CAAA;EAAmB;;;EAEnC,UAAA,CAAA,CAAA,EAlDU,OAkDV,CAAA,IAAA,CAAA;EACE;;;mBApCW;;;;WAeR;;;;;iBAkBD,+BAA+B,8BACnC,qBACA,4BAA4B,aACrC,WAAW"}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
//#region src/worker.ts
|
|
2
|
+
/**
|
|
3
|
+
* Type-safe AMQP worker for consuming messages
|
|
4
|
+
*/
|
|
5
|
+
var AmqpWorker = class {
|
|
6
|
+
channel = null;
|
|
7
|
+
connection = null;
|
|
8
|
+
consumerTags = [];
|
|
9
|
+
constructor(contract, handlers) {
|
|
10
|
+
this.contract = contract;
|
|
11
|
+
this.handlers = handlers;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Connect to AMQP broker
|
|
15
|
+
*/
|
|
16
|
+
async connect(connection) {
|
|
17
|
+
this.connection = connection;
|
|
18
|
+
this.channel = await connection.createChannel();
|
|
19
|
+
if (this.contract.exchanges && this.channel) for (const exchange of Object.values(this.contract.exchanges)) await this.channel.assertExchange(exchange.name, exchange.type, {
|
|
20
|
+
durable: exchange.durable,
|
|
21
|
+
autoDelete: exchange.autoDelete,
|
|
22
|
+
internal: exchange.internal,
|
|
23
|
+
arguments: exchange.arguments
|
|
24
|
+
});
|
|
25
|
+
if (this.contract.queues && this.channel) for (const queue of Object.values(this.contract.queues)) await this.channel.assertQueue(queue.name, {
|
|
26
|
+
durable: queue.durable,
|
|
27
|
+
exclusive: queue.exclusive,
|
|
28
|
+
autoDelete: queue.autoDelete,
|
|
29
|
+
arguments: queue.arguments
|
|
30
|
+
});
|
|
31
|
+
if (this.contract.bindings && this.channel) for (const binding of Object.values(this.contract.bindings)) await this.channel.bindQueue(binding.queue, binding.exchange, binding.routingKey ?? "", binding.arguments);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Start consuming messages for a specific consumer
|
|
35
|
+
*/
|
|
36
|
+
async consume(consumerName) {
|
|
37
|
+
if (!this.channel) throw new Error("Worker not connected. Call connect() first.");
|
|
38
|
+
const consumers = this.contract.consumers;
|
|
39
|
+
if (!consumers) throw new Error("No consumers defined in contract");
|
|
40
|
+
const consumer = consumers[consumerName];
|
|
41
|
+
if (!consumer || typeof consumer !== "object") throw new Error(`Consumer "${String(consumerName)}" not found in contract`);
|
|
42
|
+
const consumerDef = consumer;
|
|
43
|
+
const handler = this.handlers[consumerName];
|
|
44
|
+
if (!handler) throw new Error(`Handler for "${String(consumerName)}" not provided`);
|
|
45
|
+
if (consumerDef.prefetch !== void 0) await this.channel.prefetch(consumerDef.prefetch);
|
|
46
|
+
const result = await this.channel.consume(consumerDef.queue, async (msg) => {
|
|
47
|
+
if (!msg) return;
|
|
48
|
+
try {
|
|
49
|
+
const content = JSON.parse(msg.content.toString());
|
|
50
|
+
const validation = consumerDef.message["~standard"].validate(content);
|
|
51
|
+
if (typeof validation === "object" && validation !== null && "issues" in validation && validation.issues) {
|
|
52
|
+
console.error("Message validation failed:", validation.issues);
|
|
53
|
+
this.channel?.nack(msg, false, false);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
await handler(typeof validation === "object" && validation !== null && "value" in validation ? validation.value : content);
|
|
57
|
+
if (!consumerDef.noAck) this.channel?.ack(msg);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error("Error processing message:", error);
|
|
60
|
+
this.channel?.nack(msg, false, true);
|
|
61
|
+
}
|
|
62
|
+
}, { noAck: consumerDef.noAck ?? false });
|
|
63
|
+
this.consumerTags.push(result.consumerTag);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Start consuming messages for all consumers
|
|
67
|
+
*/
|
|
68
|
+
async consumeAll() {
|
|
69
|
+
if (!this.contract.consumers) throw new Error("No consumers defined in contract");
|
|
70
|
+
const consumerNames = Object.keys(this.contract.consumers);
|
|
71
|
+
for (const consumerName of consumerNames) await this.consume(consumerName);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Stop consuming messages
|
|
75
|
+
*/
|
|
76
|
+
async stopConsuming() {
|
|
77
|
+
if (!this.channel) return;
|
|
78
|
+
for (const tag of this.consumerTags) await this.channel.cancel(tag);
|
|
79
|
+
this.consumerTags = [];
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Close the connection
|
|
83
|
+
*/
|
|
84
|
+
async close() {
|
|
85
|
+
await this.stopConsuming();
|
|
86
|
+
if (this.channel) {
|
|
87
|
+
await this.channel.close();
|
|
88
|
+
this.channel = null;
|
|
89
|
+
}
|
|
90
|
+
if (this.connection) {
|
|
91
|
+
await this.connection.close();
|
|
92
|
+
this.connection = null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Create a type-safe AMQP worker from a contract
|
|
98
|
+
*/
|
|
99
|
+
function createWorker(contract, handlers) {
|
|
100
|
+
return new AmqpWorker(contract, handlers);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
//#endregion
|
|
104
|
+
export { AmqpWorker, createWorker };
|
|
105
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["contract: TContract","handlers: WorkerInferConsumerHandlers<TContract>"],"sources":["../src/worker.ts"],"sourcesContent":["import type { Channel, Connection as AmqpConnection, ConsumeMessage } from 'amqplib';\nimport type {\n ContractDefinition,\n InferConsumerNames,\n WorkerInferConsumerHandlers,\n} from '@amqp-contract/contract';\n\n/**\n * Type-safe AMQP worker for consuming messages\n */\nexport class AmqpWorker<TContract extends ContractDefinition> {\n private channel: Channel | null = null;\n private connection: AmqpConnection | null = null;\n private consumerTags: string[] = [];\n\n constructor(\n private readonly contract: TContract,\n private readonly handlers: WorkerInferConsumerHandlers<TContract>\n ) {}\n\n /**\n * Connect to AMQP broker\n */\n async connect(connection: AmqpConnection): Promise<void> {\n this.connection = connection;\n this.channel = await (\n connection as unknown as { createChannel(): Promise<Channel> }\n ).createChannel();\n\n // Setup exchanges\n if (this.contract.exchanges && this.channel) {\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 && this.channel) {\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 && this.channel) {\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 a specific consumer\n */\n async consume<TName extends InferConsumerNames<TContract>>(consumerName: TName): Promise<void> {\n if (!this.channel) {\n throw new Error('Worker not connected. Call connect() first.');\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 * Start consuming messages for all consumers\n */\n 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 * Stop consuming messages\n */\n 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 /**\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 as unknown as { close(): Promise<void> }).close();\n this.connection = null;\n }\n }\n}\n\n/**\n * Create a type-safe AMQP worker from a contract\n */\nexport function createWorker<TContract extends ContractDefinition>(\n contract: TContract,\n handlers: WorkerInferConsumerHandlers<TContract>\n): AmqpWorker<TContract> {\n return new AmqpWorker(contract, handlers);\n}\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@amqp-contract/worker",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Worker utilities for consuming messages using amqp-contract",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"amqp",
|
|
7
|
+
"typescript",
|
|
8
|
+
"contract",
|
|
9
|
+
"worker",
|
|
10
|
+
"rabbitmq"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://github.com/btravers/amqp-contract#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/btravers/amqp-contract/issues"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/btravers/amqp-contract.git",
|
|
19
|
+
"directory": "packages/worker"
|
|
20
|
+
},
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"author": "Benoit TRAVERS <benoit.travers.fr@gmail.com>",
|
|
23
|
+
"type": "module",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"import": {
|
|
27
|
+
"types": "./dist/index.d.mts",
|
|
28
|
+
"default": "./dist/index.mjs"
|
|
29
|
+
},
|
|
30
|
+
"require": {
|
|
31
|
+
"types": "./dist/index.d.cts",
|
|
32
|
+
"default": "./dist/index.cjs"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"./package.json": "./package.json"
|
|
36
|
+
},
|
|
37
|
+
"main": "./dist/index.cjs",
|
|
38
|
+
"module": "./dist/index.mjs",
|
|
39
|
+
"types": "./dist/index.d.mts",
|
|
40
|
+
"files": [
|
|
41
|
+
"dist"
|
|
42
|
+
],
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@amqp-contract/contract": "0.0.1"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/amqplib": "0.10.8",
|
|
48
|
+
"@types/node": "24.10.2",
|
|
49
|
+
"@vitest/coverage-v8": "4.0.15",
|
|
50
|
+
"amqplib": "0.10.9",
|
|
51
|
+
"tsdown": "0.17.2",
|
|
52
|
+
"typescript": "5.9.3",
|
|
53
|
+
"vitest": "4.0.15",
|
|
54
|
+
"@amqp-contract/tsconfig": "0.0.0"
|
|
55
|
+
},
|
|
56
|
+
"peerDependencies": {
|
|
57
|
+
"amqplib": ">=0.10.0"
|
|
58
|
+
},
|
|
59
|
+
"scripts": {
|
|
60
|
+
"build": "tsdown src/index.ts --format cjs,esm --dts --clean",
|
|
61
|
+
"dev": "tsdown src/index.ts --format cjs,esm --dts --watch",
|
|
62
|
+
"test": "vitest run",
|
|
63
|
+
"test:watch": "vitest",
|
|
64
|
+
"typecheck": "tsc --noEmit"
|
|
65
|
+
}
|
|
66
|
+
}
|