fh-wfm-mediator
Version:
An implementation of the mediator pattern for use with WFM
162 lines (147 loc) • 5.37 kB
JavaScript
const Promise = require('bluebird');
const _ = require('lodash');
function Topics(mediator) {
this.mediator = mediator;
this.subscriptions = {};
}
/**
* Sets the prefix configuration for this instance, which will be part of the name
* of the handled topics.
* @param {String} prefix
* @return {Topics} returns self for chaining
*/
Topics.prototype.prefix = function(prefix) {
this.prefix = prefix;
return this;
};
/**
* Sets the entity configuration for this instance, which will be part of the name
* of the handled topics.
* This property is present as a convenience so it can be accessed by handlers via `this.entity`
*
* @param {String} entity
* @return {Topics} returns self for chaining
*/
Topics.prototype.entity = function(entity) {
this.entity = entity;
return this;
};
/**
* Internal function to add a subscription to the internal collection
* @param {string} topic topic id
* @param {Function} fn handler for the topic
*/
Topics.prototype.addSubscription = function(topic, fn) {
this.subscriptions[topic] = this.mediator.subscribe(topic, fn);
};
/**
* Builds a topic name out of the configured {@link prefix} and {@link entity}
* @param {String} topicName The name of the sub-topic to build
* @param {String} prefix An optional prefix to the final topic, i.e. 'done'
* @param {String} topicUid An optional unique identifier to append
* @return {String} The complete topic name,
* i.e. {prefix}:{this.prefix}:{this.entity}:{topicName}:{topicUid}
*/
Topics.prototype.getTopic = function(topicName, prefix, topicUid) {
// create, done => done:wfm:user:create
var parts = _.compact([this.prefix, this.entity, topicName, topicUid]);
if (prefix) {
parts.unshift(prefix);
}
return parts.join(':');
};
/**
* Internal function to wrap a `on` handler in a promise that will publish to the
* related 'done:' and 'error:' topics
* @param {Topics} self The instance, receive as a param to avoid exposing this function
* in the prototype
* @param {String} method The base topic to publish results to
* @param {Function} fn Handler to wrap, can return a value or a Promise, will be invoked bound to self
* @return {Function} Wrapped handler
*/
function wrapInMediatorPromise(self, method, fn) {
function publishDone(result) {
if (_.isUndefined(result)) {
return;
}
var topic = self.getTopic(method, 'done');
if (_.has(result, 'id')) {
topic = [topic, result.id].join(':');
} else if (typeof result === 'string') {
topic = [topic, result].join(':');
}
self.mediator.publish(topic, result);
return result;
}
function publishError(error) {
var topic = self.getTopic(method, 'error');
if (_.has(error, 'id')) {
topic = [topic, error.id].join(':');
}
self.mediator.publish(topic, error);
}
return function() {
return Promise.resolve(fn.apply(self, arguments))
.then(publishDone)
.catch(publishError);
};
}
/**
* Setup a handler for a namespaced topic, if this handler returns a value or throws an Error,
* it will get published to 'done' and 'error'-predixed topics as per convention.
*
* @param {String} method Topic name inside the namespace
* @param {Function} fn Handler that can optionally return a value or a Promise
* that will be treated as the result of a `request`
* @return {Topics} Returns self for chaining
*/
Topics.prototype.on = function(method, fn) {
var topic = this.getTopic(method);
this.addSubscription(topic, wrapInMediatorPromise(this, method, fn));
return this;
};
/**
* Setup a handler for a namespaced topic, with the 'done:' prefix
* @param {String} method Topic name inside the namespace
* @param {Function} fn Handler function for the topic
* @return {Topics} Returns self for chaining
*/
Topics.prototype.onDone = function(method, fn) {
var topic = this.getTopic(method, 'done');
this.addSubscription(topic, fn.bind(this));
return this;
};
/**
* Setup a handler for a namespaced topic, with the 'done:' prefix
* @param {String} method Topic name inside the namespace
* @param {Function} fn Handler function for the topic
* @return {Topics} Returns self for chaining
*/
Topics.prototype.onError = function(method, fn) {
var topic = this.getTopic(method, 'error');
this.addSubscription(topic, fn.bind(this));
return this;
};
/**
* Modifies mediator to unsubscribe to all topics configured through this instance
*/
Topics.prototype.unsubscribeAll = function() {
var subId;
for (var topic in this.subscriptions) {
if (this.subscriptions.hasOwnProperty(topic)) {
subId = this.subscriptions[topic].id;
this.mediator.remove(topic, subId);
}
}
};
/**
* Does a {@link Mediator.request} in the context of the namespaced topics
* @param {String} topic Base topic inside the configured namespace
* @param {Any} params Data for the `request`
* @param {Object} options Options for the `request`
* @return {Promise} The result of the `request`
*/
Topics.prototype.request = function(topic, params, options) {
return this.mediator.request(this.getTopic(topic), params, options);
};
module.exports = Topics;