@warlock.js/herald 4.0.100
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 +364 -0
- package/cjs/communicators/communicator-registry.d.ts +155 -0
- package/cjs/communicators/communicator-registry.d.ts.map +1 -0
- package/cjs/communicators/communicator-registry.js +206 -0
- package/cjs/communicators/communicator-registry.js.map +1 -0
- package/cjs/communicators/communicator.d.ts +90 -0
- package/cjs/communicators/communicator.d.ts.map +1 -0
- package/cjs/communicators/communicator.js +93 -0
- package/cjs/communicators/communicator.js.map +1 -0
- package/cjs/communicators/index.d.ts +3 -0
- package/cjs/communicators/index.d.ts.map +1 -0
- package/cjs/contracts/channel.contract.d.ts +175 -0
- package/cjs/contracts/channel.contract.d.ts.map +1 -0
- package/cjs/contracts/communicator-driver.contract.d.ts +168 -0
- package/cjs/contracts/communicator-driver.contract.d.ts.map +1 -0
- package/cjs/contracts/index.d.ts +3 -0
- package/cjs/contracts/index.d.ts.map +1 -0
- package/cjs/drivers/index.d.ts +2 -0
- package/cjs/drivers/index.d.ts.map +1 -0
- package/cjs/drivers/rabbitmq/index.d.ts +3 -0
- package/cjs/drivers/rabbitmq/index.d.ts.map +1 -0
- package/cjs/drivers/rabbitmq/rabbitmq-channel.d.ts +70 -0
- package/cjs/drivers/rabbitmq/rabbitmq-channel.d.ts.map +1 -0
- package/cjs/drivers/rabbitmq/rabbitmq-channel.js +400 -0
- package/cjs/drivers/rabbitmq/rabbitmq-channel.js.map +1 -0
- package/cjs/drivers/rabbitmq/rabbitmq-driver.d.ts +100 -0
- package/cjs/drivers/rabbitmq/rabbitmq-driver.d.ts.map +1 -0
- package/cjs/drivers/rabbitmq/rabbitmq-driver.js +299 -0
- package/cjs/drivers/rabbitmq/rabbitmq-driver.js.map +1 -0
- package/cjs/index.d.ts +45 -0
- package/cjs/index.d.ts.map +1 -0
- package/cjs/index.js +1 -0
- package/cjs/index.js.map +1 -0
- package/cjs/types.d.ts +396 -0
- package/cjs/types.d.ts.map +1 -0
- package/cjs/utils/connect-to-communicator.d.ts +86 -0
- package/cjs/utils/connect-to-communicator.d.ts.map +1 -0
- package/cjs/utils/connect-to-communicator.js +122 -0
- package/cjs/utils/connect-to-communicator.js.map +1 -0
- package/cjs/utils/index.d.ts +2 -0
- package/cjs/utils/index.d.ts.map +1 -0
- package/esm/communicators/communicator-registry.d.ts +155 -0
- package/esm/communicators/communicator-registry.d.ts.map +1 -0
- package/esm/communicators/communicator-registry.js +206 -0
- package/esm/communicators/communicator-registry.js.map +1 -0
- package/esm/communicators/communicator.d.ts +90 -0
- package/esm/communicators/communicator.d.ts.map +1 -0
- package/esm/communicators/communicator.js +93 -0
- package/esm/communicators/communicator.js.map +1 -0
- package/esm/communicators/index.d.ts +3 -0
- package/esm/communicators/index.d.ts.map +1 -0
- package/esm/contracts/channel.contract.d.ts +175 -0
- package/esm/contracts/channel.contract.d.ts.map +1 -0
- package/esm/contracts/communicator-driver.contract.d.ts +168 -0
- package/esm/contracts/communicator-driver.contract.d.ts.map +1 -0
- package/esm/contracts/index.d.ts +3 -0
- package/esm/contracts/index.d.ts.map +1 -0
- package/esm/drivers/index.d.ts +2 -0
- package/esm/drivers/index.d.ts.map +1 -0
- package/esm/drivers/rabbitmq/index.d.ts +3 -0
- package/esm/drivers/rabbitmq/index.d.ts.map +1 -0
- package/esm/drivers/rabbitmq/rabbitmq-channel.d.ts +70 -0
- package/esm/drivers/rabbitmq/rabbitmq-channel.d.ts.map +1 -0
- package/esm/drivers/rabbitmq/rabbitmq-channel.js +400 -0
- package/esm/drivers/rabbitmq/rabbitmq-channel.js.map +1 -0
- package/esm/drivers/rabbitmq/rabbitmq-driver.d.ts +100 -0
- package/esm/drivers/rabbitmq/rabbitmq-driver.d.ts.map +1 -0
- package/esm/drivers/rabbitmq/rabbitmq-driver.js +299 -0
- package/esm/drivers/rabbitmq/rabbitmq-driver.js.map +1 -0
- package/esm/index.d.ts +45 -0
- package/esm/index.d.ts.map +1 -0
- package/esm/index.js +1 -0
- package/esm/index.js.map +1 -0
- package/esm/types.d.ts +396 -0
- package/esm/types.d.ts.map +1 -0
- package/esm/utils/connect-to-communicator.d.ts +86 -0
- package/esm/utils/connect-to-communicator.d.ts.map +1 -0
- package/esm/utils/connect-to-communicator.js +122 -0
- package/esm/utils/connect-to-communicator.js.map +1 -0
- package/esm/utils/index.d.ts +2 -0
- package/esm/utils/index.d.ts.map +1 -0
- package/package.json +47 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import type { ChannelContract } from "./channel.contract";
|
|
2
|
+
import type { ChannelOptions, CommunicatorEvent, CommunicatorEventListener, CommunicatorDriverType, HealthCheckResult } from "../types";
|
|
3
|
+
/**
|
|
4
|
+
* Communicator Driver Contract
|
|
5
|
+
*
|
|
6
|
+
* Base contract for all message bus drivers (RabbitMQ, Kafka, etc.)
|
|
7
|
+
* Similar to DriverContract in @warlock.js/cascade
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* // Driver implementation
|
|
12
|
+
* class RabbitMQDriver implements CommunicatorDriverContract {
|
|
13
|
+
* readonly name = "rabbitmq";
|
|
14
|
+
* // ...
|
|
15
|
+
* }
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export interface CommunicatorDriverContract {
|
|
19
|
+
/**
|
|
20
|
+
* Driver name identifier
|
|
21
|
+
*
|
|
22
|
+
* @example "rabbitmq", "kafka", "redis-streams"
|
|
23
|
+
*/
|
|
24
|
+
readonly name: CommunicatorDriverType;
|
|
25
|
+
/**
|
|
26
|
+
* Whether currently connected to the message broker
|
|
27
|
+
*/
|
|
28
|
+
readonly isConnected: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Establish connection to the message broker
|
|
31
|
+
*
|
|
32
|
+
* @throws Error if connection fails
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```typescript
|
|
36
|
+
* await driver.connect();
|
|
37
|
+
* console.log("Connected to RabbitMQ");
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
connect(): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Close connection gracefully
|
|
43
|
+
*
|
|
44
|
+
* Ensures all pending operations complete before disconnecting.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```typescript
|
|
48
|
+
* await driver.disconnect();
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
disconnect(): Promise<void>;
|
|
52
|
+
/**
|
|
53
|
+
* Register event listeners for driver lifecycle events
|
|
54
|
+
*
|
|
55
|
+
* @param event - Event name to listen for
|
|
56
|
+
* @param listener - Callback function
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```typescript
|
|
60
|
+
* driver.on("connected", () => {
|
|
61
|
+
* console.log("Connected to broker");
|
|
62
|
+
* });
|
|
63
|
+
*
|
|
64
|
+
* driver.on("disconnected", () => {
|
|
65
|
+
* console.log("Disconnected from broker");
|
|
66
|
+
* });
|
|
67
|
+
*
|
|
68
|
+
* driver.on("error", (error) => {
|
|
69
|
+
* console.error("Driver error:", error);
|
|
70
|
+
* });
|
|
71
|
+
*
|
|
72
|
+
* driver.on("reconnecting", (attempt) => {
|
|
73
|
+
* console.log(`Reconnection attempt ${attempt}`);
|
|
74
|
+
* });
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
on(event: CommunicatorEvent, listener: CommunicatorEventListener): void;
|
|
78
|
+
/**
|
|
79
|
+
* Remove an event listener
|
|
80
|
+
*
|
|
81
|
+
* @param event - Event name
|
|
82
|
+
* @param listener - Callback to remove
|
|
83
|
+
*/
|
|
84
|
+
off(event: CommunicatorEvent, listener: CommunicatorEventListener): void;
|
|
85
|
+
/**
|
|
86
|
+
* Get or create a channel
|
|
87
|
+
*
|
|
88
|
+
* Channels are lazy-created and cached for reuse.
|
|
89
|
+
*
|
|
90
|
+
* @param name - Channel/queue/topic name
|
|
91
|
+
* @param options - Channel configuration
|
|
92
|
+
* @returns Channel instance
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```typescript
|
|
96
|
+
* // Simple channel
|
|
97
|
+
* const channel = driver.channel("user.created");
|
|
98
|
+
*
|
|
99
|
+
* // With options
|
|
100
|
+
* const orderChannel = driver.channel("orders", {
|
|
101
|
+
* durable: true,
|
|
102
|
+
* deadLetter: { channel: "orders.failed" },
|
|
103
|
+
* });
|
|
104
|
+
*
|
|
105
|
+
* // Typed channel
|
|
106
|
+
* const typedChannel = driver.channel<OrderPayload>("orders", {
|
|
107
|
+
* schema: OrderSchema,
|
|
108
|
+
* });
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
channel<TPayload = unknown>(name: string, options?: ChannelOptions<TPayload>): ChannelContract<TPayload>;
|
|
112
|
+
/**
|
|
113
|
+
* Start consuming messages from all subscribed channels
|
|
114
|
+
*
|
|
115
|
+
* Call this after setting up all subscriptions to begin processing.
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* ```typescript
|
|
119
|
+
* // Set up subscriptions
|
|
120
|
+
* channel1.subscribe(handler1);
|
|
121
|
+
* channel2.subscribe(handler2);
|
|
122
|
+
*
|
|
123
|
+
* // Start consuming
|
|
124
|
+
* await driver.startConsuming();
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
127
|
+
startConsuming(): Promise<void>;
|
|
128
|
+
/**
|
|
129
|
+
* Stop consuming messages gracefully
|
|
130
|
+
*
|
|
131
|
+
* Waits for currently processing messages to complete.
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* ```typescript
|
|
135
|
+
* await driver.stopConsuming();
|
|
136
|
+
* ```
|
|
137
|
+
*/
|
|
138
|
+
stopConsuming(): Promise<void>;
|
|
139
|
+
/**
|
|
140
|
+
* Perform a health check on the connection
|
|
141
|
+
*
|
|
142
|
+
* @returns Health check result with status and optional latency
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* ```typescript
|
|
146
|
+
* const health = await driver.healthCheck();
|
|
147
|
+
* if (health.healthy) {
|
|
148
|
+
* console.log(`Healthy, latency: ${health.latency}ms`);
|
|
149
|
+
* } else {
|
|
150
|
+
* console.error(`Unhealthy: ${health.error}`);
|
|
151
|
+
* }
|
|
152
|
+
* ```
|
|
153
|
+
*/
|
|
154
|
+
healthCheck(): Promise<HealthCheckResult>;
|
|
155
|
+
/**
|
|
156
|
+
* Get list of all channels managed by this driver
|
|
157
|
+
*
|
|
158
|
+
* @returns Array of channel names
|
|
159
|
+
*/
|
|
160
|
+
getChannelNames(): string[];
|
|
161
|
+
/**
|
|
162
|
+
* Close and remove a specific channel
|
|
163
|
+
*
|
|
164
|
+
* @param name - Channel name to close
|
|
165
|
+
*/
|
|
166
|
+
closeChannel(name: string): Promise<void>;
|
|
167
|
+
}
|
|
168
|
+
//# sourceMappingURL=communicator-driver.contract.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"communicator-driver.contract.d.ts","sourceRoot":"","sources":["../../src/contracts/communicator-driver.contract.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,KAAK,EACV,cAAc,EACd,iBAAiB,EACjB,yBAAyB,EACzB,sBAAsB,EACtB,iBAAiB,EAClB,MAAM,UAAU,CAAC;AAElB;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,0BAA0B;IACzC;;;;OAIG;IACH,QAAQ,CAAC,IAAI,EAAE,sBAAsB,CAAC;IAEtC;;OAEG;IACH,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAE9B;;;;;;;;;;OAUG;IACH,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAEzB;;;;;;;;;OASG;IACH,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAE5B;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IACH,EAAE,CAAC,KAAK,EAAE,iBAAiB,EAAE,QAAQ,EAAE,yBAAyB,GAAG,IAAI,CAAC;IAExE;;;;;OAKG;IACH,GAAG,CAAC,KAAK,EAAE,iBAAiB,EAAE,QAAQ,EAAE,yBAAyB,GAAG,IAAI,CAAC;IAEzE;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;IACH,OAAO,CAAC,QAAQ,GAAG,OAAO,EACxB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,cAAc,CAAC,QAAQ,CAAC,GACjC,eAAe,CAAC,QAAQ,CAAC,CAAC;IAE7B;;;;;;;;;;;;;;OAcG;IACH,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAEhC;;;;;;;;;OASG;IACH,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAE/B;;;;;;;;;;;;;;OAcG;IACH,WAAW,IAAI,OAAO,CAAC,iBAAiB,CAAC,CAAC;IAE1C;;;;OAIG;IACH,eAAe,IAAI,MAAM,EAAE,CAAC;IAE5B;;;;OAIG;IACH,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3C"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/contracts/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC;AACnC,cAAc,gCAAgC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/drivers/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/drivers/rabbitmq/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { ChannelContract } from "../../contracts";
|
|
2
|
+
import type { ChannelOptions, ChannelStats, MessageHandler, PublishOptions, RequestOptions, ResponseHandler, SubscribeOptions, Subscription } from "../../types";
|
|
3
|
+
/**
|
|
4
|
+
* RabbitMQ Channel Implementation
|
|
5
|
+
*
|
|
6
|
+
* Wraps a RabbitMQ queue/exchange with a unified API.
|
|
7
|
+
*
|
|
8
|
+
* @template TPayload - The typed payload
|
|
9
|
+
*/
|
|
10
|
+
export declare class RabbitMQChannel<TPayload = unknown> implements ChannelContract<TPayload> {
|
|
11
|
+
readonly name: string;
|
|
12
|
+
readonly options: ChannelOptions<TPayload>;
|
|
13
|
+
private readonly amqpChannel;
|
|
14
|
+
private readonly subscriptions;
|
|
15
|
+
private asserted;
|
|
16
|
+
/**
|
|
17
|
+
* Create a new RabbitMQ channel
|
|
18
|
+
*/
|
|
19
|
+
constructor(name: string, amqpChannel: any, options?: ChannelOptions<TPayload>);
|
|
20
|
+
/**
|
|
21
|
+
* Assert the queue exists
|
|
22
|
+
*/
|
|
23
|
+
assert(): Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Publish a message
|
|
26
|
+
*/
|
|
27
|
+
publish(payload: TPayload, options?: PublishOptions): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Publish multiple messages
|
|
30
|
+
*/
|
|
31
|
+
publishBatch(messages: TPayload[], options?: PublishOptions): Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* Subscribe to messages
|
|
34
|
+
*
|
|
35
|
+
* Smart auto-ack behavior (when autoAck is not true):
|
|
36
|
+
* - If handler completes successfully without explicit ack/nack/reject → auto-ack
|
|
37
|
+
* - If handler throws an error → auto-nack (with retry if configured)
|
|
38
|
+
* - If handler explicitly calls ack/nack/reject → respects that call
|
|
39
|
+
*/
|
|
40
|
+
subscribe(handler: MessageHandler<TPayload>, options?: SubscribeOptions): Promise<Subscription>;
|
|
41
|
+
/**
|
|
42
|
+
* Send message to dead-letter queue
|
|
43
|
+
*/
|
|
44
|
+
private sendToDeadLetter;
|
|
45
|
+
/**
|
|
46
|
+
* Request-response pattern
|
|
47
|
+
*/
|
|
48
|
+
request<TResponse = unknown>(payload: TPayload, options?: RequestOptions): Promise<TResponse>;
|
|
49
|
+
/**
|
|
50
|
+
* Register response handler for RPC
|
|
51
|
+
*/
|
|
52
|
+
respond<TResponse = unknown>(handler: ResponseHandler<TPayload, TResponse>): Promise<Subscription>;
|
|
53
|
+
/**
|
|
54
|
+
* Get queue statistics
|
|
55
|
+
*/
|
|
56
|
+
stats(): Promise<ChannelStats>;
|
|
57
|
+
/**
|
|
58
|
+
* Purge all messages
|
|
59
|
+
*/
|
|
60
|
+
purge(): Promise<number>;
|
|
61
|
+
/**
|
|
62
|
+
* Check if queue exists
|
|
63
|
+
*/
|
|
64
|
+
exists(): Promise<boolean>;
|
|
65
|
+
/**
|
|
66
|
+
* Delete the queue
|
|
67
|
+
*/
|
|
68
|
+
delete(): Promise<void>;
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=rabbitmq-channel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rabbitmq-channel.d.ts","sourceRoot":"","sources":["../../../src/drivers/rabbitmq/rabbitmq-channel.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACvD,OAAO,KAAK,EACV,cAAc,EACd,YAAY,EAGZ,cAAc,EAEd,cAAc,EACd,cAAc,EACd,eAAe,EACf,gBAAgB,EAChB,YAAY,EACb,MAAM,aAAa,CAAC;AAErB;;;;;;GAMG;AACH,qBAAa,eAAe,CAAC,QAAQ,GAAG,OAAO,CAAE,YAAW,eAAe,CAAC,QAAQ,CAAC;IACnF,SAAgB,IAAI,EAAE,MAAM,CAAC;IAC7B,SAAgB,OAAO,EAAE,cAAc,CAAC,QAAQ,CAAC,CAAC;IAElD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAM;IAClC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA2C;IACzE,OAAO,CAAC,QAAQ,CAAS;IAEzB;;OAEG;gBACgB,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,GAAG,EAAE,OAAO,CAAC,EAAE,cAAc,CAAC,QAAQ,CAAC;IAMrF;;OAEG;IACU,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAiBpC;;OAEG;IACU,OAAO,CAAC,OAAO,EAAE,QAAQ,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAkDhF;;OAEG;IACU,YAAY,CAAC,QAAQ,EAAE,QAAQ,EAAE,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAMxF;;;;;;;OAOG;IACU,SAAS,CACpB,OAAO,EAAE,cAAc,CAAC,QAAQ,CAAC,EACjC,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC,YAAY,CAAC;IAmLxB;;OAEG;YACW,gBAAgB;IAe9B;;OAEG;IACU,OAAO,CAAC,SAAS,GAAG,OAAO,EACtC,OAAO,EAAE,QAAQ,EACjB,OAAO,CAAC,EAAE,cAAc,GACvB,OAAO,CAAC,SAAS,CAAC;IAiDrB;;OAEG;IACU,OAAO,CAAC,SAAS,GAAG,OAAO,EACtC,OAAO,EAAE,eAAe,CAAC,QAAQ,EAAE,SAAS,CAAC,GAC5C,OAAO,CAAC,YAAY,CAAC;IAQxB;;OAEG;IACU,KAAK,IAAI,OAAO,CAAC,YAAY,CAAC;IAY3C;;OAEG;IACU,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;IAOrC;;OAEG;IACU,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC;IASvC;;OAEG;IACU,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;CAerC"}
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
'use strict';var seal=require('@warlock.js/seal'),node_crypto=require('node:crypto');/**
|
|
2
|
+
* RabbitMQ Channel Implementation
|
|
3
|
+
*
|
|
4
|
+
* Wraps a RabbitMQ queue/exchange with a unified API.
|
|
5
|
+
*
|
|
6
|
+
* @template TPayload - The typed payload
|
|
7
|
+
*/
|
|
8
|
+
class RabbitMQChannel {
|
|
9
|
+
name;
|
|
10
|
+
options;
|
|
11
|
+
amqpChannel;
|
|
12
|
+
subscriptions = new Map();
|
|
13
|
+
asserted = false;
|
|
14
|
+
/**
|
|
15
|
+
* Create a new RabbitMQ channel
|
|
16
|
+
*/
|
|
17
|
+
constructor(name, amqpChannel, options) {
|
|
18
|
+
this.name = name;
|
|
19
|
+
this.amqpChannel = amqpChannel;
|
|
20
|
+
this.options = options ?? {};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Assert the queue exists
|
|
24
|
+
*/
|
|
25
|
+
async assert() {
|
|
26
|
+
if (this.asserted)
|
|
27
|
+
return;
|
|
28
|
+
const queueOptions = {
|
|
29
|
+
durable: this.options.durable ?? true,
|
|
30
|
+
autoDelete: this.options.autoDelete ?? false,
|
|
31
|
+
exclusive: this.options.exclusive ?? false,
|
|
32
|
+
messageTtl: this.options.messageTtl,
|
|
33
|
+
maxLength: this.options.maxLength,
|
|
34
|
+
deadLetterExchange: this.options.deadLetter?.channel ? "" : undefined,
|
|
35
|
+
deadLetterRoutingKey: this.options.deadLetter?.channel,
|
|
36
|
+
};
|
|
37
|
+
await this.amqpChannel.assertQueue(this.name, queueOptions);
|
|
38
|
+
this.asserted = true;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Publish a message
|
|
42
|
+
*/
|
|
43
|
+
async publish(payload, options) {
|
|
44
|
+
await this.assert();
|
|
45
|
+
// Validate with schema if provided
|
|
46
|
+
if (this.options.schema) {
|
|
47
|
+
const context = {
|
|
48
|
+
allValues: payload,
|
|
49
|
+
value: payload,
|
|
50
|
+
};
|
|
51
|
+
const result = await seal.v.validate(this.options.schema, payload, { context });
|
|
52
|
+
if (!result.isValid) {
|
|
53
|
+
throw new Error(`Message validation failed: ${JSON.stringify(result.errors)}`);
|
|
54
|
+
}
|
|
55
|
+
payload = result.data;
|
|
56
|
+
}
|
|
57
|
+
const messageId = node_crypto.randomUUID();
|
|
58
|
+
const timestamp = new Date();
|
|
59
|
+
const messageContent = JSON.stringify({
|
|
60
|
+
payload,
|
|
61
|
+
metadata: {
|
|
62
|
+
messageId,
|
|
63
|
+
timestamp: timestamp.toISOString(),
|
|
64
|
+
correlationId: options?.correlationId,
|
|
65
|
+
headers: options?.headers,
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
const publishOptions = {
|
|
69
|
+
persistent: options?.persistent ?? true,
|
|
70
|
+
messageId,
|
|
71
|
+
timestamp: timestamp.getTime(),
|
|
72
|
+
correlationId: options?.correlationId,
|
|
73
|
+
expiration: options?.expiration?.toString(),
|
|
74
|
+
priority: options?.priority,
|
|
75
|
+
headers: options?.headers,
|
|
76
|
+
};
|
|
77
|
+
// Handle delayed messages (requires rabbitmq-delayed-message-exchange plugin)
|
|
78
|
+
if (options?.delay) {
|
|
79
|
+
publishOptions.headers = {
|
|
80
|
+
...publishOptions.headers,
|
|
81
|
+
"x-delay": options.delay,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
this.amqpChannel.sendToQueue(this.name, Buffer.from(messageContent), publishOptions);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Publish multiple messages
|
|
88
|
+
*/
|
|
89
|
+
async publishBatch(messages, options) {
|
|
90
|
+
for (const payload of messages) {
|
|
91
|
+
await this.publish(payload, options);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Subscribe to messages
|
|
96
|
+
*
|
|
97
|
+
* Smart auto-ack behavior (when autoAck is not true):
|
|
98
|
+
* - If handler completes successfully without explicit ack/nack/reject → auto-ack
|
|
99
|
+
* - If handler throws an error → auto-nack (with retry if configured)
|
|
100
|
+
* - If handler explicitly calls ack/nack/reject → respects that call
|
|
101
|
+
*/
|
|
102
|
+
async subscribe(handler, options) {
|
|
103
|
+
await this.assert();
|
|
104
|
+
const subscriptionId = node_crypto.randomUUID();
|
|
105
|
+
// Set prefetch if specified
|
|
106
|
+
if (options?.prefetch) {
|
|
107
|
+
await this.amqpChannel.prefetch(options.prefetch);
|
|
108
|
+
}
|
|
109
|
+
// If autoAck is true, RabbitMQ handles ack immediately (fire-and-forget)
|
|
110
|
+
const isFireAndForget = options?.autoAck === true;
|
|
111
|
+
const consumerOptions = {
|
|
112
|
+
noAck: isFireAndForget,
|
|
113
|
+
exclusive: options?.exclusive ?? false,
|
|
114
|
+
consumerTag: options?.group ?? subscriptionId,
|
|
115
|
+
};
|
|
116
|
+
const { consumerTag } = await this.amqpChannel.consume(this.name, async (msg) => {
|
|
117
|
+
if (!msg)
|
|
118
|
+
return;
|
|
119
|
+
// Track if acknowledgment was handled explicitly
|
|
120
|
+
let ackHandled = isFireAndForget;
|
|
121
|
+
try {
|
|
122
|
+
const content = JSON.parse(msg.content.toString());
|
|
123
|
+
let payload = content.payload;
|
|
124
|
+
// Validate with schema if provided
|
|
125
|
+
if (this.options.schema) {
|
|
126
|
+
const schemaContext = {
|
|
127
|
+
allValues: payload,
|
|
128
|
+
parent: null,
|
|
129
|
+
value: payload,
|
|
130
|
+
key: "",
|
|
131
|
+
path: "",
|
|
132
|
+
translateRule: (t) => t.rule?.defaultErrorMessage ?? "Validation failed",
|
|
133
|
+
translateAttribute: (t) => t.attribute ?? "",
|
|
134
|
+
};
|
|
135
|
+
const result = await this.options.schema.validate(payload, schemaContext);
|
|
136
|
+
if (!result.isValid) {
|
|
137
|
+
// Reject invalid messages
|
|
138
|
+
this.amqpChannel.nack(msg, false, false);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
payload = result.data;
|
|
142
|
+
}
|
|
143
|
+
const metadata = {
|
|
144
|
+
messageId: msg.properties.messageId || content.metadata?.messageId || node_crypto.randomUUID(),
|
|
145
|
+
timestamp: new Date(msg.properties.timestamp || content.metadata?.timestamp),
|
|
146
|
+
correlationId: msg.properties.correlationId || content.metadata?.correlationId,
|
|
147
|
+
replyTo: msg.properties.replyTo,
|
|
148
|
+
priority: msg.properties.priority,
|
|
149
|
+
headers: msg.properties.headers,
|
|
150
|
+
retryCount: msg.properties.headers?.["x-retry-count"] || 0,
|
|
151
|
+
originalChannel: this.name,
|
|
152
|
+
};
|
|
153
|
+
const message = {
|
|
154
|
+
metadata,
|
|
155
|
+
payload,
|
|
156
|
+
raw: msg,
|
|
157
|
+
};
|
|
158
|
+
const context = {
|
|
159
|
+
ack: async () => {
|
|
160
|
+
if (!ackHandled) {
|
|
161
|
+
ackHandled = true;
|
|
162
|
+
this.amqpChannel.ack(msg);
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
nack: async (requeue = true) => {
|
|
166
|
+
if (!ackHandled) {
|
|
167
|
+
ackHandled = true;
|
|
168
|
+
this.amqpChannel.nack(msg, false, requeue);
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
reject: async () => {
|
|
172
|
+
if (!ackHandled) {
|
|
173
|
+
ackHandled = true;
|
|
174
|
+
this.amqpChannel.reject(msg, false);
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
reply: async (replyPayload) => {
|
|
178
|
+
if (msg.properties.replyTo) {
|
|
179
|
+
const replyContent = JSON.stringify({
|
|
180
|
+
payload: replyPayload,
|
|
181
|
+
metadata: {
|
|
182
|
+
messageId: node_crypto.randomUUID(),
|
|
183
|
+
timestamp: new Date().toISOString(),
|
|
184
|
+
correlationId: msg.properties.correlationId,
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
this.amqpChannel.sendToQueue(msg.properties.replyTo, Buffer.from(replyContent), {
|
|
188
|
+
correlationId: msg.properties.correlationId,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
retry: async (delay) => {
|
|
193
|
+
if (ackHandled)
|
|
194
|
+
return;
|
|
195
|
+
ackHandled = true;
|
|
196
|
+
const retryCount = (metadata.retryCount || 0) + 1;
|
|
197
|
+
const maxRetries = options?.retry?.maxRetries ?? 3;
|
|
198
|
+
if (retryCount > maxRetries) {
|
|
199
|
+
// Send to dead-letter if configured
|
|
200
|
+
if (options?.deadLetter) {
|
|
201
|
+
await this.sendToDeadLetter(message, options.deadLetter.channel);
|
|
202
|
+
}
|
|
203
|
+
this.amqpChannel.ack(msg);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
// Republish with retry count
|
|
207
|
+
const headers = {
|
|
208
|
+
...msg.properties.headers,
|
|
209
|
+
"x-retry-count": retryCount,
|
|
210
|
+
};
|
|
211
|
+
if (delay) {
|
|
212
|
+
headers["x-delay"] = delay;
|
|
213
|
+
}
|
|
214
|
+
this.amqpChannel.sendToQueue(this.name, msg.content, { ...msg.properties, headers });
|
|
215
|
+
this.amqpChannel.ack(msg);
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
// Execute handler
|
|
219
|
+
await handler(message, context);
|
|
220
|
+
// Smart auto-ack: if handler succeeded and didn't explicitly handle ack
|
|
221
|
+
if (!ackHandled) {
|
|
222
|
+
this.amqpChannel.ack(msg);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
// Smart auto-nack: if handler threw and didn't explicitly handle ack
|
|
227
|
+
if (ackHandled)
|
|
228
|
+
return;
|
|
229
|
+
// Handle errors - nack and potentially retry
|
|
230
|
+
if (options?.retry) {
|
|
231
|
+
const retryCount = msg.properties.headers?.["x-retry-count"] || 0;
|
|
232
|
+
if (retryCount < options.retry.maxRetries) {
|
|
233
|
+
// Requeue for retry
|
|
234
|
+
this.amqpChannel.nack(msg, false, true);
|
|
235
|
+
}
|
|
236
|
+
else if (options.deadLetter) {
|
|
237
|
+
// Send to dead-letter
|
|
238
|
+
this.amqpChannel.nack(msg, false, false);
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
this.amqpChannel.reject(msg, false);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
// No retry configured - reject without requeue
|
|
246
|
+
this.amqpChannel.nack(msg, false, false);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}, consumerOptions);
|
|
250
|
+
const subscription = new RabbitMQSubscription(subscriptionId, this.name, consumerTag, this.amqpChannel);
|
|
251
|
+
this.subscriptions.set(subscriptionId, subscription);
|
|
252
|
+
return subscription;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Send message to dead-letter queue
|
|
256
|
+
*/
|
|
257
|
+
async sendToDeadLetter(message, deadLetterChannel) {
|
|
258
|
+
const content = JSON.stringify({
|
|
259
|
+
payload: message.payload,
|
|
260
|
+
metadata: {
|
|
261
|
+
...message.metadata,
|
|
262
|
+
originalChannel: this.name,
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
this.amqpChannel.sendToQueue(deadLetterChannel, Buffer.from(content), { persistent: true });
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Request-response pattern
|
|
269
|
+
*/
|
|
270
|
+
async request(payload, options) {
|
|
271
|
+
await this.assert();
|
|
272
|
+
const correlationId = node_crypto.randomUUID();
|
|
273
|
+
const timeout = options?.timeout ?? 30000;
|
|
274
|
+
// Create exclusive reply queue
|
|
275
|
+
const { queue: replyQueue } = await this.amqpChannel.assertQueue("", {
|
|
276
|
+
exclusive: true,
|
|
277
|
+
autoDelete: true,
|
|
278
|
+
});
|
|
279
|
+
return new Promise((resolve, reject) => {
|
|
280
|
+
const timeoutId = setTimeout(() => {
|
|
281
|
+
reject(new Error(`Request timeout after ${timeout}ms`));
|
|
282
|
+
}, timeout);
|
|
283
|
+
// Consume reply
|
|
284
|
+
this.amqpChannel.consume(replyQueue, (msg) => {
|
|
285
|
+
if (msg?.properties.correlationId === correlationId) {
|
|
286
|
+
clearTimeout(timeoutId);
|
|
287
|
+
const content = JSON.parse(msg.content.toString());
|
|
288
|
+
resolve(content.payload);
|
|
289
|
+
}
|
|
290
|
+
}, { noAck: true });
|
|
291
|
+
// Send request
|
|
292
|
+
const messageContent = JSON.stringify({
|
|
293
|
+
payload,
|
|
294
|
+
metadata: {
|
|
295
|
+
messageId: node_crypto.randomUUID(),
|
|
296
|
+
timestamp: new Date().toISOString(),
|
|
297
|
+
correlationId,
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
this.amqpChannel.sendToQueue(this.name, Buffer.from(messageContent), {
|
|
301
|
+
correlationId,
|
|
302
|
+
replyTo: replyQueue,
|
|
303
|
+
expiration: timeout.toString(),
|
|
304
|
+
...options,
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Register response handler for RPC
|
|
310
|
+
*/
|
|
311
|
+
async respond(handler) {
|
|
312
|
+
return this.subscribe(async (message, ctx) => {
|
|
313
|
+
const response = await handler(message, ctx);
|
|
314
|
+
await ctx.reply(response);
|
|
315
|
+
await ctx.ack();
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Get queue statistics
|
|
320
|
+
*/
|
|
321
|
+
async stats() {
|
|
322
|
+
await this.assert();
|
|
323
|
+
const queueInfo = await this.amqpChannel.checkQueue(this.name);
|
|
324
|
+
return {
|
|
325
|
+
name: this.name,
|
|
326
|
+
messageCount: queueInfo.messageCount,
|
|
327
|
+
consumerCount: queueInfo.consumerCount,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Purge all messages
|
|
332
|
+
*/
|
|
333
|
+
async purge() {
|
|
334
|
+
await this.assert();
|
|
335
|
+
const result = await this.amqpChannel.purgeQueue(this.name);
|
|
336
|
+
return result.messageCount;
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Check if queue exists
|
|
340
|
+
*/
|
|
341
|
+
async exists() {
|
|
342
|
+
try {
|
|
343
|
+
await this.amqpChannel.checkQueue(this.name);
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Delete the queue
|
|
352
|
+
*/
|
|
353
|
+
async delete() {
|
|
354
|
+
// Cancel all subscriptions
|
|
355
|
+
for (const subscription of this.subscriptions.values()) {
|
|
356
|
+
await subscription.unsubscribe();
|
|
357
|
+
}
|
|
358
|
+
this.subscriptions.clear();
|
|
359
|
+
try {
|
|
360
|
+
await this.amqpChannel.deleteQueue(this.name);
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
363
|
+
// Ignore if queue doesn't exist
|
|
364
|
+
}
|
|
365
|
+
this.asserted = false;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* RabbitMQ Subscription Implementation
|
|
370
|
+
*/
|
|
371
|
+
class RabbitMQSubscription {
|
|
372
|
+
id;
|
|
373
|
+
channel;
|
|
374
|
+
consumerTag;
|
|
375
|
+
amqpChannel;
|
|
376
|
+
_isActive = true;
|
|
377
|
+
constructor(id, channel, consumerTag, amqpChannel) {
|
|
378
|
+
this.id = id;
|
|
379
|
+
this.channel = channel;
|
|
380
|
+
this.consumerTag = consumerTag;
|
|
381
|
+
this.amqpChannel = amqpChannel;
|
|
382
|
+
}
|
|
383
|
+
async unsubscribe() {
|
|
384
|
+
if (!this._isActive)
|
|
385
|
+
return;
|
|
386
|
+
await this.amqpChannel.cancel(this.consumerTag);
|
|
387
|
+
this._isActive = false;
|
|
388
|
+
}
|
|
389
|
+
async pause() {
|
|
390
|
+
// RabbitMQ doesn't have native pause, cancel consumer
|
|
391
|
+
await this.amqpChannel.cancel(this.consumerTag);
|
|
392
|
+
}
|
|
393
|
+
async resume() {
|
|
394
|
+
// Would need to re-subscribe - not directly supported
|
|
395
|
+
throw new Error("Resume is not supported for RabbitMQ. Please create a new subscription.");
|
|
396
|
+
}
|
|
397
|
+
isActive() {
|
|
398
|
+
return this._isActive;
|
|
399
|
+
}
|
|
400
|
+
}exports.RabbitMQChannel=RabbitMQChannel;//# sourceMappingURL=rabbitmq-channel.js.map
|