From f802c4dc446bdaa2de66eeb1b06a061096e812cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Edman?= Date: Thu, 22 Jan 2026 06:51:03 +0100 Subject: [PATCH 1/8] bump checkout github action --- .github/workflows/build.yaml | 2 +- .github/workflows/release.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 6b9fc77..5469c23 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -15,7 +15,7 @@ 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: diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 5cb1490..f0e297b 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: From c6c01868284c49fe0ccb973d7440feb27938fa9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Edman?= Date: Tue, 28 Apr 2026 06:14:14 +0200 Subject: [PATCH 2/8] bump dev deps --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index a63775f..e8e003b 100644 --- a/package.json +++ b/package.json @@ -63,11 +63,11 @@ "@babel/preset-env": "^7.24.4", "c8": "^10.1.2", "chai": "^6.2.0", - "chronokinesis": "^7.0.0", + "chronokinesis": "^8.0.0", "eslint": "^9.10.0", "markdown-toc": "^1.2.0", "mocha": "^11.0.1", "prettier": "^3.2.5", - "texample": "^0.1.0" + "texample": "^1.0.0" } } From dd45865b22a2464d7a17afcb45e69dba72b08d02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Edman?= Date: Wed, 29 Apr 2026 05:49:24 +0200 Subject: [PATCH 3/8] decorate broker --- .gitignore | 7 + .prettierignore | 2 + dist/Broker.js | 259 ++++++++++++++- dist/Exchange.js | 41 +++ dist/Queue.js | 7 + package.json | 9 +- scripts/build-types.js | 23 ++ src/Broker.js | 217 ++++++++++++- src/Exchange.js | 34 ++ src/Queue.js | 6 + test/types-test.js | 110 +++++++ tsconfig.json | 6 +- types/index.d.ts | 708 ++++++++++++++++++++++++++++++++++++++++- types/interfaces.d.ts | 204 ++++++++++++ 14 files changed, 1611 insertions(+), 22 deletions(-) create mode 100644 scripts/build-types.js create mode 100644 test/types-test.js create mode 100644 types/interfaces.d.ts diff --git a/.gitignore b/.gitignore index b80821b..af42f6d 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,10 @@ package-lock.json .nyc_output *.log + +# ts +*.d.ts.map + +# AI +.claude +AGENTS.md 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/dist/Broker.js b/dist/Broker.js index 3174865..5b0baf3 100644 --- a/dist/Broker.js +++ b/dist/Broker.js @@ -11,11 +11,17 @@ var _shared = require("./shared.js"); var _Errors = require("./Errors.js"); const kEntities = Symbol.for('entities'); const kEventHandler = Symbol.for('eventHandler'); + +/** + * Smqp message broker + * @param {any} [owner] optional broker owner, forwarded to message consumer + */ function Broker(owner) { if (!(this instanceof Broker)) { return new Broker(owner); } this.owner = owner; + /** @type {import('./Exchange.js').ExchangeBase} */ 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); @@ -37,6 +43,15 @@ 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'); if (options?.consumerTag) this.validateConsumerTag(options.consumerTag); @@ -49,12 +64,33 @@ Broker.prototype.subscribe = function subscribe(exchangeName, pattern, queueName exchange.bindQueue(queue, pattern, queueOptions); 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); @@ -75,11 +111,24 @@ Broker.prototype.subscribeOnce = function subscribeOnce(exchangeName, pattern, o onMessage(...args); } }; + +/** + * 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) { @@ -91,11 +140,26 @@ Broker.prototype.assertExchange = function assertExchange(exchangeName, type, op this[kEntities].get('exchanges').set(exchangeName, exchange); 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; @@ -103,11 +167,24 @@ Broker.prototype.unbindQueue = function unbindQueue(queueName, exchangeName, pat if (!queue) return; 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 _Errors.SmqpError(`Queue with name <${queueName}> was not found`, _Errors.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; @@ -128,14 +205,30 @@ Broker.prototype.getConsumers = function getConsumers() { } return result; }; + +/** + * Get consumer by tag + * @param {string} consumerTag consumer tag + */ 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 + */ 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; @@ -143,17 +236,29 @@ Broker.prototype.deleteExchange = function deleteExchange(exchangeName, options) exchange.close(); 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(); for (const exchange of entities.get('exchanges').values()) exchange.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(); @@ -163,6 +268,11 @@ Broker.prototype.reset = function reset() { entities.get('consumers').clear(); 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); @@ -172,6 +282,11 @@ Broker.prototype.getState = function getState(onlyWithContent) { queues }; }; + +/** + * 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) { @@ -190,6 +305,14 @@ 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(name, { @@ -206,28 +329,61 @@ Broker.prototype.bindExchange = function bindExchange(source, destination, patte }); return new _Shovel.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 _Errors.SmqpError(`Queue with name <${queueName}> was not found`, _Errors.ERR_QUEUE_NOT_FOUND); return queue.queueMessage({}, content, options); }; + +/** + * @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 = []; @@ -235,9 +391,15 @@ Broker.prototype._getQueuesState = function getQueuesState(onlyWithContent) { } return result; }; + +/** + * @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 = []; @@ -245,6 +407,12 @@ 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-${(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`); @@ -253,10 +421,21 @@ Broker.prototype.createQueue = function createQueue(queueName, options) { this[kEntities].get('queues').set(queueName, queue); return queue; }; + +/** + * Get queue by name + * @param {string} queueName queue name + */ 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); const queue = this.getQueue(queueName); @@ -268,11 +447,23 @@ Broker.prototype.assertQueue = function assertQueue(queueName, options) { if (queue.options.durable !== queueOptions?.durable) throw new _Errors.SmqpError("Durable doesn't match", _Errors.ERR_QUEUE_DURABLE_MISMATCH); 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; @@ -280,24 +471,63 @@ Broker.prototype.get = function getMessageFromQueue(queueName, options) { 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 _Errors.SmqpError(`Shovel name must be unique, ${name} is occupied`, _Errors.ERR_SHOVEL_NAME_CONFLICT); @@ -309,6 +539,11 @@ Broker.prototype.createShovel = function createShovel(name, source, destination, shovels.set(name, shovel); return shovel; }; + +/** + * Close shovel by name + * @param {string} name shovel name + */ Broker.prototype.closeShovel = function closeShovel(name) { const shovel = this.getShovel(name); if (shovel) { @@ -317,12 +552,26 @@ Broker.prototype.closeShovel = function closeShovel(name) { } return false; }; + +/** + * Get shovel by name + * @param {string} name shovel name + */ Broker.prototype.getShovel = function getShovel(name) { return this[kEntities].get('shovels').get(name); }; + +/** List all shovels */ 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, @@ -337,6 +586,12 @@ 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 diff --git a/dist/Exchange.js b/dist/Exchange.js index 042b499..be802da 100644 --- a/dist/Exchange.js +++ b/dist/Exchange.js @@ -13,12 +13,24 @@ const kType = Symbol.for('type'); const kStopped = Symbol.for('stopped'); const kBindings = Symbol.for('bindings'); const kDeliveryQueue = Symbol.for('deliveryQueue'); + +/** + * 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 + */ 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); } + +/** + * Event exchange + * @param {string} [name] optional event exchange name, defaults to smq.ename- + */ function EventExchange(name) { if (!name) name = `smq.ename-${(0, _shared.generateId)()}`; return new ExchangeBase(name, 'topic', { @@ -26,6 +38,14 @@ function EventExchange(name) { autoDelete: true }); } + +/** + * Exchange + * @param {string} name name + * @param {import('#types').exchangeType} type + * @param {import('#types').ExchangeOptions} [options] + * @param {ExchangeBase} [eventExchange] + */ function ExchangeBase(name, type, options, eventExchange) { this[kName] = name; this[kType] = type; @@ -267,6 +287,14 @@ ExchangeBase.prototype.closeBinding = function closeBinding(binding) { binding.close(); if (!bindings.length && this.options.autoDelete) this.emit('delete', this); }; + +/** + * + * @param {ExchangeBase} exchange + * @param {import('./Queue.js').Queue} queue + * @param {string} pattern message routing key pattern + * @param {import('#types').BindingOptions} [bindOptions] + */ function Binding(exchange, queue, pattern, bindOptions) { this.id = `${queue.name}/${pattern}`; this.options = { @@ -281,12 +309,25 @@ function Binding(exchange, queue, pattern, bindOptions) { 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, diff --git a/dist/Queue.js b/dist/Queue.js index 526bd5a..a31bfa6 100644 --- a/dist/Queue.js +++ b/dist/Queue.js @@ -16,6 +16,13 @@ const kInternalQueue = Symbol.for('internalQueue'); const kIsReady = Symbol.for('isReady'); const kAvailableCount = Symbol.for('availableCount'); const kStopped = Symbol.for('stopped'); + +/** + * + * @param {string} name + * @param {import('#types').QueueOptions} options + * @param {import('./Exchange.js').EventExchange} eventEmitter + */ 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; diff --git a/package.json b/package.json index e8e003b..fd24f22 100644 --- a/package.json +++ b/package.json @@ -26,9 +26,10 @@ "url": "https://github.com/paed01/smqp/issues" }, "files": [ - "types", + "dist", "src", - "dist" + "types/index.d.ts", + "types/index.d.ts.map" ], "scripts": { "test": "mocha", @@ -37,7 +38,8 @@ "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", + "types": "node ./scripts/build-types.js", + "prepack": "npm run dist && npm run types", "toc": "node ./scripts/generate-api-toc.js", "lint": "eslint . --cache && prettier . -c --cache" }, @@ -64,6 +66,7 @@ "c8": "^10.1.2", "chai": "^6.2.0", "chronokinesis": "^8.0.0", + "dts-buddy": "^0.7.0", "eslint": "^9.10.0", "markdown-toc": "^1.2.0", "mocha": "^11.0.1", diff --git a/scripts/build-types.js b/scripts/build-types.js new file mode 100644 index 0000000..1939927 --- /dev/null +++ b/scripts/build-types.js @@ -0,0 +1,23 @@ +import fs from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { createBundle } from 'dts-buddy'; + +export async function buildTypes(output = 'types/index.d.ts') { + await createBundle({ + project: 'tsconfig.json', + output, + modules: { smqp: 'src/index.js' }, + }); + + // dts-buddy renames the default export to `Broker_1` and drops the named `Broker` export + // (the runtime exports both via `export { Broker }; export default Broker;`). + // Re-add the named export so `import { Broker } from 'smqp'` typechecks. + const dts = fs.readFileSync(output, 'utf8'); + const patched = dts.replace(/(\n)(}\n+\/\/# sourceMappingURL=)/, '$1\n\texport { Broker_1 as Broker };\n$2'); + if (patched === dts) throw new Error('build-types: failed to inject named Broker export'); + fs.writeFileSync(output, patched); +} + +if (process.argv[1] === fileURLToPath(import.meta.url)) { + buildTypes(); +} diff --git a/src/Broker.js b/src/Broker.js index d33521f..d95fd7f 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('./Exchange.js').ExchangeBase} */ 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; @@ -144,16 +211,29 @@ Broker.prototype.getConsumers = function getConsumers() { return result; }; +/** + * Get consumer by tag + * @param {string} consumerTag consumer tag + */ 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 + */ 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 +243,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 +262,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 +275,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 +291,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 +316,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 +344,60 @@ 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); }; +/** + * @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,14 @@ Broker.prototype._getQueuesState = function getQueuesState(onlyWithContent) { return result; }; +/** + * @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 +422,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 +440,20 @@ Broker.prototype.createQueue = function createQueue(queueName, options) { return queue; }; +/** + * Get queue by name + * @param {string} queueName queue name + */ 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 +466,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 +489,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 +554,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 +567,25 @@ Broker.prototype.closeShovel = function closeShovel(name) { return false; }; +/** + * Get shovel by name + * @param {string} name shovel name + */ Broker.prototype.getShovel = function getShovel(name) { return this[kEntities].get('shovels').get(name); }; +/** List all shovels */ 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 +599,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..41e336a 100644 --- a/src/Exchange.js +++ b/src/Exchange.js @@ -8,6 +8,12 @@ const kStopped = Symbol.for('stopped'); const kBindings = Symbol.for('bindings'); const kDeliveryQueue = Symbol.for('deliveryQueue'); +/** + * 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,11 +22,22 @@ 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 }); } +/** + * Exchange + * @param {string} name name + * @param {import('#types').exchangeType} type + * @param {import('#types').ExchangeOptions} [options] + * @param {ExchangeBase} [eventExchange] + */ function ExchangeBase(name, type, options, eventExchange) { this[kName] = name; this[kType] = type; @@ -269,6 +286,13 @@ ExchangeBase.prototype.closeBinding = function closeBinding(binding) { if (!bindings.length && this.options.autoDelete) this.emit('delete', this); }; +/** + * + * @param {ExchangeBase} exchange + * @param {import('./Queue.js').Queue} queue + * @param {string} pattern message routing key pattern + * @param {import('#types').BindingOptions} [bindOptions] + */ function Binding(exchange, queue, pattern, bindOptions) { this.id = `${queue.name}/${pattern}`; this.options = { priority: 0, ...bindOptions }; @@ -282,14 +306,24 @@ function Binding(exchange, queue, pattern, bindOptions) { }); } +/** + * 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, diff --git a/src/Queue.js b/src/Queue.js index a97608d..30fc260 100644 --- a/src/Queue.js +++ b/src/Queue.js @@ -11,6 +11,12 @@ const kIsReady = Symbol.for('isReady'); const kAvailableCount = Symbol.for('availableCount'); const kStopped = Symbol.for('stopped'); +/** + * + * @param {string} name + * @param {import('#types').QueueOptions} options + * @param {import('./Exchange.js').EventExchange} eventEmitter + */ 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()}`; diff --git a/test/types-test.js b/test/types-test.js new file mode 100644 index 0000000..922b054 --- /dev/null +++ b/test/types-test.js @@ -0,0 +1,110 @@ +import fs from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; +import { buildTypes } from '../scripts/build-types.js'; + +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 buildTypes(output); + 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\)/); + }); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index b39c687..849d7ad 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,9 +1,9 @@ { - "include": ["src/**/*", "types"], + "include": ["src/**/*", "types/interfaces.d.ts"], "compilerOptions": { "emitDeclarationOnly": true, "sourceMap": false, - "rootDir": "src", + "rootDir": ".", "lib": ["es2017"], "target": "es2017", "outDir": "./tmp/ignore", @@ -23,7 +23,7 @@ "noUnusedParameters": true, "typeRoots": ["./node_modules/@types"], "paths": { - "types": ["./types/types.js"] + "#types": ["./types/interfaces.d.ts"] } } } diff --git a/types/index.d.ts b/types/index.d.ts index f04ea0b..f237e12 100755 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,12 +1,696 @@ -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' { + export function Message(fields: any, content: any, properties: any, onConsumed: any): void; + export class Message { + constructor(fields: any, content: any, properties: any, onConsumed: any); + fields: any; + content: any; + properties: any; + get pending(): boolean; + ack(allUpTo: any): void; + nack(allUpTo: any, requeue?: boolean): void; + reject(requeue?: boolean): void; + _consume(consumerTag: any, consumedCb: any): void; + [kOnConsumed]: any[]; + [kPending]: boolean; + } + const kPending: unique symbol; + const kOnConsumed: unique symbol; + export function Shovel(name: any, source: any, destination: any, options: any): Shovel | undefined; + export class Shovel { + constructor(name: any, source: any, destination: any, options: any); + source: any; + destination: any; + events: any; + emit(eventName: any, content: any): void; + on(eventName: any, handler: any, options: any): any; + off(eventName: any, handler: any): any; + close(): void; + _messageHandler(message: any): any; + _onShovelMessage(routingKey: any, message: any): any; + readonly name: string; + readonly closed: boolean; + readonly consumerTag: string; + } + function Exchange2Exchange(shovel: any): void; + class Exchange2Exchange { + constructor(shovel: any); + on(...args: any[]): any; + 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; + /** + * Event exchange + * @param name optional event exchange name, defaults to smq.ename- + */ + function EventExchange(name?: string): ExchangeBase; + interface ExchangeBase { + readonly name: string; + readonly type: exchangeType; + readonly bindingCount: number; + readonly bindings: Binding[]; + readonly stopped: boolean; + readonly undeliveredCount: number; + } + interface Binding { + id: string; + pattern: string; + options: BindingOptions; + exchange: ExchangeBase; + queue: Queue; + } + /** + * 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: { + durable: boolean; + autoDelete: boolean; + }; + events: ExchangeBase | undefined; + publish(routingKey: any, content: any, properties: any): number | void; + _onTopicMessage(routingKey: any, message: any): number; + _onDirectMessage(routingKey: any, message: any): 0 | 1; + _emitReturn(routingKey: any, content: any, properties: any): void; + bindQueue(queue: any, pattern: any, bindOptions: any): any; + unbindQueue(queue: any, pattern: any): void; + unbindQueueByName(queueName: any): void; + close(): void; + getState(): { + bindings?: any[] | undefined; + deliveryQueue?: { + name: string; + options: { + autoDelete: boolean; + durable?: boolean; + messageTtl?: number; + maxLength?: number; + deadLetterExchange?: string; + deadLetterRoutingKey?: string; + }; + } | undefined; + name: any; + type: any; + options: { + durable: boolean; + autoDelete: boolean; + }; + }; + stop(): void; + recover(state: any, getQueue: any): this | undefined; + getBinding(queueName: any, pattern: any): any; + emit(eventName: any, content: any): number | void; + on(pattern: any, handler: any, consumeOptions: any): any; + off(pattern: any, handler: any): any; + closeBinding(binding: any): void; + [kName]: string; + [kType]: exchangeType; + [kBindings]: any[]; + [kStopped]: boolean; + [kDeliveryQueue]: Queue; + } + const kName: unique symbol; + const kType: unique symbol; + const kBindings: unique symbol; + const kStopped: unique symbol; + const kDeliveryQueue: unique symbol; + 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"; + export function getRoutingKeyPattern(pattern: any): RegExp | DirectRoutingKeyPattern | EndMatchRoutingKeyPattern; + function DirectRoutingKeyPattern(pattern: any): void; + class DirectRoutingKeyPattern { + constructor(pattern: any); + _match: any; + test(routingKey: any): boolean; + } + function EndMatchRoutingKeyPattern(pattern: any): void; + class EndMatchRoutingKeyPattern { + constructor(pattern: any); + _match: any; + test(routingKey: any): 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: ExchangeBase; + /** + * 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): any; + /** + * 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): any; + /** + * 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): any; + /** + * 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): any; + /** + * 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): any; + /** + * 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; + getConsumers(): { + queue: any; + consumerTag: any; + ready: any; + options: any; + }[]; + /** + * Get consumer by tag + * @param consumerTag consumer tag + */ + getConsumer(consumerTag: string): any; + /** + * Get exchange by name + * @param exchangeName exchange name + */ + getExchange(exchangeName: string): any; + /** + * 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: any[] | undefined; + queues: { + name: string; + options: { + autoDelete: boolean; + durable?: boolean; + messageTtl?: number; + maxLength?: number; + deadLetterExchange?: string; + deadLetterRoutingKey?: string; + }; + }[] | 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): any; + /** + * 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): any; + /** + * @param onlyWithContent skip queues without messages + */ + _getQueuesState(onlyWithContent?: boolean): { + name: string; + options: { + autoDelete: boolean; + durable?: boolean; + messageTtl?: number; + maxLength?: number; + deadLetterExchange?: string; + deadLetterRoutingKey?: string; + }; + }[] | undefined; + _getExchangeState(onlyWithContent: any): any[] | undefined; + /** + * 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): any; + /** + * 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): any; + /** + * Delete queue + * @param queueName queue name + * @param options optional delete guards + */ + deleteQueue(queueName: string, options?: DeleteQueueOptions): any; + /** + * 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): any; + /** List all shovels */ + getShovels(): any[]; + /** + * 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; + } + export function Queue(name: string, options: QueueOptions, eventEmitter: EventExchange): void; + export class Queue { + + constructor(name: string, options: QueueOptions, eventEmitter: EventExchange); + options: { + autoDelete: boolean; + durable?: boolean; + messageTtl?: number; + maxLength?: number; + deadLetterExchange?: string; + deadLetterRoutingKey?: string; + }; + messages: any[]; + events: EventExchange; + _onMessageConsumed: any; + queueMessage(fields: any, content: any, properties: any): number | undefined; + evictFirst(compareMessage: any): boolean | undefined; + _consumeNext(): number | undefined; + consume(onMessage: any, consumeOptions: any, owner: any): Consumer; + assertConsumer(onMessage: any, consumeOptions: any, owner: any): any; + get(options: any): any; + _consumeMessages(n: any, consumeOptions: any): any[]; + ack(message: any, allUpTo: any): void; + nack(message: any, allUpTo: any, requeue?: boolean): void; + reject(message: any, requeue?: boolean): void; + ackAll(): void; + nackAll(requeue?: boolean): void; + _getPendingMessages(untilIndex: any): any[]; + peek(ignoreDelivered: any): any; + cancel(consumerTag: any, requeue: any): boolean; + dismiss(onMessage: any, requeue: any): void; + unbindConsumer(consumer: any, requeue?: boolean): void; + emit(eventName: any, content: any): void; + on(eventName: any, handler: any, options: any): any; + off(eventName: any, handler: any): any; + purge(): number; + _dequeueMessage(message: any): number; + getState(): { + name: string; + options: { + autoDelete: boolean; + durable?: boolean; + messageTtl?: number; + maxLength?: number; + deadLetterExchange?: string; + deadLetterRoutingKey?: string; + }; + }; + recover(state: any): this; + delete(options: any): { + messageCount: number; + } | undefined; + close(): void; + stop(): void; + _getCapacity(): number; + readonly name: string; + readonly consumerCount: number; + readonly consumers: Consumer[]; + readonly exclusive: boolean; + readonly messageCount: number; + readonly stopped: boolean; + } + export function Consumer(queue: any, onMessage: any, options: any, owner: any, eventEmitter: any): void; + export class Consumer { + constructor(queue: any, onMessage: any, options: any, owner: any, eventEmitter: any); + options: any; + queue: any; + onMessage: any; + owner: any; + events: any; + _push(messages: any): void; + _consume(): void; + nackAll(requeue: any): void; + ackAll(): void; + cancel(requeue?: boolean): void; + prefetch(value: any): void; + emit(eventName: any, content: any): void; + on(eventName: any, handler: any): 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; + + type exchangeType = 'topic' | 'direct'; + + interface ConsumeOptions { + noAck?: boolean; + consumerTag?: string; + exclusive?: boolean; + prefetch?: number; + 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; + } + + 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[]; + } + + 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 { + fields: MessageFields; + content?: any; + properties: MessageProperties; + } + + interface ShovelOptions { + cloneMessage?: (message: MessageEnvelope) => MessageEnvelope; + [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; + } + + export {}; + + 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..86f0bb1 --- /dev/null +++ b/types/interfaces.d.ts @@ -0,0 +1,204 @@ +export type onMessage = (routingKey: string, message: import('../src/Message.js').Message, owner: any) => void; + +export type exchangeType = 'topic' | 'direct'; + +export interface ConsumeOptions { + noAck?: boolean; + consumerTag?: string; + exclusive?: boolean; + prefetch?: number; + 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 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: Binding[]; + readonly stopped: boolean; + readonly undeliveredCount: number; + } + interface Binding { + id: string; + pattern: string; + options: import('#types').BindingOptions; + exchange: ExchangeBase; + queue: import('../src/Queue.js').Queue; + } +} + +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; + } +} From deb5af1c006ff2efc56340c07c38bdcf301e4264 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Edman?= Date: Wed, 29 Apr 2026 07:30:22 +0200 Subject: [PATCH 4/8] move Bindings to separate file, queue types --- CHANGELOG.md | 4 + dist/Binding.js | 58 ++++++ dist/Broker.js | 2 + dist/Exchange.js | 70 ++----- dist/Message.js | 23 ++ dist/Queue.js | 178 ++++++++++++++++ dist/shared.js | 9 + package.json | 4 +- src/Binding.js | 49 +++++ src/Broker.js | 2 + src/Exchange.js | 64 ++---- src/Message.js | 21 ++ src/Queue.js | 146 +++++++++++++ src/shared.js | 8 + test/types-test.js | 42 ++++ types/index.d.ts | 474 ++++++++++++++++++++++++++++++------------ types/interfaces.d.ts | 32 ++- 17 files changed, 940 insertions(+), 246 deletions(-) create mode 100644 dist/Binding.js create mode 100644 src/Binding.js diff --git a/CHANGELOG.md b/CHANGELOG.md index ec26478..bf4994a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## v12.0.0 - 2026-04-29 + +- refactor types by using dts-buddy + ## v11.0.1 - 2025-12-02 - mitigate degraded performance as a result of refactoring consumer tag uniqueness diff --git a/dist/Binding.js b/dist/Binding.js new file mode 100644 index 0000000..b4379e0 --- /dev/null +++ b/dist/Binding.js @@ -0,0 +1,58 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.Binding = Binding; +var _shared = require("./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] + */ +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 = (0, _shared.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 + }; +}; \ No newline at end of file diff --git a/dist/Broker.js b/dist/Broker.js index 5b0baf3..d4817c4 100644 --- a/dist/Broker.js +++ b/dist/Broker.js @@ -377,6 +377,7 @@ Broker.prototype.sendToQueue = function sendToQueue(queueName, content, options) }; /** + * @private * @param {boolean} [onlyWithContent] skip queues without messages */ Broker.prototype._getQueuesState = function getQueuesState(onlyWithContent) { @@ -393,6 +394,7 @@ Broker.prototype._getQueuesState = function getQueuesState(onlyWithContent) { }; /** + * @private * @param {boolean} [onlyWithContent] skip exchanges without undelivered messages */ Broker.prototype._getExchangeState = function getExchangeState(onlyWithContent) { diff --git a/dist/Exchange.js b/dist/Exchange.js index be802da..3baaac5 100644 --- a/dist/Exchange.js +++ b/dist/Exchange.js @@ -5,8 +5,10 @@ Object.defineProperty(exports, "__esModule", { }); exports.EventExchange = EventExchange; exports.Exchange = Exchange; +exports.ExchangeBase = ExchangeBase; var _Message = require("./Message.js"); var _Queue = require("./Queue.js"); +var _Binding = require("./Binding.js"); var _shared = require("./shared.js"); const kName = Symbol.for('name'); const kType = Symbol.for('type'); @@ -14,6 +16,8 @@ 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 @@ -49,14 +53,18 @@ function EventExchange(name) { 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.Queue('delivery-q', { autoDelete: false }); @@ -125,6 +133,12 @@ ExchangeBase.prototype._onTopicMessage = function topic(routingKey, message) { } return delivered; }; + +/** + * @ignore + * @param {string} routingKey + * @param {Message} message + */ ExchangeBase.prototype._onDirectMessage = function direct(routingKey, message) { const publishedMsg = message.content; const bindings = this[kBindings]; @@ -171,7 +185,7 @@ ExchangeBase.prototype.bindQueue = function bindQueue(queue, pattern, bindOption for (const binding of bindings) { if (binding.queue === queue && binding.pattern === pattern) return binding; } - const binding = new Binding(this, queue, pattern, bindOptions); + const binding = new _Binding.Binding(this, queue, pattern, bindOptions); if (bindings.push(binding) > 1 && binding.options.priority) { bindings.sort(_shared.sortByPriority); } @@ -198,11 +212,14 @@ ExchangeBase.prototype.close = function close() { deliveryQueue.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, @@ -286,55 +303,4 @@ ExchangeBase.prototype.closeBinding = function closeBinding(binding) { bindings.splice(idx, 1); binding.close(); if (!bindings.length && this.options.autoDelete) this.emit('delete', this); -}; - -/** - * - * @param {ExchangeBase} exchange - * @param {import('./Queue.js').Queue} queue - * @param {string} pattern message routing key pattern - * @param {import('#types').BindingOptions} [bindOptions] - */ -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(); - }); -} - -/** - * 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 - }; }; \ No newline at end of file diff --git a/dist/Message.js b/dist/Message.js index 9b10ba1..4f56d36 100644 --- a/dist/Message.js +++ b/dist/Message.js @@ -8,6 +8,16 @@ exports.kPending = void 0; var _shared = require("./shared.js"); const kPending = exports.kPending = Symbol.for('pending'); const kOnConsumed = Symbol.for('onConsumed'); + +/** @typedef {Pick} SerializedMessage */ + +/** + * What it is all about - message + * @param {import('#types').MessageFields} fields + * @param {any} [content] + * @param {import('#types').MessageProperties} [properties] + * @param {CallableFunction} [onConsumed] + */ function Message(fields, content, properties, onConsumed) { this[kOnConsumed] = [null, onConsumed]; this[kPending] = false; @@ -23,8 +33,21 @@ function Message(fields, content, properties, onConsumed) { 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; } Object.defineProperty(Message.prototype, 'pending', { diff --git a/dist/Queue.js b/dist/Queue.js index a31bfa6..bd96162 100644 --- a/dist/Queue.js +++ b/dist/Queue.js @@ -26,10 +26,14 @@ 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; + + /** @type {import('#types').QueueOptions} */ this.options = { autoDelete: true, ...options }; + + /** @type {Message[]} */ this.messages = []; this.events = eventEmitter; this[kConsumers] = []; @@ -70,6 +74,13 @@ 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'); @@ -96,6 +107,11 @@ 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; @@ -116,6 +132,13 @@ 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) { @@ -134,6 +157,13 @@ Queue.prototype.consume = function consume(onMessage, consumeOptions, owner) { if (pendingMessages.length) consumer._push(pendingMessages); 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) { const consumers = this[kConsumers]; if (!consumers.length) return this.consume(onMessage, consumeOptions, owner); @@ -150,6 +180,11 @@ 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, @@ -162,6 +197,12 @@ Queue.prototype.get = function getMessage(options) { } return message; }; + +/** + * @private + * @param {number} n + * @param {import('#types').ConsumeOptions} consumeOptions + */ Queue.prototype._consumeMessages = function consumeMessages(n, consumeOptions) { const msgs = []; if (this[kStopped] || !this[kAvailableCount] || !n) return msgs; @@ -184,15 +225,42 @@ 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[_Message.kPending] = false; }; + +/** + * 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[_Message.kPending] = false; }; + +/** + * 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[_Message.kPending] = false; }; + +/** + * @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; const msgIdx = this._dequeueMessage(message); @@ -254,11 +322,21 @@ Queue.prototype.ackAll = function ackAll() { msg.ack(false); } }; + +/** + * 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; @@ -272,6 +350,11 @@ 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; @@ -281,6 +364,12 @@ Queue.prototype.peek = function peek(ignoreDelivered) { if (!msg.pending) return msg; } }; + +/** + * 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); @@ -289,12 +378,24 @@ Queue.prototype.cancel = function cancel(consumerTag, requeue) { this.unbindConsumer(consumer, 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); if (!consumer) return; 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); @@ -306,14 +407,33 @@ Queue.prototype.unbindConsumer = function unbindConsumer(consumer, requeue = tru this.emit('consumer.cancel', consumer); 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); @@ -329,6 +449,11 @@ Queue.prototype.purge = function purge() { if (!this.messages.length) this.emit('depleted', this); return toDelete.length; }; + +/** + * @private + * @param {Message} message + */ Queue.prototype._dequeueMessage = function dequeueMessage(message) { const messages = this.messages; const msgIdx = messages.indexOf(message); @@ -338,6 +463,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').SerializedMessage[] }} */ const state = { name: this.name, options: { @@ -355,6 +481,11 @@ 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]; @@ -390,6 +521,11 @@ 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; @@ -417,12 +553,25 @@ Queue.prototype.stop = function stop() { consumer.stop(); } }; + +/** + * @private + */ Queue.prototype._getCapacity = function getCapacity() { if ('maxLength' in this.options) { return this.options.maxLength - this.messages.length; } 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 {{ emit(eventName: string, content?: any): any, on(pattern: string, handler: Function): any }} [eventEmitter] internal queue event bridge + */ function Consumer(queue, onMessage, options, owner, eventEmitter) { if (typeof onMessage !== 'function') throw new TypeError('message callback is required and must be a function'); const { @@ -507,28 +656,57 @@ Consumer.prototype._consume = function consume() { if (this[kStopped]) break; } }; + +/** + * 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/dist/shared.js b/dist/shared.js index 8462e1b..271adb8 100644 --- a/dist/shared.js +++ b/dist/shared.js @@ -24,6 +24,15 @@ function EndMatchRoutingKeyPattern(pattern) { 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 {typeof RegExp.test} test function to test routing key pattern + */ function getRoutingKeyPattern(pattern) { const len = pattern.length; const hashIdx = pattern.indexOf('#'); diff --git a/package.json b/package.json index fd24f22..7be4cae 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "smqp", - "version": "11.0.1", + "version": "12.0.0", "type": "module", "description": "Synchronous message queueing package", "author": { @@ -37,7 +37,7 @@ "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", + "dist": "dts-buddy && babel src/**.js -d dist", "types": "node ./scripts/build-types.js", "prepack": "npm run dist && npm run types", "toc": "node ./scripts/generate-api-toc.js", 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 d95fd7f..4e639b1 100644 --- a/src/Broker.js +++ b/src/Broker.js @@ -391,6 +391,7 @@ Broker.prototype.sendToQueue = function sendToQueue(queueName, content, options) }; /** + * @private * @param {boolean} [onlyWithContent] skip queues without messages */ Broker.prototype._getQueuesState = function getQueuesState(onlyWithContent) { @@ -407,6 +408,7 @@ Broker.prototype._getQueuesState = function getQueuesState(onlyWithContent) { }; /** + * @private * @param {boolean} [onlyWithContent] skip exchanges without undelivered messages */ Broker.prototype._getExchangeState = function getExchangeState(onlyWithContent) { diff --git a/src/Exchange.js b/src/Exchange.js index 41e336a..782a987 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,8 @@ 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 @@ -38,14 +41,17 @@ export function EventExchange(name) { * @param {import('#types').ExchangeOptions} [options] * @param {ExchangeBase} [eventExchange] */ -function ExchangeBase(name, type, options, 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' }); @@ -116,6 +122,11 @@ ExchangeBase.prototype._onTopicMessage = function topic(routingKey, message) { return delivered; }; +/** + * @ignore + * @param {string} routingKey + * @param {Message} message + */ ExchangeBase.prototype._onDirectMessage = function direct(routingKey, message) { const publishedMsg = message.content; const bindings = this[kBindings]; @@ -196,12 +207,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, @@ -285,50 +298,3 @@ ExchangeBase.prototype.closeBinding = function closeBinding(binding) { if (!bindings.length && this.options.autoDelete) this.emit('delete', this); }; - -/** - * - * @param {ExchangeBase} exchange - * @param {import('./Queue.js').Queue} queue - * @param {string} pattern message routing key pattern - * @param {import('#types').BindingOptions} [bindOptions] - */ -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(); - }); -} - -/** - * 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/Message.js b/src/Message.js index bf1bb4f..d9b584a 100644 --- a/src/Message.js +++ b/src/Message.js @@ -3,6 +3,15 @@ import { generateId } from './shared.js'; export const kPending = Symbol.for('pending'); const kOnConsumed = Symbol.for('onConsumed'); +/** @typedef {Pick} SerializedMessage */ + +/** + * 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) { this[kOnConsumed] = [null, onConsumed]; this[kPending] = false; @@ -18,8 +27,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; } diff --git a/src/Queue.js b/src/Queue.js index 30fc260..040f3dd 100644 --- a/src/Queue.js +++ b/src/Queue.js @@ -22,8 +22,10 @@ export function Queue(name, options, eventEmitter) { 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] = []; @@ -66,6 +68,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'); @@ -96,6 +104,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; @@ -121,6 +133,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) { @@ -147,6 +165,12 @@ 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) { const consumers = this[kConsumers]; if (!consumers.length) return this.consume(onMessage, consumeOptions, owner); @@ -166,6 +190,10 @@ 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; @@ -177,6 +205,11 @@ Queue.prototype.get = function getMessage(options) { return message; }; +/** + * @private + * @param {number} n + * @param {import('#types').ConsumeOptions} consumeOptions + */ Queue.prototype._consumeMessages = function consumeMessages(n, consumeOptions) { const msgs = []; @@ -205,18 +238,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; }; +/** + * 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; }; +/** + * 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; }; +/** + * @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; @@ -282,12 +338,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; @@ -305,6 +369,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; @@ -317,6 +385,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); @@ -328,6 +401,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); @@ -335,6 +413,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); @@ -352,16 +435,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); @@ -379,6 +478,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); @@ -389,6 +492,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').SerializedMessage[] }} */ const state = { name: this.name, options: { ...this.options }, @@ -406,6 +510,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]; @@ -440,6 +548,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; @@ -471,6 +583,9 @@ Queue.prototype.stop = function stop() { } }; +/** + * @private + */ Queue.prototype._getCapacity = function getCapacity() { if ('maxLength' in this.options) { return this.options.maxLength - this.messages.length; @@ -478,6 +593,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 {{ emit(eventName: string, content?: any): any, on(pattern: string, handler: Function): any }} [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'); @@ -571,33 +694,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/shared.js b/src/shared.js index 3a69efa..4054bca 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 {typeof RegExp.test} test function to test routing key pattern + */ export function getRoutingKeyPattern(pattern) { const len = pattern.length; const hashIdx = pattern.indexOf('#'); diff --git a/test/types-test.js b/test/types-test.js index 922b054..35a32a7 100644 --- a/test/types-test.js +++ b/test/types-test.js @@ -107,4 +107,46 @@ describe('generated types bundle', () => { 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\)/); + }); + }); }); diff --git a/types/index.d.ts b/types/index.d.ts index f237e12..b710a31 100755 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,19 +1,38 @@ declare module 'smqp' { - export function Message(fields: any, content: any, properties: any, onConsumed: any): void; + /** + * What it is all about - message + * + */ + export function Message(fields: MessageFields, content?: any, properties?: MessageProperties, onConsumed?: CallableFunction): void; export class Message { - constructor(fields: any, content: any, properties: any, onConsumed: any); - fields: any; + + /** + * What it is all about - message + * + */ + constructor(fields: MessageFields, content?: any, properties?: MessageProperties, onConsumed?: CallableFunction); + /** + * Message fields + * */ + fields: MessageFields; + /** + * Message content + * */ content: any; - properties: any; + /** + * Message properties + * */ + properties: MessageProperties; get pending(): boolean; ack(allUpTo: any): void; nack(allUpTo: any, requeue?: boolean): void; reject(requeue?: boolean): void; _consume(consumerTag: any, consumedCb: any): void; - [kOnConsumed]: any[]; + [kOnConsumed]: (CallableFunction | null | undefined)[]; [kPending]: boolean; } const kPending: unique symbol; + type SerializedMessage = Pick; const kOnConsumed: unique symbol; export function Shovel(name: any, source: any, destination: any, options: any): Shovel | undefined; export class Shovel { @@ -55,21 +74,6 @@ declare module 'smqp' { * @param name optional event exchange name, defaults to smq.ename- */ function EventExchange(name?: string): ExchangeBase; - interface ExchangeBase { - readonly name: string; - readonly type: exchangeType; - readonly bindingCount: number; - readonly bindings: Binding[]; - readonly stopped: boolean; - readonly undeliveredCount: number; - } - interface Binding { - id: string; - pattern: string; - options: BindingOptions; - exchange: ExchangeBase; - queue: Queue; - } /** * Exchange * @param name name @@ -83,57 +87,54 @@ declare module 'smqp' { * */ constructor(name: string, type: exchangeType, options?: ExchangeOptions, eventExchange?: ExchangeBase); - options: { - durable: boolean; - autoDelete: boolean; - }; + + options: ExchangeOptions; events: ExchangeBase | undefined; - publish(routingKey: any, content: any, properties: any): number | void; + publish(routingKey: any, content: any, properties: any): any; _onTopicMessage(routingKey: any, message: any): number; - _onDirectMessage(routingKey: any, message: any): 0 | 1; + + _onDirectMessage(routingKey: string, message: Message): 0 | 1; _emitReturn(routingKey: any, content: any, properties: any): void; bindQueue(queue: any, pattern: any, bindOptions: any): any; unbindQueue(queue: any, pattern: any): void; unbindQueueByName(queueName: any): void; close(): void; getState(): { - bindings?: any[] | undefined; - deliveryQueue?: { - name: string; + bindings?: { + id: string; options: { - autoDelete: boolean; - durable?: boolean; - messageTtl?: number; - maxLength?: number; - deadLetterExchange?: string; - deadLetterRoutingKey?: string; + priority: number; }; + queueName: string; + pattern: string; + }[] | undefined; + deliveryQueue?: { + name: string; + options: QueueOptions; + messages?: SerializedMessage[]; } | undefined; - name: any; - type: any; + name: string; + type: exchangeType; options: { - durable: boolean; - autoDelete: boolean; + [x: string]: any; + durable?: boolean; + autoDelete?: boolean; }; }; stop(): void; recover(state: any, getQueue: any): this | undefined; getBinding(queueName: any, pattern: any): any; - emit(eventName: any, content: any): number | void; + emit(eventName: any, content: any): any; on(pattern: any, handler: any, consumeOptions: any): any; off(pattern: any, handler: any): any; closeBinding(binding: any): void; - [kName]: string; - [kType]: exchangeType; - [kBindings]: any[]; - [kStopped]: boolean; - [kDeliveryQueue]: Queue; + readonly name: string; + readonly type: exchangeType; + readonly bindingCount: number; + readonly bindings: Binding[]; + readonly stopped: boolean; + readonly undeliveredCount: number; } - const kName: unique symbol; - const kType: unique symbol; - const kBindings: unique symbol; - const kStopped: unique symbol; - const kDeliveryQueue: unique symbol; export class SmqpError extends Error { constructor(message: any, code: any); type: string; @@ -149,19 +150,20 @@ declare module 'smqp' { 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"; - export function getRoutingKeyPattern(pattern: any): RegExp | DirectRoutingKeyPattern | EndMatchRoutingKeyPattern; - function DirectRoutingKeyPattern(pattern: any): void; - class DirectRoutingKeyPattern { - constructor(pattern: any); - _match: any; - test(routingKey: any): boolean; - } - function EndMatchRoutingKeyPattern(pattern: any): void; - class EndMatchRoutingKeyPattern { - constructor(pattern: any); - _match: any; - test(routingKey: any): boolean; - } + /** + * Get routing key pattern + * @param pattern routing key pattern + * */ + export function getRoutingKeyPattern(pattern: string): RoutingKeyPattern; + /** + * Get routing key pattern + */ + type RoutingKeyPattern = { + /** + * function to test routing key pattern + */ + test: typeof RegExp.test; + }; /** * Smqp message broker * @param owner optional broker owner, forwarded to message consumer @@ -287,18 +289,33 @@ declare module 'smqp' { * @param onlyWithContent omit exchanges and queues without content */ getState(onlyWithContent?: boolean): { - exchanges: any[] | undefined; - queues: { + exchanges: { + bindings?: { + id: string; + options: { + priority: number; + }; + queueName: string; + pattern: string; + }[] | undefined; + deliveryQueue?: { + name: string; + options: QueueOptions; + messages?: SerializedMessage[]; + } | undefined; name: string; + type: exchangeType; options: { - autoDelete: boolean; + [x: string]: any; durable?: boolean; - messageTtl?: number; - maxLength?: number; - deadLetterExchange?: string; - deadLetterRoutingKey?: string; + autoDelete?: boolean; }; }[] | undefined; + queues: { + name: string; + options: QueueOptions; + messages?: SerializedMessage[]; + }[] | undefined; } | undefined; /** * Recover broker from previously captured state @@ -340,21 +357,8 @@ declare module 'smqp' { * @param options optional message properties */ sendToQueue(queueName: string, content: any, options?: MessageProperties): any; - /** - * @param onlyWithContent skip queues without messages - */ - _getQueuesState(onlyWithContent?: boolean): { - name: string; - options: { - autoDelete: boolean; - durable?: boolean; - messageTtl?: number; - maxLength?: number; - deadLetterExchange?: string; - deadLetterRoutingKey?: string; - }; - }[] | undefined; - _getExchangeState(onlyWithContent: any): any[] | undefined; + private _getQueuesState; + private _getExchangeState; /** * Create queue * @param queueName queue name, defaults to a generated name @@ -461,57 +465,137 @@ declare module 'smqp' { export class Queue { constructor(name: string, options: QueueOptions, eventEmitter: EventExchange); - options: { - autoDelete: boolean; - durable?: boolean; - messageTtl?: number; - maxLength?: number; - deadLetterExchange?: string; - deadLetterRoutingKey?: string; - }; - messages: any[]; + + options: QueueOptions; + + messages: Message[]; events: EventExchange; _onMessageConsumed: any; - queueMessage(fields: any, content: any, properties: any): number | undefined; - evictFirst(compareMessage: any): boolean | undefined; + /** + * 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; _consumeNext(): number | undefined; - consume(onMessage: any, consumeOptions: any, owner: any): Consumer; - assertConsumer(onMessage: any, consumeOptions: any, owner: any): any; - get(options: any): any; - _consumeMessages(n: any, consumeOptions: any): any[]; - ack(message: any, allUpTo: any): void; - nack(message: any, allUpTo: any, requeue?: boolean): void; - reject(message: any, requeue?: boolean): void; + /** + * 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): any; + /** + * 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; - _getPendingMessages(untilIndex: any): any[]; - peek(ignoreDelivered: any): any; - cancel(consumerTag: any, requeue: any): boolean; - dismiss(onMessage: any, requeue: any): void; - unbindConsumer(consumer: any, requeue?: boolean): void; - emit(eventName: any, content: any): void; - on(eventName: any, handler: any, options: any): any; - off(eventName: any, handler: any): any; + 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; - _dequeueMessage(message: any): number; + private _dequeueMessage; getState(): { name: string; - options: { - autoDelete: boolean; - durable?: boolean; - messageTtl?: number; - maxLength?: number; - deadLetterExchange?: string; - deadLetterRoutingKey?: string; - }; + options: QueueOptions; + messages?: SerializedMessage[]; }; - recover(state: any): this; - delete(options: any): { + /** + * 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; - _getCapacity(): number; + private _getCapacity; readonly name: string; readonly consumerCount: number; readonly consumers: Consumer[]; @@ -519,22 +603,76 @@ declare module 'smqp' { readonly messageCount: number; readonly stopped: boolean; } - export function Consumer(queue: any, onMessage: any, options: any, owner: any, eventEmitter: any): void; + /** + * 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?: { + emit(eventName: string, content?: any): any; + on(pattern: string, handler: Function): any; + }): void; export class Consumer { - constructor(queue: any, onMessage: any, options: any, owner: any, eventEmitter: any); - options: any; - queue: any; - onMessage: any; + /** + * 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?: { + emit(eventName: string, content?: any): any; + on(pattern: string, handler: Function): any; + }); + options: { + noAck: boolean; + consumerTag?: string; + exclusive?: boolean; + prefetch: number; + priority: number; + }; + queue: Queue; + onMessage: onMessage; owner: any; - events: any; + events: { + emit(eventName: string, content?: any): any; + on(pattern: string, handler: Function): any; + } | undefined; _push(messages: any): void; _consume(): void; - nackAll(requeue: any): void; + /** + * 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; - prefetch(value: any): void; - emit(eventName: any, content: any): void; - on(eventName: any, handler: any): any; + /** + * 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; @@ -549,10 +687,15 @@ declare module 'smqp' { 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; } @@ -585,6 +728,24 @@ declare module 'smqp' { 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; @@ -687,10 +848,53 @@ declare module 'smqp' { /** 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 {}; - - 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 index 86f0bb1..9d7e2ef 100644 --- a/types/interfaces.d.ts +++ b/types/interfaces.d.ts @@ -3,10 +3,15 @@ export type onMessage = (routingKey: string, message: import('../src/Message.js' 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; } @@ -39,6 +44,24 @@ export interface DeleteQueueOptions { 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; @@ -155,17 +178,10 @@ declare module '../src/Exchange.js' { readonly name: string; readonly type: import('#types').exchangeType; readonly bindingCount: number; - readonly bindings: Binding[]; + readonly bindings: import('../src/Binding.js').Binding[]; readonly stopped: boolean; readonly undeliveredCount: number; } - interface Binding { - id: string; - pattern: string; - options: import('#types').BindingOptions; - exchange: ExchangeBase; - queue: import('../src/Queue.js').Queue; - } } declare module '../src/Queue.js' { From e134f6f133a75d405203b1f1738027b5e5ba502d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Edman?= Date: Wed, 29 Apr 2026 07:53:55 +0200 Subject: [PATCH 5/8] more entity types --- dist/Broker.js | 23 +-- dist/Exchange.js | 67 +++++++- dist/Message.js | 31 +++- dist/Queue.js | 40 +++-- dist/Shovel.js | 61 +++++++- dist/index.js | 10 +- dist/shared.js | 2 +- package.json | 4 +- scripts/build-types.js | 23 --- src/Broker.js | 20 ++- src/Exchange.js | 55 ++++++- src/Message.js | 26 ++- src/Queue.js | 37 +++-- src/Shovel.js | 51 +++++- src/index.js | 3 +- src/shared.js | 2 +- test/types-test.js | 68 +++++++- types/index.d.ts | 347 +++++++++++++++++++++++++++++------------ types/interfaces.d.ts | 11 ++ 19 files changed, 701 insertions(+), 180 deletions(-) delete mode 100644 scripts/build-types.js mode change 100755 => 100644 types/index.d.ts diff --git a/dist/Broker.js b/dist/Broker.js index d4817c4..f3a0486 100644 --- a/dist/Broker.js +++ b/dist/Broker.js @@ -21,7 +21,7 @@ function Broker(owner) { return new Broker(owner); } this.owner = owner; - /** @type {import('./Exchange.js').ExchangeBase} */ + /** @type {import('#types').ExchangeEventEmitter} */ 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); @@ -191,17 +191,13 @@ Broker.prototype.cancel = function cancel(consumerTag, requeue = true) { consumer.cancel(requeue); 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; }; @@ -209,6 +205,7 @@ Broker.prototype.getConsumers = function getConsumers() { /** * 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'); @@ -218,6 +215,7 @@ Broker.prototype.getConsumer = function getConsumer(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'); @@ -427,6 +425,7 @@ Broker.prototype.createQueue = function createQueue(queueName, options) { /** * 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'); @@ -558,12 +557,16 @@ Broker.prototype.closeShovel = function closeShovel(name) { /** * 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 */ +/** + * List all shovels + * @returns {import('./Shovel.js').Shovel[]} + */ Broker.prototype.getShovels = function getShovels() { return [...this[kEntities].get('shovels').values()]; }; diff --git a/dist/Exchange.js b/dist/Exchange.js index 3baaac5..8f28e8a 100644 --- a/dist/Exchange.js +++ b/dist/Exchange.js @@ -106,6 +106,13 @@ 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); @@ -116,6 +123,8 @@ ExchangeBase.prototype.publish = function publish(routingKey, content, propertie properties }); }; + +/** @private */ ExchangeBase.prototype._onTopicMessage = function topic(routingKey, message) { const publishedMsg = message.content; message.ack(); @@ -135,7 +144,7 @@ ExchangeBase.prototype._onTopicMessage = function topic(routingKey, message) { }; /** - * @ignore + * @private * @param {string} routingKey * @param {Message} message */ @@ -165,6 +174,8 @@ ExchangeBase.prototype._onDirectMessage = function direct(routingKey, message) { }, publishedMsg.content, publishedMsg.properties); return 1; }; + +/** @private */ ExchangeBase.prototype._emitReturn = function emitReturn(routingKey, content, properties) { if (!this.events || !properties) return; if (properties.confirm) { @@ -180,6 +191,13 @@ ExchangeBase.prototype._emitReturn = function emitReturn(routingKey, content, pr }, content, properties)); } }; + +/** + * 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]; for (const binding of bindings) { @@ -191,6 +209,12 @@ 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) { @@ -198,6 +222,11 @@ 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); @@ -238,6 +267,12 @@ ExchangeBase.prototype.getState = function getState() { 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; @@ -259,15 +294,34 @@ 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); const eventQueue = new _Queue.Queue(null, { @@ -285,6 +339,12 @@ ExchangeBase.prototype.on = function on(pattern, 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); const { @@ -296,6 +356,11 @@ 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); diff --git a/dist/Message.js b/dist/Message.js index 4f56d36..3de2977 100644 --- a/dist/Message.js +++ b/dist/Message.js @@ -4,12 +4,13 @@ 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'); +/** @type {symbol} */ +const kPending = Symbol.for('pending'); +/** @type {symbol} */ const kOnConsumed = Symbol.for('onConsumed'); -/** @typedef {Pick} SerializedMessage */ +/** @typedef {Pick} MessageEnvelope */ /** * What it is all about - message @@ -19,6 +20,7 @@ const kOnConsumed = Symbol.for('onConsumed'); * @param {CallableFunction} [onConsumed] */ function Message(fields, content, properties, onConsumed) { + /** @private */ this[kOnConsumed] = [null, onConsumed]; this[kPending] = false; const mproperties = { @@ -55,6 +57,11 @@ Object.defineProperty(Message.prototype, 'pending', { return this[kPending]; } }); + +/** + * 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]) { @@ -62,6 +69,12 @@ 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]) { @@ -69,11 +82,23 @@ 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; }; \ No newline at end of file diff --git a/dist/Queue.js b/dist/Queue.js index bd96162..ebf2869 100644 --- a/dist/Queue.js +++ b/dist/Queue.js @@ -18,10 +18,10 @@ const kAvailableCount = Symbol.for('availableCount'); const kStopped = Symbol.for('stopped'); /** - * - * @param {string} name - * @param {import('#types').QueueOptions} options - * @param {import('./Exchange.js').EventExchange} eventEmitter + * 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 */ 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)()}`; @@ -40,6 +40,7 @@ function Queue(name, options, eventEmitter) { this[kStopped] = false; this[kAvailableCount] = 0; this[kExclusive] = false; + /** @private */ this._onMessageConsumed = this._onMessageConsumed.bind(this); } Object.defineProperties(Queue.prototype, { @@ -118,6 +119,8 @@ Queue.prototype.evictFirst = function evictFirst(compareMessage) { evict.nack(false, false); return evict === compareMessage; }; + +/** @private */ Queue.prototype._consumeNext = function consumeNext() { if (this[kStopped] || !this[kAvailableCount]) return; const consumers = this[kConsumers]; @@ -165,6 +168,7 @@ Queue.prototype.consume = function consume(onMessage, consumeOptions, owner) { * @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) { @@ -193,7 +197,7 @@ Queue.prototype.get = function getMessage(options) { if (!message) return false; if (options?.noAck) { this._dequeueMessage(message); - message[_Message.kPending] = false; + message._clearPending(); } return message; }; @@ -232,7 +236,7 @@ Queue.prototype._consumeMessages = function consumeMessages(n, consumeOptions) { * @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[_Message.kPending] = false; + if (this._onMessageConsumed(message, 'ack', allUpTo, false)) message._clearPending(); }; /** @@ -242,7 +246,7 @@ Queue.prototype.ack = function ack(message, allUpTo) { * @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[_Message.kPending] = false; + if (this._onMessageConsumed(message, 'nack', allUpTo, requeue)) message._clearPending(); }; /** @@ -251,7 +255,7 @@ Queue.prototype.nack = function nack(message, allUpTo, requeue = true) { * @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[_Message.kPending] = false; + if (this._onMessageConsumed(message, 'nack', false, requeue)) message._clearPending(); }; /** @@ -463,7 +467,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').SerializedMessage[] }} */ + /** @type {{name: string, options: import('#types').QueueOptions, messages?: import('./Message.js').MessageEnvelope[] }} */ const state = { name: this.name, options: { @@ -570,7 +574,7 @@ Queue.prototype._getCapacity = function getCapacity() { * @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 {{ emit(eventName: string, content?: any): any, on(pattern: string, handler: Function): any }} [eventEmitter] internal queue event bridge + * @param {import('#types').ExchangeEventEmitter} [eventEmitter] internal queue event bridge */ function Consumer(queue, onMessage, options, owner, eventEmitter) { if (typeof onMessage !== 'function') throw new TypeError('message callback is required and must be a function'); @@ -628,6 +632,20 @@ 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) { @@ -642,6 +660,8 @@ Consumer.prototype._push = function push(messages) { } } }; + +/** @private */ Consumer.prototype._consume = function consume() { const internalQ = this[kInternalQueue]; const consumerTag = this[kName]; diff --git a/dist/Shovel.js b/dist/Shovel.js index 767f519..91c1d05 100644 --- a/dist/Shovel.js +++ b/dist/Shovel.js @@ -7,16 +7,34 @@ exports.Exchange2Exchange = Exchange2Exchange; exports.Shovel = Shovel; var _Exchange = require("./Exchange.js"); var _Errors = require("./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 + */ function Shovel(name, source, destination, options) { if (!name || typeof name !== 'string') throw new TypeError('Shovel name is required and must be a string'); const { @@ -51,6 +69,7 @@ function Shovel(name, source, destination, options) { this.destination = { ...destination }; + /** @type {import('#types').ExchangeEventEmitter} */ this.events = new _Exchange.EventExchange('shovel__events'); const consumerTag = this[kConsumerTag] = source.consumerTag || `smq.shoveltag-${name}`; this[kClosed] = false; @@ -93,15 +112,36 @@ 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; @@ -112,6 +152,8 @@ Shovel.prototype.close = function closeShovel() { events.close(); this[kSourceBroker].cancel(this[kConsumerTag]); }; + +/** @private */ Shovel.prototype._messageHandler = function messageHandler(message) { const cloneMessage = this[kCloneMessage]; if (!cloneMessage) return message; @@ -141,6 +183,8 @@ 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(); @@ -157,6 +201,11 @@ Shovel.prototype._onShovelMessage = function onShovelMessage(routingKey, message destinationExchange.publish(this.destination.exchangeKey || routingKey, content, props); message.ack(); }; + +/** + * Exchange-to-exchange shovel wrapper, returned by `broker.bindExchange` + * @param {Shovel} shovel underlying shovel + */ function Exchange2Exchange(shovel) { this[kE2EShovel] = shovel; } @@ -192,9 +241,17 @@ 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(); }; \ No newline at end of file diff --git a/dist/index.js b/dist/index.js index 67b84a8..6effbf8 100644 --- a/dist/index.js +++ b/dist/index.js @@ -48,7 +48,12 @@ Object.defineProperty(exports, "Shovel", { return _Shovel.Shovel; } }); -exports.default = void 0; +Object.defineProperty(exports, "default", { + enumerable: true, + get: function () { + return _Broker.Broker; + } +}); Object.defineProperty(exports, "getRoutingKeyPattern", { enumerable: true, get: function () { @@ -72,5 +77,4 @@ Object.keys(_Errors).forEach(function (key) { } }); }); -var _shared = require("./shared.js"); -var _default = exports.default = _Broker.Broker; \ No newline at end of file +var _shared = require("./shared.js"); \ No newline at end of file diff --git a/dist/shared.js b/dist/shared.js index 271adb8..ee0058d 100644 --- a/dist/shared.js +++ b/dist/shared.js @@ -31,7 +31,7 @@ EndMatchRoutingKeyPattern.prototype.test = function test(routingKey) { * @returns {RoutingKeyPattern} * * @typedef {object} RoutingKeyPattern - * @property {typeof RegExp.test} test function to test routing key pattern + * @property {(this: RoutingKeyPattern, routingKey: string) => boolean} test method to test a routing key against the pattern; receiver-bound — destructuring is unsupported */ function getRoutingKeyPattern(pattern) { const len = pattern.length; diff --git a/package.json b/package.json index 7be4cae..69902f5 100644 --- a/package.json +++ b/package.json @@ -38,8 +38,8 @@ "test:md": "texample ./README.md,API.md", "cov:html": "c8 -r html -r text mocha", "dist": "dts-buddy && babel src/**.js -d dist", - "types": "node ./scripts/build-types.js", - "prepack": "npm run dist && npm run types", + "types": "dts-buddy", + "prepack": "npm run dist", "toc": "node ./scripts/generate-api-toc.js", "lint": "eslint . --cache && prettier . -c --cache" }, diff --git a/scripts/build-types.js b/scripts/build-types.js deleted file mode 100644 index 1939927..0000000 --- a/scripts/build-types.js +++ /dev/null @@ -1,23 +0,0 @@ -import fs from 'node:fs'; -import { fileURLToPath } from 'node:url'; -import { createBundle } from 'dts-buddy'; - -export async function buildTypes(output = 'types/index.d.ts') { - await createBundle({ - project: 'tsconfig.json', - output, - modules: { smqp: 'src/index.js' }, - }); - - // dts-buddy renames the default export to `Broker_1` and drops the named `Broker` export - // (the runtime exports both via `export { Broker }; export default Broker;`). - // Re-add the named export so `import { Broker } from 'smqp'` typechecks. - const dts = fs.readFileSync(output, 'utf8'); - const patched = dts.replace(/(\n)(}\n+\/\/# sourceMappingURL=)/, '$1\n\texport { Broker_1 as Broker };\n$2'); - if (patched === dts) throw new Error('build-types: failed to inject named Broker export'); - fs.writeFileSync(output, patched); -} - -if (process.argv[1] === fileURLToPath(import.meta.url)) { - buildTypes(); -} diff --git a/src/Broker.js b/src/Broker.js index 4e639b1..e7a6701 100644 --- a/src/Broker.js +++ b/src/Broker.js @@ -24,7 +24,7 @@ export function Broker(owner) { return new Broker(owner); } this.owner = owner; - /** @type {import('./Exchange.js').ExchangeBase} */ + /** @type {import('#types').ExchangeEventEmitter} */ const events = (this.events = new EventExchange('broker__events')); const entities = (this[kEntities] = new Map([ ['exchanges', new Map()], @@ -198,15 +198,12 @@ 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; }; @@ -214,6 +211,7 @@ Broker.prototype.getConsumers = function getConsumers() { /** * 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'); @@ -223,6 +221,7 @@ Broker.prototype.getConsumer = function getConsumer(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'); @@ -445,6 +444,7 @@ Broker.prototype.createQueue = function createQueue(queueName, options) { /** * 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'); @@ -572,12 +572,16 @@ Broker.prototype.closeShovel = function closeShovel(name) { /** * 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 */ +/** + * List all shovels + * @returns {import('./Shovel.js').Shovel[]} + */ Broker.prototype.getShovels = function getShovels() { return [...this[kEntities].get('shovels').values()]; }; diff --git a/src/Exchange.js b/src/Exchange.js index 782a987..31f7589 100644 --- a/src/Exchange.js +++ b/src/Exchange.js @@ -90,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); @@ -103,6 +109,7 @@ ExchangeBase.prototype.publish = function publish(routingKey, content, propertie ); }; +/** @private */ ExchangeBase.prototype._onTopicMessage = function topic(routingKey, message) { const publishedMsg = message.content; @@ -123,7 +130,7 @@ ExchangeBase.prototype._onTopicMessage = function topic(routingKey, message) { }; /** - * @ignore + * @private * @param {string} routingKey * @param {Message} message */ @@ -157,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; @@ -168,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]; @@ -183,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) { @@ -191,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); @@ -229,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; @@ -250,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); @@ -275,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); @@ -288,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); diff --git a/src/Message.js b/src/Message.js index d9b584a..bc17436 100644 --- a/src/Message.js +++ b/src/Message.js @@ -1,9 +1,11 @@ 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} SerializedMessage */ +/** @typedef {Pick} MessageEnvelope */ /** * What it is all about - message @@ -13,6 +15,7 @@ const kOnConsumed = Symbol.for('onConsumed'); * @param {CallableFunction} [onConsumed] */ export function Message(fields, content, properties, onConsumed) { + /** @private */ this[kOnConsumed] = [null, onConsumed]; this[kPending] = false; @@ -50,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]) { @@ -58,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]) { @@ -66,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 040f3dd..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'); @@ -12,10 +12,10 @@ const kAvailableCount = Symbol.for('availableCount'); const kStopped = Symbol.for('stopped'); /** - * - * @param {string} name - * @param {import('#types').QueueOptions} options - * @param {import('./Exchange.js').EventExchange} eventEmitter + * 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'); @@ -32,6 +32,7 @@ export function Queue(name, options, eventEmitter) { this[kStopped] = false; this[kAvailableCount] = 0; this[kExclusive] = false; + /** @private */ this._onMessageConsumed = this._onMessageConsumed.bind(this); } @@ -115,6 +116,7 @@ Queue.prototype.evictFirst = function evictFirst(compareMessage) { return evict === compareMessage; }; +/** @private */ Queue.prototype._consumeNext = function consumeNext() { if (this[kStopped] || !this[kAvailableCount]) return; @@ -172,6 +174,7 @@ Queue.prototype.consume = function consume(onMessage, consumeOptions, owner) { * @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) { @@ -199,7 +202,7 @@ Queue.prototype.get = function getMessage(options) { if (!message) return false; if (options?.noAck) { this._dequeueMessage(message); - message[kPending] = false; + message._clearPending(); } return message; @@ -244,7 +247,7 @@ Queue.prototype._consumeMessages = function consumeMessages(n, consumeOptions) { * @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(); }; /** @@ -254,7 +257,7 @@ Queue.prototype.ack = function ack(message, allUpTo) { * @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(); }; /** @@ -263,7 +266,7 @@ Queue.prototype.nack = function nack(message, allUpTo, requeue = true) { * @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(); }; /** @@ -492,7 +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').SerializedMessage[] }} */ + /** @type {{name: string, options: import('#types').QueueOptions, messages?: import('./Message.js').MessageEnvelope[] }} */ const state = { name: this.name, options: { ...this.options }, @@ -599,7 +602,7 @@ Queue.prototype._getCapacity = function getCapacity() { * @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 {{ emit(eventName: string, content?: any): any, on(pattern: string, handler: Function): any }} [eventEmitter] internal queue event bridge + * @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'); @@ -660,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) { @@ -675,6 +689,7 @@ Consumer.prototype._push = function push(messages) { } }; +/** @private */ Consumer.prototype._consume = function consume() { const internalQ = this[kInternalQueue]; const consumerTag = this[kName]; 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 4054bca..4b6305d 100644 --- a/src/shared.js +++ b/src/shared.js @@ -26,7 +26,7 @@ EndMatchRoutingKeyPattern.prototype.test = function test(routingKey) { * @returns {RoutingKeyPattern} * * @typedef {object} RoutingKeyPattern - * @property {typeof RegExp.test} test function to test routing key pattern + * @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; diff --git a/test/types-test.js b/test/types-test.js index 35a32a7..11476d2 100644 --- a/test/types-test.js +++ b/test/types-test.js @@ -1,7 +1,7 @@ import fs from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; -import { buildTypes } from '../scripts/build-types.js'; +import { createBundle } from 'dts-buddy'; describe('generated types bundle', () => { let dts; @@ -10,7 +10,11 @@ describe('generated types bundle', () => { this.timeout(20000); const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'smqp-dts-')); const output = path.join(dir, 'index.d.ts'); - await buildTypes(output); + 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 }); }); @@ -149,4 +153,64 @@ describe('generated types bundle', () => { 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/types/index.d.ts b/types/index.d.ts old mode 100755 new mode 100644 index b710a31..8e37df0 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -23,37 +23,107 @@ declare module 'smqp' { * Message properties * */ properties: MessageProperties; - get pending(): boolean; - ack(allUpTo: any): void; - nack(allUpTo: any, requeue?: boolean): void; + 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; - _consume(consumerTag: any, consumedCb: any): void; - [kOnConsumed]: (CallableFunction | null | undefined)[]; - [kPending]: boolean; + private _consume; + private _clearPending; } - const kPending: unique symbol; - type SerializedMessage = Pick; - const kOnConsumed: unique symbol; - export function Shovel(name: any, source: any, destination: any, options: any): Shovel | undefined; + 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 { - constructor(name: any, source: any, destination: any, options: any); - source: any; - destination: any; - events: any; - emit(eventName: any, content: any): void; - on(eventName: any, handler: any, options: any): any; - off(eventName: any, handler: any): any; + /** + * 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; - _messageHandler(message: any): any; - _onShovelMessage(routingKey: any, message: any): any; + private _messageHandler; + private _onShovelMessage; readonly name: string; readonly closed: boolean; readonly consumerTag: string; } - function Exchange2Exchange(shovel: any): void; + /** + * Exchange-to-exchange shovel wrapper, returned by `broker.bindExchange` + * @param shovel underlying shovel + */ + function Exchange2Exchange(shovel: Shovel): void; class Exchange2Exchange { - constructor(shovel: any); - on(...args: any[]): any; + /** + * 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; @@ -69,11 +139,6 @@ declare module 'smqp' { * @param options optional exchange options */ export function Exchange(name: string, type?: exchangeType, options?: ExchangeOptions): ExchangeBase; - /** - * Event exchange - * @param name optional event exchange name, defaults to smq.ename- - */ - function EventExchange(name?: string): ExchangeBase; /** * Exchange * @param name name @@ -90,14 +155,34 @@ declare module 'smqp' { options: ExchangeOptions; events: ExchangeBase | undefined; - publish(routingKey: any, content: any, properties: any): any; - _onTopicMessage(routingKey: any, message: any): number; - - _onDirectMessage(routingKey: string, message: Message): 0 | 1; - _emitReturn(routingKey: any, content: any, properties: any): void; - bindQueue(queue: any, pattern: any, bindOptions: any): any; - unbindQueue(queue: any, pattern: any): void; - unbindQueueByName(queueName: any): void; + /** + * 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?: { @@ -111,7 +196,7 @@ declare module 'smqp' { deliveryQueue?: { name: string; options: QueueOptions; - messages?: SerializedMessage[]; + messages?: MessageEnvelope[]; } | undefined; name: string; type: exchangeType; @@ -122,12 +207,44 @@ declare module 'smqp' { }; }; stop(): void; - recover(state: any, getQueue: any): this | undefined; - getBinding(queueName: any, pattern: any): any; - emit(eventName: any, content: any): any; - on(pattern: any, handler: any, consumeOptions: any): any; - off(pattern: any, handler: any): any; - closeBinding(binding: any): 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; @@ -160,9 +277,9 @@ declare module 'smqp' { */ type RoutingKeyPattern = { /** - * function to test routing key pattern + * method to test a routing key against the pattern; receiver-bound — destructuring is unsupported */ - test: typeof RegExp.test; + test: (this: RoutingKeyPattern, routingKey: string) => boolean; }; /** * Smqp message broker @@ -176,7 +293,7 @@ declare module 'smqp' { */ constructor(owner?: any); owner: any; - events: ExchangeBase; + events: ExchangeEventEmitter; /** * Subscribe to exchange via queue * @param exchangeName exhange name @@ -185,7 +302,7 @@ declare module 'smqp' { * @param onMessage message handlers * @param options optional subscribe options */ - subscribe(exchangeName: string, pattern: string, queueName: string, onMessage: onMessage, options?: SubscribeOptions): any; + subscribe(exchangeName: string, pattern: string, queueName: string, onMessage: onMessage, options?: SubscribeOptions): Consumer; /** * Subscribe to exchange via temporary, non-durable queue * @param exchangeName exchange name @@ -193,7 +310,7 @@ declare module 'smqp' { * @param onMessage message handler * @param options optional subscribe options */ - subscribeTmp(exchangeName: string, pattern: string, onMessage: onMessage, options?: SubscribeOptions): any; + subscribeTmp(exchangeName: string, pattern: string, onMessage: onMessage, options?: SubscribeOptions): Consumer; /** * Subscribe once to first matching message, then auto-cancel. * @@ -206,7 +323,7 @@ declare module 'smqp' { * @param onMessage message handler * @param options optional subscribe options */ - subscribeOnce(exchangeName: string, pattern: string, onMessage: onMessage, options?: SubscribeOptions): any; + subscribeOnce(exchangeName: string, pattern: string, onMessage: onMessage, options?: SubscribeOptions): Consumer; /** * Cancel consumer matching queue + handler * @param queueName queue name @@ -219,7 +336,7 @@ declare module 'smqp' { * @param type exchange type, defaults to topic * @param options optional exchange options */ - assertExchange(exchangeName: string, type?: exchangeType, options?: ExchangeOptions): any; + assertExchange(exchangeName: string, type?: exchangeType, options?: ExchangeOptions): ExchangeBase | undefined; /** * Bind queue to exchange with routing key pattern * @param queueName queue name @@ -241,29 +358,36 @@ declare module 'smqp' { * @param onMessage message handler * @param options optional consume options */ - consume(queueName: string, onMessage: onMessage, options?: ConsumeOptions): any; + 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: any; - consumerTag: any; - ready: any; - options: any; + 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): any; + * */ + getConsumer(consumerTag: string): Consumer | undefined; /** * Get exchange by name * @param exchangeName exchange name - */ - getExchange(exchangeName: string): any; + * */ + getExchange(exchangeName: string): ExchangeBase | undefined; /** * Delete exchange * @param exchangeName exchange name @@ -301,7 +425,7 @@ declare module 'smqp' { deliveryQueue?: { name: string; options: QueueOptions; - messages?: SerializedMessage[]; + messages?: MessageEnvelope[]; } | undefined; name: string; type: exchangeType; @@ -314,7 +438,7 @@ declare module 'smqp' { queues: { name: string; options: QueueOptions; - messages?: SerializedMessage[]; + messages?: MessageEnvelope[]; }[] | undefined; } | undefined; /** @@ -349,14 +473,14 @@ declare module 'smqp' { * Purge all non-pending messages from queue * @param queueName queue name */ - purgeQueue(queueName: string): any; + 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): any; + sendToQueue(queueName: string, content: any, options?: MessageProperties): number | undefined; private _getQueuesState; private _getExchangeState; /** @@ -368,20 +492,22 @@ declare module 'smqp' { /** * Get queue by name * @param queueName queue name - */ - getQueue(queueName: string): any; + * */ + 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): any; + assertQueue(queueName?: string, options?: QueueOptions): Queue; /** * Delete queue * @param queueName queue name * @param options optional delete guards */ - deleteQueue(queueName: string, options?: DeleteQueueOptions): any; + deleteQueue(queueName: string, options?: DeleteQueueOptions): { + messageCount: number; + } | undefined; /** * Get one message from queue * @param queueName queue name @@ -435,10 +561,12 @@ declare module 'smqp' { /** * Get shovel by name * @param name shovel name - */ - getShovel(name: string): any; - /** List all shovels */ - getShovels(): any[]; + * */ + getShovel(name: string): Shovel | undefined; + /** + * List all shovels + * */ + getShovels(): Shovel[]; /** * Subscribe to broker event * @param eventName event name pattern @@ -461,16 +589,28 @@ declare module 'smqp' { readonly queueCount: number; readonly consumerCount: number; } - export function Queue(name: string, options: QueueOptions, eventEmitter: EventExchange): void; + /** + * 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 { - - constructor(name: string, options: QueueOptions, eventEmitter: EventExchange); + /** + * 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: EventExchange; - _onMessageConsumed: any; + events: ExchangeEventEmitter | undefined; + + private _onMessageConsumed; /** * Enqueue a message * @param fields message fields @@ -483,7 +623,7 @@ declare module 'smqp' { * @param compareMessage message to compare against the evicted one */ evictFirst(compareMessage?: Message): boolean | undefined; - _consumeNext(): number | undefined; + private _consumeNext; /** * Add a consumer * @param onMessage message handler @@ -497,7 +637,7 @@ declare module 'smqp' { * @param consumeOptions optional consume options * @param owner forwarded to the message handler as the third arg */ - assertConsumer(onMessage: onMessage, consumeOptions?: ConsumeOptions, owner?: any): any; + assertConsumer(onMessage: onMessage, consumeOptions?: ConsumeOptions, owner?: any): Consumer; /** * Get next message from queue * @param options optional consume options @@ -579,7 +719,7 @@ declare module 'smqp' { getState(): { name: string; options: QueueOptions; - messages?: SerializedMessage[]; + messages?: MessageEnvelope[]; }; /** * Recover queue from previously captured state @@ -611,10 +751,7 @@ declare module 'smqp' { * @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?: { - emit(eventName: string, content?: any): any; - on(pattern: string, handler: Function): any; - }): void; + export function Consumer(queue: Queue, onMessage: onMessage, options?: ConsumeOptions, owner?: any, eventEmitter?: ExchangeEventEmitter): void; export class Consumer { /** * Queue consumer @@ -624,10 +761,7 @@ declare module 'smqp' { * @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?: { - emit(eventName: string, content?: any): any; - on(pattern: string, handler: Function): any; - }); + constructor(queue: Queue, onMessage: onMessage, options?: ConsumeOptions, owner?: any, eventEmitter?: ExchangeEventEmitter); options: { noAck: boolean; consumerTag?: string; @@ -638,12 +772,22 @@ declare module 'smqp' { queue: Queue; onMessage: onMessage; owner: any; - events: { - emit(eventName: string, content?: any): any; - on(pattern: string, handler: Function): any; - } | undefined; - _push(messages: any): void; - _consume(): void; + 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 @@ -684,6 +828,17 @@ declare module 'smqp' { } 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 { @@ -769,7 +924,7 @@ declare module 'smqp' { interface QueueState { name: string; options: QueueOptions; - messages?: MessageEnvelope[]; + messages?: MessageEnvelope_1[]; } interface ExchangeState { @@ -816,14 +971,14 @@ declare module 'smqp' { 'shovel-name'?: string; } - interface MessageEnvelope { + interface MessageEnvelope_1 { fields: MessageFields; content?: any; properties: MessageProperties; } interface ShovelOptions { - cloneMessage?: (message: MessageEnvelope) => MessageEnvelope; + cloneMessage?: (message: MessageEnvelope_1) => MessageEnvelope_1; [x: string]: any; } @@ -894,7 +1049,7 @@ declare module 'smqp' { }; } - export {}; + 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 index 9d7e2ef..d1159dc 100644 --- a/types/interfaces.d.ts +++ b/types/interfaces.d.ts @@ -1,5 +1,16 @@ 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 { From 4e81be1771cbd16ced36ed1b0bda19c9b6581065 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Edman?= Date: Fri, 1 May 2026 07:45:38 +0200 Subject: [PATCH 6/8] node 22 and build commonjs with rollup --- .babelrc | 12 - .github/workflows/build.yaml | 3 + .github/workflows/release.yaml | 4 +- .gitignore | 2 + .nvmrc | 2 +- API.md | 197 +++++---- CHANGELOG.md | 6 +- LICENSE | 2 +- appveyor.yml | 2 +- dist/Binding.js | 58 --- dist/Broker.js | 694 ----------------------------- dist/Errors.js | 24 -- dist/Exchange.js | 371 ---------------- dist/Message.js | 104 ----- dist/Queue.js | 767 --------------------------------- dist/Shovel.js | 257 ----------- dist/index.js | 80 ---- dist/package.json | 3 - dist/shared.js | 52 --- eslint.config.js | 5 +- package.json | 30 +- rollup.config.js | 20 + scripts/generate-api-toc.js | 36 -- scripts/toc.js | 102 +++++ tsconfig.json | 10 +- types/Broker.d.ts | 123 ------ types/Errors.d.ts | 18 - types/Exchange.d.ts | 71 --- types/Message.d.ts | 57 --- types/Queue.d.ts | 93 ---- types/Shovel.d.ts | 54 --- types/shared.d.ts | 6 - types/types.d.ts | 19 - 33 files changed, 261 insertions(+), 3023 deletions(-) delete mode 100644 .babelrc delete mode 100644 dist/Binding.js delete mode 100644 dist/Broker.js delete mode 100644 dist/Errors.js delete mode 100644 dist/Exchange.js delete mode 100644 dist/Message.js delete mode 100644 dist/Queue.js delete mode 100644 dist/Shovel.js delete mode 100644 dist/index.js delete mode 100644 dist/package.json delete mode 100644 dist/shared.js create mode 100644 rollup.config.js delete mode 100644 scripts/generate-api-toc.js create mode 100644 scripts/toc.js delete mode 100644 types/Broker.d.ts delete mode 100644 types/Errors.d.ts delete mode 100644 types/Exchange.d.ts delete mode 100644 types/Message.d.ts delete mode 100644 types/Queue.d.ts delete mode 100644 types/Shovel.d.ts delete mode 100644 types/shared.d.ts delete mode 100644 types/types.d.ts 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 5469c23..913e814 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -22,6 +22,9 @@ jobs: node-version: ${{ matrix.node-version }} - run: npm i - 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 f0e297b..f42f20b 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -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 af42f6d..2d20306 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,5 @@ package-lock.json # 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/API.md b/API.md index 32ca31b..8ee6bca 100644 --- a/API.md +++ b/API.md @@ -4,105 +4,104 @@ 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.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) + + # API reference diff --git a/CHANGELOG.md b/CHANGELOG.md index bf4994a..b1ec7f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,11 @@ ## Unreleased -## v12.0.0 - 2026-04-29 +## v12.0.0 - 2026-05-01 -- refactor types by using dts-buddy +- 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 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/Binding.js b/dist/Binding.js deleted file mode 100644 index b4379e0..0000000 --- a/dist/Binding.js +++ /dev/null @@ -1,58 +0,0 @@ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.Binding = Binding; -var _shared = require("./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] - */ -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 = (0, _shared.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 - }; -}; \ No newline at end of file diff --git a/dist/Broker.js b/dist/Broker.js deleted file mode 100644 index f3a0486..0000000 --- a/dist/Broker.js +++ /dev/null @@ -1,694 +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'); - -/** - * Smqp message broker - * @param {any} [owner] optional broker owner, forwarded to message consumer - */ -function Broker(owner) { - if (!(this instanceof Broker)) { - return new Broker(owner); - } - this.owner = owner; - /** @type {import('#types').ExchangeEventEmitter} */ - 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; - } - } -}); - -/** - * 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'); - 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); -}; - -/** - * 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); - 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); - } -}; - -/** - * 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) { - 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; -}; - -/** - * 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; - const queue = this.getQueue(queueName); - if (!queue) return; - 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 _Errors.SmqpError(`Queue with name <${queueName}> was not found`, _Errors.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; - consumer.cancel(requeue); - 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(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; - this[kEntities].get('exchanges').delete(exchangeName); - exchange.close(); - 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(); - for (const exchange of entities.get('exchanges').values()) exchange.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(); - const entities = this[kEntities]; - entities.get('exchanges').clear(); - entities.get('queues').clear(); - entities.get('consumers').clear(); - 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); - if (onlyWithContent && !exchanges && !queues) return; - return { - exchanges, - queues - }; -}; - -/** - * 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) { - 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; -}; - -/** - * 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(name, { - broker: this, - exchange: source, - pattern, - priority: args?.priority, - consumerTag: `smq.ctag-${name}` - }, { - broker: this, - exchange: destination - }, { - ...args - }); - return new _Shovel.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 _Errors.SmqpError(`Queue with name <${queueName}> was not found`, _Errors.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; - /** @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 = []; - result.push(queue.getState()); - } - return result; -}; - -/** - * @private - * @param {boolean} [onlyWithContent] skip exchanges without undelivered messages - */ -Broker.prototype._getExchangeState = function getExchangeState(onlyWithContent) { - let result; - /** @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 = []; - result.push(exchange.getState()); - } - 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-${(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; -}; - -/** - * 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); - 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; -}; - -/** - * 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; - 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 _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; -}; - -/** - * Close shovel by name - * @param {string} name shovel name - */ -Broker.prototype.closeShovel = function closeShovel(name) { - const shovel = this.getShovel(name); - if (shovel) { - shovel.close(); - return true; - } - 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 - }); - function getEventCallback() { - return function eventCallback(name, msg) { - callback({ - name, - ...msg.content - }); - }; - } -}; - -/** - * 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) { - 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 8f28e8a..0000000 --- a/dist/Exchange.js +++ /dev/null @@ -1,371 +0,0 @@ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.EventExchange = EventExchange; -exports.Exchange = Exchange; -exports.ExchangeBase = ExchangeBase; -var _Message = require("./Message.js"); -var _Queue = require("./Queue.js"); -var _Binding = require("./Binding.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'); - -/** @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 - */ -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); -} - -/** - * Event exchange - * @param {string} [name] optional event exchange name, defaults to smq.ename- - */ -function EventExchange(name) { - if (!name) name = `smq.ename-${(0, _shared.generateId)()}`; - return new ExchangeBase(name, 'topic', { - durable: false, - autoDelete: true - }); -} - -/** - * Exchange - * @param {string} name name - * @param {import('#types').exchangeType} type - * @param {import('#types').ExchangeOptions} [options] - * @param {ExchangeBase} [eventExchange] - */ -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.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; - } - } -}); - -/** - * 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); - return this[kDeliveryQueue].queueMessage({ - routingKey - }, { - content, - properties - }); -}; - -/** @private */ -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; -}; - -/** - * @private - * @param {string} routingKey - * @param {Message} message - */ -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; -}; - -/** @private */ -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)); - } -}; - -/** - * 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]; - for (const binding of bindings) { - if (binding.queue === queue && binding.pattern === pattern) return binding; - } - const binding = new _Binding.Binding(this, queue, pattern, bindOptions); - if (bindings.push(binding) > 1 && binding.options.priority) { - bindings.sort(_shared.sortByPriority); - } - 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) { - return this.closeBinding(binding); - } - } -}; - -/** - * 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); - } -}; -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() { - /** @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, - type: this.type, - options: { - ...this.options - }, - ...(deliveryQueue.messageCount && { - deliveryQueue: deliveryQueue.getState() - }), - ...(bindingsState.length && { - bindings: bindingsState - }) - }; -}; -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; - 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; -}; - -/** - * 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); - 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); -}; - -/** - * 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); - const { - consumerTag - } = handler; - for (const binding of this[kBindings]) { - if (binding.pattern === pattern) { - if (consumerTag) binding.queue.cancel(consumerTag);else binding.queue.dismiss(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); - if (idx === -1) return; - bindings.splice(idx, 1); - binding.close(); - if (!bindings.length && this.options.autoDelete) this.emit('delete', this); -}; \ No newline at end of file diff --git a/dist/Message.js b/dist/Message.js deleted file mode 100644 index 3de2977..0000000 --- a/dist/Message.js +++ /dev/null @@ -1,104 +0,0 @@ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.Message = Message; -var _shared = require("./shared.js"); -/** @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] - */ -function Message(fields, content, properties, onConsumed) { - /** @private */ - 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; - - /** - * 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; -} -Object.defineProperty(Message.prototype, 'pending', { - get() { - return this[kPending]; - } -}); - -/** - * 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]) { - if (fn) fn(this, '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]) { - if (fn) fn(this, 'nack', allUpTo, requeue); - } - 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; -}; \ No newline at end of file diff --git a/dist/Queue.js b/dist/Queue.js deleted file mode 100644 index ebf2869..0000000 --- a/dist/Queue.js +++ /dev/null @@ -1,767 +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'); - -/** - * 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 - */ -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; - - /** @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); -} -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]; - } - } -}); - -/** - * 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'); - 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(); -}; - -/** - * 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; - evict.nack(false, false); - return evict === compareMessage; -}; - -/** @private */ -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; -}; - -/** - * 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) { - 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; -}; - -/** - * 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) { - 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); -}; - -/** - * 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._clearPending(); - } - return message; -}; - -/** - * @private - * @param {number} n - * @param {import('#types').ConsumeOptions} consumeOptions - */ -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; -}; - -/** - * 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._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._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._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; - 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); - } -}; - -/** - * 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; - 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; -}; - -/** - * 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; - if (!ignoreDelivered) return message; - if (!message.pending) return message; - for (const msg of this.messages) { - if (!msg.pending) return msg; - } -}; - -/** - * 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); - if (idx === -1) return false; - const consumer = consumers[idx]; - this.unbindConsumer(consumer, 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); - if (!consumer) return; - 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); - 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); -}; - -/** - * 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); -}; -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; -}; - -/** - * @private - * @param {Message} message - */ -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; - /** @type {{name: string, options: import('#types').QueueOptions, messages?: import('./Message.js').MessageEnvelope[] }} */ - 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; -}; - -/** - * 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]; - 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; -}; - -/** - * 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; - 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(); - } -}; - -/** - * @private - */ -Queue.prototype._getCapacity = function getCapacity() { - if ('maxLength' in this.options) { - return this.options.maxLength - this.messages.length; - } - 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 - */ -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; - } - } -}); - -/** 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) { - internalQueue.queueMessage(message.fields, message, message.properties); - } - if (!this[kConsuming]) { - this[kConsuming] = true; - try { - this._consume(); - } finally { - this[kConsuming] = false; - } - } -}; - -/** @private */ -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; - } -}; - -/** - * 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); -}; -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 91c1d05..0000000 --- a/dist/Shovel.js +++ /dev/null @@ -1,257 +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"); -/** @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 - */ -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 - }; - /** @type {import('#types').ExchangeEventEmitter} */ - 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]; - } - } -}); - -/** - * 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; - 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]); -}; - -/** @private */ -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 - } - }; -}; - -/** @private */ -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(); -}; - -/** - * Exchange-to-exchange shovel wrapper, returned by `broker.bindExchange` - * @param {Shovel} shovel underlying shovel - */ -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; - } - } -}); - -/** - * 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(); -}; \ No newline at end of file diff --git a/dist/index.js b/dist/index.js deleted file mode 100644 index 6effbf8..0000000 --- a/dist/index.js +++ /dev/null @@ -1,80 +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; - } -}); -Object.defineProperty(exports, "default", { - enumerable: true, - get: function () { - return _Broker.Broker; - } -}); -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"); \ 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 ee0058d..0000000 --- a/dist/shared.js +++ /dev/null @@ -1,52 +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); -}; - -/** - * 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 - */ -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 69902f5..06d56c9 100644 --- a/package.json +++ b/package.json @@ -7,15 +7,17 @@ "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": { @@ -31,16 +33,19 @@ "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": "dts-buddy && babel src/**.js -d dist", + "dist": "rollup -c && dts-buddy && npm run toc", "types": "dts-buddy", - "prepack": "npm run dist", - "toc": "node ./scripts/generate-api-toc.js", + "prepublishOnly": "npm run dist", + "toc": "node ./scripts/toc.js API.md", "lint": "eslint . --cache && prettier . -c --cache" }, "keywords": [ @@ -57,20 +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": "^8.0.0", "dts-buddy": "^0.7.0", - "eslint": "^9.10.0", - "markdown-toc": "^1.2.0", + "eslint": "^10.2.1", + "globals": "^17.5.0", "mocha": "^11.0.1", "prettier": "^3.2.5", + "rollup": "^4.60.2", "texample": "^1.0.0" } } 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/tsconfig.json b/tsconfig.json index 849d7ad..71fa019 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,24 +4,22 @@ "emitDeclarationOnly": true, "sourceMap": false, "rootDir": ".", - "lib": ["es2017"], - "target": "es2017", + "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/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/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; -} From bd0a3fea43be8490e9169b79a163787593fe6482 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Edman?= Date: Fri, 1 May 2026 08:11:08 +0200 Subject: [PATCH 7/8] update api documentation --- API.md | 79 ++++++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 60 insertions(+), 19 deletions(-) diff --git a/API.md b/API.md index 8ee6bca..9924434 100644 --- a/API.md +++ b/API.md @@ -39,7 +39,9 @@ The api is inspired by the amusing [`amqplib`](https://github.com/squaremo/amqp. - [`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) @@ -96,6 +98,9 @@ The api is inspired by the amusing [`amqplib`](https://github.com/squaremo/amqp. - [`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) @@ -129,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 @@ -166,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 @@ -173,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)` @@ -243,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])` @@ -273,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). @@ -330,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). @@ -369,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)` @@ -391,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])` @@ -452,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. @@ -522,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()` @@ -540,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])` @@ -600,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. @@ -652,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 @@ -782,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()` @@ -840,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])` @@ -962,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. From 19b8a4028353430b7b75d76fb6943f1d4336972f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Edman?= Date: Fri, 1 May 2026 09:03:57 +0200 Subject: [PATCH 8/8] use latest texample --- .github/workflows/build.yaml | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 913e814..b821599 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -20,7 +20,7 @@ jobs: 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 diff --git a/package.json b/package.json index 06d56c9..48c9b11 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,6 @@ "mocha": "^11.0.1", "prettier": "^3.2.5", "rollup": "^4.60.2", - "texample": "^1.0.0" + "texample": "^1.0.1" } }