topic-subscribe
Version:
Universal PubSub module for node (including old versions) and browser with subscription filtering and broadcasting.
1,127 lines (1,007 loc) • 37.4 kB
JavaScript
;(function() {
;
//removeIf(node)
//endRemoveIf(node)
/**
* 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;
}
//removeIf(node)
//endRemoveIf(node)
//removeIf(node)
//endRemoveIf(node)
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(node)
//endRemoveIf(node)
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(node)
//endRemoveIf(node)
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;
}
}
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(node)
if (window.jQuery || window.$) {
const $ = window.jQuery || window.$;
const mirrorActions = ['publish', 'broadcast', 'subscribe', 'unsubscribe', 'mirror', 'source'];
const pubsubs = new WeakMap();
$.fn.pubsub = function(...params) {
function pubsubGet(item) {
if (!pubsubs.has(item)) pubsubs.set(item, new PubSub(...params));
pubsubs.get(item).unsubscribe();
return pubsubs.get(item);
}
function pubsubAction(items, action, params) {
items.each((n, item)=>pubsubGet(item)[action](...params));
return items;
}
function pubsubMirror(actions, items) {
actions.forEach(action=>{
items[action] = (...params)=>pubsubAction(items, action, params)
});
return items;
}
return pubsubMirror(mirrorActions, this);
};
}
//endRemoveIf(node)
//removeIf(node)
if (window.angular) {
const pubsub = new PubSub();
window.angular.module("TopSubscribe", []).factory("pubsub", pubsub);
}
//endRemoveIf(node)
}());
//# sourceMappingURL=topic-subscribe.js.map