@onlineapps/conn-infra-mq 1.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/LICENSE +21 -0
- package/README.md +223 -0
- package/package.json +96 -0
- package/src/BaseClient.js +219 -0
- package/src/ConnectorMQClient.js +446 -0
- package/src/config/configSchema.js +70 -0
- package/src/config/defaultConfig.js +48 -0
- package/src/index.js +65 -0
- package/src/layers/ForkJoinHandler.js +312 -0
- package/src/layers/QueueManager.js +263 -0
- package/src/layers/RPCHandler.js +324 -0
- package/src/layers/RetryHandler.js +370 -0
- package/src/layers/WorkflowRouter.js +136 -0
- package/src/transports/rabbitmqClient.js +216 -0
- package/src/transports/transportFactory.js +33 -0
- package/src/utils/errorHandler.js +120 -0
- package/src/utils/logger.js +38 -0
- package/src/utils/serializer.js +44 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 ONLINE APPS s.r.o.; info@onlineapps.cz
|
|
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
|
|
13
|
+
all 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
|
|
21
|
+
THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# @onlineapps/connector-mq-client
|
|
2
|
+
|
|
3
|
+
[](https://github.com/onlineapps/connector-mq-client/actions)
|
|
4
|
+
[](https://codecov.io/gh/onlineapps/connector-mq-client)
|
|
5
|
+
[](https://www.npmjs.com/package/@onlineapps/connector-mq-client)
|
|
6
|
+
|
|
7
|
+
> Message queue connector with **layered architecture** for workflow orchestration, RPC, fork-join, and retry patterns. Built on top of RabbitMQ with clean separation of concerns.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## π Features
|
|
12
|
+
|
|
13
|
+
- **Layered Architecture**: Clean separation into specialized layers (WorkflowRouter, QueueManager, ForkJoinHandler, RPCHandler, RetryHandler)
|
|
14
|
+
- **Workflow Orchestration**: Decentralized workflow routing without central orchestrator
|
|
15
|
+
- **Fork-Join Pattern**: Parallel processing with result aggregation and built-in join strategies
|
|
16
|
+
- **RPC Support**: Request-response communication with correlation IDs and timeouts
|
|
17
|
+
- **Automatic Retry**: Exponential backoff, dead letter queue management, configurable retry policies
|
|
18
|
+
- **Queue Management**: TTL, DLQ, auto-delete, temporary queues, exchange bindings
|
|
19
|
+
- **Promise-based API**: All operations return promises for clean async/await usage
|
|
20
|
+
- **Built-in Serialization**: JSON with custom error handling
|
|
21
|
+
- **Config Validation**: Strict schema validation via Ajv
|
|
22
|
+
- **Extensible Transport**: Clear separation between core logic and transport layer
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## π¦ Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install @onlineapps/connector-mq-client
|
|
30
|
+
# or
|
|
31
|
+
yarn add @onlineapps/connector-mq-client
|
|
32
|
+
````
|
|
33
|
+
|
|
34
|
+
> Requires Node.js β₯12. For RabbitMQ usage, ensure an accessible AMQP server.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## ποΈ Architecture
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
ConnectorMQClient (main orchestrator)
|
|
42
|
+
βββ BaseClient (core AMQP operations)
|
|
43
|
+
βββ WorkflowRouter (workflow orchestration)
|
|
44
|
+
βββ QueueManager (queue lifecycle management)
|
|
45
|
+
βββ ForkJoinHandler (parallel processing)
|
|
46
|
+
βββ RPCHandler (request-response patterns)
|
|
47
|
+
βββ RetryHandler (error recovery & DLQ)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## π§ Quick Start
|
|
51
|
+
|
|
52
|
+
```js
|
|
53
|
+
'use strict';
|
|
54
|
+
|
|
55
|
+
const ConnectorMQClient = require('@onlineapps/connector-mq-client');
|
|
56
|
+
|
|
57
|
+
(async () => {
|
|
58
|
+
// 1. Create client with configuration
|
|
59
|
+
const client = new ConnectorMQClient({
|
|
60
|
+
host: 'amqp://localhost:5672',
|
|
61
|
+
serviceName: 'my-service',
|
|
62
|
+
queue: 'default-queue',
|
|
63
|
+
durable: true,
|
|
64
|
+
prefetch: 5, // Default prefetch count for consumers
|
|
65
|
+
noAck: false, // Default auto-acknowledge = false
|
|
66
|
+
retryPolicy: { // Optional reconnection policy (not enforced in v1.0.0)
|
|
67
|
+
retries: 5,
|
|
68
|
+
initialDelayMs: 1000,
|
|
69
|
+
maxDelayMs: 30000,
|
|
70
|
+
factor: 2
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// 2. Register a global error handler
|
|
75
|
+
client.onError(err => {
|
|
76
|
+
console.error('[AgentMQClient] Error:', err);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// 3. Connect to RabbitMQ
|
|
80
|
+
try {
|
|
81
|
+
await client.connect();
|
|
82
|
+
console.log('Connected to broker');
|
|
83
|
+
} catch (err) {
|
|
84
|
+
console.error('Connection failed:', err);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 4. Publish a sample message
|
|
89
|
+
const samplePayload = { taskId: 'abc123', action: 'processData', timestamp: Date.now() };
|
|
90
|
+
try {
|
|
91
|
+
await client.publish('job_queue', samplePayload, {
|
|
92
|
+
persistent: true,
|
|
93
|
+
headers: { origin: 'quickStart' }
|
|
94
|
+
});
|
|
95
|
+
console.log('Message published:', samplePayload);
|
|
96
|
+
} catch (err) {
|
|
97
|
+
console.error('Publish error:', err);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 5. Consume messages
|
|
101
|
+
try {
|
|
102
|
+
await client.consume(
|
|
103
|
+
'job_queue',
|
|
104
|
+
async (msg) => {
|
|
105
|
+
const data = JSON.parse(msg.content.toString('utf8'));
|
|
106
|
+
console.log('Received:', data);
|
|
107
|
+
// Process message...
|
|
108
|
+
await client.ack(msg);
|
|
109
|
+
},
|
|
110
|
+
{ prefetch: 5, noAck: false }
|
|
111
|
+
);
|
|
112
|
+
console.log('Consuming from "job_queue"...');
|
|
113
|
+
} catch (err) {
|
|
114
|
+
console.error('Consume error:', err);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 6. Graceful shutdown on SIGINT
|
|
118
|
+
process.on('SIGINT', async () => {
|
|
119
|
+
console.log('Shutting down...');
|
|
120
|
+
try {
|
|
121
|
+
await client.disconnect();
|
|
122
|
+
console.log('Disconnected, exiting.');
|
|
123
|
+
process.exit(0);
|
|
124
|
+
} catch (discErr) {
|
|
125
|
+
console.error('Error during disconnect:', discErr);
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
})();
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## π Configuration
|
|
135
|
+
|
|
136
|
+
Configuration can be provided to the `AgentMQClient` constructor or as overrides to `connect()`. Below is a summary of supported fields (see `docs/api.md` for full details):
|
|
137
|
+
|
|
138
|
+
| Field | Type | Description | Default |
|
|
139
|
+
| ------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------- |
|
|
140
|
+
| `type` | `string` | Transport type: `'rabbitmq'` | `'rabbitmq'` |
|
|
141
|
+
| `host` | `string` | Connection URI or hostname. For RabbitMQ: e.g. `'amqp://user:pass@localhost:5672'`. | *Required* |
|
|
142
|
+
| `queue` | `string` | Default queue name for publish/consume if not overridden per call. | `''` |
|
|
143
|
+
| `exchange` | `string` | Default exchange name. Empty string uses the default direct exchange. | `''` |
|
|
144
|
+
| `durable` | `boolean` | Declare queues/exchanges as durable. | `true` |
|
|
145
|
+
| `prefetch` | `integer` | Default prefetch count for consumers. | `1` |
|
|
146
|
+
| `noAck` | `boolean` | Default auto-acknowledge setting for consumers. If `true`, messages will be auto-acked. | `false` |
|
|
147
|
+
| `logger` | `object` | Custom logger with methods: `info()`, `warn()`, `error()`, `debug()`. If omitted, `console` is used. | `null` |
|
|
148
|
+
| `retryPolicy` | `object` | Reconnection policy with properties:<br>β `retries` (number)<br>β `initialDelayMs` (ms)<br>β `maxDelayMs` (ms)<br>β `factor` (multiplier). Not enforced in v1.0.0. | `{ retries: 5, initialDelayMs: 1000, maxDelayMs: 30000, factor: 2 }` |
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## π οΈ API Reference
|
|
153
|
+
|
|
154
|
+
For full class and method documentation, including parameter descriptions, return values, and error details, see [docs/api.md](https://github.com/onlineapps/agent-mq-client/blob/main/docs/api.md).
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## β
Testing
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
npm test # All tests
|
|
162
|
+
npm run test:unit # Unit tests only
|
|
163
|
+
npm run test:component # Component tests
|
|
164
|
+
npm run test:integration # Integration tests
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Test Coverage Status
|
|
168
|
+
- **Overall Coverage**: 24.52% (improving after refactoring)
|
|
169
|
+
- **Passing Tests**: 75/104 (72%)
|
|
170
|
+
- **Test Suites**: 10/14 passing
|
|
171
|
+
- **Well Tested**: Config, Transports, Error handling (100%)
|
|
172
|
+
- **Needs Testing**: New layers (1-5%)
|
|
173
|
+
|
|
174
|
+
See [Test Report](docs/TEST_REPORT.md) for detailed coverage analysis.
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## π¨ Coding Standards
|
|
179
|
+
|
|
180
|
+
* **Linting**: ESLint (`eslint:recommended` + Prettier).
|
|
181
|
+
* **Formatting**: Prettier β check with `npm run prettier:check`, fix with `npm run prettier:fix`.
|
|
182
|
+
* **Testing**: Jest, aiming for β₯90% coverage.
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## π― Refactoring Benefits
|
|
187
|
+
|
|
188
|
+
### β
What Was Achieved
|
|
189
|
+
1. **Clean Layered Architecture** - Separated into specialized layers
|
|
190
|
+
2. **Removed Technical Debt** - Replaced MQWrapper with cleaner design
|
|
191
|
+
3. **Improved Extensibility** - Easy to add new patterns
|
|
192
|
+
4. **Better Developer Experience** - Cleaner API and documentation
|
|
193
|
+
|
|
194
|
+
### π Quality Improvements
|
|
195
|
+
- **Separation of Concerns** - Each layer has single responsibility
|
|
196
|
+
- **Modular Design** - Use individual layers independently
|
|
197
|
+
- **Testability** - Each layer can be tested in isolation
|
|
198
|
+
- **Maintainability** - Easier to understand and modify
|
|
199
|
+
- **Backwards Compatibility** - MQWrapper alias maintained
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## π€ Contributing
|
|
204
|
+
|
|
205
|
+
Contributions welcome! Please see [CONTRIBUTING.md](https://github.com/onlineapps/agent-mq-client/blob/main/CONTRIBUTING.md) for guidelines:
|
|
206
|
+
|
|
207
|
+
1. Fork the repo.
|
|
208
|
+
2. Create a feature branch: `git checkout -b feature/your-feature`.
|
|
209
|
+
3. Run tests locally and ensure linting passes.
|
|
210
|
+
4. Commit your changes and push to your branch.
|
|
211
|
+
5. Open a Pull Request against `main`.
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## π License
|
|
216
|
+
|
|
217
|
+
This project is licensed under the MIT License. See [LICENSE](https://github.com/onlineapps/agent-mq-client/blob/main/LICENSE) for details.
|
|
218
|
+
|
|
219
|
+
## π Documentation
|
|
220
|
+
|
|
221
|
+
- [Complete Connectors Documentation](../../../docs/modules/connector.md)
|
|
222
|
+
- [Testing Standards](../../../docs/modules/connector.md#testing-standards)
|
|
223
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@onlineapps/conn-infra-mq",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "A promise-based, broker-agnostic client for sending and receiving messages via RabbitMQ",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/onlineapps/connector-mq-client.git"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src/",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE",
|
|
14
|
+
"CHANGELOG.md"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"test": "jest --coverage",
|
|
18
|
+
"test:unit": "jest --testPathPattern=unit --coverage",
|
|
19
|
+
"test:component": "jest --testPathPattern=component --coverage",
|
|
20
|
+
"test:integration": "jest --config=jest.integration.config.js",
|
|
21
|
+
"test:integration:required": "REQUIRE_SERVICES=true jest --config=jest.integration.config.js",
|
|
22
|
+
"test:watch": "jest --watch",
|
|
23
|
+
"test:coverage": "jest --coverage --coverageReporters=text-lcov html",
|
|
24
|
+
"lint": "eslint \"src/**/*.js\" \"test/**/*.js\"",
|
|
25
|
+
"prettier:check": "prettier --check \"src/**/*.js\" \"test/**/*.js\"",
|
|
26
|
+
"prettier:fix": "prettier --write \"src/**/*.js\" \"test/**/*.js\""
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"connector",
|
|
30
|
+
"mq",
|
|
31
|
+
"rabbitmq",
|
|
32
|
+
"kafka",
|
|
33
|
+
"message-queue",
|
|
34
|
+
"microservice",
|
|
35
|
+
"client",
|
|
36
|
+
"async"
|
|
37
|
+
],
|
|
38
|
+
"author": "OnlineApps Team <dev@onlineapps.io>",
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://github.com/onlineapps/connector-mq-client/issues"
|
|
42
|
+
},
|
|
43
|
+
"homepage": "https://github.com/onlineapps/connector-mq-client#readme",
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"ajv": "^8.11.0",
|
|
46
|
+
"amqplib": "^0.10.3",
|
|
47
|
+
"lodash.merge": "^4.6.2"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
51
|
+
"@semantic-release/commit-analyzer": "^11.1.0",
|
|
52
|
+
"@semantic-release/git": "^10.0.1",
|
|
53
|
+
"@semantic-release/github": "^9.2.6",
|
|
54
|
+
"@semantic-release/npm": "^11.0.3",
|
|
55
|
+
"@semantic-release/release-notes-generator": "^12.1.0",
|
|
56
|
+
"dotenv": "^17.2.2",
|
|
57
|
+
"eslint": "^8.30.0",
|
|
58
|
+
"eslint-config-prettier": "^8.5.0",
|
|
59
|
+
"jest": "^29.5.0",
|
|
60
|
+
"prettier": "^2.8.0",
|
|
61
|
+
"sinon": "^15.0.0",
|
|
62
|
+
"uuid": "^13.0.0"
|
|
63
|
+
},
|
|
64
|
+
"engines": {
|
|
65
|
+
"node": ">=12"
|
|
66
|
+
},
|
|
67
|
+
"eslintConfig": {
|
|
68
|
+
"env": {
|
|
69
|
+
"node": true,
|
|
70
|
+
"jest": true,
|
|
71
|
+
"es2021": true
|
|
72
|
+
},
|
|
73
|
+
"extends": [
|
|
74
|
+
"eslint:recommended",
|
|
75
|
+
"prettier"
|
|
76
|
+
],
|
|
77
|
+
"parserOptions": {
|
|
78
|
+
"ecmaVersion": 12,
|
|
79
|
+
"sourceType": "script"
|
|
80
|
+
},
|
|
81
|
+
"rules": {
|
|
82
|
+
"no-console": "off",
|
|
83
|
+
"strict": [
|
|
84
|
+
"error",
|
|
85
|
+
"global"
|
|
86
|
+
]
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
"prettier": {
|
|
90
|
+
"printWidth": 100,
|
|
91
|
+
"singleQuote": true,
|
|
92
|
+
"trailingComma": "es5",
|
|
93
|
+
"tabWidth": 2,
|
|
94
|
+
"endOfLine": "lf"
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ConnectorMQClient: a promise-based, broker-agnostic client for RabbitMQ
|
|
5
|
+
* Uses transportFactory to select the appropriate transport implementation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const Ajv = require('ajv');
|
|
9
|
+
const merge = require('lodash.merge');
|
|
10
|
+
|
|
11
|
+
const configSchema = require('./config/configSchema');
|
|
12
|
+
const defaultConfig = require('./config/defaultConfig');
|
|
13
|
+
const transportFactory = require('./transports/transportFactory');
|
|
14
|
+
const serializer = require('./utils/serializer');
|
|
15
|
+
const {
|
|
16
|
+
ConnectionError,
|
|
17
|
+
PublishError,
|
|
18
|
+
ConsumeError,
|
|
19
|
+
ValidationError,
|
|
20
|
+
SerializationError,
|
|
21
|
+
} = require('./utils/errorHandler');
|
|
22
|
+
|
|
23
|
+
class ConnectorMQClient {
|
|
24
|
+
/**
|
|
25
|
+
* @param {Object} config - User-supplied configuration.
|
|
26
|
+
* @throws {ValidationError} If required fields are missing or invalid.
|
|
27
|
+
*/
|
|
28
|
+
constructor(config) {
|
|
29
|
+
const ajv = new Ajv({ allErrors: true, useDefaults: true });
|
|
30
|
+
const validate = ajv.compile(configSchema);
|
|
31
|
+
|
|
32
|
+
// Merge user config with defaults
|
|
33
|
+
this._config = merge({}, defaultConfig, config || {});
|
|
34
|
+
|
|
35
|
+
// Validate merged config
|
|
36
|
+
const valid = validate(this._config);
|
|
37
|
+
if (!valid) {
|
|
38
|
+
const details = validate.errors.map((err) => ({
|
|
39
|
+
path: err.instancePath,
|
|
40
|
+
message: err.message,
|
|
41
|
+
}));
|
|
42
|
+
throw new ValidationError('Invalid configuration', details);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
this._transport = null;
|
|
46
|
+
this._connected = false;
|
|
47
|
+
this._errorHandlers = [];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Connects to the message broker using merged configuration.
|
|
52
|
+
* @param {Object} [options] - Optional overrides for host, queue, etc.
|
|
53
|
+
* @returns {Promise<void>}
|
|
54
|
+
* @throws {ConnectionError} If connecting fails.
|
|
55
|
+
*/
|
|
56
|
+
async connect(options = {}) {
|
|
57
|
+
if (this._connected) return;
|
|
58
|
+
|
|
59
|
+
// Merge overrides into existing config
|
|
60
|
+
this._config = merge({}, this._config, options);
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
// Instantiate appropriate transport: RabbitMQClient
|
|
64
|
+
this._transport = transportFactory.create(this._config);
|
|
65
|
+
|
|
66
|
+
// Register internal error propagation
|
|
67
|
+
this._transport.on('error', (err) => this._handleError(err));
|
|
68
|
+
|
|
69
|
+
await this._transport.connect(this._config);
|
|
70
|
+
this._connected = true;
|
|
71
|
+
} catch (err) {
|
|
72
|
+
throw new ConnectionError('Failed to connect to broker', err);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Disconnects from the message broker.
|
|
78
|
+
* @returns {Promise<void>}
|
|
79
|
+
* @throws {Error} If disconnecting fails unexpectedly.
|
|
80
|
+
*/
|
|
81
|
+
async disconnect() {
|
|
82
|
+
if (!this._connected || !this._transport) return;
|
|
83
|
+
try {
|
|
84
|
+
await this._transport.disconnect();
|
|
85
|
+
this._connected = false;
|
|
86
|
+
this._transport = null;
|
|
87
|
+
} catch (err) {
|
|
88
|
+
throw new Error(`Error during disconnect: ${err.message}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Publishes a message to the specified queue.
|
|
94
|
+
* @param {string} queue - Target queue name.
|
|
95
|
+
* @param {Object|Buffer|string} message - Payload to send.
|
|
96
|
+
* @param {Object} [options] - RabbitMQ-specific overrides (routingKey, persistent, headers).
|
|
97
|
+
* @returns {Promise<void>}
|
|
98
|
+
* @throws {ConnectionError} If not connected.
|
|
99
|
+
* @throws {PublishError} If publish fails.
|
|
100
|
+
*/
|
|
101
|
+
async publish(queue, message, options = {}) {
|
|
102
|
+
if (!this._connected || !this._transport) {
|
|
103
|
+
throw new ConnectionError('Cannot publish: client is not connected');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let buffer;
|
|
107
|
+
try {
|
|
108
|
+
if (Buffer.isBuffer(message)) {
|
|
109
|
+
buffer = message;
|
|
110
|
+
} else if (typeof message === 'string') {
|
|
111
|
+
buffer = Buffer.from(message, 'utf8');
|
|
112
|
+
} else {
|
|
113
|
+
const json = serializer.serialize(message);
|
|
114
|
+
buffer = Buffer.from(json, 'utf8');
|
|
115
|
+
}
|
|
116
|
+
} catch (err) {
|
|
117
|
+
throw new SerializationError('Failed to serialize message', message, err);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
await this._transport.publish(queue, buffer, options);
|
|
122
|
+
} catch (err) {
|
|
123
|
+
throw new PublishError(`Failed to publish to queue "${queue}"`, queue, err);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Begins consuming messages from the specified queue.
|
|
129
|
+
* @param {string} queue - Name of the queue to consume from.
|
|
130
|
+
* @param {function(Object): Promise<void>} messageHandler - Async function to process each message.
|
|
131
|
+
* @param {Object} [options] - RabbitMQ-specific overrides (prefetch, noAck).
|
|
132
|
+
* @returns {Promise<void>}
|
|
133
|
+
* @throws {ConnectionError} If not connected.
|
|
134
|
+
* @throws {ConsumeError} If consumer setup fails.
|
|
135
|
+
*/
|
|
136
|
+
async consume(queue, messageHandler, options = {}) {
|
|
137
|
+
if (!this._connected || !this._transport) {
|
|
138
|
+
throw new ConnectionError('Cannot consume: client is not connected');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Apply prefetch and noAck overrides if provided
|
|
142
|
+
const { prefetch, noAck } = options;
|
|
143
|
+
const consumeOptions = {};
|
|
144
|
+
if (typeof prefetch === 'number') consumeOptions.prefetch = prefetch;
|
|
145
|
+
if (typeof noAck === 'boolean') consumeOptions.noAck = noAck;
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
await this._transport.consume(
|
|
149
|
+
queue,
|
|
150
|
+
async (msg) => {
|
|
151
|
+
try {
|
|
152
|
+
await messageHandler(msg);
|
|
153
|
+
if (consumeOptions.noAck === false) {
|
|
154
|
+
await this.ack(msg);
|
|
155
|
+
}
|
|
156
|
+
} catch (handlerErr) {
|
|
157
|
+
// On handler error, nack with requeue: true
|
|
158
|
+
await this.nack(msg, { requeue: true });
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
consumeOptions
|
|
162
|
+
);
|
|
163
|
+
} catch (err) {
|
|
164
|
+
throw new ConsumeError(`Failed to start consumer for queue "${queue}"`, queue, err);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Acknowledges a RabbitMQ message.
|
|
170
|
+
* @param {Object} msg - RabbitMQ message object.
|
|
171
|
+
* @returns {Promise<void>}
|
|
172
|
+
*/
|
|
173
|
+
async ack(msg) {
|
|
174
|
+
if (!this._connected || !this._transport) {
|
|
175
|
+
throw new ConnectionError('Cannot ack: client is not connected');
|
|
176
|
+
}
|
|
177
|
+
return this._transport.ack(msg);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Negative-acknowledges a RabbitMQ message.
|
|
182
|
+
* @param {Object} msg - RabbitMQ message object.
|
|
183
|
+
* @param {Object} [options] - Options such as { requeue: boolean }.
|
|
184
|
+
* @returns {Promise<void>}
|
|
185
|
+
*/
|
|
186
|
+
async nack(msg, options = {}) {
|
|
187
|
+
if (!this._connected || !this._transport) {
|
|
188
|
+
throw new ConnectionError('Cannot nack: client is not connected');
|
|
189
|
+
}
|
|
190
|
+
return this._transport.nack(msg, options);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Registers a global error handler. Internal or transport-level errors will be forwarded here.
|
|
195
|
+
* @param {function(Error): void} callback
|
|
196
|
+
*/
|
|
197
|
+
onError(callback) {
|
|
198
|
+
if (typeof callback === 'function') {
|
|
199
|
+
this._errorHandlers.push(callback);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Internal helper to invoke all registered error handlers.
|
|
205
|
+
* @param {Error} error
|
|
206
|
+
* @private
|
|
207
|
+
*/
|
|
208
|
+
_handleError(error) {
|
|
209
|
+
this._errorHandlers.forEach((cb) => {
|
|
210
|
+
try {
|
|
211
|
+
cb(error);
|
|
212
|
+
} catch (_) {
|
|
213
|
+
// Ignore errors in user-provided error handlers
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
module.exports = ConnectorMQClient;
|