superglue
Version:
Sticky mediator that promotes loose-coupling and makes app-flow clearer.
277 lines (224 loc) • 6.25 kB
JavaScript
/*
* Subscriber Storage
* ==================
*
* The primary responsibility of this module is to
* expose methods for working with event-listeners
* and event-listener-groups.
*/
module.exports = {
match : match,
flush : flush,
Listener : Listener,
GroupListener : GroupListener,
/*
* for tests
*/
_listeners : function () {
return listeners;
},
_groups : function () {
return groups;
}
};
/*
* Storage of event-listeners & event-groups
*/
var listeners = [];
var groups = [];
/**
* Search for event-listeners containing the
* event names that match a given event name.
*
* @param {String} name
* @return {Array} results
*/
function match (name) {
var results;
results = listeners.filter(function (listener) {
return listener.eventNames.indexOf(name) !== -1;
});
// search for groups with event-name matching `name`
for (var i = 0 , len = groups.length, group ; i < len ; i++ ) {
group = groups[i];
if (group.eventNames.indexOf(name) !== -1) {
// recursively call .match on each .fire event-names
for (var j = 0 , jlen = group.fire.length ; j < jlen ; j++) {
results = results.concat( match( group.fire[j] ) );
}
}
}
return results;
}
/**
* Flush listeners and groups with the matching name.
*
* If a group or listener has multiple names, then the
* name matching the argument `name` will be removed.
* If the group or listener has only one name matching
* the arg. `name`, then the entire object will be removed.
*
* @param {String} name
*/
function flush (name) {
function spliceByName (arr) {
for (var i=arr.length, index; i-- ;) {
index = arr[i].eventNames.indexOf(name);
if (index !== -1) {
if (arr[i].eventNames.length === 1) {
arr.splice(i, 1);
} else {
arr[i].eventNames.splice(index, 1);
}
}
}
}
spliceByName(listeners);
spliceByName(groups);
}
////////////////////////////////////////////////////////////////////////
// Listener Constructor //
////////////////////////////////////////////////////////////////////////
/*
* When enough listener components (predicates, dependencies, logic)
* are added to make a valid event listener, then the event listener
* instance is added to the `listeners` array and updated anytime the
* instance changes.
*/
function Listener () {
this._id = null;
this.eventNames = [];
this.predicates = [];
this.dependencies = [];
this.logic = [];
}
Listener.prototype = Object.create(ParentListener.prototype);
/**
* Add event-listener logic
*
* @method addLogic
* @param {Function} fn
* @chainable
*/
Listener.prototype.addLogic = function (fn) {
if (typeof fn === "function") {
this.logic.push(fn);
this._persist();
}
return this;
};
/**
* Add event-listener predicate
*
* @method addPredicate
* @param {Function} predicate
* @chainable
*/
Listener.prototype.addPredicate = function (predicate) {
if (typeof predicate === "function") {
this.predicates.push(predicate);
this._persist();
}
return this;
};
/**
* Add event-listener dependency
*
* @method addDependency
* @param {Function} dependency
* @chainable
*/
Listener.prototype.addDependency = function (dependency) {
if (typeof dependency === "function") {
this.dependencies.push(dependency);
this._persist();
}
return this;
};
Listener.prototype._persist = persist(listeners, ["eventNames", "logic"]);
////////////////////////////////////////////////////////////////////////
// Group Listener Constructor //
////////////////////////////////////////////////////////////////////////
/*
* Similar to event-listeners, when enough components are registered, the
* group listener instance is saved. When the instance is updated, the
* instance stored will also be updated.
*/
function GroupListener () {
this._id = null;
this.eventNames = [];
this.fire = [];
}
GroupListener.prototype = Object.create(ParentListener.prototype);
/**
* Add event-names as those needing to be fired
*
* @method addFire
* @param {String} name
* @chainable
*/
GroupListener.prototype.addFire = function (name) {
if (typeof name === "string" && name) {
this.fire.push(name);
this._persist();
}
return this;
};
GroupListener.prototype._persist = persist(groups, ["eventNames", "fire"]);
////////////////////////////////////////////////////////////////////////
// Parent Listener Constructor //
////////////////////////////////////////////////////////////////////////
/*
* There are enough similarities between Listener instances and
* GroupListener instances to warrant the use of a parent constructor.
*/
function ParentListener () {}
/**
* Test whether the [group] listener has had enough required
* components added to be registered in `listeners` or `groups` array.
*
* @method isSaved
* @public
* @return {Boolean} test
*/
ParentListener.prototype.isSaved = function () {
return this._id !== null;
};
/**
* Add event-listener name
*
* @method addEventName
* @param {String} name
* @chainable
*/
ParentListener.prototype.addEventName = function (name) {
if (typeof name === "string" && name) {
this.eventNames.push(name);
this._persist();
}
return this;
};
/**
* If the instance has already been pushed into `storage`,
* update the instance with the new state. Else, check
* if all the required components are there and if so,
* push instance into `storage`.
*
* Closure allows us to bind variables to the returning
* function, perfect for class methods not having to
* require parameters.
*/
function persist (storage, required) {
// anticipated that required fields will be an array or string
var testEach = function (field) {
return this[field].length > 0;
};
return function () {
if (this.isSaved()) {
storage[this._id] = this;
} else if (required.every(testEach, this)) {
this._id = storage.push(this) - 1;
storage[this._id]._id = this._id;
}
};
}