UNPKG

superglue

Version:

Sticky mediator that promotes loose-coupling and makes app-flow clearer.

248 lines (197 loc) 5.37 kB
/* * Publisher Facade * ================ */ module.exports = Publish; var match = require("./storage").match; var ContextError = require("./errors/ContextError"); /** * Publish constructor * * Argument order is insignificant. Pass in the context when you'd like the * events fired to have it. * * @constructor * @param {Array} initial - array of event-names to trigger (strings) & contexts (object) */ function Publish (initial) { this._cxt = null; this._err = null; if (Array.isArray(initial)) { for (var i = 0 , len = initial.length ; i < len ; i++) { if (typeof initial[i] === "string" && initial[i]) { this.events(initial[i]); } else if (typeof initial[i] === "object" && initial[i]) { this.context(initial[i]); } } } } /** * Assign the context. * * @method context * @param {Object} cxt * @chainable */ Publish.prototype.context = function (cxt) { if (typeof cxt === "object" && cxt) { this._cxt = cxt; } return this; }; /** * Fire off events. * * No limit to event name arguments or to how many times you * can call .event & .events * * @example * publish * .context(req) * .events("users:read:build-query", "users:read:query", "users:read:format-results") * * @method events * @param {String} name; can pass in multiple * @chainable */ Publish.prototype.events = function () { var name, listeners, listener, fn, returned; if (this._err || arguments.length === 0) { return this; } for (var i = 0, len = arguments.length ; i < len ; i++) { name = arguments[i]; if (typeof name !== "string") { continue; } listeners = match(name); // if any listeners were found... for (var j = 0 ; j < listeners.length ; j++) { listener = listeners[j]; // if all listener-predicates pass if (validate(listener.predicates, this._cxt)) { // if the dependencies aren't met in the context, ContextError if (!validate(listener.dependencies, this._cxt)) { this._err = new ContextError("event: '" + name + "' had missing context dependencies"); return this; } for (var k = 0 ; k < listener.logic.length ; k++) { fn = listener.logic[k]; returned = fn.call(this._cxt); // if error is returned, set & stop execution if (returned instanceof Error) { returned.failedOn = name; this._err = returned; return this; } } } } } return this; }; /** * Alias for .events * * @method event */ Publish.prototype.event = Publish.prototype.events; /** * Fire Tasks * * If the task accidently begins task name with ":", * .tasks() will still work correctly. ":" can be contained * inside the task name, which might be useful if doing * sub-tasks. * * @example * // intended use * .tasks("event", ["task1", "task2"]); * // also will work * .tasks("event", "task"); * .tasks("event", ":task"); * * @method tasks * @param {String} eventName * @param {Array|String} task(s) - can be a string if only one task is being fired * @chainable */ Publish.prototype.tasks = function (eventName, tasks) { if (typeof tasks === "string") { tasks = [tasks]; } if (typeof eventName === "string" && Array.isArray(tasks)) { for (var i = 0, len = tasks.length, task, eventTaskName ; i < len ; i++) { task = tasks[i]; eventTaskName = task.slice(0,1) === ":" ? eventName + task : eventName + ":" + task; this.events(eventTaskName); } } return this; }; /** * Alias for .tasks * * @method task */ Publish.prototype.task = Publish.prototype.tasks; /** * Publisher provided function * * Publisher functions can update the context * just like subscriber functions. * * If an error has been thrown, publisher functions * will not be invoked (just like listener functions) * UNLESS the publisher function has a .length === 1 * * Publisher functions can be called at any time * in the event-flow, not just at the end. * * @example * publish * .context({ data : true }) * .event("event-name") * .then(function () {...}, function () {...}) * .events("more-event-names") * .then(function (err) {...}); * * @method then * @param {Function} * @chainable */ Publish.prototype.then = function () { var returned; for (var i = 0, len = arguments.length, fn ; i < len ; i++) { fn = arguments[i]; if (typeof fn === "function") { // always invoke functions that accept errors if (fn.length === 1) { returned = fn.call(this._cxt, this._err); } else if (!this._err) { returned = fn.call(this._cxt); } // assign/reassign error if returned from function if (returned instanceof Error) { this._err = returned; } } } return this; }; /** * Helper Function: Test if all predicates * return true. Also works with context * dependency-test functions. * * If predicates/dependency functions array * is empty, return true. * * @param {Array} predicates * @param {mixed} context */ function validate (predicates, context) { for (var i = 0, len = predicates.length ; i < len ; i++) { if (!predicates[i].call(context)) { return false; } } return true; }