diff --git a/.babelrc b/.babelrc deleted file mode 100644 index dfca7d3..0000000 --- a/.babelrc +++ /dev/null @@ -1,12 +0,0 @@ -{ - "presets": [ - [ - "@babel/env", - { - "targets": { - "node": "current" - } - } - ] - ] -} diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 6b9fc77..b821599 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -15,13 +15,16 @@ jobs: node-version: [18, 20, 22, 24, latest] steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v6 with: node-version: ${{ matrix.node-version }} - - run: npm i + - run: npm i --loglevel error - run: npm run test:lcov + - name: Lint + if: matrix.node-version == 22 || matrix.node-version == 24 + run: npm run lint - name: Coveralls uses: coverallsapp/github-action@v2 with: diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 5cb1490..f42f20b 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -13,7 +13,7 @@ jobs: publish: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: @@ -23,4 +23,6 @@ jobs: - name: Update npm run: npm install -g npm@latest - run: npm i - - run: npm publish + - run: npm publish $VERSION_TAG + env: + VERSION_TAG: ${{ endsWith(github.ref_name, '-rc') && '--tag rc' || '' }} diff --git a/.gitignore b/.gitignore index b80821b..2d20306 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,12 @@ package-lock.json .nyc_output *.log + +# ts +*.d.ts.map + +# AI +.claude +AGENTS.md + +dist/ diff --git a/.nvmrc b/.nvmrc index 3c03207..2bd5a0a 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -18 +22 diff --git a/.prettierignore b/.prettierignore index 8e6dc6e..b67abb8 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,3 +2,5 @@ dist/ node_modules/ coverage/ tmp/ +types/index.d.ts +types/index.d.ts.map diff --git a/API.md b/API.md index 32ca31b..9924434 100644 --- a/API.md +++ b/API.md @@ -4,105 +4,109 @@ The api is inspired by the amusing [`amqplib`](https://github.com/squaremo/amqp. -- [API reference](#api-reference) - - [`new Broker([owner])`](#new-brokerowner) - - [`broker.subscribe(exchangeName, pattern, queueName, onMessage[, options])`](#brokersubscribeexchangename-pattern-queuename-onmessage-options) - - [`broker.subscribeTmp(exchangeName, pattern, onMessage[, options])`](#brokersubscribetmpexchangename-pattern-onmessage-options) - - [`broker.subscribeOnce(exchangeName, pattern, onMessage[, options])`](#brokersubscribeonceexchangename-pattern-onmessage-options) - - [`broker.unsubscribe(queueName, onMessage)`](#brokerunsubscribequeuename-onmessage) - - [`broker.publish(exchangeName, routingKey[, content, options])`](#brokerpublishexchangename-routingkey-content-options) - - [`broker.close()`](#brokerclose) - - [`broker.assertExchange(exchangeName[, type = topic, options])`](#brokerassertexchangeexchangename-type--topic-options) - - [`broker.deleteExchange(exchangeName[, {ifUnused}])`](#brokerdeleteexchangeexchangename-ifunused) - - [`broker.bindExchange(source, destination[, pattern, args])`](#brokerbindexchangesource-destination-pattern-args) - - [`broker.unbindExchange(source, destination[, pattern])`](#brokerunbindexchangesource-destination-pattern) - - [`broker.assertQueue(queueName[, options])`](#brokerassertqueuequeuename-options) - - [`broker.bindQueue(queueName, exchangeName, pattern[, options])`](#brokerbindqueuequeuename-exchangename-pattern-options) - - [`broker.unbindQueue(queueName, exchangeName, pattern)`](#brokerunbindqueuequeuename-exchangename-pattern) - - [`broker.consume(queueName, onMessage[, options])`](#brokerconsumequeuename-onmessage-options) - - [`broker.cancel(consumerTag[, requeue = true])`](#brokercancelconsumertag-requeue--true) - - [`broker.createQueue([queueName, options])`](#brokercreatequeuequeuename-options) - - [`broker.deleteQueue(queueName[, {ifUnused, ifEmpty}])`](#brokerdeletequeuequeuename-ifunused-ifempty) - - [`broker.getExchange(exchangeName)`](#brokergetexchangeexchangename) - - [`broker.getQueue(queueName)`](#brokergetqueuequeuename) - - [`broker.getConsumers()`](#brokergetconsumers) - - [`broker.getConsumer(consumerTag)`](#brokergetconsumerconsumertag) - - [`broker.getState([onlyWithContent])`](#brokergetstateonlywithcontent) - - [`broker.recover([state])`](#brokerrecoverstate) - - [`broker.purgeQueue(queueName)`](#brokerpurgequeuequeuename) - - [`broker.sendToQueue(queueName, content[, options])`](#brokersendtoqueuequeuename-content-options) - - [`broker.stop()`](#brokerstop) - - [`broker.get(queueName[, options])`](#brokergetqueuename-options) - - [`broker.ack(message[, allUpTo])`](#brokerackmessage-allupto) - - [`broker.ackAll()`](#brokerackall) - - [`broker.nack(message[, allUpTo, requeue])`](#brokernackmessage-allupto-requeue) - - [`broker.nackAll([requeue])`](#brokernackallrequeue) - - [`broker.reject(message[, requeue])`](#brokerrejectmessage-requeue) - - [`broker.createShovel(name, source, destination[, options])`](#brokercreateshovelname-source-destination-options) - - [`broker.getShovel(name)`](#brokergetshovelname) - - [`broker.closeShovel(name)`](#brokercloseshovelname) - - [`broker.on(eventName, callback[, options])`](#brokeroneventname-callback-options) - - [`broker.off(eventName, callbackOrObject)`](#brokeroffeventname-callbackorobject) - - [`broker.prefetch(count)`](#brokerprefetchcount) - - [`broker.reset()`](#brokerreset) - - [Exchange](#exchange) - - [`exchange.bindQueue(queue, pattern[, bindOptions])`](#exchangebindqueuequeue-pattern-bindoptions) - - [`exchange.close()`](#exchangeclose) - - [`exchange.emit(eventName[, content])`](#exchangeemiteventname-content) - - [`exchange.getBinding(queueName, pattern)`](#exchangegetbindingqueuename-pattern) - - [`exchange.getState()`](#exchangegetstate) - - [`exchange.on(pattern, handler[, consumeOptions])`](#exchangeonpattern-handler-consumeoptions) - - [`exchange.off(pattern, handlerOrObject)`](#exchangeoffpattern-handlerorobject) - - [`exchange.publish(routingKey[, content, properties])`](#exchangepublishroutingkey-content-properties) - - [`exchange.recover([state, getQueue])`](#exchangerecoverstate-getqueue) - - [`exchange.stop()`](#exchangestop) - - [`exchange.unbindQueue(queue, pattern)`](#exchangeunbindqueuequeue-pattern) - - [`exchange.unbindQueueByName(queueName)`](#exchangeunbindqueuebynamequeuename) - - [`exchange.closeBinding(binding)`](#exchangeclosebindingbinding) - - [Binding](#binding) - - [`binding.testPattern(routingKey)`](#bindingtestpatternroutingkey) - - [`binding.close()`](#bindingclose) - - [Queue](#queue) - - [`queue.ack(message[, allUpTo])`](#queueackmessage-allupto) - - [`queue.ackAll()`](#queueackall) - - [`queue.assertConsumer(onMessage[, consumeOptions, owner])`](#queueassertconsumeronmessage-consumeoptions-owner) - - [`queue.cancel(consumerTag[, requeue = true])`](#queuecancelconsumertag-requeue--true) - - [`queue.close()`](#queueclose) - - [`queue.consume(onMessage[, options, owner])`](#queueconsumeonmessage-options-owner) - - [`queue.delete([deleteOptions])`](#queuedeletedeleteoptions) - - [`queue.dismiss(onMessage[, requeue = true])`](#queuedismissonmessage-requeue--true) - - [`queue.get([consumeOptions])`](#queuegetconsumeoptions) - - [`queue.getState()`](#queuegetstate) - - [`queue.nack(message[, allUpTo, requeue = true])`](#queuenackmessage-allupto-requeue--true) - - [`queue.nackAll([requeue = true])`](#queuenackallrequeue--true) - - [`queue.on(eventName, handler[, consumeOptions])`](#queueoneventname-handler-consumeoptions) - - [`queue.off(eventName, handler)`](#queueoffeventname-handler) - - [`queue.peek([ignoreDelivered])`](#queuepeekignoredelivered) - - [`queue.purge()`](#queuepurge) - - [`queue.queueMessage(fields[, content, properties])`](#queuequeuemessagefields-content-properties) - - [`queue.recover([state])`](#queuerecoverstate) - - [`queue.reject(message[, requeue = true])`](#queuerejectmessage-requeue--true) - - [`queue.stop()`](#queuestop) - - [`queue.unbindConsumer(consumer[, requeue = true])`](#queueunbindconsumerconsumer-requeue--true) - - [Consumer](#consumer) - - [`consumer.ackAll()`](#consumerackall) - - [`consumer.nackAll([requeue])`](#consumernackallrequeue) - - [`consumer.cancel([requeue = true])`](#consumercancelrequeue--true) - - [`consumer.prefetch(numberOfMessages)`](#consumerprefetchnumberofmessages) - - [Message](#message) - - [`message.ack([allUpTo])`](#messageackallupto) - - [`message.nack([allUpTo, requeue])`](#messagenackallupto-requeue) - - [`message.reject([requeue])`](#messagerejectrequeue) - - [`new Shovel(name, source, destination[, options])`](#new-shovelname-source-destination-options) - - [`shovel.close()`](#shovelclose) - - [`shovel.on(eventName, callback[, options])`](#shoveloneventname-callback-options) - - [`shovel.off(eventName, callbackOrObject)`](#shoveloffeventname-callbackorobject) - - [SmqpError](#smqperror) - - [`error.code`](#errorcode) - - [`getRoutingKeyPattern(pattern)`](#getroutingkeypatternpattern) - - [Message eviction](#message-eviction) - - +- [`new Broker([owner])`](#new-brokerowner) + - [`broker.subscribe(exchangeName, pattern, queueName, onMessage[, options])`](#brokersubscribeexchangename-pattern-queuename-onmessage-options) + - [`broker.subscribeTmp(exchangeName, pattern, onMessage[, options])`](#brokersubscribetmpexchangename-pattern-onmessage-options) + - [`broker.subscribeOnce(exchangeName, pattern, onMessage[, options])`](#brokersubscribeonceexchangename-pattern-onmessage-options) + - [`broker.unsubscribe(queueName, onMessage)`](#brokerunsubscribequeuename-onmessage) + - [`broker.publish(exchangeName, routingKey[, content, options])`](#brokerpublishexchangename-routingkey-content-options) + - [`broker.close()`](#brokerclose) + - [`broker.assertExchange(exchangeName[, type = topic, options])`](#brokerassertexchangeexchangename-type-topic-options) + - [`broker.deleteExchange(exchangeName[, {ifUnused}])`](#brokerdeleteexchangeexchangename-ifunused) + - [`broker.bindExchange(source, destination[, pattern, args])`](#brokerbindexchangesource-destination-pattern-args) + - [`broker.unbindExchange(source, destination[, pattern])`](#brokerunbindexchangesource-destination-pattern) + - [`broker.assertQueue(queueName[, options])`](#brokerassertqueuequeuename-options) + - [`broker.bindQueue(queueName, exchangeName, pattern[, options])`](#brokerbindqueuequeuename-exchangename-pattern-options) + - [`broker.unbindQueue(queueName, exchangeName, pattern)`](#brokerunbindqueuequeuename-exchangename-pattern) + - [`broker.consume(queueName, onMessage[, options])`](#brokerconsumequeuename-onmessage-options) + - [`broker.cancel(consumerTag[, requeue = true])`](#brokercancelconsumertag-requeue-true) + - [`broker.createQueue([queueName, options])`](#brokercreatequeuequeuename-options) + - [`broker.deleteQueue(queueName[, {ifUnused, ifEmpty}])`](#brokerdeletequeuequeuename-ifunused-ifempty) + - [`broker.getExchange(exchangeName)`](#brokergetexchangeexchangename) + - [`broker.getQueue(queueName)`](#brokergetqueuequeuename) + - [`broker.getConsumers()`](#brokergetconsumers) + - [`broker.getConsumer(consumerTag)`](#brokergetconsumerconsumertag) + - [`broker.getState([onlyWithContent])`](#brokergetstateonlywithcontent) + - [`broker.recover([state])`](#brokerrecoverstate) + - [`broker.purgeQueue(queueName)`](#brokerpurgequeuequeuename) + - [`broker.sendToQueue(queueName, content[, options])`](#brokersendtoqueuequeuename-content-options) + - [`broker.stop()`](#brokerstop) + - [`broker.get(queueName[, options])`](#brokergetqueuename-options) + - [`broker.ack(message[, allUpTo])`](#brokerackmessage-allupto) + - [`broker.ackAll()`](#brokerackall) + - [`broker.nack(message[, allUpTo, requeue])`](#brokernackmessage-allupto-requeue) + - [`broker.nackAll([requeue])`](#brokernackallrequeue) + - [`broker.reject(message[, requeue])`](#brokerrejectmessage-requeue) + - [`broker.createShovel(name, source, destination[, options])`](#brokercreateshovelname-source-destination-options) + - [`broker.getShovel(name)`](#brokergetshovelname) + - [`broker.getShovels()`](#brokergetshovels) + - [`broker.closeShovel(name)`](#brokercloseshovelname) + - [`broker.validateConsumerTag(consumerTag)`](#brokervalidateconsumertagconsumertag) + - [`broker.on(eventName, callback[, options])`](#brokeroneventname-callback-options) + - [`broker.off(eventName, callbackOrObject)`](#brokeroffeventname-callbackorobject) + - [`broker.prefetch(count)`](#brokerprefetchcount) + - [`broker.reset()`](#brokerreset) +- [Exchange](#exchange) + - [`exchange.bindQueue(queue, pattern[, bindOptions])`](#exchangebindqueuequeue-pattern-bindoptions) + - [`exchange.close()`](#exchangeclose) + - [`exchange.emit(eventName[, content])`](#exchangeemiteventname-content) + - [`exchange.getBinding(queueName, pattern)`](#exchangegetbindingqueuename-pattern) + - [`exchange.getState()`](#exchangegetstate) + - [`exchange.on(pattern, handler[, consumeOptions])`](#exchangeonpattern-handler-consumeoptions) + - [`exchange.off(pattern, handlerOrObject)`](#exchangeoffpattern-handlerorobject) + - [`exchange.publish(routingKey[, content, properties])`](#exchangepublishroutingkey-content-properties) + - [`exchange.recover([state, getQueue])`](#exchangerecoverstate-getqueue) + - [`exchange.stop()`](#exchangestop) + - [`exchange.unbindQueue(queue, pattern)`](#exchangeunbindqueuequeue-pattern) + - [`exchange.unbindQueueByName(queueName)`](#exchangeunbindqueuebynamequeuename) + - [`exchange.closeBinding(binding)`](#exchangeclosebindingbinding) +- [Binding](#binding) + - [`binding.testPattern(routingKey)`](#bindingtestpatternroutingkey) + - [`binding.close()`](#bindingclose) +- [Queue](#queue) + - [`queue.ack(message[, allUpTo])`](#queueackmessage-allupto) + - [`queue.ackAll()`](#queueackall) + - [`queue.assertConsumer(onMessage[, consumeOptions, owner])`](#queueassertconsumeronmessage-consumeoptions-owner) + - [`queue.cancel(consumerTag[, requeue = true])`](#queuecancelconsumertag-requeue-true) + - [`queue.close()`](#queueclose) + - [`queue.consume(onMessage[, options, owner])`](#queueconsumeonmessage-options-owner) + - [`queue.delete([deleteOptions])`](#queuedeletedeleteoptions) + - [`queue.dismiss(onMessage[, requeue = true])`](#queuedismissonmessage-requeue-true) + - [`queue.get([consumeOptions])`](#queuegetconsumeoptions) + - [`queue.getState()`](#queuegetstate) + - [`queue.nack(message[, allUpTo, requeue = true])`](#queuenackmessage-allupto-requeue-true) + - [`queue.nackAll([requeue = true])`](#queuenackallrequeue-true) + - [`queue.on(eventName, handler[, consumeOptions])`](#queueoneventname-handler-consumeoptions) + - [`queue.off(eventName, handler)`](#queueoffeventname-handler) + - [`queue.peek([ignoreDelivered])`](#queuepeekignoredelivered) + - [`queue.purge()`](#queuepurge) + - [`queue.queueMessage(fields[, content, properties])`](#queuequeuemessagefields-content-properties) + - [`queue.recover([state])`](#queuerecoverstate) + - [`queue.reject(message[, requeue = true])`](#queuerejectmessage-requeue-true) + - [`queue.stop()`](#queuestop) + - [`queue.unbindConsumer(consumer[, requeue = true])`](#queueunbindconsumerconsumer-requeue-true) +- [Consumer](#consumer) + - [`consumer.ackAll()`](#consumerackall) + - [`consumer.nackAll([requeue])`](#consumernackallrequeue) + - [`consumer.cancel([requeue = true])`](#consumercancelrequeue-true) + - [`consumer.prefetch(numberOfMessages)`](#consumerprefetchnumberofmessages) +- [Message](#message) + - [`message.ack([allUpTo])`](#messageackallupto) + - [`message.nack([allUpTo, requeue])`](#messagenackallupto-requeue) + - [`message.reject([requeue])`](#messagerejectrequeue) +- [`new Shovel(name, source, destination[, options])`](#new-shovelname-source-destination-options) + - [`shovel.close()`](#shovelclose) + - [`shovel.on(eventName, callback[, options])`](#shoveloneventname-callback-options) + - [`shovel.off(eventName, callbackOrObject)`](#shoveloffeventname-callbackorobject) +- [Exchange2Exchange](#exchange2exchange) + - [`exchange2exchange.on(eventName, handler)`](#exchange2exchangeoneventname-handler) + - [`exchange2exchange.close()`](#exchange2exchangeclose) +- [SmqpError](#smqperror) + - [`error.code`](#errorcode) +- [`getRoutingKeyPattern(pattern)`](#getroutingkeypatternpattern) +- [Message eviction](#message-eviction) + + # API reference @@ -130,6 +134,7 @@ To make sure the exchange, and or queue has the desired behaviour, please use [` - `autoDelete`: boolean, defaults to `true`, exchange will be deleted when all bindings are removed; the queue will be removed when all consumers are down - `consumerTag`: unique consumer tag - `deadLetterExchange`: string, name of dead letter exchange. Will be asserted as topic exchange + - `deadLetterRoutingKey`: optional string, override routing key when publishing dead-lettered messages - `durable`: boolean, defaults to `true`, makes exchange and queue durable, i.e. will be returned when getting state - `exclusive`: boolean, queue is exclusively consumed - `noAck`: boolean, set to `true` if there is no need to acknowledge message @@ -167,6 +172,7 @@ Asserts exchange and creates a temporary queue with random name, i.e. not durabl - `autoDelete`: boolean, defaults to `true`, exchange will be deleted when all bindings are removed; the queue will be removed when all consumers are down - `consumerTag`: unique consumer tag - `deadLetterExchange`: string, name of dead letter exchange. Will be asserted as topic exchange + - `deadLetterRoutingKey`: optional string, override routing key when publishing dead-lettered messages - **`durable`**: set to `false` with no option to override - `noAck`: boolean, set to `true` if there is no need to acknowledge message - `prefetch`: integer, defaults to `1`, number of messages to consume at a time @@ -174,16 +180,12 @@ Asserts exchange and creates a temporary queue with random name, i.e. not durabl ### `broker.subscribeOnce(exchangeName, pattern, onMessage[, options])` -Same as `subscribeTmp` and will immediately close consumer when first message arrive. +Same as `subscribeTmp` and will immediately close consumer when first message arrive. Accepts the same option object as [`broker.subscribe`](#brokersubscribeexchangename-pattern-queuename-onmessage-options), but only `consumerTag` and `priority` are honored — `noAck`, `autoDelete`, and `durable` are forced internally, and queue-lifecycle / dead-letter options are ignored because the temporary queue is deleted after the first delivery. - `exchangeName`: exchange name - `pattern`: queue binding pattern - `onMessage`: message callback -- `options`: - - `consumerTag`: unique consumer tag - - `priority`: integer, defaults to `0`, higher value gets messages first - -Oh, btw, option `noAck` will be set to `true` so there is no need to ack message in message callback. +- `options`: optional object, see above ### `broker.unsubscribe(queueName, onMessage)` @@ -244,16 +246,7 @@ Arguments: - `priority`: optional binding priority - `cloneMessage`: clone message function called with shoveled message -Returns: - -- `name`: name of e2e binding -- `source`: source exchange name -- `destination`: destination exchange name -- `pattern`: pattern -- `queue`: name of source e2e queue -- `consumerTag`: consumer tag for temporary source e2e queue -- `on(eventName, handler)`: listen for shovel events, returns event consumer -- `close()`: close e2e binding +Returns an [Exchange2Exchange](#exchange2exchange) wrapper. ### `broker.unbindExchange(source, destination[, pattern])` @@ -274,6 +267,8 @@ Assert a queue into existence. - `durable`: boolean, defaults to `true`, makes queue durable, i.e. will be returned when getting state - `autoDelete`: boolean, defaults to `true`, the queue will be removed when all consumers are down - `deadLetterExchange`: string, name of dead letter exchange. Will be asserted as topic exchange if non-existing + - `deadLetterRoutingKey`: optional string, override routing key when publishing dead-lettered messages + - `maxLength`: integer, drop the oldest non-pending message when the queue would exceed this length - `messageTtl`: integer, expire message after milliseconds, [see Message Eviction](#message-eviction) Returns [Queue](#queue). @@ -331,6 +326,8 @@ Create queue with name. Throws if queue already exists. - `durable`: boolean, defaults to `true`, makes queue durable, i.e. will be returned when getting state - `autoDelete`: boolean, defaults to `true`, the queue will be removed when all consumers are down - `deadLetterExchange`: string, name of dead letter exchange. Will be asserted as topic exchange if non-existing + - `deadLetterRoutingKey`: optional string, override routing key when publishing dead-lettered messages + - `maxLength`: integer, drop the oldest non-pending message when the queue would exceed this length - `messageTtl`: integer, expire message after milliseconds, [see Message Eviction](#message-eviction) Returns [Queue](#queue). @@ -370,7 +367,7 @@ Return serializable object containing durable exchanges, bindings, and durable q ### `broker.recover([state])` -Recovers exchanges, bindings, and queues with messages. A state may be passed, preferably from [`getState()`](#brokergetstate). +Recovers exchanges, bindings, and queues with messages. A state may be passed, preferably from [`getState()`](#brokergetstate). With no argument, restarts stopped exchanges and queues in place. ### `broker.purgeQueue(queueName)` @@ -392,7 +389,11 @@ Arguments: - `queueName`: name of queue - `options`: optional object with options + - `consumerTag`: optional consumer tag to attach to the consumed message + - `exclusive`: boolean, takes the queue exclusively for this read - `noAck`: optional boolean, defaults to `false` + - `prefetch`: integer, currently passes through but only one message is ever returned + - `priority`: integer, defaults to `0` ### `broker.ack(message[, allUpTo])` @@ -453,10 +454,18 @@ Get shovel by name. Returns [Shovel](#new-shovelname-source-destination-options). +### `broker.getShovels()` + +List all active shovels owned by this broker. Returns an array of [Shovel](#new-shovelname-source-destination-options) instances. + ### `broker.closeShovel(name)` Close shovel by name. +### `broker.validateConsumerTag(consumerTag)` + +Throws [`SmqpError`](#smqperror) with code `ERR_SMQP_CONSUMER_TAG_CONFLICT` if the tag is already taken on this broker, otherwise returns `true`. Mainly useful when constructing a consumer tag manually before calling `subscribe`/`consume`. + ### `broker.on(eventName, callback[, options])` Listen for events from Broker. @@ -523,7 +532,7 @@ broker.off('return', { consumerTag: 'my-event-consumertag' }); ### `broker.prefetch(count)` -Noop, only placeholder. +Noop, only placeholder — accepts a `count` argument for amqp-shape compatibility but ignores it. ### `broker.reset()` @@ -541,6 +550,7 @@ Properties: - `bindingCount`: getter for number of bindings - `bindings`: getter for list of [bindings](#binding) - `stopped`: boolean for if the exchange is stopped +- `undeliveredCount`: getter for number of messages held in the exchange's internal delivery queue (not yet routed) ### `exchange.bindQueue(queue, pattern[, bindOptions])` @@ -601,6 +611,8 @@ Recover exchange. ### `exchange.stop()` +Stop the exchange. Subsequent `publish` calls are silently dropped until [`exchange.recover()`](#exchangerecoverstate-getqueue) is called. + ### `exchange.unbindQueue(queue, pattern)` Unbind queue from exchange. @@ -653,6 +665,7 @@ Properties: - `messages`: actual messages array, probably a good idea to not mess with, but it's there - `messageCount`: message count - `consumerCount`: consumer count +- `consumers`: snapshot array of [consumer](#consumer) instances currently bound to the queue - `stopped`: is stopped - `exclusive`: is exclusively consumed - `maxLength`: get or set max length of queue @@ -783,6 +796,8 @@ Queue message. ### `queue.recover([state])` +Recover queue, optionally from a previous [`queue.getState()`](#queuegetstate). With no argument, requeues messages held by current consumers and resumes consumption. + ### `queue.reject(message[, requeue = true])` ### `queue.stop()` @@ -841,7 +856,11 @@ What it is all about - convey messages. - `messageId`: unique message id - `persistent`: persist message, if unset queue option durable prevails - `timestamp`: `Date.now()` - - `expiration`: Expire message after milliseconds + - `expiration`: expire message after milliseconds + - `ttl`: absolute expiry timestamp (`timestamp + expiration`), set automatically when `expiration` is provided + - `mandatory`: boolean, if `true` the publishing exchange emits `return` when the message isn't routed to any queue + - `source-exchange`: present on shovelled / e2e-routed messages, names the originating exchange + - `shovel-name`: present on cross-broker shovelled messages, names the shovel - `get pending()`: boolean indicating that the message is awaiting ack (true) or is acked/nacked (false) ### `message.ack([allUpTo])` @@ -963,6 +982,27 @@ Arguments: - `callbackOrObject`: event callback function to off or object with basically one property: - `consumerTag`: optional event consumer tag to off +## Exchange2Exchange + +In-broker exchange-to-exchange shovel wrapper returned by [`broker.bindExchange`](#brokerbindexchangesource-destination-pattern-args). Thin facade over an internal [Shovel](#new-shovelname-source-destination-options). + +**Properties** (all readonly): + +- `name`: e2e binding name +- `source`: source exchange name +- `destination`: destination exchange name +- `pattern`: binding pattern +- `queue`: name of the source e2e queue +- `consumerTag`: consumer tag of the source e2e consumer + +### `exchange2exchange.on(eventName, handler)` + +Listen for events from the underlying shovel. Returns the event [consumer](#consumer). + +### `exchange2exchange.close()` + +Close the e2e binding (closes the underlying shovel). + ## SmqpError `throw SmqpError(message, code)` inherited from Error, it is thrown when package specific errors occur. diff --git a/CHANGELOG.md b/CHANGELOG.md index ec26478..b1ec7f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ ## Unreleased +## v12.0.0 - 2026-05-01 + +- build commonjs with rollup into one file, namely dist/index.cjs +- refactor types by using jsdoc typing and dts-buddy, should probably be closer to the truth but be aware +- development is now performed in node 22 since eslint dropped support for previous versions + ## v11.0.1 - 2025-12-02 - mitigate degraded performance as a result of refactoring consumer tag uniqueness diff --git a/LICENSE b/LICENSE index 3c701d1..7e6f7ca 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2020 Pål Edman +Copyright (c) 2020 PÃ¥l Edman Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/appveyor.yml b/appveyor.yml index 728be5e..235a36a 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,6 +1,6 @@ environment: matrix: - - nodejs_version: 20 + - nodejs_version: 22 install: - ps: Install-Product node $env:nodejs_version diff --git a/dist/Broker.js b/dist/Broker.js deleted file mode 100644 index 3174865..0000000 --- a/dist/Broker.js +++ /dev/null @@ -1,434 +0,0 @@ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.Broker = Broker; -var _Exchange = require("./Exchange.js"); -var _Queue = require("./Queue.js"); -var _Shovel = require("./Shovel.js"); -var _shared = require("./shared.js"); -var _Errors = require("./Errors.js"); -const kEntities = Symbol.for('entities'); -const kEventHandler = Symbol.for('eventHandler'); -function Broker(owner) { - if (!(this instanceof Broker)) { - return new Broker(owner); - } - this.owner = owner; - const events = this.events = new _Exchange.EventExchange('broker__events'); - const entities = this[kEntities] = new Map([['exchanges', new Map()], ['queues', new Map()], ['consumers', new Map()], ['shovels', new Map()]]); - this[kEventHandler] = new BrokerEventHandler(events, entities); -} -Object.defineProperties(Broker.prototype, { - exchangeCount: { - get() { - return this[kEntities].get('exchanges').size; - } - }, - queueCount: { - get() { - return this[kEntities].get('queues').size; - } - }, - consumerCount: { - get() { - return this[kEntities].get('consumers').size; - } - } -}); -Broker.prototype.subscribe = function subscribe(exchangeName, pattern, queueName, onMessage, options) { - if (!exchangeName || !pattern || typeof onMessage !== 'function') throw new TypeError('exchange name, pattern, and message callback are required'); - if (options?.consumerTag) this.validateConsumerTag(options.consumerTag); - const exchange = this.assertExchange(exchangeName); - const queueOptions = { - durable: true, - ...options - }; - const queue = this.assertQueue(queueName, queueOptions); - exchange.bindQueue(queue, pattern, queueOptions); - return queue.assertConsumer(onMessage, queueOptions, this.owner); -}; -Broker.prototype.subscribeTmp = function subscribeTmp(exchangeName, pattern, onMessage, options) { - return this.subscribe(exchangeName, pattern, null, onMessage, { - ...options, - durable: false - }); -}; -Broker.prototype.subscribeOnce = function subscribeOnce(exchangeName, pattern, onMessage, options) { - if (typeof onMessage !== 'function') throw new TypeError('message callback is required'); - if (options?.consumerTag) this.validateConsumerTag(options.consumerTag); - const exchange = this.assertExchange(exchangeName); - const onceOptions = { - autoDelete: true, - durable: false, - priority: options?.priority ?? 0 - }; - const onceQueue = this.createQueue(null, onceOptions); - exchange.bindQueue(onceQueue, pattern, onceOptions); - return this.consume(onceQueue.name, wrappedOnMessage, { - noAck: true, - consumerTag: options?.consumerTag - }); - function wrappedOnMessage(...args) { - onceQueue.delete(); - onMessage(...args); - } -}; -Broker.prototype.unsubscribe = function unsubscribe(queueName, onMessage) { - const queue = this.getQueue(queueName); - if (!queue) return; - queue.dismiss(onMessage); -}; -Broker.prototype.assertExchange = function assertExchange(exchangeName, type, options) { - let exchange = this.getExchange(exchangeName); - if (exchange) { - if (type && exchange.type !== type) throw new _Errors.SmqpError("Type doesn't match", _Errors.ERR_EXCHANGE_TYPE_MISMATCH); - return exchange; - } - exchange = new _Exchange.Exchange(exchangeName, type || 'topic', options); - this[kEventHandler].listen(exchange.events); - this[kEntities].get('exchanges').set(exchangeName, exchange); - return exchange; -}; -Broker.prototype.bindQueue = function bindQueue(queueName, exchangeName, pattern, bindOptions) { - const exchange = this.getExchange(exchangeName); - const queue = this.getQueue(queueName); - return exchange.bindQueue(queue, pattern, bindOptions); -}; -Broker.prototype.unbindQueue = function unbindQueue(queueName, exchangeName, pattern) { - const exchange = this.getExchange(exchangeName); - if (!exchange) return; - const queue = this.getQueue(queueName); - if (!queue) return; - exchange.unbindQueue(queue, pattern); -}; -Broker.prototype.consume = function consume(queueName, onMessage, options) { - const queue = this.getQueue(queueName); - if (!queue) throw new _Errors.SmqpError(`Queue with name <${queueName}> was not found`, _Errors.ERR_QUEUE_NOT_FOUND); - return queue.consume(onMessage, options, this.owner); -}; -Broker.prototype.cancel = function cancel(consumerTag, requeue = true) { - const consumer = this.getConsumer(consumerTag); - if (!consumer) return false; - consumer.cancel(requeue); - return true; -}; -Broker.prototype.getConsumers = function getConsumers() { - const result = []; - for (const consumer of this[kEntities].get('consumers').values()) { - result.push({ - queue: consumer.queue.name, - consumerTag: consumer.options.consumerTag, - ready: consumer.ready, - options: { - ...consumer.options - } - }); - } - return result; -}; -Broker.prototype.getConsumer = function getConsumer(consumerTag) { - if (typeof consumerTag !== 'string') throw new TypeError('consumer tag must be a string'); - return this[kEntities].get('consumers').get(consumerTag); -}; -Broker.prototype.getExchange = function getExchange(exchangeName) { - if (typeof exchangeName !== 'string') throw new TypeError('exchange name must be a string'); - return this[kEntities].get('exchanges').get(exchangeName); -}; -Broker.prototype.deleteExchange = function deleteExchange(exchangeName, options) { - const exchange = this.getExchange(exchangeName); - if (!exchange || options?.ifUnused && exchange.bindingCount) return false; - this[kEntities].get('exchanges').delete(exchangeName); - exchange.close(); - return true; -}; -Broker.prototype.stop = function stop() { - const entities = this[kEntities]; - for (const exchange of entities.get('exchanges').values()) exchange.stop(); - for (const queue of entities.get('queues').values()) queue.stop(); -}; -Broker.prototype.close = function close() { - const entities = this[kEntities]; - for (const shovel of entities.get('shovels').values()) shovel.close(); - for (const exchange of entities.get('exchanges').values()) exchange.close(); - for (const queue of entities.get('queues').values()) queue.close(); -}; -Broker.prototype.reset = function reset() { - this.stop(); - this.close(); - const entities = this[kEntities]; - entities.get('exchanges').clear(); - entities.get('queues').clear(); - entities.get('consumers').clear(); - entities.get('shovels').clear(); -}; -Broker.prototype.getState = function getState(onlyWithContent) { - const exchanges = this._getExchangeState(onlyWithContent); - const queues = this._getQueuesState(onlyWithContent); - if (onlyWithContent && !exchanges && !queues) return; - return { - exchanges, - queues - }; -}; -Broker.prototype.recover = function recover(state) { - const boundGetQueue = this.getQueue.bind(this); - if (state) { - if (state.queues) { - for (const qState of state.queues) this.assertQueue(qState.name, qState.options).recover(qState); - } - if (state.exchanges) for (const eState of state.exchanges) this.assertExchange(eState.name, eState.type, eState.options).recover(eState, boundGetQueue); - } else { - const entities = this[kEntities]; - for (const queue of entities.get('queues').values()) { - if (queue.stopped) queue.recover(); - } - for (const exchange of entities.get('exchanges').values()) { - if (exchange.stopped) exchange.recover(null, boundGetQueue); - } - } - return this; -}; -Broker.prototype.bindExchange = function bindExchange(source, destination, pattern = '#', args) { - const name = `e2e-${source}2${destination}-${pattern}`; - const shovel = this.createShovel(name, { - broker: this, - exchange: source, - pattern, - priority: args?.priority, - consumerTag: `smq.ctag-${name}` - }, { - broker: this, - exchange: destination - }, { - ...args - }); - return new _Shovel.Exchange2Exchange(shovel); -}; -Broker.prototype.unbindExchange = function unbindExchange(source, destination, pattern = '#') { - const name = `e2e-${source}2${destination}-${pattern}`; - return this.closeShovel(name); -}; -Broker.prototype.publish = function publish(exchangeName, routingKey, content, properties) { - const exchange = this.getExchange(exchangeName); - if (!exchange) return; - return exchange.publish(routingKey, content, properties); -}; -Broker.prototype.purgeQueue = function purgeQueue(queueName) { - const queue = this.getQueue(queueName); - if (!queue) return; - return queue.purge(); -}; -Broker.prototype.sendToQueue = function sendToQueue(queueName, content, options) { - const queue = this.getQueue(queueName); - if (!queue) throw new _Errors.SmqpError(`Queue with name <${queueName}> was not found`, _Errors.ERR_QUEUE_NOT_FOUND); - return queue.queueMessage({}, content, options); -}; -Broker.prototype._getQueuesState = function getQueuesState(onlyWithContent) { - let result; - for (const queue of this[kEntities].get('queues').values()) { - if (!queue.options.durable) continue; - if (onlyWithContent && !queue.messageCount) continue; - if (!result) result = []; - result.push(queue.getState()); - } - return result; -}; -Broker.prototype._getExchangeState = function getExchangeState(onlyWithContent) { - let result; - for (const exchange of this[kEntities].get('exchanges').values()) { - if (!exchange.options.durable) continue; - if (onlyWithContent && !exchange.undeliveredCount) continue; - if (!result) result = []; - result.push(exchange.getState()); - } - return result; -}; -Broker.prototype.createQueue = function createQueue(queueName, options) { - if (queueName && typeof queueName !== 'string') throw new TypeError('queue name must be a string');else if (!queueName) queueName = `smq.qname-${(0, _shared.generateId)()}`;else if (this.getQueue(queueName)) throw new _Errors.SmqpError(`Queue named ${queueName} already exists`, _Errors.ERR_QUEUE_NAME_CONFLICT); - const queueEmitter = new _Exchange.EventExchange(`${queueName}__events`); - this[kEventHandler].listen(queueEmitter); - const queue = new _Queue.Queue(queueName, options, queueEmitter); - this[kEntities].get('queues').set(queueName, queue); - return queue; -}; -Broker.prototype.getQueue = function getQueue(queueName) { - if (!queueName || typeof queueName !== 'string') throw new TypeError('queue name must be a string'); - return this[kEntities].get('queues').get(queueName); -}; -Broker.prototype.assertQueue = function assertQueue(queueName, options) { - if (queueName && typeof queueName !== 'string') throw new TypeError('queue name must be a string');else if (!queueName) return this.createQueue(null, options); - const queue = this.getQueue(queueName); - const queueOptions = { - durable: true, - ...options - }; - if (!queue) return this.createQueue(queueName, queueOptions); - if (queue.options.durable !== queueOptions?.durable) throw new _Errors.SmqpError("Durable doesn't match", _Errors.ERR_QUEUE_DURABLE_MISMATCH); - return queue; -}; -Broker.prototype.deleteQueue = function deleteQueue(queueName, options) { - const queue = this.getQueue(queueName); - if (!queue) return; - return queue.delete(options); -}; -Broker.prototype.get = function getMessageFromQueue(queueName, options) { - const queue = this.getQueue(queueName); - if (!queue) return; - return queue.get({ - noAck: options?.noAck - }); -}; -Broker.prototype.ack = function ack(message, allUpTo) { - message.ack(allUpTo); -}; -Broker.prototype.ackAll = function ackAll() { - for (const queue of this[kEntities].get('queues').values()) queue.ackAll(); -}; -Broker.prototype.nack = function nack(message, allUpTo, requeue) { - message.nack(allUpTo, requeue); -}; -Broker.prototype.nackAll = function nackAll(requeue) { - for (const queue of this[kEntities].get('queues').values()) queue.nackAll(requeue); -}; -Broker.prototype.reject = function reject(message, requeue) { - message.reject(requeue); -}; -Broker.prototype.validateConsumerTag = function validateConsumerTag(consumerTag) { - return this[kEventHandler].validateConsumerTag('' + consumerTag); -}; -Broker.prototype.createShovel = function createShovel(name, source, destination, options) { - const shovels = this[kEntities].get('shovels'); - if (shovels.has(name)) throw new _Errors.SmqpError(`Shovel name must be unique, ${name} is occupied`, _Errors.ERR_SHOVEL_NAME_CONFLICT); - const shovel = new _Shovel.Shovel(name, { - ...source, - broker: this - }, destination, options); - this[kEventHandler].listen(shovel.events); - shovels.set(name, shovel); - return shovel; -}; -Broker.prototype.closeShovel = function closeShovel(name) { - const shovel = this.getShovel(name); - if (shovel) { - shovel.close(); - return true; - } - return false; -}; -Broker.prototype.getShovel = function getShovel(name) { - return this[kEntities].get('shovels').get(name); -}; -Broker.prototype.getShovels = function getShovels() { - return [...this[kEntities].get('shovels').values()]; -}; -Broker.prototype.on = function on(eventName, callback, options) { - return this.events.on(eventName, getEventCallback(), { - ...options, - origin: callback - }); - function getEventCallback() { - return function eventCallback(name, msg) { - callback({ - name, - ...msg.content - }); - }; - } -}; -Broker.prototype.off = function off(eventName, callbackOrObject) { - const { - consumerTag - } = callbackOrObject; - for (const binding of this.events.bindings) { - if (binding.pattern === eventName) { - if (consumerTag) { - binding.queue.cancel(consumerTag); - continue; - } - for (const consumer of binding.queue.consumers) { - if (consumer.options && consumer.options.origin === callbackOrObject) { - consumer.cancel(); - } - } - } - } -}; -Broker.prototype.prefetch = function prefetch() {}; -function BrokerEventHandler(eventExchange, entities) { - this.eventExchange = eventExchange; - this.entities = entities; - this.handler = this.handler.bind(this); -} -BrokerEventHandler.prototype.listen = function listen(emitter) { - emitter.on('#', this.handler); -}; -BrokerEventHandler.prototype.validateConsumerTag = function validateConsumerTag(consumerTag) { - if (this.entities.get('consumers').has(consumerTag)) { - throw new _Errors.SmqpError(`Consumer tag must be unique, ${consumerTag} is occupied`, _Errors.ERR_CONSUMER_TAG_CONFLICT); - } - return true; -}; -BrokerEventHandler.prototype.handler = function eventHandler(eventName, msg) { - switch (eventName) { - case 'exchange.delete': - { - this.entities.get('exchanges').delete(msg.content.name); - break; - } - case 'exchange.return': - { - this.eventExchange.publish('return', msg.content); - break; - } - case 'exchange.message.undelivered': - { - this.eventExchange.publish('message.undelivered', msg.content); - break; - } - case 'queue.delete': - { - this.entities.get('queues').delete(msg.content.name); - break; - } - case 'queue.dead-letter': - { - const exchange = this.entities.get('exchanges').get(msg.content.deadLetterExchange); - if (!exchange) return; - const { - fields, - content, - properties - } = msg.content.message; - exchange.publish(fields.routingKey, content, properties); - break; - } - case 'queue.consume': - { - this.validateConsumerTag(msg.content.consumerTag); - this.entities.get('consumers').set(msg.content.consumerTag, msg.content); - break; - } - case 'queue.consumer.cancel': - { - this.entities.get('consumers').delete(msg.content.consumerTag); - break; - } - case 'queue.message.consumed.ack': - case 'queue.message.consumed.nack': - { - const { - operation, - message - } = msg.content; - this.eventExchange.publish(`message.${operation}`, message); - break; - } - case 'shovel.close': - { - this.entities.get('shovels').delete(msg.content.name); - break; - } - } -}; \ No newline at end of file diff --git a/dist/Errors.js b/dist/Errors.js deleted file mode 100644 index 2c06d62..0000000 --- a/dist/Errors.js +++ /dev/null @@ -1,24 +0,0 @@ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.SmqpError = exports.ERR_SHOVEL_SOURCE_EXCHANGE_NOT_FOUND = exports.ERR_SHOVEL_NAME_CONFLICT = exports.ERR_SHOVEL_DESTINATION_EXCHANGE_NOT_FOUND = exports.ERR_QUEUE_NOT_FOUND = exports.ERR_QUEUE_NAME_CONFLICT = exports.ERR_QUEUE_DURABLE_MISMATCH = exports.ERR_EXCLUSIVE_NOT_ALLOWED = exports.ERR_EXCLUSIVE_CONFLICT = exports.ERR_EXCHANGE_TYPE_MISMATCH = exports.ERR_CONSUMER_TAG_CONFLICT = void 0; -class SmqpError extends Error { - constructor(message, code) { - super(message); - this.type = this.name = this.constructor.name; - this.code = code; - } -} -exports.SmqpError = SmqpError; -const ERR_CONSUMER_TAG_CONFLICT = exports.ERR_CONSUMER_TAG_CONFLICT = 'ERR_SMQP_CONSUMER_TAG_CONFLICT'; -const ERR_EXCHANGE_TYPE_MISMATCH = exports.ERR_EXCHANGE_TYPE_MISMATCH = 'ERR_SMQP_EXCHANGE_TYPE_MISMATCH'; -const ERR_EXCLUSIVE_CONFLICT = exports.ERR_EXCLUSIVE_CONFLICT = 'ERR_SMQP_EXCLUSIVE_CONFLICT'; -const ERR_EXCLUSIVE_NOT_ALLOWED = exports.ERR_EXCLUSIVE_NOT_ALLOWED = 'ERR_SMQP_EXCLUSIVE_NOT_ALLOWED'; -const ERR_QUEUE_DURABLE_MISMATCH = exports.ERR_QUEUE_DURABLE_MISMATCH = 'ERR_SMQP_QUEUE_DURABLE_MISMATCH'; -const ERR_QUEUE_NAME_CONFLICT = exports.ERR_QUEUE_NAME_CONFLICT = 'ERR_SMQP_QUEUE_NAME_CONFLICT'; -const ERR_QUEUE_NOT_FOUND = exports.ERR_QUEUE_NOT_FOUND = 'ERR_SMQP_QUEUE_NOT_FOUND'; -const ERR_SHOVEL_DESTINATION_EXCHANGE_NOT_FOUND = exports.ERR_SHOVEL_DESTINATION_EXCHANGE_NOT_FOUND = 'ERR_SMQP_SHOVEL_DESTINATION_EXCHANGE_NOT_FOUND'; -const ERR_SHOVEL_NAME_CONFLICT = exports.ERR_SHOVEL_NAME_CONFLICT = 'ERR_SMQP_SHOVEL_NAME_CONFLICT'; -const ERR_SHOVEL_SOURCE_EXCHANGE_NOT_FOUND = exports.ERR_SHOVEL_SOURCE_EXCHANGE_NOT_FOUND = 'ERR_SMQP_SHOVEL_SOURCE_EXCHANGE_NOT_FOUND'; \ No newline at end of file diff --git a/dist/Exchange.js b/dist/Exchange.js deleted file mode 100644 index 042b499..0000000 --- a/dist/Exchange.js +++ /dev/null @@ -1,299 +0,0 @@ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.EventExchange = EventExchange; -exports.Exchange = Exchange; -var _Message = require("./Message.js"); -var _Queue = require("./Queue.js"); -var _shared = require("./shared.js"); -const kName = Symbol.for('name'); -const kType = Symbol.for('type'); -const kStopped = Symbol.for('stopped'); -const kBindings = Symbol.for('bindings'); -const kDeliveryQueue = Symbol.for('deliveryQueue'); -function Exchange(name, type = 'topic', options) { - if (!name || typeof name !== 'string') throw new TypeError('Exchange name is required and must be a string'); - if (type !== 'topic' && type !== 'direct') throw new TypeError('Exchange type must be one of topic or direct'); - const eventExchange = new EventExchange(`${name}__events`); - return new ExchangeBase(name, type, options, eventExchange); -} -function EventExchange(name) { - if (!name) name = `smq.ename-${(0, _shared.generateId)()}`; - return new ExchangeBase(name, 'topic', { - durable: false, - autoDelete: true - }); -} -function ExchangeBase(name, type, options, eventExchange) { - this[kName] = name; - this[kType] = type; - this[kBindings] = []; - this[kStopped] = false; - this.options = { - durable: true, - autoDelete: true, - ...options - }; - this.events = eventExchange; - const deliveryQueue = this[kDeliveryQueue] = new _Queue.Queue('delivery-q', { - autoDelete: false - }); - const onMessage = (type === 'topic' ? this._onTopicMessage : this._onDirectMessage).bind(this); - deliveryQueue.consume(onMessage, { - exclusive: true, - consumerTag: '_exchange-tag' - }); -} -Object.defineProperties(ExchangeBase.prototype, { - name: { - get() { - return this[kName]; - } - }, - bindingCount: { - get() { - return this[kBindings].length; - } - }, - bindings: { - get() { - return this[kBindings].slice(); - } - }, - type: { - get() { - return this[kType]; - } - }, - stopped: { - get() { - return this[kStopped]; - } - }, - undeliveredCount: { - get() { - return this[kDeliveryQueue].messageCount; - } - } -}); -ExchangeBase.prototype.publish = function publish(routingKey, content, properties) { - if (this[kStopped]) return; - if (!this[kBindings].length) return this._emitReturn(routingKey, content, properties); - return this[kDeliveryQueue].queueMessage({ - routingKey - }, { - content, - properties - }); -}; -ExchangeBase.prototype._onTopicMessage = function topic(routingKey, message) { - const publishedMsg = message.content; - message.ack(); - let delivered = 0; - for (const binding of this[kBindings].slice()) { - if (!binding.testPattern(routingKey)) continue; - binding.queue.queueMessage({ - routingKey, - exchange: this.name - }, publishedMsg.content, publishedMsg.properties); - ++delivered; - } - if (!delivered) { - this._emitReturn(routingKey, publishedMsg.content, publishedMsg.properties); - } - return delivered; -}; -ExchangeBase.prototype._onDirectMessage = function direct(routingKey, message) { - const publishedMsg = message.content; - const bindings = this[kBindings]; - let deliverToBinding; - for (const binding of bindings) { - if (!binding.testPattern(routingKey)) continue; - deliverToBinding = binding; - break; - } - if (!deliverToBinding) { - message.ack(); - this._emitReturn(routingKey, publishedMsg.content, publishedMsg.properties); - return 0; - } - if (bindings.length > 1) { - const idx = bindings.indexOf(deliverToBinding); - bindings.splice(idx, 1); - bindings.push(deliverToBinding); - } - message.ack(); - deliverToBinding.queue.queueMessage({ - routingKey, - exchange: this.name - }, publishedMsg.content, publishedMsg.properties); - return 1; -}; -ExchangeBase.prototype._emitReturn = function emitReturn(routingKey, content, properties) { - if (!this.events || !properties) return; - if (properties.confirm) { - this.emit('message.undelivered', new _Message.Message({ - routingKey, - exchange: this.name - }, content, properties)); - } - if (properties.mandatory) { - this.emit('return', new _Message.Message({ - routingKey, - exchange: this.name - }, content, properties)); - } -}; -ExchangeBase.prototype.bindQueue = function bindQueue(queue, pattern, bindOptions) { - const bindings = this[kBindings]; - for (const binding of bindings) { - if (binding.queue === queue && binding.pattern === pattern) return binding; - } - const binding = new Binding(this, queue, pattern, bindOptions); - if (bindings.push(binding) > 1 && binding.options.priority) { - bindings.sort(_shared.sortByPriority); - } - return binding; -}; -ExchangeBase.prototype.unbindQueue = function unbindQueue(queue, pattern) { - for (const binding of this[kBindings]) { - if (binding.queue === queue && binding.pattern === pattern) { - return this.closeBinding(binding); - } - } -}; -ExchangeBase.prototype.unbindQueueByName = function unbindQueueByName(queueName) { - for (const binding of this[kBindings]) { - if (binding.queue.name === queueName) this.closeBinding(binding); - } -}; -ExchangeBase.prototype.close = function close() { - for (const binding of this[kBindings]) { - binding.close(); - } - const deliveryQueue = this[kDeliveryQueue]; - deliveryQueue.cancel('_exchange-tag'); - deliveryQueue.close(); -}; -ExchangeBase.prototype.getState = function getState() { - const bindingsState = []; - for (const binding of this[kBindings]) { - if (!binding.queue.options.durable) continue; - bindingsState.push(binding.getState()); - } - const deliveryQueue = this[kDeliveryQueue]; - return { - name: this.name, - type: this.type, - options: { - ...this.options - }, - ...(deliveryQueue.messageCount && { - deliveryQueue: deliveryQueue.getState() - }), - ...(bindingsState.length && { - bindings: bindingsState - }) - }; -}; -ExchangeBase.prototype.stop = function stop() { - this[kStopped] = true; -}; -ExchangeBase.prototype.recover = function recover(state, getQueue) { - this[kStopped] = false; - if (!state) return this; - const deliveryQueue = this[kDeliveryQueue]; - if (state.bindings) { - for (const bindingState of state.bindings) { - const queue = getQueue(bindingState.queueName); - if (!queue) return; - this.bindQueue(queue, bindingState.pattern, bindingState.options); - } - } - deliveryQueue.recover(state.deliveryQueue); - if (!deliveryQueue.consumerCount) { - const onMessage = (this[kType] === 'topic' ? this._onTopicMessage : this._onDirectMessage).bind(this); - deliveryQueue.consume(onMessage, { - exclusive: true, - consumerTag: '_exchange-tag' - }); - } - return this; -}; -ExchangeBase.prototype.getBinding = function getBinding(queueName, pattern) { - for (const binding of this[kBindings]) { - if (binding.queue.name === queueName && binding.pattern === pattern) return binding; - } -}; -ExchangeBase.prototype.emit = function emit(eventName, content) { - if (this.events) return this.events.publish(`exchange.${eventName}`, content); - return this.publish(eventName, content); -}; -ExchangeBase.prototype.on = function on(pattern, handler, consumeOptions) { - if (this.events) return this.events.on(`exchange.${pattern}`, handler, consumeOptions); - const eventQueue = new _Queue.Queue(null, { - durable: false, - autoDelete: true - }); - const binding = this.bindQueue(eventQueue, pattern); - eventQueue.events = { - emit(eventName) { - if (eventName === 'queue.delete') binding.close(); - } - }; - return eventQueue.consume(handler, { - ...consumeOptions, - noAck: true - }, this); -}; -ExchangeBase.prototype.off = function off(pattern, handler) { - if (this.events) return this.events.off(`exchange.${pattern}`, handler); - const { - consumerTag - } = handler; - for (const binding of this[kBindings]) { - if (binding.pattern === pattern) { - if (consumerTag) binding.queue.cancel(consumerTag);else binding.queue.dismiss(handler); - } - } -}; -ExchangeBase.prototype.closeBinding = function closeBinding(binding) { - const bindings = this[kBindings]; - const idx = bindings.indexOf(binding); - if (idx === -1) return; - bindings.splice(idx, 1); - binding.close(); - if (!bindings.length && this.options.autoDelete) this.emit('delete', this); -}; -function Binding(exchange, queue, pattern, bindOptions) { - this.id = `${queue.name}/${pattern}`; - this.options = { - priority: 0, - ...bindOptions - }; - this.pattern = pattern; - this.exchange = exchange; - this.queue = queue; - this._compiledPattern = (0, _shared.getRoutingKeyPattern)(pattern); - queue.on('delete', () => { - this.close(); - }); -} -Binding.prototype.testPattern = function testPattern(routingKey) { - return this._compiledPattern.test(routingKey); -}; -Binding.prototype.close = function closeBinding() { - this.exchange.unbindQueue(this.queue, this.pattern); -}; -Binding.prototype.getState = function getBindingState() { - return { - id: this.id, - options: { - ...this.options - }, - queueName: this.queue.name, - pattern: this.pattern - }; -}; \ No newline at end of file diff --git a/dist/Message.js b/dist/Message.js deleted file mode 100644 index 9b10ba1..0000000 --- a/dist/Message.js +++ /dev/null @@ -1,56 +0,0 @@ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.Message = Message; -exports.kPending = void 0; -var _shared = require("./shared.js"); -const kPending = exports.kPending = Symbol.for('pending'); -const kOnConsumed = Symbol.for('onConsumed'); -function Message(fields, content, properties, onConsumed) { - this[kOnConsumed] = [null, onConsumed]; - this[kPending] = false; - const mproperties = { - ...properties, - messageId: properties?.messageId || `smq.mid-${(0, _shared.generateId)()}` - }; - const timestamp = mproperties.timestamp = mproperties.timestamp || Date.now(); - if (mproperties.expiration) { - mproperties.ttl = timestamp + parseInt(mproperties.expiration); - } - const { - consumerTag, - ...mfields - } = fields; - this.fields = mfields; - this.content = content; - this.properties = mproperties; -} -Object.defineProperty(Message.prototype, 'pending', { - get() { - return this[kPending]; - } -}); -Message.prototype.ack = function ack(allUpTo) { - if (!this[kPending]) return; - for (const fn of this[kOnConsumed]) { - if (fn) fn(this, 'ack', allUpTo); - } - this[kPending] = false; -}; -Message.prototype.nack = function nack(allUpTo, requeue = true) { - if (!this[kPending]) return; - for (const fn of this[kOnConsumed]) { - if (fn) fn(this, 'nack', allUpTo, requeue); - } - this[kPending] = false; -}; -Message.prototype.reject = function reject(requeue = true) { - this.nack(false, requeue); -}; -Message.prototype._consume = function consume(consumerTag, consumedCb) { - this[kPending] = true; - this.fields.consumerTag = consumerTag; - this[kOnConsumed][0] = consumedCb; -}; \ No newline at end of file diff --git a/dist/Queue.js b/dist/Queue.js deleted file mode 100644 index 526bd5a..0000000 --- a/dist/Queue.js +++ /dev/null @@ -1,562 +0,0 @@ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.Consumer = Consumer; -exports.Queue = Queue; -var _shared = require("./shared.js"); -var _Message = require("./Message.js"); -var _Errors = require("./Errors.js"); -const kName = Symbol.for('name'); -const kConsumers = Symbol.for('consumers'); -const kConsuming = Symbol.for('consuming'); -const kExclusive = Symbol.for('exclusive'); -const kInternalQueue = Symbol.for('internalQueue'); -const kIsReady = Symbol.for('isReady'); -const kAvailableCount = Symbol.for('availableCount'); -const kStopped = Symbol.for('stopped'); -function Queue(name, options, eventEmitter) { - if (name && typeof name !== 'string') throw new TypeError('Queue name must be a string');else if (!name) name = `smq.qname-${(0, _shared.generateId)()}`; - this[kName] = name; - this.options = { - autoDelete: true, - ...options - }; - this.messages = []; - this.events = eventEmitter; - this[kConsumers] = []; - this[kStopped] = false; - this[kAvailableCount] = 0; - this[kExclusive] = false; - this._onMessageConsumed = this._onMessageConsumed.bind(this); -} -Object.defineProperties(Queue.prototype, { - name: { - get() { - return this[kName]; - } - }, - consumerCount: { - get() { - return this[kConsumers].length; - } - }, - consumers: { - get() { - return this[kConsumers].slice(); - } - }, - exclusive: { - get() { - return this[kExclusive]; - } - }, - messageCount: { - get() { - return this.messages.length; - } - }, - stopped: { - get() { - return this[kStopped]; - } - } -}); -Queue.prototype.queueMessage = function queueMessage(fields, content, properties) { - if (fields && typeof fields !== 'object') throw new TypeError('fields must be an object'); - if (properties && typeof properties !== 'object') throw new TypeError('properties must be an object'); - if (this[kStopped]) return; - const messageTtl = this.options.messageTtl; - const messageProperties = { - ...properties - }; - if (messageTtl && !('expiration' in messageProperties)) { - messageProperties.expiration = messageTtl; - } - const message = new _Message.Message(fields ?? {}, content, messageProperties, this._onMessageConsumed); - const capacity = this._getCapacity(); - this.messages.push(message); - this[kAvailableCount]++; - let discarded; - switch (capacity) { - case 0: - discarded = this.evictFirst(message); - break; - case 1: - this.emit('saturated', this); - break; - } - return discarded ? 0 : this._consumeNext(); -}; -Queue.prototype.evictFirst = function evictFirst(compareMessage) { - const evict = this.get(); - if (!evict) return; - evict.nack(false, false); - return evict === compareMessage; -}; -Queue.prototype._consumeNext = function consumeNext() { - if (this[kStopped] || !this[kAvailableCount]) return; - const consumers = this[kConsumers]; - let consumed = 0; - if (!consumers.length) return consumed; - for (const consumer of consumers) { - if (!consumer.ready) continue; - const msgs = this._consumeMessages(consumer.capacity, consumer.options); - if (!msgs.length) return consumed; - consumer._push(msgs); - consumed += msgs.length; - } - return consumed; -}; -Queue.prototype.consume = function consume(onMessage, consumeOptions, owner) { - const consumers = this[kConsumers]; - if (consumers.length) { - if (this[kExclusive]) throw new _Errors.SmqpError(`Queue ${this.name} is exclusively consumed by ${consumers[0].consumerTag}`, _Errors.ERR_EXCLUSIVE_CONFLICT); - if (consumeOptions?.exclusive) throw new _Errors.SmqpError(`Queue ${this.name} already has consumers and cannot be exclusively consumed`, _Errors.ERR_EXCLUSIVE_NOT_ALLOWED); - } - const consumer = new Consumer(this, onMessage, consumeOptions, owner, new ConsumerEmitter(this)); - this.emit('consume', consumer); - if (consumers.push(consumer) > 1 && consumer.options.priority) { - consumers.sort(_shared.sortByPriority); - } - if (consumer.options.exclusive) { - this[kExclusive] = true; - } - const pendingMessages = this._consumeMessages(consumer.capacity, consumer.options); - if (pendingMessages.length) consumer._push(pendingMessages); - return consumer; -}; -Queue.prototype.assertConsumer = function assertConsumer(onMessage, consumeOptions, owner) { - const consumers = this[kConsumers]; - if (!consumers.length) return this.consume(onMessage, consumeOptions, owner); - for (const consumer of consumers) { - if (consumer.onMessage !== onMessage) continue; - if (consumeOptions) { - if (consumeOptions.consumerTag && consumeOptions.consumerTag !== consumer.consumerTag) { - continue; - } else if ('exclusive' in consumeOptions && consumeOptions.exclusive !== consumer.options.exclusive) { - continue; - } - } - return consumer; - } - return this.consume(onMessage, consumeOptions, owner); -}; -Queue.prototype.get = function getMessage(options) { - const message = this._consumeMessages(1, { - noAck: options?.noAck, - consumerTag: options?.consumerTag - })[0]; - if (!message) return false; - if (options?.noAck) { - this._dequeueMessage(message); - message[_Message.kPending] = false; - } - return message; -}; -Queue.prototype._consumeMessages = function consumeMessages(n, consumeOptions) { - const msgs = []; - if (this[kStopped] || !this[kAvailableCount] || !n) return msgs; - const messages = this.messages; - if (!messages.length) return msgs; - const evict = []; - for (const message of messages) { - if (message.pending) continue; - if (message.properties.expiration && message.properties.ttl < Date.now()) { - evict.push(message); - continue; - } - message._consume(consumeOptions?.consumerTag); - this[kAvailableCount]--; - msgs.push(message); - if (! --n) break; - } - if (evict.length) { - for (const expired of evict) this.nack(expired, false, false); - } - return msgs; -}; -Queue.prototype.ack = function ack(message, allUpTo) { - if (this._onMessageConsumed(message, 'ack', allUpTo, false)) message[_Message.kPending] = false; -}; -Queue.prototype.nack = function nack(message, allUpTo, requeue = true) { - if (this._onMessageConsumed(message, 'nack', allUpTo, requeue)) message[_Message.kPending] = false; -}; -Queue.prototype.reject = function reject(message, requeue = true) { - if (this._onMessageConsumed(message, 'nack', false, requeue)) message[_Message.kPending] = false; -}; -Queue.prototype._onMessageConsumed = function onMessageConsumed(message, operation, allUpTo, requeue) { - if (this[kStopped]) return; - const msgIdx = this._dequeueMessage(message); - if (msgIdx === -1) return false; - const messages = this.messages; - const pending = allUpTo && this._getPendingMessages(msgIdx); - let deadLetterExchange; - switch (operation) { - case 'ack': - break; - case 'nack': - { - if (requeue) { - this[kAvailableCount]++; - messages.splice(msgIdx, 0, new _Message.Message({ - ...message.fields, - redelivered: true - }, message.content, message.properties, this._onMessageConsumed)); - } else { - deadLetterExchange = this.options.deadLetterExchange; - } - break; - } - } - let capacity; - if (!messages.length) this.emit('depleted', this);else if ((capacity = this._getCapacity()) === 1) this.emit('ready', capacity); - const pendingLength = pending && pending.length; - if (!pendingLength) this._consumeNext(); - if (!requeue && message.properties.confirm) { - this.emit(`message.consumed.${operation}`, { - operation, - message: { - ...message - } - }); - } - if (deadLetterExchange) { - const deadLetterRoutingKey = this.options.deadLetterRoutingKey; - const { - expiration, - ...messageProperties - } = message.properties; - const deadMessage = new _Message.Message(message.fields, message.content, messageProperties); - if (deadLetterRoutingKey) deadMessage.fields.routingKey = deadLetterRoutingKey; - this.emit('dead-letter', { - deadLetterExchange, - message: deadMessage - }); - } - if (pendingLength) { - for (const msg of pending) { - msg[operation](false, requeue); - } - } - return true; -}; -Queue.prototype.ackAll = function ackAll() { - for (const msg of this._getPendingMessages()) { - msg.ack(false); - } -}; -Queue.prototype.nackAll = function nackAll(requeue = true) { - for (const msg of this._getPendingMessages()) { - msg.nack(false, requeue); - } -}; -Queue.prototype._getPendingMessages = function getPendingMessages(untilIndex) { - const messages = this.messages; - const l = messages.length; - const result = []; - if (!l) return result; - const until = untilIndex ?? l; - for (let i = 0; i < until; ++i) { - const msg = messages[i]; - if (!msg.pending) continue; - result.push(msg); - } - return result; -}; -Queue.prototype.peek = function peek(ignoreDelivered) { - const message = this.messages[0]; - if (!message) return; - if (!ignoreDelivered) return message; - if (!message.pending) return message; - for (const msg of this.messages) { - if (!msg.pending) return msg; - } -}; -Queue.prototype.cancel = function cancel(consumerTag, requeue) { - const consumers = this[kConsumers]; - const idx = consumers.findIndex(c => c.consumerTag === consumerTag); - if (idx === -1) return false; - const consumer = consumers[idx]; - this.unbindConsumer(consumer, requeue); - return true; -}; -Queue.prototype.dismiss = function dismiss(onMessage, requeue) { - const consumers = this[kConsumers]; - const consumer = consumers.find(c => c.onMessage === onMessage); - if (!consumer) return; - this.unbindConsumer(consumer, requeue); -}; -Queue.prototype.unbindConsumer = function unbindConsumer(consumer, requeue = true) { - const consumers = this[kConsumers]; - const idx = consumers.indexOf(consumer); - if (idx === -1) return; - consumers.splice(idx, 1); - this[kExclusive] = false; - consumer.stop(); - consumer.nackAll(requeue); - this.emit('consumer.cancel', consumer); - if (!consumers.length && this.options.autoDelete) return this.emit('delete', this); -}; -Queue.prototype.emit = function emit(eventName, content) { - if (!this.events) return; - this.events.emit(`queue.${eventName}`, content); -}; -Queue.prototype.on = function on(eventName, handler, options) { - if (!this.events) return; - return this.events.on(`queue.${eventName}`, handler, options); -}; -Queue.prototype.off = function off(eventName, handler) { - if (!this.events) return; - return this.events.off(`queue.${eventName}`, handler); -}; -Queue.prototype.purge = function purge() { - const toDelete = this.messages.filter(({ - pending - }) => !pending); - this[kAvailableCount] = 0; - for (const msg of toDelete) { - this._dequeueMessage(msg); - } - if (!this.messages.length) this.emit('depleted', this); - return toDelete.length; -}; -Queue.prototype._dequeueMessage = function dequeueMessage(message) { - const messages = this.messages; - const msgIdx = messages.indexOf(message); - if (msgIdx === -1) return msgIdx; - messages.splice(msgIdx, 1); - return msgIdx; -}; -Queue.prototype.getState = function getState() { - const msgs = this.messages; - const state = { - name: this.name, - options: { - ...this.options - } - }; - if (msgs.length) { - try { - state.messages = JSON.parse(JSON.stringify(msgs)); - } catch (err) { - err.code = 'EQUEUE_STATE'; - err.queue = this.name; - throw err; - } - } - return state; -}; -Queue.prototype.recover = function recover(state) { - this[kStopped] = false; - const consumers = this[kConsumers]; - if (!state) { - for (const c of consumers.slice()) c.recover(); - this._consumeNext(); - return this; - } - this.messages.splice(0); - let continueConsume; - if (consumers.length) { - for (const c of consumers) c.nackAll(false); - continueConsume = true; - } - if (!state.messages) return this; - const onConsumed = this._onMessageConsumed; - for (const { - fields, - content, - properties - } of state.messages) { - if (properties?.persistent === false) continue; - const msg = new _Message.Message({ - ...fields, - redelivered: true - }, content, properties, onConsumed); - this.messages.push(msg); - } - this[kAvailableCount] = this.messages.length; - for (const c of consumers) c.recover(); - if (continueConsume) { - this._consumeNext(); - } - return this; -}; -Queue.prototype.delete = function deleteQueue(options) { - const consumers = this[kConsumers]; - if (options?.ifUnused && consumers.length) return; - const messages = this.messages; - if (options?.ifEmpty && messages.length) return; - this[kStopped] = true; - const messageCount = messages.length; - for (const consumer of this[kConsumers].splice(0)) { - this.emit('consumer.cancel', consumer); - } - messages.splice(0); - this.emit('delete', this); - return { - messageCount - }; -}; -Queue.prototype.close = function close() { - for (const consumer of this[kConsumers].slice()) { - this.unbindConsumer(consumer); - } -}; -Queue.prototype.stop = function stop() { - this[kStopped] = true; - for (const consumer of this[kConsumers].slice()) { - consumer.stop(); - } -}; -Queue.prototype._getCapacity = function getCapacity() { - if ('maxLength' in this.options) { - return this.options.maxLength - this.messages.length; - } - return Infinity; -}; -function Consumer(queue, onMessage, options, owner, eventEmitter) { - if (typeof onMessage !== 'function') throw new TypeError('message callback is required and must be a function'); - const { - consumerTag - } = this.options = { - prefetch: 1, - priority: 0, - noAck: false, - ...options - }; - if (!consumerTag) this.options.consumerTag = `smq.ctag-${(0, _shared.generateId)()}`;else if (typeof consumerTag !== 'string') throw new TypeError('consumerTag must be a string'); - this.queue = queue; - this.onMessage = onMessage; - this.owner = owner; - this.events = eventEmitter; - this[kName] = this.options.consumerTag; - this[kIsReady] = true; - this[kStopped] = false; - this[kConsuming] = false; - this[kInternalQueue] = new Queue(`${this.options.consumerTag}-q`, { - autoDelete: false, - maxLength: this.options.prefetch - }, new ConsumerQueueEvents(this)); -} -Object.defineProperties(Consumer.prototype, { - consumerTag: { - get() { - return this[kName]; - } - }, - ready: { - get() { - return this[kIsReady] && !this[kStopped]; - } - }, - stopped: { - get() { - return this[kStopped]; - } - }, - capacity: { - get() { - return this[kInternalQueue]._getCapacity(); - } - }, - messageCount: { - get() { - return this[kInternalQueue].messageCount; - } - }, - queueName: { - get() { - return this.queue.name; - } - } -}); -Consumer.prototype._push = function push(messages) { - const internalQueue = this[kInternalQueue]; - for (const message of messages) { - internalQueue.queueMessage(message.fields, message, message.properties); - } - if (!this[kConsuming]) { - this[kConsuming] = true; - try { - this._consume(); - } finally { - this[kConsuming] = false; - } - } -}; -Consumer.prototype._consume = function consume() { - const internalQ = this[kInternalQueue]; - const consumerTag = this[kName]; - let _msg; - while (_msg = internalQ.get()) { - const msg = _msg; - msg._consume(consumerTag); - const message = msg.content; - message._consume(consumerTag, () => msg.ack(false)); - if (this.options.noAck) message.ack(); - this.onMessage(msg.fields.routingKey, message, this.owner); - if (this[kStopped]) break; - } -}; -Consumer.prototype.nackAll = function nackAll(requeue) { - for (const msg of this[kInternalQueue].messages.slice()) { - msg.content.nack(false, requeue); - } -}; -Consumer.prototype.ackAll = function ackAll() { - for (const msg of this[kInternalQueue].messages.slice()) { - msg.content.ack(false); - } -}; -Consumer.prototype.cancel = function cancel(requeue = true) { - this.stop(); - if (!requeue) this.nackAll(requeue); - this.emit('cancel', this); -}; -Consumer.prototype.prefetch = function prefetch(value) { - this.options.prefetch = this[kInternalQueue].options.maxLength = value; -}; -Consumer.prototype.emit = function emit(eventName, content) { - const routingKey = `consumer.${eventName}`; - this.events.emit(routingKey, content); -}; -Consumer.prototype.on = function on(eventName, handler) { - const pattern = `consumer.${eventName}`; - return this.events.on(pattern, handler); -}; -Consumer.prototype.recover = function recover() { - this[kStopped] = false; -}; -Consumer.prototype.stop = function stop() { - this[kStopped] = true; -}; -function ConsumerEmitter(queue) { - this.queue = queue; -} -ConsumerEmitter.prototype.on = function on(...args) { - return this.queue.on(...args); -}; -ConsumerEmitter.prototype.emit = function emit(eventName, content) { - if (eventName === 'consumer.cancel') { - return this.queue.unbindConsumer(content); - } - this.queue.emit(eventName, content); -}; -function ConsumerQueueEvents(consumer) { - this.consumer = consumer; -} -ConsumerQueueEvents.prototype.emit = function queueHandler(eventName) { - switch (eventName) { - case 'queue.saturated': - { - this.consumer[kIsReady] = false; - break; - } - case 'queue.depleted': - case 'queue.ready': - this.consumer[kIsReady] = true; - break; - } -}; \ No newline at end of file diff --git a/dist/Shovel.js b/dist/Shovel.js deleted file mode 100644 index 767f519..0000000 --- a/dist/Shovel.js +++ /dev/null @@ -1,200 +0,0 @@ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.Exchange2Exchange = Exchange2Exchange; -exports.Shovel = Shovel; -var _Exchange = require("./Exchange.js"); -var _Errors = require("./Errors.js"); -const kName = Symbol.for('name'); -const kBrokerInternal = Symbol.for('brokerInternal'); -const kCloneMessage = Symbol.for('cloneMessage'); -const kClosed = Symbol.for('closed'); -const kConsumerTag = Symbol.for('consumerTag'); -const kDestinationExchange = Symbol.for('destinationExchange'); -const kEventHandlers = Symbol.for('eventHandlers'); -const kSourceBroker = Symbol.for('sourceBroker'); -const kSourceExchange = Symbol.for('sourceExchange'); -const kE2EShovel = Symbol.for('shovel'); -function Shovel(name, source, destination, options) { - if (!name || typeof name !== 'string') throw new TypeError('Shovel name is required and must be a string'); - const { - broker: sourceBroker, - exchange: sourceExchangeName, - pattern, - queue, - priority - } = source; - const { - broker: destinationBroker, - exchange: destinationExchangeName - } = destination; - const sourceExchange = sourceBroker.getExchange(sourceExchangeName); - if (!sourceExchange) { - throw new _Errors.SmqpError(`shovel ${name} source exchange <${sourceExchangeName}> not found`, _Errors.ERR_SHOVEL_SOURCE_EXCHANGE_NOT_FOUND); - } - const destinationExchange = destinationBroker.getExchange(destinationExchangeName); - if (!destinationExchange) { - throw new _Errors.SmqpError(`shovel ${name} destination exchange <${destinationExchangeName}> not found`, _Errors.ERR_SHOVEL_DESTINATION_EXCHANGE_NOT_FOUND); - } - if (!(this instanceof Shovel)) { - return new Shovel(name, source, destination, options); - } - this[kBrokerInternal] = sourceBroker === destinationBroker; - const routingKeyPattern = pattern || '#'; - this[kName] = name; - this.source = { - ...source, - pattern: routingKeyPattern - }; - this.destination = { - ...destination - }; - this.events = new _Exchange.EventExchange('shovel__events'); - const consumerTag = this[kConsumerTag] = source.consumerTag || `smq.shoveltag-${name}`; - this[kClosed] = false; - this[kSourceBroker] = sourceBroker; - this[kSourceExchange] = sourceExchange; - this[kDestinationExchange] = destinationExchange; - this[kCloneMessage] = options?.cloneMessage; - const boundClose = this.close.bind(this); - const eventHandlers = this[kEventHandlers] = new Set([sourceExchange.on('delete', boundClose), destinationExchange.on('delete', boundClose)]); - let consumer; - const shovelHandler = this._onShovelMessage.bind(this); - if (queue) { - consumer = sourceBroker.subscribe(sourceExchangeName, routingKeyPattern, queue, shovelHandler, { - consumerTag, - priority - }); - } else { - consumer = sourceBroker.subscribeTmp(sourceExchangeName, routingKeyPattern, shovelHandler, { - consumerTag, - priority - }); - this.source.queue = consumer.queue.name; - } - eventHandlers.add(consumer.on('cancel', boundClose)); -} -Object.defineProperties(Shovel.prototype, { - name: { - get() { - return this[kName]; - } - }, - closed: { - get() { - return this[kClosed]; - } - }, - consumerTag: { - get() { - return this[kConsumerTag]; - } - } -}); -Shovel.prototype.emit = function emit(eventName, content) { - this.events.emit(`shovel.${eventName}`, content); -}; -Shovel.prototype.on = function on(eventName, handler, options) { - return this.events.on(`shovel.${eventName}`, handler, options); -}; -Shovel.prototype.off = function off(eventName, handler) { - return this.events.off(`shovel.${eventName}`, handler); -}; -Shovel.prototype.close = function closeShovel() { - if (this[kClosed]) return; - this[kClosed] = true; - for (const eh of this[kEventHandlers]) eh.cancel(); - this[kEventHandlers].clear(); - const events = this.events; - this.emit('close', this); - events.close(); - this[kSourceBroker].cancel(this[kConsumerTag]); -}; -Shovel.prototype._messageHandler = function messageHandler(message) { - const cloneMessage = this[kCloneMessage]; - if (!cloneMessage) return message; - const { - fields, - content, - properties - } = message; - const { - content: newContent, - properties: newProperties - } = cloneMessage({ - fields: { - ...fields - }, - content, - properties: { - ...properties - } - }); - return { - fields, - content: newContent, - properties: { - ...properties, - ...newProperties - } - }; -}; -Shovel.prototype._onShovelMessage = function onShovelMessage(routingKey, message) { - const destinationExchange = this[kDestinationExchange]; - if (!destinationExchange.bindingCount && !message.properties.mandatory) return message.ack(); - const { - content, - properties - } = this._messageHandler(message); - const props = { - ...properties, - ...this.destination.publishProperties, - 'source-exchange': this[kSourceExchange].name - }; - if (!this[kBrokerInternal]) props['shovel-name'] = this[kName]; - destinationExchange.publish(this.destination.exchangeKey || routingKey, content, props); - message.ack(); -}; -function Exchange2Exchange(shovel) { - this[kE2EShovel] = shovel; -} -Object.defineProperties(Exchange2Exchange.prototype, { - name: { - get() { - return this[kE2EShovel].name; - } - }, - source: { - get() { - return this[kE2EShovel].source.exchange; - } - }, - destination: { - get() { - return this[kE2EShovel].destination.exchange; - } - }, - pattern: { - get() { - return this[kE2EShovel].source.pattern; - } - }, - queue: { - get() { - return this[kE2EShovel].source.queue; - } - }, - consumerTag: { - get() { - return this[kE2EShovel].consumerTag; - } - } -}); -Exchange2Exchange.prototype.on = function e2eon(...args) { - return this[kE2EShovel].on(...args); -}; -Exchange2Exchange.prototype.close = function e2eclose() { - return this[kE2EShovel].close(); -}; \ No newline at end of file diff --git a/dist/index.js b/dist/index.js deleted file mode 100644 index 67b84a8..0000000 --- a/dist/index.js +++ /dev/null @@ -1,76 +0,0 @@ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -var _exportNames = { - Broker: true, - Message: true, - Queue: true, - Consumer: true, - Shovel: true, - Exchange: true, - getRoutingKeyPattern: true -}; -Object.defineProperty(exports, "Broker", { - enumerable: true, - get: function () { - return _Broker.Broker; - } -}); -Object.defineProperty(exports, "Consumer", { - enumerable: true, - get: function () { - return _Queue.Consumer; - } -}); -Object.defineProperty(exports, "Exchange", { - enumerable: true, - get: function () { - return _Exchange.Exchange; - } -}); -Object.defineProperty(exports, "Message", { - enumerable: true, - get: function () { - return _Message.Message; - } -}); -Object.defineProperty(exports, "Queue", { - enumerable: true, - get: function () { - return _Queue.Queue; - } -}); -Object.defineProperty(exports, "Shovel", { - enumerable: true, - get: function () { - return _Shovel.Shovel; - } -}); -exports.default = void 0; -Object.defineProperty(exports, "getRoutingKeyPattern", { - enumerable: true, - get: function () { - return _shared.getRoutingKeyPattern; - } -}); -var _Broker = require("./Broker.js"); -var _Message = require("./Message.js"); -var _Queue = require("./Queue.js"); -var _Shovel = require("./Shovel.js"); -var _Exchange = require("./Exchange.js"); -var _Errors = require("./Errors.js"); -Object.keys(_Errors).forEach(function (key) { - if (key === "default" || key === "__esModule") return; - if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; - if (key in exports && exports[key] === _Errors[key]) return; - Object.defineProperty(exports, key, { - enumerable: true, - get: function () { - return _Errors[key]; - } - }); -}); -var _shared = require("./shared.js"); -var _default = exports.default = _Broker.Broker; \ No newline at end of file diff --git a/dist/package.json b/dist/package.json deleted file mode 100644 index 5bbefff..0000000 --- a/dist/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "type": "commonjs" -} diff --git a/dist/shared.js b/dist/shared.js deleted file mode 100644 index 8462e1b..0000000 --- a/dist/shared.js +++ /dev/null @@ -1,43 +0,0 @@ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.generateId = generateId; -exports.getRoutingKeyPattern = getRoutingKeyPattern; -exports.sortByPriority = sortByPriority; -const allDots = /\./g; -const allAstx = /\*/g; -const allHashs = /#/g; -function generateId() { - return Math.random().toString(16).substring(2, 12); -} -function DirectRoutingKeyPattern(pattern) { - this._match = pattern; -} -DirectRoutingKeyPattern.prototype.test = function test(routingKey) { - return this._match === routingKey; -}; -function EndMatchRoutingKeyPattern(pattern) { - this._match = pattern.replace('#', ''); -} -EndMatchRoutingKeyPattern.prototype.test = function test(routingKey) { - return !routingKey.indexOf(this._match); -}; -function getRoutingKeyPattern(pattern) { - const len = pattern.length; - const hashIdx = pattern.indexOf('#'); - const astxIdx = pattern.indexOf('*'); - if (hashIdx === -1) { - if (astxIdx === -1) { - return new DirectRoutingKeyPattern(pattern); - } - } else if (hashIdx === len - 1 && astxIdx === -1) { - return new EndMatchRoutingKeyPattern(pattern); - } - const rpattern = pattern.replace(allDots, '\\.').replace(allAstx, '[^.]+?').replace(allHashs, '.*?'); - return new RegExp(`^${rpattern}$`); -} -function sortByPriority(a, b) { - return (b.options.priority || 0) - (a.options.priority || 0); -} \ No newline at end of file diff --git a/eslint.config.js b/eslint.config.js index ad0778d..2f05c69 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -78,7 +78,7 @@ export default [ languageOptions: { parserOptions: { sourceType: 'module', - ecmaVersion: 2020, + ecmaVersion: 2025, }, globals: { ...globals['shared-node-browser'], @@ -93,6 +93,9 @@ export default [ ...globals.node, }, }, + rules: { + 'no-process-exit': 0, + }, }, { files: ['test/**/*.js'], diff --git a/package.json b/package.json index a63775f..48c9b11 100644 --- a/package.json +++ b/package.json @@ -1,21 +1,23 @@ { "name": "smqp", - "version": "11.0.1", + "version": "12.0.0", "type": "module", "description": "Synchronous message queueing package", "author": { "name": "PÃ¥l Edman", "url": "https://github.com/paed01" }, - "main": "./dist/index.js", "module": "./src/index.js", + "main": "./dist/index.cjs", + "browser": "./dist/smqp.cjs", "jsnext:main": "./src/index.js", "sideEffects": false, "types": "./types/index.d.ts", "exports": { "types": "./types/index.d.ts", "import": "./src/index.js", - "require": "./dist/index.js" + "require": "./dist/index.cjs", + "browser": "./dist/smqp.cjs" }, "license": "MIT", "repository": { @@ -26,19 +28,24 @@ "url": "https://github.com/paed01/smqp/issues" }, "files": [ - "types", + "dist", "src", - "dist" + "types/index.d.ts", + "types/index.d.ts.map" ], + "engines": { + "node": ">=18" + }, "scripts": { "test": "mocha", - "test:lcov": "c8 -r lcov mocha --exit && npm run lint", + "test:lcov": "c8 -r lcov mocha --exit", "posttest": "npm run dist && npm run lint && npm run toc && npm run test:md", "test:md": "texample ./README.md,API.md", "cov:html": "c8 -r html -r text mocha", - "dist": "babel src/**.js -d dist", - "prepack": "npm run dist", - "toc": "node ./scripts/generate-api-toc.js", + "dist": "rollup -c && dts-buddy && npm run toc", + "types": "dts-buddy", + "prepublishOnly": "npm run dist", + "toc": "node ./scripts/toc.js API.md", "lint": "eslint . --cache && prettier . -c --cache" }, "keywords": [ @@ -55,19 +62,21 @@ "nack", "reject", "topic", - "direct" + "direct", + "independent" ], "devDependencies": { - "@babel/cli": "^7.24.1", - "@babel/core": "^7.24.4", - "@babel/preset-env": "^7.24.4", + "@eslint/js": "^9.39.4", + "@rollup/plugin-commonjs": "^29.0.2", "c8": "^10.1.2", "chai": "^6.2.0", - "chronokinesis": "^7.0.0", - "eslint": "^9.10.0", - "markdown-toc": "^1.2.0", + "chronokinesis": "^8.0.0", + "dts-buddy": "^0.7.0", + "eslint": "^10.2.1", + "globals": "^17.5.0", "mocha": "^11.0.1", "prettier": "^3.2.5", - "texample": "^0.1.0" + "rollup": "^4.60.2", + "texample": "^1.0.1" } } diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 0000000..e56e3be --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,20 @@ +import commonjs from '@rollup/plugin-commonjs'; + +import pkg from './package.json' with { type: 'json' }; + +export default { + input: pkg.exports.import, + plugins: [ + commonjs({ + sourceMap: false, + }), + ], + output: [ + { + file: pkg.exports.require, + format: 'cjs', + exports: 'named', + footer: 'module.exports = Object.assign(exports.default, exports);', + }, + ], +}; diff --git a/scripts/generate-api-toc.js b/scripts/generate-api-toc.js deleted file mode 100644 index 108cb3b..0000000 --- a/scripts/generate-api-toc.js +++ /dev/null @@ -1,36 +0,0 @@ -import fs from 'node:fs'; -import { createRequire } from 'node:module'; -import { fileURLToPath } from 'node:url'; -import Toc from 'markdown-toc'; - -const nodeRequire = createRequire(fileURLToPath(import.meta.url)); -const { version } = nodeRequire('../package.json'); - -const filenames = getFileNames(); - -function getFileNames() { - const arg = process.argv[2] || './API.md'; - return arg.split(','); -} - -function generate(filename) { - const api = fs.readFileSync(filename, 'utf8'); - const tocOptions = { - bullets: '-', - slugify(text) { - return text - .toLowerCase() - .replace(/\s/g, '-') - .replace(/[^\w-]/g, ''); - }, - }; - - const output = Toc.insert(api, tocOptions).replace( - /(.|\n)*/, - '\n\n# ' + version + ' API Reference\n\n' - ); - - fs.writeFileSync(filename, output); -} - -filenames.forEach(generate); diff --git a/scripts/toc.js b/scripts/toc.js new file mode 100644 index 0000000..074d73b --- /dev/null +++ b/scripts/toc.js @@ -0,0 +1,102 @@ +#!/usr/bin/env node +/* eslint-disable no-console */ +import { readFile, writeFile } from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; +import { dirname, resolve } from 'node:path'; + +const TOC_START = ''; +const TOC_END = ''; + +const here = dirname(fileURLToPath(import.meta.url)); +const repoRoot = resolve(here, '..'); + +const args = process.argv.slice(2); +const files = args.length ? args : ['README.md']; + +let exitCode = 0; +for (const arg of files) { + try { + await processFile(resolve(repoRoot, arg)); + } catch (err) { + console.error(`${arg}: ${err.message}`); + exitCode = 1; + } +} +process.exit(exitCode); + +/** @param {string} mdPath */ +async function processFile(mdPath) { + const source = await readFile(mdPath, 'utf8'); + + const inFence = (() => { + const fences = []; + let open = false; + for (const [i, line] of source.split('\n').entries()) { + if (/^```/.test(line)) { + open = !open; + } + fences[i] = open; + } + return (i) => fences[i]; + })(); + + const lines = source.split('\n'); + /** @type {Array<{ level: number, text: string, slug: string }>} */ + const headlines = []; + const slugCounts = new Map(); + let firstHeadlineIdx = -1; + + for (const [i, line] of lines.entries()) { + if (inFence(i)) continue; + const match = /^(#{1,6})\s+(.+?)\s*$/.exec(line); + if (!match) continue; + const level = match[1].length; + if (level === 1) continue; + const text = match[2].replace(/\*/g, '\\*'); + const baseSlug = slugify(match[2]); + const n = slugCounts.get(baseSlug) ?? 0; + slugCounts.set(baseSlug, n + 1); + const slug = n === 0 ? baseSlug : `${baseSlug}-${n}`; + headlines.push({ level, text, slug }); + if (firstHeadlineIdx === -1) firstHeadlineIdx = i; + } + + if (firstHeadlineIdx === -1 || headlines.length === 0) { + console.error(`${mdPath}: no non-title headlines found; nothing to do.`); + return; + } + + const minLevel = Math.min(...headlines.map((h) => h.level)); + const tocLines = headlines.map(({ level, text, slug }) => `${' '.repeat(level - minLevel)}- [${text}](#${slug})`); + const tocBlock = [TOC_START, '', ...tocLines, '', TOC_END].join('\n'); + + const startIdx = lines.findIndex((l) => l.trim() === TOC_START); + const endIdx = lines.findIndex((l) => l.trim() === TOC_END); + + let updated; + if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) { + updated = [...lines.slice(0, startIdx), tocBlock, ...lines.slice(endIdx + 1)].join('\n'); + } else { + updated = [...lines.slice(0, firstHeadlineIdx), tocBlock, '', ...lines.slice(firstHeadlineIdx)].join('\n'); + } + + if (updated === source) { + console.log(`${mdPath}: TOC already up to date.`); + } else { + await writeFile(mdPath, updated); + console.log(`${mdPath}: inserted TOC with ${headlines.length} entries.`); + } +} + +/** + * @param {string} text + * @returns {string} + */ +function slugify(text) { + return text + .toLowerCase() + .replace(/[`*_~]/g, '') + .replace(/[^\w\s-]/g, '') + .trim() + .replace(/\s+/g, '-'); +} diff --git a/src/Binding.js b/src/Binding.js new file mode 100644 index 0000000..0a3d59e --- /dev/null +++ b/src/Binding.js @@ -0,0 +1,49 @@ +import { getRoutingKeyPattern } from './shared.js'; + +/** + * + * @param {import('./Exchange.js').ExchangeBase} exchange + * @param {import('./Queue.js').Queue} queue + * @param {string} pattern message routing key pattern + * @param {import('#types').BindingOptions} [bindOptions] + */ +export function Binding(exchange, queue, pattern, bindOptions) { + this.id = `${queue.name}/${pattern}`; + this.options = { priority: 0, ...bindOptions }; + this.pattern = pattern; + this.exchange = exchange; + this.queue = queue; + /** @type {{ test(routingKey: string): boolean }} */ + this._compiledPattern = getRoutingKeyPattern(pattern); + + queue.on('delete', () => { + this.close(); + }); +} + +/** + * Test routing key against pattern + * @param {string} routingKey message routing key + */ +Binding.prototype.testPattern = function testPattern(routingKey) { + return this._compiledPattern.test(routingKey); +}; + +/** + * Close binding + */ +Binding.prototype.close = function closeBinding() { + this.exchange.unbindQueue(this.queue, this.pattern); +}; + +/** + * Get binding state + */ +Binding.prototype.getState = function getBindingState() { + return { + id: this.id, + options: { ...this.options }, + queueName: this.queue.name, + pattern: this.pattern, + }; +}; diff --git a/src/Broker.js b/src/Broker.js index d33521f..e7a6701 100644 --- a/src/Broker.js +++ b/src/Broker.js @@ -15,11 +15,16 @@ import { const kEntities = Symbol.for('entities'); const kEventHandler = Symbol.for('eventHandler'); +/** + * Smqp message broker + * @param {any} [owner] optional broker owner, forwarded to message consumer + */ export function Broker(owner) { if (!(this instanceof Broker)) { return new Broker(owner); } this.owner = owner; + /** @type {import('#types').ExchangeEventEmitter} */ const events = (this.events = new EventExchange('broker__events')); const entities = (this[kEntities] = new Map([ ['exchanges', new Map()], @@ -48,6 +53,14 @@ Object.defineProperties(Broker.prototype, { }, }); +/** + * Subscribe to exchange via queue + * @param {string} exchangeName exhange name + * @param {string} pattern routing key pattern + * @param {string} queueName queue name + * @param {import('#types').onMessage} onMessage message handlers + * @param {import('#types').SubscribeOptions} [options] optional subscribe options + */ Broker.prototype.subscribe = function subscribe(exchangeName, pattern, queueName, onMessage, options) { if (!exchangeName || !pattern || typeof onMessage !== 'function') throw new TypeError('exchange name, pattern, and message callback are required'); @@ -62,10 +75,29 @@ Broker.prototype.subscribe = function subscribe(exchangeName, pattern, queueName return queue.assertConsumer(onMessage, queueOptions, this.owner); }; +/** + * Subscribe to exchange via temporary, non-durable queue + * @param {string} exchangeName exchange name + * @param {string} pattern routing key pattern + * @param {import('#types').onMessage} onMessage message handler + * @param {import('#types').SubscribeOptions} [options] optional subscribe options + */ Broker.prototype.subscribeTmp = function subscribeTmp(exchangeName, pattern, onMessage, options) { return this.subscribe(exchangeName, pattern, null, onMessage, { ...options, durable: false }); }; +/** + * Subscribe once to first matching message, then auto-cancel. + * + * Only `consumerTag` and `priority` from `options` are honored. `noAck`, `autoDelete`, + * and `durable` are forced internally; queue-lifecycle and dead-letter options are ignored + * because the temporary queue is deleted after the first delivery. + * + * @param {string} exchangeName exchange name + * @param {string} pattern routing key pattern + * @param {import('#types').onMessage} onMessage message handler + * @param {import('#types').SubscribeOptions} [options] optional subscribe options + */ Broker.prototype.subscribeOnce = function subscribeOnce(exchangeName, pattern, onMessage, options) { if (typeof onMessage !== 'function') throw new TypeError('message callback is required'); if (options?.consumerTag) this.validateConsumerTag(options.consumerTag); @@ -84,12 +116,23 @@ Broker.prototype.subscribeOnce = function subscribeOnce(exchangeName, pattern, o } }; +/** + * Cancel consumer matching queue + handler + * @param {string} queueName queue name + * @param {import('#types').onMessage} onMessage handler previously passed to subscribe + */ Broker.prototype.unsubscribe = function unsubscribe(queueName, onMessage) { const queue = this.getQueue(queueName); if (!queue) return; queue.dismiss(onMessage); }; +/** + * Assert exchange exists, create if absent + * @param {string} exchangeName exchange name + * @param {import('#types').exchangeType} [type] exchange type, defaults to topic + * @param {import('#types').ExchangeOptions} [options] optional exchange options + */ Broker.prototype.assertExchange = function assertExchange(exchangeName, type, options) { let exchange = this.getExchange(exchangeName); if (exchange) { @@ -104,12 +147,25 @@ Broker.prototype.assertExchange = function assertExchange(exchangeName, type, op return exchange; }; +/** + * Bind queue to exchange with routing key pattern + * @param {string} queueName queue name + * @param {string} exchangeName exchange name + * @param {string} pattern routing key pattern + * @param {import('#types').BindingOptions} [bindOptions] optional binding options + */ Broker.prototype.bindQueue = function bindQueue(queueName, exchangeName, pattern, bindOptions) { const exchange = this.getExchange(exchangeName); const queue = this.getQueue(queueName); return exchange.bindQueue(queue, pattern, bindOptions); }; +/** + * Unbind queue from exchange + * @param {string} queueName queue name + * @param {string} exchangeName exchange name + * @param {string} pattern routing key pattern + */ Broker.prototype.unbindQueue = function unbindQueue(queueName, exchangeName, pattern) { const exchange = this.getExchange(exchangeName); if (!exchange) return; @@ -118,12 +174,23 @@ Broker.prototype.unbindQueue = function unbindQueue(queueName, exchangeName, pat exchange.unbindQueue(queue, pattern); }; +/** + * Add consumer to queue + * @param {string} queueName queue name + * @param {import('#types').onMessage} onMessage message handler + * @param {import('#types').ConsumeOptions} [options] optional consume options + */ Broker.prototype.consume = function consume(queueName, onMessage, options) { const queue = this.getQueue(queueName); if (!queue) throw new SmqpError(`Queue with name <${queueName}> was not found`, ERR_QUEUE_NOT_FOUND); return queue.consume(onMessage, options, this.owner); }; +/** + * Cancel consumer by tag + * @param {string} consumerTag consumer tag + * @param {boolean} [requeue] requeue messages held by the consumer, defaults to true + */ Broker.prototype.cancel = function cancel(consumerTag, requeue = true) { const consumer = this.getConsumer(consumerTag); if (!consumer) return false; @@ -131,29 +198,41 @@ Broker.prototype.cancel = function cancel(consumerTag, requeue = true) { return true; }; +/** List all consumers as serializable projections */ Broker.prototype.getConsumers = function getConsumers() { + /** @type {ReturnType[]} */ const result = []; for (const consumer of this[kEntities].get('consumers').values()) { - result.push({ - queue: consumer.queue.name, - consumerTag: consumer.options.consumerTag, - ready: consumer.ready, - options: { ...consumer.options }, - }); + result.push(consumer.toJSON()); } return result; }; +/** + * Get consumer by tag + * @param {string} consumerTag consumer tag + * @returns {import('./Queue.js').Consumer | undefined} + */ Broker.prototype.getConsumer = function getConsumer(consumerTag) { if (typeof consumerTag !== 'string') throw new TypeError('consumer tag must be a string'); return this[kEntities].get('consumers').get(consumerTag); }; +/** + * Get exchange by name + * @param {string} exchangeName exchange name + * @returns {import('./Exchange.js').ExchangeBase | undefined} + */ Broker.prototype.getExchange = function getExchange(exchangeName) { if (typeof exchangeName !== 'string') throw new TypeError('exchange name must be a string'); return this[kEntities].get('exchanges').get(exchangeName); }; +/** + * Delete exchange + * @param {string} exchangeName exchange name + * @param {{ ifUnused?: boolean }} [options] only delete if no bindings remain + */ Broker.prototype.deleteExchange = function deleteExchange(exchangeName, options) { const exchange = this.getExchange(exchangeName); if (!exchange || (options?.ifUnused && exchange.bindingCount)) return false; @@ -163,12 +242,18 @@ Broker.prototype.deleteExchange = function deleteExchange(exchangeName, options) return true; }; +/** + * Stop broker with corresponding exchanges and queues, entities remain but does not accepts messages + */ Broker.prototype.stop = function stop() { const entities = this[kEntities]; for (const exchange of entities.get('exchanges').values()) exchange.stop(); for (const queue of entities.get('queues').values()) queue.stop(); }; +/** + * Close and clean-up all entities + */ Broker.prototype.close = function close() { const entities = this[kEntities]; for (const shovel of entities.get('shovels').values()) shovel.close(); @@ -176,6 +261,9 @@ Broker.prototype.close = function close() { for (const queue of entities.get('queues').values()) queue.close(); }; +/** + * Danger! Resets all entities, stop, close and delete + */ Broker.prototype.reset = function reset() { this.stop(); this.close(); @@ -186,6 +274,10 @@ Broker.prototype.reset = function reset() { entities.get('shovels').clear(); }; +/** + * Get broker state for persistence + * @param {boolean} [onlyWithContent] omit exchanges and queues without content + */ Broker.prototype.getState = function getState(onlyWithContent) { const exchanges = this._getExchangeState(onlyWithContent); const queues = this._getQueuesState(onlyWithContent); @@ -198,6 +290,10 @@ Broker.prototype.getState = function getState(onlyWithContent) { }; }; +/** + * Recover broker from previously captured state + * @param {import('#types').BrokerState} [state] broker state, omit to recover stopped entities in place + */ Broker.prototype.recover = function recover(state) { const boundGetQueue = this.getQueue.bind(this); if (state) { @@ -219,6 +315,13 @@ Broker.prototype.recover = function recover(state) { return this; }; +/** + * Bind one exchange to another via internal shovel + * @param {string} source source exchange name + * @param {string} destination destination exchange name + * @param {string} [pattern] routing key pattern, defaults to # + * @param {import('#types').ShovelOptions} [args] optional shovel options + */ Broker.prototype.bindExchange = function bindExchange(source, destination, pattern = '#', args) { const name = `e2e-${source}2${destination}-${pattern}`; const shovel = this.createShovel( @@ -240,32 +343,61 @@ Broker.prototype.bindExchange = function bindExchange(source, destination, patte return new Exchange2Exchange(shovel); }; +/** + * Unbind exchange-to-exchange shovel + * @param {string} source source exchange name + * @param {string} destination destination exchange name + * @param {string} [pattern] routing key pattern, defaults to # + */ Broker.prototype.unbindExchange = function unbindExchange(source, destination, pattern = '#') { const name = `e2e-${source}2${destination}-${pattern}`; return this.closeShovel(name); }; +/** + * Publish a message to an exchange + * @param {string} exchangeName exchange name + * @param {string} routingKey routing key + * @param {any} [content] message content + * @param {import('#types').MessageProperties} [properties] optional message properties + */ Broker.prototype.publish = function publish(exchangeName, routingKey, content, properties) { const exchange = this.getExchange(exchangeName); if (!exchange) return; return exchange.publish(routingKey, content, properties); }; +/** + * Purge all non-pending messages from queue + * @param {string} queueName queue name + */ Broker.prototype.purgeQueue = function purgeQueue(queueName) { const queue = this.getQueue(queueName); if (!queue) return; return queue.purge(); }; +/** + * Send content directly to a queue, bypassing exchanges + * @param {string} queueName queue name + * @param {any} content message content + * @param {import('#types').MessageProperties} [options] optional message properties + */ Broker.prototype.sendToQueue = function sendToQueue(queueName, content, options) { const queue = this.getQueue(queueName); if (!queue) throw new SmqpError(`Queue with name <${queueName}> was not found`, ERR_QUEUE_NOT_FOUND); return queue.queueMessage({}, content, options); }; +/** + * @private + * @param {boolean} [onlyWithContent] skip queues without messages + */ Broker.prototype._getQueuesState = function getQueuesState(onlyWithContent) { let result; - for (const queue of this[kEntities].get('queues').values()) { + /** @type {Set} */ + const queues = this[kEntities].get('queues').values(); + for (const queue of queues) { if (!queue.options.durable) continue; if (onlyWithContent && !queue.messageCount) continue; if (!result) result = []; @@ -274,9 +406,15 @@ Broker.prototype._getQueuesState = function getQueuesState(onlyWithContent) { return result; }; +/** + * @private + * @param {boolean} [onlyWithContent] skip exchanges without undelivered messages + */ Broker.prototype._getExchangeState = function getExchangeState(onlyWithContent) { let result; - for (const exchange of this[kEntities].get('exchanges').values()) { + /** @type {Set} */ + const exhanges = this[kEntities].get('exchanges').values(); + for (const exchange of exhanges) { if (!exchange.options.durable) continue; if (onlyWithContent && !exchange.undeliveredCount) continue; if (!result) result = []; @@ -285,6 +423,11 @@ Broker.prototype._getExchangeState = function getExchangeState(onlyWithContent) return result; }; +/** + * Create queue + * @param {string} [queueName] queue name, defaults to a generated name + * @param {import('#types').QueueOptions} [options] optional queue options + */ Broker.prototype.createQueue = function createQueue(queueName, options) { if (queueName && typeof queueName !== 'string') throw new TypeError('queue name must be a string'); else if (!queueName) queueName = `smq.qname-${generateId()}`; @@ -298,11 +441,21 @@ Broker.prototype.createQueue = function createQueue(queueName, options) { return queue; }; +/** + * Get queue by name + * @param {string} queueName queue name + * @returns {import('./Queue.js').Queue | undefined} + */ Broker.prototype.getQueue = function getQueue(queueName) { if (!queueName || typeof queueName !== 'string') throw new TypeError('queue name must be a string'); return this[kEntities].get('queues').get(queueName); }; +/** + * Assert queue exists, create if absent + * @param {string} [queueName] queue name, defaults to a generated name + * @param {import('#types').QueueOptions} [options] optional queue options + */ Broker.prototype.assertQueue = function assertQueue(queueName, options) { if (queueName && typeof queueName !== 'string') throw new TypeError('queue name must be a string'); else if (!queueName) return this.createQueue(null, options); @@ -315,12 +468,22 @@ Broker.prototype.assertQueue = function assertQueue(queueName, options) { return queue; }; +/** + * Delete queue + * @param {string} queueName queue name + * @param {import('#types').DeleteQueueOptions} [options] optional delete guards + */ Broker.prototype.deleteQueue = function deleteQueue(queueName, options) { const queue = this.getQueue(queueName); if (!queue) return; return queue.delete(options); }; +/** + * Get one message from queue + * @param {string} queueName queue name + * @param {import('#types').ConsumeOptions} [options] optional consume options + */ Broker.prototype.get = function getMessageFromQueue(queueName, options) { const queue = this.getQueue(queueName); if (!queue) return; @@ -328,30 +491,62 @@ Broker.prototype.get = function getMessageFromQueue(queueName, options) { return queue.get({ noAck: options?.noAck }); }; +/** + * Acknowledge message + * @param {import('./Message.js').Message} message message to ack + * @param {boolean} [allUpTo] ack all messages up to and including this one + */ Broker.prototype.ack = function ack(message, allUpTo) { message.ack(allUpTo); }; +/** Acknowledge all outstanding messages across all queues */ Broker.prototype.ackAll = function ackAll() { for (const queue of this[kEntities].get('queues').values()) queue.ackAll(); }; +/** + * Reject message + * @param {import('./Message.js').Message} message message to nack + * @param {boolean} [allUpTo] nack all messages up to and including this one + * @param {boolean} [requeue] requeue nacked messages, defaults to true + */ Broker.prototype.nack = function nack(message, allUpTo, requeue) { message.nack(allUpTo, requeue); }; +/** + * Reject all outstanding messages across all queues + * @param {boolean} [requeue] requeue nacked messages, defaults to true + */ Broker.prototype.nackAll = function nackAll(requeue) { for (const queue of this[kEntities].get('queues').values()) queue.nackAll(requeue); }; +/** + * Reject message + * @param {import('./Message.js').Message} message message to reject + * @param {boolean} [requeue] requeue rejected message, defaults to true + */ Broker.prototype.reject = function reject(message, requeue) { message.reject(requeue); }; +/** + * Validate that a consumer tag is unused; throws if occupied + * @param {string} consumerTag consumer tag to validate + */ Broker.prototype.validateConsumerTag = function validateConsumerTag(consumerTag) { return this[kEventHandler].validateConsumerTag('' + consumerTag); }; +/** + * Create shovel between source and destination exchanges + * @param {string} name unique shovel name + * @param {import('#types').ShovelSource} source source spec + * @param {import('#types').ShovelDestination} destination destination spec + * @param {import('#types').ShovelOptions} [options] optional shovel options + */ Broker.prototype.createShovel = function createShovel(name, source, destination, options) { const shovels = this[kEntities].get('shovels'); if (shovels.has(name)) throw new SmqpError(`Shovel name must be unique, ${name} is occupied`, ERR_SHOVEL_NAME_CONFLICT); @@ -361,6 +556,10 @@ Broker.prototype.createShovel = function createShovel(name, source, destination, return shovel; }; +/** + * Close shovel by name + * @param {string} name shovel name + */ Broker.prototype.closeShovel = function closeShovel(name) { const shovel = this.getShovel(name); if (shovel) { @@ -370,14 +569,29 @@ Broker.prototype.closeShovel = function closeShovel(name) { return false; }; +/** + * Get shovel by name + * @param {string} name shovel name + * @returns {import('./Shovel.js').Shovel | undefined} + */ Broker.prototype.getShovel = function getShovel(name) { return this[kEntities].get('shovels').get(name); }; +/** + * List all shovels + * @returns {import('./Shovel.js').Shovel[]} + */ Broker.prototype.getShovels = function getShovels() { return [...this[kEntities].get('shovels').values()]; }; +/** + * Subscribe to broker event + * @param {string} eventName event name pattern + * @param {(event: { name: string } & Record) => void} callback event callback + * @param {import('#types').ConsumeOptions} [options] optional consume options + */ Broker.prototype.on = function on(eventName, callback, options) { return this.events.on(eventName, getEventCallback(), { ...options, origin: callback }); @@ -391,6 +605,11 @@ Broker.prototype.on = function on(eventName, callback, options) { } }; +/** + * Unsubscribe from broker event + * @param {string} eventName event name previously passed to on + * @param {Function | { consumerTag?: string }} callbackOrObject the callback used in on, or an object with the consumer tag + */ Broker.prototype.off = function off(eventName, callbackOrObject) { const { consumerTag } = callbackOrObject; for (const binding of this.events.bindings) { diff --git a/src/Exchange.js b/src/Exchange.js index 0cbc129..31f7589 100644 --- a/src/Exchange.js +++ b/src/Exchange.js @@ -1,6 +1,7 @@ import { Message } from './Message.js'; import { Queue } from './Queue.js'; -import { sortByPriority, getRoutingKeyPattern, generateId } from './shared.js'; +import { Binding } from './Binding.js'; +import { sortByPriority, generateId } from './shared.js'; const kName = Symbol.for('name'); const kType = Symbol.for('type'); @@ -8,6 +9,14 @@ const kStopped = Symbol.for('stopped'); const kBindings = Symbol.for('bindings'); const kDeliveryQueue = Symbol.for('deliveryQueue'); +/** @typedef {import('./Binding.js').Binding} Binding */ + +/** + * Exchange + * @param {string} name required exchange name + * @param {import('#types').exchangeType} [type] optional type, defaults to topic + * @param {import('#types').ExchangeOptions} [options] optional exchange options + */ export function Exchange(name, type = 'topic', options) { if (!name || typeof name !== 'string') throw new TypeError('Exchange name is required and must be a string'); @@ -16,19 +25,33 @@ export function Exchange(name, type = 'topic', options) { return new ExchangeBase(name, type, options, eventExchange); } +/** + * Event exchange + * @param {string} [name] optional event exchange name, defaults to smq.ename- + */ export function EventExchange(name) { if (!name) name = `smq.ename-${generateId()}`; return new ExchangeBase(name, 'topic', { durable: false, autoDelete: true }); } -function ExchangeBase(name, type, options, eventExchange) { +/** + * Exchange + * @param {string} name name + * @param {import('#types').exchangeType} type + * @param {import('#types').ExchangeOptions} [options] + * @param {ExchangeBase} [eventExchange] + */ +export function ExchangeBase(name, type, options, eventExchange) { this[kName] = name; this[kType] = type; + /** @type {Binding[]} */ this[kBindings] = []; this[kStopped] = false; + /** @type {import('#types').ExchangeOptions} */ this.options = { durable: true, autoDelete: true, ...options }; this.events = eventExchange; + /** @type {Queue} */ const deliveryQueue = (this[kDeliveryQueue] = new Queue('delivery-q', { autoDelete: false })); const onMessage = (type === 'topic' ? this._onTopicMessage : this._onDirectMessage).bind(this); deliveryQueue.consume(onMessage, { exclusive: true, consumerTag: '_exchange-tag' }); @@ -67,6 +90,12 @@ Object.defineProperties(ExchangeBase.prototype, { }, }); +/** + * Publish a message through the exchange + * @param {string} routingKey routing key + * @param {any} [content] message content + * @param {import('#types').MessageProperties} [properties] optional message properties + */ ExchangeBase.prototype.publish = function publish(routingKey, content, properties) { if (this[kStopped]) return; if (!this[kBindings].length) return this._emitReturn(routingKey, content, properties); @@ -80,6 +109,7 @@ ExchangeBase.prototype.publish = function publish(routingKey, content, propertie ); }; +/** @private */ ExchangeBase.prototype._onTopicMessage = function topic(routingKey, message) { const publishedMsg = message.content; @@ -99,6 +129,11 @@ ExchangeBase.prototype._onTopicMessage = function topic(routingKey, message) { return delivered; }; +/** + * @private + * @param {string} routingKey + * @param {Message} message + */ ExchangeBase.prototype._onDirectMessage = function direct(routingKey, message) { const publishedMsg = message.content; const bindings = this[kBindings]; @@ -129,6 +164,7 @@ ExchangeBase.prototype._onDirectMessage = function direct(routingKey, message) { return 1; }; +/** @private */ ExchangeBase.prototype._emitReturn = function emitReturn(routingKey, content, properties) { if (!this.events || !properties) return; @@ -140,6 +176,12 @@ ExchangeBase.prototype._emitReturn = function emitReturn(routingKey, content, pr } }; +/** + * Bind a queue to this exchange with a routing key pattern + * @param {import('./Queue.js').Queue} queue queue to bind + * @param {string} pattern routing key pattern + * @param {import('#types').BindingOptions} [bindOptions] optional binding options + */ ExchangeBase.prototype.bindQueue = function bindQueue(queue, pattern, bindOptions) { const bindings = this[kBindings]; @@ -155,6 +197,11 @@ ExchangeBase.prototype.bindQueue = function bindQueue(queue, pattern, bindOption return binding; }; +/** + * Unbind a queue from this exchange + * @param {import('./Queue.js').Queue} queue queue previously bound + * @param {string} pattern routing key pattern + */ ExchangeBase.prototype.unbindQueue = function unbindQueue(queue, pattern) { for (const binding of this[kBindings]) { if (binding.queue === queue && binding.pattern === pattern) { @@ -163,6 +210,10 @@ ExchangeBase.prototype.unbindQueue = function unbindQueue(queue, pattern) { } }; +/** + * Unbind every binding pointing at the named queue + * @param {string} queueName queue name + */ ExchangeBase.prototype.unbindQueueByName = function unbindQueueByName(queueName) { for (const binding of this[kBindings]) { if (binding.queue.name === queueName) this.closeBinding(binding); @@ -179,12 +230,14 @@ ExchangeBase.prototype.close = function close() { }; ExchangeBase.prototype.getState = function getState() { + /** @type {ReturnType[]} */ const bindingsState = []; for (const binding of this[kBindings]) { if (!binding.queue.options.durable) continue; bindingsState.push(binding.getState()); } + /** @type {Queue} */ const deliveryQueue = this[kDeliveryQueue]; return { name: this.name, @@ -199,6 +252,11 @@ ExchangeBase.prototype.stop = function stop() { this[kStopped] = true; }; +/** + * Recover exchange from previously captured state + * @param {import('#types').ExchangeState} [state] exchange state, omit to recover stopped exchange in place + * @param {(name: string) => import('./Queue.js').Queue} [getQueue] callback to resolve a queue by name (used during binding restore) + */ ExchangeBase.prototype.recover = function recover(state, getQueue) { this[kStopped] = false; if (!state) return this; @@ -220,17 +278,33 @@ ExchangeBase.prototype.recover = function recover(state, getQueue) { return this; }; +/** + * Find a binding by queue name and pattern + * @param {string} queueName queue name + * @param {string} pattern routing key pattern + */ ExchangeBase.prototype.getBinding = function getBinding(queueName, pattern) { for (const binding of this[kBindings]) { if (binding.queue.name === queueName && binding.pattern === pattern) return binding; } }; +/** + * Emit an exchange event (or, if no event sub-exchange, publish on the exchange itself) + * @param {string} eventName event name (without `exchange.` prefix) + * @param {any} [content] event payload + */ ExchangeBase.prototype.emit = function emit(eventName, content) { if (this.events) return this.events.publish(`exchange.${eventName}`, content); return this.publish(eventName, content); }; +/** + * Subscribe to an exchange event + * @param {string} pattern event name pattern (without `exchange.` prefix) + * @param {import('#types').onMessage} handler event handler + * @param {import('#types').ConsumeOptions} [consumeOptions] optional consume options + */ ExchangeBase.prototype.on = function on(pattern, handler, consumeOptions) { if (this.events) return this.events.on(`exchange.${pattern}`, handler, consumeOptions); @@ -245,6 +319,11 @@ ExchangeBase.prototype.on = function on(pattern, handler, consumeOptions) { return eventQueue.consume(handler, { ...consumeOptions, noAck: true }, this); }; +/** + * Unsubscribe from an exchange event + * @param {string} pattern event name pattern previously passed to on + * @param {import('#types').onMessage | { consumerTag?: string }} handler the handler used in on, or an object with the consumer tag + */ ExchangeBase.prototype.off = function off(pattern, handler) { if (this.events) return this.events.off(`exchange.${pattern}`, handler); @@ -258,6 +337,10 @@ ExchangeBase.prototype.off = function off(pattern, handler) { } }; +/** + * Remove a single binding from this exchange + * @param {import('./Binding.js').Binding} binding binding to close + */ ExchangeBase.prototype.closeBinding = function closeBinding(binding) { const bindings = this[kBindings]; const idx = bindings.indexOf(binding); @@ -268,33 +351,3 @@ ExchangeBase.prototype.closeBinding = function closeBinding(binding) { if (!bindings.length && this.options.autoDelete) this.emit('delete', this); }; - -function Binding(exchange, queue, pattern, bindOptions) { - this.id = `${queue.name}/${pattern}`; - this.options = { priority: 0, ...bindOptions }; - this.pattern = pattern; - this.exchange = exchange; - this.queue = queue; - this._compiledPattern = getRoutingKeyPattern(pattern); - - queue.on('delete', () => { - this.close(); - }); -} - -Binding.prototype.testPattern = function testPattern(routingKey) { - return this._compiledPattern.test(routingKey); -}; - -Binding.prototype.close = function closeBinding() { - this.exchange.unbindQueue(this.queue, this.pattern); -}; - -Binding.prototype.getState = function getBindingState() { - return { - id: this.id, - options: { ...this.options }, - queueName: this.queue.name, - pattern: this.pattern, - }; -}; diff --git a/src/Message.js b/src/Message.js index bf1bb4f..bc17436 100644 --- a/src/Message.js +++ b/src/Message.js @@ -1,9 +1,21 @@ import { generateId } from './shared.js'; -export const kPending = Symbol.for('pending'); +/** @type {symbol} */ +const kPending = Symbol.for('pending'); +/** @type {symbol} */ const kOnConsumed = Symbol.for('onConsumed'); +/** @typedef {Pick} MessageEnvelope */ + +/** + * What it is all about - message + * @param {import('#types').MessageFields} fields + * @param {any} [content] + * @param {import('#types').MessageProperties} [properties] + * @param {CallableFunction} [onConsumed] + */ export function Message(fields, content, properties, onConsumed) { + /** @private */ this[kOnConsumed] = [null, onConsumed]; this[kPending] = false; @@ -18,8 +30,20 @@ export function Message(fields, content, properties, onConsumed) { const { consumerTag, ...mfields } = fields; + /** + * Message fields + * @type {import('#types').MessageFields} + */ this.fields = mfields; + /** + * Message content + * @type {any} + */ this.content = content; + /** + * Message properties + * @type {import('#types').MessageProperties} + */ this.properties = mproperties; } @@ -29,6 +53,10 @@ Object.defineProperty(Message.prototype, 'pending', { }, }); +/** + * Acknowledge message + * @param {boolean} [allUpTo] all outstanding messages prior to and including the given message shall be considered acknowledged. If false, or omitted, only the message supplied is acknowledged. Defaults to false + */ Message.prototype.ack = function ack(allUpTo) { if (!this[kPending]) return; for (const fn of this[kOnConsumed]) { @@ -37,6 +65,11 @@ Message.prototype.ack = function ack(allUpTo) { this[kPending] = false; }; +/** + * Reject message + * @param {boolean} [allUpTo] all outstanding messages prior to and including the given message shall be considered rejected. If false, or omitted, only the message supplied is rejected. Defaults to false + * @param {boolean} [requeue] put the message or messages back on the queue, defaults to true + */ Message.prototype.nack = function nack(allUpTo, requeue = true) { if (!this[kPending]) return; for (const fn of this[kOnConsumed]) { @@ -45,12 +78,22 @@ Message.prototype.nack = function nack(allUpTo, requeue = true) { this[kPending] = false; }; +/** + * Reject message + * @param {boolean} [requeue] put the message back on the queue, defaults to true + */ Message.prototype.reject = function reject(requeue = true) { this.nack(false, requeue); }; +/** @private */ Message.prototype._consume = function consume(consumerTag, consumedCb) { this[kPending] = true; this.fields.consumerTag = consumerTag; this[kOnConsumed][0] = consumedCb; }; + +/** @private */ +Message.prototype._clearPending = function clearPending() { + this[kPending] = false; +}; diff --git a/src/Queue.js b/src/Queue.js index a97608d..a74f129 100644 --- a/src/Queue.js +++ b/src/Queue.js @@ -1,5 +1,5 @@ import { generateId, sortByPriority } from './shared.js'; -import { kPending, Message } from './Message.js'; +import { Message } from './Message.js'; import { SmqpError, ERR_EXCLUSIVE_CONFLICT, ERR_EXCLUSIVE_NOT_ALLOWED } from './Errors.js'; const kName = Symbol.for('name'); @@ -11,19 +11,28 @@ const kIsReady = Symbol.for('isReady'); const kAvailableCount = Symbol.for('availableCount'); const kStopped = Symbol.for('stopped'); +/** + * Queue + * @param {string} [name] optional, but recommended queue name, defaults to `smq.qname-` + * @param {import('#types').QueueOptions} [options] queue options + * @param {import('#types').ExchangeEventEmitter} [eventEmitter] optional event emitter + */ export function Queue(name, options, eventEmitter) { if (name && typeof name !== 'string') throw new TypeError('Queue name must be a string'); else if (!name) name = `smq.qname-${generateId()}`; this[kName] = name; + /** @type {import('#types').QueueOptions} */ this.options = { autoDelete: true, ...options }; + /** @type {Message[]} */ this.messages = []; this.events = eventEmitter; this[kConsumers] = []; this[kStopped] = false; this[kAvailableCount] = 0; this[kExclusive] = false; + /** @private */ this._onMessageConsumed = this._onMessageConsumed.bind(this); } @@ -60,6 +69,12 @@ Object.defineProperties(Queue.prototype, { }, }); +/** + * Enqueue a message + * @param {import('#types').MessageFields} fields message fields + * @param {any} [content] message content + * @param {import('#types').MessageProperties} [properties] message properties + */ Queue.prototype.queueMessage = function queueMessage(fields, content, properties) { if (fields && typeof fields !== 'object') throw new TypeError('fields must be an object'); if (properties && typeof properties !== 'object') throw new TypeError('properties must be an object'); @@ -90,6 +105,10 @@ Queue.prototype.queueMessage = function queueMessage(fields, content, properties return discarded ? 0 : this._consumeNext(); }; +/** + * Evict first non-pending message; returns true if it was the supplied message + * @param {Message} [compareMessage] message to compare against the evicted one + */ Queue.prototype.evictFirst = function evictFirst(compareMessage) { const evict = this.get(); if (!evict) return; @@ -97,6 +116,7 @@ Queue.prototype.evictFirst = function evictFirst(compareMessage) { return evict === compareMessage; }; +/** @private */ Queue.prototype._consumeNext = function consumeNext() { if (this[kStopped] || !this[kAvailableCount]) return; @@ -115,6 +135,12 @@ Queue.prototype._consumeNext = function consumeNext() { return consumed; }; +/** + * Add a consumer + * @param {import('#types').onMessage} onMessage message handler + * @param {import('#types').ConsumeOptions} [consumeOptions] optional consume options + * @param {any} [owner] forwarded to the message handler as the third arg + */ Queue.prototype.consume = function consume(onMessage, consumeOptions, owner) { const consumers = this[kConsumers]; if (consumers.length) { @@ -141,7 +167,14 @@ Queue.prototype.consume = function consume(onMessage, consumeOptions, owner) { return consumer; }; +/** + * Assert consumer matching handler + options exists, create if absent + * @param {import('#types').onMessage} onMessage message handler + * @param {import('#types').ConsumeOptions} [consumeOptions] optional consume options + * @param {any} [owner] forwarded to the message handler as the third arg + */ Queue.prototype.assertConsumer = function assertConsumer(onMessage, consumeOptions, owner) { + /** @type {Consumer[]} */ const consumers = this[kConsumers]; if (!consumers.length) return this.consume(onMessage, consumeOptions, owner); for (const consumer of consumers) { @@ -160,17 +193,26 @@ Queue.prototype.assertConsumer = function assertConsumer(onMessage, consumeOptio return this.consume(onMessage, consumeOptions, owner); }; +/** + * Get next message from queue + * @param {import('#types').ConsumeOptions} [options] optional consume options + */ Queue.prototype.get = function getMessage(options) { const message = this._consumeMessages(1, { noAck: options?.noAck, consumerTag: options?.consumerTag })[0]; if (!message) return false; if (options?.noAck) { this._dequeueMessage(message); - message[kPending] = false; + message._clearPending(); } return message; }; +/** + * @private + * @param {number} n + * @param {import('#types').ConsumeOptions} consumeOptions + */ Queue.prototype._consumeMessages = function consumeMessages(n, consumeOptions) { const msgs = []; @@ -199,18 +241,41 @@ Queue.prototype._consumeMessages = function consumeMessages(n, consumeOptions) { return msgs; }; +/** + * Acknowledge message + * @param {Message} message message to ack + * @param {boolean} [allUpTo] ack all messages up to and including this one + */ Queue.prototype.ack = function ack(message, allUpTo) { - if (this._onMessageConsumed(message, 'ack', allUpTo, false)) message[kPending] = false; + if (this._onMessageConsumed(message, 'ack', allUpTo, false)) message._clearPending(); }; +/** + * Reject message + * @param {Message} message message to nack + * @param {boolean} [allUpTo] nack all messages up to and including this one + * @param {boolean} [requeue] requeue nacked message(s), defaults to true + */ Queue.prototype.nack = function nack(message, allUpTo, requeue = true) { - if (this._onMessageConsumed(message, 'nack', allUpTo, requeue)) message[kPending] = false; + if (this._onMessageConsumed(message, 'nack', allUpTo, requeue)) message._clearPending(); }; +/** + * Reject message + * @param {Message} message message to reject + * @param {boolean} [requeue] requeue rejected message, defaults to true + */ Queue.prototype.reject = function reject(message, requeue = true) { - if (this._onMessageConsumed(message, 'nack', false, requeue)) message[kPending] = false; + if (this._onMessageConsumed(message, 'nack', false, requeue)) message._clearPending(); }; +/** + * @private + * @param {Message} message + * @param {string} operation + * @param {boolean} allUpTo + * @param {boolean} requeue + */ Queue.prototype._onMessageConsumed = function onMessageConsumed(message, operation, allUpTo, requeue) { if (this[kStopped]) return; @@ -276,12 +341,20 @@ Queue.prototype.ackAll = function ackAll() { } }; +/** + * Reject all pending messages + * @param {boolean} [requeue] requeue nacked messages, defaults to true + */ Queue.prototype.nackAll = function nackAll(requeue = true) { for (const msg of this._getPendingMessages()) { msg.nack(false, requeue); } }; +/** + * @private + * @param {number} untilIndex + */ Queue.prototype._getPendingMessages = function getPendingMessages(untilIndex) { const messages = this.messages; const l = messages.length; @@ -299,6 +372,10 @@ Queue.prototype._getPendingMessages = function getPendingMessages(untilIndex) { return result; }; +/** + * Peek at the next message without consuming it + * @param {boolean} [ignoreDelivered] skip pending messages + */ Queue.prototype.peek = function peek(ignoreDelivered) { const message = this.messages[0]; if (!message) return; @@ -311,6 +388,11 @@ Queue.prototype.peek = function peek(ignoreDelivered) { } }; +/** + * Cancel consumer by tag + * @param {string} consumerTag consumer tag + * @param {boolean} [requeue] requeue messages held by the consumer, defaults to true + */ Queue.prototype.cancel = function cancel(consumerTag, requeue) { const consumers = this[kConsumers]; const idx = consumers.findIndex((c) => c.consumerTag === consumerTag); @@ -322,6 +404,11 @@ Queue.prototype.cancel = function cancel(consumerTag, requeue) { return true; }; +/** + * Cancel consumer matching the given handler + * @param {import('#types').onMessage} onMessage handler previously passed to consume + * @param {boolean} [requeue] requeue messages held by the consumer, defaults to true + */ Queue.prototype.dismiss = function dismiss(onMessage, requeue) { const consumers = this[kConsumers]; const consumer = consumers.find((c) => c.onMessage === onMessage); @@ -329,6 +416,11 @@ Queue.prototype.dismiss = function dismiss(onMessage, requeue) { this.unbindConsumer(consumer, requeue); }; +/** + * Unbind consumer from queue + * @param {Consumer} consumer consumer to unbind + * @param {boolean} [requeue] requeue messages held by the consumer, defaults to true + */ Queue.prototype.unbindConsumer = function unbindConsumer(consumer, requeue = true) { const consumers = this[kConsumers]; const idx = consumers.indexOf(consumer); @@ -346,16 +438,32 @@ Queue.prototype.unbindConsumer = function unbindConsumer(consumer, requeue = tru if (!consumers.length && this.options.autoDelete) return this.emit('delete', this); }; +/** + * Emit a queue event + * @param {string} eventName event name (without `queue.` prefix) + * @param {any} [content] event payload + */ Queue.prototype.emit = function emit(eventName, content) { if (!this.events) return; this.events.emit(`queue.${eventName}`, content); }; +/** + * Subscribe to a queue event + * @param {import('#types').QueueEventNames | string} eventName event name (without `queue.` prefix); accepts known names or a routing pattern + * @param {Function} handler event handler + * @param {import('#types').ConsumeOptions} [options] optional consume options + */ Queue.prototype.on = function on(eventName, handler, options) { if (!this.events) return; return this.events.on(`queue.${eventName}`, handler, options); }; +/** + * Unsubscribe from a queue event + * @param {import('#types').QueueEventNames | string} eventName event name previously passed to on + * @param {Function | { consumerTag?: string }} handler the handler used in on, or an object with the consumer tag + */ Queue.prototype.off = function off(eventName, handler) { if (!this.events) return; return this.events.off(`queue.${eventName}`, handler); @@ -373,6 +481,10 @@ Queue.prototype.purge = function purge() { return toDelete.length; }; +/** + * @private + * @param {Message} message + */ Queue.prototype._dequeueMessage = function dequeueMessage(message) { const messages = this.messages; const msgIdx = messages.indexOf(message); @@ -383,6 +495,7 @@ Queue.prototype._dequeueMessage = function dequeueMessage(message) { Queue.prototype.getState = function getState() { const msgs = this.messages; + /** @type {{name: string, options: import('#types').QueueOptions, messages?: import('./Message.js').MessageEnvelope[] }} */ const state = { name: this.name, options: { ...this.options }, @@ -400,6 +513,10 @@ Queue.prototype.getState = function getState() { return state; }; +/** + * Recover queue from previously captured state + * @param {import('#types').QueueState} [state] queue state, omit to recover stopped queue in place + */ Queue.prototype.recover = function recover(state) { this[kStopped] = false; const consumers = this[kConsumers]; @@ -434,6 +551,10 @@ Queue.prototype.recover = function recover(state) { return this; }; +/** + * Delete queue + * @param {import('#types').DeleteQueueOptions} [options] optional delete guards + */ Queue.prototype.delete = function deleteQueue(options) { const consumers = this[kConsumers]; if (options?.ifUnused && consumers.length) return; @@ -465,6 +586,9 @@ Queue.prototype.stop = function stop() { } }; +/** + * @private + */ Queue.prototype._getCapacity = function getCapacity() { if ('maxLength' in this.options) { return this.options.maxLength - this.messages.length; @@ -472,6 +596,14 @@ Queue.prototype._getCapacity = function getCapacity() { return Infinity; }; +/** + * Queue consumer + * @param {Queue} queue queue this consumer reads from + * @param {import('#types').onMessage} onMessage message handler + * @param {import('#types').ConsumeOptions} [options] consume options + * @param {any} [owner] forwarded to the message handler as the third arg + * @param {import('#types').ExchangeEventEmitter} [eventEmitter] internal queue event bridge + */ export function Consumer(queue, onMessage, options, owner, eventEmitter) { if (typeof onMessage !== 'function') throw new TypeError('message callback is required and must be a function'); @@ -531,6 +663,17 @@ Object.defineProperties(Consumer.prototype, { }, }); +/** Project consumer state for serialization (used by `Broker.getConsumers` and `JSON.stringify`) */ +Consumer.prototype.toJSON = function toJSON() { + return { + queue: this.queue.name, + consumerTag: this.consumerTag, + ready: this.ready, + options: { ...this.options }, + }; +}; + +/** @private */ Consumer.prototype._push = function push(messages) { const internalQueue = this[kInternalQueue]; for (const message of messages) { @@ -546,6 +689,7 @@ Consumer.prototype._push = function push(messages) { } }; +/** @private */ Consumer.prototype._consume = function consume() { const internalQ = this[kInternalQueue]; const consumerTag = this[kName]; @@ -565,33 +709,56 @@ Consumer.prototype._consume = function consume() { } }; +/** + * Reject all messages held by this consumer + * @param {boolean} [requeue] requeue nacked messages, defaults to true + */ Consumer.prototype.nackAll = function nackAll(requeue) { for (const msg of this[kInternalQueue].messages.slice()) { msg.content.nack(false, requeue); } }; +/** Acknowledge all messages held by this consumer */ Consumer.prototype.ackAll = function ackAll() { for (const msg of this[kInternalQueue].messages.slice()) { msg.content.ack(false); } }; +/** + * Cancel consumer + * @param {boolean} [requeue] requeue messages held by the consumer, defaults to true + */ Consumer.prototype.cancel = function cancel(requeue = true) { this.stop(); if (!requeue) this.nackAll(requeue); this.emit('cancel', this); }; +/** + * Set consumer prefetch count + * @param {number} value new prefetch count + */ Consumer.prototype.prefetch = function prefetch(value) { this.options.prefetch = this[kInternalQueue].options.maxLength = value; }; +/** + * Emit consumer event + * @param {string} eventName event name (without `consumer.` prefix) + * @param {any} [content] event payload + */ Consumer.prototype.emit = function emit(eventName, content) { const routingKey = `consumer.${eventName}`; this.events.emit(routingKey, content); }; +/** + * Subscribe to consumer event + * @param {string} eventName event name (without `consumer.` prefix) + * @param {Function} handler event handler + */ Consumer.prototype.on = function on(eventName, handler) { const pattern = `consumer.${eventName}`; return this.events.on(pattern, handler); diff --git a/src/Shovel.js b/src/Shovel.js index 39ab850..768a74c 100644 --- a/src/Shovel.js +++ b/src/Shovel.js @@ -1,17 +1,34 @@ import { EventExchange } from './Exchange.js'; import { SmqpError, ERR_SHOVEL_DESTINATION_EXCHANGE_NOT_FOUND, ERR_SHOVEL_SOURCE_EXCHANGE_NOT_FOUND } from './Errors.js'; +/** @type {symbol} */ const kName = Symbol.for('name'); +/** @type {symbol} */ const kBrokerInternal = Symbol.for('brokerInternal'); +/** @type {symbol} */ const kCloneMessage = Symbol.for('cloneMessage'); +/** @type {symbol} */ const kClosed = Symbol.for('closed'); +/** @type {symbol} */ const kConsumerTag = Symbol.for('consumerTag'); +/** @type {symbol} */ const kDestinationExchange = Symbol.for('destinationExchange'); +/** @type {symbol} */ const kEventHandlers = Symbol.for('eventHandlers'); +/** @type {symbol} */ const kSourceBroker = Symbol.for('sourceBroker'); +/** @type {symbol} */ const kSourceExchange = Symbol.for('sourceExchange'); +/** @type {symbol} */ const kE2EShovel = Symbol.for('shovel'); +/** + * Shovel — pipe messages from a source exchange to a destination exchange + * @param {string} name unique shovel name + * @param {import('#types').ShovelSource} source source spec + * @param {import('#types').ShovelDestination} destination destination spec + * @param {import('#types').ShovelOptions} [options] optional shovel options + */ export function Shovel(name, source, destination, options) { if (!name || typeof name !== 'string') throw new TypeError('Shovel name is required and must be a string'); @@ -41,6 +58,7 @@ export function Shovel(name, source, destination, options) { this[kName] = name; this.source = { ...source, pattern: routingKeyPattern }; this.destination = { ...destination }; + /** @type {import('#types').ExchangeEventEmitter} */ this.events = new EventExchange('shovel__events'); const consumerTag = (this[kConsumerTag] = source.consumerTag || `smq.shoveltag-${name}`); @@ -86,18 +104,35 @@ Object.defineProperties(Shovel.prototype, { }, }); +/** + * Emit shovel event + * @param {string} eventName event name (without `shovel.` prefix) + * @param {any} [content] event payload + */ Shovel.prototype.emit = function emit(eventName, content) { this.events.emit(`shovel.${eventName}`, content); }; +/** + * Subscribe to shovel event + * @param {string} eventName event name (without `shovel.` prefix) + * @param {Function} handler event handler + * @param {import('#types').ConsumeOptions} [options] optional consume options + */ Shovel.prototype.on = function on(eventName, handler, options) { return this.events.on(`shovel.${eventName}`, handler, options); }; +/** + * Unsubscribe from shovel event + * @param {string} eventName event name previously passed to on + * @param {Function | { consumerTag?: string }} handler the handler used in on, or an object with the consumer tag + */ Shovel.prototype.off = function off(eventName, handler) { return this.events.off(`shovel.${eventName}`, handler); }; +/** Close shovel and cancel its source consumer */ Shovel.prototype.close = function closeShovel() { if (this[kClosed]) return; this[kClosed] = true; @@ -109,6 +144,7 @@ Shovel.prototype.close = function closeShovel() { this[kSourceBroker].cancel(this[kConsumerTag]); }; +/** @private */ Shovel.prototype._messageHandler = function messageHandler(message) { const cloneMessage = this[kCloneMessage]; if (!cloneMessage) return message; @@ -127,6 +163,7 @@ Shovel.prototype._messageHandler = function messageHandler(message) { }; }; +/** @private */ Shovel.prototype._onShovelMessage = function onShovelMessage(routingKey, message) { const destinationExchange = this[kDestinationExchange]; if (!destinationExchange.bindingCount && !message.properties.mandatory) return message.ack(); @@ -138,6 +175,10 @@ Shovel.prototype._onShovelMessage = function onShovelMessage(routingKey, message message.ack(); }; +/** + * Exchange-to-exchange shovel wrapper, returned by `broker.bindExchange` + * @param {Shovel} shovel underlying shovel + */ export function Exchange2Exchange(shovel) { this[kE2EShovel] = shovel; } @@ -175,10 +216,16 @@ Object.defineProperties(Exchange2Exchange.prototype, { }, }); -Exchange2Exchange.prototype.on = function e2eon(...args) { - return this[kE2EShovel].on(...args); +/** + * Subscribe to underlying shovel events + * @param {string} eventName event name (without `shovel.` prefix) + * @param {Function} handler event handler + */ +Exchange2Exchange.prototype.on = function e2eon(eventName, handler) { + return this[kE2EShovel].on(eventName, handler); }; +/** Close the underlying shovel */ Exchange2Exchange.prototype.close = function e2eclose() { return this[kE2EShovel].close(); }; diff --git a/src/index.js b/src/index.js index cf162e6..a5e4c85 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,6 @@ import { Broker } from './Broker.js'; +export { Broker as default }; export { Broker }; export { Message } from './Message.js'; export { Queue, Consumer } from './Queue.js'; @@ -7,5 +8,3 @@ export { Shovel } from './Shovel.js'; export { Exchange } from './Exchange.js'; export * from './Errors.js'; export { getRoutingKeyPattern } from './shared.js'; - -export default Broker; diff --git a/src/shared.js b/src/shared.js index 3a69efa..4b6305d 100644 --- a/src/shared.js +++ b/src/shared.js @@ -20,6 +20,14 @@ EndMatchRoutingKeyPattern.prototype.test = function test(routingKey) { return !routingKey.indexOf(this._match); }; +/** + * Get routing key pattern + * @param {string} pattern routing key pattern + * @returns {RoutingKeyPattern} + * + * @typedef {object} RoutingKeyPattern + * @property {(this: RoutingKeyPattern, routingKey: string) => boolean} test method to test a routing key against the pattern; receiver-bound — destructuring is unsupported + */ export function getRoutingKeyPattern(pattern) { const len = pattern.length; const hashIdx = pattern.indexOf('#'); diff --git a/test/types-test.js b/test/types-test.js new file mode 100644 index 0000000..11476d2 --- /dev/null +++ b/test/types-test.js @@ -0,0 +1,216 @@ +import fs from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; +import { createBundle } from 'dts-buddy'; + +describe('generated types bundle', () => { + let dts; + + before(async function before() { + this.timeout(20000); + const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'smqp-dts-')); + const output = path.join(dir, 'index.d.ts'); + await createBundle({ + project: 'tsconfig.json', + output, + modules: { smqp: 'src/index.js' }, + }); + dts = await fs.readFile(output, 'utf8'); + await fs.rm(dir, { recursive: true, force: true }); + }); + + function classBlock(name) { + const startRe = new RegExp(`(class|interface)\\s+${name}(\\s|<|\\{|_)`); + const start = dts.split('\n').findIndex((l) => startRe.test(l)); + if (start === -1) throw new Error(`class ${name} not found in bundle`); + const lines = dts.split('\n').slice(start); + let depth = 0; + let end = -1; + for (let i = 0; i < lines.length; i++) { + depth += (lines[i].match(/\{/g) || []).length; + depth -= (lines[i].match(/\}/g) || []).length; + if (depth === 0 && i > 0) { + end = i; + break; + } + } + return lines.slice(0, end + 1).join('\n'); + } + + const expectations = { + Broker_1: ['exchangeCount: number', 'queueCount: number', 'consumerCount: number'], + ExchangeBase: [ + 'name: string', + 'bindingCount: number', + 'bindings: Binding[]', + 'type: exchangeType', + 'stopped: boolean', + 'undeliveredCount: number', + ], + Queue: [ + 'name: string', + 'consumerCount: number', + 'consumers: Consumer[]', + 'exclusive: boolean', + 'messageCount: number', + 'stopped: boolean', + ], + Consumer: [ + 'consumerTag: string', + 'ready: boolean', + 'stopped: boolean', + 'capacity: number', + 'messageCount: number', + 'queueName: string', + ], + Shovel: ['name: string', 'closed: boolean', 'consumerTag: string'], + Exchange2Exchange: ['name: string', 'source: string', 'destination: string', 'pattern: string', 'queue: string', 'consumerTag: string'], + }; + + for (const [klass, members] of Object.entries(expectations)) { + describe(klass, () => { + for (const member of members) { + it(`exposes ${member}`, () => { + const block = classBlock(klass); + expect(block).to.include(member); + }); + } + }); + } + + describe('module exports', () => { + it('re-exports Broker as a named export (build-types.js patch)', () => { + expect(dts).to.include('export { Broker_1 as Broker };'); + }); + + it('exports Broker as default', () => { + expect(dts).to.match(/export default (function|class) Broker_1/); + }); + }); + + describe('Broker method signatures', () => { + it('subscribe carries typed params', () => { + expect(dts).to.match( + /subscribe\(exchangeName: string, pattern: string, queueName: string, onMessage: onMessage, options\?: SubscribeOptions\)/ + ); + }); + + it('publish carries typed params', () => { + expect(dts).to.match(/publish\(exchangeName: string, routingKey: string, content\?: any, properties\?: MessageProperties\)/); + }); + + it('assertExchange carries typed params', () => { + expect(dts).to.match(/assertExchange\(exchangeName: string, type\?: exchangeType, options\?: ExchangeOptions\)/); + }); + + it('ack carries typed params', () => { + expect(dts).to.match(/ack\(message: Message, allUpTo\?: boolean\)/); + }); + + it('createShovel carries typed params', () => { + expect(dts).to.match(/createShovel\(name: string, source: ShovelSource, destination: ShovelDestination, options\?: ShovelOptions\)/); + }); + }); + + describe('Queue method signatures', () => { + it('queueMessage carries typed params', () => { + expect(dts).to.match(/queueMessage\(fields: MessageFields, content\?: any, properties\?: MessageProperties\)/); + }); + + it('consume carries typed params', () => { + expect(dts).to.match(/consume\(onMessage: onMessage, consumeOptions\?: ConsumeOptions, owner\?: any\)/); + }); + + it('ack carries typed params', () => { + expect(dts).to.match(/ack\(message: Message, allUpTo\?: boolean\)/); + }); + + it('recover carries typed params', () => { + expect(dts).to.match(/recover\(state\?: QueueState\)/); + }); + + it('on uses QueueEventNames union', () => { + expect(dts).to.match(/on\(eventName: QueueEventNames \| string, handler: Function, options\?: ConsumeOptions\)/); + }); + + it('off uses QueueEventNames union', () => { + expect(dts).to.match(/off\(eventName: QueueEventNames \| string/); + }); + + it('exposes QueueEventNames literal union', () => { + expect(dts).to.include("'consumer.cancel'"); + expect(dts).to.include("'dead-letter'"); + expect(dts).to.include("'saturated'"); + }); + }); + + describe('Consumer method signatures', () => { + it('cancel carries typed params', () => { + expect(dts).to.match(/cancel\(requeue\?: boolean\)/); + }); + + it('prefetch carries typed params', () => { + expect(dts).to.match(/prefetch\(value: number\)/); + }); + }); + + describe('private state does not leak', () => { + it('Message does not expose [kPending] symbol-keyed property', () => { + expect(dts).to.not.match(/\[kPending\]:/); + }); + + it('Message does not expose [kOnConsumed] symbol-keyed property', () => { + expect(dts).to.not.match(/\[kOnConsumed\]:/); + }); + + it('Shovel does not expose Symbol.for-keyed properties', () => { + expect(dts).to.not.match(/\[kSourceBroker\]:/); + expect(dts).to.not.match(/\[kEventHandlers\]:/); + expect(dts).to.not.match(/\[kE2EShovel\]:/); + }); + }); + + describe('Shovel method signatures', () => { + it('Shovel constructor carries typed params', () => { + expect(dts).to.match(/constructor\(name: string, source: ShovelSource, destination: ShovelDestination, options\?: ShovelOptions\)/); + }); + + it('Shovel.on carries typed params', () => { + expect(dts).to.match(/on\(eventName: string, handler: Function, options\?: ConsumeOptions\)/); + }); + + it('Exchange2Exchange.on carries typed params', () => { + expect(dts).to.match(/on\(eventName: string, handler: Function\)/); + }); + }); + + describe('RoutingKeyPattern.test cannot be destructured', () => { + it('test signature carries explicit this: RoutingKeyPattern', () => { + expect(dts).to.match(/test:\s*\(this: RoutingKeyPattern, routingKey: string\) => boolean/); + }); + }); + + describe('underscore-prefixed prototype methods are private', () => { + it('all internal _-methods are emitted as private', () => { + const internals = [ + '_onTopicMessage', + '_onDirectMessage', + '_emitReturn', + '_consumeNext', + '_consumeMessages', + '_onMessageConsumed', + '_getPendingMessages', + '_dequeueMessage', + '_getCapacity', + '_push', + '_messageHandler', + '_onShovelMessage', + '_getQueuesState', + '_getExchangeState', + '_clearPending', + ]; + const missing = internals.filter((name) => !dts.includes(`private ${name}`)); + expect(missing, `missing private marker for: ${missing.join(', ')}`).to.have.lengthOf(0); + }); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index b39c687..71fa019 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,29 +1,27 @@ { - "include": ["src/**/*", "types"], + "include": ["src/**/*", "types/interfaces.d.ts"], "compilerOptions": { "emitDeclarationOnly": true, "sourceMap": false, - "rootDir": "src", - "lib": ["es2017"], - "target": "es2017", + "rootDir": ".", + "lib": ["es2022"], + "target": "es2022", "outDir": "./tmp/ignore", "declaration": true, "noEmitOnError": true, "noErrorTruncation": true, "allowJs": true, "checkJs": false, - "module": "esnext", - "moduleResolution": "node", + "module": "nodenext", + "moduleResolution": "nodenext", "resolveJsonModule": false, "allowSyntheticDefaultImports": true, "strict": true, "stripInternal": true, - "noImplicitThis": true, "noUnusedLocals": true, "noUnusedParameters": true, - "typeRoots": ["./node_modules/@types"], "paths": { - "types": ["./types/types.js"] + "#types": ["./types/interfaces.d.ts"] } } } diff --git a/types/Broker.d.ts b/types/Broker.d.ts deleted file mode 100644 index 360048d..0000000 --- a/types/Broker.d.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { ConsumeOptions, onMessage } from './types.js'; -import { Queue, Consumer, queueOptions, deleteQueueOptions, QueueState } from './Queue.js'; -import { Shovel, Exchange2Exchange, ShovelDestination, ShovelOptions } from './Shovel.js'; -import { Message, MessageProperties } from './Message.js'; -import { Exchange, exchangeType, Binding, ExchangeOptions, bindingOptions, ExchangeState } from './Exchange.js'; - -type subscribeOptions = { - /** defaults to true, exchange will be deleted when all bindings are removed; the queue will be removed when all consumers are down */ - autoDelete?: boolean; - /** defaults to true, makes exchange and queue durable, i.e. will be returned when getting state */ - durable?: boolean; - /** unique consumer tag */ - consumerTag?: string; - /** dead letter exchange */ - deadLetterExchange?: string; - /** publish dead letter with routing key */ - deadLetterRoutingKey?: string; - /** queue is exclusively consumed */ - exclusive?: boolean; - /** set to true if there is no need to acknowledge message */ - noAck?: boolean; - /** defaults to 1, number of messages to consume at a time */ - prefetch?: number; - /** defaults to 0, higher value gets messages first */ - priority?: number; - [x: string]: any; -}; - -interface ConsumerInfo { - queue: string; - consumerTag: string; - ready: boolean; - options: ConsumeOptions; -} - -interface BrokerShovelSource { - exchange: string; - pattern?: string; - priority?: number; - queue?: string; - consumerTag?: string; -} - -export interface BrokerState { - exchanges?: ExchangeState[]; - queues?: QueueState[]; -} - -export function Broker(owner?: any): Broker; -export class Broker { - constructor(owner?: any); - owner?: any; - get exchangeCount(): number; - get queueCount(): number; - get consumerCount(): number; - subscribe(exchangeName: string, pattern: string, queueName: string, onMessage: onMessage, options?: subscribeOptions): Consumer; - subscribeTmp(exchangeName: string, pattern: string, onMessage: onMessage, options?: subscribeOptions): Consumer; - subscribeOnce(exchangeName: string, pattern: string, onMessage: onMessage, options?: subscribeOptions): Consumer; - unsubscribe(queueName: string, onMessage: onMessage): void; - assertExchange(exchangeName: string, type?: exchangeType, options?: ExchangeOptions): Exchange; - assertQueue(queueName: string, options?: queueOptions): Queue; - bindQueue(queueName: string, exchangeName: string, pattern: string, bindOptions?: bindingOptions): Binding; - unbindQueue(queueName: string, exchangeName: string, pattern: string): void; - consume(queueName: string, onMessage: onMessage, options?: ConsumeOptions): Consumer; - /** - * Cancel consumer - * @param consumerTag Consumer tag - * @param requeue optional boolean to requeue messages consumed by consumer, defaults to true - * @returns true if found, false if not - */ - cancel(consumerTag: string, requeue?: boolean): boolean; - getExchange(exchangeName: string): Exchange; - getQueue(queueName: string): Queue; - createQueue(queueName?: string, options?: queueOptions): Queue; - deleteExchange(exchangeName: string, options?: { ifUnused?: boolean }): boolean; - purgeQueue(queueName: string): number; - sendToQueue(queueName: string, content: any, options?: MessageProperties): number; - deleteQueue(queueName: string, options?: deleteQueueOptions): { messageCount: number } | undefined; - bindExchange(source: string, destination: string, pattern?: string, options?: ShovelOptions): Exchange2Exchange; - unbindExchange(source: string, destination: string, pattern?: string): boolean; - createShovel(name: string, source: BrokerShovelSource, destination: ShovelDestination, options?: ShovelOptions): Shovel; - closeShovel(name: string): boolean; - getShovel(name: string): Shovel; - getShovels(): Shovel[]; - getConsumers(): ConsumerInfo[]; - getConsumer(consumerTag: string): Consumer; - stop(): void; - close(): void; - /** - * Get broker state with durable entities - */ - getState(): BrokerState; - /** - * Get broker state with durable entities - * @param {boolean} onlyWithContent only return state if any durable exchanges or queues that have messages - */ - getState(onlyWithContent: boolean): BrokerState | undefined; - recover(state?: BrokerState): Broker; - publish(exchangeName: string, routingKey: string, content?: any, options?: MessageProperties): number; - get(queueName: string, options?: { noAck: boolean }): ReturnType; - ack(message: Message, allUpTo?: boolean): void; - ackAll(): void; - nack(message: Message, allUpTo?: boolean, requeue?: boolean): void; - nackAll(requeue?: boolean): void; - reject(message: Message, requeue?: boolean): void; - /** - * Check if consumer tag is occupied - * @param consumerTag - * @returns {boolean} true if not occupied, throws SmqpError if it is - */ - validateConsumerTag(consumerTag: string): boolean; - /** - * Listen broker for events - * @param eventName event name, e.g. return for undelivered messages, a routing key pattern can be used, e.g. queue.# for all queue events - * @param callback event handler function - * @param options consume options, consumerTag is probably the most usable option, noAck is ignored and always true - */ - on(eventName: string, callback: CallableFunction, options?: ConsumeOptions): Consumer; - off(eventName: string, callbackOrObject: CallableFunction | ConsumeOptions): void; - prefetch(value?: number): void; - /** DANGER deletes all broker entities and closes broker */ - reset(): void; -} diff --git a/types/Errors.d.ts b/types/Errors.d.ts deleted file mode 100644 index fb5e872..0000000 --- a/types/Errors.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -export class SmqpError extends Error { - name: string; - type: string; - code: string; - constructor(message: string, code: string); -} - -export const ERR_CONSUMER_TAG_CONFLICT = 'ERR_SMQP_CONSUMER_TAG_CONFLICT'; -export const ERR_EXCHANGE_NOT_FOUND = 'ERR_SMQP_EXCHANGE_NOT_FOUND'; -export const ERR_EXCHANGE_TYPE_MISMATCH = 'ERR_SMQP_EXCHANGE_TYPE_MISMATCH'; -export const ERR_EXCLUSIVE_CONFLICT = 'ERR_SMQP_EXCLUSIVE_CONFLICT'; -export const ERR_EXCLUSIVE_NOT_ALLOWED = 'ERR_SMQP_EXCLUSIVE_NOT_ALLOWED'; -export const ERR_QUEUE_DURABLE_MISMATCH = 'ERR_SMQP_QUEUE_DURABLE_MISMATCH'; -export const ERR_QUEUE_NAME_CONFLICT = 'ERR_SMQP_QUEUE_NAME_CONFLICT'; -export const ERR_QUEUE_NOT_FOUND = 'ERR_SMQP_QUEUE_NOT_FOUND'; -export const ERR_SHOVEL_DESTINATION_EXCHANGE_NOT_FOUND = 'ERR_SMQP_SHOVEL_DESTINATION_EXCHANGE_NOT_FOUND'; -export const ERR_SHOVEL_NAME_CONFLICT = 'ERR_SMQP_SHOVEL_NAME_CONFLICT'; -export const ERR_SHOVEL_SOURCE_EXCHANGE_NOT_FOUND = 'ERR_SMQP_SHOVEL_SOURCE_EXCHANGE_NOT_FOUND'; diff --git a/types/Exchange.d.ts b/types/Exchange.d.ts deleted file mode 100644 index 932c834..0000000 --- a/types/Exchange.d.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { ExchangeEventEmitter } from './types.js'; -import { MessageProperties } from './Message.js'; -import { Queue, QueueState } from './Queue.js'; - -type exchangeType = 'topic' | 'direct'; - -interface ExchangeOptions { - /** makes exchange durable, i.e. will be returned when getting state, defaults to true */ - durable?: boolean; - /** remove exchange when all bindings are gone, defaults to true */ - autoDelete?: boolean; - [x: string]: any; -} - -type bindingOptions = { - priority?: number; - [x: string]: any; -}; - -export interface BindingState { - id: string; - options: bindingOptions; - queueName: string; - pattern: string; -} - -export interface ExchangeState { - name: string; - type: exchangeType; - options: ExchangeOptions; - bindings?: BindingState[]; - /** Undelivered message queue */ - deliveryQueue?: QueueState; -} - -export class Exchange extends ExchangeEventEmitter { - constructor(name: string, type?: exchangeType, options?: ExchangeOptions); - get name(): string; - options: ExchangeOptions; - get type(): exchangeType; - get bindingCount(): number; - get bindings(): Binding[]; - get stopped(): boolean; - publish(routingKey: string, content?: any, properties?: MessageProperties): number; - bindQueue(queue: Queue, pattern: string, bindOptions?: bindingOptions): Binding; - unbindQueue(queue: Queue, pattern: string): void; - unbindQueueByName(queueName: string): void; - closeBinding(binding: Binding): void; - close(): void; - getState(): ExchangeState; - stop(): void; - /** - * Recover exchange - */ - recover(): Exchange; - /** - * Recover exchange - * @param state - * @param getQueue function to get queue instance from broker - */ - recover(state: ExchangeState, getQueue: CallableFunction): Exchange; - getBinding(queueName: string, pattern: string): Binding; -} - -export interface Binding { - id: string; - options: bindingOptions; - pattern: string; - exchange: Exchange; - queue: Queue; -} diff --git a/types/Message.d.ts b/types/Message.d.ts deleted file mode 100644 index 37804b3..0000000 --- a/types/Message.d.ts +++ /dev/null @@ -1,57 +0,0 @@ -export interface MessageFields extends Record { - /** published through exchange */ - exchange?: string; - /** published with routing key, if any */ - routingKey?: string; - /** identifying the consumer for which the message is destined */ - consumerTag?: string; - /** message has been redelivered, i.e. nacked or recovered */ - redelivered?: boolean; -} - -export interface MessageProperties extends Record { - /** unique identifier for the message */ - messageId?: string; - /** integer, expire message after milliseconds */ - expiration?: number; - /** integer, message time to live in milliseconds */ - ttl?: number; - /** Date.now() when message was sent */ - timestamp?: number; - /** indicating if message is mandatory. True emits return if not routed to any queue */ - mandatory?: boolean; - /** persist message, if unset queue option durable prevails */ - persistent?: boolean; - /** shovel or e2e message source exchange */ - 'source-exchange'?: string; - /** shovel name */ - 'shovel-name'?: string; -} - -export abstract class MessageMessage { - fields: MessageFields; - content?: any; - properties: MessageProperties; -} - -export class Message extends MessageMessage { - constructor(fields: MessageFields, content?: any, properties?: MessageProperties); - /** - * Acknowledge message - * @param allUpTo all outstanding messages prior to and including the given message shall be considered acknowledged. If false, or omitted, only the message supplied is acknowledged. Defaults to false - */ - ack(allUpTo?: boolean): void; - /** - * Reject message - * @param allUpTo all outstanding messages prior to and including the given message shall be considered rejected. If false, or omitted, only the message supplied is rejected. Defaults to false - * @param requeue put the message or messages back on the queue, defaults to true - */ - nack(allUpTo?: boolean, requeue?: boolean): void; - /** - * Reject message - * @param requeue put the message back on the queue, defaults to true - */ - reject(requeue?: boolean): void; - /** Message is pending ack */ - get pending(): boolean; -} diff --git a/types/Queue.d.ts b/types/Queue.d.ts deleted file mode 100644 index 711d0b5..0000000 --- a/types/Queue.d.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { ConsumeOptions, ExchangeEventEmitter, onMessage } from './types.js'; -import { Message, MessageFields, MessageProperties, MessageMessage } from './Message.js'; - -type queueOptions = { - /** makes queue durable, i.e. will be returned when getting state, defaults to true */ - autoDelete?: boolean; - /** makes queue durable, i.e. will be returned when getting state */ - durable?: boolean; - messageTtl?: number; - maxLength?: number; - deadLetterExchange?: string; - deadLetterRoutingKey?: string; - [x: string]: any; -}; - -type deleteQueueOptions = { - ifUnused?: boolean; - ifEmpty?: boolean; -}; - -export interface QueueState { - name: string; - options: queueOptions; - messages?: MessageMessage[]; -} - -export const enum QueueEventNames { - /** Consumer was cancelled */ - QueueConsumerCancel = 'consumer.cancel', - /** Consumer was added */ - QueueConsume = 'consume', - /** Message was dead-lettered, sends deadLetterExchange name and message */ - QueueDeadLetter = 'dead-letter', - /** Queue was deleted */ - QueueDelete = 'delete', - /** Queue is depleted */ - QueueDepleted = 'depleted', - /** Message was queued */ - QueueMessage = 'message', - /** Queue is ready to receive new messages */ - QueueReady = 'ready', - /** Queue is saturated, i.e. max capacity was reached */ - QueueSaturated = 'saturated', -} - -export class Queue extends ExchangeEventEmitter { - constructor(name?: string, options?: queueOptions, eventEmitter?: ExchangeEventEmitter); - get name(): string; - options: queueOptions; - get consumerCount(): number; - get consumers(): Consumer[]; - get exclusive(): boolean; - get messageCount(): number; - get stopped(): boolean; - queueMessage(fields: MessageFields, content?: any, properties?: MessageProperties): number; - evictFirst(compareMessage?: Message): boolean; - consume(onMessage: onMessage, consumeOptions?: ConsumeOptions, owner?: any): Consumer; - assertConsumer(onMessage: onMessage, consumeOptions?: ConsumeOptions, owner?: any): Consumer; - get(options?: ConsumeOptions): Message | undefined; - ack(message: Message, allUpTo?: boolean): void; - nack(message: Message, allUpTo?: boolean, requeue?: boolean): void; - reject(message: Message, requeue?: boolean): void; - ackAll(): void; - nackAll(requeue?: boolean): void; - peek(ignoreDelivered?: boolean): Message | undefined; - cancel(consumerTag: string, requeue?: boolean): boolean; - dismiss(onMessage: onMessage, requeue?: boolean): void; - unbindConsumer(consumer: Consumer, requeue?: boolean): void; - emit(eventName: string, content?: any): number | undefined; - on(eventName: string | QueueEventNames, handler: CallableFunction, options?: ConsumeOptions): Consumer; - off(eventName: string | QueueEventNames, handler: CallableFunction | ConsumeOptions): void; - purge(): number; - getState(): QueueState; - recover(state?: QueueState): Queue; - delete(options?: deleteQueueOptions): { messageCount: number }; - close(): void; - stop(): void; -} - -export class Consumer { - constructor(queue: Queue, onMessage: onMessage, options: ConsumeOptions, owner?: any, eventEmitter?: ExchangeEventEmitter); - options: ConsumeOptions; - get consumerTag(): string; - get ready(): boolean; - get stopped(): boolean; - get capacity(): number; - get messageCount(): number; - get queueName(): string; - nackAll(requeue?: boolean): void; - ackAll(): void; - cancel(requeue?: boolean): void; - prefetch(value: number): void; -} diff --git a/types/Shovel.d.ts b/types/Shovel.d.ts deleted file mode 100644 index 4003b5e..0000000 --- a/types/Shovel.d.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { Consumer } from './Queue.js'; -import { ExchangeEventEmitter } from './types.js'; -import { Broker } from './Broker.js'; -import { MessageMessage } from './Message.js'; - -export interface ShovelOptions { - cloneMessage?: (message: MessageMessage) => MessageMessage; - [x: string]: any; -} - -export interface ShovelSource { - /** source broker */ - broker: Broker; - /** source exchange name */ - exchange: string; - pattern?: string; - priority?: number; - queue?: string; - consumerTag?: string; -} - -export interface ShovelDestination { - /** destination broker */ - broker: Broker; - /** destination exchange */ - exchange: string; - /** optional destination exchange routing key, defaults to original message's routing key */ - exchangeKey?: string; - /** optional object with message properties to overwrite when shovelling messages */ - publishProperties?: Record; -} - -export class Shovel extends ExchangeEventEmitter { - constructor(name: string, source: ShovelSource, destination: ShovelDestination, options?: ShovelOptions); - get name(): string; - source: ShovelSource; - destination: ShovelDestination; - get closed(): boolean; - get consumerTag(): string; - close(): void; -} - -export class Exchange2Exchange { - constructor(shovel: Shovel); - readonly name: string; - readonly source: string; - /** name of source e2e queue */ - readonly queue: string; - readonly pattern: string; - readonly destination: string; - readonly consumerTag: string; - on(eventName: string, handler: CallableFunction): Consumer; - close(): void; -} diff --git a/types/index.d.ts b/types/index.d.ts old mode 100755 new mode 100644 index f04ea0b..8e37df0 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,12 +1,1055 @@ -import { Broker } from './Broker.js'; - -export { Broker }; -export { Message, MessageMessage } from './Message.js'; -export { Queue, Consumer, QueueEventNames } from './Queue.js'; -export { Shovel, ShovelOptions } from './Shovel.js'; -export { ConsumeOptions } from './types.js'; -export { Exchange, ExchangeOptions } from './Exchange.js'; -export * from './Errors.js'; -export { getRoutingKeyPattern } from './shared.js'; - -export default Broker; +declare module 'smqp' { + /** + * What it is all about - message + * + */ + export function Message(fields: MessageFields, content?: any, properties?: MessageProperties, onConsumed?: CallableFunction): void; + export class Message { + + /** + * What it is all about - message + * + */ + constructor(fields: MessageFields, content?: any, properties?: MessageProperties, onConsumed?: CallableFunction); + /** + * Message fields + * */ + fields: MessageFields; + /** + * Message content + * */ + content: any; + /** + * Message properties + * */ + properties: MessageProperties; + get pending(): any; + /** + * Acknowledge message + * @param allUpTo all outstanding messages prior to and including the given message shall be considered acknowledged. If false, or omitted, only the message supplied is acknowledged. Defaults to false + */ + ack(allUpTo?: boolean): void; + /** + * Reject message + * @param allUpTo all outstanding messages prior to and including the given message shall be considered rejected. If false, or omitted, only the message supplied is rejected. Defaults to false + * @param requeue put the message or messages back on the queue, defaults to true + */ + nack(allUpTo?: boolean, requeue?: boolean): void; + /** + * Reject message + * @param requeue put the message back on the queue, defaults to true + */ + reject(requeue?: boolean): void; + private _consume; + private _clearPending; + } + type MessageEnvelope = Pick; + /** + * Shovel — pipe messages from a source exchange to a destination exchange + * @param name unique shovel name + * @param source source spec + * @param destination destination spec + * @param options optional shovel options + */ + export function Shovel(name: string, source: ShovelSource, destination: ShovelDestination, options?: ShovelOptions): Shovel | undefined; + export class Shovel { + /** + * Shovel — pipe messages from a source exchange to a destination exchange + * @param name unique shovel name + * @param source source spec + * @param destination destination spec + * @param options optional shovel options + */ + constructor(name: string, source: ShovelSource, destination: ShovelDestination, options?: ShovelOptions); + source: { + pattern: string; + broker: Broker_1; + exchange: string; + priority?: number; + queue?: string; + consumerTag?: string; + } | undefined; + destination: { + broker: Broker_1; + exchange: string; + exchangeKey?: string; + publishProperties?: Record; + } | undefined; + + events: ExchangeEventEmitter; + /** + * Emit shovel event + * @param eventName event name (without `shovel.` prefix) + * @param content event payload + */ + emit(eventName: string, content?: any): void; + /** + * Subscribe to shovel event + * @param eventName event name (without `shovel.` prefix) + * @param handler event handler + * @param options optional consume options + */ + on(eventName: string, handler: Function, options?: ConsumeOptions): any; + /** + * Unsubscribe from shovel event + * @param eventName event name previously passed to on + * @param handler the handler used in on, or an object with the consumer tag + */ + off(eventName: string, handler: Function | { + consumerTag?: string; + }): any; + /** Close shovel and cancel its source consumer */ + close(): void; + private _messageHandler; + private _onShovelMessage; + readonly name: string; + readonly closed: boolean; + readonly consumerTag: string; + } + /** + * Exchange-to-exchange shovel wrapper, returned by `broker.bindExchange` + * @param shovel underlying shovel + */ + function Exchange2Exchange(shovel: Shovel): void; + class Exchange2Exchange { + /** + * Exchange-to-exchange shovel wrapper, returned by `broker.bindExchange` + * @param shovel underlying shovel + */ + constructor(shovel: Shovel); + /** + * Subscribe to underlying shovel events + * @param eventName event name (without `shovel.` prefix) + * @param handler event handler + */ + on(eventName: string, handler: Function): any; + /** Close the underlying shovel */ + close(): any; + readonly name: string; + readonly source: string; + readonly destination: string; + readonly pattern: string; + readonly queue: string; + readonly consumerTag: string; + } + /** + * Exchange + * @param name required exchange name + * @param type optional type, defaults to topic + * @param options optional exchange options + */ + export function Exchange(name: string, type?: exchangeType, options?: ExchangeOptions): ExchangeBase; + /** + * Exchange + * @param name name + * + */ + function ExchangeBase(name: string, type: exchangeType, options?: ExchangeOptions, eventExchange?: ExchangeBase): void; + class ExchangeBase { + /** + * Exchange + * @param name name + * + */ + constructor(name: string, type: exchangeType, options?: ExchangeOptions, eventExchange?: ExchangeBase); + + options: ExchangeOptions; + events: ExchangeBase | undefined; + /** + * Publish a message through the exchange + * @param routingKey routing key + * @param content message content + * @param properties optional message properties + */ + publish(routingKey: string, content?: any, properties?: MessageProperties): any; + private _onTopicMessage; + private _onDirectMessage; + private _emitReturn; + /** + * Bind a queue to this exchange with a routing key pattern + * @param queue queue to bind + * @param pattern routing key pattern + * @param bindOptions optional binding options + */ + bindQueue(queue: Queue, pattern: string, bindOptions?: BindingOptions): any; + /** + * Unbind a queue from this exchange + * @param queue queue previously bound + * @param pattern routing key pattern + */ + unbindQueue(queue: Queue, pattern: string): void; + /** + * Unbind every binding pointing at the named queue + * @param queueName queue name + */ + unbindQueueByName(queueName: string): void; + close(): void; + getState(): { + bindings?: { + id: string; + options: { + priority: number; + }; + queueName: string; + pattern: string; + }[] | undefined; + deliveryQueue?: { + name: string; + options: QueueOptions; + messages?: MessageEnvelope[]; + } | undefined; + name: string; + type: exchangeType; + options: { + [x: string]: any; + durable?: boolean; + autoDelete?: boolean; + }; + }; + stop(): void; + /** + * Recover exchange from previously captured state + * @param state exchange state, omit to recover stopped exchange in place + * @param getQueue callback to resolve a queue by name (used during binding restore) + */ + recover(state?: ExchangeState, getQueue?: (name: string) => Queue): this | undefined; + /** + * Find a binding by queue name and pattern + * @param queueName queue name + * @param pattern routing key pattern + */ + getBinding(queueName: string, pattern: string): any; + /** + * Emit an exchange event (or, if no event sub-exchange, publish on the exchange itself) + * @param eventName event name (without `exchange.` prefix) + * @param content event payload + */ + emit(eventName: string, content?: any): any; + /** + * Subscribe to an exchange event + * @param pattern event name pattern (without `exchange.` prefix) + * @param handler event handler + * @param consumeOptions optional consume options + */ + on(pattern: string, handler: onMessage, consumeOptions?: ConsumeOptions): any; + /** + * Unsubscribe from an exchange event + * @param pattern event name pattern previously passed to on + * @param handler the handler used in on, or an object with the consumer tag + */ + off(pattern: string, handler: onMessage | { + consumerTag?: string; + }): any; + /** + * Remove a single binding from this exchange + * @param binding binding to close + */ + closeBinding(binding: Binding): void; + readonly name: string; + readonly type: exchangeType; + readonly bindingCount: number; + readonly bindings: Binding[]; + readonly stopped: boolean; + readonly undeliveredCount: number; + } + export class SmqpError extends Error { + constructor(message: any, code: any); + type: string; + code: any; + } + export const ERR_CONSUMER_TAG_CONFLICT: "ERR_SMQP_CONSUMER_TAG_CONFLICT"; + export const ERR_EXCHANGE_TYPE_MISMATCH: "ERR_SMQP_EXCHANGE_TYPE_MISMATCH"; + export const ERR_EXCLUSIVE_CONFLICT: "ERR_SMQP_EXCLUSIVE_CONFLICT"; + export const ERR_EXCLUSIVE_NOT_ALLOWED: "ERR_SMQP_EXCLUSIVE_NOT_ALLOWED"; + export const ERR_QUEUE_DURABLE_MISMATCH: "ERR_SMQP_QUEUE_DURABLE_MISMATCH"; + export const ERR_QUEUE_NAME_CONFLICT: "ERR_SMQP_QUEUE_NAME_CONFLICT"; + export const ERR_QUEUE_NOT_FOUND: "ERR_SMQP_QUEUE_NOT_FOUND"; + export const ERR_SHOVEL_DESTINATION_EXCHANGE_NOT_FOUND: "ERR_SMQP_SHOVEL_DESTINATION_EXCHANGE_NOT_FOUND"; + export const ERR_SHOVEL_NAME_CONFLICT: "ERR_SMQP_SHOVEL_NAME_CONFLICT"; + export const ERR_SHOVEL_SOURCE_EXCHANGE_NOT_FOUND: "ERR_SMQP_SHOVEL_SOURCE_EXCHANGE_NOT_FOUND"; + /** + * Get routing key pattern + * @param pattern routing key pattern + * */ + export function getRoutingKeyPattern(pattern: string): RoutingKeyPattern; + /** + * Get routing key pattern + */ + type RoutingKeyPattern = { + /** + * method to test a routing key against the pattern; receiver-bound — destructuring is unsupported + */ + test: (this: RoutingKeyPattern, routingKey: string) => boolean; + }; + /** + * Smqp message broker + * @param owner optional broker owner, forwarded to message consumer + */ + export default function Broker_1(owner?: any): Broker_1 | undefined; + export default class Broker_1 { + /** + * Smqp message broker + * @param owner optional broker owner, forwarded to message consumer + */ + constructor(owner?: any); + owner: any; + events: ExchangeEventEmitter; + /** + * Subscribe to exchange via queue + * @param exchangeName exhange name + * @param pattern routing key pattern + * @param queueName queue name + * @param onMessage message handlers + * @param options optional subscribe options + */ + subscribe(exchangeName: string, pattern: string, queueName: string, onMessage: onMessage, options?: SubscribeOptions): Consumer; + /** + * Subscribe to exchange via temporary, non-durable queue + * @param exchangeName exchange name + * @param pattern routing key pattern + * @param onMessage message handler + * @param options optional subscribe options + */ + subscribeTmp(exchangeName: string, pattern: string, onMessage: onMessage, options?: SubscribeOptions): Consumer; + /** + * Subscribe once to first matching message, then auto-cancel. + * + * Only `consumerTag` and `priority` from `options` are honored. `noAck`, `autoDelete`, + * and `durable` are forced internally; queue-lifecycle and dead-letter options are ignored + * because the temporary queue is deleted after the first delivery. + * + * @param exchangeName exchange name + * @param pattern routing key pattern + * @param onMessage message handler + * @param options optional subscribe options + */ + subscribeOnce(exchangeName: string, pattern: string, onMessage: onMessage, options?: SubscribeOptions): Consumer; + /** + * Cancel consumer matching queue + handler + * @param queueName queue name + * @param onMessage handler previously passed to subscribe + */ + unsubscribe(queueName: string, onMessage: onMessage): void; + /** + * Assert exchange exists, create if absent + * @param exchangeName exchange name + * @param type exchange type, defaults to topic + * @param options optional exchange options + */ + assertExchange(exchangeName: string, type?: exchangeType, options?: ExchangeOptions): ExchangeBase | undefined; + /** + * Bind queue to exchange with routing key pattern + * @param queueName queue name + * @param exchangeName exchange name + * @param pattern routing key pattern + * @param bindOptions optional binding options + */ + bindQueue(queueName: string, exchangeName: string, pattern: string, bindOptions?: BindingOptions): any; + /** + * Unbind queue from exchange + * @param queueName queue name + * @param exchangeName exchange name + * @param pattern routing key pattern + */ + unbindQueue(queueName: string, exchangeName: string, pattern: string): void; + /** + * Add consumer to queue + * @param queueName queue name + * @param onMessage message handler + * @param options optional consume options + */ + consume(queueName: string, onMessage: onMessage, options?: ConsumeOptions): Consumer; + /** + * Cancel consumer by tag + * @param consumerTag consumer tag + * @param requeue requeue messages held by the consumer, defaults to true + */ + cancel(consumerTag: string, requeue?: boolean): boolean; + /** List all consumers as serializable projections */ + getConsumers(): { + queue: string; + consumerTag: string; + ready: boolean; + options: { + noAck: boolean; + consumerTag?: string; + exclusive?: boolean; + prefetch: number; + priority: number; + }; + }[]; + /** + * Get consumer by tag + * @param consumerTag consumer tag + * */ + getConsumer(consumerTag: string): Consumer | undefined; + /** + * Get exchange by name + * @param exchangeName exchange name + * */ + getExchange(exchangeName: string): ExchangeBase | undefined; + /** + * Delete exchange + * @param exchangeName exchange name + * @param options only delete if no bindings remain + */ + deleteExchange(exchangeName: string, options?: { + ifUnused?: boolean; + }): boolean; + /** + * Stop broker with corresponding exchanges and queues, entities remain but does not accepts messages + */ + stop(): void; + /** + * Close and clean-up all entities + */ + close(): void; + /** + * Danger! Resets all entities, stop, close and delete + */ + reset(): void; + /** + * Get broker state for persistence + * @param onlyWithContent omit exchanges and queues without content + */ + getState(onlyWithContent?: boolean): { + exchanges: { + bindings?: { + id: string; + options: { + priority: number; + }; + queueName: string; + pattern: string; + }[] | undefined; + deliveryQueue?: { + name: string; + options: QueueOptions; + messages?: MessageEnvelope[]; + } | undefined; + name: string; + type: exchangeType; + options: { + [x: string]: any; + durable?: boolean; + autoDelete?: boolean; + }; + }[] | undefined; + queues: { + name: string; + options: QueueOptions; + messages?: MessageEnvelope[]; + }[] | undefined; + } | undefined; + /** + * Recover broker from previously captured state + * @param state broker state, omit to recover stopped entities in place + */ + recover(state?: BrokerState): this; + /** + * Bind one exchange to another via internal shovel + * @param source source exchange name + * @param destination destination exchange name + * @param pattern routing key pattern, defaults to # + * @param args optional shovel options + */ + bindExchange(source: string, destination: string, pattern?: string, args?: ShovelOptions): Exchange2Exchange; + /** + * Unbind exchange-to-exchange shovel + * @param source source exchange name + * @param destination destination exchange name + * @param pattern routing key pattern, defaults to # + */ + unbindExchange(source: string, destination: string, pattern?: string): boolean; + /** + * Publish a message to an exchange + * @param exchangeName exchange name + * @param routingKey routing key + * @param content message content + * @param properties optional message properties + */ + publish(exchangeName: string, routingKey: string, content?: any, properties?: MessageProperties): any; + /** + * Purge all non-pending messages from queue + * @param queueName queue name + */ + purgeQueue(queueName: string): number | undefined; + /** + * Send content directly to a queue, bypassing exchanges + * @param queueName queue name + * @param content message content + * @param options optional message properties + */ + sendToQueue(queueName: string, content: any, options?: MessageProperties): number | undefined; + private _getQueuesState; + private _getExchangeState; + /** + * Create queue + * @param queueName queue name, defaults to a generated name + * @param options optional queue options + */ + createQueue(queueName?: string, options?: QueueOptions): Queue; + /** + * Get queue by name + * @param queueName queue name + * */ + getQueue(queueName: string): Queue | undefined; + /** + * Assert queue exists, create if absent + * @param queueName queue name, defaults to a generated name + * @param options optional queue options + */ + assertQueue(queueName?: string, options?: QueueOptions): Queue; + /** + * Delete queue + * @param queueName queue name + * @param options optional delete guards + */ + deleteQueue(queueName: string, options?: DeleteQueueOptions): { + messageCount: number; + } | undefined; + /** + * Get one message from queue + * @param queueName queue name + * @param options optional consume options + */ + get(queueName: string, options?: ConsumeOptions): any; + /** + * Acknowledge message + * @param message message to ack + * @param allUpTo ack all messages up to and including this one + */ + ack(message: Message, allUpTo?: boolean): void; + /** Acknowledge all outstanding messages across all queues */ + ackAll(): void; + /** + * Reject message + * @param message message to nack + * @param allUpTo nack all messages up to and including this one + * @param requeue requeue nacked messages, defaults to true + */ + nack(message: Message, allUpTo?: boolean, requeue?: boolean): void; + /** + * Reject all outstanding messages across all queues + * @param requeue requeue nacked messages, defaults to true + */ + nackAll(requeue?: boolean): void; + /** + * Reject message + * @param message message to reject + * @param requeue requeue rejected message, defaults to true + */ + reject(message: Message, requeue?: boolean): void; + /** + * Validate that a consumer tag is unused; throws if occupied + * @param consumerTag consumer tag to validate + */ + validateConsumerTag(consumerTag: string): any; + /** + * Create shovel between source and destination exchanges + * @param name unique shovel name + * @param source source spec + * @param destination destination spec + * @param options optional shovel options + */ + createShovel(name: string, source: ShovelSource, destination: ShovelDestination, options?: ShovelOptions): Shovel; + /** + * Close shovel by name + * @param name shovel name + */ + closeShovel(name: string): boolean; + /** + * Get shovel by name + * @param name shovel name + * */ + getShovel(name: string): Shovel | undefined; + /** + * List all shovels + * */ + getShovels(): Shovel[]; + /** + * Subscribe to broker event + * @param eventName event name pattern + * @param callback event callback + * @param options optional consume options + */ + on(eventName: string, callback: (event: { + name: string; + } & Record) => void, options?: ConsumeOptions): any; + /** + * Unsubscribe from broker event + * @param eventName event name previously passed to on + * @param callbackOrObject the callback used in on, or an object with the consumer tag + */ + off(eventName: string, callbackOrObject: Function | { + consumerTag?: string; + }): void; + prefetch(): void; + readonly exchangeCount: number; + readonly queueCount: number; + readonly consumerCount: number; + } + /** + * Queue + * @param name optional, but recommended queue name, defaults to `smq.qname-` + * @param options queue options + * @param eventEmitter optional event emitter + */ + export function Queue(name?: string, options?: QueueOptions, eventEmitter?: ExchangeEventEmitter): void; + export class Queue { + /** + * Queue + * @param name optional, but recommended queue name, defaults to `smq.qname-` + * @param options queue options + * @param eventEmitter optional event emitter + */ + constructor(name?: string, options?: QueueOptions, eventEmitter?: ExchangeEventEmitter); + + options: QueueOptions; + + messages: Message[]; + events: ExchangeEventEmitter | undefined; + + private _onMessageConsumed; + /** + * Enqueue a message + * @param fields message fields + * @param content message content + * @param properties message properties + */ + queueMessage(fields: MessageFields, content?: any, properties?: MessageProperties): number | undefined; + /** + * Evict first non-pending message; returns true if it was the supplied message + * @param compareMessage message to compare against the evicted one + */ + evictFirst(compareMessage?: Message): boolean | undefined; + private _consumeNext; + /** + * Add a consumer + * @param onMessage message handler + * @param consumeOptions optional consume options + * @param owner forwarded to the message handler as the third arg + */ + consume(onMessage: onMessage, consumeOptions?: ConsumeOptions, owner?: any): Consumer; + /** + * Assert consumer matching handler + options exists, create if absent + * @param onMessage message handler + * @param consumeOptions optional consume options + * @param owner forwarded to the message handler as the third arg + */ + assertConsumer(onMessage: onMessage, consumeOptions?: ConsumeOptions, owner?: any): Consumer; + /** + * Get next message from queue + * @param options optional consume options + */ + get(options?: ConsumeOptions): any; + private _consumeMessages; + /** + * Acknowledge message + * @param message message to ack + * @param allUpTo ack all messages up to and including this one + */ + ack(message: Message, allUpTo?: boolean): void; + /** + * Reject message + * @param message message to nack + * @param allUpTo nack all messages up to and including this one + * @param requeue requeue nacked message(s), defaults to true + */ + nack(message: Message, allUpTo?: boolean, requeue?: boolean): void; + /** + * Reject message + * @param message message to reject + * @param requeue requeue rejected message, defaults to true + */ + reject(message: Message, requeue?: boolean): void; + ackAll(): void; + /** + * Reject all pending messages + * @param requeue requeue nacked messages, defaults to true + */ + nackAll(requeue?: boolean): void; + private _getPendingMessages; + /** + * Peek at the next message without consuming it + * @param ignoreDelivered skip pending messages + */ + peek(ignoreDelivered?: boolean): Message | undefined; + /** + * Cancel consumer by tag + * @param consumerTag consumer tag + * @param requeue requeue messages held by the consumer, defaults to true + */ + cancel(consumerTag: string, requeue?: boolean): boolean; + /** + * Cancel consumer matching the given handler + * @param onMessage handler previously passed to consume + * @param requeue requeue messages held by the consumer, defaults to true + */ + dismiss(onMessage: onMessage, requeue?: boolean): void; + /** + * Unbind consumer from queue + * @param consumer consumer to unbind + * @param requeue requeue messages held by the consumer, defaults to true + */ + unbindConsumer(consumer: Consumer, requeue?: boolean): void; + /** + * Emit a queue event + * @param eventName event name (without `queue.` prefix) + * @param content event payload + */ + emit(eventName: string, content?: any): void; + /** + * Subscribe to a queue event + * @param eventName event name (without `queue.` prefix); accepts known names or a routing pattern + * @param handler event handler + * @param options optional consume options + */ + on(eventName: QueueEventNames | string, handler: Function, options?: ConsumeOptions): any; + /** + * Unsubscribe from a queue event + * @param eventName event name previously passed to on + * @param handler the handler used in on, or an object with the consumer tag + */ + off(eventName: QueueEventNames | string, handler: Function | { + consumerTag?: string; + }): any; + purge(): number; + private _dequeueMessage; + getState(): { + name: string; + options: QueueOptions; + messages?: MessageEnvelope[]; + }; + /** + * Recover queue from previously captured state + * @param state queue state, omit to recover stopped queue in place + */ + recover(state?: QueueState): this; + /** + * Delete queue + * @param options optional delete guards + */ + delete(options?: DeleteQueueOptions): { + messageCount: number; + } | undefined; + close(): void; + stop(): void; + private _getCapacity; + readonly name: string; + readonly consumerCount: number; + readonly consumers: Consumer[]; + readonly exclusive: boolean; + readonly messageCount: number; + readonly stopped: boolean; + } + /** + * Queue consumer + * @param queue queue this consumer reads from + * @param onMessage message handler + * @param options consume options + * @param owner forwarded to the message handler as the third arg + * @param eventEmitter internal queue event bridge + */ + export function Consumer(queue: Queue, onMessage: onMessage, options?: ConsumeOptions, owner?: any, eventEmitter?: ExchangeEventEmitter): void; + export class Consumer { + /** + * Queue consumer + * @param queue queue this consumer reads from + * @param onMessage message handler + * @param options consume options + * @param owner forwarded to the message handler as the third arg + * @param eventEmitter internal queue event bridge + */ + constructor(queue: Queue, onMessage: onMessage, options?: ConsumeOptions, owner?: any, eventEmitter?: ExchangeEventEmitter); + options: { + noAck: boolean; + consumerTag?: string; + exclusive?: boolean; + prefetch: number; + priority: number; + }; + queue: Queue; + onMessage: onMessage; + owner: any; + events: ExchangeEventEmitter | undefined; + /** Project consumer state for serialization (used by `Broker.getConsumers` and `JSON.stringify`) */ + toJSON(): { + queue: string; + consumerTag: string; + ready: boolean; + options: { + noAck: boolean; + consumerTag?: string; + exclusive?: boolean; + prefetch: number; + priority: number; + }; + }; + private _push; + private _consume; + /** + * Reject all messages held by this consumer + * @param requeue requeue nacked messages, defaults to true + */ + nackAll(requeue?: boolean): void; + /** Acknowledge all messages held by this consumer */ + ackAll(): void; + /** + * Cancel consumer + * @param requeue requeue messages held by the consumer, defaults to true + */ + cancel(requeue?: boolean): void; + /** + * Set consumer prefetch count + * @param value new prefetch count + */ + prefetch(value: number): void; + /** + * Emit consumer event + * @param eventName event name (without `consumer.` prefix) + * @param content event payload + */ + emit(eventName: string, content?: any): void; + /** + * Subscribe to consumer event + * @param eventName event name (without `consumer.` prefix) + * @param handler event handler + */ + on(eventName: string, handler: Function): any; + recover(): void; + stop(): void; + readonly consumerTag: string; + readonly ready: boolean; + readonly stopped: boolean; + readonly capacity: number; + readonly messageCount: number; + readonly queueName: string; + } + type onMessage = (routingKey: string, message: Message, owner: any) => void; + + /** + * Minimal event-emitter shape used as the `eventEmitter` argument of `Queue`, `Consumer`, and `Shovel`. + * `ExchangeBase` and `EventExchange` instances satisfy this structurally; the `Queue` constructor only + * needs `emit`/`on`/`off`, so this narrows the type away from the full `ExchangeBase` surface. + */ + interface ExchangeEventEmitter { + emit(eventName: string, content?: any): any; + on(pattern: string, handler: Function, options?: ConsumeOptions): any; + off(pattern: string, handler: Function): any; + } + + type exchangeType = 'topic' | 'direct'; + + interface ConsumeOptions { + /** set to true if there is no need to acknowledge message, message is immediately consumed */ + noAck?: boolean; + /** unique consumer tag */ + consumerTag?: string; + /** queue is exclusively consumed */ + exclusive?: boolean; + /** defaults to 1, number of messages to consume at a time */ + prefetch?: number; + /** defaults to 0, higher value gets messages first */ + priority?: number; + [x: string]: any; + } + + interface SubscribeOptions extends ConsumeOptions { + /** defaults to true, exchange will be deleted when all bindings are removed; the queue will be removed when all consumers are down */ + autoDelete?: boolean; + /** defaults to true, makes exchange and queue durable, i.e. will be returned when getting state */ + durable?: boolean; + /** dead letter exchange */ + deadLetterExchange?: string; + /** publish dead letter with routing key */ + deadLetterRoutingKey?: string; + } + + interface QueueOptions { + /** remove queue when last consumer leaves, defaults to true */ + autoDelete?: boolean; + /** makes queue durable, i.e. will be returned when getting state */ + durable?: boolean; + messageTtl?: number; + maxLength?: number; + deadLetterExchange?: string; + deadLetterRoutingKey?: string; + [x: string]: any; + } + + interface DeleteQueueOptions { + ifUnused?: boolean; + ifEmpty?: boolean; + } + + type QueueEventNames = + /** consumer was cancelled */ + | 'consumer.cancel' + /** consumer was added */ + | 'consume' + /** message was dead-lettered, payload includes `deadLetterExchange` name and message */ + | 'dead-letter' + /** queue was deleted */ + | 'delete' + /** queue is depleted */ + | 'depleted' + /** message was queued */ + | 'message' + /** queue is ready to receive new messages */ + | 'ready' + /** queue is saturated, i.e. max capacity was reached */ + | 'saturated'; + + interface ExchangeOptions { + /** makes exchange durable, i.e. will be returned when getting state, defaults to true */ + durable?: boolean; + /** remove exchange when all bindings are gone, defaults to true */ + autoDelete?: boolean; + [x: string]: any; + } + + interface BindingOptions { + priority?: number; + [x: string]: any; + } + + interface BindingState { + id: string; + options: BindingOptions; + queueName: string; + pattern: string; + } + + interface QueueState { + name: string; + options: QueueOptions; + messages?: MessageEnvelope_1[]; + } + + interface ExchangeState { + name: string; + type: exchangeType; + options: ExchangeOptions; + bindings?: BindingState[]; + /** undelivered message queue */ + deliveryQueue?: QueueState; + } + + interface BrokerState { + exchanges?: ExchangeState[]; + queues?: QueueState[]; + } + + interface MessageFields extends Record { + /** published through exchange */ + exchange?: string; + /** published with routing key, if any */ + routingKey?: string; + /** identifying the consumer for which the message is destined */ + consumerTag?: string; + /** message has been redelivered, i.e. nacked or recovered */ + redelivered?: boolean; + } + + interface MessageProperties extends Record { + /** unique identifier for the message */ + messageId?: string; + /** integer, expire message after milliseconds */ + expiration?: number; + /** integer, message time to live in milliseconds */ + ttl?: number; + /** Date.now() when message was sent */ + timestamp?: number; + /** indicating if message is mandatory. True emits return if not routed to any queue */ + mandatory?: boolean; + /** persist message, if unset queue option durable prevails */ + persistent?: boolean; + /** shovel or e2e message source exchange */ + 'source-exchange'?: string; + /** shovel name */ + 'shovel-name'?: string; + } + + interface MessageEnvelope_1 { + fields: MessageFields; + content?: any; + properties: MessageProperties; + } + + interface ShovelOptions { + cloneMessage?: (message: MessageEnvelope_1) => MessageEnvelope_1; + [x: string]: any; + } + + interface ShovelSource { + /** source broker */ + broker: Broker_1; + /** source exchange name */ + exchange: string; + pattern?: string; + priority?: number; + queue?: string; + consumerTag?: string; + } + + interface ShovelDestination { + /** destination broker */ + broker: Broker_1; + /** destination exchange */ + exchange: string; + /** optional destination exchange routing key, defaults to original message's routing key */ + exchangeKey?: string; + /** optional object with message properties to overwrite when shovelling messages */ + publishProperties?: Record; + } + /** + * + * @param pattern message routing key pattern + * + */ + function Binding(exchange: ExchangeBase, queue: Queue, pattern: string, bindOptions?: BindingOptions): void; + class Binding { + /** + * + * @param pattern message routing key pattern + * + */ + constructor(exchange: ExchangeBase, queue: Queue, pattern: string, bindOptions?: BindingOptions); + id: string; + options: { + priority: number; + }; + pattern: string; + exchange: ExchangeBase; + queue: Queue; + + _compiledPattern: { + test(routingKey: string): boolean; + }; + /** + * Test routing key against pattern + * @param routingKey message routing key + */ + testPattern(routingKey: string): boolean; + /** + * Close binding + */ + close(): void; + /** + * Get binding state + */ + getState(): { + id: string; + options: { + priority: number; + }; + queueName: string; + pattern: string; + }; + } + + export { Broker_1 as Broker }; +} + +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/types/interfaces.d.ts b/types/interfaces.d.ts new file mode 100644 index 0000000..d1159dc --- /dev/null +++ b/types/interfaces.d.ts @@ -0,0 +1,231 @@ +export type onMessage = (routingKey: string, message: import('../src/Message.js').Message, owner: any) => void; + +/** + * Minimal event-emitter shape used as the `eventEmitter` argument of `Queue`, `Consumer`, and `Shovel`. + * `ExchangeBase` and `EventExchange` instances satisfy this structurally; the `Queue` constructor only + * needs `emit`/`on`/`off`, so this narrows the type away from the full `ExchangeBase` surface. + */ +export interface ExchangeEventEmitter { + emit(eventName: string, content?: any): any; + on(pattern: string, handler: Function, options?: ConsumeOptions): any; + off(pattern: string, handler: Function): any; +} + +export type exchangeType = 'topic' | 'direct'; + +export interface ConsumeOptions { + /** set to true if there is no need to acknowledge message, message is immediately consumed */ + noAck?: boolean; + /** unique consumer tag */ + consumerTag?: string; + /** queue is exclusively consumed */ + exclusive?: boolean; + /** defaults to 1, number of messages to consume at a time */ + prefetch?: number; + /** defaults to 0, higher value gets messages first */ + priority?: number; + [x: string]: any; +} + +export interface SubscribeOptions extends ConsumeOptions { + /** defaults to true, exchange will be deleted when all bindings are removed; the queue will be removed when all consumers are down */ + autoDelete?: boolean; + /** defaults to true, makes exchange and queue durable, i.e. will be returned when getting state */ + durable?: boolean; + /** dead letter exchange */ + deadLetterExchange?: string; + /** publish dead letter with routing key */ + deadLetterRoutingKey?: string; +} + +export interface QueueOptions { + /** remove queue when last consumer leaves, defaults to true */ + autoDelete?: boolean; + /** makes queue durable, i.e. will be returned when getting state */ + durable?: boolean; + messageTtl?: number; + maxLength?: number; + deadLetterExchange?: string; + deadLetterRoutingKey?: string; + [x: string]: any; +} + +export interface DeleteQueueOptions { + ifUnused?: boolean; + ifEmpty?: boolean; +} + +export type QueueEventNames = + /** consumer was cancelled */ + | 'consumer.cancel' + /** consumer was added */ + | 'consume' + /** message was dead-lettered, payload includes `deadLetterExchange` name and message */ + | 'dead-letter' + /** queue was deleted */ + | 'delete' + /** queue is depleted */ + | 'depleted' + /** message was queued */ + | 'message' + /** queue is ready to receive new messages */ + | 'ready' + /** queue is saturated, i.e. max capacity was reached */ + | 'saturated'; + +export interface ExchangeOptions { + /** makes exchange durable, i.e. will be returned when getting state, defaults to true */ + durable?: boolean; + /** remove exchange when all bindings are gone, defaults to true */ + autoDelete?: boolean; + [x: string]: any; +} + +export interface BindingOptions { + priority?: number; + [x: string]: any; +} + +export interface BindingState { + id: string; + options: BindingOptions; + queueName: string; + pattern: string; +} + +export interface QueueState { + name: string; + options: QueueOptions; + messages?: MessageEnvelope[]; +} + +export interface ExchangeState { + name: string; + type: exchangeType; + options: ExchangeOptions; + bindings?: BindingState[]; + /** undelivered message queue */ + deliveryQueue?: QueueState; +} + +export interface BrokerState { + exchanges?: ExchangeState[]; + queues?: QueueState[]; +} + +export interface MessageFields extends Record { + /** published through exchange */ + exchange?: string; + /** published with routing key, if any */ + routingKey?: string; + /** identifying the consumer for which the message is destined */ + consumerTag?: string; + /** message has been redelivered, i.e. nacked or recovered */ + redelivered?: boolean; +} + +export interface MessageProperties extends Record { + /** unique identifier for the message */ + messageId?: string; + /** integer, expire message after milliseconds */ + expiration?: number; + /** integer, message time to live in milliseconds */ + ttl?: number; + /** Date.now() when message was sent */ + timestamp?: number; + /** indicating if message is mandatory. True emits return if not routed to any queue */ + mandatory?: boolean; + /** persist message, if unset queue option durable prevails */ + persistent?: boolean; + /** shovel or e2e message source exchange */ + 'source-exchange'?: string; + /** shovel name */ + 'shovel-name'?: string; +} + +export interface MessageEnvelope { + fields: MessageFields; + content?: any; + properties: MessageProperties; +} + +export interface ShovelOptions { + cloneMessage?: (message: MessageEnvelope) => MessageEnvelope; + [x: string]: any; +} + +export interface ShovelSource { + /** source broker */ + broker: import('../src/Broker.js').Broker; + /** source exchange name */ + exchange: string; + pattern?: string; + priority?: number; + queue?: string; + consumerTag?: string; +} + +export interface ShovelDestination { + /** destination broker */ + broker: import('../src/Broker.js').Broker; + /** destination exchange */ + exchange: string; + /** optional destination exchange routing key, defaults to original message's routing key */ + exchangeKey?: string; + /** optional object with message properties to overwrite when shovelling messages */ + publishProperties?: Record; +} + +declare module '../src/Broker.js' { + interface Broker { + readonly exchangeCount: number; + readonly queueCount: number; + readonly consumerCount: number; + } +} + +declare module '../src/Exchange.js' { + interface ExchangeBase { + readonly name: string; + readonly type: import('#types').exchangeType; + readonly bindingCount: number; + readonly bindings: import('../src/Binding.js').Binding[]; + readonly stopped: boolean; + readonly undeliveredCount: number; + } +} + +declare module '../src/Queue.js' { + interface Queue { + readonly name: string; + readonly consumerCount: number; + readonly consumers: Consumer[]; + readonly exclusive: boolean; + readonly messageCount: number; + readonly stopped: boolean; + } + interface Consumer { + readonly consumerTag: string; + readonly ready: boolean; + readonly stopped: boolean; + readonly capacity: number; + readonly messageCount: number; + readonly queueName: string; + } +} + +declare module '../src/Shovel.js' { + interface Shovel { + readonly name: string; + readonly closed: boolean; + readonly consumerTag: string; + } + interface Exchange2Exchange { + readonly name: string; + readonly source: string; + readonly destination: string; + readonly pattern: string; + readonly queue: string; + readonly consumerTag: string; + } +} diff --git a/types/shared.d.ts b/types/shared.d.ts deleted file mode 100644 index a8da153..0000000 --- a/types/shared.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -interface RoutingKeyPattern { - test(routingKey: string): boolean; -} - -export function generateId(): string; -export function getRoutingKeyPattern(pattern: string): RegExp | RoutingKeyPattern; diff --git a/types/types.d.ts b/types/types.d.ts deleted file mode 100644 index 0516cc7..0000000 --- a/types/types.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Message } from './Message.js'; -import { Consumer } from './Queue.js'; - -export type onMessage = (routingKey: string, message: Message, owner: any) => void; - -export interface ConsumeOptions { - noAck?: boolean; - consumerTag?: string; - exclusive?: boolean; - prefetch?: number; - priority?: number; - [x: string]: any; -} - -export abstract class ExchangeEventEmitter { - emit(eventName: string, content?: any): number | undefined; - on(pattern: string, handler: CallableFunction, consumeOptions?: ConsumeOptions): Consumer; - off(pattern: string, handler: CallableFunction | ConsumeOptions): void; -}