Comparing version 0.2.1 to 0.3.0
281
lib/pubst.js
@@ -6,6 +6,5 @@ 'use strict'; | ||
/** | ||
* Pubst - Basic JavaScript Pub/Sub Event Emitter | ||
* @module pubst | ||
* Pubst - A slightly opinionated pub/sub library for JavaScript. | ||
* | ||
* Copyright 2017 Jason Schindler | ||
* Copyright 2017-2018 Jason Schindler | ||
* | ||
@@ -23,2 +22,8 @@ * Licensed under the Apache License, Version 2.0 (the "License"); | ||
* limitations under the License. | ||
* | ||
* @file pubst.js | ||
* @module pubst | ||
* @author Jason Schindler | ||
* @copyright Jason Schindler 2017-2018 | ||
* @description A slightly opinionated pub/sub library for JavaScript. | ||
*/ | ||
@@ -59,7 +64,15 @@ | ||
/** | ||
* Set global configuration. | ||
* @summary Set global configuration. | ||
* | ||
* @alias module:pubst.configure | ||
* @param {Object} config - Your configuration | ||
* | ||
* @description | ||
* <p> | ||
* Available options are: | ||
* `showWarnings` (default: true) - Show warnings in the console. | ||
* <ul> | ||
* <li>`showWarnings` (default: true) - Show warnings in the console.</li> | ||
* <li>`topics` - An array of topic configurations. (See: `addTopic` for topic configuration options)</li> | ||
* </ul> | ||
* </p> | ||
*/ | ||
@@ -74,2 +87,6 @@ function configure() { | ||
} | ||
if (Array.isArray(userConfig.topics)) { | ||
addTopics(userConfig.topics); | ||
} | ||
} | ||
@@ -80,3 +97,91 @@ | ||
var regexSubs = []; | ||
var topics = {}; | ||
var DEFAULT_TOPIC_CONFIG = { | ||
name: '', | ||
default: undefined, | ||
eventOnly: false, | ||
doPrime: true, | ||
allowRepeats: false | ||
}; | ||
var ALLOWED_SUB_PROPS = ['topic', 'handler', 'default', 'doPrime', 'allowRepeats']; | ||
function buildConfig(base, extensions) { | ||
var result = {}; | ||
for (var key in base) { | ||
result[key] = base[key]; | ||
} | ||
for (var _key2 in extensions) { | ||
if (result.hasOwnProperty(_key2)) { | ||
result[_key2] = extensions[_key2]; | ||
} | ||
} | ||
return result; | ||
} | ||
/** | ||
* @summary Configure a new topic. | ||
* | ||
* @alias module:pubst.addTopic | ||
* @param {Object} newTopicConfig - Topic configuration | ||
* | ||
* @description | ||
* <p> | ||
* Allows you to configure a new topic in pubst. | ||
* | ||
* Available options are: | ||
* <ul> | ||
* <li>`name` (<strong>REQUIRED</strong>) - A string representing the name of the topic.</li> | ||
* <li> | ||
* `default` (default: undefined) - The default value presented to subscribers when the topic is undefined or null. | ||
* This can be overridden by subscribers. | ||
* </li> | ||
* <li> | ||
* `eventOnly` (default: false) - Set this to true if this topic will not have payload data. | ||
* </li> | ||
* <li> | ||
* `doPrime` (default: true) - Should new subscribers automatically receive the last published value on the topic? | ||
* If no valid value is present, new subscribers will be primed with the default value (if configured). | ||
* This can be overridden by subscribers. | ||
* </li> | ||
* <li> | ||
* `allowRepeats` (default: false) - Alert subscribers of all publish events, even if the value is equal (by strict comparison) to the last value sent. | ||
* This can be overridden by subscribers. | ||
* </li> | ||
* </ul> | ||
* </p> | ||
*/ | ||
function addTopic(newTopicConfig) { | ||
var topic = buildConfig(DEFAULT_TOPIC_CONFIG, newTopicConfig); | ||
if (!topic.name) { | ||
throw new Error('Topics must have a name.'); | ||
} | ||
if (topics[topic.name]) { | ||
warn('The \'' + topic.name + '\' topic has already been configured. The previous configuration will be overwritten.'); | ||
} | ||
topics[topic.name] = topic; | ||
} | ||
/** | ||
* @summary Configure new topics. | ||
* | ||
* @alias module:pubst.addTopics | ||
* @param {Array<Object>} newTopicConfigs - Topic configurations | ||
* | ||
* @description | ||
* <p> | ||
* Allows you to configure new topics. This will call `addTopic` with each item passed. | ||
* For available options, see `addTopic`. | ||
*/ | ||
function addTopics(topics) { | ||
topics.forEach(addTopic); | ||
} | ||
function getStringSubsFor(topic) { | ||
@@ -94,4 +199,13 @@ return Array.isArray(stringSubs[topic]) ? stringSubs[topic] : []; | ||
if (typeof subscriber.topic === 'string') { | ||
if (!topics[subscriber.topic]) { | ||
warn('Adding a subscriber to non-configured topic \'' + subscriber.topic + '\''); | ||
} | ||
stringSubs[subscriber.topic] = getStringSubsFor(subscriber.topic).concat(subscriber); | ||
} else if (subscriber.topic instanceof RegExp) { | ||
var matchCount = Object.keys(topics).filter(function (topic) { | ||
return topic.match(subscriber.topic); | ||
}).length; | ||
if (matchCount === 0) { | ||
warn('Adding a RegExp subscriber that matches no configured topics.'); | ||
} | ||
regexSubs.push(subscriber); | ||
@@ -119,4 +233,12 @@ } else { | ||
function isUndefined(input) { | ||
return typeof input === 'undefined'; | ||
} | ||
function isDefined(input) { | ||
return !isUndefined(input); | ||
} | ||
function isNotSet(item) { | ||
return item === null || typeof item === 'undefined'; | ||
return item === null || isUndefined(item); | ||
} | ||
@@ -129,3 +251,3 @@ | ||
function valueOrDefault(value, def) { | ||
if (isNotSet(value) && typeof def !== 'undefined') { | ||
if (isNotSet(value) && isDefined(def)) { | ||
return def; | ||
@@ -137,15 +259,27 @@ } | ||
function getTopicConfig(topic) { | ||
return topics[topic] || buildConfig(DEFAULT_TOPIC_CONFIG, { name: topic }); | ||
} | ||
function scheduleCall(sub, payload, topic) { | ||
setTimeout(function () { | ||
var value = valueOrDefault(payload, sub.default); | ||
if (sub.allowRepeats || sub.lastVal !== value || sub.lastTopic !== topic) { | ||
var topicConfig = getTopicConfig(topic); | ||
var defVal = typeof sub.default === 'undefined' ? topicConfig.default : sub.default; | ||
var eventOnly = sub.hasOwnProperty('eventOnly') ? sub.eventOnly : topicConfig.eventOnly; | ||
var allowRepeats = sub.hasOwnProperty('allowRepeats') ? sub.allowRepeats : topicConfig.allowRepeats; | ||
var value = eventOnly ? topic : valueOrDefault(payload, defVal); | ||
if (eventOnly || allowRepeats || sub.lastVal !== value || sub.lastTopic !== topic) { | ||
setTimeout(function () { | ||
sub.handler(value, topic); | ||
sub.lastVal = value; | ||
sub.lastTopic = topic; | ||
} | ||
}, 0); | ||
}, 0); | ||
} | ||
} | ||
/** | ||
* Publish to a topic | ||
* @summary Publish to a topic | ||
* | ||
* @alias module:pubst.publish | ||
* @param {string} topic - The topic to publish to | ||
@@ -155,2 +289,6 @@ * @param {*} payload The payload to publish | ||
function publish(topic, payload) { | ||
if (!topics[topic]) { | ||
warn('Received a publish for ' + topic + ' but that topic has not been configured.'); | ||
} | ||
store[topic] = payload; | ||
@@ -169,5 +307,8 @@ var subs = allSubsFor(topic); | ||
/** | ||
* Subscribe to one or more topics | ||
* @summary Subscribe to one or more topics | ||
* | ||
* @alias module:pubst.subscribe | ||
* @param {string|RegExp} topic - The topic to receive updates for | ||
* @param {Function|Object} handler | ||
* @param {Function|Object} handler - Either your handler function or | ||
* a subscription configuration object | ||
* @param {*} default - (Optional) Value to send when topic is empty | ||
@@ -178,2 +319,4 @@ * | ||
* | ||
* @description | ||
* <p> | ||
* The first argument may be a string or a regular expression. | ||
@@ -184,36 +327,38 @@ * If a string is provided, the handler will be called for all | ||
* expression. | ||
* </p> | ||
* | ||
* <p> | ||
* The second argument may be a function or an object. The object | ||
* is necessary if you want to provide configuration options for | ||
* this subscription. Available options are: | ||
* <ul> | ||
* <li>`default` - (Default: undefined) - Default value for this sub.</li> | ||
* <li>`doPrime` - (Default: true) - Should the handler be primed with | ||
* the last value?</li> | ||
* <li>`allowRepeats` - (Default: false) - Should the handler be called | ||
* when the value doesn't change?</li> | ||
* <li>`handler` - (Required) - The handler to call.</li> | ||
* </ul> | ||
* </p> | ||
* | ||
* `default` - (Default: undefined) - Default value for this sub. | ||
* `doPrime` - (Default: true) - Should the handler be primed with | ||
* the last value? | ||
* `allowRepeats` - (Default: false) - Should the handler be called | ||
* when the value doesn't change | ||
* `handler` - (Required) - The handler to call. | ||
* | ||
* <p> | ||
* The handler will be called on topic updates. It will be passed | ||
* the new value of the topic as the first argument, and the name of | ||
* the topic as the second argument. | ||
* </p> | ||
*/ | ||
function subscribe(topic, handler, def) { | ||
var subscription = { | ||
topic: topic, | ||
default: undefined, | ||
doPrime: true, | ||
allowRepeats: false, | ||
handler: function handler() {} | ||
}; | ||
var subscription = void 0; | ||
if (typeof handler === 'function') { | ||
subscription.default = def; | ||
subscription.handler = handler; | ||
subscription = { topic: topic, default: def, handler: handler }; | ||
} else if ((typeof handler === 'undefined' ? 'undefined' : _typeof(handler)) === 'object') { | ||
for (var key in handler) { | ||
if (subscription.hasOwnProperty(key)) { | ||
subscription[key] = handler[key]; | ||
} | ||
} | ||
subscription = {}; | ||
Object.keys(handler).filter(function (key) { | ||
return ALLOWED_SUB_PROPS.includes(key); | ||
}).forEach(function (key) { | ||
subscription[key] = handler[key]; | ||
}); | ||
subscription.topic = topic; | ||
} | ||
@@ -223,28 +368,29 @@ | ||
if (subscription.doPrime) { | ||
var stored = void 0; | ||
var stored = void 0; | ||
if (typeof topic === 'string') { | ||
stored = [{ | ||
topic: topic, | ||
val: currentVal(topic, def) | ||
}]; | ||
} else if (topic instanceof RegExp) { | ||
stored = Object.keys(store).filter(function (key) { | ||
return key.match(topic); | ||
}).map(function (key) { | ||
return { | ||
topic: key, | ||
val: currentVal(key, def) | ||
}; | ||
}); | ||
} | ||
stored.forEach(function (item) { | ||
if (isSet(item.val)) { | ||
scheduleCall(subscription, item.val, item.topic); | ||
} | ||
if (typeof topic === 'string') { | ||
stored = [{ | ||
topic: topic, | ||
val: currentVal(topic, def) | ||
}]; | ||
} else if (topic instanceof RegExp) { | ||
stored = Object.keys(store).filter(function (key) { | ||
return key.match(topic); | ||
}).map(function (key) { | ||
return { | ||
topic: key, | ||
val: currentVal(key, def) | ||
}; | ||
}); | ||
} | ||
stored.forEach(function (item) { | ||
var topicConfig = getTopicConfig(item.topic); | ||
var doPrime = subscription.hasOwnProperty('doPrime') ? subscription.doPrime : topicConfig.doPrime; | ||
if (doPrime && (topicConfig.eventOnly || isSet(item.val))) { | ||
scheduleCall(subscription, item.val, item.topic); | ||
} | ||
}); | ||
return function () { | ||
@@ -256,3 +402,5 @@ removeSub(subscription); | ||
/** | ||
* Get the current value of a topic. | ||
* @summary Get the current value of a topic. | ||
* | ||
* @alias module:pubst.currentVal | ||
* @param {string} topic - The topic to get the value of. | ||
@@ -264,10 +412,13 @@ * @param {*} default - (Optional) a value to return if the topic is | ||
function currentVal(topic, def) { | ||
return valueOrDefault(store[topic], def); | ||
var defToUse = isDefined(def) ? def : getTopicConfig(topic).default; | ||
return valueOrDefault(store[topic], defToUse); | ||
} | ||
/** | ||
* Clears a given topic. | ||
* @summary Clears a given topic. | ||
* | ||
* @alias module:pubst.clear | ||
* @param {string} topic - The topic to clear | ||
* | ||
* Clears the topic by publishing a `null` to it. | ||
* @description Clears the topic by publishing a `null` to it. | ||
*/ | ||
@@ -281,3 +432,5 @@ function clear(topic) { | ||
/** | ||
* Clears all known topics. | ||
* @summary Clears all known topics. | ||
* | ||
* @alias module:pubst.clearAll | ||
*/ | ||
@@ -289,2 +442,4 @@ function clearAll() { | ||
return { | ||
addTopic: addTopic, | ||
addTopics: addTopics, | ||
clear: clear, | ||
@@ -291,0 +446,0 @@ clearAll: clearAll, |
{ | ||
"name": "pubst", | ||
"version": "0.2.1", | ||
"version": "0.3.0", | ||
"description": "A slightly opinionated pub/sub event emitter for JavaScript.", | ||
"main": "lib/pubst.js", | ||
"scripts": { | ||
"build": "babel src -d lib", | ||
"clean": "shx rm -rf lib doc", | ||
"build-docs": "jsdoc ./src -d ./doc", | ||
"lint": "eslint ./src", | ||
"prepublish": "babel src -d lib", | ||
"test": "mocha ./src/*.test.js" | ||
"prepare": "npm-run-all clean build-docs build", | ||
"test": "mocha ./src/*.test.js", | ||
"verify": "npm-run-all lint test" | ||
}, | ||
@@ -34,10 +38,13 @@ "repository": { | ||
"babel-cli": "6.26.0", | ||
"babel-preset-env": "1.6.1", | ||
"babel-preset-env": "1.7.0", | ||
"chai": "4.1.2", | ||
"clear-require": "2.0.0", | ||
"eslint": "4.19.1", | ||
"mocha": "5.1.1", | ||
"sinon": "4.5.0", | ||
"sinon-chai": "3.0.0" | ||
"eslint": "5.5.0", | ||
"jsdoc": "3.5.5", | ||
"mocha": "5.2.0", | ||
"npm-run-all": "4.1.3", | ||
"shx": "0.3.2", | ||
"sinon": "6.2.0", | ||
"sinon-chai": "3.2.0" | ||
} | ||
} |
@@ -22,2 +22,4 @@ # Pubst | ||
While it isn't necessarily required, it is a good idea to configure the topics you are going to use. | ||
```js | ||
@@ -27,8 +29,18 @@ // File1.js | ||
pubst.addTopic({ | ||
name: 'NAME', | ||
default: 'World' | ||
}); | ||
``` | ||
```js | ||
// File2.js | ||
const pubst = require('pubst'); | ||
pubst.subscribe('NAME', name => { | ||
console.log(`Hello ${name}!`); | ||
}, 'World'); | ||
}); | ||
``` | ||
Because the subscriber passed a default value of 'World' and the topic is currently empty, the subscriber is primed with the default value. | ||
Because the topic has a default value of 'World' and the topic is currently empty, the subscriber is primed with the default value. | ||
``` | ||
@@ -60,3 +72,3 @@ Hello World! | ||
Each subscriber is free to define their own default for each topic. | ||
A subscriber is free to override the default value for their topics. | ||
@@ -72,2 +84,3 @@ ## API | ||
+ `showWarnings` (default: true) - Show warnings in the console. | ||
+ `topics` - An array of topic configurations. | ||
@@ -77,5 +90,59 @@ #### Example | ||
```js | ||
pubst.configure({showWarnings: false}); | ||
pubst.configure({ | ||
showWarnings: false, | ||
topics: [ | ||
{ | ||
name: 'user.basicInfo', | ||
default: { | ||
lastName: 'No User Logged In', | ||
firstName: 'No User Logged In' | ||
} | ||
} | ||
] | ||
}); | ||
``` | ||
### `addTopic(topicConfig)` or `addTopics(topicConfigArrays)` | ||
Sets the configuration for a new topic. | ||
#### Available options: | ||
+ `name` (*REQUIRED*) - A string representing the name of the topic. | ||
+ `default` (default: undefined) - The default value presented to subscribers when the topic is undefined or null. | ||
+ This can be overridden by subscribers. | ||
+ `eventOnly` (default: false) - Set this to `true` if this topic will not have payload data. | ||
+ `doPrime` (default: true) - Should new subscribers automatically receive the last published value on the topic? | ||
+ This can be overridden by subscribers. | ||
+ `allowRepeats` (default: false) - Alert subscribers of all publish events, even if the value is equal (by strict comparison) to the last value sent. | ||
+ This can be overridden by subscribers. | ||
#### Examples | ||
```js | ||
pubst.addTopics([ | ||
{ | ||
name: 'game.started', | ||
default: false, | ||
doPrime: true | ||
}, | ||
{ | ||
name: 'player.guess', | ||
default: -1, | ||
allowRepeats: true, | ||
doPrime: false | ||
}, | ||
{ | ||
name: 'player.won', | ||
eventOnly: true, | ||
doPrime: false | ||
} | ||
]); | ||
pubst.addTopic({ | ||
name: 'player.name', | ||
default: 'Player 1' | ||
}); | ||
``` | ||
### `publish(topic, payload)` | ||
@@ -206,2 +273,1 @@ | ||
2. Experiment with creating a React library that makes linking topics with props mostly painless. | ||
3. Add topic-wide configuration (like: defaults) |
280
src/pubst.js
/** | ||
* Pubst - Basic JavaScript Pub/Sub Event Emitter | ||
* @module pubst | ||
* Pubst - A slightly opinionated pub/sub library for JavaScript. | ||
* | ||
* Copyright 2017 Jason Schindler | ||
* Copyright 2017-2018 Jason Schindler | ||
* | ||
@@ -18,2 +17,8 @@ * Licensed under the Apache License, Version 2.0 (the "License"); | ||
* limitations under the License. | ||
* | ||
* @file pubst.js | ||
* @module pubst | ||
* @author Jason Schindler | ||
* @copyright Jason Schindler 2017-2018 | ||
* @description A slightly opinionated pub/sub library for JavaScript. | ||
*/ | ||
@@ -48,7 +53,15 @@ | ||
/** | ||
* Set global configuration. | ||
* @summary Set global configuration. | ||
* | ||
* @alias module:pubst.configure | ||
* @param {Object} config - Your configuration | ||
* | ||
* @description | ||
* <p> | ||
* Available options are: | ||
* `showWarnings` (default: true) - Show warnings in the console. | ||
* <ul> | ||
* <li>`showWarnings` (default: true) - Show warnings in the console.</li> | ||
* <li>`topics` - An array of topic configurations. (See: `addTopic` for topic configuration options)</li> | ||
* </ul> | ||
* </p> | ||
*/ | ||
@@ -61,2 +74,6 @@ function configure(userConfig = {}) { | ||
} | ||
if (Array.isArray(userConfig.topics)) { | ||
addTopics(userConfig.topics); | ||
} | ||
} | ||
@@ -67,3 +84,98 @@ | ||
let regexSubs = []; | ||
const topics = {}; | ||
const DEFAULT_TOPIC_CONFIG = { | ||
name: '', | ||
default: undefined, | ||
eventOnly: false, | ||
doPrime: true, | ||
allowRepeats: false | ||
}; | ||
const ALLOWED_SUB_PROPS = [ | ||
'topic', | ||
'handler', | ||
'default', | ||
'doPrime', | ||
'allowRepeats' | ||
]; | ||
function buildConfig(base, extensions) { | ||
const result = {}; | ||
for (const key in base) { | ||
result[key] = base[key]; | ||
} | ||
for (const key in extensions) { | ||
if (result.hasOwnProperty(key)) { | ||
result[key] = extensions[key]; | ||
} | ||
} | ||
return result; | ||
} | ||
/** | ||
* @summary Configure a new topic. | ||
* | ||
* @alias module:pubst.addTopic | ||
* @param {Object} newTopicConfig - Topic configuration | ||
* | ||
* @description | ||
* <p> | ||
* Allows you to configure a new topic in pubst. | ||
* | ||
* Available options are: | ||
* <ul> | ||
* <li>`name` (<strong>REQUIRED</strong>) - A string representing the name of the topic.</li> | ||
* <li> | ||
* `default` (default: undefined) - The default value presented to subscribers when the topic is undefined or null. | ||
* This can be overridden by subscribers. | ||
* </li> | ||
* <li> | ||
* `eventOnly` (default: false) - Set this to true if this topic will not have payload data. | ||
* </li> | ||
* <li> | ||
* `doPrime` (default: true) - Should new subscribers automatically receive the last published value on the topic? | ||
* If no valid value is present, new subscribers will be primed with the default value (if configured). | ||
* This can be overridden by subscribers. | ||
* </li> | ||
* <li> | ||
* `allowRepeats` (default: false) - Alert subscribers of all publish events, even if the value is equal (by strict comparison) to the last value sent. | ||
* This can be overridden by subscribers. | ||
* </li> | ||
* </ul> | ||
* </p> | ||
*/ | ||
function addTopic(newTopicConfig) { | ||
const topic = buildConfig(DEFAULT_TOPIC_CONFIG, newTopicConfig); | ||
if (!topic.name) { | ||
throw new Error('Topics must have a name.'); | ||
} | ||
if (topics[topic.name]) { | ||
warn(`The '${topic.name}' topic has already been configured. The previous configuration will be overwritten.`); | ||
} | ||
topics[topic.name] = topic; | ||
} | ||
/** | ||
* @summary Configure new topics. | ||
* | ||
* @alias module:pubst.addTopics | ||
* @param {Array<Object>} newTopicConfigs - Topic configurations | ||
* | ||
* @description | ||
* <p> | ||
* Allows you to configure new topics. This will call `addTopic` with each item passed. | ||
* For available options, see `addTopic`. | ||
*/ | ||
function addTopics(topics) { | ||
topics.forEach(addTopic); | ||
} | ||
function getStringSubsFor(topic) { | ||
@@ -79,4 +191,11 @@ return Array.isArray(stringSubs[topic]) ? stringSubs[topic] : []; | ||
if (typeof subscriber.topic === 'string') { | ||
if (!topics[subscriber.topic]) { | ||
warn(`Adding a subscriber to non-configured topic '${subscriber.topic}'`); | ||
} | ||
stringSubs[subscriber.topic] = getStringSubsFor(subscriber.topic).concat(subscriber); | ||
} else if (subscriber.topic instanceof RegExp) { | ||
const matchCount = Object.keys(topics).filter(topic => topic.match(subscriber.topic)).length; | ||
if (matchCount === 0) { | ||
warn(`Adding a RegExp subscriber that matches no configured topics.`); | ||
} | ||
regexSubs.push(subscriber); | ||
@@ -100,4 +219,12 @@ } else { | ||
function isUndefined(input) { | ||
return typeof input === 'undefined'; | ||
} | ||
function isDefined(input) { | ||
return !isUndefined(input); | ||
} | ||
function isNotSet(item) { | ||
return item === null || typeof item === 'undefined'; | ||
return item === null || isUndefined(item); | ||
} | ||
@@ -110,3 +237,3 @@ | ||
function valueOrDefault(value, def) { | ||
if(isNotSet(value) && typeof def !== 'undefined'){ | ||
if(isNotSet(value) && isDefined(def)){ | ||
return def; | ||
@@ -118,15 +245,27 @@ } | ||
function getTopicConfig(topic) { | ||
return topics[topic] || buildConfig(DEFAULT_TOPIC_CONFIG, {name: topic}); | ||
} | ||
function scheduleCall(sub, payload, topic) { | ||
setTimeout(() => { | ||
const value = valueOrDefault(payload, sub.default); | ||
if (sub.allowRepeats || sub.lastVal !== value || sub.lastTopic !== topic) { | ||
const topicConfig = getTopicConfig(topic); | ||
const defVal = typeof sub.default === 'undefined' ? topicConfig.default : sub.default; | ||
const eventOnly = sub.hasOwnProperty('eventOnly') ? sub.eventOnly : topicConfig.eventOnly; | ||
const allowRepeats = sub.hasOwnProperty('allowRepeats') ? sub.allowRepeats : topicConfig.allowRepeats; | ||
const value = eventOnly ? topic : valueOrDefault(payload, defVal); | ||
if (eventOnly || allowRepeats || sub.lastVal !== value || sub.lastTopic !== topic) { | ||
setTimeout(() => { | ||
sub.handler(value, topic); | ||
sub.lastVal = value; | ||
sub.lastTopic = topic; | ||
} | ||
}, 0); | ||
}, 0); | ||
} | ||
} | ||
/** | ||
* Publish to a topic | ||
* @summary Publish to a topic | ||
* | ||
* @alias module:pubst.publish | ||
* @param {string} topic - The topic to publish to | ||
@@ -136,2 +275,6 @@ * @param {*} payload The payload to publish | ||
function publish(topic, payload) { | ||
if (!topics[topic]) { | ||
warn(`Received a publish for ${topic} but that topic has not been configured.`); | ||
} | ||
store[topic] = payload; | ||
@@ -150,5 +293,8 @@ const subs = allSubsFor(topic); | ||
/** | ||
* Subscribe to one or more topics | ||
* @summary Subscribe to one or more topics | ||
* | ||
* @alias module:pubst.subscribe | ||
* @param {string|RegExp} topic - The topic to receive updates for | ||
* @param {Function|Object} handler | ||
* @param {Function|Object} handler - Either your handler function or | ||
* a subscription configuration object | ||
* @param {*} default - (Optional) Value to send when topic is empty | ||
@@ -159,2 +305,4 @@ * | ||
* | ||
* @description | ||
* <p> | ||
* The first argument may be a string or a regular expression. | ||
@@ -165,36 +313,38 @@ * If a string is provided, the handler will be called for all | ||
* expression. | ||
* </p> | ||
* | ||
* <p> | ||
* The second argument may be a function or an object. The object | ||
* is necessary if you want to provide configuration options for | ||
* this subscription. Available options are: | ||
* <ul> | ||
* <li>`default` - (Default: undefined) - Default value for this sub.</li> | ||
* <li>`doPrime` - (Default: true) - Should the handler be primed with | ||
* the last value?</li> | ||
* <li>`allowRepeats` - (Default: false) - Should the handler be called | ||
* when the value doesn't change?</li> | ||
* <li>`handler` - (Required) - The handler to call.</li> | ||
* </ul> | ||
* </p> | ||
* | ||
* `default` - (Default: undefined) - Default value for this sub. | ||
* `doPrime` - (Default: true) - Should the handler be primed with | ||
* the last value? | ||
* `allowRepeats` - (Default: false) - Should the handler be called | ||
* when the value doesn't change | ||
* `handler` - (Required) - The handler to call. | ||
* | ||
* <p> | ||
* The handler will be called on topic updates. It will be passed | ||
* the new value of the topic as the first argument, and the name of | ||
* the topic as the second argument. | ||
* </p> | ||
*/ | ||
function subscribe(topic, handler, def) { | ||
const subscription = { | ||
topic, | ||
default: undefined, | ||
doPrime: true, | ||
allowRepeats: false, | ||
handler: () => {} | ||
}; | ||
let subscription; | ||
if (typeof handler === 'function') { | ||
subscription.default = def; | ||
subscription.handler = handler; | ||
subscription = {topic, default: def, handler}; | ||
} else if (typeof handler === 'object') { | ||
for (const key in handler) { | ||
if (subscription.hasOwnProperty(key)) { | ||
subscription = {}; | ||
Object.keys(handler) | ||
.filter(key => ALLOWED_SUB_PROPS.includes(key)) | ||
.forEach(key => { | ||
subscription[key] = handler[key]; | ||
} | ||
} | ||
}); | ||
subscription.topic = topic; | ||
} | ||
@@ -204,26 +354,27 @@ | ||
if (subscription.doPrime) { | ||
let stored; | ||
let stored; | ||
if (typeof topic === 'string') { | ||
stored = [{ | ||
topic, | ||
val: currentVal(topic, def) | ||
}]; | ||
} else if (topic instanceof RegExp) { | ||
stored = Object.keys(store).filter(key => key.match(topic)).map(key => { | ||
return { | ||
topic: key, | ||
val: currentVal(key, def) | ||
}; | ||
}); | ||
} | ||
stored.forEach(item => { | ||
if (isSet(item.val)) { | ||
scheduleCall(subscription, item.val, item.topic); | ||
} | ||
if (typeof topic === 'string') { | ||
stored = [{ | ||
topic, | ||
val: currentVal(topic, def) | ||
}]; | ||
} else if (topic instanceof RegExp) { | ||
stored = Object.keys(store).filter(key => key.match(topic)).map(key => { | ||
return { | ||
topic: key, | ||
val: currentVal(key, def) | ||
}; | ||
}); | ||
} | ||
stored.forEach(item => { | ||
const topicConfig = getTopicConfig(item.topic); | ||
const doPrime = subscription.hasOwnProperty('doPrime') ? subscription.doPrime : topicConfig.doPrime; | ||
if (doPrime && (topicConfig.eventOnly || isSet(item.val))) { | ||
scheduleCall(subscription, item.val, item.topic); | ||
} | ||
}); | ||
return () => { | ||
@@ -235,3 +386,5 @@ removeSub(subscription); | ||
/** | ||
* Get the current value of a topic. | ||
* @summary Get the current value of a topic. | ||
* | ||
* @alias module:pubst.currentVal | ||
* @param {string} topic - The topic to get the value of. | ||
@@ -243,10 +396,13 @@ * @param {*} default - (Optional) a value to return if the topic is | ||
function currentVal(topic, def) { | ||
return valueOrDefault(store[topic], def); | ||
const defToUse = isDefined(def) ? def : getTopicConfig(topic).default; | ||
return valueOrDefault(store[topic], defToUse); | ||
} | ||
/** | ||
* Clears a given topic. | ||
* @summary Clears a given topic. | ||
* | ||
* @alias module:pubst.clear | ||
* @param {string} topic - The topic to clear | ||
* | ||
* Clears the topic by publishing a `null` to it. | ||
* @description Clears the topic by publishing a `null` to it. | ||
*/ | ||
@@ -260,3 +416,5 @@ function clear(topic) { | ||
/** | ||
* Clears all known topics. | ||
* @summary Clears all known topics. | ||
* | ||
* @alias module:pubst.clearAll | ||
*/ | ||
@@ -268,2 +426,4 @@ function clearAll() { | ||
return { | ||
addTopic, | ||
addTopics, | ||
clear, | ||
@@ -270,0 +430,0 @@ clearAll, |
@@ -71,2 +71,26 @@ const clearRequire = require('clear-require'); | ||
}); | ||
it('returns topic default if value was not set and no default is provided', () => { | ||
const myDefault = 'some default'; | ||
pubst.addTopic({ | ||
name: TEST_TOPIC_1, | ||
default: myDefault | ||
}); | ||
expect(pubst.currentVal(TEST_TOPIC_1)).to.equal(myDefault); | ||
}); | ||
it('returns provided default if value was not set and topic was configured with a default', () => { | ||
const myDefault = 'some default'; | ||
const topicDefault = 'topic default'; | ||
pubst.addTopic({ | ||
name: TEST_TOPIC_1, | ||
default: topicDefault | ||
}); | ||
expect(pubst.currentVal(TEST_TOPIC_1)).to.equal(topicDefault); | ||
expect(pubst.currentVal(TEST_TOPIC_1, myDefault)).to.equal(myDefault); | ||
}); | ||
}); | ||
@@ -91,2 +115,423 @@ | ||
describe('topic config', () => { | ||
describe('name', () => { | ||
it('is required', () => { | ||
let errorThrown = false; | ||
try { | ||
pubst.addTopic({}); | ||
} catch (e) { | ||
errorThrown = true; | ||
} | ||
expect(errorThrown).to.be.true; | ||
}); | ||
}); | ||
describe('default', () => { | ||
it('is configurable', () => { | ||
const handler = sinon.spy(); | ||
const defaultVal = 'DEFAULT VALUE'; | ||
pubst.addTopic({ | ||
name: TEST_TOPIC_1, | ||
default: defaultVal, | ||
doPrime: false | ||
}); | ||
pubst.publish(TEST_TOPIC_1, 'SOME VALUE'); | ||
pubst.subscribe(TEST_TOPIC_1, handler); | ||
pubst.clear(TEST_TOPIC_1); | ||
clock.tick(1); | ||
expect(handler).to.have.been.calledWith(defaultVal, TEST_TOPIC_1); | ||
}); | ||
it('is off by default', () => { | ||
const handler = sinon.spy(); | ||
pubst.addTopic({ | ||
name: TEST_TOPIC_1, | ||
doPrime: true | ||
}); | ||
pubst.subscribe(TEST_TOPIC_1, handler); | ||
clock.tick(1); | ||
expect(handler).not.to.have.been.called; | ||
}); | ||
it('is sent again after a clear', () => { | ||
const handler = sinon.spy(); | ||
const defaultVal = 'SOME DEFAULT'; | ||
const aValue = 'A real value'; | ||
pubst.addTopic({ | ||
name: TEST_TOPIC_1, | ||
default: defaultVal, | ||
doPrime: true | ||
}); | ||
pubst.subscribe(TEST_TOPIC_1, handler); | ||
clock.tick(1); | ||
expect(handler).to.have.been.calledWith(defaultVal, TEST_TOPIC_1); | ||
handler.resetHistory(); | ||
pubst.publish(TEST_TOPIC_1, aValue); | ||
clock.tick(1); | ||
expect(handler).to.have.been.calledWith(aValue, TEST_TOPIC_1); | ||
handler.resetHistory(); | ||
pubst.clear(TEST_TOPIC_1); | ||
clock.tick(1); | ||
expect(handler).to.have.been.calledWith(defaultVal, TEST_TOPIC_1); | ||
}); | ||
it('can be overriden', () => { | ||
const handler1 = sinon.spy(); | ||
const handler2 = sinon.spy(); | ||
const handler3 = sinon.spy(); | ||
const topicDefault = 'topic default'; | ||
const sub2Default = 'sub 2 default'; | ||
const sub3Default = 'sub 3 default'; | ||
const someValue = 'some value'; | ||
pubst.addTopic({ | ||
name: TEST_TOPIC_1, | ||
default: topicDefault, | ||
doPrime: false | ||
}); | ||
pubst.subscribe(TEST_TOPIC_1, handler1); | ||
pubst.subscribe(TEST_TOPIC_1, handler2, sub2Default); | ||
pubst.subscribe(TEST_TOPIC_1, { | ||
handler: handler3, | ||
default: sub3Default | ||
}); | ||
pubst.publish(TEST_TOPIC_1, someValue); | ||
clock.tick(1); | ||
expect(handler1).to.have.been.calledWith(someValue, TEST_TOPIC_1); | ||
expect(handler2).to.have.been.calledWith(someValue, TEST_TOPIC_1); | ||
expect(handler3).to.have.been.calledWith(someValue, TEST_TOPIC_1); | ||
handler1.resetHistory(); | ||
handler2.resetHistory(); | ||
handler3.resetHistory(); | ||
pubst.clear(TEST_TOPIC_1); | ||
clock.tick(1); | ||
expect(handler1).to.have.been.calledWith(topicDefault, TEST_TOPIC_1); | ||
expect(handler2).to.have.been.calledWith(sub2Default, TEST_TOPIC_1); | ||
expect(handler3).to.have.been.calledWith(sub3Default, TEST_TOPIC_1); | ||
}); | ||
}); | ||
describe('eventOnly', () => { | ||
it('creates topics that do not publish a payload', () => { | ||
const handler = sinon.spy(); | ||
pubst.addTopic({ | ||
name: TEST_TOPIC_1, | ||
eventOnly: true, | ||
doPrime: true | ||
}); | ||
pubst.subscribe(TEST_TOPIC_1, handler); | ||
clock.tick(1); | ||
expect(handler).to.have.been.calledWith(TEST_TOPIC_1); | ||
handler.resetHistory(); | ||
pubst.publish(TEST_TOPIC_1, 'some ignored payload'); | ||
clock.tick(1); | ||
expect(handler).to.have.been.calledWith(TEST_TOPIC_1); | ||
handler.resetHistory(); | ||
pubst.publish(TEST_TOPIC_1); | ||
pubst.publish(TEST_TOPIC_1); | ||
pubst.publish(TEST_TOPIC_1); | ||
clock.tick(1); | ||
expect(handler).to.have.been.calledThrice; | ||
}); | ||
it('is off by default', () => { | ||
const payload = 'a payload'; | ||
const handler = sinon.spy(); | ||
pubst.addTopic({ | ||
name: TEST_TOPIC_1, | ||
doPrime: false | ||
}); | ||
pubst.subscribe(TEST_TOPIC_1, handler); | ||
pubst.publish(TEST_TOPIC_1, payload); | ||
clock.tick(1); | ||
expect(handler).to.have.been.calledOnce; | ||
expect(handler).to.have.been.calledWith(payload); | ||
handler.resetHistory(); | ||
pubst.publish(TEST_TOPIC_1, payload); | ||
clock.tick(1); | ||
expect(handler).not.to.have.been.called; | ||
}); | ||
}); | ||
describe('doPrime', () => { | ||
it('sends current value to new subscribers when on', () => { | ||
pubst.addTopics([ | ||
{ | ||
name: TEST_TOPIC_1, | ||
doPrime: true | ||
}, | ||
{ | ||
name: TEST_TOPIC_2, | ||
doPrime: false | ||
} | ||
]); | ||
const testPayload = 'some test payload'; | ||
pubst.publish(TEST_TOPIC_1, testPayload); | ||
pubst.publish(TEST_TOPIC_2, testPayload); | ||
const handler1 = sinon.spy(); | ||
const handler2 = sinon.spy(); | ||
pubst.subscribe(TEST_TOPIC_1, handler1); | ||
pubst.subscribe(TEST_TOPIC_2, handler2); | ||
clock.tick(1); | ||
expect(handler1).to.have.been.calledOnce; | ||
expect(handler1).to.have.been.calledWith(testPayload); | ||
expect(handler2).not.to.have.been.called; | ||
}); | ||
it('can be overriden', () => { | ||
pubst.addTopics([ | ||
{ | ||
name: TEST_TOPIC_1, | ||
doPrime: true | ||
}, | ||
{ | ||
name: TEST_TOPIC_2, | ||
doPrime: false | ||
} | ||
]); | ||
const testPayload = 'some test payload'; | ||
pubst.publish(TEST_TOPIC_1, testPayload); | ||
pubst.publish(TEST_TOPIC_2, testPayload); | ||
const handler1 = sinon.spy(); | ||
const handler2 = sinon.spy(); | ||
pubst.subscribe(TEST_TOPIC_1, { | ||
doPrime: false, | ||
handler: handler1 | ||
}); | ||
pubst.subscribe(TEST_TOPIC_2, { | ||
doPrime: true, | ||
handler: handler2 | ||
}); | ||
clock.tick(1); | ||
expect(handler1).not.to.have.been.called; | ||
expect(handler2).to.have.been.calledOnce; | ||
expect(handler2).to.have.been.calledWith(testPayload); | ||
}); | ||
it('is on by default', () => { | ||
pubst.addTopics([ | ||
{ | ||
name: TEST_TOPIC_1 | ||
} | ||
]); | ||
const testPayload = 'some test payload'; | ||
pubst.publish(TEST_TOPIC_1, testPayload); | ||
const handler = sinon.spy(); | ||
pubst.subscribe(TEST_TOPIC_1, handler); | ||
clock.tick(1); | ||
expect(handler).to.have.been.calledOnce; | ||
expect(handler).to.have.been.calledWith(testPayload); | ||
}); | ||
it('sends default value if no other value is available', () => { | ||
const defaultPayload = 'some test payload'; | ||
pubst.addTopics([ | ||
{ | ||
name: TEST_TOPIC_1, | ||
doPrime: true, | ||
default: defaultPayload | ||
} | ||
]); | ||
const handler = sinon.spy(); | ||
pubst.subscribe(TEST_TOPIC_1, handler); | ||
clock.tick(1); | ||
expect(handler).to.have.been.calledOnce; | ||
expect(handler).to.have.been.calledWith(defaultPayload); | ||
}); | ||
}); | ||
describe('allowRepeats', () => { | ||
it('calls subs when the value does not change', () => { | ||
pubst.addTopics([ | ||
{ | ||
name: TEST_TOPIC_1, | ||
allowRepeats: true | ||
}, | ||
{ | ||
name: TEST_TOPIC_2, | ||
allowRepeats: false | ||
} | ||
]); | ||
const testPayload = 'some test payload'; | ||
const handler1 = sinon.spy(); | ||
const handler2 = sinon.spy(); | ||
pubst.subscribe(TEST_TOPIC_1, handler1); | ||
pubst.subscribe(TEST_TOPIC_2, handler2); | ||
clock.tick(1); | ||
expect(handler1).not.to.have.been.called; | ||
expect(handler2).not.to.have.been.called; | ||
pubst.publish(TEST_TOPIC_1, testPayload); | ||
pubst.publish(TEST_TOPIC_2, testPayload); | ||
clock.tick(1); | ||
expect(handler1).to.have.been.calledWith(testPayload, TEST_TOPIC_1); | ||
expect(handler2).to.have.been.calledWith(testPayload, TEST_TOPIC_2); | ||
handler1.resetHistory(); | ||
handler2.resetHistory(); | ||
pubst.publish(TEST_TOPIC_1, testPayload); | ||
pubst.publish(TEST_TOPIC_2, testPayload); | ||
clock.tick(1); | ||
expect(handler1).to.have.been.calledWith(testPayload, TEST_TOPIC_1); | ||
expect(handler2).not.to.have.been.called; | ||
}); | ||
it('is off by default', () => { | ||
pubst.addTopic({ | ||
name: TEST_TOPIC_1 | ||
}); | ||
const testPayload = 'some test payload'; | ||
const handler = sinon.spy(); | ||
pubst.subscribe(TEST_TOPIC_1, handler); | ||
pubst.publish(TEST_TOPIC_1, testPayload); | ||
clock.tick(1); | ||
pubst.publish(TEST_TOPIC_1, testPayload); | ||
pubst.publish(TEST_TOPIC_1, testPayload); | ||
pubst.publish(TEST_TOPIC_1, testPayload); | ||
pubst.publish(TEST_TOPIC_1, testPayload); | ||
pubst.publish(TEST_TOPIC_1, testPayload); | ||
clock.tick(1); | ||
expect(handler).to.have.been.calledOnceWith(testPayload, TEST_TOPIC_1); | ||
handler.resetHistory(); | ||
pubst.publish(TEST_TOPIC_1, 'something else'); | ||
clock.tick(1); | ||
expect(handler).to.have.been.calledOnceWith('something else', TEST_TOPIC_1); | ||
handler.resetHistory(); | ||
pubst.publish(TEST_TOPIC_1, testPayload); | ||
clock.tick(1); | ||
pubst.publish(TEST_TOPIC_1, testPayload); | ||
pubst.publish(TEST_TOPIC_1, testPayload); | ||
clock.tick(1); | ||
expect(handler).to.have.been.calledOnceWith(testPayload, TEST_TOPIC_1); | ||
}); | ||
it('can be overriden', () => { | ||
pubst.addTopic({ | ||
name: TEST_TOPIC_1, | ||
allowRepeats: false | ||
}); | ||
const testPayload = 'some test payload'; | ||
const handler = sinon.spy(); | ||
pubst.subscribe(TEST_TOPIC_1, { | ||
allowRepeats: true, | ||
handler | ||
}); | ||
pubst.publish(TEST_TOPIC_1, testPayload); | ||
clock.tick(1); | ||
pubst.publish(TEST_TOPIC_1, testPayload); | ||
pubst.publish(TEST_TOPIC_1, testPayload); | ||
clock.tick(1); | ||
expect(handler).to.have.been.calledThrice; | ||
expect(handler).to.have.been.calledWith(testPayload, TEST_TOPIC_1); | ||
}); | ||
}); | ||
}); | ||
describe('publish & subscribe', () => { | ||
@@ -93,0 +538,0 @@ it('calls the subscriber if the topic already has a set value', () => { |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
78471
1537
268
11