UNPKG

iambus

Version:

minimalist pattern-matching pub-sub message-bus

119 lines (104 loc) 2.99 kB
'use strict' const assert = require('nanoassert') const streamx = require('streamx') const FALLBACK_CUTOVER_DELAY = 180_000 // 3 minutes class Subscriber extends streamx.PassThrough { _timeout = null constructor (bus, pattern, opts = {}) { super(opts) this.bus = bus this.bus.subscribers.add(this) this.pattern = pattern this.queue = [] this.max = opts.max ?? 32 this.retain = opts.retain ?? false if (this.retain) this.cutover(FALLBACK_CUTOVER_DELAY) this.opts = opts } _cutover = (after) => { this.queue.length = 0 clearTimeout(this._timeout) this._timeout = null this.off('close', this._cutover) this.emit('cutover', after) } cutover (after = 0) { if (this._timeout !== null) { clearTimeout(this._timeout) this._timeout = null this.off('close', this._cutover) } this._timeout = setTimeout(this._cutover, after, after) this.once('close', this._cutover) } feed (subscriber) { if (this.bus.subscribers.has(subscriber) === false) return subscriber for (const message of this.queue) { subscriber.pushOnMatch(message) } return subscriber } push (message) { if (this.retain) { this.queue.push(message) // usage should avoid this case, but just in case, lose oldest: if (this.queue.length > this.max) this.queue.shift() } return super.push(message) } pushOnMatch (message) { if (match(message, this.pattern)) { this.push(message) } } end () { this.push(null) } _destroy (cb) { this.bus.subscribers.delete(this) this.once('cutover', () => { cb() }) this.cutover(0) } } module.exports = class Iambus { static match = match static Subscriber = Subscriber constructor ({ onsub = () => {} } = {}) { this.subscribers = new Set() this._onsub = onsub } pub (message) { for (const subscriber of this.subscribers) { subscriber.pushOnMatch(message) } } sub (pattern = {}, opts) { assert(typeof pattern === 'object' && pattern !== null, 'Pass a pattern object: bus.sub(pattern)') const subscriber = new Subscriber(this, pattern, opts) this._onsub(subscriber) return subscriber } destroy () { for (const subscriber of this.subscribers) { subscriber.destroy() } } } function match (message, pattern) { if (typeof pattern !== 'object' || pattern === null) return false for (const key in pattern) { if (Object.hasOwn(pattern, key) === false) continue if (Object.hasOwn(message, key) === false) return false const messageValue = message[key] const patternValue = pattern[key] const nested = typeof patternValue === 'object' && patternValue !== null && typeof messageValue === 'object' && messageValue !== null if (nested) { if (match(messageValue, patternValue) === false) return false } else if (messageValue !== patternValue) { return false } } return true }