UNPKG

smqp

Version:

Synchronous message queueing package

301 lines (248 loc) 8.76 kB
import { Message } from './Message.js'; import { Queue } from './Queue.js'; import { sortByPriority, getRoutingKeyPattern, generateId } from './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'); export 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); } export function EventExchange(name) { if (!name) name = `smq.ename-${generateId()}`; return new ExchangeBase(name, 'topic', { durable: false, autoDelete: true }); } function ExchangeBase(name, type, options, eventExchange) { this[kName] = name; this[kType] = type; this[kBindings] = []; this[kStopped] = false; this.options = { durable: true, autoDelete: true, ...options }; this.events = eventExchange; const deliveryQueue = (this[kDeliveryQueue] = new Queue('delivery-q', { autoDelete: false })); const onMessage = (type === 'topic' ? this._onTopicMessage : this._onDirectMessage).bind(this); deliveryQueue.consume(onMessage, { exclusive: true, consumerTag: '_exchange-tag' }); } Object.defineProperties(ExchangeBase.prototype, { name: { get() { return this[kName]; }, }, bindingCount: { get() { return this[kBindings].length; }, }, bindings: { get() { return this[kBindings].slice(); }, }, type: { get() { return this[kType]; }, }, stopped: { get() { return this[kStopped]; }, }, undeliveredCount: { get() { return this[kDeliveryQueue].messageCount; }, }, }); ExchangeBase.prototype.publish = function publish(routingKey, content, properties) { if (this[kStopped]) return; if (!this[kBindings].length) return this._emitReturn(routingKey, content, properties); return this[kDeliveryQueue].queueMessage( { routingKey }, { content, properties, } ); }; ExchangeBase.prototype._onTopicMessage = function topic(routingKey, message) { const publishedMsg = message.content; message.ack(); let delivered = 0; for (const binding of this[kBindings].slice()) { if (!binding.testPattern(routingKey)) continue; binding.queue.queueMessage({ routingKey, exchange: this.name }, publishedMsg.content, publishedMsg.properties); ++delivered; } if (!delivered) { this._emitReturn(routingKey, publishedMsg.content, publishedMsg.properties); } return delivered; }; ExchangeBase.prototype._onDirectMessage = function direct(routingKey, message) { const publishedMsg = message.content; const bindings = this[kBindings]; let deliverToBinding; for (const binding of bindings) { if (!binding.testPattern(routingKey)) continue; deliverToBinding = binding; break; } if (!deliverToBinding) { message.ack(); this._emitReturn(routingKey, publishedMsg.content, publishedMsg.properties); return 0; } if (bindings.length > 1) { const idx = bindings.indexOf(deliverToBinding); bindings.splice(idx, 1); bindings.push(deliverToBinding); } message.ack(); deliverToBinding.queue.queueMessage({ routingKey, exchange: this.name }, publishedMsg.content, publishedMsg.properties); return 1; }; ExchangeBase.prototype._emitReturn = function emitReturn(routingKey, content, properties) { if (!this.events || !properties) return; if (properties.confirm) { this.emit('message.undelivered', new Message({ routingKey, exchange: this.name }, content, properties)); } if (properties.mandatory) { this.emit('return', new Message({ routingKey, exchange: this.name }, content, properties)); } }; ExchangeBase.prototype.bindQueue = function bindQueue(queue, pattern, bindOptions) { const bindings = this[kBindings]; for (const binding of bindings) { if (binding.queue === queue && binding.pattern === pattern) return binding; } const binding = new Binding(this, queue, pattern, bindOptions); if (bindings.push(binding) > 1 && binding.options.priority) { bindings.sort(sortByPriority); } return binding; }; ExchangeBase.prototype.unbindQueue = function unbindQueue(queue, pattern) { for (const binding of this[kBindings]) { if (binding.queue === queue && binding.pattern === pattern) { return this.closeBinding(binding); } } }; ExchangeBase.prototype.unbindQueueByName = function unbindQueueByName(queueName) { for (const binding of this[kBindings]) { if (binding.queue.name === queueName) this.closeBinding(binding); } }; ExchangeBase.prototype.close = function close() { for (const binding of this[kBindings]) { binding.close(); } const deliveryQueue = this[kDeliveryQueue]; deliveryQueue.cancel('_exchange-tag'); deliveryQueue.close(); }; ExchangeBase.prototype.getState = function getState() { const bindingsState = []; for (const binding of this[kBindings]) { if (!binding.queue.options.durable) continue; bindingsState.push(binding.getState()); } const deliveryQueue = this[kDeliveryQueue]; return { name: this.name, type: this.type, options: { ...this.options }, ...(deliveryQueue.messageCount && { deliveryQueue: deliveryQueue.getState() }), ...(bindingsState.length && { bindings: bindingsState }), }; }; ExchangeBase.prototype.stop = function stop() { this[kStopped] = true; }; ExchangeBase.prototype.recover = function recover(state, getQueue) { this[kStopped] = false; if (!state) return this; const deliveryQueue = this[kDeliveryQueue]; if (state.bindings) { for (const bindingState of state.bindings) { const queue = getQueue(bindingState.queueName); if (!queue) return; this.bindQueue(queue, bindingState.pattern, bindingState.options); } } deliveryQueue.recover(state.deliveryQueue); if (!deliveryQueue.consumerCount) { const onMessage = (this[kType] === 'topic' ? this._onTopicMessage : this._onDirectMessage).bind(this); deliveryQueue.consume(onMessage, { exclusive: true, consumerTag: '_exchange-tag' }); } return this; }; ExchangeBase.prototype.getBinding = function getBinding(queueName, pattern) { for (const binding of this[kBindings]) { if (binding.queue.name === queueName && binding.pattern === pattern) return binding; } }; ExchangeBase.prototype.emit = function emit(eventName, content) { if (this.events) return this.events.publish(`exchange.${eventName}`, content); return this.publish(eventName, content); }; ExchangeBase.prototype.on = function on(pattern, handler, consumeOptions) { if (this.events) return this.events.on(`exchange.${pattern}`, handler, consumeOptions); const eventQueue = new Queue(null, { durable: false, autoDelete: true }); const binding = this.bindQueue(eventQueue, pattern); eventQueue.events = { emit(eventName) { if (eventName === 'queue.delete') binding.close(); }, }; return eventQueue.consume(handler, { ...consumeOptions, noAck: true }, this); }; ExchangeBase.prototype.off = function off(pattern, handler) { if (this.events) return this.events.off(`exchange.${pattern}`, handler); const { consumerTag } = handler; for (const binding of this[kBindings]) { if (binding.pattern === pattern) { if (consumerTag) binding.queue.cancel(consumerTag); else binding.queue.dismiss(handler); } } }; ExchangeBase.prototype.closeBinding = function closeBinding(binding) { const bindings = this[kBindings]; const idx = bindings.indexOf(binding); if (idx === -1) return; bindings.splice(idx, 1); binding.close(); if (!bindings.length && this.options.autoDelete) this.emit('delete', this); }; function Binding(exchange, queue, pattern, bindOptions) { this.id = `${queue.name}/${pattern}`; this.options = { priority: 0, ...bindOptions }; this.pattern = pattern; this.exchange = exchange; this.queue = queue; this._compiledPattern = getRoutingKeyPattern(pattern); queue.on('delete', () => { this.close(); }); } Binding.prototype.testPattern = function testPattern(routingKey) { return this._compiledPattern.test(routingKey); }; Binding.prototype.close = function closeBinding() { this.exchange.unbindQueue(this.queue, this.pattern); }; Binding.prototype.getState = function getBindingState() { return { id: this.id, options: { ...this.options }, queueName: this.queue.name, pattern: this.pattern, }; };