UNPKG

topic-subscribe

Version:

Universal PubSub module for node (including old versions) and browser with subscription filtering and broadcasting.

1,055 lines (944 loc) 35.9 kB
'use strict'; /** * Turn the given value into an array. If it is already an array then return it; if it is a set then convert to an * array; and if neither then return as the first item in an array. The purpose of this function is for function * or method parameters where they can be either a array or not. You can use this to ensure you are working on * an array. * * @public * @param {Array|Set|*} value Value to return or convert. * @returns {Array} The converted value (or original if already an array). */ function makeArray(value) { if (value === undefined) return []; if (value instanceof Set) return [...value]; return Array.isArray(value) ? value : [value]; } /** * Test whether the given value a string? * * @public * @param {*} value Value to test. * @returns {boolean} Is it a string? */ function isString(value) { return typeof value === 'string'; } /** * Is the given value a function. * * @public * @param {*} value Value to test. * @returns {boolean} Is it a function? */ function isFunction(value) { return !!(value && value.constructor && value.call && value.apply); } /** * Test whether the given value a standard object (and not null). * * @public * @param {*} value Value to test. * @returns {boolean} Is it an object? */ function isObject(value) { return Object.prototype.toString.call(value) === '[object Object]' && !(value === null); } /** * Is the given value a RegExp. * * @public * @param {*} value Value to test. * @returns {boolean} Is it a RegExp? */ function isRegExp(value) { return value instanceof RegExp; } /** * Lop an item of the end of a string with the given separator. The separator defaults to '/' for handling * file-like paths. * * @public * @param {string} text Text to perform lop on. * @param {string} [separator='/'] The separator to use. * @returns {string} Lopped string. */ function lop(text, separator = '/') { const parts = text.split(separator); parts.pop(); return parts.join(separator); } /** * Generator for given lop operation. * * @public * @generator * @param {string} text Text to perform lopping on. * @param {string} [separator='/'] The separator to use. * @yields {string} Lopped text. */ function* lopper(text, separator = '/') { let _text = text; let last = _text; while (_text.length) { last = _text; yield _text; _text = lop(_text, separator = '/'); } if (last !== '/') yield '/'; } /** * Get all the property names (including inherited) of the given object. * * @public * @param {Object} obj The object to get properties for. * @returns {Set} A set of all the property names. */ function getAllPropertyNames(obj) { const all = new Set(); do { Object.getOwnPropertyNames(obj).forEach(property => all.add(property)); } while (obj = Object.getPrototypeOf(obj)); return all; } /** * Perform a clone (with deep option) of given object. Methods are bound to the cloned object. * * @note This will not clone getter/setters but only their value at clone time. It does not climb the prototype chain, * so no inherited properties are cloned. * * @param {Object} obj Object to clone. * @param {boolean} [deep=false] Do a deep clone? * @returns {Object} Cloned object. */ function clone(obj, deep = false) { const _obj = {}; Object.getOwnPropertyNames(obj).forEach(property => { if (isFunction(obj[property])) { _obj[property] = obj[property].bind(_obj); } else if (!deep || Array.isArray(obj[property]) || !isObject(obj[property])) { _obj[property] = obj[property]; } else { _obj[property] = clone(obj[property], deep); } }); return _obj; } const $private = new WeakMap(); /** * Given a collection, test whether an item exists and if not use the given constructor and constructor parameters * to create the value. The collection can be a Set, Map, Array or Object. * * @param {Map|Set|Array|Object} collection The collection to test and set on. * @param {string} item The item to set. * @param {Object} constructor The constructor to use. * @param {Array.<*>} [...constructorParams] The parameters to use on the constructor. * @returns {*} */ function hasConstruct(collection, item, constructor, ...constructorParams) { if (constructor) { if (collection.has && !collection.has(item)) { if (collection.set) return collection.set(item, new constructor(...constructorParams)); if (collection.add) return collection.add(new constructor(...constructorParams)); } else if (Array.isArray(collection) && !collection.includes(item)) { collection.push(new constructor(...constructorParams)); } else if (!getAllPropertyNames(collection).has(item)) { collection[item] = new constructor(...constructorParams); } } } /** * Class for maintaining private values in a given class. * * @class */ class Private { /** * Get a given private value on the given class instance. A constructor can be supplied with construct * arguments if the value does not exist. If no values exist for given instance create a new value map for * future use. * * @static * @method * @public * @param {Object} classInstance The class instance to get for. * @param {string} property The property to get. * @param {Object} [defaultConstructor] The constructor to use for new values. * @param {Array.<*>} [...constructorParams] The parameters to pass to the constructor. * @returns {*} */ static get(classInstance, property, defaultConstructor, ...constructorParams) { hasConstruct($private, classInstance, Map); hasConstruct($private.get(classInstance), property, defaultConstructor, ...constructorParams); return $private.get(classInstance).get(property); } /** * Set a private value on a given class instance. * * @static * @method * @public * @param {Object} classInstance The class instance to set on. * @param {string} property The property to set. * @param {*} value The value to set. * @returns {boolean} Did the value set? */ static set(classInstance, property, value) { hasConstruct($private, classInstance, Map); return $private.get(classInstance).set(property, value); } } //removeIf(browser) //endRemoveIf(browser) const errors = { 'CallbackNotFunction': () => `Expected callback to be a function.`, 'ChannelNotAString': () => `Expected channel to be a channel string (starting with a slash) or regular-expression.`, 'FilterNotAnObject': () => `Expected filter to be an object.` }; /** * Create an error object against the given id. If id supplied then look it up in errors object. * * @public * @param {Object} errorConstructor Error constructor object. * @param {string|function|*} errorId The error id to lookup and use text of. If id is not found assume id is * the text to apply to the error. If id is function run function with * supplied parameters to get error constructor params. * @param params Parameters to run through the function returned by error id lookup. * @returns {Error} New error instance that can be thrown. */ function createError(errorConstructor, errorId, ...params) { if (errors[errorId]) { if (isFunction(errors[errorId])) return new errorConstructor(errors[errorId](...params)); return new errorConstructor(errors[errorId]); } return new errorConstructor(errorId); } //removeIf(browser) 'use strict'; //endRemoveIf(browser) const AT_TARGET = Symbol('Message at target'); const BUBBLING_PHASE = Symbol('Message in bubbling phase'); /** * Message class, this is the class that is passed to listeners, it contains all the given data and other event style * information that might be useful. Each listener get's it's own instance of the class. * * @class */ class Message { /** * Create a new event instance. * * @method * @param {*} message The message being published/broadcast. * @param {Object} options The construction options (including channels). */ constructor(message, options) { message = message || {}; Private.set(this, 'data', message); Private.set(this, 'target', options.target); Private.set(this, 'currentTarget', options.currentTarget); if (options.broadcast) Private.set(this, 'broadcast', !!options.broadcast); if (options.publish) Private.set(this, 'publish', !!options.publish); Private.set(this, 'timestamp', new Date().getTime()); if (message.sourceTimestamp || message.timestamp) { Private.set(this, 'sourceTimestamp', message.sourceTimestamp || message.timestamp); } const atTarget = !!makeArray(options.target).find(channel => isRegExp(channel) ? channel.test(options.currentTarget) : channel === options.currentTarget); Private.set(this, 'eventPhase', atTarget ? Message.AT_TARGET : Message.BUBBLING_PHASE); } /** * The message data. * * @public * @property {*} */ get data() { return Private.get(this, 'data'); } /** * The original channel this was published/broadcast to. * * @public * @property {Array} */ get target() { return Private.get(this, 'target'); } /** * The channel now receiving the message (might be different due to broadcast and publish bubbling). * * @public * @property {Array} */ get currentTarget() { return Private.get(this, 'currentTarget'); } /** * Is the message a broadcast message? * * @public * @property {boolean} */ get broadcast() { return Private.get(this, 'broadcast'); } /** * Is the message a publish message?. * * @public * @property {boolean} */ get publish() { return Private.get(this, 'publish'); } /** * The timestamp on message creation. * * @public * @property {integer} Time in milliseconds. */ get timestamp() { return Private.get(this, 'timestamp'); } /** * The timestamp from original message if a mirrored message. * * @public * @property {integer|undefined} Time in milliseconds or undefined if not available. */ get sourceTimestamp() { return Private.get(this, 'sourceTimestamp'); } /** * The current event phase (bubbling or not). * * @public * @property {Symbol} The phase value. */ get eventPhase() { return Private.get(this, 'eventPhase'); } static get AT_TARGET() { return AT_TARGET; } static get BUBBLING_PHASE() { return BUBBLING_PHASE; } } //removeIf(browser) module.exports = Message; //endRemoveIf(browser) //removeIf(browser) const sift = require('sift'); //endRemoveIf(browser) //removeIf(browser) //endRemoveIf(browser) const listenerToChannel = new WeakMap(); /** * Apply a given action on a given set, against a given channel with the given subscription. * * @param {Set} subscriptions The subscriptions to work on. * @param {string} channel The channel to apply this with. * @param {string} action The action to do. * @param {object} subscription The subscription object to apply. */ function _subscriptionAction(subscriptions, channel, action, subscription) { if (!subscriptions.has(channel)) subscriptions.set(channel, new Set()); subscriptions.get(channel)[action](subscription); } /** * Perform a give action on all on the channels supplied using the subscription value supplied. This is basically, a * way of performing the same action on a sets (eg. add or delete). * * @private * @param {Set} subscriptions The subscriptions to work on. * @param {Array.<string|RegExp>} channels The channels to apply this with. * @param {string} action The action to do. * @param {object} subscription The subscription object to apply. */ function _subscriptionsAction(subscriptions, channels, action, subscription) { channels.forEach(channel => _subscriptionAction(subscriptions, channel, action, subscription)); } /** * Test an array of channels returning true if all channels are correct type and format. Returns false if any of the * channels fail the criteria. * * @private * @param {Array<string|RegExp>} channels Channels to test. * @param {boolean} allowRegExp Do we allow regular expressions for channels? * @returns {boolean} Did they all pass? */ function _allChannelsAreCorrectType(channels, allowRegExp = true) { return channels.filter(channel => isString(channel) ? channel.charAt(0) === '/' : allowRegExp ? isRegExp(channel) : false).length === channels.length; } /** * Generator for all ancestor channels of a given array of channels. * * @private * @generator * @param {Array.<string>} channels Channels to expand. * @yields {string} Channel. */ function* _allAncestorChannels(channels) { const loppers = channels.map(channel => lopper(channel)); while (true) { let done = 0; for (let n = 0; n < loppers.length; n++) { let value = loppers[n].next(); if (!value.done) { yield value.value; } else { done++; } } if (done >= loppers.length) break; } } /** * Get an array of unique ancestor channels from array of channels. * * @private * @param {Array.<string>} channels Channels array. * @returns {Array.<string>} All calculated channels. */ function _uniqueChannels(channels) { const uniqueChannels = new Set(); const lopper = _allAncestorChannels(channels); for (let channel of lopper) uniqueChannels.add(channel); return Array.from(uniqueChannels); } /** * Take a channel string and trim any trailing slashes or empty channel parts. * * @private * @param {sgtring} channel Thr channel to trim. * @returns {string} The trimmed channel. */ function _removingTrailingSlash(channel) { return isString(channel) && channel.charAt(0) === '/' ? '/' + channel.split('/').filter(part => part.trim() !== '').join('/') : channel; } /** * Send a given message to listeners in supplied Set. * * @private * @param {Set.<function>} listeners Listeners to send messages to. * @param {*} message Message to send. * @param {Object} options Options object used in Message constructor * @returns {boolean} Did any listeners receive? */ function _messageListeners(listeners, message, options) { listeners.forEach(listener => { listener(new Message(message, Object.assign({}, options, { currentTarget: getChannelForListener(listeners, listener) }))); }); return !!listeners.size; } function addToFilterSet(filterSet, listener, channel) { if (!filterSet.has(listener)) { if (!listenerToChannel.has(filterSet)) listenerToChannel.set(filterSet, new WeakMap()); listenerToChannel.get(filterSet).set(listener, channel); filterSet.add(listener); } } function getChannelForListener(filterSet, listener) { if (listenerToChannel.has(filterSet)) return listenerToChannel.get(filterSet).get(listener); } /** * Add listeners to a set based on whether the given message passes the subscription filter. * * @private * @param {Set} listeners The listeners to filter. * @param {string} channel Channel listener isattached to. * @param {*} message The message to test against. * @param {Set} filterSet The set to add filtered listeners to. */ function _filterListeners(listeners, channel, message, filterSet) { listeners.forEach(subscription => { if (sift(subscription.filter, [message]).length) { if (!filterSet.has(subscription.listener)) addToFilterSet(filterSet, subscription.listener, channel); } }); } /** * Generator that perform filtered matching from a given lookup of listeners. Run a test function against each channel * and yield listeners for each that passes. * * @private * @generator * @param {Map} listenerLookup Channel lookup to cycle through running channel tests against. * @param {Array.<string|RegExp>} channels Channels. * @param {function} channelTest Channel test function. * @yields {Set} Listeners from matching channel. */ function* _channelMatcher(listenerLookup, channels, channelTest) { for (let [listenerChannel, listeners] of listenerLookup) { const filteredChannels = channels.filter(channel => channelTest(listenerChannel, channel)); if (filteredChannels.length) yield [listenerChannel, listeners]; } } /** * Generator for obtaining listeners for given channels. * * @private * @generator * @param {Map} listenerLookup Lookup to test against. * @param {Array.<string|RegExp>} channels Channels to look for. * @yield {Set} Listeners found. */ function* _inChannelsLookup(listenerLookup, channels) { for (let n = 0; n < channels.length; n++) { if (listenerLookup.has(channels[n])) yield [channels[n], listenerLookup.get(channels[n])]; } } /** * Given a set of channels and a listener lookup map, get the listeners to publish to. * * @private * @param {Map} listenerLookup The listener lookup map. * @param {Array.<string|RegExp>} channels The channels array. * @param {Object} message The message we are publishing. * @returns {Set.<Function>} The listeners. */ function _getPublishTo(listenerLookup, channels, message) { const publishTo = new Set(); const matcher1 = _channelMatcher(listenerLookup, channels, (listenerChannel, channel) => { if (isRegExp(listenerChannel)) return listenerChannel.test(channel); }); const matcher2 = _inChannelsLookup(listenerLookup, channels); for (const [listenerChannel, listeners] of [...matcher1, ...matcher2]) _filterListeners(listeners, listenerChannel, message, publishTo); return publishTo; } /** * Given a set of channels and a listener lookup map, get the listeners to broadcast to. * * @private * @param {Map} listenerLookup The listener lookup map. * @param {Array.<string>} channels The channels array. * @param {Object} message The message we are broadcasting. * @returns {Set.<Function>} The listeners. */ function _getBroadcastTo(listenerLookup, channels, message) { const broadcastTo = new Set(); const matcher = _channelMatcher(listenerLookup, channels, (listenerChannel, channel) => { if (isRegExp(listenerChannel)) return false; return listenerChannel.substr(0, channel.length) === channel; }); for (const [listenerChannel, listeners] of matcher) _filterListeners(listeners, listenerChannel, message, broadcastTo); return broadcastTo; } /** * Given a channel, and a listener lookup map unsubscribe all listeners from it. * * @private * @param {Map} listenerLookup The listener lookup to use. * @param {string|RegExp} channel The channel to unsubscribe listeners from. * @returns {boolean} Did anything unsubscibe? */ function _unsubscribeChannel(listenerLookup, channel) { let unsubscribed = false; if (listenerLookup.has(channel)) { listenerLookup.get(channel).clear(); listenerLookup.delete(channel); unsubscribed = true; } return unsubscribed; } /** * Given a listener and listener lookup map, unsubscribe the listener on every channel it listens on. * * @private * @param {Map} listenerLookup The listener lookup to use. * @param {Function} listener The listener to unsubscribe. * @returns {boolean} Did anything unsubscibe? */ function _unsubscribeListener(listenerLookup, listener) { let unsubscribed = false; listenerLookup.forEach(subscriptions => { subscriptions.forEach(subscription => { if (subscription.listener === listener) { subscriptions.delete(subscription); unsubscribed = true; } }); }); return unsubscribed; } /** * Get an array of channels corrected with no-trailing slashes from given array, string or regular-expression. * * @private * @param {string|Array|set} channels Channel(s) to publish on (including RegExp). * @returns {Array.<string|RegExp>} The channels to publish on. */ function _getCorrectedChannelsArray(channels) { const correctedChannels = makeArray(channels).map(channel => _removingTrailingSlash(channel)); if (!_allChannelsAreCorrectType(correctedChannels, false)) throw createError(TypeError, 'ChannelNotAString'); return correctedChannels; } /** * Parse the given message to make ready for publishing / broadcasting via PubSub. * * @private * @param {Message|Object|*} message The message object to mirror. * @param {Array.<Function>} [parsers=[]] The parsers to use in converting into the correct format. * @param {string} [base=''] The base to prepend to channels. * @returns {Object} The parsed message object. */ function _mirrorMessageParse(message, parsers = [], base = '') { let _message = clone(message || {}); base = isString(parsers) ? parsers : base; (isString(parsers) ? [] : makeArray(parsers)).forEach(parser => { _message = parser(_message, base) || _message; }); _message.target = makeArray(_message.target || []); _message.target = (_message.target.length ? _message.target : base !== '' ? [''] : []).map(channel => { if (isString(channel)) return base + channel; return channel; }); return _message; } function getSourceSubscriber(pubsub, name) { const sourceSubscribers = Private.get(pubsub, 'sourceSubscribers', Map); const staticSourceSubscribers = Private.get(PubSub, 'sourceSubscribers', Map); return sourceSubscribers.get(name) || staticSourceSubscribers.get(name); } function _sourceGeneric(emitter, method, channel, params) { params.forEach(params => emitter[method](channel, ...params)); } function _getOnParams(pubsub, channel, options) { return makeArray(channel).map(channel => { return [(message, ...params) => { pubsub.mirror(message, options.parsers, options.base); }]; }); } /** * Given an subscription method (like PubSub) on an emitter, mirror given events to pubsub instance. The method name is * given via options.on. * * @private * @param {PubSub} pubsub PubSub instance to mirror to. * @param {Object} emitter Emitter to source from. * @param {Array|string} channel Channel(s) to listen on. * @param {Object} options Options used to create mirroring. */ function _sourceSubscribe(pubsub, emitter, channel, options) { const params = _getOnParams(pubsub, channel, options).map(params => { if (options.filter) params.unshift(options.filter); return params; }); _sourceGeneric(emitter, 'subscribe', channel, params); } /** * Given an addEventListener subscription method (DOM style) on an emitter, mirror given events to pubsub instance. * The method name is given via options.on. * * @private * @param {PubSub} pubsub PubSub instance to mirror to. * @param {Object} emitter Emitter to source from. * @param {Array|string} channel Channel(s) to listen on. * @param {Object} options Options used to create mirroring. */ function _sourceAddEventListener(pubsub, emitter, channel, options) { const params = _getOnParams(pubsub, channel, options).map(params => { if (options.options) { params.push(options.options); } else if (options.useCapture) { params.push(options.useCapture); if (options.wantsUntrusted) params.push(options.wantsUntrusted); } return params; }); _sourceGeneric(emitter, 'addEventListener', channel, params); } /** * Given an $on style (Angular like) subscription method on an emitter, mirror given events to pubsub instance. The * method name is given via options.on. * * @private * @param {PubSub} pubsub PubSub instance to mirror to. * @param {Object} emitter Emitter to source from. * @param {Array|string} channel Channel(s) to listen on. * @param {Object} options Options used to create mirroring. */ function _source$On(pubsub, emitter, channel, options) { _sourceGeneric(emitter, '$on', channel, _getOnParams(pubsub, channel, options)); } /** * Given an user-defined subscription method on an emitter, mirror given events to pubsub instance. The method name is * given via options.on. * * @private * @param {PubSub} pubsub PubSub instance to mirror to. * @param {Object} emitter Emitter to source from. * @param {Array|string} channel Channel(s) to listen on. * @param {Object} options Options used to create mirroring. */ function _sourceUserDefined(pubsub, emitter, channel, options) { const params = _getOnParams(pubsub, channel, options).map(params => { return [...makeArray(options.beforeListenerParams), ...params, ...makeArray(options.afterListenerParams)]; }); _sourceGeneric(emitter, options.on, channel, params); } /** * Given an on style method (jQuery-like) on an emitter, mirror given events to pubsub instance. * * @private * @param {PubSub} pubsub PubSub instance to mirror to. * @param {Object} emitter Emitter to source from. * @param {Array|string} channel Channel(s) to listen on. * @param {Object} options Options used to create mirroring. */ function _sourceOn(pubsub, emitter, channel, options) { const params = _getOnParams(pubsub, channel, options).map(params => { if (options.selector) params.unshift(options.selector); if (options.data) params.unshift(options.data); return params; }); _sourceGeneric(emitter, 'on', channel, params); } /** * Publish a message to the given listeners on the given channels. * * @param {Map} listenerLookup The listener lookup to use. * @param {Array.<string|RegExp>} channels The channels to publish on. * @param {*} message The publish message. * @returns {boolean} Did any listeners receive the message. */ function _publish(listenerLookup, channels, message) { const publishTo = _getPublishTo(listenerLookup, _uniqueChannels(channels), message); return _messageListeners(publishTo, message, { target: channels, publish: true }); } /** * Broadcast a message to the given listeners on the given channels. * * @param {Map} listenerLookup The listener lookup to use. * @param {Array.<string>} channels The channels to broadcast on. * @param {*} message The broadcast message. * @returns {boolean} Did any listeners receive the message. */ function _broadcast(listenerLookup, channels, message) { const broadcastTo = _getBroadcastTo(listenerLookup, channels, message); return _messageListeners(broadcastTo, message, { target: channels, broadcast: true }); } /** * Take an input message of type similar to Message. Based on it's content broadcast or publish to given * PubSub instance. * * @private * @param {Map} listenerLookup The Pubsub instance. * @param {Message|Object} message Message to mirror. * @returns {boolean} Did anything receive the message? */ function _mirror(listenerLookup, message) { const channels = _getCorrectedChannelsArray(message.target); let mirrored = false; if (!message.data) message.data = {}; if (!message.broadcast && !message.publish) message.publish = true; let mirrorTo; const options = { target: channels }; if (message.broadcast === true) { mirrorTo = _getBroadcastTo(listenerLookup, channels, message.data); options.broadcast = true; } else if (message.publish === true) { mirrorTo = _getPublishTo(listenerLookup, _uniqueChannels(channels), message.data); options.publish = true; } if (mirrorTo && mirrorTo.size) { if (message.timestamp) options.timestamp = message.timestamp; mirrored = _messageListeners(mirrorTo, message.data, options) || mirrored; } return mirrored; } /** * Subscribe to the given channels with the supplied listener and filter. Will use the supplied listener lookup map to * set listeners. * * @private * @param {Map} listenerLookup The listener lookup map. * @param {Array.<string|RegExp>|Set.<string|RegExp>} channels The channels to subscribe to. * @param {Object} filter The sift filter to use. * @param {Function} listener The listener to fire when messages received. * @returns {Function} Unsubscribe function. */ function _subscribe(listenerLookup, channels, filter, listener) { if (!isFunction(listener)) throw createError(TypeError, 'CallbackNotFunction'); if (!isObject(filter)) throw createError(TypeError, 'FilterNotAnObject'); if (!_allChannelsAreCorrectType(channels)) throw createError(TypeError, 'ChannelNotAString'); const subscription = { listener, filter }; _subscriptionsAction(listenerLookup, channels, 'add', subscription); return () => _subscriptionsAction(listenerLookup, channels, 'delete', subscription); } /** * Unsubscribe the given listener throughout this PubSub instance or if channel(s) given unsubscribe from the given * channel(s) through PubSub instance. * * @private * @method * @param {Map} listenerLookup The listener lookup to use. * @param {Array.<string|Function|RegExp>} channelsOrListeners The channel or listener to unsubscribe. * @return {boolean} Did anything unsubscibe? */ function _unsubscribe(listenerLookup, channelsOrListeners) { let unsubscribed = false; channelsOrListeners.forEach(channel => { if (isFunction(channel)) { unsubscribed = _unsubscribeListener(listenerLookup, channel) || unsubscribed; } else { unsubscribed = _unsubscribeChannel(listenerLookup, channel) || unsubscribed; } }); return unsubscribed; } /** * Publish and Subscription class. * * @public * @class */ class PubSub { constructor() { const emitterSourceOrder = this.emitterSourceOrder; emitterSourceOrder.add({ name: 'Generic', fromOptions: true, method: 'on' }); emitterSourceOrder.add({ name: 'jQuery', fromOptions: false, method: 'on' }); emitterSourceOrder.add({ name: 'Angular', fromOptions: false, method: '$on' }); emitterSourceOrder.add({ name: 'PubSub', fromOptions: false, method: 'subscribe' }); emitterSourceOrder.add({ name: 'DOM', fromOptions: false, method: 'addEventListener' }); } /** * Subscribe to information published on a given channel(s) path with optional filtering. If a regular-expression is * given for a channel it will receive published data but not broadcast data. * * @public * @method * @param {string|RegExp|Array.<string|RegExp>|Set.<string|RegExp>} channel Channel(s) to subscribe to * (including glob-style patterns). * @param {Object} [filter={}] Filter to filter-out messages that * are not wanted. * @param {Function} listener Listener for caught messages. * @returns {Function} Unsubscribe function. */ subscribe(channel, filter, listener) { return _subscribe(Private.get(this, 'channels', Map), makeArray(channel).map(channel => _removingTrailingSlash(channel)), listener ? filter : {}, listener ? listener : filter); } /** * Unsubscribe the given listener throughout this PubSub instance or if channel(s) given unsubscribe from the given * channel(s) through PubSub instance. * * @public * @method * @param {string|Function|RegExp|Array.<string|Function|RegExp>} channel The channel or listener to unsubscribe. * @return {boolean} Did anything unsubscibe? */ unsubscribe(channel) { const channels = Private.get(this, 'channels', Map); return _unsubscribe(channels, makeArray(channel)); } /** * Publish a message to the given channel(s). Publishing causes a message to be read on given channel and all * parent channels. * * @public * @method * @param {string|Array|set} channel Channel(s) to publish on (including glob-style patterns). * @param {*} message Message to publish. * @returns {boolean} Did the message publish? */ publish(channel, message) { return _publish(Private.get(this, 'channels', Map), _getCorrectedChannelsArray(channel), message); } /** * Broadcast a message to the given channel(s). Broadcasting causes a message to be read on given channel and all * descendant channels. Will not be read on channel subscriptions that are regular-expressions. * * @public * @method * @param {string|Array|set} channel Channel(s) to publish on (including glob-style patterns). * @param {*} message Message to publish. * @returns {boolean} Did the message publish? */ broadcast(channel, message) { return _broadcast(Private.get(this, 'channels', Map), _getCorrectedChannelsArray(channel), message); } /** * Mirror a message from on pubsub source into this instance. It expects a message object in a similar format to * the one used in this class. Messages do not have to be identical but they should have a target, which is the * channels to broadcast/publish to; a data property, which is the message; and either a publish or broadcast * boolean property. * * Parsers can be supplied to parse the message into the correct format. A base maybe supplied if we want to prepend * a new base to each channel. * * @note: Messages are mirrored not republished; original messages are cloned. * * @param {Message|Object|*} message The message object to mirror. * @param {Array.<Function>} [parsers=[]] The parsers to use in converting into the correct format. * @param {string} [base=''] The base to prepend to channels. * @returns {boolean} Did anything receive the message? */ mirror(message, parsers = [], base = '/') { return _mirror(Private.get(this, 'channels', Map), _mirrorMessageParse(message, parsers, base)); } /** * Given a source emitter, mirror given events to this pubsub instance. This is a convenience method, to easily * mirror events from the DOM, jQuery or Angular. It covers most use cases and therefore makes it easier to just * plug another emitter into this PubSub class. * * Method can be called with options but without channel. In this case the default channel of '/' is applied. * * @param {Object} emitter The source emitter. * @param {string|Array.<string>|*} [channel='/'] The 'channel' to 'subscribe' on the emitter. * @param {Object} [options={}] Options object for creating the mirror. */ source(emitter, channel = '/', options = {}) { if (!isString(channel) && !Array.isArray(channel) && !(channel instanceof Set)) { if (isObject(options) && !Object.keys(options).length) { options = channel; channel = '/'; } } if (options.type) { const sourceMirror = getSourceSubscriber(this, options.type); if (sourceMirror) return sourceMirror(this, emitter, channel, options); } [...this.emitterSourceOrder].every(details => { const method = details.fromOptions ? options[details.method] : details.method; if (emitter[method]) { const sourceMirror = getSourceSubscriber(this, details.name); if (sourceMirror) { sourceMirror(this, emitter, channel, options); return false; } } return true; }); } addSourceSubscriber(name, method) { const sourceSubscribers = Private.get(this, 'sourceSubscribers', Map); sourceSubscribers.set(name, method); } deleteSourceSubscriber(name) { const sourceSubscribers = Private.get(this, 'sourceSubscribers', Map); sourceSubscribers.delete(name); } static addSourceSubscriber(name, method) { const sourceSubscribers = Private.get(PubSub, 'sourceSubscribers', Map); sourceSubscribers.set(name, method); } static deleteSourceSubscriber(name) { const sourceSubscribers = Private.get(PubSub, 'sourceSubscribers', Map); sourceSubscribers.delete(name); } get emitterSourceOrder() { return Private.get(this, 'emitterSourceOrder', Set); } } PubSub.addSourceSubscriber('jQuery', _sourceOn); PubSub.addSourceSubscriber('Angular', _source$On); PubSub.addSourceSubscriber('PubSub', _sourceSubscribe); PubSub.addSourceSubscriber('DOM', _sourceAddEventListener); PubSub.addSourceSubscriber('Generic', _sourceUserDefined); //removeIf(browser) module.exports = PubSub; //endRemoveIf(browser)