├── .editorconfig ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md ├── copilot-instructions.md └── workflows │ ├── benchmark.yml │ ├── dependencies.yml │ ├── integration.yml │ └── notification.yml ├── .gitignore ├── .npmignore ├── .vscode ├── launch.json └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── assets ├── dead_letter.png ├── legacy.png ├── overview.png └── redis_queue.png ├── benchmark ├── .eslintrc.js ├── index.js └── suites │ ├── latency.js │ └── throughput.js ├── examples ├── .eslintrc.js ├── channel-name │ └── index.js ├── context │ └── index.js ├── dead-letter │ └── index.js ├── external-message │ └── index.js ├── headers │ └── index.js ├── index.js ├── maxInFlight │ └── index.js ├── metrics │ └── index.js ├── multi-adapter │ └── index.js ├── multi-subs │ └── index.js ├── mw-hooks │ └── index.js ├── nats-wildcard │ └── index.js ├── prefix │ └── index.js ├── redis-claim │ ├── index.js │ └── stable.service.js ├── redis-cluster │ └── index.js ├── redis-pending │ └── index.js ├── retry │ └── index.js ├── send-in-started │ └── index.js ├── serializer │ └── index.js ├── simple │ └── index.js └── tracking │ └── index.js ├── index.js ├── package-lock.json ├── package.json ├── prettier.config.js ├── src ├── adapters │ ├── amqp.js │ ├── base.js │ ├── fake.js │ ├── index.js │ ├── kafka.js │ ├── nats.js │ └── redis.js ├── constants.js ├── index.js ├── tracing.js └── utils.js ├── test ├── docker-compose.yml ├── integration │ └── index.spec.js └── unit │ └── index.spec.js ├── tsconfig.json └── types ├── index.d.ts └── src ├── adapters ├── amqp.d.ts ├── base.d.ts ├── fake.d.ts ├── index.d.ts ├── kafka.d.ts ├── nats.d.ts └── redis.d.ts ├── constants.d.ts ├── index.d.ts └── tracing.d.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | # Change these settings to your own preference 10 | indent_style = tab 11 | indent_size = 4 12 | space_after_anon_function = true 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | indent_style = space 23 | indent_size = 4 24 | 25 | [{package,bower}.json] 26 | indent_style = space 27 | indent_size = 2 28 | 29 | [*.{yml,yaml}] 30 | indent_style = space 31 | indent_size = 2 32 | 33 | [*.js] 34 | quote_type = "double" 35 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | node: true, 4 | commonjs: true, 5 | es6: true, 6 | jquery: false, 7 | jest: true, 8 | jasmine: true 9 | }, 10 | extends: ["eslint:recommended", "plugin:security/recommended", "plugin:prettier/recommended"], 11 | parserOptions: { 12 | sourceType: "module", 13 | ecmaVersion: 2020 14 | }, 15 | plugins: ["node", "promise", "security"], 16 | rules: { 17 | "no-var": ["error"], 18 | "no-console": ["warn"], 19 | "no-unused-vars": ["warn"], 20 | "no-trailing-spaces": ["error"], 21 | "security/detect-object-injection": ["off"], 22 | "security/detect-non-literal-require": ["off"], 23 | "security/detect-non-literal-fs-filename": ["off"], 24 | "no-process-exit": ["off"], 25 | "node/no-unpublished-require": 0, 26 | "require-atomic-updates": 0, 27 | "object-curly-spacing": ["warn", "always"] 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | ## Prerequisites 8 | 9 | Please answer the following questions for yourself before submitting an issue. 10 | 11 | - [ ] I am running the latest version 12 | - [ ] I checked the documentation and found no answer 13 | - [ ] I checked to make sure that this issue has not already been filed 14 | - [ ] I'm reporting the issue to the correct repository 15 | 16 | ## Current Behavior 17 | 18 | 19 | 20 | ## Expected Behavior 21 | 22 | 23 | 24 | ## Failure Information 25 | 26 | 27 | 28 | ### Steps to Reproduce 29 | 30 | Please provide detailed steps for reproducing the issue. 31 | 32 | 1. step 1 33 | 2. step 2 34 | 3. you get it... 35 | 36 | ### Reproduce code snippet 37 | 39 | 40 | ```js 41 | const broker = new ServiceBroker({ 42 | logger: console, 43 | transporter: "NATS" 44 | }); 45 | 46 | broker.createService({ 47 | name: "test", 48 | channels: { 49 | async "order.created"(payload) { 50 | return payload 51 | } 52 | } 53 | }); 54 | ``` 55 | 56 | ### Context 57 | 58 | Please provide any relevant information about your setup. This is important in case the issue is not reproducible except for under certain conditions. 59 | 60 | * Moleculer version: 61 | * NodeJS version: 62 | * Operating System: 63 | 64 | ### Failure Logs 65 | ``` 66 | 67 | ``` 68 | -------------------------------------------------------------------------------- /.github/copilot-instructions.md: -------------------------------------------------------------------------------- 1 | # Context 2 | 3 | This is a codebase for message delivery for MoleculerJS services via external queue/channel/topic. Unlike moleculer built-in events, this is **not** a fire-and-forget solution. It's a persistent, durable and reliable message sending solution. The module uses an external message queue/streaming server that stores messages until they are successfully processed. 4 | The module supports multiple queue/streaming servers, including Redis, RabbitMQ, NATS JetStream, Kafka. It provides a unified interface for sending and receiving messages across different brokers. 5 | 6 | # Directory Structure 7 | 8 | ``` 9 | . 10 | ├── index.js 11 | ├── package.json 12 | ├── examples 13 | ├── src 14 | │ ├── adapters 15 | │ │ ├── amqp.js 16 | │ │ ├── base.js 17 | │ │ ├── fake.js 18 | │ │ ├── index.js 19 | │ │ ├── kafka.js 20 | │ │ ├── nats.js 21 | │ │ └── redis.js 22 | │ ├── constants.js 23 | │ ├── index.js 24 | │ └── tracing.js 25 | ├── test 26 | │ ├── docker-compose.yml 27 | │ ├── integration 28 | │ │ └── index.spec.js 29 | │ └── unit 30 | │ └── index.spec.js 31 | ``` 32 | 33 | # Key Files 34 | 35 | - `index.js`: Entry point of the module. 36 | - `src/adapters/`: Contains adapter implementations for different message brokers (Redis, RabbitMQ, NATS, Kafka). All of adapters extend from `base.js`. 37 | - `src/constants.js`: Defines constants used across the module such as metrics names, Redis header keys, etc. 38 | - `src/tracing.js`: Implements tracing middleware for tracing the message flow. The middleware is implemented via MoleculerJS middleware, and it works with any adapter. It is initialized via `created()` [lifecycle method](https://moleculer.services/docs/0.15/middlewares#created-broker-async) of the service broker. 39 | - `src/index.js`: Main file that implements `created()`, `serviceCreated()`, `serviceStopping()`, and `stopped()`. It is responsible for creating broker-level methods like `sendToChannel()` (configurable via `sendMethodName` option), exposing raw adapter via `channelAdapter` (configurable via `adapterPropertyName` option), registering metrics, registering service topics listeners and cleaning up resources on broker stop. 40 | - `test/`: Contains integration tests. Integration tests use Docker Compose to spin up required message brokers. The focus of the tests is to verify the integration with different message brokers and ensure that messages are sent and received correctly and that from Moleculer's perspective, the behavior is consistent across different adapters. 41 | - `package.json`: Defines the module's dependencies, scripts, jest configuration, and metadata 42 | - `examples/`: Contains different ways of using the module with different adapters and configurations. 43 | 44 | The core file from which the adapters are created from is `src/adapters/base.js`. All adapters extend from this base class and implement the required methods for sending and receiving messages. 45 | 46 | ** Skeleton of `src/adapters/base.js` ** 47 | 48 | ```js 49 | class BaseAdapter { 50 | /** 51 | * Constructor of adapter 52 | * @param {Object?} opts 53 | */ 54 | constructor(opts) { 55 | /** @type {BaseDefaultOptions} */ 56 | this.opts = _.defaultsDeep({}, opts, { 57 | consumerName: null, 58 | prefix: null, 59 | serializer: "JSON", 60 | maxRetries: 3, 61 | maxInFlight: 1, 62 | deadLettering: { 63 | enabled: false, 64 | queueName: "FAILED_MESSAGES" 65 | } 66 | }); 67 | 68 | /** 69 | * Tracks the messages that are still being processed by different clients 70 | * @type {Map>} 71 | */ 72 | this.activeMessages = new Map(); 73 | 74 | /** @type {Boolean} Flag indicating the adapter's connection status */ 75 | this.connected = false; 76 | } 77 | 78 | /** 79 | * Initialize the adapter. 80 | * 81 | * @param {ServiceBroker} broker 82 | * @param {Logger} logger 83 | */ 84 | init(broker, logger) { 85 | this.broker = broker; 86 | this.logger = logger; 87 | this.Promise = broker.Promise; 88 | 89 | if (!this.opts.consumerName) this.opts.consumerName = this.broker.nodeID; 90 | if (this.opts.prefix == null) this.opts.prefix = broker.namespace; 91 | 92 | this.logger.info("Channel consumer name:", this.opts.consumerName); 93 | this.logger.info("Channel prefix:", this.opts.prefix); 94 | 95 | // create an instance of serializer (default to JSON) 96 | /** @type {Serializer} */ 97 | this.serializer = Serializers.resolve(this.opts.serializer); 98 | this.serializer.init(this.broker); 99 | this.logger.info("Channel serializer:", this.broker.getConstructorName(this.serializer)); 100 | 101 | this.registerAdapterMetrics(broker); 102 | } 103 | 104 | /** 105 | * Register adapter related metrics 106 | * @param {ServiceBroker} broker 107 | */ 108 | registerAdapterMetrics(broker) { 109 | if (!broker.isMetricsEnabled()) return; 110 | 111 | broker.metrics.register({ 112 | type: METRIC.TYPE_COUNTER, 113 | name: C.METRIC_CHANNELS_MESSAGES_ERRORS_TOTAL, 114 | labelNames: ["channel", "group"], 115 | rate: true, 116 | unit: "msg" 117 | }); 118 | 119 | broker.metrics.register({ 120 | type: METRIC.TYPE_COUNTER, 121 | name: C.METRIC_CHANNELS_MESSAGES_RETRIES_TOTAL, 122 | labelNames: ["channel", "group"], 123 | rate: true, 124 | unit: "msg" 125 | }); 126 | 127 | broker.metrics.register({ 128 | type: METRIC.TYPE_COUNTER, 129 | name: C.METRIC_CHANNELS_MESSAGES_DEAD_LETTERING_TOTAL, 130 | labelNames: ["channel", "group"], 131 | rate: true, 132 | unit: "msg" 133 | }); 134 | } 135 | 136 | /** 137 | * 138 | * @param {String} metricName 139 | * @param {Channel} chan 140 | */ 141 | metricsIncrement(metricName, chan) { 142 | if (!this.broker.isMetricsEnabled()) return; 143 | 144 | this.broker.metrics.increment(metricName, { 145 | channel: chan.name, 146 | group: chan.group 147 | }); 148 | } 149 | 150 | /** 151 | * Check the installed client library version. 152 | * https://github.com/npm/node-semver#usage 153 | * 154 | * @param {String} library 155 | * @param {String} requiredVersions 156 | * @returns {Boolean} 157 | */ 158 | checkClientLibVersion(library, requiredVersions) { 159 | const pkg = require(`${library}/package.json`); 160 | const installedVersion = pkg.version; 161 | 162 | if (semver.satisfies(installedVersion, requiredVersions)) { 163 | return true; 164 | } else { 165 | this.logger.warn( 166 | `The installed ${library} library is not supported officially. Proper functionality cannot be guaranteed. Supported versions:`, 167 | requiredVersions 168 | ); 169 | return false; 170 | } 171 | } 172 | 173 | /** 174 | * Init active messages list for tracking messages of a channel 175 | * @param {string} channelID 176 | * @param {Boolean?} toThrow Throw error if already exists 177 | */ 178 | initChannelActiveMessages(channelID, toThrow = true) { 179 | if (this.activeMessages.has(channelID)) { 180 | if (toThrow) 181 | throw new MoleculerError( 182 | `Already tracking active messages of channel ${channelID}` 183 | ); 184 | 185 | return; 186 | } 187 | 188 | this.activeMessages.set(channelID, []); 189 | } 190 | 191 | /** 192 | * Remove active messages list of a channel 193 | * @param {string} channelID 194 | */ 195 | stopChannelActiveMessages(channelID) { 196 | if (!this.activeMessages.has(channelID)) { 197 | throw new MoleculerError(`Not tracking active messages of channel ${channelID}`); 198 | } 199 | 200 | if (this.activeMessages.get(channelID).length !== 0) { 201 | throw new MoleculerError( 202 | `Can't stop tracking active messages of channel ${channelID}. It still has ${ 203 | this.activeMessages.get(channelID).length 204 | } messages being processed.` 205 | ); 206 | } 207 | 208 | this.activeMessages.delete(channelID); 209 | } 210 | 211 | /** 212 | * Add IDs of the messages that are currently being processed 213 | * 214 | * @param {string} channelID Channel ID 215 | * @param {Array} IDs List of IDs 216 | */ 217 | addChannelActiveMessages(channelID, IDs) { 218 | if (!this.activeMessages.has(channelID)) { 219 | throw new MoleculerError(`Not tracking active messages of channel ${channelID}`); 220 | } 221 | 222 | this.activeMessages.get(channelID).push(...IDs); 223 | } 224 | 225 | /** 226 | * Remove IDs of the messages that were already processed 227 | * 228 | * @param {string} channelID Channel ID 229 | * @param {string[]|number[]} IDs List of IDs 230 | */ 231 | removeChannelActiveMessages(channelID, IDs) { 232 | if (!this.activeMessages.has(channelID)) { 233 | throw new MoleculerError(`Not tracking active messages of channel ${channelID}`); 234 | } 235 | 236 | const messageList = this.activeMessages.get(channelID); 237 | 238 | IDs.forEach(id => { 239 | const idx = messageList.indexOf(id); 240 | if (idx != -1) { 241 | messageList.splice(idx, 1); 242 | } 243 | }); 244 | } 245 | 246 | /** 247 | * Get the number of active messages of a channel 248 | * 249 | * @param {string} channelID Channel ID 250 | */ 251 | getNumberOfChannelActiveMessages(channelID) { 252 | if (!this.activeMessages.has(channelID)) { 253 | //throw new MoleculerError(`Not tracking active messages of channel ${channelID}`); 254 | return 0; 255 | } 256 | 257 | return this.activeMessages.get(channelID).length; 258 | } 259 | 260 | /** 261 | * Get the number of channels 262 | */ 263 | getNumberOfTrackedChannels() { 264 | return this.activeMessages.size; 265 | } 266 | 267 | /** 268 | * Given a topic name adds the prefix 269 | * 270 | * @param {String} topicName 271 | * @returns {String} New topic name 272 | */ 273 | addPrefixTopic(topicName) { 274 | if (this.opts.prefix != null && this.opts.prefix != "" && topicName) { 275 | return `${this.opts.prefix}.${topicName}`; 276 | } 277 | 278 | return topicName; 279 | } 280 | 281 | /** 282 | * Connect to the adapter. 283 | */ 284 | async connect() { 285 | /* istanbul ignore next */ 286 | throw new Error("This method is not implemented."); 287 | } 288 | 289 | /** 290 | * Disconnect from adapter 291 | */ 292 | async disconnect() { 293 | /* istanbul ignore next */ 294 | throw new Error("This method is not implemented."); 295 | } 296 | 297 | /** 298 | * Subscribe to a channel. 299 | * 300 | * @param {Channel} chan 301 | * @param {Service} svc 302 | */ 303 | async subscribe(chan, svc) { 304 | /* istanbul ignore next */ 305 | throw new Error("This method is not implemented."); 306 | } 307 | 308 | /** 309 | * Unsubscribe from a channel. 310 | * 311 | * @param {Channel} chan 312 | */ 313 | async unsubscribe(chan) { 314 | /* istanbul ignore next */ 315 | throw new Error("This method is not implemented."); 316 | } 317 | 318 | /** 319 | * Publish a payload to a channel. 320 | * @param {String} channelName 321 | * @param {any} payload 322 | * @param {Object?} opts 323 | */ 324 | async publish(channelName, payload, opts) { 325 | /* istanbul ignore next */ 326 | throw new Error("This method is not implemented."); 327 | } 328 | 329 | /** 330 | * Parse the headers from incoming message to a POJO. 331 | * @param {any} raw 332 | * @returns {object} 333 | */ 334 | parseMessageHeaders(raw) { 335 | return raw ? raw.headers : null; 336 | } 337 | } 338 | 339 | module.exports = BaseAdapter; 340 | ``` 341 | 342 | # Initialization example 343 | 344 | ```js 345 | // moleculer.config.js 346 | const ChannelsMiddleware = require("@moleculer/channels").Middleware; 347 | 348 | module.exports = { 349 | logger: true, 350 | 351 | middlewares: [ 352 | ChannelsMiddleware({ 353 | adapter: "redis://localhost:6379", 354 | sendMethodName: "sendToChannel", 355 | adapterPropertyName: "channelAdapter", 356 | schemaProperty: "channels" 357 | }) 358 | ] 359 | }; 360 | ``` 361 | 362 | # Concepts 363 | 364 | ## Failed message 365 | 366 | If the service is not able to process a message, it should throw an `Error` inside the handler function. In case of error and if `maxRetries` option is a positive number, the adapter will redeliver the message to one of all consumers. 367 | When the number of redelivering reaches the `maxRetries`, it will drop the message to avoid the 'retry-loop' effect. 368 | If the dead-lettering feature is enabled with `deadLettering.enabled: true` option then the adapter will move the message into the `deadLettering.queueName` queue/topic. 369 | 370 | ## Graceful stopping 371 | 372 | The adapters track the messages that are being processed. This means that when a service or the broker is stopping the adapter will block the process and wait until all active messages are processed. 373 | 374 | ## Middleware hooks 375 | 376 | It is possible to wrap the handlers and the send method in Moleculer middleware. The module defines two hooks to cover it. The `localChannel` hook is similar to [`localAction`](https://moleculer.services/docs/0.15/middlewares.html#localAction-next-action) but it wraps the channel handlers in service schema. The `sendToChannel` hook is similar to [`emit`](https://moleculer.services/docs/0.15/middlewares.html#emit-next) or [`broadcast`](https://moleculer.services/docs/0.15/middlewares.html#broadcast-next) but it wraps the `broker.sendToChannel` publisher method. 377 | 378 | **Example** 379 | 380 | ```js 381 | // moleculer.config.js 382 | const ChannelsMiddleware = require("@moleculer/channels").Middleware; 383 | 384 | const MyMiddleware = { 385 | name: "MyMiddleware", 386 | 387 | // Wrap the channel handlers 388 | localChannel(next, chan) { 389 | return async (msg, raw) => { 390 | this.logger.info(kleur.magenta(` Before localChannel for '${chan.name}'`), msg); 391 | await next(msg, raw); 392 | this.logger.info(kleur.magenta(` After localChannel for '${chan.name}'`), msg); 393 | }; 394 | }, 395 | 396 | // Wrap the `broker.sendToChannel` method 397 | sendToChannel(next) { 398 | return async (channelName, payload, opts) => { 399 | this.logger.info(kleur.yellow(`Before sendToChannel for '${channelName}'`), payload); 400 | await next(channelName, payload, opts); 401 | this.logger.info(kleur.yellow(`After sendToChannel for '${channelName}'`), payload); 402 | }; 403 | } 404 | }; 405 | 406 | module.exports = { 407 | logger: true, 408 | 409 | middlewares: [ 410 | MyMiddleware, 411 | ChannelsMiddleware({ 412 | adapter: "redis://localhost:6379" 413 | }) 414 | ] 415 | }; 416 | ``` 417 | 418 | ## Context-based messages 419 | 420 | In order to use Moleculer Context in handlers (transferring `ctx.meta` and tracing information) you should set the `context: true` option in channel definition object or in middleware options to enable it for all channel handlers. 421 | 422 | # Instructions 423 | 424 | To solve any potential issues or add new features, please follow these steps: 425 | 426 | - create a new folder in the `examples/` directory with a descriptive name for your example 427 | - create `index.js` file inside the new folder that demonstrates the feature or fixes the issue 428 | - modify or add logic in the `src/` directory as needed 429 | - create or modify tests in the `test/` directory to cover your changes 430 | -------------------------------------------------------------------------------- /.github/workflows/benchmark.yml: -------------------------------------------------------------------------------- 1 | name: Benchmark Test 2 | 3 | on: [workflow_dispatch] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v4 11 | 12 | - name: Use Node.js 13 | uses: actions/setup-node@v4 14 | with: 15 | node-version: 20.x 16 | 17 | - name: Install dependencies 18 | run: npm ci 19 | 20 | - name: Start containers 21 | run: docker compose up -d 22 | working-directory: ./test 23 | 24 | - name: Sleeping 30 secs 25 | run: sleep 30 26 | 27 | - name: Check containers 28 | run: docker compose ps 29 | working-directory: ./test 30 | 31 | - name: Run benchmark tests 32 | run: npm run bench 33 | timeout-minutes: 15 34 | env: 35 | GITHUB_ACTIONS_CI: true 36 | 37 | - name: Show container logs (in case of failure) 38 | run: docker compose logs 39 | if: failure() 40 | working-directory: ./test 41 | 42 | - name: Stop containers 43 | run: docker compose down -v 44 | working-directory: ./test 45 | 46 | - name: Commit the result 47 | if: success() && github.ref == 'refs/heads/master' 48 | uses: EndBug/add-and-commit@v7 # You can change this to use a specific version 49 | with: 50 | # The arguments for the `git add` command (see the paragraph below for more info) 51 | # Default: '.' 52 | add: './benchmark/results' 53 | default_author: github_actions 54 | push: true 55 | -------------------------------------------------------------------------------- /.github/workflows/dependencies.yml: -------------------------------------------------------------------------------- 1 | name: Update dependencies 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 1 1 *' 6 | workflow_dispatch: 7 | jobs: 8 | update-dependencies: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Use Node.js 13 | uses: actions/setup-node@v4 14 | with: 15 | node-version: '20.x' 16 | 17 | - run: npm ci 18 | 19 | - run: npm run ci-deps >/tmp/ci-deps.txt 20 | 21 | - run: | 22 | git config user.name "GitHub Actions Bot" 23 | git config user.email "hello@moleculer.services" 24 | git checkout -b update-deps-$GITHUB_RUN_ID 25 | 26 | - run: npm run ci-update-deps 27 | - run: npm i --legacy-peer-deps 28 | - run: npm audit fix || true 29 | 30 | - run: | 31 | git commit -am "Update dependencies (auto)" 32 | git push origin update-deps-$GITHUB_RUN_ID 33 | - run: | 34 | gh pr create --title "[deps]: Update all dependencies" --body-file /tmp/ci-deps.txt 35 | env: 36 | GITHUB_TOKEN: ${{secrets.CI_ACCESS_TOKEN}} 37 | -------------------------------------------------------------------------------- /.github/workflows/integration.yml: -------------------------------------------------------------------------------- 1 | name: Integration Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | paths-ignore: 8 | - 'benchmark/**' 9 | - 'docs/**' 10 | - 'examples/**' 11 | - '*.md' 12 | 13 | pull_request: 14 | branches: 15 | - master 16 | paths-ignore: 17 | - 'benchmark/**' 18 | - 'docs/**' 19 | - 'examples/**' 20 | - '*.md' 21 | 22 | jobs: 23 | test: 24 | runs-on: ubuntu-latest 25 | 26 | strategy: 27 | matrix: 28 | node-version: [20.x, 22.x, 24.x] 29 | adapter: [Fake, Redis, Redis-Cluster, AMQP, NATS, Kafka, Multi] 30 | fail-fast: false 31 | steps: 32 | - uses: actions/checkout@v4 33 | 34 | - name: Use Node.js ${{ matrix.node-version }} with ${{ matrix.adapter }} adaoter 35 | uses: actions/setup-node@v4 36 | with: 37 | node-version: ${{ matrix.node-version }} 38 | 39 | - name: Cache node modules 40 | uses: actions/cache@v4 41 | env: 42 | cache-name: cache-node-modules 43 | with: 44 | # npm cache files are stored in `~/.npm` on Linux/macOS 45 | path: ~/.npm 46 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} 47 | restore-keys: | 48 | ${{ runner.os }}-build-${{ env.cache-name }}- 49 | ${{ runner.os }}-build- 50 | ${{ runner.os }}- 51 | 52 | - name: Install dependencies 53 | run: npm ci --legacy-peer-deps 54 | 55 | #- name: Run unit tests 56 | # run: npm run test:unit 57 | 58 | - name: Start containers for Redis adapter 59 | if: ${{ matrix.adapter == 'Redis' || matrix.adapter == 'Multi' }} 60 | run: docker compose up -d redis 61 | working-directory: ./test 62 | 63 | - name: Start containers for Redis cluster adapter 64 | if: ${{ matrix.adapter == 'Redis-Cluster' }} 65 | run: docker compose up -d redis-node-1 redis-node-2 redis-node-3 66 | working-directory: ./test 67 | 68 | - name: Start containers for AMQP adapter 69 | if: ${{ matrix.adapter == 'AMQP' || matrix.adapter == 'Multi' }} 70 | run: docker compose up -d rabbitmq 71 | working-directory: ./test 72 | 73 | - name: Start containers for NATS adapter 74 | if: ${{ matrix.adapter == 'NATS' }} 75 | run: docker compose up -d nats 76 | working-directory: ./test 77 | 78 | - name: Start containers for Kafka adapter 79 | if: ${{ matrix.adapter == 'Kafka' }} 80 | run: docker compose up -d zookeeper kafka 81 | working-directory: ./test 82 | 83 | - name: Sleeping 30 secs 84 | if: ${{ matrix.adapter != 'Fake' }} 85 | run: sleep 30 86 | 87 | - name: Check containers 88 | run: docker compose ps 89 | working-directory: ./test 90 | 91 | - name: Check logs 92 | run: docker compose logs 93 | working-directory: ./test 94 | 95 | - name: Run tests 96 | run: npm test 97 | timeout-minutes: 5 98 | env: 99 | GITHUB_ACTIONS_CI: true 100 | ADAPTER: ${{ matrix.adapter }} 101 | 102 | # - name: Run leak detection tests 103 | # run: npm run test:leak 104 | # env: 105 | # GITHUB_ACTIONS_CI: true 106 | 107 | - name: Show container logs (in case of failure) 108 | run: docker compose logs 109 | if: failure() 110 | working-directory: ./test 111 | 112 | - name: Stop containers 113 | run: docker compose down -v 114 | working-directory: ./test 115 | 116 | # - name: Upload code coverage 117 | # run: npm run coverall 118 | # if: success() && github.ref == 'refs/heads/master' 119 | # env: 120 | # COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_TOKEN }} 121 | -------------------------------------------------------------------------------- /.github/workflows/notification.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: Discord Notification 4 | 5 | on: 6 | release: 7 | types: 8 | - published 9 | 10 | jobs: 11 | notify: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Discord notification 16 | env: 17 | DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} 18 | uses: Ilshidur/action-discord@master 19 | with: 20 | args: ":tada: **The {{ EVENT_PAYLOAD.repository.name }} {{ EVENT_PAYLOAD.release.tag_name }} has been released.**:tada:\nChangelog: {{EVENT_PAYLOAD.release.html_url}}" 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # Sqlite DB files 107 | *.db-journal 108 | *.sqlite3 109 | *.sqlite3-journal 110 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | .github/ 3 | assets/ 4 | benchmark/ 5 | coverage/ 6 | dev/ 7 | docs/ 8 | example/ 9 | examples/ 10 | typings/ 11 | test/ 12 | *.db 13 | .editorconfig 14 | .eslintrc.js 15 | prettier.config.js 16 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible Node.js debug attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | 8 | { 9 | "type": "node", 10 | "request": "launch", 11 | "name": "Launch demo", 12 | "program": "examples/index.js", 13 | "cwd": "${workspaceRoot}", 14 | "args": [ 15 | "simple" 16 | ], 17 | "console": "integratedTerminal", 18 | "env": { 19 | //"ADAPTER": "kafka://localhost:9093" 20 | } 21 | }, 22 | { 23 | "name": "Attach by Process ID", 24 | "processId": "${command:PickProcess}", 25 | "request": "attach", 26 | "skipFiles": [ 27 | "/**" 28 | ], 29 | "type": "pwa-node" 30 | }, 31 | { 32 | "type": "node", 33 | "request": "launch", 34 | "name": "Jest", 35 | "program": "${workspaceRoot}/node_modules/jest-cli/bin/jest.js", 36 | "args": ["--testMatch", "\"**/integration/**/*.spec.js\"", "--runInBand"], 37 | "cwd": "${workspaceRoot}", 38 | "runtimeArgs": [ 39 | "--nolazy" 40 | ] 41 | }, 42 | { 43 | "type": "node", 44 | "request": "launch", 45 | "name": "Jest single", 46 | "program": "${workspaceRoot}/node_modules/jest-cli/bin/jest.js", 47 | "args": ["--runInBand", "${fileBasenameNoExtension}"], 48 | "console": "internalConsole", 49 | "cwd": "${workspaceRoot}", 50 | "runtimeArgs": [ 51 | "--nolazy" 52 | ] 53 | }, 54 | ] 55 | } 56 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "jest.autoRun": "off" 3 | } 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # [0.2.0](https://github.com/moleculerjs/moleculer-channels/compare/v0.1.8...v0.2.0) (2025-05-31) 4 | 5 | - Minimum Node version is 20.x 6 | - Fixing Redis connection issue at starting when Redis server is not running. 7 | - Add NATS reconnecting at starting when NATS server is not running. 8 | - update dependencies 9 | 10 | 11 | 12 | # [0.1.8](https://github.com/moleculerjs/moleculer-channels/compare/v0.1.7...v0.1.8) (2023-08-06) 13 | 14 | - chore(types): update MiddlewareOptions [72](https://github.com/moleculerjs/moleculer-channels/pull/72). 15 | - Support Redis capped streams [70](https://github.com/moleculerjs/moleculer-channels/pull/70). 16 | 17 | 18 | 19 | # [0.1.7](https://github.com/moleculerjs/moleculer-channels/compare/v0.1.6...v0.1.7) (2023-07-15) 20 | 21 | - add options for enable one-time assertExchange calling [63](https://github.com/moleculerjs/moleculer-channels/pull/63). 22 | - asserting dead-letter exchange and queue [68](https://github.com/moleculerjs/moleculer-channels/pull/68). 23 | 24 | 25 | 26 | # [0.1.6](https://github.com/moleculerjs/moleculer-channels/compare/v0.1.5...v0.1.6) (2023-02-26) 27 | 28 | - add Context-based handlers [64](https://github.com/moleculerjs/moleculer-channels/pull/64). Read more about [here](https://github.com/moleculerjs/moleculer-channels#context-based-messages) 29 | 30 | 31 | 32 | # [0.1.5](https://github.com/moleculerjs/moleculer-channels/compare/v0.1.4...v0.1.5) (2023-02-19) 33 | 34 | - fix emitLocalChannelHandler [62](https://github.com/moleculerjs/moleculer-channels/pull/62) 35 | - enforce buffer type on data passed to serializer [58](https://github.com/moleculerjs/moleculer-channels/pull/58) 36 | 37 | 38 | 39 | # [0.1.4](https://github.com/moleculerjs/moleculer-channels/compare/v0.1.3...v0.1.4) (2023-01-08) 40 | 41 | - allow to subscribe with wildcard in NATS JetStream adapter [57](https://github.com/moleculerjs/moleculer-channels/pull/57) 42 | - update dependencies 43 | 44 | 45 | 46 | # [0.1.3](https://github.com/moleculerjs/moleculer-channels/compare/v0.1.2...v0.1.3) (2022-12-17) 47 | 48 | - add `Fake` adapter based on Moleculer built-in events. 49 | - support amqplib v0.9, kafkajs v2, ioredis v5 50 | 51 | 52 | 53 | # [0.1.2](https://github.com/moleculerjs/moleculer-channels/compare/v0.1.1...v0.1.2) (2022-01-08) 54 | 55 | - update deps. 56 | - add emitLocalChannelHandler method. [#34](https://github.com/moleculerjs/moleculer-channels/pull/34) 57 | 58 | 59 | 60 | # [0.1.1](https://github.com/moleculerjs/moleculer-channels/compare/v0.1.0...v0.1.1) (2021-12-28) 61 | 62 | - Added Typescript support. 63 | - Added `connection` flag that prevents publishing events before the adapter is connected. 64 | - Added metrics. 65 | 66 | 67 | 68 | # v0.1.0 (2021-10-17) 69 | 70 | First public version. 71 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 MoleculerJS 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. 22 | -------------------------------------------------------------------------------- /assets/dead_letter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moleculerjs/moleculer-channels/3929e43cb36c50d52152aba9625614255ab8bd4b/assets/dead_letter.png -------------------------------------------------------------------------------- /assets/legacy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moleculerjs/moleculer-channels/3929e43cb36c50d52152aba9625614255ab8bd4b/assets/legacy.png -------------------------------------------------------------------------------- /assets/overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moleculerjs/moleculer-channels/3929e43cb36c50d52152aba9625614255ab8bd4b/assets/overview.png -------------------------------------------------------------------------------- /assets/redis_queue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moleculerjs/moleculer-channels/3929e43cb36c50d52152aba9625614255ab8bd4b/assets/redis_queue.png -------------------------------------------------------------------------------- /benchmark/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | node: true, 4 | commonjs: true, 5 | es6: true 6 | }, 7 | extends: ["eslint:recommended"], 8 | parserOptions: { 9 | sourceType: "module", 10 | ecmaVersion: 2018 11 | }, 12 | rules: { 13 | indent: ["warn", "tab", { SwitchCase: 1 }], 14 | quotes: ["warn", "double"], 15 | semi: ["error", "always"], 16 | "no-var": ["warn"], 17 | "no-console": ["off"], 18 | "no-unused-vars": ["off"], 19 | "no-trailing-spaces": ["error"], 20 | "security/detect-possible-timing-attacks": ["off"] 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /benchmark/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | require("./suites/" + (process.argv[2] || "latency")); 4 | -------------------------------------------------------------------------------- /benchmark/suites/latency.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | //const Benchmarkify = require("benchmarkify"); 4 | const _ = require("lodash"); 5 | const kleur = require("kleur"); 6 | const { ServiceBroker } = require("moleculer"); 7 | const { polyfillPromise, humanize } = require("moleculer").Utils; 8 | const ChannelsMiddleware = require("../..").Middleware; 9 | //const { writeResult } = require("../utils"); 10 | //const { generateMarkdown } = require("../generate-result"); 11 | polyfillPromise(Promise); 12 | 13 | const Adapters = [ 14 | { type: "Redis", options: {} }, 15 | { 16 | type: "Redis", 17 | name: "RedisCluster", 18 | options: { 19 | cluster: { 20 | nodes: [ 21 | { host: "127.0.0.1", port: 6381 }, 22 | { host: "127.0.0.1", port: 6382 }, 23 | { host: "127.0.0.1", port: 6383 } 24 | ] 25 | } 26 | } 27 | }, 28 | { type: "AMQP", options: {} }, 29 | { type: "NATS", options: {} }, 30 | { type: "Kafka", options: { kafka: { brokers: ["localhost:9093"] } } } 31 | ]; 32 | 33 | Promise.mapSeries(Adapters, async adapterDef => { 34 | const adapterName = adapterDef.name || adapterDef.type; 35 | console.log(kleur.yellow().bold(`Start benchmark... Adapter: ${adapterName}`)); 36 | 37 | const broker = new ServiceBroker({ 38 | namespace: `bench-${adapterName}`, 39 | logLevel: "warn", 40 | middlewares: [ChannelsMiddleware({ adapter: adapterDef })] 41 | }); 42 | 43 | let count = 0; 44 | function doPublish() { 45 | count++; 46 | broker.sendToChannel("bench.topic", { c: count }); 47 | } 48 | 49 | broker.createService({ 50 | name: "consumer", 51 | channels: { 52 | "bench.topic": { 53 | handler() { 54 | doPublish(); 55 | } 56 | } 57 | } 58 | }); 59 | 60 | await broker.start(); 61 | 62 | let cycle = 0; 63 | const cycleRps = []; 64 | await new Promise(resolve => { 65 | setTimeout(() => { 66 | let startTime = Date.now(); 67 | 68 | let timer = setInterval(() => { 69 | const rps = count / ((Date.now() - startTime) / 1000); 70 | cycleRps.push(rps); 71 | console.log( 72 | " ", 73 | rps.toLocaleString("hu-HU", { maximumFractionDigits: 0 }), 74 | "msg/s" 75 | ); 76 | count = 0; 77 | startTime = Date.now(); 78 | cycle++; 79 | if (cycle >= 10) { 80 | clearInterval(timer); 81 | resolve(); 82 | } 83 | }, 1000); 84 | 85 | broker.waitForServices(["consumer"]).then(() => doPublish()); 86 | }, 1000); 87 | }); 88 | 89 | const avg = _.mean(cycleRps); 90 | console.log(kleur.magenta().bold("------------------")); 91 | console.log( 92 | kleur 93 | .magenta() 94 | .bold(`Average: ${avg.toLocaleString("hu-HU", { maximumFractionDigits: 0 })} msg/s`) 95 | ); 96 | console.log(""); 97 | console.log(kleur.magenta().bold(`Latency: ${humanize(1000 / avg)}`)); 98 | console.log(""); 99 | adapterDef.avg = avg; 100 | 101 | await broker.stop(); 102 | }); 103 | -------------------------------------------------------------------------------- /benchmark/suites/throughput.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | //const Benchmarkify = require("benchmarkify"); 4 | const _ = require("lodash"); 5 | const kleur = require("kleur"); 6 | const { ServiceBroker } = require("moleculer"); 7 | const { polyfillPromise, humanize } = require("moleculer").Utils; 8 | const ChannelsMiddleware = require("../..").Middleware; 9 | //const { writeResult } = require("../utils"); 10 | //const { generateMarkdown } = require("../generate-result"); 11 | polyfillPromise(Promise); 12 | 13 | const Adapters = [ 14 | { type: "Redis", options: {} }, 15 | { 16 | type: "Redis", 17 | name: "RedisCluster", 18 | options: { 19 | cluster: { 20 | nodes: [ 21 | { host: "127.0.0.1", port: 6381 }, 22 | { host: "127.0.0.1", port: 6382 }, 23 | { host: "127.0.0.1", port: 6383 } 24 | ] 25 | } 26 | } 27 | }, 28 | { type: "AMQP", options: {} }, 29 | { type: "NATS", options: {} }, 30 | { type: "Kafka", options: { kafka: { brokers: ["localhost:9093"] } } } 31 | ]; 32 | 33 | Promise.mapSeries(Adapters, async adapterDef => { 34 | const adapterName = adapterDef.name || adapterDef.type; 35 | console.log(kleur.yellow().bold(`Start benchmark... Adapter: ${adapterName}`)); 36 | 37 | const broker = new ServiceBroker({ 38 | namespace: `bench-${adapterName}`, 39 | logLevel: "warn", 40 | middlewares: [ 41 | ChannelsMiddleware({ 42 | adapter: _.defaultsDeep({ options: { maxInFlight: 10 } }, adapterDef) 43 | }) 44 | ] 45 | }); 46 | const MAX = 10 * 1000; 47 | let startTime; 48 | let done; 49 | let count = 0; 50 | 51 | broker.createService({ 52 | name: "consumer", 53 | channels: { 54 | "bench.topic": { 55 | handler() { 56 | count++; 57 | if (count == MAX) { 58 | const diff = process.hrtime(startTime); 59 | const ms = diff[0] * 1e3 + diff[1] * 1e-6; 60 | console.log(` ${count} messages processed in ${humanize(ms)}`); 61 | const sec = diff[0] + diff[1] / 1e9; 62 | const thr = MAX / sec; 63 | console.log(kleur.yellow().bold("------------------")); 64 | console.log( 65 | kleur.yellow().bold( 66 | `Throughput: ${thr.toLocaleString("hu-HU", { 67 | maximumFractionDigits: 0 68 | })} msg/sec` 69 | ) 70 | ); 71 | console.log(""); 72 | adapterDef.thr = thr; 73 | done(); 74 | } 75 | } 76 | } 77 | } 78 | }); 79 | 80 | await broker.start(); 81 | 82 | await new Promise(resolve => { 83 | setTimeout(async () => { 84 | startTime = process.hrtime(); 85 | done = resolve; 86 | 87 | console.log(` Start publish ${MAX} messages...`); 88 | for (let i = 0; i < MAX; i++) { 89 | try { 90 | await broker.sendToChannel("bench.topic", { count: i }); 91 | } catch (err) { 92 | if (err.message.endsWith("is full.")) { 93 | console.debug(" Buffer is full. Wait..."); 94 | await Promise.delay(10); 95 | } else { 96 | console.error( 97 | `Unable to send at ${i} of ${MAX}. Received: ${count}`, 98 | err.message 99 | ); 100 | process.exit(1); 101 | } 102 | } 103 | } 104 | console.log(" Waiting for consumers..."); 105 | }, 1000); 106 | }); 107 | 108 | await broker.stop(); 109 | }); 110 | -------------------------------------------------------------------------------- /examples/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | "no-console": 0, 4 | "no-unused-vars": 0 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /examples/channel-name/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { ServiceBroker } = require("moleculer"); 4 | const ChannelsMiddleware = require("../..").Middleware; 5 | const TracingMiddleware = require("../..").Tracing; 6 | 7 | let c = 1; 8 | 9 | // Create broker 10 | const broker = new ServiceBroker({ 11 | logLevel: { 12 | CHANNELS: "info", 13 | "**": "info" 14 | }, 15 | middlewares: [ 16 | ChannelsMiddleware({ 17 | // adapter: { 18 | // type: "Fake" 19 | // }, 20 | /*adapter: { 21 | type: "Kafka", 22 | options: { kafka: { brokers: ["localhost:9093"] } } 23 | },*/ 24 | /*adapter: { 25 | type: "AMQP" 26 | },*/ 27 | adapter: { 28 | type: "NATS" 29 | }, 30 | /* 31 | adapter: { 32 | type: "Redis", 33 | options: { 34 | redis: "localhost:6379" 35 | //serializer: "MsgPack" 36 | } 37 | }, 38 | */ 39 | context: true 40 | }), 41 | TracingMiddleware() 42 | ], 43 | replCommands: [ 44 | // { 45 | // command: "publish", 46 | // alias: ["p"], 47 | // async action(broker, args) { 48 | // const payload = { 49 | // id: ++c, 50 | // name: "Jane Doe", 51 | // pid: process.pid 52 | // }; 53 | // await broker.call( 54 | // "publisher.publish", 55 | // { payload, headers: { a: "123" } }, 56 | // { 57 | // meta: { 58 | // loggedInUser: { 59 | // id: 12345, 60 | // name: "John Doe", 61 | // roles: ["admin"], 62 | // status: true 63 | // } 64 | // } 65 | // } 66 | // ); 67 | // } 68 | // } 69 | ] 70 | }); 71 | 72 | broker.createService({ 73 | name: "publisher", 74 | actions: { 75 | async publish(ctx) { 76 | const parentChannelName = ctx.parentChannelName; 77 | const level = ctx.level; 78 | const caller = ctx.caller; 79 | const msg = `Flow level: ${level}, Type: Action, Name: ${ctx.action.name}, Caller: ${caller}, Parent channel: ${parentChannelName}`; 80 | this.logger.info(msg); 81 | 82 | await broker.sendToChannel("my.topic.level.2", ctx.params.payload, { 83 | ctx, 84 | headers: ctx.params.headers 85 | }); 86 | 87 | await broker.Promise.delay(1000); 88 | } 89 | } 90 | }); 91 | 92 | broker.createService({ 93 | name: "sub2", 94 | channels: { 95 | "my.topic.level.2": { 96 | async handler(ctx, raw) { 97 | const parentChannelName = ctx.parentChannelName; 98 | const level = ctx.level; 99 | const caller = ctx.caller; 100 | const msg = `Flow level: ${level}, Type: Channel, Name: ${ctx.channelName}, Caller: ${caller}, Parent channel: ${parentChannelName}`; 101 | this.logger.info(msg); 102 | 103 | await Promise.delay(100); 104 | 105 | const headers = this.broker.channelAdapter.parseMessageHeaders(raw); 106 | 107 | await broker.sendToChannel("my.topic.level.3", ctx.params, { 108 | ctx, 109 | headers 110 | }); 111 | } 112 | } 113 | } 114 | }); 115 | 116 | broker.createService({ 117 | name: "sub3", 118 | channels: { 119 | "my.topic.level.3": { 120 | async handler(ctx, raw) { 121 | const parentChannelName = ctx.parentChannelName; 122 | const level = ctx.level; 123 | const caller = ctx.caller; 124 | const msg = `Flow level: ${level}, Type: Channel, Name: ${ctx.channelName}, Caller: ${caller}, Parent channel: ${parentChannelName}`; 125 | this.logger.info(msg); 126 | 127 | await Promise.delay(100); 128 | 129 | const headers = this.broker.channelAdapter.parseMessageHeaders(raw); 130 | 131 | await broker.sendToChannel("my.topic.level.4", ctx.params, { 132 | ctx, 133 | headers 134 | }); 135 | } 136 | } 137 | } 138 | }); 139 | 140 | broker.createService({ 141 | name: "sub4", 142 | channels: { 143 | "my.topic.level.4": { 144 | async handler(ctx, raw) { 145 | const parentChannelName = ctx.parentChannelName; 146 | const level = ctx.level; 147 | const caller = ctx.caller; 148 | const msg = `Flow level: ${level}, Type: Channel, Name: ${ctx.channelName}, Caller: ${caller}, Parent channel: ${parentChannelName}`; 149 | this.logger.info(msg); 150 | 151 | await Promise.delay(100); 152 | 153 | const headers = this.broker.channelAdapter.parseMessageHeaders(raw); 154 | 155 | await broker.sendToChannel("my.topic.level.5", ctx.params, { 156 | ctx, 157 | headers 158 | }); 159 | } 160 | }, 161 | 162 | "my.topic.level.5": { 163 | async handler(ctx, raw) { 164 | const parentChannelName = ctx.parentChannelName; 165 | const level = ctx.level; 166 | const caller = ctx.caller; 167 | const msg = `Flow level: ${level}, Type: Channel, Name: ${ctx.channelName}, Caller: ${caller}, Parent channel: ${parentChannelName}`; 168 | this.logger.info(msg); 169 | 170 | await Promise.delay(100); 171 | 172 | await ctx.call("test.demo.level.6", null, { parentCtx: ctx }); 173 | } 174 | } 175 | } 176 | }); 177 | 178 | broker.createService({ 179 | name: "test", 180 | actions: { 181 | "demo.level.6": { 182 | async handler(ctx) { 183 | const channelName = ctx?.options?.parentCtx?.channelName; 184 | const level = ctx.level; 185 | const caller = ctx.caller; 186 | const msg = `Flow level: ${level}, Type: Action, Name: ${ctx.action.name}, Caller: ${caller}, Channel name: ${channelName}`; 187 | this.logger.info(msg); 188 | // this.logger.info("Demo service called", ctx); 189 | } 190 | } 191 | } 192 | }); 193 | 194 | broker 195 | .start() 196 | .then(async () => { 197 | broker.repl(); 198 | 199 | const payload = { 200 | id: ++c, 201 | name: "Jane Doe", 202 | pid: process.pid 203 | }; 204 | 205 | broker.logger.info("Initializing the flow..."); 206 | 207 | await broker.call( 208 | "publisher.publish", 209 | { payload, headers: { a: "123" } }, 210 | { 211 | meta: { 212 | loggedInUser: { 213 | id: 12345, 214 | name: "John Doe", 215 | roles: ["admin"], 216 | status: true 217 | } 218 | } 219 | } 220 | ); 221 | }) 222 | .catch(err => { 223 | broker.logger.error(err); 224 | broker.stop(); 225 | }); 226 | -------------------------------------------------------------------------------- /examples/context/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { ServiceBroker } = require("moleculer"); 4 | const ChannelsMiddleware = require("../..").Middleware; 5 | const TracingMiddleware = require("../..").Tracing; 6 | 7 | let c = 1; 8 | 9 | // Create broker 10 | const broker = new ServiceBroker({ 11 | logLevel: { 12 | CHANNELS: "debug", 13 | "**": "info" 14 | }, 15 | tracing: { 16 | enabled: true, 17 | exporter: [{ type: "Console" }, { type: "Event" }] 18 | }, 19 | middlewares: [ 20 | ChannelsMiddleware({ 21 | adapter: { 22 | type: "Fake" 23 | }, 24 | /*adapter: { 25 | type: "Kafka", 26 | options: { kafka: { brokers: ["localhost:9093"] } } 27 | },*/ 28 | /*adapter: { 29 | type: "AMQP" 30 | },*/ 31 | /*adapter: { 32 | type: "NATS" 33 | },*/ 34 | /* 35 | adapter: { 36 | type: "Redis", 37 | options: { 38 | redis: "localhost:6379" 39 | //serializer: "MsgPack" 40 | } 41 | }, 42 | */ 43 | context: true 44 | }), 45 | TracingMiddleware() 46 | ], 47 | replCommands: [ 48 | { 49 | command: "publish", 50 | alias: ["p"], 51 | async action(broker, args) { 52 | const payload = { 53 | id: ++c, 54 | name: "Jane Doe", 55 | pid: process.pid 56 | }; 57 | 58 | await broker.call( 59 | "publisher.publish", 60 | { payload, headers: { a: "123" } }, 61 | { 62 | meta: { 63 | loggedInUser: { 64 | id: 12345, 65 | name: "John Doe", 66 | roles: ["admin"], 67 | status: true 68 | } 69 | } 70 | } 71 | ); 72 | } 73 | } 74 | ] 75 | }); 76 | 77 | broker.createService({ 78 | name: "publisher", 79 | actions: { 80 | async publish(ctx) { 81 | await broker.sendToChannel("my.topic", ctx.params.payload, { 82 | ctx, 83 | headers: ctx.params.headers 84 | }); 85 | 86 | await broker.Promise.delay(1000); 87 | } 88 | } 89 | }); 90 | 91 | broker.createService({ 92 | name: "sub1", 93 | channels: { 94 | "my.topic": { 95 | //context: true, 96 | tracing: { 97 | //spanName: ctx => `My custom span: ${ctx.params.id}`, 98 | tags: { 99 | params: true, 100 | meta: true 101 | } 102 | }, 103 | async handler(ctx, raw) { 104 | this.logger.info("Processing...", ctx); 105 | this.logger.info("RAW:", raw); 106 | 107 | await Promise.delay(100); 108 | 109 | await ctx.call("test.demo"); 110 | 111 | this.logger.info("Processed!", ctx.params, ctx.meta); 112 | } 113 | } 114 | } 115 | }); 116 | 117 | broker.createService({ 118 | name: "test", 119 | actions: { 120 | async demo(ctx) { 121 | this.logger.info("Demo service called"); 122 | } 123 | } 124 | }); 125 | 126 | broker.createService({ 127 | name: "event-handler", 128 | events: { 129 | "$tracing.spans": { 130 | tracing: false, 131 | handler(ctx) { 132 | this.logger.info("Tracing event received"); 133 | ctx.params.forEach(span => this.logger.info(span)); 134 | } 135 | } 136 | } 137 | }); 138 | 139 | broker 140 | .start() 141 | .then(async () => { 142 | broker.repl(); 143 | }) 144 | .catch(err => { 145 | broker.logger.error(err); 146 | broker.stop(); 147 | }); 148 | -------------------------------------------------------------------------------- /examples/dead-letter/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { 4 | ServiceBroker, 5 | Errors: { MoleculerError } 6 | } = require("moleculer"); 7 | const ChannelsMiddleware = require("../..").Middleware; 8 | 9 | let c = 1; 10 | 11 | // Create broker 12 | const broker = new ServiceBroker({ 13 | logLevel: { 14 | CHANNELS: "debug", 15 | "**": "info" 16 | }, 17 | middlewares: [ 18 | ChannelsMiddleware({ 19 | adapter: process.env.ADAPTER || "redis://localhost:6379" 20 | }) 21 | ], 22 | replOptions: { 23 | customCommands: [ 24 | { 25 | command: "publish", 26 | alias: ["p"], 27 | async action(broker, args) { 28 | const payload = { 29 | id: 2, 30 | name: "Jane Doe", 31 | status: false, 32 | count: ++c, 33 | pid: process.pid 34 | }; 35 | 36 | await broker.sendToChannel("my.fail.topic", payload, { 37 | key: "" + c, 38 | headers: { a: "123" } 39 | }); 40 | } 41 | } 42 | ] 43 | } 44 | }); 45 | 46 | broker.createService({ 47 | name: "sub1", 48 | channels: { 49 | "my.fail.topic": { 50 | group: "failgroup", 51 | redis: { 52 | minIdleTime: 1000, 53 | claimInterval: 500 54 | }, 55 | maxRetries: 3, 56 | deadLettering: { 57 | enabled: true, 58 | queueName: "DEAD_LETTER", 59 | exchangeName: "DEAD_LETTER" 60 | }, 61 | handler() { 62 | this.logger.error("Ups! Something happened"); 63 | return Promise.reject( 64 | new MoleculerError("Something happened", 123, "SOMETHING_ERROR", { 65 | errorInfo: "Additional error information", 66 | someOtherErrorData: 456, 67 | anotherErrorField: true, 68 | finalField: null, 69 | nested: { a: 1, b: "two", c: false } 70 | }) 71 | ); 72 | } 73 | } 74 | } 75 | }); 76 | 77 | /** 78 | broker.createService({ 79 | name: "sub2", 80 | channels: { 81 | "my.fail.topic": { 82 | group: "goodgroup", 83 | handler(msg) { 84 | this.logger.info(">>> I processed", msg); 85 | } 86 | } 87 | } 88 | }); 89 | */ 90 | 91 | broker.createService({ 92 | name: "sub3", 93 | channels: { 94 | DEAD_LETTER: { 95 | context: true, 96 | group: "failgroup", 97 | handler(ctx, raw) { 98 | this.logger.info("--> FAILED HANDLER PARAMS <--"); 99 | this.logger.info(ctx.params); 100 | this.logger.info("--> FAILED HANDLER HEADERS <--"); 101 | this.logger.info(ctx.headers); 102 | 103 | // Send a notification about the failure 104 | 105 | this.logger.info("--> RAW (ENTIRE) MESSAGE <--"); 106 | this.logger.info(raw); 107 | } 108 | } 109 | } 110 | }); 111 | 112 | broker 113 | .start() 114 | .then(() => { 115 | broker.repl(); 116 | }) 117 | .catch(err => { 118 | broker.logger.error(err); 119 | broker.stop(); 120 | }); 121 | -------------------------------------------------------------------------------- /examples/external-message/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // Based on the example from: https://github.com/moleculerjs/moleculer-channels/issues/76 4 | 5 | const { ServiceBroker } = require("moleculer"); 6 | const ChannelsMiddleware = require("../..").Middleware; 7 | const { jetstream } = require("nats"); 8 | const { connect } = require("nats"); 9 | 10 | let counter = 1; 11 | 12 | // pointer to the NATS connection 13 | let nc = null; 14 | 15 | const broker = new ServiceBroker({ 16 | namespace: "uat", 17 | logLevel: { 18 | CHANNELS: "debug", 19 | "**": "info" 20 | }, 21 | middlewares: [ 22 | ChannelsMiddleware({ 23 | adapter: "NATS" 24 | }) 25 | ], 26 | replCommands: [ 27 | { 28 | command: "publish", 29 | alias: ["p"], 30 | async action(broker, args) { 31 | const { options } = args; 32 | //console.log(options); 33 | await broker.sendToChannel( 34 | "my.first.topic", 35 | { 36 | id: 2, 37 | name: "Jane Doe", 38 | status: false, 39 | count: ++counter, 40 | pid: process.pid 41 | }, 42 | { key: "" + counter, headers: { a: "something" } } 43 | ); 44 | } 45 | }, 46 | { 47 | command: "natsJSON", 48 | alias: ["nj"], 49 | async action(broker, args) { 50 | const { options } = args; 51 | //console.log(options); 52 | try { 53 | nc.publish( 54 | "uat.my.first.topic", 55 | JSON.stringify({ 56 | id: 2, 57 | name: "Jane Doe", 58 | status: false, 59 | count: ++counter, 60 | pid: process.pid 61 | }) 62 | ); 63 | } catch (error) { 64 | console.error(error); 65 | } 66 | } 67 | }, 68 | { 69 | command: "natsString", 70 | alias: ["ns"], 71 | async action(broker, args) { 72 | const { options } = args; 73 | //console.log(options); 74 | try { 75 | nc.publish("uat.my.first.topic", "Hello World"); 76 | } catch (error) { 77 | console.error(error); 78 | } 79 | } 80 | }, 81 | { 82 | command: "natsStringify", 83 | alias: ["nsf"], 84 | async action(broker, args) { 85 | const { options } = args; 86 | //console.log(options); 87 | try { 88 | nc.publish("uat.my.first.topic", JSON.stringify("Hello World")); 89 | } catch (error) { 90 | console.error(error); 91 | } 92 | } 93 | } 94 | ], 95 | async started() { 96 | try { 97 | nc = await connect({ servers: "nats://localhost:4222" }); 98 | } catch (error) { 99 | console.error(error); 100 | } 101 | }, 102 | async stopped() { 103 | if (nc) { 104 | try { 105 | await nc.close(); 106 | } catch (error) { 107 | console.error(error); 108 | } 109 | } 110 | } 111 | }); 112 | 113 | broker.createService({ 114 | name: "posts", 115 | version: 1, 116 | channels: { 117 | async "my.first.topic"(msg, raw) { 118 | this.logger.info("[POSTS] Channel One msg received", msg, raw.headers); 119 | } 120 | } 121 | }); 122 | 123 | broker.start().then(async () => { 124 | broker.repl(); 125 | }); 126 | -------------------------------------------------------------------------------- /examples/headers/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { ServiceBroker } = require("moleculer"); 4 | const ChannelsMiddleware = require("../..").Middleware; 5 | 6 | let c = 1; 7 | 8 | // Create broker 9 | const broker = new ServiceBroker({ 10 | logLevel: { 11 | CHANNELS: "debug", 12 | "**": "info" 13 | }, 14 | metrics: { 15 | enabled: true, 16 | reporter: { 17 | type: "Console", 18 | options: { 19 | includes: ["moleculer.channels.**"] 20 | } 21 | } 22 | }, 23 | middlewares: [ 24 | ChannelsMiddleware({ 25 | adapter: { 26 | type: "Redis", 27 | options: { 28 | redis: "localhost:6379", 29 | serializer: "MsgPack" 30 | } 31 | } 32 | }) 33 | ], 34 | replCommands: [ 35 | { 36 | command: "publish", 37 | alias: ["p"], 38 | async action(broker, args) { 39 | const payload = { 40 | id: 2, 41 | name: "Jane Doe", 42 | status: false, 43 | count: ++c, 44 | pid: process.pid 45 | }; 46 | 47 | await broker.sendToChannel("my.fail.topic", payload, { headers: { a: "123" } }); 48 | } 49 | } 50 | ] 51 | }); 52 | 53 | broker.createService({ 54 | name: "sub1", 55 | channels: { 56 | "my.fail.topic": { 57 | group: "failgroup", 58 | redis: { 59 | minIdleTime: 1000, 60 | claimInterval: 500 61 | }, 62 | maxRetries: 2, 63 | deadLettering: { 64 | enabled: true, 65 | queueName: "DEAD_LETTER", 66 | exchangeName: "DEAD_LETTER" 67 | }, 68 | handler(msg, raw) { 69 | this.logger.error("Processing...", msg); 70 | 71 | this.logger.error("Ups! Something happened"); 72 | return Promise.reject(new Error("Something happened")); 73 | } 74 | } 75 | } 76 | }); 77 | 78 | broker.createService({ 79 | name: "sub2", 80 | channels: { 81 | DEAD_LETTER: { 82 | group: "failgroup", 83 | handler(msg, raw) { 84 | this.logger.info("--> FAILED HANDLER <--"); 85 | this.logger.info(msg); 86 | // Send a notification about the failure 87 | 88 | this.logger.info("--> RAW (ENTIRE) MESSAGE <--"); 89 | this.logger.info(raw); 90 | } 91 | } 92 | } 93 | }); 94 | 95 | broker 96 | .start() 97 | .then(async () => { 98 | broker.repl(); 99 | 100 | const payload = { 101 | id: 2, 102 | name: "Jane Doe", 103 | status: false, 104 | count: ++c, 105 | pid: process.pid 106 | }; 107 | 108 | // await broker.sendToChannel("my.fail.topic", payload, { headers: { a: "123" } }); 109 | }) 110 | .catch(err => { 111 | broker.logger.error(err); 112 | broker.stop(); 113 | }); 114 | -------------------------------------------------------------------------------- /examples/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const moduleName = process.argv[2] || "simple"; 4 | process.argv.splice(2, 1); 5 | 6 | require("./" + moduleName); 7 | -------------------------------------------------------------------------------- /examples/maxInFlight/index.js: -------------------------------------------------------------------------------- 1 | const _ = require("lodash"); 2 | const { ServiceBroker } = require("moleculer"); 3 | const ChannelsMiddleware = require("../../").Middleware; 4 | 5 | let FLOW = []; 6 | let id = 0; 7 | 8 | const broker = new ServiceBroker({ 9 | logLevel: { 10 | CHANNELS: "debug", 11 | "**": "info" 12 | }, 13 | middlewares: [ 14 | ChannelsMiddleware({ 15 | adapter: process.env.ADAPTER || "redis://localhost:6379" 16 | }) 17 | ], 18 | 19 | replCommands: [ 20 | { 21 | command: "pub0", 22 | alias: ["p0"], 23 | async action(broker, args) { 24 | const { options } = args; 25 | //console.log(options); 26 | await Promise.all( 27 | _.times(5, i => broker.sendToChannel("test.mif.topic", { id: i })) 28 | ); 29 | } 30 | }, 31 | { 32 | command: "pub1", 33 | alias: ["p1"], 34 | async action(broker, args) { 35 | const { options } = args; 36 | //console.log(options); 37 | await broker.sendToChannel("test.mif.topic", { 38 | id: id++, 39 | name: "test.mif.topic", 40 | pid: process.pid 41 | }); 42 | } 43 | }, 44 | { 45 | command: "pub2", 46 | alias: ["p2"], 47 | async action(broker, args) { 48 | console.log(FLOW); 49 | 50 | FLOW = []; 51 | id = 0; 52 | } 53 | } 54 | ] 55 | }); 56 | 57 | broker.createService({ 58 | name: "sub1", 59 | channels: { 60 | "test.mif.topic": { 61 | maxInFlight: 1, 62 | async handler(payload) { 63 | const consumerID = Array.from(this.broker.channelAdapter.activeMessages.keys())[0]; 64 | 65 | console.log( 66 | `----> Processing ${ 67 | payload.id 68 | } || Active messages ${this.broker.channelAdapter.getNumberOfChannelActiveMessages( 69 | consumerID 70 | )} <----` 71 | ); 72 | 73 | FLOW.push(`BEGIN: ${payload.id}`); 74 | await this.Promise.delay(300); 75 | FLOW.push(`END: ${payload.id}`); 76 | } 77 | } 78 | } 79 | }); 80 | 81 | broker 82 | .start() 83 | .then(() => { 84 | broker.repl(); 85 | }) 86 | .catch(err => { 87 | broker.logger.error(err); 88 | broker.stop(); 89 | }); 90 | -------------------------------------------------------------------------------- /examples/metrics/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { ServiceBroker } = require("moleculer"); 4 | const ChannelsMiddleware = require("../../").Middleware; 5 | 6 | let c = 1; 7 | 8 | // Create broker 9 | const broker = new ServiceBroker({ 10 | logLevel: { 11 | //CHANNELS: "debug", 12 | "**": "info" 13 | }, 14 | metrics: { 15 | enabled: true, 16 | reporter: { 17 | type: "Console", 18 | options: { 19 | includes: ["moleculer.channels.**"] 20 | } 21 | } 22 | }, 23 | 24 | middlewares: [ 25 | ChannelsMiddleware({ 26 | adapter: process.env.ADAPTER || "redis://localhost:6379" 27 | }) 28 | ], 29 | replCommands: [ 30 | { 31 | command: "publish", 32 | alias: ["p"], 33 | async action(broker, args) { 34 | const { options } = args; 35 | //console.log(options); 36 | await broker.sendToChannel( 37 | "my.first.topic", 38 | { 39 | id: 2, 40 | name: "Jane Doe", 41 | status: false, 42 | count: ++c, 43 | pid: process.pid 44 | }, 45 | { key: "" + c, headers: { a: "something" } } 46 | ); 47 | } 48 | }, 49 | { 50 | command: "publish2", 51 | alias: ["p2"], 52 | async action(broker, args) { 53 | const { options } = args; 54 | //console.log(options); 55 | await broker.sendToChannel("my.second.topic", { 56 | id: 2, 57 | name: "Jane Doe", 58 | status: true, 59 | pid: process.pid 60 | }); 61 | } 62 | } 63 | ] 64 | }); 65 | 66 | broker.createService({ 67 | name: "posts", 68 | version: 1, 69 | channels: { 70 | async "my.first.topic"(msg, raw) { 71 | this.logger.info("[POSTS] Channel One msg received", msg, raw.key, raw.headers); 72 | await this.Promise.delay(300 + Math.random() * 500); 73 | /*if (Math.random() > 0.7) { 74 | this.logger.warn("Throwing some error..."); 75 | throw new Error("Something happened"); 76 | }*/ 77 | }, 78 | 79 | "my.second.topic": { 80 | group: "other", 81 | // maxInFlight: 1, 82 | async handler(msg) { 83 | this.logger.info("[POSTS] Channel Two msg received", msg); 84 | await this.Promise.delay(50 + Math.random() * 200); 85 | } 86 | } 87 | } 88 | }); 89 | 90 | broker 91 | .start() 92 | .then(async () => { 93 | broker.repl(); 94 | 95 | setInterval(() => { 96 | c++; 97 | console.log("Publish 'my.first.topic' message...", c); 98 | broker.sendToChannel("my.first.topic", { 99 | id: 1, 100 | name: "John Doe", 101 | status: true, 102 | count: c, 103 | pid: process.pid 104 | }); 105 | }, 2000); 106 | }) 107 | .catch(err => { 108 | broker.logger.error(err); 109 | broker.stop(); 110 | }); 111 | -------------------------------------------------------------------------------- /examples/multi-adapter/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { ServiceBroker } = require("moleculer"); 4 | const ChannelsMiddleware = require("../../").Middleware; 5 | 6 | // Create broker 7 | const broker = new ServiceBroker({ 8 | logLevel: { 9 | CHANNELS: "debug", 10 | "**": "info" 11 | }, 12 | middlewares: [ 13 | ChannelsMiddleware({ 14 | adapter: process.env.ADAPTER || "redis://localhost:6379", 15 | schemaProperty: "redisChannel", 16 | sendMethodName: "sendToRedisChannel", 17 | adapterPropertyName: "redisAdapter" 18 | }), 19 | ChannelsMiddleware({ 20 | adapter: process.env.ADAPTER || "redis://localhost:6379", 21 | schemaProperty: "redisAnother", 22 | sendMethodName: "sendToAnotherRedisChannel", 23 | adapterPropertyName: "anotherRedisAdapter" 24 | }) 25 | ], 26 | replCommands: [ 27 | {}, 28 | { 29 | command: "pub1", 30 | alias: ["p1"], 31 | async action(broker, args) { 32 | const { options } = args; 33 | //console.log(options); 34 | await broker.sendToRedisChannel("test.redisChannel", { 35 | name: "redisChannel", 36 | pid: process.pid 37 | }); 38 | } 39 | }, 40 | { 41 | command: "pub2", 42 | alias: ["p2"], 43 | async action(broker, args) { 44 | const { options } = args; 45 | //console.log(options); 46 | await broker.sendToAnotherRedisChannel("test.redisAnother", { 47 | name: "redisAnother", 48 | pid: process.pid 49 | }); 50 | } 51 | } 52 | ] 53 | }); 54 | 55 | broker.createService({ 56 | name: "sub1", 57 | redisChannel: { 58 | "test.redisChannel": { 59 | group: "mygroup", 60 | handler() { 61 | this.logger.info("redisChannel"); 62 | } 63 | } 64 | }, 65 | redisAnother: { 66 | "test.redisAnother": { 67 | group: "mygroup", 68 | handler() { 69 | this.logger.info("redisAnother"); 70 | } 71 | } 72 | } 73 | }); 74 | 75 | broker 76 | .start() 77 | .then(() => { 78 | broker.repl(); 79 | }) 80 | .catch(err => { 81 | broker.logger.error(err); 82 | broker.stop(); 83 | }); 84 | -------------------------------------------------------------------------------- /examples/multi-subs/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { ServiceBroker } = require("moleculer"); 4 | const ChannelsMiddleware = require("../..").Middleware; 5 | 6 | function createBrokerService(numbBrokers, numChannels) { 7 | let brokerList = []; 8 | 9 | for (let i = 0; i < numbBrokers; i++) { 10 | // Create broker 11 | let broker = new ServiceBroker({ 12 | logLevel: { 13 | CHANNELS: "info", 14 | "**": "info" 15 | }, 16 | middlewares: [ 17 | ChannelsMiddleware({ 18 | adapter: process.env.ADAPTER || "redis://localhost:6379" 19 | }) 20 | ], 21 | replCommands: [ 22 | { 23 | command: "publish", 24 | alias: ["p"], 25 | async action(broker, args) { 26 | const { options } = args; 27 | 28 | // Sample a topic 29 | let topicName = `topic.${ 30 | globalTopicList[Math.floor(Math.random() * globalTopicList.length)] 31 | }`; 32 | 33 | await broker.sendToChannel(topicName, { 34 | id: 2, 35 | name: "Jane Doe", 36 | status: false, 37 | pid: process.pid 38 | }); 39 | } 40 | } 41 | ] 42 | }); 43 | 44 | // Create service 45 | let serviceSchema = { 46 | name: `service.${String(i)}`, 47 | 48 | channels: {} 49 | }; 50 | 51 | const topicList = Array.from(Array(numTopics).keys()).map(entry => String(entry)); 52 | 53 | // Add channels to the service 54 | for (let j = 0; j < numChannels; j++) { 55 | // Pick a topic 56 | let idx = Math.floor(Math.random() * topicList.length); 57 | let topicName = `topic.${topicList.splice(idx, 1)[0]}`; 58 | 59 | serviceSchema.channels[`${topicName}`] = { 60 | async handler(msg) { 61 | this.logger.info( 62 | `[${serviceSchema.name}] Channel '${topicName}' received msg`, 63 | msg 64 | ); 65 | } 66 | }; 67 | } 68 | 69 | broker.createService(serviceSchema); 70 | brokerList.push(broker); 71 | } 72 | 73 | return brokerList; 74 | } 75 | 76 | const numTopics = 100; 77 | const numBrokers = 100; 78 | const numChannelPerService = 5; 79 | 80 | const globalTopicList = Array.from(Array(numTopics).keys()).map(entry => String(entry)); 81 | 82 | const brokerList = createBrokerService(numBrokers, numChannelPerService); 83 | 84 | Promise.all(brokerList.map(broker => broker.start())) 85 | .then(() => { 86 | brokerList[0].logger.info( 87 | `Started ${brokerList.length} brokers and ${ 88 | brokerList.length * (numChannelPerService + 1) 89 | } Redis Connections` 90 | ); 91 | brokerList[0].repl(); 92 | }) 93 | .catch(err => { 94 | brokerList[0].logger.error(err); 95 | 96 | brokerList.map(broker => broker.stop()); 97 | }); 98 | -------------------------------------------------------------------------------- /examples/mw-hooks/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { ServiceBroker } = require("moleculer"); 4 | const ChannelsMiddleware = require("../../").Middleware; 5 | const kleur = require("kleur"); 6 | 7 | let c = 0; 8 | 9 | const MyMiddleware = { 10 | name: "MyMiddleware", 11 | 12 | // Wrap the channel handlers 13 | localChannel(next, chan) { 14 | return async msg => { 15 | this.logger.info(kleur.magenta(` Before localChannel for '${chan.name}'`), msg); 16 | await next(msg); 17 | this.logger.info(kleur.magenta(` After localChannel for '${chan.name}'`), msg); 18 | }; 19 | }, 20 | 21 | // Wrap the `broker.sendToChannel` method 22 | sendToChannel(next) { 23 | return async (channelName, payload, opts) => { 24 | this.logger.info(kleur.yellow(`Before sendToChannel for '${channelName}'`), payload); 25 | await next(channelName, payload, opts); 26 | this.logger.info(kleur.yellow(`After sendToChannel for '${channelName}'`), payload); 27 | }; 28 | } 29 | }; 30 | 31 | // Create broker 32 | const broker = new ServiceBroker({ 33 | middlewares: [ 34 | ChannelsMiddleware({ 35 | adapter: process.env.ADAPTER || "redis://localhost:6379" 36 | }), 37 | MyMiddleware 38 | ], 39 | replCommands: [ 40 | { 41 | command: "publish", 42 | alias: ["p"], 43 | async action(broker, args) { 44 | const { options } = args; 45 | //console.log(options); 46 | await broker.sendToChannel("my.first.topic", { 47 | name: "Jane Doe", 48 | count: ++c, 49 | pid: process.pid 50 | }); 51 | } 52 | } 53 | ] 54 | }); 55 | 56 | broker.createService({ 57 | name: "posts", 58 | version: 1, 59 | channels: { 60 | async "my.first.topic"(msg) { 61 | this.logger.info(kleur.cyan(" Message received in handler"), msg); 62 | } 63 | } 64 | }); 65 | 66 | broker 67 | .start() 68 | .then(async () => { 69 | broker.repl(); 70 | 71 | await Promise.delay(1000); 72 | broker.logger.info("Sending..."); 73 | await broker.sendToChannel("my.first.topic", { 74 | name: "John", 75 | count: c, 76 | pid: process.pid 77 | }); 78 | }) 79 | .catch(err => { 80 | broker.logger.error(err); 81 | broker.stop(); 82 | }); 83 | -------------------------------------------------------------------------------- /examples/nats-wildcard/index.js: -------------------------------------------------------------------------------- 1 | const { ServiceBroker } = require("moleculer"); 2 | const ChannelsMiddleware = require("../..").Middleware; 3 | 4 | async function main() { 5 | const broker = new ServiceBroker({ 6 | nodeID: "channelTest", 7 | // logger: false, 8 | logLevel: "debug", 9 | middlewares: [ 10 | ChannelsMiddleware({ 11 | schemaProperty: "streamOne", 12 | adapterPropertyName: "streamOneAdapter", 13 | sendMethodName: "sendToStreamOneChannel", 14 | channelHandlerTrigger: "emitStreamOneLocalChannelHandler", 15 | adapter: { 16 | type: "NATS", 17 | options: { 18 | nats: { 19 | // url: process.env.NATS_SERVER, 20 | connectionOptions: { 21 | // debug: true 22 | // user: process.env.NATS_USER, 23 | // pass: process.env.NATS_PASSWORD 24 | }, 25 | streamConfig: { 26 | name: "streamOne", 27 | subjects: ["streamOneTopic.*"] 28 | }, 29 | consumerOptions: { 30 | config: { 31 | deliver_policy: "new", 32 | ack_policy: "explicit", 33 | max_ack_pending: 1 34 | } 35 | } 36 | }, 37 | maxInFlight: 10, 38 | maxRetries: 3, 39 | deadLettering: { 40 | enabled: false, 41 | queueName: "DEAD_LETTER_REG" 42 | } 43 | } 44 | } 45 | }) 46 | ] 47 | }); 48 | 49 | broker.createService({ 50 | name: "sub", 51 | streamOne: { 52 | "streamOneTopic.>": { 53 | group: "other", 54 | nats: { 55 | consumerOptions: { 56 | config: { 57 | deliver_policy: "new" 58 | } 59 | }, 60 | streamConfig: { 61 | // Create a single stream for all topics that match `streamOneTopic.>` 62 | // Note: Will override the streamConfig defined in middleware config 63 | name: "streamOne", 64 | subjects: ["streamOneTopic.>"] 65 | } 66 | }, 67 | // This handler will be called for all topics that match `streamOneTopic.>` 68 | async handler(payload) { 69 | console.log(`Processing streamOneTopic: ${JSON.stringify(payload)}}`); 70 | } 71 | } 72 | } 73 | }); 74 | 75 | await broker.start().delay(2000); 76 | 77 | const msg = { 78 | id: 1, 79 | name: "John", 80 | age: 25 81 | }; 82 | await broker.sendToStreamOneChannel("streamOneTopic.abc", { ...msg, topic: "abc" }); 83 | await broker.Promise.delay(200); 84 | await broker.sendToStreamOneChannel("streamOneTopic.abc.def", { ...msg, topic: "abc.def" }); 85 | await broker.Promise.delay(200); 86 | await broker.sendToStreamOneChannel("streamOneTopic.xyz", { ...msg, topic: "xyz" }); 87 | await broker.Promise.delay(200); 88 | broker.repl(); 89 | } 90 | 91 | main().catch(err => console.error(err)); 92 | -------------------------------------------------------------------------------- /examples/prefix/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { ServiceBroker } = require("moleculer"); 4 | const ChannelsMiddleware = require("../../").Middleware; 5 | 6 | let c = 1; 7 | 8 | // Create broker 9 | const broker = new ServiceBroker({ 10 | namespace: "abc", 11 | logLevel: { 12 | CHANNELS: "debug", 13 | "**": "info" 14 | }, 15 | middlewares: [ 16 | ChannelsMiddleware({ 17 | adapter: process.env.ADAPTER || "redis://localhost:6379" 18 | }) 19 | ], 20 | replCommands: [ 21 | { 22 | command: "publish", 23 | alias: ["p"], 24 | async action(broker, args) { 25 | const { options } = args; 26 | //console.log(options); 27 | await broker.sendToChannel("test.prefix.topic", { 28 | id: 2, 29 | name: "Jane Doe", 30 | status: false, 31 | count: ++c, 32 | pid: process.pid 33 | }); 34 | } 35 | } 36 | ] 37 | }); 38 | 39 | broker.createService({ 40 | name: "sub1", 41 | channels: { 42 | "test.prefix.topic": { 43 | group: "mygroup", 44 | handler(msg) { 45 | this.logger.info("____________________"); 46 | this.logger.info(msg); 47 | this.logger.info("____________________"); 48 | } 49 | } 50 | } 51 | }); 52 | 53 | broker 54 | .start() 55 | .then(async () => { 56 | broker.repl(); 57 | }) 58 | .catch(err => { 59 | broker.logger.error(err); 60 | broker.stop(); 61 | }); 62 | -------------------------------------------------------------------------------- /examples/redis-claim/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { ServiceBroker } = require("moleculer"); 4 | const ChannelsMiddleware = require("../../").Middleware; 5 | 6 | const Sub2Schema = require("./stable.service"); 7 | 8 | let c = 1; 9 | 10 | // Create broker 11 | const broker = new ServiceBroker({ 12 | logLevel: { 13 | CHANNELS: "debug", 14 | "**": "info" 15 | }, 16 | middlewares: [ 17 | ChannelsMiddleware({ 18 | adapter: process.env.ADAPTER || "redis://localhost:6379" 19 | }) 20 | ], 21 | replCommands: [ 22 | { 23 | command: "publish", 24 | alias: ["p"], 25 | async action(broker, args) { 26 | const { options } = args; 27 | //console.log(options); 28 | await broker.sendToChannel("test.unstable.topic", { 29 | id: 2, 30 | name: "Jane Doe", 31 | status: false, 32 | count: ++c, 33 | pid: process.pid 34 | }); 35 | } 36 | } 37 | ] 38 | }); 39 | 40 | broker.createService({ 41 | name: "sub1", 42 | channels: { 43 | "test.unstable.topic": { 44 | group: "mygroup", 45 | handler() { 46 | this.logger.error("Ups! Something happened"); 47 | return Promise.reject(new Error("Something happened")); 48 | } 49 | } 50 | } 51 | }); 52 | 53 | broker.createService(Sub2Schema); 54 | 55 | broker 56 | .start() 57 | .then(() => { 58 | broker.repl(); 59 | }) 60 | .catch(err => { 61 | broker.logger.error(err); 62 | broker.stop(); 63 | }); 64 | -------------------------------------------------------------------------------- /examples/redis-claim/stable.service.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | name: "sub2", 5 | 6 | channels: { 7 | "test.unstable.topic": { 8 | group: "mygroup", 9 | handler(msg) { 10 | this.logger.info("Msg received", msg); 11 | } 12 | } 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /examples/redis-cluster/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { ServiceBroker } = require("moleculer"); 4 | const ChannelsMiddleware = require("../../").Middleware; 5 | 6 | let c = 0; 7 | 8 | // --- BROKER 1 --- 9 | const broker1 = new ServiceBroker({ 10 | nodeID: "node-1", 11 | logLevel: { 12 | CHANNELS: "debug", 13 | "**": "info" 14 | }, 15 | middlewares: [ 16 | ChannelsMiddleware({ 17 | adapter: { 18 | type: "Redis", 19 | options: { 20 | cluster: { 21 | nodes: [ 22 | { host: "127.0.0.1", port: 6381 } 23 | //{ host: "127.0.0.1", port: 6382 }, 24 | //{ host: "127.0.0.1", port: 6383 } 25 | ] 26 | } 27 | } 28 | } 29 | }) 30 | ], 31 | replCommands: [ 32 | { 33 | command: "pub", 34 | alias: ["p"], 35 | async action(broker, args) { 36 | const { options } = args; 37 | //console.log(options); 38 | await broker.sendToChannel("test.redisCluster", { 39 | id: ++c, 40 | pid: process.pid 41 | }); 42 | } 43 | } 44 | ] 45 | }); 46 | 47 | // --- BROKER 2 --- 48 | const broker2 = new ServiceBroker({ 49 | nodeID: "node-2", 50 | logLevel: { 51 | CHANNELS: "debug", 52 | "**": "info" 53 | }, 54 | middlewares: [ 55 | ChannelsMiddleware({ 56 | adapter: { 57 | type: "Redis", 58 | options: { 59 | cluster: { 60 | nodes: [ 61 | //{ host: "127.0.0.1", port: 6381 }, 62 | { host: "127.0.0.1", port: 6382 } 63 | //{ host: "127.0.0.1", port: 6383 } 64 | ] 65 | } 66 | } 67 | } 68 | }) 69 | ] 70 | }); 71 | 72 | broker2.createService({ 73 | name: "sub1", 74 | channels: { 75 | "test.redisCluster": { 76 | handler(payload) { 77 | this.logger.info(`Received message on ${broker2.nodeID}`, payload); 78 | } 79 | } 80 | } 81 | }); 82 | 83 | // --- BROKER 3 --- 84 | const broker3 = new ServiceBroker({ 85 | nodeID: "node-3", 86 | logLevel: { 87 | CHANNELS: "debug", 88 | "**": "info" 89 | }, 90 | middlewares: [ 91 | ChannelsMiddleware({ 92 | adapter: { 93 | type: "Redis", 94 | options: { 95 | cluster: { 96 | nodes: [ 97 | //{ host: "127.0.0.1", port: 6381 }, 98 | //{ host: "127.0.0.1", port: 6382 }, 99 | { host: "127.0.0.1", port: 6383 } 100 | ] 101 | } 102 | } 103 | } 104 | }) 105 | ] 106 | }); 107 | 108 | broker3.createService({ 109 | name: "sub2", 110 | channels: { 111 | "test.redisCluster": { 112 | handler(payload) { 113 | this.logger.info(`Received message on ${broker3.nodeID}`, payload); 114 | } 115 | } 116 | } 117 | }); 118 | 119 | Promise.all([broker1.start(), broker2.start(), broker3.start()]) 120 | .then(() => broker1.repl()) 121 | .catch(async err => broker1.logger.error(err)); 122 | -------------------------------------------------------------------------------- /examples/redis-pending/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // Adapted from: https://github.com/moleculerjs/moleculer-channels/issues/74 4 | 5 | const { ServiceBroker } = require("moleculer"); 6 | const ChannelsMiddleware = require("../../types").Middleware; 7 | const broker = new ServiceBroker({ 8 | namespace: "test", 9 | nodeID: "test1", 10 | transporter: "TCP", 11 | middlewares: [ 12 | ChannelsMiddleware({ 13 | adapter: "redis://127.0.0.1:6379" 14 | }) 15 | ] 16 | }); 17 | 18 | const serviceSchema = { 19 | name: "subscriber", 20 | channels: { 21 | "order.created": { 22 | group: "mygroup", 23 | redis: { 24 | minIdleTime: 1000, 25 | claimInterval: 1, 26 | startID: "0" 27 | }, 28 | maxRetries: 100, 29 | handler(payload) { 30 | this.logger.info("Received order.created event", payload); 31 | throw new Error(); 32 | } 33 | } 34 | } 35 | }; 36 | broker.createService(serviceSchema); 37 | 38 | // Start the Moleculer broker 39 | broker.start().then(async () => { 40 | try { 41 | broker.repl(); 42 | 43 | for (let i = 0; i < 10; i++) { 44 | await broker.sendToChannel("order.created", { id: i, items: "test" }); 45 | 46 | await broker.Promise.delay(100); 47 | } 48 | 49 | await broker.destroyService("subscriber"); 50 | 51 | setTimeout(() => { 52 | broker.logger.info("Recreate service"); 53 | broker.createService(serviceSchema); 54 | }, 10000); 55 | } catch (error) { 56 | console.log(error); 57 | } 58 | }); 59 | -------------------------------------------------------------------------------- /examples/retry/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const _ = require("lodash"); 4 | const { ServiceBroker } = require("moleculer"); 5 | const ChannelsMiddleware = require("../..").Middleware; 6 | 7 | let numMessages = 100; 8 | let ids; 9 | let sub1Counter = 0; 10 | let sub2Counter = 0; 11 | const maxRetries = 10; 12 | 13 | const broker = new ServiceBroker({ 14 | logLevel: { 15 | CHANNELS: "debug", 16 | "**": "info" 17 | }, 18 | middlewares: [ 19 | ChannelsMiddleware({ 20 | adapter: process.env.ADAPTER || "redis://localhost:6379" 21 | }) 22 | ], 23 | replCommands: [ 24 | { 25 | command: "publish", 26 | alias: ["p"], 27 | async action(broker, args) { 28 | const { options } = args; 29 | 30 | await broker.call("sub1.resetIDs"); 31 | 32 | await Promise.all( 33 | _.times(numMessages, id => broker.sendToChannel("test.unstable.topic", { id })) 34 | ); 35 | 36 | await broker.Promise.delay(1000); 37 | 38 | const { remainingIDs, sub1Calls, sub2Calls } = await broker.call("sub1.checkIDs"); 39 | 40 | broker.logger.info(` ===>> Remaining IDs : ${remainingIDs} <<=== `); 41 | broker.logger.info( 42 | ` ===>> Error Handler : ${sub1Calls} || Good Handler : ${sub2Calls} <<=== ` 43 | ); 44 | } 45 | } 46 | ] 47 | }); 48 | 49 | broker.createService({ 50 | name: "sub1", 51 | actions: { 52 | resetIDs: { 53 | handler() { 54 | ids = new Set(Array.from(Array(numMessages).keys())); 55 | 56 | sub1Counter = 0; 57 | sub2Counter = 0; 58 | } 59 | }, 60 | 61 | checkIDs: { 62 | handler() { 63 | return { 64 | remainingIDs: ids.size, 65 | sub1Calls: sub1Counter, 66 | sub2Calls: sub2Counter 67 | }; 68 | } 69 | } 70 | }, 71 | channels: { 72 | "test.unstable.topic": { 73 | group: "mygroup", 74 | maxRetries: maxRetries, 75 | handler(payload, raw) { 76 | sub1Counter++; 77 | this.logger.error( 78 | `Ups! Something happened messageID:${payload.id} || JS_SequenceID:${raw.seq}` 79 | ); 80 | return Promise.reject(new Error("Something happened")); 81 | } 82 | } 83 | } 84 | }); 85 | 86 | broker.createService({ 87 | name: "sub2", 88 | channels: { 89 | "test.unstable.topic": { 90 | group: "mygroup", 91 | // Defaults to 1 hour. Decrease for unit tests 92 | minIdleTime: 10, 93 | claimInterval: 10, 94 | maxRetries: maxRetries, 95 | handler(msg) { 96 | sub2Counter++; 97 | 98 | ids.delete(msg.id); 99 | 100 | this.logger.info(msg); 101 | } 102 | } 103 | } 104 | }); 105 | 106 | broker 107 | .start() 108 | .then(() => { 109 | broker.repl(); 110 | }) 111 | .catch(err => { 112 | broker.logger.error(err); 113 | broker.stop(); 114 | }); 115 | -------------------------------------------------------------------------------- /examples/send-in-started/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { ServiceBroker } = require("moleculer"); 4 | const ChannelsMiddleware = require("../../").Middleware; 5 | 6 | let c = 1; 7 | 8 | // Create broker 9 | const broker = new ServiceBroker({ 10 | namespace: "uat", 11 | logLevel: { 12 | CHANNELS: "debug", 13 | "**": "info" 14 | }, 15 | middlewares: [ 16 | ChannelsMiddleware({ 17 | adapter: process.env.ADAPTER || "redis://localhost:6379" 18 | }) 19 | ], 20 | replCommands: [ 21 | { 22 | command: "publish", 23 | alias: ["p"], 24 | async action(broker, args) { 25 | const { options } = args; 26 | //console.log(options); 27 | await broker.sendToChannel( 28 | "my.first.topic", 29 | { 30 | id: 2, 31 | name: "Jane Doe", 32 | status: false, 33 | count: ++c, 34 | pid: process.pid 35 | }, 36 | { key: "" + c, headers: { a: "something" }, xaddMaxLen: "~10" } 37 | ); 38 | } 39 | } 40 | ] 41 | }); 42 | 43 | broker.createService({ 44 | name: "users", 45 | channels: { 46 | async "my.first.topic"(msg) { 47 | this.logger.info("[USERS] Channel One msg received", msg); 48 | } 49 | }, 50 | async started() { 51 | this.logger.info("Service started. Sending message to channel..."); 52 | await this.broker.sendToChannel( 53 | "my.first.topic", 54 | { 55 | id: 1, 56 | name: "John Doe", 57 | status: true, 58 | count: ++c, 59 | pid: process.pid 60 | }, 61 | { key: "" + c, headers: { a: "something" }, xaddMaxLen: "~10" } 62 | ); 63 | } 64 | }); 65 | 66 | broker 67 | .start() 68 | .then(async () => { 69 | broker.repl(); 70 | }) 71 | .catch(err => { 72 | broker.logger.error(err); 73 | broker.stop(); 74 | }); 75 | -------------------------------------------------------------------------------- /examples/serializer/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { ServiceBroker } = require("moleculer"); 4 | const ChannelsMiddleware = require("../../").Middleware; 5 | 6 | let c = 1; 7 | 8 | // Create broker 9 | const broker = new ServiceBroker({ 10 | logLevel: { 11 | CHANNELS: "debug", 12 | "**": "info" 13 | }, 14 | middlewares: [ 15 | ChannelsMiddleware({ 16 | adapter: { 17 | type: "Redis", 18 | options: { 19 | url: "localhost:4222", 20 | amqp: { 21 | url: "amqp://localhost:5672" 22 | }, 23 | redis: "localhost:6379", 24 | serializer: "MsgPack" 25 | } 26 | } 27 | }) 28 | ], 29 | replCommands: [ 30 | { 31 | command: "publish", 32 | alias: ["p"], 33 | async action(broker, args) { 34 | await broker.sendToChannel( 35 | "my.first.topic", 36 | { 37 | id: 2, 38 | name: "Jane Doe", 39 | status: false, 40 | count: ++c, 41 | pid: process.pid 42 | }, 43 | { key: "" + c, headers: { a: "something" } } 44 | ); 45 | } 46 | } 47 | ] 48 | }); 49 | 50 | broker.createService({ 51 | name: "posts", 52 | version: 1, 53 | channels: { 54 | async "my.first.topic"(msg, raw) { 55 | this.logger.info("[POSTS] Channel One msg received", msg); 56 | 57 | this.logger.info("[POSTS] Channel One raw received", raw); 58 | } 59 | } 60 | }); 61 | 62 | broker 63 | .start() 64 | .then(async () => { 65 | broker.repl(); 66 | 67 | console.log("Publish 'my.first.topic' message..."); 68 | await broker.sendToChannel( 69 | "my.first.topic", 70 | { 71 | id: 1, 72 | name: "John Doe", 73 | status: true, 74 | count: c, 75 | pid: process.pid 76 | }, 77 | { key: "" + c, headers: { a: "something" } } 78 | ); 79 | }) 80 | .catch(err => { 81 | broker.logger.error(err); 82 | broker.stop(); 83 | }); 84 | -------------------------------------------------------------------------------- /examples/simple/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { ServiceBroker } = require("moleculer"); 4 | const ChannelsMiddleware = require("../../").Middleware; 5 | 6 | let c = 1; 7 | 8 | // Create broker 9 | const broker = new ServiceBroker({ 10 | namespace: "uat", 11 | logLevel: { 12 | CHANNELS: "debug", 13 | "**": "info" 14 | }, 15 | middlewares: [ 16 | ChannelsMiddleware({ 17 | adapter: process.env.ADAPTER || "redis://localhost:6379" 18 | //adapter: process.env.ADAPTER || "nats://localhost:4222" 19 | //adapter: process.env.ADAPTER || "amqp://localhost:5672" 20 | //adapter: process.env.ADAPTER || "kafka://localhost:9093" 21 | }) 22 | ], 23 | replCommands: [ 24 | { 25 | command: "publish", 26 | alias: ["p"], 27 | async action(broker, args) { 28 | const { options } = args; 29 | //console.log(options); 30 | await broker.sendToChannel( 31 | "my.first.topic", 32 | { 33 | id: 2, 34 | name: "Jane Doe", 35 | status: false, 36 | count: ++c, 37 | pid: process.pid 38 | }, 39 | { key: "" + c, headers: { a: "something" }, xaddMaxLen: "~10" } 40 | ); 41 | } 42 | }, 43 | { 44 | command: "publish2", 45 | alias: ["p2"], 46 | async action(broker, args) { 47 | const { options } = args; 48 | //console.log(options); 49 | await broker.sendToChannel("my.second.topic", { 50 | id: 2, 51 | name: "Jane Doe", 52 | status: true, 53 | pid: process.pid 54 | }); 55 | } 56 | }, 57 | { 58 | command: "publish3", 59 | alias: ["p3"], 60 | async action(broker, args) { 61 | const { options } = args; 62 | //console.log(options); 63 | await broker.sendToChannel( 64 | "", 65 | { 66 | id: 2, 67 | name: "Jane Doe", 68 | status: true, 69 | pid: process.pid 70 | }, 71 | { routingKey: "demoo" } 72 | ); 73 | } 74 | } 75 | ] 76 | }); 77 | 78 | broker.createService({ 79 | name: "posts", 80 | version: 1, 81 | channels: { 82 | async "my.first.topic"(msg, raw) { 83 | this.logger.info("[POSTS] Channel One msg received", msg, raw.key, raw.headers); 84 | /*if (Math.random() > 0.7) { 85 | this.logger.warn("Throwing some error..."); 86 | throw new Error("Something happened"); 87 | }*/ 88 | }, 89 | 90 | "my.second.topic": { 91 | group: "other", 92 | // maxInFlight: 1, 93 | async handler(msg) { 94 | this.logger.info("[POSTS] Channel Two msg received", msg); 95 | } 96 | } 97 | } 98 | }); 99 | 100 | broker.createService({ 101 | name: "users", 102 | channels: { 103 | async "my.first.topic"(msg) { 104 | this.logger.info("[USERS] Channel One msg received", msg); 105 | }, 106 | 107 | "my.second.topic": { 108 | group: "other", 109 | // maxInFlight: 1, 110 | async handler(msg) { 111 | this.logger.info("[USERS] Channel Two msg received", msg); 112 | } 113 | } 114 | } 115 | }); 116 | 117 | broker 118 | .start() 119 | .then(async () => { 120 | broker.repl(); 121 | 122 | //await Promise.delay(1000); 123 | console.log("Publish 'my.first.topic' message..."); 124 | await broker.sendToChannel("my.first.topic", { 125 | id: 1, 126 | name: "John Doe", 127 | status: true, 128 | count: c, 129 | pid: process.pid 130 | }); 131 | 132 | await Promise.delay(5000); 133 | console.log("Publish 'my.second.topic' message..."); 134 | await broker.sendToChannel("my.second.topic", { id: 2, name: "Jane Doe", status: true }); 135 | 136 | /*setInterval(() => { 137 | c++; 138 | console.log("Publish 'my.first.topic' message...", c); 139 | broker.sendToChannel("my.first.topic", { 140 | id: 1, 141 | name: "John Doe", 142 | status: true, 143 | count: c, 144 | pid: process.pid 145 | }); 146 | }, 2000);*/ 147 | }) 148 | .catch(err => { 149 | broker.logger.error(err); 150 | broker.stop(); 151 | }); 152 | -------------------------------------------------------------------------------- /examples/tracking/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { ServiceBroker } = require("moleculer"); 4 | const ChannelsMiddleware = require("../..").Middleware; 5 | 6 | let c = 1; 7 | 8 | // Create broker 9 | const broker = new ServiceBroker({ 10 | logLevel: { 11 | CHANNELS: "debug", 12 | "**": "info" 13 | }, 14 | middlewares: [ 15 | ChannelsMiddleware({ 16 | adapter: process.env.ADAPTER || "redis://localhost:6379" 17 | }) 18 | ], 19 | replCommands: [ 20 | { 21 | command: "publish", 22 | alias: ["p"], 23 | async action(broker, args) { 24 | const { options } = args; 25 | //console.log(options); 26 | const payload = { 27 | id: 2, 28 | name: "Jane Doe", 29 | status: false, 30 | count: ++c, 31 | pid: process.pid 32 | }; 33 | 34 | await broker.sendToChannel("my.tracked.topic", payload); 35 | } 36 | }, 37 | { 38 | command: "terminate", 39 | alias: ["t"], 40 | async action(broker, args) { 41 | broker.logger.warn("Send SIGTERM..."); 42 | process.emit("SIGTERM"); 43 | } 44 | } 45 | ] 46 | }); 47 | 48 | broker.createService({ 49 | name: "sub1", 50 | channels: { 51 | "my.tracked.topic": { 52 | maxInFlight: 10, 53 | async handler() { 54 | this.logger.info(">>> Start processing message. It will takes 10s..."); 55 | await this.Promise.delay(10 * 1000); 56 | this.logger.info(">>> Processing finished."); 57 | } 58 | } 59 | } 60 | }); 61 | 62 | broker 63 | .start() 64 | .then(async () => { 65 | broker.repl(); 66 | }) 67 | .catch(err => { 68 | broker.logger.error(err); 69 | broker.stop(); 70 | }); 71 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @moleculer/channels 3 | * Copyright (c) 2021 MoleculerJS (https://github.com/moleculerjs/channels) 4 | * MIT Licensed 5 | */ 6 | 7 | "use strict"; 8 | 9 | module.exports = { 10 | Middleware: require("./src"), 11 | Tracing: require("./src/tracing"), 12 | Adapters: require("./src/adapters") 13 | }; 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@moleculer/channels", 3 | "version": "0.2.0", 4 | "description": "Reliable messages for Moleculer services", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "nodemon examples/index.js", 8 | "check": "tsc --noEmit true", 9 | "types": "tsc --emitDeclarationOnly true --noEmitOnError false", 10 | "ci": "jest --watch", 11 | "test": "jest --coverage --verbose", 12 | "lint": "eslint --ext=.js src examples test", 13 | "bench": "node benchmark/index.js", 14 | "bench:watch": "nodemon benchmark/index.js", 15 | "deps": "ncu -i", 16 | "ci-deps": "ncu --target minor", 17 | "ci-update-deps": "ncu -u --target minor", 18 | "coverall": "cat ./coverage/lcov.info | ./node_modules/coveralls-next/bin/coveralls.js", 19 | "test:up": "docker compose -f test/docker-compose.yml up -d", 20 | "test:down": "docker compose -f test/docker-compose.yml down -v", 21 | "release": "npm publish --access public && git push --follow-tags" 22 | }, 23 | "keywords": [ 24 | "moleculer", 25 | "microservice" 26 | ], 27 | "repository": { 28 | "type": "git", 29 | "url": "https://github.com/moleculerjs/moleculer-channels.git" 30 | }, 31 | "typings": "types/index.d.ts", 32 | "author": "MoleculerJS", 33 | "license": "MIT", 34 | "peerDependencies": { 35 | "moleculer": "^0.14.12 || ^0.15.0-0" 36 | }, 37 | "devDependencies": { 38 | "@types/jest": "^30.0.0", 39 | "@types/node": "^24.7.2", 40 | "amqplib": "^0.10.9", 41 | "benchmarkify": "^4.0.0", 42 | "coveralls-next": "^5.0.0", 43 | "eslint": "^8.56.0", 44 | "eslint-config-prettier": "^8.10.0", 45 | "eslint-plugin-node": "^11.1.0", 46 | "eslint-plugin-prettier": "^4.2.1", 47 | "eslint-plugin-promise": "^6.1.1", 48 | "eslint-plugin-security": "^1.7.1", 49 | "ioredis": "^5.8.1", 50 | "jest": "^29.7.0", 51 | "jest-cli": "^29.7.0", 52 | "kafkajs": "^2.2.4", 53 | "kleur": "^4.1.5", 54 | "moleculer": "^0.15.0-beta2", 55 | "moleculer-repl": "^0.7.4", 56 | "msgpack5": "^6.0.2", 57 | "nats": "^2.29.3", 58 | "nodemon": "^3.1.10", 59 | "npm-check-updates": "^19.0.0", 60 | "prettier": "^3.6.2", 61 | "typescript": "^5.9.3" 62 | }, 63 | "jest": { 64 | "testEnvironment": "node", 65 | "rootDir": "./src", 66 | "roots": [ 67 | "../test" 68 | ], 69 | "coverageDirectory": "../coverage", 70 | "coveragePathIgnorePatterns": [ 71 | "/node_modules/" 72 | ] 73 | }, 74 | "engines": { 75 | "node": ">= 20.x.x" 76 | }, 77 | "dependencies": { 78 | "lodash": "^4.17.21", 79 | "semver": "^7.7.2" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | useTabs: true, 3 | printWidth: 100, 4 | trailingComma: "none", 5 | tabWidth: 4, 6 | singleQuote: false, 7 | semi: true, 8 | bracketSpacing: true, 9 | arrowParens: "avoid", 10 | overrides: [ 11 | { 12 | files: "*.md", 13 | options: { 14 | useTabs: false 15 | } 16 | }, 17 | { 18 | files: "*.json", 19 | options: { 20 | tabWidth: 2, 21 | useTabs: false 22 | } 23 | } 24 | ] 25 | }; 26 | -------------------------------------------------------------------------------- /src/adapters/base.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @moleculer/channels 3 | * Copyright (c) 2021 MoleculerJS (https://github.com/moleculerjs/channels) 4 | * MIT Licensed 5 | */ 6 | 7 | "use strict"; 8 | 9 | const _ = require("lodash"); 10 | const semver = require("semver"); 11 | const { MoleculerError } = require("moleculer").Errors; 12 | const { Serializers, METRIC } = require("moleculer"); 13 | const C = require("../constants"); 14 | const { transformErrorToHeaders, transformHeadersToErrorData } = require("../utils"); 15 | 16 | /** 17 | * @typedef {import("moleculer").ServiceBroker} ServiceBroker Moleculer Service Broker instance 18 | * @typedef {import("moleculer").Service} Service Moleculer Service definition 19 | * @typedef {import("moleculer").Logger} Logger Logger instance 20 | * @typedef {import("moleculer").Serializers.Base} Serializer Moleculer Serializer 21 | * @typedef {import("../index").Channel} Channel Base channel definition 22 | * @typedef {import("../index").DeadLetteringOptions} DeadLetteringOptions Dead-letter-queue options 23 | */ 24 | 25 | /** 26 | * @typedef {Object} BaseDefaultOptions Base Adapter configuration 27 | * @property {String?} prefix Adapter prefix 28 | * @property {String} consumerName Name of the consumer 29 | * @property {String} serializer Type of serializer to use in message exchange. Defaults to JSON 30 | * @property {Number} maxRetries Maximum number of retries before sending the message to dead-letter-queue or drop 31 | * @property {Number} maxInFlight Maximum number of messages that can be processed in parallel. 32 | * @property {DeadLetteringOptions} deadLettering Dead-letter-queue options 33 | */ 34 | 35 | class BaseAdapter { 36 | /** 37 | * Constructor of adapter 38 | * @param {Object?} opts 39 | */ 40 | constructor(opts) { 41 | /** @type {BaseDefaultOptions} */ 42 | this.opts = _.defaultsDeep({}, opts, { 43 | consumerName: null, 44 | prefix: null, 45 | serializer: "JSON", 46 | maxRetries: 3, 47 | maxInFlight: 1, 48 | deadLettering: { 49 | enabled: false, 50 | queueName: "FAILED_MESSAGES" 51 | } 52 | }); 53 | 54 | /** 55 | * Tracks the messages that are still being processed by different clients 56 | * @type {Map>} 57 | */ 58 | this.activeMessages = new Map(); 59 | 60 | /** @type {Boolean} Flag indicating the adapter's connection status */ 61 | this.connected = false; 62 | 63 | /** 64 | * Function to convert Error to a plain object and gets error info ready to be placed in message headers 65 | * 66 | * @type {(error: Error) => Record} 67 | */ 68 | this.transformErrorToHeaders = 69 | this.opts?.deadLettering?.transformErrorToHeaders || transformErrorToHeaders; 70 | 71 | /** 72 | * Function to parse error info from message headers to a plain object. Also attempts to covert data entries to original types 73 | * @type {(headers: Record) => Record} 74 | */ 75 | this.transformHeadersToErrorData = 76 | this.opts?.deadLettering?.transformHeadersToErrorData || transformHeadersToErrorData; 77 | } 78 | 79 | /** 80 | * Initialize the adapter. 81 | * 82 | * @param {ServiceBroker} broker 83 | * @param {Logger} logger 84 | */ 85 | init(broker, logger) { 86 | this.broker = broker; 87 | this.logger = logger; 88 | this.Promise = broker.Promise; 89 | 90 | if (!this.opts.consumerName) this.opts.consumerName = this.broker.nodeID; 91 | if (this.opts.prefix == null) this.opts.prefix = broker.namespace; 92 | 93 | this.logger.info("Channel consumer name:", this.opts.consumerName); 94 | this.logger.info("Channel prefix:", this.opts.prefix); 95 | 96 | // create an instance of serializer (default to JSON) 97 | /** @type {Serializer} */ 98 | this.serializer = Serializers.resolve(this.opts.serializer); 99 | this.serializer.init(this.broker); 100 | this.logger.info("Channel serializer:", this.broker.getConstructorName(this.serializer)); 101 | 102 | this.registerAdapterMetrics(broker); 103 | } 104 | 105 | /** 106 | * Register adapter related metrics 107 | * @param {ServiceBroker} broker 108 | */ 109 | registerAdapterMetrics(broker) { 110 | if (!broker.isMetricsEnabled()) return; 111 | 112 | broker.metrics.register({ 113 | type: METRIC.TYPE_COUNTER, 114 | name: C.METRIC_CHANNELS_MESSAGES_ERRORS_TOTAL, 115 | labelNames: ["channel", "group"], 116 | rate: true, 117 | unit: "msg" 118 | }); 119 | 120 | broker.metrics.register({ 121 | type: METRIC.TYPE_COUNTER, 122 | name: C.METRIC_CHANNELS_MESSAGES_RETRIES_TOTAL, 123 | labelNames: ["channel", "group"], 124 | rate: true, 125 | unit: "msg" 126 | }); 127 | 128 | broker.metrics.register({ 129 | type: METRIC.TYPE_COUNTER, 130 | name: C.METRIC_CHANNELS_MESSAGES_DEAD_LETTERING_TOTAL, 131 | labelNames: ["channel", "group"], 132 | rate: true, 133 | unit: "msg" 134 | }); 135 | } 136 | 137 | /** 138 | * 139 | * @param {String} metricName 140 | * @param {Channel} chan 141 | */ 142 | metricsIncrement(metricName, chan) { 143 | if (!this.broker.isMetricsEnabled()) return; 144 | 145 | this.broker.metrics.increment(metricName, { 146 | channel: chan.name, 147 | group: chan.group 148 | }); 149 | } 150 | 151 | /** 152 | * Check the installed client library version. 153 | * https://github.com/npm/node-semver#usage 154 | * 155 | * @param {String} library 156 | * @param {String} requiredVersions 157 | * @returns {Boolean} 158 | */ 159 | checkClientLibVersion(library, requiredVersions) { 160 | const pkg = require(`${library}/package.json`); 161 | const installedVersion = pkg.version; 162 | 163 | if (semver.satisfies(installedVersion, requiredVersions)) { 164 | return true; 165 | } else { 166 | this.logger.warn( 167 | `The installed ${library} library is not supported officially. Proper functionality cannot be guaranteed. Supported versions:`, 168 | requiredVersions 169 | ); 170 | return false; 171 | } 172 | } 173 | 174 | /** 175 | * Init active messages list for tracking messages of a channel 176 | * @param {string} channelID 177 | * @param {Boolean?} toThrow Throw error if already exists 178 | */ 179 | initChannelActiveMessages(channelID, toThrow = true) { 180 | if (this.activeMessages.has(channelID)) { 181 | if (toThrow) 182 | throw new MoleculerError( 183 | `Already tracking active messages of channel ${channelID}` 184 | ); 185 | 186 | return; 187 | } 188 | 189 | this.activeMessages.set(channelID, []); 190 | } 191 | 192 | /** 193 | * Remove active messages list of a channel 194 | * @param {string} channelID 195 | */ 196 | stopChannelActiveMessages(channelID) { 197 | if (!this.activeMessages.has(channelID)) { 198 | throw new MoleculerError(`Not tracking active messages of channel ${channelID}`); 199 | } 200 | 201 | if (this.activeMessages.get(channelID).length !== 0) { 202 | throw new MoleculerError( 203 | `Can't stop tracking active messages of channel ${channelID}. It still has ${ 204 | this.activeMessages.get(channelID).length 205 | } messages being processed.` 206 | ); 207 | } 208 | 209 | this.activeMessages.delete(channelID); 210 | } 211 | 212 | /** 213 | * Add IDs of the messages that are currently being processed 214 | * 215 | * @param {string} channelID Channel ID 216 | * @param {Array} IDs List of IDs 217 | */ 218 | addChannelActiveMessages(channelID, IDs) { 219 | if (!this.activeMessages.has(channelID)) { 220 | throw new MoleculerError(`Not tracking active messages of channel ${channelID}`); 221 | } 222 | 223 | this.activeMessages.get(channelID).push(...IDs); 224 | } 225 | 226 | /** 227 | * Remove IDs of the messages that were already processed 228 | * 229 | * @param {string} channelID Channel ID 230 | * @param {string[]|number[]} IDs List of IDs 231 | */ 232 | removeChannelActiveMessages(channelID, IDs) { 233 | if (!this.activeMessages.has(channelID)) { 234 | throw new MoleculerError(`Not tracking active messages of channel ${channelID}`); 235 | } 236 | 237 | const messageList = this.activeMessages.get(channelID); 238 | 239 | IDs.forEach(id => { 240 | const idx = messageList.indexOf(id); 241 | if (idx != -1) { 242 | messageList.splice(idx, 1); 243 | } 244 | }); 245 | } 246 | 247 | /** 248 | * Get the number of active messages of a channel 249 | * 250 | * @param {string} channelID Channel ID 251 | */ 252 | getNumberOfChannelActiveMessages(channelID) { 253 | if (!this.activeMessages.has(channelID)) { 254 | //throw new MoleculerError(`Not tracking active messages of channel ${channelID}`); 255 | return 0; 256 | } 257 | 258 | return this.activeMessages.get(channelID).length; 259 | } 260 | 261 | /** 262 | * Get the number of channels 263 | */ 264 | getNumberOfTrackedChannels() { 265 | return this.activeMessages.size; 266 | } 267 | 268 | /** 269 | * Given a topic name adds the prefix 270 | * 271 | * @param {String} topicName 272 | * @returns {String} New topic name 273 | */ 274 | addPrefixTopic(topicName) { 275 | if (this.opts.prefix != null && this.opts.prefix != "" && topicName) { 276 | return `${this.opts.prefix}.${topicName}`; 277 | } 278 | 279 | return topicName; 280 | } 281 | 282 | /** 283 | * Connect to the adapter. 284 | */ 285 | async connect() { 286 | /* istanbul ignore next */ 287 | throw new Error("This method is not implemented."); 288 | } 289 | 290 | /** 291 | * Disconnect from adapter 292 | */ 293 | async disconnect() { 294 | /* istanbul ignore next */ 295 | throw new Error("This method is not implemented."); 296 | } 297 | 298 | /** 299 | * Subscribe to a channel. 300 | * 301 | * @param {Channel} chan 302 | * @param {Service} svc 303 | */ 304 | async subscribe(chan, svc) { 305 | /* istanbul ignore next */ 306 | throw new Error("This method is not implemented."); 307 | } 308 | 309 | /** 310 | * Unsubscribe from a channel. 311 | * 312 | * @param {Channel} chan 313 | */ 314 | async unsubscribe(chan) { 315 | /* istanbul ignore next */ 316 | throw new Error("This method is not implemented."); 317 | } 318 | 319 | /** 320 | * Publish a payload to a channel. 321 | * @param {String} channelName 322 | * @param {any} payload 323 | * @param {Object?} opts 324 | */ 325 | async publish(channelName, payload, opts) { 326 | /* istanbul ignore next */ 327 | throw new Error("This method is not implemented."); 328 | } 329 | 330 | /** 331 | * Parse the headers from incoming message to a POJO. 332 | * @param {any} raw 333 | * @returns {object} 334 | */ 335 | parseMessageHeaders(raw) { 336 | return raw ? raw.headers : null; 337 | } 338 | } 339 | 340 | module.exports = BaseAdapter; 341 | -------------------------------------------------------------------------------- /src/adapters/fake.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @moleculer/channels 3 | * Copyright (c) 2022 MoleculerJS (https://github.com/moleculerjs/channels) 4 | * MIT Licensed 5 | */ 6 | 7 | "use strict"; 8 | 9 | const _ = require("lodash"); 10 | const BaseAdapter = require("./base"); 11 | const C = require("../constants"); 12 | 13 | /** 14 | * @typedef {import("moleculer").ServiceBroker} ServiceBroker Moleculer Service Broker instance 15 | * @typedef {import("moleculer").Context} Context Context instance 16 | * @typedef {import("moleculer").Service} Service Service instance 17 | * @typedef {import("moleculer").LoggerInstance} Logger Logger instance 18 | * @typedef {import("../index").Channel} Channel Base channel definition 19 | * @typedef {import("./base").BaseDefaultOptions} BaseDefaultOptions Base adapter options 20 | */ 21 | 22 | /** 23 | * @typedef {Object} FakeOptions Fake Adapter configuration 24 | * @property {Number} servicePrefix Prefix for service names 25 | * @property {Number} eventPrefix Prefix for event names 26 | */ 27 | 28 | /** 29 | * Fake (Moleculer Event-based) adapter 30 | * 31 | * @class FakeAdapter 32 | * @extends {BaseAdapter} 33 | */ 34 | class FakeAdapter extends BaseAdapter { 35 | /** 36 | * Constructor of adapter. 37 | * 38 | * @param {Object?} opts 39 | */ 40 | constructor(opts) { 41 | if (_.isString(opts)) opts = {}; 42 | 43 | super(opts); 44 | 45 | /** @type {FakeOptions & BaseDefaultOptions} */ 46 | this.opts = _.defaultsDeep(this.opts, { 47 | servicePrefix: "$channel", 48 | eventPrefix: "channels" 49 | }); 50 | 51 | this.services = new Map(); 52 | 53 | this.stopping = false; 54 | } 55 | 56 | /** 57 | * Initialize the adapter. 58 | * 59 | * @param {ServiceBroker} broker 60 | * @param {Logger} logger 61 | */ 62 | init(broker, logger) { 63 | super.init(broker, logger); 64 | } 65 | 66 | /** 67 | * Connect to the adapter. 68 | */ 69 | async connect() { 70 | this.connected = true; 71 | } 72 | 73 | /** 74 | * Disconnect from adapter 75 | */ 76 | async disconnect() { 77 | this.connected = false; 78 | } 79 | 80 | /** 81 | * Subscribe to a channel with a handler. 82 | * 83 | * @param {Channel} chan 84 | * @param {Service} svc 85 | */ 86 | async subscribe(chan, svc) { 87 | this.logger.debug( 88 | `Subscribing to '${chan.name}' chan with '${chan.group}' group...'`, 89 | chan.id 90 | ); 91 | 92 | try { 93 | if (chan.maxInFlight == null) chan.maxInFlight = this.opts.maxInFlight; 94 | if (chan.maxRetries == null) chan.maxRetries = this.opts.maxRetries; 95 | chan.deadLettering = _.defaultsDeep({}, chan.deadLettering, this.opts.deadLettering); 96 | if (chan.deadLettering.enabled) { 97 | chan.deadLettering.queueName = this.addPrefixTopic(chan.deadLettering.queueName); 98 | } 99 | 100 | const schema = { 101 | name: 102 | this.opts.servicePrefix + 103 | ":" + 104 | svc.fullName + 105 | ":" + 106 | chan.name + 107 | ":" + 108 | chan.group, 109 | events: { 110 | [this.opts.eventPrefix + "." + chan.name]: { 111 | bulkhead: 112 | chan.maxInFlight != null 113 | ? { 114 | enabled: true, 115 | concurrency: chan.maxInFlight 116 | } 117 | : undefined, 118 | group: chan.group, 119 | handler: ctx => this.processMessage(chan, ctx) 120 | } 121 | } 122 | }; 123 | 124 | // Create a handler service 125 | const service = this.broker.createService(schema); 126 | this.services.set(chan.id, service); 127 | this.initChannelActiveMessages(chan.id); 128 | } catch (err) { 129 | this.logger.error( 130 | `Error while subscribing to '${chan.name}' chan with '${chan.group}' group`, 131 | err 132 | ); 133 | throw err; 134 | } 135 | } 136 | 137 | /** 138 | * Unsubscribe from a channel. 139 | * 140 | * @param {Channel} chan 141 | */ 142 | async unsubscribe(chan) { 143 | if (chan.unsubscribing) return; 144 | chan.unsubscribing = true; 145 | 146 | this.logger.debug(`Unsubscribing from '${chan.name}' chan with '${chan.group}' group...'`); 147 | 148 | const service = this.services.get(chan.id); 149 | if (service) { 150 | await this.broker.destroyService(service); 151 | this.stopChannelActiveMessages(chan.id); 152 | } 153 | } 154 | 155 | /** 156 | * Process incoming messages. 157 | * 158 | * @param {Channel} chan 159 | * @param {Context} ctx 160 | */ 161 | async processMessage(chan, ctx) { 162 | const { payload } = ctx.params; 163 | const id = ctx.id; 164 | 165 | try { 166 | this.addChannelActiveMessages(chan.id, [id]); 167 | await chan.handler(payload, ctx.params); 168 | // TODO: acking? 169 | this.removeChannelActiveMessages(chan.id, [id]); 170 | } catch (err) { 171 | this.logger.error(`Error while processing message`, err); 172 | this.removeChannelActiveMessages(chan.id, [id]); 173 | 174 | this.metricsIncrement(C.METRIC_CHANNELS_MESSAGES_ERRORS_TOTAL, chan); 175 | 176 | // TODO: retrying & dead letter? 177 | throw err; 178 | } 179 | } 180 | 181 | /** 182 | * Publish a payload to a channel. 183 | * 184 | * @param {String} channelName 185 | * @param {any} payload 186 | * @param {Object?} opts 187 | */ 188 | async publish(channelName, payload, opts = {}) { 189 | this.logger.debug(`Publish a message to '${channelName}' channel...`, payload, opts); 190 | 191 | this.broker.emit( 192 | this.opts.eventPrefix + "." + channelName, 193 | { payload, headers: opts.headers }, 194 | {} 195 | ); 196 | } 197 | } 198 | 199 | module.exports = FakeAdapter; 200 | -------------------------------------------------------------------------------- /src/adapters/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @moleculer/channels 3 | * Copyright (c) 2021 MoleculerJS (https://github.com/moleculerjs/channels) 4 | * MIT Licensed 5 | */ 6 | 7 | "use strict"; 8 | 9 | /** 10 | * @typedef {import("./base")} BaseAdapter 11 | */ 12 | const { isObject, isString } = require("lodash"); 13 | const { ServiceSchemaError } = require("moleculer").Errors; 14 | 15 | const Adapters = { 16 | Base: require("./base"), 17 | AMQP: require("./amqp"), 18 | Fake: require("./fake"), 19 | Kafka: require("./kafka"), 20 | NATS: require("./nats"), 21 | Redis: require("./redis") 22 | }; 23 | 24 | function getByName(name) { 25 | if (!name) return null; 26 | 27 | let n = Object.keys(Adapters).find(n => n.toLowerCase() == name.toLowerCase()); 28 | if (n) return Adapters[n]; 29 | } 30 | 31 | /** 32 | * Resolve adapter by name 33 | * 34 | * @param {object|string} opt 35 | * @returns {BaseAdapter} 36 | */ 37 | function resolve(opt) { 38 | if (opt instanceof Adapters.Base) { 39 | return opt; 40 | } else if (isString(opt)) { 41 | const AdapterClass = getByName(opt); 42 | if (AdapterClass) { 43 | return new AdapterClass(); 44 | } else if (opt.startsWith("redis://") || opt.startsWith("rediss://")) { 45 | return new Adapters.Redis(opt); 46 | } else if (opt.startsWith("amqp://") || opt.startsWith("amqps://")) { 47 | return new Adapters.AMQP(opt); 48 | } else if (opt.startsWith("kafka://")) { 49 | return new Adapters.Kafka(opt); 50 | } else if (opt.startsWith("nats://")) { 51 | return new Adapters.NATS(opt); 52 | } else { 53 | throw new ServiceSchemaError(`Invalid Adapter type '${opt}'.`, { type: opt }); 54 | } 55 | } else if (isObject(opt)) { 56 | const AdapterClass = getByName(opt.type || "Redis"); 57 | if (AdapterClass) { 58 | return new AdapterClass(opt.options); 59 | } else { 60 | throw new ServiceSchemaError(`Invalid Adapter type '${opt.type}'.`, { 61 | type: opt.type 62 | }); 63 | } 64 | } 65 | 66 | return new Adapters.Redis(); 67 | } 68 | 69 | /** 70 | * Register a new Channel Adapter 71 | * @param {String} name 72 | * @param {BaseAdapter} value 73 | */ 74 | function register(name, value) { 75 | Adapters[name] = value; 76 | } 77 | 78 | module.exports = Object.assign(Adapters, { resolve, register }); 79 | -------------------------------------------------------------------------------- /src/adapters/kafka.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @moleculer/channels 3 | * Copyright (c) 2021 MoleculerJS (https://github.com/moleculerjs/channels) 4 | * MIT Licensed 5 | */ 6 | 7 | "use strict"; 8 | 9 | const BaseAdapter = require("./base"); 10 | const _ = require("lodash"); 11 | const { MoleculerError, MoleculerRetryableError } = require("moleculer").Errors; 12 | const C = require("../constants"); 13 | const { INVALID_MESSAGE_SERIALIZATION_ERROR_CODE } = require("../constants"); 14 | /** Name of the partition where an error occurred while processing the message */ 15 | const HEADER_ORIGINAL_PARTITION = "x-original-partition"; 16 | 17 | /** 18 | * @typedef {import('kafkajs').Kafka} KafkaClient Kafka Client 19 | * @typedef {import('kafkajs').Producer} KafkaProducer Kafka Producer 20 | * @typedef {import('kafkajs').Consumer} KafkaConsumer Kafka Consumer 21 | * @typedef {import('kafkajs').KafkaConfig} KafkaConfig Kafka configuration 22 | * @typedef {import('kafkajs').ProducerConfig} ProducerConfig Kafka producer configuration 23 | * @typedef {import('kafkajs').ConsumerConfig} ConsumerConfig Kafka consumer configuration 24 | * @typedef {import('kafkajs').EachMessagePayload} EachMessagePayload Incoming message payload 25 | * @typedef {import("moleculer").ServiceBroker} ServiceBroker Moleculer Service Broker instance 26 | * @typedef {import("moleculer").Logger} Logger Logger instance 27 | * @typedef {import("../index").Channel} Channel Base channel definition 28 | * @typedef {import("./base").BaseDefaultOptions} BaseDefaultOptions Base adapter options 29 | */ 30 | 31 | /** 32 | * @typedef {Object} KafkaDefaultOptions Kafka Adapter configuration 33 | * @property {Number} maxInFlight Max-in-flight messages 34 | * @property {KafkaConfig} kafka Kafka config 35 | */ 36 | 37 | /** @type {KafkaClient} */ 38 | let Kafka; 39 | 40 | /** @type {import('kafkajs').logLevel} */ 41 | let KafkaJsLogLevel; 42 | 43 | function convertLogLevel(level) { 44 | switch (level) { 45 | case KafkaJsLogLevel.NOTHING: 46 | case KafkaJsLogLevel.ERROR: 47 | case KafkaJsLogLevel.WARN: 48 | return "warn"; 49 | case KafkaJsLogLevel.DEBUG: 50 | return "debug"; 51 | default: 52 | return "info"; 53 | } 54 | } 55 | 56 | /** 57 | * Kafka adapter 58 | * 59 | * @class KafkaAdapter 60 | * @extends {BaseAdapter} 61 | */ 62 | class KafkaAdapter extends BaseAdapter { 63 | /** 64 | * Constructor of adapter 65 | * @param {KafkaDefaultOptions|String?} opts 66 | */ 67 | constructor(opts) { 68 | if (_.isString(opts)) { 69 | opts = { 70 | kafka: { 71 | brokers: [opts.replace("kafka://", "")] 72 | } 73 | }; 74 | } 75 | 76 | super(opts); 77 | 78 | /** @type {Logger} */ 79 | this.kafkaLogger = null; 80 | 81 | /** @type {KafkaDefaultOptions & BaseDefaultOptions} */ 82 | this.opts = _.defaultsDeep(this.opts, { 83 | maxInFlight: 1, 84 | kafka: { 85 | brokers: ["localhost:9092"], 86 | logCreator: 87 | () => 88 | ({ namespace, level, log }) => { 89 | this.kafkaLogger[convertLogLevel(level)]( 90 | `[${namespace}${log.groupId != null ? ":" + log.groupId : ""}]`, 91 | log.message 92 | ); 93 | }, 94 | producerOptions: undefined, 95 | consumerOptions: undefined 96 | } 97 | }); 98 | 99 | /** @type {KafkaClient} */ 100 | this.client = null; 101 | 102 | /** @type {KafkaProducer} */ 103 | this.producer = null; 104 | 105 | /** 106 | * @type {Map} 107 | */ 108 | this.consumers = new Map(); 109 | 110 | this.connected = false; 111 | this.stopping = false; 112 | } 113 | 114 | /** 115 | * Initialize the adapter. 116 | * 117 | * @param {ServiceBroker} broker 118 | * @param {Logger} logger 119 | */ 120 | init(broker, logger) { 121 | super.init(broker, logger); 122 | 123 | try { 124 | Kafka = require("kafkajs").Kafka; 125 | KafkaJsLogLevel = require("kafkajs").logLevel; 126 | } catch (err) { 127 | /* istanbul ignore next */ 128 | this.broker.fatal( 129 | "The 'kafkajs' package is missing! Please install it with 'npm install kafkajs --save' command.", 130 | err, 131 | true 132 | ); 133 | } 134 | 135 | this.checkClientLibVersion("kafkajs", "^1.15.0 || ^2.0.0"); 136 | 137 | this.opts.kafka.clientId = this.opts.consumerName; 138 | 139 | this.kafkaLogger = this.broker.getLogger("Channels.KafkaJs"); 140 | } 141 | 142 | /** 143 | * Connect to the adapter with reconnecting logic 144 | */ 145 | connect() { 146 | return new Promise(resolve => { 147 | const doConnect = () => { 148 | this.tryConnect() 149 | .then(resolve) 150 | .catch(err => { 151 | this.logger.error("Unable to connect Kafka brokers.", err); 152 | setTimeout(() => { 153 | this.logger.info("Reconnecting..."); 154 | doConnect(); 155 | }, 2000); 156 | }); 157 | }; 158 | 159 | doConnect(); 160 | }); 161 | } 162 | 163 | /** 164 | * Trying connect to the adapter. 165 | */ 166 | async tryConnect() { 167 | this.logger.debug("Connecting to Kafka brokers...", this.opts.kafka.brokers); 168 | 169 | this.client = new Kafka(this.opts.kafka); 170 | 171 | this.producer = this.client.producer(this.opts.kafka.producerOptions); 172 | await this.producer.connect(); 173 | 174 | this.logger.info("Kafka adapter is connected."); 175 | 176 | this.connected = true; 177 | } 178 | 179 | /** 180 | * Disconnect from adapter 181 | */ 182 | async disconnect() { 183 | if (this.stopping) return; 184 | 185 | this.stopping = true; 186 | try { 187 | this.logger.info("Closing Kafka connection..."); 188 | if (this.producer) { 189 | await this.producer.disconnect(); 190 | this.producer = null; 191 | } 192 | 193 | await new Promise((resolve, reject) => { 194 | const checkPendingMessages = () => { 195 | if (this.getNumberOfTrackedChannels() === 0) { 196 | // Stop the publisher client 197 | // The subscriber clients are stopped in unsubscribe() method, which is called in serviceStopping() 198 | const promises = Array.from(this.consumers.values()).map(consumer => 199 | consumer.disconnect() 200 | ); 201 | 202 | return Promise.all(promises) 203 | .then(() => { 204 | // Release the pointers 205 | this.consumers = new Map(); 206 | }) 207 | .then(() => { 208 | this.connected = false; 209 | resolve(); 210 | }) 211 | .catch(err => reject(err)); 212 | } else { 213 | this.logger.warn( 214 | `Processing ${this.getNumberOfTrackedChannels()} active connections(s)...` 215 | ); 216 | 217 | setTimeout(checkPendingMessages, 1000); 218 | } 219 | }; 220 | 221 | setImmediate(checkPendingMessages); 222 | }); 223 | } catch (err) { 224 | this.logger.error("Error while closing Kafka connection.", err); 225 | } 226 | this.stopping = false; 227 | } 228 | 229 | /** 230 | * Subscribe to a channel. 231 | * 232 | * @param {Channel & KafkaDefaultOptions} chan 233 | */ 234 | async subscribe(chan) { 235 | this.logger.debug( 236 | `Subscribing to '${chan.name}' chan with '${chan.group}' group...'`, 237 | chan.id 238 | ); 239 | 240 | try { 241 | if (chan.maxInFlight == null) chan.maxInFlight = this.opts.maxInFlight; 242 | if (chan.maxRetries == null) chan.maxRetries = this.opts.maxRetries; 243 | chan.deadLettering = _.defaultsDeep({}, chan.deadLettering, this.opts.deadLettering); 244 | if (chan.deadLettering.enabled) { 245 | chan.deadLettering.queueName = this.addPrefixTopic(chan.deadLettering.queueName); 246 | chan.deadLettering.exchangeName = this.addPrefixTopic( 247 | chan.deadLettering.exchangeName 248 | ); 249 | } 250 | 251 | if (!chan.kafka) { 252 | chan.kafka = {}; 253 | } 254 | 255 | let consumer = this.client.consumer({ 256 | groupId: `${chan.group}:${chan.name}`, 257 | maxInFlightRequests: chan.maxInFlight, 258 | ...(this.opts.kafka.consumerOptions || {}), 259 | ...chan.kafka 260 | }); 261 | this.consumers.set(chan.id, consumer); 262 | await consumer.connect(); 263 | 264 | this.initChannelActiveMessages(chan.id); 265 | 266 | await consumer.subscribe({ topic: chan.name, fromBeginning: chan.kafka.fromBeginning }); 267 | 268 | await consumer.run({ 269 | autoCommit: false, 270 | partitionsConsumedConcurrently: chan.kafka.partitionsConsumedConcurrently, 271 | eachMessage: payload => this.processMessage(chan, consumer, payload) 272 | }); 273 | } catch (err) { 274 | this.logger.error( 275 | `Error while subscribing to '${chan.name}' chan with '${chan.group}' group`, 276 | err 277 | ); 278 | throw err; 279 | } 280 | } 281 | 282 | /** 283 | * Commit new offset to Kafka broker. 284 | * 285 | * @param {KafkaConsumer} consumer 286 | * @param {String} topic 287 | * @param {Number} partition 288 | * @param {String} offset 289 | */ 290 | async commitOffset(consumer, topic, partition, offset) { 291 | this.logger.debug("Committing new offset.", { topic, partition, offset }); 292 | await consumer.commitOffsets([{ topic, partition, offset }]); 293 | } 294 | 295 | /** 296 | * Process a message 297 | * 298 | * @param {Channel & KafkaDefaultOptions} chan 299 | * @param {KafkaConsumer} consumer 300 | * @param {EachMessagePayload} payload 301 | * @returns {Promise} 302 | */ 303 | async processMessage(chan, consumer, { topic, partition, message }) { 304 | // Service is stopping. Skip processing... 305 | if (chan.unsubscribing) return; 306 | 307 | this.logger.debug( 308 | `Kafka consumer received a message in '${chan.name}' queue. Processing...`, 309 | { 310 | topic, 311 | partition, 312 | offset: message.offset, 313 | headers: message.headers 314 | } 315 | ); 316 | 317 | const id = `${partition}:${message.offset}`; 318 | const newOffset = Number(message.offset) + 1; 319 | 320 | // Check group filtering 321 | if (message.headers && message.headers[C.HEADER_GROUP]) { 322 | const group = message.headers[C.HEADER_GROUP].toString(); 323 | if (group !== chan.group) { 324 | this.logger.debug( 325 | `The message is addressed to other group '${group}'. Current group: '${chan.group}'. Skipping...` 326 | ); 327 | // Acknowledge 328 | await this.commitOffset(consumer, topic, partition, newOffset); 329 | return; 330 | } 331 | } 332 | 333 | try { 334 | this.addChannelActiveMessages(chan.id, [id]); 335 | 336 | let content; 337 | try { 338 | content = this.serializer.deserialize(message.value); 339 | } catch (error) { 340 | const msg = `Failed to parse incoming message at '${chan.name}' channel. Incoming messages must use ${this.opts.serializer} serialization.`; 341 | throw new MoleculerError(msg, 400, INVALID_MESSAGE_SERIALIZATION_ERROR_CODE, { 342 | error 343 | }); 344 | } 345 | //this.logger.debug("Content:", content); 346 | 347 | await chan.handler(content, message); 348 | 349 | this.logger.info("Message is processed. Committing offset", { 350 | topic, 351 | partition, 352 | offset: newOffset 353 | }); 354 | // Acknowledge 355 | await this.commitOffset(consumer, topic, partition, newOffset); 356 | 357 | this.removeChannelActiveMessages(chan.id, [id]); 358 | } catch (err) { 359 | this.removeChannelActiveMessages(chan.id, [id]); 360 | 361 | this.metricsIncrement(C.METRIC_CHANNELS_MESSAGES_ERRORS_TOTAL, chan); 362 | 363 | this.logger.warn(`Kafka message processing error in '${chan.name}' queue.`, err); 364 | if (!chan.maxRetries) { 365 | if (chan.deadLettering.enabled) { 366 | // Reached max retries and has dead-letter topic, move message 367 | this.logger.debug( 368 | `No retries, moving message to '${chan.deadLettering.queueName}' queue...` 369 | ); 370 | await this.moveToDeadLetter( 371 | chan, 372 | { topic, partition, message }, 373 | this.transformErrorToHeaders(err) 374 | ); 375 | } else { 376 | // No retries, drop message 377 | this.logger.error(`No retries, drop message...`); 378 | } 379 | await this.commitOffset(consumer, topic, partition, newOffset); 380 | return; 381 | } 382 | 383 | let redeliveryCount = 384 | message.headers[C.HEADER_REDELIVERED_COUNT] != null 385 | ? Number(message.headers[C.HEADER_REDELIVERED_COUNT]) 386 | : 0; 387 | redeliveryCount++; 388 | if (chan.maxRetries > 0 && redeliveryCount >= chan.maxRetries) { 389 | if (chan.deadLettering.enabled) { 390 | // Reached max retries and has dead-letter topic, move message 391 | this.logger.debug( 392 | `Message redelivered too many times (${redeliveryCount}). Moving message to '${chan.deadLettering.queueName}' queue...` 393 | ); 394 | await this.moveToDeadLetter( 395 | chan, 396 | { topic, partition, message }, 397 | this.transformErrorToHeaders(err) 398 | ); 399 | } else { 400 | // Reached max retries and no dead-letter topic, drop message 401 | this.logger.error( 402 | `Message redelivered too many times (${redeliveryCount}). Drop message...` 403 | ); 404 | } 405 | } else { 406 | // Redeliver the message 407 | this.logger.warn( 408 | `Redeliver message into '${chan.name}' topic. Count: ${redeliveryCount}` 409 | ); 410 | 411 | await this.publish(chan.name, message.value, { 412 | raw: true, 413 | key: message.key, 414 | headers: Object.assign({}, message.headers, { 415 | [C.HEADER_REDELIVERED_COUNT]: redeliveryCount.toString(), 416 | [C.HEADER_GROUP]: chan.group 417 | }) 418 | }); 419 | 420 | this.metricsIncrement(C.METRIC_CHANNELS_MESSAGES_RETRIES_TOTAL, chan); 421 | } 422 | await this.commitOffset(consumer, topic, partition, newOffset); 423 | } 424 | } 425 | 426 | /** 427 | * Moves message into dead letter 428 | * 429 | * @param {Channel} chan 430 | * @param {Object} message message 431 | * @param {Record} [errorData] Optional error data to store as headers 432 | */ 433 | async moveToDeadLetter(chan, { partition, message }, errorData) { 434 | try { 435 | const headers = { 436 | ...(message.headers || {}), 437 | [C.HEADER_ORIGINAL_CHANNEL]: chan.name, 438 | [C.HEADER_ORIGINAL_GROUP]: chan.group, 439 | [HEADER_ORIGINAL_PARTITION]: "" + partition 440 | }; 441 | 442 | if (errorData) { 443 | Object.entries(errorData).forEach(([key, value]) => (headers[key] = value)); 444 | } 445 | 446 | // Remove original group filter after redelivery. 447 | delete headers[C.HEADER_GROUP]; 448 | 449 | await this.publish(chan.deadLettering.queueName, message.value, { 450 | raw: true, 451 | key: message.key, 452 | headers 453 | }); 454 | 455 | this.metricsIncrement(C.METRIC_CHANNELS_MESSAGES_DEAD_LETTERING_TOTAL, chan); 456 | 457 | this.logger.warn(`Moved message to '${chan.deadLettering.queueName}'`, message.key); 458 | } catch (error) { 459 | this.logger.info("An error occurred while moving", error); 460 | } 461 | } 462 | 463 | /** 464 | * Unsubscribe from a channel. 465 | * 466 | * @param {Channel & KafkaDefaultOptions} chan 467 | */ 468 | async unsubscribe(chan) { 469 | if (chan.unsubscribing) return; 470 | chan.unsubscribing = true; 471 | 472 | this.logger.debug(`Unsubscribing from '${chan.name}' chan with '${chan.group}' group...'`); 473 | 474 | const consumer = this.consumers.get(chan.id); 475 | if (!consumer) return; 476 | 477 | await new Promise((resolve, reject) => { 478 | const checkPendingMessages = () => { 479 | try { 480 | if (this.getNumberOfChannelActiveMessages(chan.id) === 0) { 481 | this.logger.debug( 482 | `Unsubscribing from '${chan.name}' chan with '${chan.group}' group...'` 483 | ); 484 | 485 | // Stop tracking channel's active messages 486 | this.stopChannelActiveMessages(chan.id); 487 | 488 | resolve(); 489 | } else { 490 | this.logger.warn( 491 | `Processing ${this.getNumberOfChannelActiveMessages( 492 | chan.id 493 | )} message(s) of '${chan.id}'...` 494 | ); 495 | 496 | setTimeout(() => checkPendingMessages(), 1000); 497 | } 498 | } catch (err) { 499 | reject(err); 500 | } 501 | }; 502 | 503 | checkPendingMessages(); 504 | }); 505 | 506 | // Disconnect consumer 507 | await consumer.disconnect(); 508 | 509 | // Remove consumer 510 | this.consumers.delete(chan.id); 511 | } 512 | 513 | /** 514 | * Publish a payload to a channel. 515 | * 516 | * @param {String} channelName 517 | * @param {any} payload 518 | * @param {Object?} opts 519 | * @param {Boolean?} opts.raw 520 | * @param {Buffer?|string?} opts.key 521 | * @param {Number?} opts.partition 522 | * @param {Object?} opts.headers 523 | */ 524 | async publish(channelName, payload, opts = {}) { 525 | // Adapter is stopping. Publishing no longer is allowed 526 | if (this.stopping) return; 527 | 528 | if (!this.connected) { 529 | throw new MoleculerRetryableError("Adapter not yet connected. Skipping publishing."); 530 | } 531 | 532 | this.logger.debug(`Publish a message to '${channelName}' topic...`, payload, opts); 533 | 534 | const data = opts.raw ? payload : this.serializer.serialize(payload); 535 | const res = await this.producer.send({ 536 | topic: channelName, 537 | messages: [ 538 | { key: opts.key, value: data, partition: opts.partition, headers: opts.headers } 539 | ], 540 | acks: opts.acks, 541 | timeout: opts.timeout, 542 | compression: opts.compression 543 | }); 544 | 545 | if (res.length == 0 || res[0].errorCode != 0) { 546 | throw new MoleculerError( 547 | `Unable to publish message to '${channelName}'. Error code: ${res[0].errorCode}`, 548 | 500, 549 | "UNABLE_PUBLISH", 550 | { channelName, result: res } 551 | ); 552 | } 553 | this.logger.debug(`Message was published at '${channelName}'`, res); 554 | } 555 | 556 | /** 557 | * Parse the headers from incoming message to a POJO. 558 | * @param {any} raw 559 | * @returns {object} 560 | */ 561 | parseMessageHeaders(raw) { 562 | if (raw.headers) { 563 | const res = {}; 564 | for (const [key, value] of Object.entries(raw.headers)) { 565 | res[key] = value != null ? value.toString() : null; 566 | } 567 | 568 | return res; 569 | } 570 | return null; 571 | } 572 | } 573 | 574 | module.exports = KafkaAdapter; 575 | -------------------------------------------------------------------------------- /src/adapters/nats.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @moleculer/channels 3 | * Copyright (c) 2021 MoleculerJS (https://github.com/moleculerjs/channels) 4 | * MIT Licensed 5 | */ 6 | 7 | "use strict"; 8 | 9 | const BaseAdapter = require("./base"); 10 | const _ = require("lodash"); 11 | const C = require("../constants"); 12 | const { INVALID_MESSAGE_SERIALIZATION_ERROR_CODE } = require("../constants"); 13 | const { transformErrorToHeaders } = require("../utils"); 14 | const { MoleculerRetryableError, MoleculerError } = require("moleculer").Errors; 15 | 16 | let NATS; 17 | 18 | /** 19 | * @typedef {import("nats").NatsConnection} NatsConnection NATS Connection 20 | * @typedef {import("nats").ConnectionOptions} ConnectionOptions NATS Connection Opts 21 | * @typedef {import("nats").StreamConfig} StreamConfig NATS Configuration Options 22 | * @typedef {import("nats").JetStreamManager} JetStreamManager NATS Jet Stream Manager 23 | * @typedef {import("nats").JetStreamClient} JetStreamClient NATS JetStream Client 24 | * @typedef {import("nats").JetStreamPublishOptions} JetStreamPublishOptions JetStream Publish Options 25 | * @typedef {import("nats").ConsumerOptsBuilder} ConsumerOptsBuilder NATS JetStream ConsumerOptsBuilder 26 | * @typedef {import("nats").ConsumerOpts} ConsumerOpts Jet Stream Consumer Opts 27 | * @typedef {import("nats").JetStreamOptions} JetStreamOptions Jet Stream Options 28 | * @typedef {import("nats").JsMsg} JsMsg Jet Stream Message 29 | * @typedef {import("nats").JetStreamSubscription} JetStreamSubscription Jet Stream Subscription 30 | * @typedef {import("nats").MsgHdrs} MsgHdrs Jet Stream Headers 31 | * @typedef {import("moleculer").ServiceBroker} ServiceBroker Moleculer Service Broker instance 32 | * @typedef {import("moleculer").Logger} Logger Logger instance 33 | * @typedef {import("../index").Channel} Channel Base channel definition 34 | * @typedef {import("./base").BaseDefaultOptions} BaseDefaultOptions Base adapter options 35 | */ 36 | 37 | /** 38 | * @typedef {Object} NatsDefaultOptions 39 | * @property {Object} nats NATS lib configuration 40 | * @property {String} url String containing the URL to NATS server 41 | * @property {ConnectionOptions} nats.connectionOptions 42 | * @property {StreamConfig} nats.streamConfig More info: https://docs.nats.io/jetstream/concepts/streams 43 | * @property {ConsumerOpts} nats.consumerOptions More info: https://docs.nats.io/jetstream/concepts/consumers 44 | */ 45 | 46 | /** 47 | * NATS JetStream adapter 48 | * 49 | * More info: https://github.com/nats-io/nats.deno/blob/main/jetstream.md 50 | * More info: https://github.com/nats-io/nats-architecture-and-design#jetstream 51 | * More info: https://docs.nats.io/jetstream/concepts/ 52 | * 53 | * @class NatsAdapter 54 | * @extends {BaseAdapter} 55 | */ 56 | class NatsAdapter extends BaseAdapter { 57 | constructor(opts) { 58 | if (_.isString(opts)) opts = { url: opts }; 59 | 60 | super(opts); 61 | 62 | /** @type { BaseDefaultOptions & NatsDefaultOptions } */ 63 | this.opts = _.defaultsDeep(this.opts, { 64 | nats: { 65 | /** @type {ConnectionOptions} */ 66 | connectionOptions: {}, 67 | /** @type {Partial} More info: https://docs.nats.io/jetstream/concepts/streams */ 68 | streamConfig: {}, 69 | /** @type {ConsumerOpts} More info: https://docs.nats.io/jetstream/concepts/consumers */ 70 | consumerOptions: { 71 | // Manual ACK 72 | mack: true, 73 | config: { 74 | // More info: https://docs.nats.io/jetstream/concepts/consumers#deliverpolicy-optstartseq-optstarttime 75 | deliver_policy: "new", 76 | // More info: https://docs.nats.io/jetstream/concepts/consumers#ackpolicy 77 | ack_policy: "explicit", 78 | // More info: https://docs.nats.io/jetstream/concepts/consumers#maxackpending 79 | max_ack_pending: this.opts.maxInFlight 80 | } 81 | } 82 | } 83 | }); 84 | 85 | // Adapted from: https://github.com/moleculerjs/moleculer/blob/3f7e712a8ce31087c7d333ad9dbaf63617c8497b/src/transporters/nats.js#L141-L143 86 | if (this.opts.nats.url) 87 | this.opts.nats.connectionOptions.servers = this.opts.nats.url 88 | .split(",") 89 | .map(server => new URL(server).host); 90 | 91 | /** @type {NatsConnection} */ 92 | this.connection = null; 93 | 94 | /** @type {JetStreamManager} */ 95 | this.manager = null; 96 | 97 | /** @type {JetStreamClient} */ 98 | this.client = null; 99 | 100 | /** @type {Map} */ 101 | this.subscriptions = new Map(); 102 | } 103 | 104 | /** 105 | * Initialize the adapter. 106 | * 107 | * @param {ServiceBroker} broker 108 | * @param {Logger} logger 109 | */ 110 | init(broker, logger) { 111 | super.init(broker, logger); 112 | 113 | try { 114 | NATS = require("nats"); 115 | } catch (err) { 116 | /* istanbul ignore next */ 117 | this.broker.fatal( 118 | "The 'nats' package is missing! Please install it with 'npm install nats --save' command.", 119 | err, 120 | true 121 | ); 122 | } 123 | 124 | this.checkClientLibVersion("nats", "^2.2.0"); 125 | } 126 | 127 | /** 128 | * Connect to the adapter. 129 | */ 130 | async connect() { 131 | return new this.Promise(resolve => { 132 | const doConnect = async () => { 133 | try { 134 | this.connection = await NATS.connect(this.opts.nats.connectionOptions); 135 | 136 | this.manager = await this.connection.jetstreamManager(); 137 | 138 | this.client = this.connection.jetstream(); // JetStreamOptions 139 | 140 | this.connected = true; 141 | resolve(); 142 | } catch (err) { 143 | this.logger.error( 144 | "Error while connecting to NATS JetStream server.", 145 | err.message 146 | ); 147 | this.logger.debug(err); 148 | setTimeout(() => doConnect(), 5 * 1000); 149 | } 150 | }; 151 | 152 | doConnect(); 153 | }); 154 | } 155 | 156 | /** 157 | * Disconnect from adapter 158 | */ 159 | async disconnect() { 160 | this.stopping = true; 161 | 162 | try { 163 | if (this.connection) { 164 | this.logger.info("Closing NATS JetStream connection..."); 165 | await this.connection.drain(); 166 | await this.connection.close(); 167 | 168 | this.logger.info("NATS JetStream connection closed."); 169 | } 170 | } catch (error) { 171 | this.logger.error("Error while closing NATS JetStream connection.", error); 172 | } 173 | 174 | this.connected = false; 175 | } 176 | 177 | /** 178 | * Subscribe to a channel with a handler. 179 | * 180 | * @param {Channel & NatsDefaultOptions} chan 181 | */ 182 | async subscribe(chan) { 183 | this.logger.debug( 184 | `Subscribing to '${chan.name}' chan with '${chan.group}' group...'`, 185 | chan.id 186 | ); 187 | 188 | if (chan.maxInFlight == null) chan.maxInFlight = this.opts.maxInFlight; 189 | if (chan.maxRetries == null) chan.maxRetries = this.opts.maxRetries; 190 | 191 | chan.deadLettering = _.defaultsDeep({}, chan.deadLettering, this.opts.deadLettering); 192 | if (chan.deadLettering.enabled) { 193 | chan.deadLettering.queueName = this.addPrefixTopic(chan.deadLettering.queueName); 194 | } 195 | 196 | // 1. Create stream 197 | // NATS Stream name does not support: spaces, tabs, period (.), greater than (>) or asterisk (*) are prohibited. 198 | // More info: https://docs.nats.io/jetstream/administration/naming 199 | const streamName = chan.name.split(".").join("_"); 200 | await this.createStream(streamName, [chan.name], chan.nats ? chan.nats.streamConfig : {}); 201 | 202 | if (chan.deadLettering && chan.deadLettering.enabled) { 203 | const deadLetteringStreamName = chan.deadLettering.queueName.split(".").join("_"); 204 | await this.createStream( 205 | deadLetteringStreamName, 206 | [chan.deadLettering.queueName], 207 | chan.nats ? chan.nats.streamConfig : {} 208 | ); 209 | } 210 | 211 | // 2. Configure NATS consumer 212 | this.initChannelActiveMessages(chan.id); 213 | 214 | /** @type {ConsumerOpts} More info: https://docs.nats.io/jetstream/concepts/consumers */ 215 | const consumerOpts = _.defaultsDeep( 216 | {}, 217 | chan.nats ? chan.nats.consumerOptions : {}, 218 | this.opts.nats.consumerOptions 219 | ); 220 | 221 | consumerOpts.queue = streamName; 222 | consumerOpts.config.deliver_group = streamName; 223 | // NATS Stream name does not support: spaces, tabs, period (.), greater than (>) or asterisk (*) are prohibited. 224 | // More info: https://docs.nats.io/jetstream/administration/naming 225 | consumerOpts.config.durable_name = chan.group.split(".").join("_"); 226 | consumerOpts.config.deliver_subject = chan.id.replace(/[*|>]/g, "_"); 227 | consumerOpts.config.max_ack_pending = chan.maxInFlight; 228 | consumerOpts.callbackFn = this.createConsumerHandler(chan); 229 | 230 | // 3. Create a subscription 231 | try { 232 | const sub = await this.client.subscribe(chan.name, consumerOpts); 233 | this.subscriptions.set(chan.id, sub); 234 | } catch (err) { 235 | this.logger.error( 236 | `Error while subscribing to '${chan.name}' chan with '${chan.group}' group`, 237 | err 238 | ); 239 | throw err; 240 | } 241 | } 242 | 243 | /** 244 | * Creates the callback handler 245 | * 246 | * @param {Channel} chan 247 | * @returns 248 | */ 249 | createConsumerHandler(chan) { 250 | /** 251 | * @param {import("nats").NatsError} err 252 | * @param {JsMsg} message 253 | */ 254 | return async (err, message) => { 255 | // Service is stopping. Skip processing... 256 | if (chan.unsubscribing) return; 257 | 258 | // NATS "regular" message with stats. Not a JetStream message 259 | // Both err and message are "null" 260 | // More info: https://github.com/nats-io/nats.deno/blob/main/jetstream.md#callbacks 261 | if (err === null && message === null) return; 262 | 263 | if (err) { 264 | this.logger.error(err); 265 | return; 266 | } 267 | 268 | if (message) { 269 | this.addChannelActiveMessages(chan.id, [message.seq]); 270 | 271 | try { 272 | // Working on the message and thus prevent receiving the message again as a redelivery. 273 | message.working(); 274 | 275 | let content; 276 | try { 277 | content = this.serializer.deserialize(Buffer.from(message.data)); 278 | } catch (error) { 279 | const msg = `Failed to parse incoming message at '${chan.name}' channel. Incoming messages must use ${this.opts.serializer} serialization.`; 280 | throw new MoleculerError( 281 | msg, 282 | 400, 283 | INVALID_MESSAGE_SERIALIZATION_ERROR_CODE, 284 | { error } 285 | ); 286 | } 287 | 288 | await chan.handler(content, message); 289 | message.ack(); 290 | } catch (error) { 291 | this.logger.warn(`NATS message processing error in '${chan.name}'`, error); 292 | this.metricsIncrement(C.METRIC_CHANNELS_MESSAGES_ERRORS_TOTAL, chan); 293 | 294 | // Message rejected 295 | if (!chan.maxRetries) { 296 | // No retries 297 | 298 | if (chan.deadLettering.enabled) { 299 | this.logger.debug( 300 | `No retries, moving message to '${chan.deadLettering.queueName}' queue...` 301 | ); 302 | await this.moveToDeadLetter( 303 | chan, 304 | message, 305 | this.transformErrorToHeaders(error) 306 | ); 307 | } else { 308 | // Drop message 309 | this.logger.error(`No retries, drop message...`, message.seq); 310 | } 311 | 312 | message.ack(); 313 | } else if ( 314 | chan.maxRetries > 0 && 315 | message.info.redeliveryCount >= chan.maxRetries 316 | ) { 317 | // Retries enabled and limit reached 318 | 319 | if (chan.deadLettering.enabled) { 320 | this.logger.debug( 321 | `Message redelivered too many times (${message.info.redeliveryCount}). Moving message to '${chan.deadLettering.queueName}' queue...` 322 | ); 323 | await this.moveToDeadLetter( 324 | chan, 325 | message, 326 | this.transformErrorToHeaders(error) 327 | ); 328 | } else { 329 | // Drop message 330 | this.logger.error( 331 | `Message redelivered too many times (${message.info.redeliveryCount}). Drop message...`, 332 | message.seq 333 | ); 334 | // this.logger.error(`Drop message...`, message.seq); 335 | } 336 | 337 | message.ack(); 338 | } else { 339 | // Retries enabled but limit NOT reached 340 | // NACK the message for redelivery 341 | this.metricsIncrement(C.METRIC_CHANNELS_MESSAGES_RETRIES_TOTAL, chan); 342 | 343 | this.logger.debug(`NACKing message...`, message.seq); 344 | message.nak(); 345 | } 346 | } 347 | 348 | this.removeChannelActiveMessages(chan.id, [message.seq]); 349 | } 350 | }; 351 | } 352 | 353 | /** 354 | * Create a NATS Stream 355 | * 356 | * More info: https://docs.nats.io/jetstream/concepts/streams 357 | * 358 | * @param {String} streamName Name of the Stream 359 | * @param {Array} subjects A list of subjects/topics to store in a stream 360 | * @param {StreamConfig} streamOpts JetStream stream configs 361 | */ 362 | async createStream(streamName, subjects, streamOpts) { 363 | const streamConfig = _.defaultsDeep( 364 | { 365 | name: 366 | // Local stream config 367 | streamOpts && streamOpts.name 368 | ? streamOpts.name 369 | : // Global stream config 370 | this.opts.nats.streamConfig && this.opts.nats.streamConfig.name 371 | ? this.opts.nats.streamConfig.name 372 | : // Default 373 | streamName, 374 | 375 | subjects: 376 | // Local stream subjects 377 | streamOpts && streamOpts.subjects 378 | ? streamOpts.subjects 379 | : // Global stream subjects 380 | this.opts.nats.streamConfig && this.opts.nats.streamConfig.subjects 381 | ? this.opts.nats.streamConfig.subjects 382 | : // Default 383 | subjects 384 | }, 385 | streamOpts, 386 | this.opts.nats.streamConfig 387 | ); 388 | 389 | try { 390 | const streamInfo = await this.manager.streams.add(streamConfig); 391 | this.logger.debug("streamInfo:", streamInfo); 392 | return streamInfo; 393 | } catch (error) { 394 | if (error.message === "stream name already in use") { 395 | // Silently ignore the error. Channel or Consumer Group already exists 396 | this.logger.debug(`NATS Stream with name: '${streamName}' already exists.`); 397 | } else { 398 | this.logger.error("An error ocurred while create NATS Stream", error); 399 | } 400 | } 401 | } 402 | 403 | /** 404 | * Moves message into dead letter 405 | * 406 | * @param {Channel} chan 407 | * @param {JsMsg} message JetStream message 408 | * @param {Record} [errorData] Optional error data to store as headers 409 | */ 410 | async moveToDeadLetter(chan, message, errorData) { 411 | // this.logger.warn(`Moved message to '${chan.deadLettering.queueName}'`); 412 | try { 413 | /** @type {JetStreamPublishOptions} */ 414 | const opts = { 415 | raw: true, 416 | headers: { 417 | // Add info about original channel where error occurred 418 | [C.HEADER_ORIGINAL_CHANNEL]: chan.name, 419 | [C.HEADER_ORIGINAL_GROUP]: chan.group 420 | } 421 | }; 422 | 423 | if (errorData) { 424 | Object.entries(errorData).forEach(([key, value]) => (opts.headers[key] = value)); 425 | } 426 | 427 | await this.publish(chan.deadLettering.queueName, message.data, opts); 428 | 429 | this.metricsIncrement(C.METRIC_CHANNELS_MESSAGES_DEAD_LETTERING_TOTAL, chan); 430 | 431 | this.logger.warn(`Moved message to '${chan.deadLettering.queueName}'`, message.seq); 432 | } catch (error) { 433 | this.logger.info("An error occurred while moving", error); 434 | } 435 | } 436 | 437 | /** 438 | * Unsubscribe from a channel. 439 | * 440 | * @param {Channel} chan 441 | */ 442 | async unsubscribe(chan) { 443 | if (chan.unsubscribing) return; 444 | chan.unsubscribing = true; 445 | 446 | const sub = this.subscriptions.get(chan.id); 447 | if (!sub) return; 448 | 449 | await new Promise((resolve, reject) => { 450 | const checkPendingMessages = () => { 451 | try { 452 | if (this.getNumberOfChannelActiveMessages(chan.id) === 0) { 453 | // More info: https://github.com/nats-io/nats.deno/blob/main/jetstream.md#push-subscriptions 454 | return sub 455 | .drain() 456 | .then(() => sub.unsubscribe()) 457 | .then(() => { 458 | this.logger.debug( 459 | `Unsubscribing from '${chan.name}' chan with '${chan.group}' group...'` 460 | ); 461 | 462 | // Stop tracking channel's active messages 463 | this.stopChannelActiveMessages(chan.id); 464 | 465 | resolve(); 466 | }) 467 | .catch(err => reject(err)); 468 | } else { 469 | this.logger.warn( 470 | `Processing ${this.getNumberOfChannelActiveMessages( 471 | chan.id 472 | )} message(s) of '${chan.id}'...` 473 | ); 474 | 475 | setTimeout(() => checkPendingMessages(), 1000); 476 | } 477 | } catch (err) { 478 | reject(err); 479 | } 480 | }; 481 | 482 | checkPendingMessages(); 483 | }); 484 | } 485 | 486 | /** 487 | * Publish a payload to a channel. 488 | * 489 | * @param {String} channelName 490 | * @param {any} payload 491 | * @param {Partial?} opts 492 | */ 493 | async publish(channelName, payload, opts = {}) { 494 | // Adapter is stopping. Publishing no longer is allowed 495 | if (this.stopping) return; 496 | 497 | if (!this.connected) { 498 | throw new MoleculerRetryableError("Adapter not yet connected. Skipping publishing."); 499 | } 500 | 501 | try { 502 | // Remap headers into JetStream format 503 | if (opts.headers) { 504 | /** @type {MsgHdrs} */ 505 | let msgHdrs = NATS.headers(); 506 | 507 | Object.keys(opts.headers).forEach(key => { 508 | msgHdrs.set(key, opts.headers[key]); 509 | }); 510 | 511 | opts.headers = msgHdrs; 512 | } 513 | 514 | const response = await this.client.publish( 515 | channelName, 516 | opts.raw ? payload : this.serializer.serialize(payload), 517 | opts 518 | ); 519 | 520 | this.logger.debug(`Message ${response.seq} was published at '${channelName}'`); 521 | } catch (error) { 522 | this.logger.error(`An error ocurred while publishing message to ${channelName}`, error); 523 | throw error; 524 | } 525 | } 526 | 527 | /** 528 | * Parse the headers from incoming message to a POJO. 529 | * @param {any} raw 530 | * @returns {object} 531 | */ 532 | parseMessageHeaders(raw) { 533 | if (raw.headers) { 534 | const res = {}; 535 | for (const [key, values] of raw.headers) { 536 | res[key] = values[0]; 537 | } 538 | 539 | return res; 540 | } 541 | return null; 542 | } 543 | } 544 | 545 | module.exports = NatsAdapter; 546 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | /** Number of redelivery attempts */ 3 | HEADER_REDELIVERED_COUNT: "x-redelivered-count", 4 | /** Consumer group name */ 5 | HEADER_GROUP: "x-group", 6 | 7 | /** Name of the channel where an error occurred while processing the message */ 8 | HEADER_ORIGINAL_CHANNEL: "x-original-channel", 9 | /** Name of consumer group that could not process the message properly */ 10 | HEADER_ORIGINAL_GROUP: "x-original-group", 11 | 12 | /** Prefix for error-related headers */ 13 | HEADER_ERROR_PREFIX: "x-error-", 14 | /** Error message */ 15 | HEADER_ERROR_MESSAGE: "x-error-message", 16 | /** Error code */ 17 | HEADER_ERROR_CODE: "x-error-code", 18 | /** Error stack trace */ 19 | HEADER_ERROR_STACK: "x-error-stack", 20 | /** Error type */ 21 | HEADER_ERROR_TYPE: "x-error-type", 22 | /** Error data */ 23 | HEADER_ERROR_DATA: "x-error-data", 24 | /** Error name */ 25 | HEADER_ERROR_NAME: "x-error-name", 26 | /** Error retryable */ 27 | HEADER_ERROR_RETRYABLE: "x-error-retryable", 28 | /** Timestamp when the error happened */ 29 | HEADER_ERROR_TIMESTAMP: "x-error-timestamp", 30 | 31 | METRIC_CHANNELS_MESSAGES_SENT: "moleculer.channels.messages.sent", 32 | METRIC_CHANNELS_MESSAGES_TOTAL: "moleculer.channels.messages.total", 33 | METRIC_CHANNELS_MESSAGES_ACTIVE: "moleculer.channels.messages.active", 34 | METRIC_CHANNELS_MESSAGES_TIME: "moleculer.channels.messages.time", 35 | 36 | METRIC_CHANNELS_MESSAGES_ERRORS_TOTAL: "moleculer.channels.messages.errors.total", 37 | METRIC_CHANNELS_MESSAGES_RETRIES_TOTAL: "moleculer.channels.messages.retries.total", 38 | METRIC_CHANNELS_MESSAGES_DEAD_LETTERING_TOTAL: 39 | "moleculer.channels.messages.deadLettering.total", 40 | 41 | /** 42 | * Thrown when incoming messages cannot be deserialized 43 | * More context: https://github.com/moleculerjs/moleculer-channels/issues/76 44 | */ 45 | INVALID_MESSAGE_SERIALIZATION_ERROR_CODE: "INVALID_MESSAGE_SERIALIZATION" 46 | }; 47 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @moleculer/channels 3 | * Copyright (c) 2021 MoleculerJS (https://github.com/moleculerjs/channels) 4 | * MIT Licensed 5 | */ 6 | 7 | "use strict"; 8 | 9 | const _ = require("lodash"); 10 | const { Context, METRIC } = require("moleculer"); 11 | const { BrokerOptionsError, ServiceSchemaError, MoleculerError } = require("moleculer").Errors; 12 | const Adapters = require("./adapters"); 13 | const C = require("./constants"); 14 | 15 | /** 16 | * @typedef {import("moleculer").ServiceBroker} ServiceBroker Moleculer Service Broker instance 17 | * @typedef {import("moleculer").Logger} Logger Logger instance 18 | * @typedef {import("moleculer").Service} Service Moleculer service 19 | * @typedef {import("moleculer").Middleware} Middleware Moleculer middleware 20 | * @typedef {import("./adapters/base")} BaseAdapter Base adapter class 21 | */ 22 | 23 | /** 24 | * @typedef {Object} DeadLetteringOptions Dead-letter-queue options 25 | * @property {Boolean} enabled Enable dead-letter-queue 26 | * @property {String} queueName Name of the dead-letter queue 27 | * @property {String} exchangeName Name of the dead-letter exchange (only for AMQP adapter) 28 | * @property {Object} exchangeOptions Options for the dead-letter exchange (only for AMQP adapter) 29 | * @property {Object} queueOptions Options for the dead-letter queue (only for AMQP adapter) 30 | * @property {(error: Error) => Record} [transformErrorToHeaders] Function to convert Error object to a plain object 31 | * @property {(headers: Record) => Record} [transformHeadersToErrorData] Function to parse error info from headers 32 | * @property {Number} errorInfoTTL Time-to-live in seconds for error info storage (only for Redis adapter) 33 | */ 34 | 35 | /** 36 | * @typedef {Object} Channel Base consumer configuration 37 | * @property {String} id Consumer ID 38 | * @property {String} name Channel/Queue/Stream name 39 | * @property {String} group Consumer group name 40 | * @property {Boolean} context Create Moleculer Context 41 | * @property {Boolean} unsubscribing Flag denoting if service is stopping 42 | * @property {Number?} maxInFlight Maximum number of messages that can be processed simultaneously 43 | * @property {Number} maxRetries Maximum number of retries before sending the message to dead-letter-queue 44 | * @property {DeadLetteringOptions?} deadLettering Dead-letter-queue options 45 | * @property {Function} handler User defined handler 46 | */ 47 | 48 | /** 49 | * @typedef {Object} ChannelRegistryEntry Registry entry 50 | * @property {Service} svc Service instance class 51 | * @property {String} name Channel name 52 | * @property {Channel} chan Channel object 53 | */ 54 | 55 | /** 56 | * @typedef {Object} AdapterConfig 57 | * @property {String} type Adapter name 58 | * @property {import("./adapters/base").BaseDefaultOptions & import("./adapters/amqp").AmqpDefaultOptions & import("./adapters/kafka").KafkaDefaultOptions & import("./adapters/nats").NatsDefaultOptions & import("./adapters/redis").RedisDefaultOptions} options Adapter options 59 | */ 60 | 61 | /** 62 | * @typedef {Object} MiddlewareOptions Middleware options 63 | * @property {String|AdapterConfig} adapter Adapter name or connection string or configuration object. 64 | * @property {String?} schemaProperty Property name of channels definition in service schema. 65 | * @property {String?} sendMethodName Method name to send messages. 66 | * @property {String?} adapterPropertyName Property name of the adapter instance in broker instance. 67 | * @property {String?} channelHandlerTrigger Method name to add to service in order to trigger channel handlers. 68 | * @property {boolean?} context Using Moleculer context in channel handlers by default. 69 | */ 70 | 71 | /** 72 | * Initialize the Channels middleware. 73 | * 74 | * @param {MiddlewareOptions} mwOpts 75 | * @returns Middleware 76 | */ 77 | module.exports = function ChannelsMiddleware(mwOpts) { 78 | mwOpts = _.defaultsDeep({}, mwOpts, { 79 | adapter: null, 80 | schemaProperty: "channels", 81 | sendMethodName: "sendToChannel", 82 | adapterPropertyName: "channelAdapter", 83 | channelHandlerTrigger: "emitLocalChannelHandler", 84 | context: false 85 | }); 86 | 87 | /** @type {ServiceBroker} */ 88 | let broker; 89 | /** @type {Logger} */ 90 | let logger; 91 | /** @type {BaseAdapter} */ 92 | let adapter; 93 | let started = false; 94 | /** @type {Array}} */ 95 | let channelRegistry = []; 96 | 97 | /** 98 | * Register cannel 99 | * @param {Service} svc 100 | * @param {Channel} chan 101 | */ 102 | function registerChannel(svc, chan) { 103 | unregisterChannel(svc, chan); 104 | channelRegistry.push({ svc, name: chan.name, chan }); 105 | } 106 | 107 | /** 108 | * Remove channel from registry 109 | * @param {Service} svc 110 | * @param {Channel=} chan 111 | */ 112 | function unregisterChannel(svc, chan) { 113 | channelRegistry = channelRegistry.filter( 114 | item => !(item.svc.fullName == svc.fullName && (chan == null || chan.name == item.name)) 115 | ); 116 | } 117 | 118 | /** 119 | * 120 | * @param {ServiceBroker} broker 121 | */ 122 | function registerChannelMetrics(broker) { 123 | if (!broker.isMetricsEnabled()) return; 124 | 125 | broker.metrics.register({ 126 | type: METRIC.TYPE_COUNTER, 127 | name: C.METRIC_CHANNELS_MESSAGES_SENT, 128 | labelNames: ["channel"], 129 | rate: true, 130 | unit: "call" 131 | }); 132 | 133 | broker.metrics.register({ 134 | type: METRIC.TYPE_COUNTER, 135 | name: C.METRIC_CHANNELS_MESSAGES_TOTAL, 136 | labelNames: ["channel", "group"], 137 | rate: true, 138 | unit: "msg" 139 | }); 140 | 141 | broker.metrics.register({ 142 | type: METRIC.TYPE_GAUGE, 143 | name: C.METRIC_CHANNELS_MESSAGES_ACTIVE, 144 | labelNames: ["channel", "group"], 145 | rate: true, 146 | unit: "msg" 147 | }); 148 | 149 | broker.metrics.register({ 150 | type: METRIC.TYPE_HISTOGRAM, 151 | name: C.METRIC_CHANNELS_MESSAGES_TIME, 152 | labelNames: ["channel", "group"], 153 | quantiles: true, 154 | unit: "msg" 155 | }); 156 | } 157 | 158 | return { 159 | name: "Channels", 160 | 161 | /** 162 | * Create lifecycle hook of service 163 | * @param {ServiceBroker} _broker 164 | */ 165 | created(_broker) { 166 | broker = _broker; 167 | logger = broker.getLogger("Channels"); 168 | 169 | // Create adapter 170 | if (!mwOpts.adapter) 171 | throw new BrokerOptionsError("Channel adapter must be defined.", { opts: mwOpts }); 172 | 173 | adapter = Adapters.resolve(mwOpts.adapter); 174 | adapter.init(broker, logger); 175 | 176 | // Populate broker with new methods 177 | if (!broker[mwOpts.sendMethodName]) { 178 | broker[mwOpts.sendMethodName] = broker.wrapMethod( 179 | "sendToChannel", 180 | (channelName, payload, opts) => { 181 | broker.metrics.increment( 182 | C.METRIC_CHANNELS_MESSAGES_SENT, 183 | { channel: channelName }, 184 | 1 185 | ); 186 | 187 | // Transfer Context properties 188 | if (opts && opts.ctx) { 189 | if (!opts.headers) opts.headers = {}; 190 | 191 | opts.headers.$requestID = opts.ctx.requestID; 192 | opts.headers.$parentID = opts.ctx.id; 193 | opts.headers.$tracing = "" + opts.ctx.tracing; 194 | opts.headers.$level = "" + opts.ctx.level; 195 | if (opts.ctx.service) { 196 | opts.headers.$caller = opts.ctx.service.fullName; 197 | } 198 | 199 | if (opts.ctx.channelName) { 200 | opts.headers.$parentChannelName = opts.ctx.channelName; 201 | } 202 | 203 | // Serialize meta and headers 204 | opts.headers.$meta = adapter.serializer 205 | .serialize(opts.ctx.meta) 206 | .toString("base64"); 207 | 208 | if (opts.ctx.headers) { 209 | opts.headers.$headers = adapter.serializer 210 | .serialize(opts.ctx.headers) 211 | .toString("base64"); 212 | } 213 | 214 | delete opts.ctx; 215 | } 216 | 217 | return adapter.publish(adapter.addPrefixTopic(channelName), payload, opts); 218 | } 219 | ); 220 | } else { 221 | throw new BrokerOptionsError( 222 | `broker.${mwOpts.sendMethodName} method is already in use by another Channel middleware`, 223 | null 224 | ); 225 | } 226 | 227 | // Add adapter reference to the broker instance 228 | if (!broker[mwOpts.adapterPropertyName]) { 229 | broker[mwOpts.adapterPropertyName] = adapter; 230 | } else { 231 | throw new BrokerOptionsError( 232 | `broker.${mwOpts.adapterPropertyName} property is already in use by another Channel middleware`, 233 | null 234 | ); 235 | } 236 | 237 | registerChannelMetrics(broker); 238 | }, 239 | 240 | /** 241 | * Created lifecycle hook of service 242 | * 243 | * @param {Service} svc 244 | */ 245 | async serviceCreated(svc) { 246 | if (_.isPlainObject(svc.schema[mwOpts.schemaProperty])) { 247 | //svc.$channels = {}; 248 | // Process `channels` in the schema 249 | await broker.Promise.mapSeries( 250 | Object.entries(svc.schema[mwOpts.schemaProperty]), 251 | async ([name, def]) => { 252 | /** @type {Partial} */ 253 | let chan; 254 | 255 | if (_.isFunction(def)) { 256 | chan = { 257 | handler: def 258 | }; 259 | } else if (_.isPlainObject(def)) { 260 | chan = _.cloneDeep(def); 261 | } else { 262 | throw new ServiceSchemaError( 263 | `Invalid channel definition in '${name}' channel in '${svc.fullName}' service!` 264 | ); 265 | } 266 | 267 | if (!_.isFunction(chan.handler)) { 268 | throw new ServiceSchemaError( 269 | `Missing channel handler on '${name}' channel in '${svc.fullName}' service!` 270 | ); 271 | } 272 | 273 | if (!chan.name) chan.name = adapter.addPrefixTopic(name); 274 | if (!chan.group) chan.group = svc.fullName; 275 | if (chan.context == null) chan.context = mwOpts.context; 276 | 277 | // Consumer ID 278 | chan.id = adapter.addPrefixTopic( 279 | `${broker.nodeID}.${svc.fullName}.${chan.name}` 280 | ); 281 | chan.unsubscribing = false; 282 | 283 | // Wrap the original handler 284 | let handler = broker.Promise.method(chan.handler).bind(svc); 285 | 286 | // Wrap the handler with custom middlewares 287 | const handler2 = broker.middlewares.wrapHandler( 288 | "localChannel", 289 | handler, 290 | chan 291 | ); 292 | 293 | let wrappedHandler = handler2; 294 | 295 | // Wrap the handler with context creating 296 | if (chan.context) { 297 | wrappedHandler = (msg, raw) => { 298 | let parentCtx, caller, meta, ctxHeaders, parentChannelName; 299 | const headers = adapter.parseMessageHeaders(raw); 300 | if (headers) { 301 | if (headers.$requestID) { 302 | parentCtx = { 303 | id: headers.$parentID, 304 | requestID: headers.$requestID, 305 | tracing: headers.$tracing === "true", 306 | level: headers.$level ? parseInt(headers.$level) : 0 307 | }; 308 | caller = headers.$caller; 309 | parentChannelName = headers.$parentChannelName; 310 | } 311 | 312 | if (headers.$meta) { 313 | meta = adapter.serializer.deserialize( 314 | Buffer.from(headers.$meta, "base64") 315 | ); 316 | } 317 | 318 | if (headers.$headers) { 319 | ctxHeaders = adapter.serializer.deserialize( 320 | Buffer.from(headers.$headers, "base64") 321 | ); 322 | } 323 | 324 | if ( 325 | Object.keys(headers).some(key => 326 | key.startsWith(C.HEADER_ERROR_PREFIX) 327 | ) 328 | ) { 329 | ctxHeaders = ctxHeaders || {}; 330 | 331 | ctxHeaders = { 332 | ...ctxHeaders, 333 | ...adapter.transformHeadersToErrorData(headers) 334 | }; 335 | } 336 | } 337 | 338 | const ctx = Context.create(broker, null, msg, { 339 | parentCtx, 340 | caller, 341 | meta, 342 | headers: ctxHeaders 343 | }); 344 | 345 | ctx.channelName = chan.name; 346 | ctx.parentChannelName = parentChannelName; 347 | 348 | // Attach current service that has the channel handler to the context 349 | ctx.service = svc; 350 | 351 | return handler2(ctx, raw); 352 | }; 353 | } 354 | 355 | chan.handler = wrappedHandler; 356 | 357 | // Add metrics for the handler 358 | if (broker.isMetricsEnabled()) { 359 | chan.handler = (...args) => { 360 | const labels = { channel: name, group: chan.group }; 361 | const timeEnd = broker.metrics.timer( 362 | C.METRIC_CHANNELS_MESSAGES_TIME, 363 | labels 364 | ); 365 | broker.metrics.increment(C.METRIC_CHANNELS_MESSAGES_TOTAL, labels); 366 | broker.metrics.increment(C.METRIC_CHANNELS_MESSAGES_ACTIVE, labels); 367 | return wrappedHandler(...args) 368 | .then(res => { 369 | timeEnd(); 370 | broker.metrics.decrement( 371 | C.METRIC_CHANNELS_MESSAGES_ACTIVE, 372 | labels 373 | ); 374 | return res; 375 | }) 376 | .catch(err => { 377 | timeEnd(); 378 | broker.metrics.decrement( 379 | C.METRIC_CHANNELS_MESSAGES_ACTIVE, 380 | labels 381 | ); 382 | 383 | throw err; 384 | }); 385 | }; 386 | } 387 | 388 | //svc.$channels[name] = chan; 389 | logger.debug( 390 | `Registering '${chan.name}' channel in '${svc.fullName}' service with group '${chan.group}'...` 391 | ); 392 | registerChannel(svc, chan); 393 | 394 | if (started) { 395 | // If middleware has already started, we should subscribe to the channel right now. 396 | await adapter.subscribe(chan, svc); 397 | } 398 | } 399 | ); 400 | 401 | // Attach method to simplify unit testing 402 | if (!svc[mwOpts.channelHandlerTrigger]) { 403 | /** 404 | * Call a local channel event handler. Useful for unit tests. 405 | * 406 | * @param {String} channelName 407 | * @param {Object} payload 408 | * @param {Object} raw 409 | * @returns 410 | */ 411 | svc[mwOpts.channelHandlerTrigger] = (channelName, payload, raw) => { 412 | svc.logger.debug( 413 | `${mwOpts.channelHandlerTrigger} called '${channelName}' channel handler` 414 | ); 415 | 416 | if (!svc.schema[mwOpts.schemaProperty][channelName]) 417 | return Promise.reject( 418 | new MoleculerError( 419 | `'${channelName}' is not registered as local channel event handler`, 420 | 500, 421 | "NOT_FOUND_CHANNEL", 422 | { channelName } 423 | ) 424 | ); 425 | 426 | // Shorthand definition 427 | if (typeof svc.schema[mwOpts.schemaProperty][channelName] === "function") 428 | return svc.schema[mwOpts.schemaProperty][channelName].call( 429 | svc, // Attach reference to service 430 | payload, 431 | raw 432 | ); 433 | 434 | // Object definition 435 | return svc.schema[mwOpts.schemaProperty][channelName].handler.call( 436 | svc, // Attach reference to service 437 | payload, 438 | raw 439 | ); 440 | }; 441 | } else { 442 | throw new BrokerOptionsError( 443 | `service.${mwOpts.channelHandlerTrigger} method is already in use by another Channel middleware`, 444 | null 445 | ); 446 | } 447 | } 448 | }, 449 | 450 | /** 451 | * Service stopping lifecycle hook. 452 | * Need to unsubscribe from the channels. 453 | * 454 | * @param {Service} svc 455 | */ 456 | async serviceStopping(svc) { 457 | await Promise.all( 458 | channelRegistry 459 | .filter(item => item.svc.fullName == svc.fullName) 460 | .map(async ({ chan }) => { 461 | await adapter.unsubscribe(chan); 462 | }) 463 | ); 464 | unregisterChannel(svc); 465 | }, 466 | 467 | /** 468 | * This hook is called after broker starting. 469 | */ 470 | async starting() { 471 | logger.info("Channel adapter is connecting..."); 472 | await adapter.connect(); 473 | logger.debug("Channel adapter connected."); 474 | 475 | logger.info(`Subscribing to ${channelRegistry.length} channels...`); 476 | await broker.Promise.mapSeries( 477 | channelRegistry, 478 | async ({ chan, svc }) => await adapter.subscribe(chan, svc) 479 | ); 480 | 481 | started = true; 482 | }, 483 | 484 | /** 485 | * This hook is called after broker stopped. 486 | */ 487 | async stopped() { 488 | logger.info("Channel adapter is disconnecting..."); 489 | await adapter.disconnect(); 490 | logger.debug("Channel adapter disconnected."); 491 | 492 | started = false; 493 | } 494 | }; 495 | }; 496 | -------------------------------------------------------------------------------- /src/tracing.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @moleculer/channels 3 | * Copyright (c) 2023 MoleculerJS (https://github.com/moleculerjs/channels) 4 | * MIT Licensed 5 | */ 6 | 7 | "use strict"; 8 | 9 | const _ = require("lodash"); 10 | const { isFunction, isPlainObject, safetyObject } = require("moleculer").Utils; 11 | 12 | module.exports = function TracingMiddleware() { 13 | let broker, tracer; 14 | 15 | function tracingLocalChannelMiddleware(handler, chan) { 16 | let opts = chan.tracing; 17 | if (opts === true || opts === false) opts = { enabled: !!opts }; 18 | opts = _.defaultsDeep({}, opts, { enabled: true }); 19 | 20 | if (broker.isTracingEnabled() && opts.enabled) { 21 | return function tracingLocalChannelMiddleware(ctx, ...rest) { 22 | ctx.requestID = ctx.requestID || tracer.getCurrentTraceID(); 23 | ctx.parentID = ctx.parentID || tracer.getActiveSpanID(); 24 | 25 | let tags = { 26 | callingLevel: ctx.level, 27 | chan: { 28 | name: chan.name, 29 | group: chan.group 30 | }, 31 | remoteCall: ctx.nodeID !== broker.nodeID, 32 | callerNodeID: ctx.nodeID, 33 | nodeID: broker.nodeID, 34 | /*options: { 35 | timeout: ctx.options.timeout, 36 | retries: ctx.options.retries 37 | },*/ 38 | requestID: ctx.requestID 39 | }; 40 | let actionTags; 41 | // local action tags take precedence 42 | if (isFunction(opts.tags)) { 43 | actionTags = opts.tags; 44 | } else { 45 | // By default all params are captured. This can be overridden globally and locally 46 | actionTags = { ...{ params: true }, ...opts.tags }; 47 | } 48 | 49 | if (isFunction(actionTags)) { 50 | const res = actionTags.call(ctx.service, ctx); 51 | if (res) Object.assign(tags, res); 52 | } else if (isPlainObject(actionTags)) { 53 | if (actionTags.params === true) 54 | tags.params = 55 | ctx.params != null && isPlainObject(ctx.params) 56 | ? Object.assign({}, ctx.params) 57 | : ctx.params; 58 | else if (Array.isArray(actionTags.params)) 59 | tags.params = _.pick(ctx.params, actionTags.params); 60 | 61 | if (actionTags.meta === true) 62 | tags.meta = ctx.meta != null ? Object.assign({}, ctx.meta) : ctx.meta; 63 | else if (Array.isArray(actionTags.meta)) 64 | tags.meta = _.pick(ctx.meta, actionTags.meta); 65 | } 66 | 67 | if (opts.safetyTags) { 68 | tags = safetyObject(tags); 69 | } 70 | 71 | let spanName = `channel '${chan.name}'`; 72 | if (opts.spanName) { 73 | switch (typeof opts.spanName) { 74 | case "string": 75 | spanName = opts.spanName; 76 | break; 77 | case "function": 78 | spanName = opts.spanName.call(ctx.service, ctx); 79 | break; 80 | } 81 | } 82 | 83 | const span = ctx.startSpan(spanName, { 84 | id: ctx.id, 85 | type: "channel", 86 | traceID: ctx.requestID, 87 | parentID: ctx.parentID, 88 | service: ctx.service, 89 | sampled: ctx.tracing, 90 | tags 91 | }); 92 | 93 | ctx.tracing = span.sampled; 94 | 95 | // Call the handler 96 | return handler(ctx, ...rest) 97 | .then(res => { 98 | ctx.finishSpan(span); 99 | return res; 100 | }) 101 | .catch(err => { 102 | span.setError(err); 103 | ctx.finishSpan(span); 104 | throw err; 105 | }); 106 | }.bind(this); 107 | } 108 | 109 | return handler; 110 | } 111 | 112 | return { 113 | name: "ChannelTracing", 114 | created(_broker) { 115 | broker = _broker; 116 | tracer = broker.tracer; 117 | }, 118 | 119 | localChannel: tracingLocalChannelMiddleware 120 | }; 121 | }; 122 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | const { 2 | HEADER_ERROR_MESSAGE, 3 | HEADER_ERROR_STACK, 4 | HEADER_ERROR_CODE, 5 | HEADER_ERROR_TYPE, 6 | HEADER_ERROR_DATA, 7 | HEADER_ERROR_NAME, 8 | HEADER_ERROR_RETRYABLE, 9 | HEADER_ERROR_TIMESTAMP, 10 | HEADER_ERROR_PREFIX 11 | } = require("./constants"); 12 | 13 | const strToBase64 = str => Buffer.from(str).toString("base64"); 14 | const fromBase64ToStr = str => Buffer.from(str, "base64").toString("utf-8"); 15 | 16 | /** 17 | * Converts data to base64 string 18 | * 19 | * @param {Object|number|string|boolean} data 20 | * @returns {string} 21 | */ 22 | const toBase64 = data => { 23 | if (typeof data === "string") return strToBase64(data); 24 | 25 | if (typeof data === "object") return strToBase64(JSON.stringify(data)); 26 | if (typeof data === "number") return strToBase64(data.toString()); 27 | if (typeof data === "boolean") return strToBase64(data ? "true" : "false"); 28 | 29 | throw new Error("Unsupported data type"); 30 | }; 31 | 32 | /** 33 | * Parses base64 string to original data type (Object, number, string, boolean) 34 | * 35 | * @param {string} b64str 36 | * @returns {Object|number|string|boolean} 37 | */ 38 | const parseBase64 = b64str => { 39 | const str = fromBase64ToStr(b64str); 40 | try { 41 | return JSON.parse(str); 42 | } catch { 43 | if (str === "true") return true; 44 | if (str === "false") return false; 45 | const num = Number(str); 46 | if (!isNaN(num)) return num; 47 | return str; 48 | } 49 | }; 50 | 51 | const parseStringData = str => { 52 | if (str === "null") return null; 53 | if (str === "undefined") return undefined; 54 | if (str === "true") return true; 55 | if (str === "false") return false; 56 | const num = Number(str); 57 | if (!isNaN(num)) return num; 58 | return str; 59 | }; 60 | 61 | /** 62 | * Converts Error object to a plain object 63 | * @param {any} err 64 | * @returns {Record|null} 65 | */ 66 | const transformErrorToHeaders = err => { 67 | if (!err) return null; 68 | 69 | let errorHeaders = { 70 | // primitive properties 71 | ...(err.message ? { [HEADER_ERROR_MESSAGE]: err.message.toString() } : {}), 72 | ...(err.code ? { [HEADER_ERROR_CODE]: err.code.toString() } : {}), 73 | ...(err.type ? { [HEADER_ERROR_TYPE]: err.type.toString() } : {}), 74 | ...(err.name ? { [HEADER_ERROR_NAME]: err.name.toString() } : {}), 75 | ...(typeof err.retryable === "boolean" 76 | ? { [HEADER_ERROR_RETRYABLE]: err.retryable.toString() } 77 | : {}), 78 | 79 | // complex properties 80 | // Encode to base64 because of special characters For example, NATS JetStream does not support \n or \r in headers 81 | ...(err.stack ? { [HEADER_ERROR_STACK]: toBase64(err.stack) } : {}), 82 | ...(err.data ? { [HEADER_ERROR_DATA]: toBase64(err.data) } : {}) 83 | }; 84 | 85 | if (Object.keys(errorHeaders).length === 0) return null; 86 | 87 | errorHeaders[HEADER_ERROR_TIMESTAMP] = Date.now().toString(); 88 | 89 | return errorHeaders; 90 | }; 91 | 92 | /** 93 | * Parses error info from headers and attempts to reconstruct original data types 94 | * 95 | * @param {Record} headers 96 | * @returns {Record} 97 | */ 98 | const transformHeadersToErrorData = headers => { 99 | if (!headers || typeof headers !== "object") return null; 100 | 101 | const complexPropertiesList = [HEADER_ERROR_STACK, HEADER_ERROR_DATA]; 102 | 103 | let errorInfo = {}; 104 | 105 | for (let key in headers) { 106 | if (!key.startsWith(HEADER_ERROR_PREFIX)) continue; 107 | 108 | errorInfo[key] = complexPropertiesList.includes(key) 109 | ? parseBase64(headers[key]) 110 | : (errorInfo[key] = parseStringData(headers[key])); 111 | } 112 | 113 | return errorInfo; 114 | }; 115 | 116 | module.exports = { 117 | transformErrorToHeaders, 118 | parseBase64, 119 | toBase64, 120 | transformHeadersToErrorData 121 | }; 122 | -------------------------------------------------------------------------------- /test/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | nats: 3 | image: nats:2-alpine 4 | ports: 5 | - "4222:4222" 6 | command: "-js" 7 | 8 | redis: 9 | image: redis:7-alpine 10 | ports: 11 | - "6379:6379" 12 | 13 | redis-node-1: 14 | image: docker.io/bitnamilegacy/redis-cluster:7.0 15 | ports: 16 | - "6381:6379" 17 | environment: 18 | - "ALLOW_EMPTY_PASSWORD=yes" 19 | - "REDIS_NODES=redis-node-1 redis-node-2 redis-node-3" 20 | - "REDIS_CLUSTER_REPLICAS=0" 21 | - "REDIS_CLUSTER_CREATOR=yes" 22 | 23 | redis-node-2: 24 | image: docker.io/bitnamilegacy/redis-cluster:7.0 25 | ports: 26 | - "6382:6379" 27 | environment: 28 | - "ALLOW_EMPTY_PASSWORD=yes" 29 | - "REDIS_NODES=redis-node-1 redis-node-2 redis-node-3" 30 | 31 | redis-node-3: 32 | image: docker.io/bitnamilegacy/redis-cluster:7.0 33 | ports: 34 | - "6383:6379" 35 | environment: 36 | - "ALLOW_EMPTY_PASSWORD=yes" 37 | - "REDIS_NODES=redis-node-1 redis-node-2 redis-node-3" 38 | 39 | #redis-cluster-init: 40 | # image: redis:6.2 41 | # restart: 'no' 42 | # depends_on: 43 | # - redis-node-1 44 | # - redis-node-2 45 | # - redis-node-3 46 | # entrypoint: [] 47 | # command: 48 | # - /bin/bash 49 | # - -c 50 | # - redis-cli --cluster create redis-node-1:6379 redis-node-2:6379 redis-node-3:6379 --cluster-replicas 0 --cluster-yes 51 | 52 | rabbitmq: 53 | image: rabbitmq:3-management 54 | ports: 55 | - "5672:5672" 56 | - "15672:15672" 57 | 58 | zookeeper: 59 | image: bitnamilegacy/zookeeper:3.9 60 | environment: 61 | - ALLOW_ANONYMOUS_LOGIN=yes 62 | ports: 63 | - "2181:2181" 64 | 65 | kafka: 66 | image: bitnamilegacy/kafka:3.9 67 | environment: 68 | - KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper:2181 69 | - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP=PLAINTEXT:PLAINTEXT,EXTERNAL:PLAINTEXT 70 | - KAFKA_LISTENERS=PLAINTEXT://:9092,EXTERNAL://:9093 71 | - KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://kafka:9092,EXTERNAL://127.0.0.1:9093 72 | - KAFKA_INTER_BROKER_LISTENER_NAME=PLAINTEXT 73 | - ALLOW_PLAINTEXT_LISTENER=yes 74 | - KAFKA_ENABLE_KRAFT=no 75 | depends_on: 76 | - zookeeper 77 | ports: 78 | - "9092:9092" 79 | - "9093:9093" 80 | -------------------------------------------------------------------------------- /test/unit/index.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { ServiceBroker } = require("moleculer"); 4 | const ChannelMiddleware = require("./../../").Middleware; 5 | 6 | describe("Test service 'channelHandlerTrigger' method", () => { 7 | const serviceSchema = { 8 | name: "helper", 9 | 10 | channels: { 11 | async "helper.sum"(payload) { 12 | // Calls the sum method 13 | return this.sum(payload.a, payload.b); 14 | }, 15 | 16 | "helper.subtract": { 17 | handler(payload) { 18 | return this.subtract(payload.a, payload.b); 19 | } 20 | } 21 | }, 22 | 23 | methods: { 24 | sum(a, b) { 25 | return a + b; 26 | }, 27 | 28 | subtract(a, b) { 29 | return a - b; 30 | } 31 | } 32 | }; 33 | 34 | describe("Test service default value", () => { 35 | let broker = new ServiceBroker({ 36 | logger: false, 37 | middlewares: [ 38 | ChannelMiddleware({ 39 | adapter: { 40 | type: "Fake" 41 | } 42 | }) 43 | ] 44 | }); 45 | let service = broker.createService(serviceSchema); 46 | beforeAll(() => broker.start()); 47 | afterAll(() => broker.stop()); 48 | 49 | it("should register default 'emitLocalChannelHandler' function declaration", async () => { 50 | // Mock the "sum" method 51 | service.sum = jest.fn(); 52 | 53 | // Call the "helper.sum" handler 54 | await service.emitLocalChannelHandler("helper.sum", { a: 5, b: 5 }); 55 | // Check if "sum" method was called 56 | expect(service.sum).toBeCalledTimes(1); 57 | expect(service.sum).toBeCalledWith(5, 5); 58 | 59 | // Restore the "sum" method 60 | service.sum.mockRestore(); 61 | }); 62 | 63 | it("should register default 'emitLocalChannelHandler' object declaration", async () => { 64 | // Mock the "sum" method 65 | service.subtract = jest.fn(); 66 | 67 | // Call the "helper.sum" handler 68 | await service.emitLocalChannelHandler("helper.subtract", { a: 5, b: 5 }); 69 | // Check if "subtract" method was called 70 | expect(service.subtract).toBeCalledTimes(1); 71 | expect(service.subtract).toBeCalledWith(5, 5); 72 | 73 | // Restore the "subtract" method 74 | service.subtract.mockRestore(); 75 | }); 76 | }); 77 | 78 | describe("Test service custom value", () => { 79 | let broker = new ServiceBroker({ 80 | logger: false, 81 | middlewares: [ 82 | ChannelMiddleware({ 83 | channelHandlerTrigger: "myTrigger", 84 | adapter: { 85 | type: "Fake" 86 | } 87 | }) 88 | ] 89 | }); 90 | let service = broker.createService(serviceSchema); 91 | beforeAll(() => broker.start()); 92 | afterAll(() => broker.stop()); 93 | 94 | it("should register with 'myTrigger'", async () => { 95 | // Mock the "sum" method 96 | service.sum = jest.fn(); 97 | 98 | // Call the "helper.sum" handler 99 | await service.myTrigger("helper.sum", { a: 5, b: 5 }); 100 | // Check if "sum" method was called 101 | expect(service.sum).toBeCalledTimes(1); 102 | expect(service.sum).toBeCalledWith(5, 5); 103 | 104 | // Restore the "sum" method 105 | service.sum.mockRestore(); 106 | }); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "strict": false, 5 | "checkJs": true, 6 | "declaration": true, 7 | "declarationDir": "types", 8 | "outDir": "dist", 9 | }, 10 | "include": ["**.js"], 11 | "exclude": ["node_modules", ".eslintrc.js", "prettier.config.js", "types"] 12 | } 13 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | export function Middleware(mwOpts: MiddlewareOptions): { 2 | name: string; 3 | created(_broker: ServiceBroker): void; 4 | serviceCreated(svc: Service): Promise; 5 | serviceStopping(svc: Service): Promise; 6 | started(): Promise; 7 | stopped(): Promise; 8 | }; 9 | export let Tracing: () => { 10 | name: string; 11 | created(_broker: any): void; 12 | localChannel: (handler: any, chan: any) => any; 13 | }; 14 | export let Adapters: { 15 | Base: typeof import("./src/adapters/base"); 16 | AMQP: typeof import("./src/adapters/amqp"); 17 | Fake: typeof import("./src/adapters/fake"); 18 | Kafka: typeof import("./src/adapters/kafka"); 19 | NATS: typeof import("./src/adapters/nats"); 20 | Redis: typeof import("./src/adapters/redis"); 21 | } & { 22 | resolve: (opt: object | string) => BaseAdapter; 23 | register: (name: string, value: BaseAdapter) => void; 24 | }; 25 | -------------------------------------------------------------------------------- /types/src/adapters/amqp.d.ts: -------------------------------------------------------------------------------- 1 | export = AmqpAdapter; 2 | /** 3 | * @typedef {import('amqplib').Connection} AMQPLibConnection AMQP connection 4 | * @typedef {import('amqplib').Channel} AMQPLibChannel AMQP Channel. More info: http://www.squaremobius.net/amqp.node/channel_api.html#channel 5 | * @typedef {import("moleculer").ServiceBroker} ServiceBroker Moleculer Service Broker instance 6 | * @typedef {import("moleculer").LoggerInstance} Logger Logger instance 7 | * @typedef {import("../index").Channel} Channel Base channel definition 8 | * @typedef {import("./base").BaseDefaultOptions} BaseDefaultOptions Base adapter options 9 | */ 10 | /** 11 | * @typedef {Object} AmqpDefaultOptions AMQP Adapter configuration 12 | * @property {Number} maxInFlight Max-in-flight messages 13 | * @property {Object} amqp AMQP lib configuration 14 | * @property {String|String[]} amqp.url Connection URI 15 | * @property {Object} amqp.socketOptions AMQP lib socket configuration 16 | * @property {Object} amqp.queueOptions AMQP lib queue configuration 17 | * @property {Object} amqp.exchangeOptions AMQP lib exchange configuration 18 | * @property {Object} amqp.messageOptions AMQP lib message configuration 19 | * @property {Object} amqp.consumerOptions AMQP lib consume configuration 20 | * @property {publishAssertExchange} amqp.publishAssertExchange AMQP lib exchange configuration for one-time calling assertExchange() before publishing in new exchange by sendToChannel 21 | */ 22 | /** 23 | * @typedef {Object} publishAssertExchange 24 | * @property {Boolean} enabled Enable/disable one-time calling channel.assertExchange() before publishing in new exchange by sendToChannel 25 | * @property {Object} exchangeOptions AMQP lib exchange configuration https://amqp-node.github.io/amqplib/channel_api.html#channel_assertExchange 26 | */ 27 | /** 28 | * @typedef {Object} SubscriptionEntry 29 | * @property {Channel & AmqpDefaultOptions} chan AMQP Channel 30 | * @property {String} consumerTag AMQP consumer tag. More info: https://www.rabbitmq.com/consumers.html#consumer-tags 31 | */ 32 | /** 33 | * AMQP adapter for RabbitMQ 34 | * 35 | * TODO: rewrite to using RabbitMQ Streams 36 | * https://www.rabbitmq.com/streams.html 37 | * 38 | * @class AmqpAdapter 39 | * @extends {BaseAdapter} 40 | */ 41 | declare class AmqpAdapter extends BaseAdapter { 42 | /** @type {AMQPLibConnection} */ 43 | connection: AMQPLibConnection; 44 | /** @type {AMQPLibChannel} */ 45 | channel: AMQPLibChannel; 46 | clients: Map; 47 | /** 48 | * @type {Map} 49 | */ 50 | subscriptions: Map; 51 | stopping: boolean; 52 | connectAttempt: number; 53 | connectionCount: number; 54 | /** 55 | * @type {Set} 56 | */ 57 | assertedExchanges: Set; 58 | /** 59 | * Connect to the adapter with reconnecting logic 60 | */ 61 | connect(): Promise; 62 | /** 63 | * Trying connect to the adapter. 64 | */ 65 | tryConnect(): Promise; 66 | /** 67 | * Subscribe to a channel. 68 | * 69 | * @param {Channel & AmqpDefaultOptions} chan 70 | */ 71 | subscribe(chan: Channel & AmqpDefaultOptions): Promise; 72 | /** 73 | * Create a handler for the consumer. 74 | * 75 | * @param {Channel & AmqpDefaultOptions} chan 76 | * @returns {Function} 77 | */ 78 | createConsumerHandler(chan: Channel & AmqpDefaultOptions): Function; 79 | /** 80 | * Moves message into dead letter 81 | * 82 | * @param {Channel & AmqpDefaultOptions} chan 83 | * @param {Object} msg 84 | */ 85 | moveToDeadLetter(chan: Channel & AmqpDefaultOptions, msg: any): Promise; 86 | /** 87 | * Unsubscribe from a channel. 88 | * 89 | * @param {Channel & AmqpDefaultOptions} chan 90 | */ 91 | unsubscribe(chan: Channel & AmqpDefaultOptions): Promise; 92 | /** 93 | * Resubscribe to all channels. 94 | * @returns {Promise} 95 | */ 96 | resubscribeAllChannels(): Promise; 97 | /** 98 | * Publish a payload to a channel. 99 | * 100 | * @param {String} channelName 101 | * @param {any} payload 102 | * @param {Object?} opts 103 | */ 104 | publish(channelName: string, payload: any, opts?: any | null): Promise; 105 | } 106 | declare namespace AmqpAdapter { 107 | export { AMQPLibConnection, AMQPLibChannel, ServiceBroker, Logger, Channel, BaseDefaultOptions, AmqpDefaultOptions, publishAssertExchange, SubscriptionEntry }; 108 | } 109 | import BaseAdapter = require("./base"); 110 | /** 111 | * AMQP connection 112 | */ 113 | type AMQPLibConnection = any; 114 | /** 115 | * AMQP Channel. More info: http://www.squaremobius.net/amqp.node/channel_api.html#channel 116 | */ 117 | type AMQPLibChannel = any; 118 | /** 119 | * Moleculer Service Broker instance 120 | */ 121 | type ServiceBroker = import("moleculer").ServiceBroker; 122 | /** 123 | * Logger instance 124 | */ 125 | type Logger = import("moleculer").LoggerInstance; 126 | /** 127 | * Base channel definition 128 | */ 129 | type Channel = import("../index").Channel; 130 | /** 131 | * Base adapter options 132 | */ 133 | type BaseDefaultOptions = import("./base").BaseDefaultOptions; 134 | /** 135 | * AMQP Adapter configuration 136 | */ 137 | type AmqpDefaultOptions = { 138 | /** 139 | * Max-in-flight messages 140 | */ 141 | maxInFlight: number; 142 | /** 143 | * AMQP lib configuration 144 | */ 145 | amqp: { 146 | url: string | string[]; 147 | socketOptions: any; 148 | queueOptions: any; 149 | exchangeOptions: any; 150 | messageOptions: any; 151 | consumerOptions: any; 152 | publishAssertExchange: publishAssertExchange; 153 | }; 154 | }; 155 | type publishAssertExchange = { 156 | /** 157 | * Enable/disable one-time calling channel.assertExchange() before publishing in new exchange by sendToChannel 158 | */ 159 | enabled: boolean; 160 | /** 161 | * AMQP lib exchange configuration https://amqp-node.github.io/amqplib/channel_api.html#channel_assertExchange 162 | */ 163 | exchangeOptions: any; 164 | }; 165 | type SubscriptionEntry = { 166 | /** 167 | * AMQP Channel 168 | */ 169 | chan: Channel & AmqpDefaultOptions; 170 | /** 171 | * AMQP consumer tag. More info: https://www.rabbitmq.com/consumers.html#consumer-tags 172 | */ 173 | consumerTag: string; 174 | }; 175 | -------------------------------------------------------------------------------- /types/src/adapters/base.d.ts: -------------------------------------------------------------------------------- 1 | export = BaseAdapter; 2 | /** 3 | * @typedef {import("moleculer").ServiceBroker} ServiceBroker Moleculer Service Broker instance 4 | * @typedef {import("moleculer").Service} Service Moleculer Service definition 5 | * @typedef {import("moleculer").LoggerInstance} Logger Logger instance 6 | * @typedef {import("moleculer").Serializer} Serializer Moleculer Serializer 7 | * @typedef {import("../index").Channel} Channel Base channel definition 8 | * @typedef {import("../index").DeadLetteringOptions} DeadLetteringOptions Dead-letter-queue options 9 | */ 10 | /** 11 | * @typedef {Object} BaseDefaultOptions Base Adapter configuration 12 | * @property {String?} prefix Adapter prefix 13 | * @property {String} consumerName Name of the consumer 14 | * @property {String} serializer Type of serializer to use in message exchange. Defaults to JSON 15 | * @property {Number} maxRetries Maximum number of retries before sending the message to dead-letter-queue or drop 16 | * @property {Number} maxInFlight Maximum number of messages that can be processed in parallel. 17 | * @property {DeadLetteringOptions} deadLettering Dead-letter-queue options 18 | */ 19 | declare class BaseAdapter { 20 | /** 21 | * Constructor of adapter 22 | * @param {Object?} opts 23 | */ 24 | constructor(opts: any | null); 25 | /** @type {BaseDefaultOptions} */ 26 | opts: BaseDefaultOptions; 27 | /** 28 | * Tracks the messages that are still being processed by different clients 29 | * @type {Map>} 30 | */ 31 | activeMessages: Map>; 32 | /** @type {Boolean} Flag indicating the adapter's connection status */ 33 | connected: boolean; 34 | /** 35 | * Initialize the adapter. 36 | * 37 | * @param {ServiceBroker} broker 38 | * @param {Logger} logger 39 | */ 40 | init(broker: ServiceBroker, logger: Logger): void; 41 | broker: import("moleculer").ServiceBroker; 42 | logger: import("moleculer").LoggerInstance; 43 | Promise: PromiseConstructorLike; 44 | /** @type {Serializer} */ 45 | serializer: Serializer; 46 | /** 47 | * Register adapter related metrics 48 | * @param {ServiceBroker} broker 49 | */ 50 | registerAdapterMetrics(broker: ServiceBroker): void; 51 | /** 52 | * 53 | * @param {String} metricName 54 | * @param {Channel} chan 55 | */ 56 | metricsIncrement(metricName: string, chan: Channel): void; 57 | /** 58 | * Check the installed client library version. 59 | * https://github.com/npm/node-semver#usage 60 | * 61 | * @param {String} library 62 | * @param {String} requiredVersions 63 | * @returns {Boolean} 64 | */ 65 | checkClientLibVersion(library: string, requiredVersions: string): boolean; 66 | /** 67 | * Init active messages list for tracking messages of a channel 68 | * @param {string} channelID 69 | * @param {Boolean?} toThrow Throw error if already exists 70 | */ 71 | initChannelActiveMessages(channelID: string, toThrow?: boolean | null): void; 72 | /** 73 | * Remove active messages list of a channel 74 | * @param {string} channelID 75 | */ 76 | stopChannelActiveMessages(channelID: string): void; 77 | /** 78 | * Add IDs of the messages that are currently being processed 79 | * 80 | * @param {string} channelID Channel ID 81 | * @param {Array} IDs List of IDs 82 | */ 83 | addChannelActiveMessages(channelID: string, IDs: Array): void; 84 | /** 85 | * Remove IDs of the messages that were already processed 86 | * 87 | * @param {string} channelID Channel ID 88 | * @param {string[]|number[]} IDs List of IDs 89 | */ 90 | removeChannelActiveMessages(channelID: string, IDs: string[] | number[]): void; 91 | /** 92 | * Get the number of active messages of a channel 93 | * 94 | * @param {string} channelID Channel ID 95 | */ 96 | getNumberOfChannelActiveMessages(channelID: string): number; 97 | /** 98 | * Get the number of channels 99 | */ 100 | getNumberOfTrackedChannels(): number; 101 | /** 102 | * Given a topic name adds the prefix 103 | * 104 | * @param {String} topicName 105 | * @returns {String} New topic name 106 | */ 107 | addPrefixTopic(topicName: string): string; 108 | /** 109 | * Connect to the adapter. 110 | */ 111 | connect(): Promise; 112 | /** 113 | * Disconnect from adapter 114 | */ 115 | disconnect(): Promise; 116 | /** 117 | * Subscribe to a channel. 118 | * 119 | * @param {Channel} chan 120 | * @param {Service} svc 121 | */ 122 | subscribe(chan: Channel, svc: Service): Promise; 123 | /** 124 | * Unsubscribe from a channel. 125 | * 126 | * @param {Channel} chan 127 | */ 128 | unsubscribe(chan: Channel): Promise; 129 | /** 130 | * Publish a payload to a channel. 131 | * @param {String} channelName 132 | * @param {any} payload 133 | * @param {Object?} opts 134 | */ 135 | publish(channelName: string, payload: any, opts: any | null): Promise; 136 | /** 137 | * Parse the headers from incoming message to a POJO. 138 | * @param {any} raw 139 | * @returns {object} 140 | */ 141 | parseMessageHeaders(raw: any): object; 142 | } 143 | declare namespace BaseAdapter { 144 | export { ServiceBroker, Service, Logger, Serializer, Channel, DeadLetteringOptions, BaseDefaultOptions }; 145 | } 146 | /** 147 | * Moleculer Service Broker instance 148 | */ 149 | type ServiceBroker = import("moleculer").ServiceBroker; 150 | /** 151 | * Moleculer Service definition 152 | */ 153 | type Service = import("moleculer").Service; 154 | /** 155 | * Logger instance 156 | */ 157 | type Logger = import("moleculer").LoggerInstance; 158 | /** 159 | * Moleculer Serializer 160 | */ 161 | type Serializer = import("moleculer").Serializer; 162 | /** 163 | * Base channel definition 164 | */ 165 | type Channel = import("../index").Channel; 166 | /** 167 | * Dead-letter-queue options 168 | */ 169 | type DeadLetteringOptions = import("../index").DeadLetteringOptions; 170 | /** 171 | * Base Adapter configuration 172 | */ 173 | type BaseDefaultOptions = { 174 | /** 175 | * Adapter prefix 176 | */ 177 | prefix: string | null; 178 | /** 179 | * Name of the consumer 180 | */ 181 | consumerName: string; 182 | /** 183 | * Type of serializer to use in message exchange. Defaults to JSON 184 | */ 185 | serializer: string; 186 | /** 187 | * Maximum number of retries before sending the message to dead-letter-queue or drop 188 | */ 189 | maxRetries: number; 190 | /** 191 | * Maximum number of messages that can be processed in parallel. 192 | */ 193 | maxInFlight: number; 194 | /** 195 | * Dead-letter-queue options 196 | */ 197 | deadLettering: DeadLetteringOptions; 198 | }; 199 | -------------------------------------------------------------------------------- /types/src/adapters/fake.d.ts: -------------------------------------------------------------------------------- 1 | export = FakeAdapter; 2 | /** 3 | * @typedef {import("moleculer").ServiceBroker} ServiceBroker Moleculer Service Broker instance 4 | * @typedef {import("moleculer").Context} Context Context instance 5 | * @typedef {import("moleculer").Service} Service Service instance 6 | * @typedef {import("moleculer").LoggerInstance} Logger Logger instance 7 | * @typedef {import("../index").Channel} Channel Base channel definition 8 | * @typedef {import("./base").BaseDefaultOptions} BaseDefaultOptions Base adapter options 9 | */ 10 | /** 11 | * @typedef {Object} FakeOptions Fake Adapter configuration 12 | * @property {Number} servicePrefix Prefix for service names 13 | * @property {Number} eventPrefix Prefix for event names 14 | */ 15 | /** 16 | * Fake (Moleculer Event-based) adapter 17 | * 18 | * @class FakeAdapter 19 | * @extends {BaseAdapter} 20 | */ 21 | declare class FakeAdapter extends BaseAdapter { 22 | services: Map; 23 | stopping: boolean; 24 | /** 25 | * Process incoming messages. 26 | * 27 | * @param {Channel} chan 28 | * @param {Context} ctx 29 | */ 30 | processMessage(chan: Channel, ctx: Context): Promise; 31 | /** 32 | * Publish a payload to a channel. 33 | * 34 | * @param {String} channelName 35 | * @param {any} payload 36 | * @param {Object?} opts 37 | */ 38 | publish(channelName: string, payload: any, opts?: any | null): Promise; 39 | } 40 | declare namespace FakeAdapter { 41 | export { ServiceBroker, Context, Service, Logger, Channel, BaseDefaultOptions, FakeOptions }; 42 | } 43 | import BaseAdapter = require("./base"); 44 | /** 45 | * Moleculer Service Broker instance 46 | */ 47 | type ServiceBroker = import("moleculer").ServiceBroker; 48 | /** 49 | * Context instance 50 | */ 51 | type Context = import("moleculer").Context; 52 | /** 53 | * Service instance 54 | */ 55 | type Service = import("moleculer").Service; 56 | /** 57 | * Logger instance 58 | */ 59 | type Logger = import("moleculer").LoggerInstance; 60 | /** 61 | * Base channel definition 62 | */ 63 | type Channel = import("../index").Channel; 64 | /** 65 | * Base adapter options 66 | */ 67 | type BaseDefaultOptions = import("./base").BaseDefaultOptions; 68 | /** 69 | * Fake Adapter configuration 70 | */ 71 | type FakeOptions = { 72 | /** 73 | * Prefix for service names 74 | */ 75 | servicePrefix: number; 76 | /** 77 | * Prefix for event names 78 | */ 79 | eventPrefix: number; 80 | }; 81 | -------------------------------------------------------------------------------- /types/src/adapters/index.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace _exports { 2 | export { BaseAdapter }; 3 | } 4 | declare const _exports: { 5 | Base: typeof import("./base"); 6 | AMQP: typeof import("./amqp"); 7 | Fake: typeof import("./fake"); 8 | Kafka: typeof import("./kafka"); 9 | NATS: typeof import("./nats"); 10 | Redis: typeof import("./redis"); 11 | } & { 12 | resolve: typeof resolve; 13 | register: typeof register; 14 | }; 15 | export = _exports; 16 | type BaseAdapter = import("./base"); 17 | /** 18 | * Resolve adapter by name 19 | * 20 | * @param {object|string} opt 21 | * @returns {BaseAdapter} 22 | */ 23 | declare function resolve(opt: object | string): BaseAdapter; 24 | /** 25 | * Register a new Channel Adapter 26 | * @param {String} name 27 | * @param {BaseAdapter} value 28 | */ 29 | declare function register(name: string, value: BaseAdapter): void; 30 | -------------------------------------------------------------------------------- /types/src/adapters/kafka.d.ts: -------------------------------------------------------------------------------- 1 | export = KafkaAdapter; 2 | /** 3 | * Kafka adapter 4 | * 5 | * @class KafkaAdapter 6 | * @extends {BaseAdapter} 7 | */ 8 | declare class KafkaAdapter extends BaseAdapter { 9 | /** 10 | * Constructor of adapter 11 | * @param {KafkaDefaultOptions|String?} opts 12 | */ 13 | constructor(opts: KafkaDefaultOptions | (string | null)); 14 | /** @type {Logger} */ 15 | kafkaLogger: Logger; 16 | /** @type {KafkaClient} */ 17 | client: KafkaClient; 18 | /** @type {KafkaProducer} */ 19 | producer: KafkaProducer; 20 | /** 21 | * @type {Map} 22 | */ 23 | consumers: Map; 24 | stopping: boolean; 25 | /** 26 | * Connect to the adapter with reconnecting logic 27 | */ 28 | connect(): Promise; 29 | /** 30 | * Trying connect to the adapter. 31 | */ 32 | tryConnect(): Promise; 33 | /** 34 | * Subscribe to a channel. 35 | * 36 | * @param {Channel & KafkaDefaultOptions} chan 37 | */ 38 | subscribe(chan: Channel & KafkaDefaultOptions): Promise; 39 | /** 40 | * Commit new offset to Kafka broker. 41 | * 42 | * @param {KafkaConsumer} consumer 43 | * @param {String} topic 44 | * @param {Number} partition 45 | * @param {String} offset 46 | */ 47 | commitOffset(consumer: KafkaConsumer, topic: string, partition: number, offset: string): Promise; 48 | /** 49 | * Process a message 50 | * 51 | * @param {Channel & KafkaDefaultOptions} chan 52 | * @param {KafkaConsumer} consumer 53 | * @param {EachMessagePayload} payload 54 | * @returns {Promise} 55 | */ 56 | processMessage(chan: Channel & KafkaDefaultOptions, consumer: KafkaConsumer, { topic, partition, message }: EachMessagePayload): Promise; 57 | /** 58 | * Moves message into dead letter 59 | * 60 | * @param {Channel} chan 61 | * @param {Object} message message 62 | */ 63 | moveToDeadLetter(chan: Channel, { partition, message }: any): Promise; 64 | /** 65 | * Unsubscribe from a channel. 66 | * 67 | * @param {Channel & KafkaDefaultOptions} chan 68 | */ 69 | unsubscribe(chan: Channel & KafkaDefaultOptions): Promise; 70 | /** 71 | * Publish a payload to a channel. 72 | * 73 | * @param {String} channelName 74 | * @param {any} payload 75 | * @param {Object?} opts 76 | * @param {Boolean?} opts.raw 77 | * @param {Buffer?|string?} opts.key 78 | * @param {Number?} opts.partition 79 | * @param {Object?} opts.headers 80 | */ 81 | publish(channelName: string, payload: any, opts?: any | null): Promise; 82 | } 83 | declare namespace KafkaAdapter { 84 | export { KafkaClient, KafkaProducer, KafkaConsumer, KafkaConfig, ProducerConfig, ConsumerConfig, EachMessagePayload, ServiceBroker, Logger, Channel, BaseDefaultOptions, KafkaDefaultOptions }; 85 | } 86 | import BaseAdapter = require("./base"); 87 | /** 88 | * Kafka Client 89 | */ 90 | type KafkaClient = import("kafkajs").Kafka; 91 | /** 92 | * Kafka Producer 93 | */ 94 | type KafkaProducer = import("kafkajs").Producer; 95 | /** 96 | * Kafka Consumer 97 | */ 98 | type KafkaConsumer = import("kafkajs").Consumer; 99 | /** 100 | * Kafka configuration 101 | */ 102 | type KafkaConfig = import("kafkajs").KafkaConfig; 103 | /** 104 | * Kafka producer configuration 105 | */ 106 | type ProducerConfig = import("kafkajs").ProducerConfig; 107 | /** 108 | * Kafka consumer configuration 109 | */ 110 | type ConsumerConfig = import("kafkajs").ConsumerConfig; 111 | /** 112 | * Incoming message payload 113 | */ 114 | type EachMessagePayload = import("kafkajs").EachMessagePayload; 115 | /** 116 | * Moleculer Service Broker instance 117 | */ 118 | type ServiceBroker = import("moleculer").ServiceBroker; 119 | /** 120 | * Logger instance 121 | */ 122 | type Logger = import("moleculer").LoggerInstance; 123 | /** 124 | * Base channel definition 125 | */ 126 | type Channel = import("../index").Channel; 127 | /** 128 | * Base adapter options 129 | */ 130 | type BaseDefaultOptions = import("./base").BaseDefaultOptions; 131 | /** 132 | * Kafka Adapter configuration 133 | */ 134 | type KafkaDefaultOptions = { 135 | /** 136 | * Max-in-flight messages 137 | */ 138 | maxInFlight: number; 139 | /** 140 | * Kafka config 141 | */ 142 | kafka: KafkaConfig; 143 | }; 144 | -------------------------------------------------------------------------------- /types/src/adapters/nats.d.ts: -------------------------------------------------------------------------------- 1 | export = NatsAdapter; 2 | /** 3 | * @typedef {import("nats").NatsConnection} NatsConnection NATS Connection 4 | * @typedef {import("nats").ConnectionOptions} ConnectionOptions NATS Connection Opts 5 | * @typedef {import("nats").StreamConfig} StreamConfig NATS Configuration Options 6 | * @typedef {import("nats").JetStreamManager} JetStreamManager NATS Jet Stream Manager 7 | * @typedef {import("nats").JetStreamClient} JetStreamClient NATS JetStream Client 8 | * @typedef {import("nats").JetStreamPublishOptions} JetStreamPublishOptions JetStream Publish Options 9 | * @typedef {import("nats").ConsumerOptsBuilder} ConsumerOptsBuilder NATS JetStream ConsumerOptsBuilder 10 | * @typedef {import("nats").ConsumerOpts} ConsumerOpts Jet Stream Consumer Opts 11 | * @typedef {import("nats").JetStreamOptions} JetStreamOptions Jet Stream Options 12 | * @typedef {import("nats").JsMsg} JsMsg Jet Stream Message 13 | * @typedef {import("nats").JetStreamSubscription} JetStreamSubscription Jet Stream Subscription 14 | * @typedef {import("nats").MsgHdrs} MsgHdrs Jet Stream Headers 15 | * @typedef {import("moleculer").ServiceBroker} ServiceBroker Moleculer Service Broker instance 16 | * @typedef {import("moleculer").LoggerInstance} Logger Logger instance 17 | * @typedef {import("../index").Channel} Channel Base channel definition 18 | * @typedef {import("./base").BaseDefaultOptions} BaseDefaultOptions Base adapter options 19 | */ 20 | /** 21 | * @typedef {Object} NatsDefaultOptions 22 | * @property {Object} nats NATS lib configuration 23 | * @property {String} url String containing the URL to NATS server 24 | * @property {ConnectionOptions} nats.connectionOptions 25 | * @property {StreamConfig} nats.streamConfig More info: https://docs.nats.io/jetstream/concepts/streams 26 | * @property {ConsumerOpts} nats.consumerOptions More info: https://docs.nats.io/jetstream/concepts/consumers 27 | */ 28 | /** 29 | * NATS JetStream adapter 30 | * 31 | * More info: https://github.com/nats-io/nats.deno/blob/main/jetstream.md 32 | * More info: https://github.com/nats-io/nats-architecture-and-design#jetstream 33 | * More info: https://docs.nats.io/jetstream/concepts/ 34 | * 35 | * @class NatsAdapter 36 | * @extends {BaseAdapter} 37 | */ 38 | declare class NatsAdapter extends BaseAdapter { 39 | constructor(opts: any); 40 | /** @type {NatsConnection} */ 41 | connection: NatsConnection; 42 | /** @type {JetStreamManager} */ 43 | manager: JetStreamManager; 44 | /** @type {JetStreamClient} */ 45 | client: JetStreamClient; 46 | /** @type {Map} */ 47 | subscriptions: Map; 48 | /** 49 | * Connect to the adapter. 50 | */ 51 | connect(): Promise; 52 | stopping: boolean; 53 | /** 54 | * Subscribe to a channel with a handler. 55 | * 56 | * @param {Channel & NatsDefaultOptions} chan 57 | */ 58 | subscribe(chan: Channel & NatsDefaultOptions): Promise; 59 | /** 60 | * Creates the callback handler 61 | * 62 | * @param {Channel} chan 63 | * @returns 64 | */ 65 | createConsumerHandler(chan: Channel): (err: import("nats").NatsError, message: JsMsg) => Promise; 66 | /** 67 | * Create a NATS Stream 68 | * 69 | * More info: https://docs.nats.io/jetstream/concepts/streams 70 | * 71 | * @param {String} streamName Name of the Stream 72 | * @param {Array} subjects A list of subjects/topics to store in a stream 73 | * @param {StreamConfig} streamOpts JetStream stream configs 74 | */ 75 | createStream(streamName: string, subjects: Array, streamOpts: StreamConfig): Promise; 76 | /** 77 | * Moves message into dead letter 78 | * 79 | * @param {Channel} chan 80 | * @param {JsMsg} message JetStream message 81 | */ 82 | moveToDeadLetter(chan: Channel, message: JsMsg): Promise; 83 | /** 84 | * Publish a payload to a channel. 85 | * 86 | * @param {String} channelName 87 | * @param {any} payload 88 | * @param {Partial?} opts 89 | */ 90 | publish(channelName: string, payload: any, opts?: Partial | null): Promise; 91 | } 92 | declare namespace NatsAdapter { 93 | export { NatsConnection, ConnectionOptions, StreamConfig, JetStreamManager, JetStreamClient, JetStreamPublishOptions, ConsumerOptsBuilder, ConsumerOpts, JetStreamOptions, JsMsg, JetStreamSubscription, MsgHdrs, ServiceBroker, Logger, Channel, BaseDefaultOptions, NatsDefaultOptions }; 94 | } 95 | import BaseAdapter = require("./base"); 96 | /** 97 | * NATS Connection 98 | */ 99 | type NatsConnection = import("nats").NatsConnection; 100 | /** 101 | * NATS Connection Opts 102 | */ 103 | type ConnectionOptions = import("nats").ConnectionOptions; 104 | /** 105 | * NATS Configuration Options 106 | */ 107 | type StreamConfig = import("nats").StreamConfig; 108 | /** 109 | * NATS Jet Stream Manager 110 | */ 111 | type JetStreamManager = import("nats").JetStreamManager; 112 | /** 113 | * NATS JetStream Client 114 | */ 115 | type JetStreamClient = import("nats").JetStreamClient; 116 | /** 117 | * JetStream Publish Options 118 | */ 119 | type JetStreamPublishOptions = import("nats").JetStreamPublishOptions; 120 | /** 121 | * NATS JetStream ConsumerOptsBuilder 122 | */ 123 | type ConsumerOptsBuilder = import("nats").ConsumerOptsBuilder; 124 | /** 125 | * Jet Stream Consumer Opts 126 | */ 127 | type ConsumerOpts = import("nats").ConsumerOpts; 128 | /** 129 | * Jet Stream Options 130 | */ 131 | type JetStreamOptions = import("nats").JetStreamOptions; 132 | /** 133 | * Jet Stream Message 134 | */ 135 | type JsMsg = import("nats").JsMsg; 136 | /** 137 | * Jet Stream Subscription 138 | */ 139 | type JetStreamSubscription = import("nats").JetStreamSubscription; 140 | /** 141 | * Jet Stream Headers 142 | */ 143 | type MsgHdrs = import("nats").MsgHdrs; 144 | /** 145 | * Moleculer Service Broker instance 146 | */ 147 | type ServiceBroker = import("moleculer").ServiceBroker; 148 | /** 149 | * Logger instance 150 | */ 151 | type Logger = import("moleculer").LoggerInstance; 152 | /** 153 | * Base channel definition 154 | */ 155 | type Channel = import("../index").Channel; 156 | /** 157 | * Base adapter options 158 | */ 159 | type BaseDefaultOptions = import("./base").BaseDefaultOptions; 160 | type NatsDefaultOptions = { 161 | /** 162 | * NATS lib configuration 163 | */ 164 | nats: any; 165 | /** 166 | * String containing the URL to NATS server 167 | */ 168 | url: string; 169 | connectionOptions: ConnectionOptions; 170 | /** 171 | * More info: https://docs.nats.io/jetstream/concepts/streams 172 | */ 173 | streamConfig: StreamConfig; 174 | /** 175 | * More info: https://docs.nats.io/jetstream/concepts/consumers 176 | */ 177 | consumerOptions: ConsumerOpts; 178 | }; 179 | -------------------------------------------------------------------------------- /types/src/adapters/redis.d.ts: -------------------------------------------------------------------------------- 1 | export = RedisAdapter; 2 | /** 3 | * @typedef {import("ioredis").Cluster} Cluster Redis cluster instance. More info: https://github.com/luin/ioredis/blob/master/API.md#Cluster 4 | * @typedef {import("ioredis").Redis} Redis Redis instance. More info: https://github.com/luin/ioredis/blob/master/API.md#Redis 5 | * @typedef {import("ioredis").RedisOptions} RedisOptions 6 | * @typedef {import("moleculer").ServiceBroker} ServiceBroker Moleculer Service Broker instance 7 | * @typedef {import("moleculer").LoggerInstance} Logger Logger instance 8 | * @typedef {import("../index").Channel} Channel Base channel definition 9 | * @typedef {import("./base").BaseDefaultOptions} BaseDefaultOptions Base adapter options 10 | */ 11 | /** 12 | * @typedef {Object} RedisDefaultOptions Redis Adapter configuration 13 | * @property {Number} readTimeoutInterval Timeout interval (in milliseconds) while waiting for new messages. By default equals to 0, i.e., never timeout 14 | * @property {Number} minIdleTime Time (in milliseconds) after which pending messages are considered NACKed and should be claimed. Defaults to 1 hour. 15 | * @property {Number} claimInterval Interval (in milliseconds) between message claims 16 | * @property {String} startID Starting point when consumers fetch data from the consumer group. By default equals to "$", i.e., consumers will only see new elements arriving in the stream. 17 | * @property {Number} processingAttemptsInterval Interval (in milliseconds) between message transfer into FAILED_MESSAGES channel 18 | */ 19 | /** 20 | * @typedef {Object} RedisChannel Redis specific channel options 21 | * @property {Function} xreadgroup Function for fetching new messages from redis stream 22 | * @property {Function} xclaim Function for claiming pending messages 23 | * @property {Function} failed_messages Function for checking NACKed messages and moving them into dead letter queue 24 | * @property {RedisDefaultOptions} redis 25 | */ 26 | /** 27 | * @typedef {Object} RedisOpts 28 | * @property {Object} redis Redis lib configuration 29 | * @property {RedisDefaultOptions} redis.consumerOptions 30 | */ 31 | /** 32 | * Redis Streams adapter 33 | * 34 | * @class RedisAdapter 35 | * @extends {BaseAdapter} 36 | */ 37 | declare class RedisAdapter extends BaseAdapter { 38 | /** 39 | * @type {Map} 40 | */ 41 | clients: Map; 42 | pubName: string; 43 | claimName: string; 44 | nackedName: string; 45 | stopping: boolean; 46 | /** 47 | * Disconnect from adapter 48 | */ 49 | disconnect(): Promise; 50 | /** 51 | * Return redis or redis.cluster client instance 52 | * 53 | * @param {string} name Client name 54 | * @param {any} opts 55 | * 56 | * @memberof RedisTransporter 57 | * @returns {Promise} 58 | */ 59 | createRedisClient(name: string, opts: any): Promise; 60 | /** 61 | * Subscribe to a channel with a handler. 62 | * 63 | * @param {Channel & RedisChannel & RedisDefaultOptions} chan 64 | */ 65 | subscribe(chan: Channel & RedisChannel & RedisDefaultOptions): Promise; 66 | /** 67 | * Unsubscribe from a channel. 68 | * 69 | * @param {Channel & RedisChannel & RedisDefaultOptions} chan 70 | */ 71 | unsubscribe(chan: Channel & RedisChannel & RedisDefaultOptions): Promise; 72 | /** 73 | * Process incoming messages. 74 | * 75 | * @param {Channel & RedisChannel & RedisDefaultOptions} chan 76 | * @param {Array} message 77 | */ 78 | processMessage(chan: Channel & RedisChannel & RedisDefaultOptions, message: Array): Promise; 79 | /** 80 | * Parse the message(s). 81 | * 82 | * @param {Array} messages 83 | * @returns {any} 84 | */ 85 | parseMessage(messages: any[]): any; 86 | /** 87 | * Moves message into dead letter 88 | * 89 | * @param {Channel & RedisChannel & RedisDefaultOptions} chan 90 | * @param {String} originalID ID of the dead message 91 | * @param {Object} message Raw (not serialized) message contents 92 | * @param {Object} headers Header contents 93 | */ 94 | moveToDeadLetter(chan: Channel & RedisChannel & RedisDefaultOptions, originalID: string, message: any, headers: any): Promise; 95 | /** 96 | * Publish a payload to a channel. 97 | * 98 | * @param {String} channelName 99 | * @param {any} payload 100 | * @param {Object?} opts 101 | */ 102 | publish(channelName: string, payload: any, opts?: any | null): Promise; 103 | } 104 | declare namespace RedisAdapter { 105 | export { Cluster, Redis, RedisOptions, ServiceBroker, Logger, Channel, BaseDefaultOptions, RedisDefaultOptions, RedisChannel, RedisOpts }; 106 | } 107 | import BaseAdapter = require("./base"); 108 | /** 109 | * Redis cluster instance. More info: https://github.com/luin/ioredis/blob/master/API.md#Cluster 110 | */ 111 | type Cluster = import("ioredis").Cluster; 112 | /** 113 | * Redis instance. More info: https://github.com/luin/ioredis/blob/master/API.md#Redis 114 | */ 115 | type Redis = import("ioredis").Redis; 116 | type RedisOptions = import("ioredis").RedisOptions; 117 | /** 118 | * Moleculer Service Broker instance 119 | */ 120 | type ServiceBroker = import("moleculer").ServiceBroker; 121 | /** 122 | * Logger instance 123 | */ 124 | type Logger = import("moleculer").LoggerInstance; 125 | /** 126 | * Base channel definition 127 | */ 128 | type Channel = import("../index").Channel; 129 | /** 130 | * Base adapter options 131 | */ 132 | type BaseDefaultOptions = import("./base").BaseDefaultOptions; 133 | /** 134 | * Redis Adapter configuration 135 | */ 136 | type RedisDefaultOptions = { 137 | /** 138 | * Timeout interval (in milliseconds) while waiting for new messages. By default equals to 0, i.e., never timeout 139 | */ 140 | readTimeoutInterval: number; 141 | /** 142 | * Time (in milliseconds) after which pending messages are considered NACKed and should be claimed. Defaults to 1 hour. 143 | */ 144 | minIdleTime: number; 145 | /** 146 | * Interval (in milliseconds) between message claims 147 | */ 148 | claimInterval: number; 149 | /** 150 | * Starting point when consumers fetch data from the consumer group. By default equals to "$", i.e., consumers will only see new elements arriving in the stream. 151 | */ 152 | startID: string; 153 | /** 154 | * Interval (in milliseconds) between message transfer into FAILED_MESSAGES channel 155 | */ 156 | processingAttemptsInterval: number; 157 | }; 158 | /** 159 | * Redis specific channel options 160 | */ 161 | type RedisChannel = { 162 | /** 163 | * Function for fetching new messages from redis stream 164 | */ 165 | xreadgroup: Function; 166 | /** 167 | * Function for claiming pending messages 168 | */ 169 | xclaim: Function; 170 | /** 171 | * Function for checking NACKed messages and moving them into dead letter queue 172 | */ 173 | failed_messages: Function; 174 | redis: RedisDefaultOptions; 175 | }; 176 | type RedisOpts = { 177 | /** 178 | * Redis lib configuration 179 | */ 180 | redis: { 181 | consumerOptions: RedisDefaultOptions; 182 | }; 183 | }; 184 | -------------------------------------------------------------------------------- /types/src/constants.d.ts: -------------------------------------------------------------------------------- 1 | export let HEADER_REDELIVERED_COUNT: string; 2 | export let HEADER_GROUP: string; 3 | export let HEADER_ORIGINAL_CHANNEL: string; 4 | export let HEADER_ORIGINAL_GROUP: string; 5 | export let METRIC_CHANNELS_MESSAGES_SENT: string; 6 | export let METRIC_CHANNELS_MESSAGES_TOTAL: string; 7 | export let METRIC_CHANNELS_MESSAGES_ACTIVE: string; 8 | export let METRIC_CHANNELS_MESSAGES_TIME: string; 9 | export let METRIC_CHANNELS_MESSAGES_ERRORS_TOTAL: string; 10 | export let METRIC_CHANNELS_MESSAGES_RETRIES_TOTAL: string; 11 | export let METRIC_CHANNELS_MESSAGES_DEAD_LETTERING_TOTAL: string; 12 | -------------------------------------------------------------------------------- /types/src/index.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace _exports { 2 | export { ServiceBroker, Logger, Service, Middleware, BaseAdapter, DeadLetteringOptions, Channel, ChannelRegistryEntry, AdapterConfig, MiddlewareOptions }; 3 | } 4 | declare function _exports(mwOpts: MiddlewareOptions): { 5 | name: string; 6 | /** 7 | * Create lifecycle hook of service 8 | * @param {ServiceBroker} _broker 9 | */ 10 | created(_broker: ServiceBroker): void; 11 | /** 12 | * Created lifecycle hook of service 13 | * 14 | * @param {Service} svc 15 | */ 16 | serviceCreated(svc: Service): Promise; 17 | /** 18 | * Service stopping lifecycle hook. 19 | * Need to unsubscribe from the channels. 20 | * 21 | * @param {Service} svc 22 | */ 23 | serviceStopping(svc: Service): Promise; 24 | /** 25 | * Start lifecycle hook of service 26 | */ 27 | started(): Promise; 28 | /** 29 | * Stop lifecycle hook of service 30 | */ 31 | stopped(): Promise; 32 | }; 33 | export = _exports; 34 | /** 35 | * Moleculer Service Broker instance 36 | */ 37 | type ServiceBroker = import("moleculer").ServiceBroker; 38 | /** 39 | * Logger instance 40 | */ 41 | type Logger = import("moleculer").LoggerInstance; 42 | /** 43 | * Moleculer service 44 | */ 45 | type Service = import("moleculer").Service; 46 | /** 47 | * Moleculer middleware 48 | */ 49 | type Middleware = import("moleculer").Middleware; 50 | /** 51 | * Base adapter class 52 | */ 53 | type BaseAdapter = import("./adapters/base"); 54 | /** 55 | * Dead-letter-queue options 56 | */ 57 | type DeadLetteringOptions = { 58 | /** 59 | * Enable dead-letter-queue 60 | */ 61 | enabled: boolean; 62 | /** 63 | * Name of the dead-letter queue 64 | */ 65 | queueName: string; 66 | /** 67 | * Name of the dead-letter exchange (only for AMQP adapter) 68 | */ 69 | exchangeName: string; 70 | /** 71 | * Options for the dead-letter exchange (only for AMQP adapter) 72 | */ 73 | exchangeOptions: any; 74 | /** 75 | * Options for the dead-letter queue (only for AMQP adapter) 76 | */ 77 | queueOptions: any; 78 | }; 79 | /** 80 | * Base consumer configuration 81 | */ 82 | type Channel = { 83 | /** 84 | * Consumer ID 85 | */ 86 | id: string; 87 | /** 88 | * Channel/Queue/Stream name 89 | */ 90 | name: string; 91 | /** 92 | * Consumer group name 93 | */ 94 | group: string; 95 | /** 96 | * Create Moleculer Context 97 | */ 98 | context: boolean; 99 | /** 100 | * Flag denoting if service is stopping 101 | */ 102 | unsubscribing: boolean; 103 | /** 104 | * Maximum number of messages that can be processed simultaneously 105 | */ 106 | maxInFlight: number | null; 107 | /** 108 | * Maximum number of retries before sending the message to dead-letter-queue 109 | */ 110 | maxRetries: number; 111 | /** 112 | * Dead-letter-queue options 113 | */ 114 | deadLettering: DeadLetteringOptions | null; 115 | /** 116 | * User defined handler 117 | */ 118 | handler: Function; 119 | }; 120 | /** 121 | * Registry entry 122 | */ 123 | type ChannelRegistryEntry = { 124 | /** 125 | * Service instance class 126 | */ 127 | svc: Service; 128 | /** 129 | * Channel name 130 | */ 131 | name: string; 132 | /** 133 | * Channel object 134 | */ 135 | chan: Channel; 136 | }; 137 | type AdapterConfig = { 138 | /** 139 | * Adapter name 140 | */ 141 | type: string; 142 | /** 143 | * Adapter options 144 | */ 145 | options: import("./adapters/base").BaseDefaultOptions & import("./adapters/amqp").AmqpDefaultOptions & import("./adapters/kafka").KafkaDefaultOptions & import("./adapters/nats").NatsDefaultOptions & import("./adapters/redis").RedisDefaultOptions; 146 | }; 147 | /** 148 | * Middleware options 149 | */ 150 | type MiddlewareOptions = { 151 | /** 152 | * Adapter name or connection string or configuration object. 153 | */ 154 | adapter: string | AdapterConfig; 155 | /** 156 | * Property name of channels definition in service schema. 157 | */ 158 | schemaProperty: string | null; 159 | /** 160 | * Method name to send messages. 161 | */ 162 | sendMethodName: string | null; 163 | /** 164 | * Property name of the adapter instance in broker instance. 165 | */ 166 | adapterPropertyName: string | null; 167 | /** 168 | * Method name to add to service in order to trigger channel handlers. 169 | */ 170 | channelHandlerTrigger: string | null; 171 | /** 172 | * Using Moleculer context in channel handlers by default. 173 | */ 174 | context: boolean | null; 175 | }; 176 | -------------------------------------------------------------------------------- /types/src/tracing.d.ts: -------------------------------------------------------------------------------- 1 | declare function _exports(): { 2 | name: string; 3 | created(_broker: any): void; 4 | localChannel: (handler: any, chan: any) => any; 5 | }; 6 | export = _exports; 7 | --------------------------------------------------------------------------------