Skip to content

Instantly share code, notes, and snippets.

@dashed
Created February 2, 2015 23:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dashed/e38d4184bc239268737e to your computer and use it in GitHub Desktop.
Save dashed/e38d4184bc239268737e to your computer and use it in GitHub Desktop.
immstructor - wrapper around immstruct [WIP - very experimental]
/**
* Events utility mixin for immstructor
*/
var
Promise = require('bluebird');
var
// TODO: Expose this?
/**
* Holds the assigned EventEmitters by name.
*
* @type {Object}
* @private
*/
_onEvents = void 0,
_doneEvents = void 0;
/**
* Representation of a single listener function.
*
* @param {Function} fn Event handler to be called.
* @param {Mixed} context Context for function execution.
* @param {Boolean} once Only emit once
* @api private
*/
function Subscriber(fn, context, once) {
this.fn = fn;
this.context = context;
this.once = once || false;
}
function addEvent(eventStore, event, fn, context, once) {
if(eventStore === 1) {
if(!_onEvents)
_onEvents = {};
eventStore = _onEvents;
} else {
if(!_doneEvents)
_doneEvents = {};
eventStore = _doneEvents;
}
once = once || false;
var listener = new Subscriber(fn, context || this, once);
if(!eventStore)
throw new Error('eventStore is not set');
if (!eventStore[event]) {
eventStore[event] = listener;
} else if (!eventStore[event].fn) {
eventStore[event].push(listener);
} else {
// convert to array
eventStore[event] = [
eventStore[event], listener
];
}
return this;
}
function removeEvent(eventStore, event, fn, once) {
if (!eventStore || !eventStore[event]) return this;
var
listeners = eventStore[event],
events = [];
if(!fn) {
delete eventStore[event];
return this;
}
if (listeners.fn && (listeners.fn !== fn || (once && !listeners.once))) {
events.push(listeners);
} else if (!listeners.fn) {
for (var i = 0, length = listeners.length; i < length; i++) {
if (listeners[i].fn !== fn || (once && !listeners[i].once)) {
events.push(listeners[i]);
}
}
}
// Reset the array, or remove it completely if we have no more listeners.
if (events.length) {
eventStore[event] = events.length === 1 ? events[0] : events;
} else {
delete eventStore[event];
}
return this;
}
function executeDone(event, structure) {
if (!_doneEvents || !_doneEvents[event]) return;
var
listeners = _doneEvents[event];
if ('function' === typeof listeners.fn) {
// one listener
if (listeners.once) this.removeDoneListener(event, listeners.fn, true);
listeners.fn.call(listeners.context, structure);
} else {
var length = listeners.length,
i;
for (i = 0; i < length; i++) {
if (listeners[i].once) this.removeDoneListener(event, listeners[i].fn, true);
listeners[i].fn.call(listeners[i].context, structure);
}
}
}
var Events = module.exports = {
on: function on(event, fn, context) {
return addEvent.call(this, 1, event, fn, context, false);
},
once: function once(event, fn, context) {
return addEvent.call(this, 1, event, fn, context, true);
},
done: function done(event, fn, context) {
return addEvent.call(this, 2, event, fn, context, false);
},
doneOnce: function doneOnce(event, fn, context) {
return addEvent.call(this, 2, event, fn, context, true);
},
/**
* Remove event listeners.
*
* @param {String} event The event we want to remove.
* @param {Function} fn The listener that we need to find.
* @param {Boolean} once Only remove once listeners.
* @api public
*/
removeListener: function removeListener(event, fn, once) {
return removeEvent.call(this, _onEvents, event, fn, once);
},
removeDoneListener: function removeDoneListener(event, fn, once) {
return removeEvent.call(this, _doneEvents, event, fn, once);
},
/**
* Emit an event to all registered event listeners.
*
* @param {String} event The name of the event.
* @param {String | Structure} structure The key of the structure or an instance of Structure.
*
* @returns {Promise} A promise that is fulfilled when all the promises returned
* by handlers of the event are either fulfilled or rejected. Otherwise, if a structure cannot be found,
* then a promise resolved to undefined is instead returned.
*
* @api public
*/
execute: function execute(event, structure) {
var self = this;
if(!structure)
return Promise.resolve();
// TODO: better check
if(structure.key && !Events.exists(structure.key)) {
return Promise.resolve();
}
// else
if(!structure.key) {
if(!Events.exists(structure))
return Promise.resolve();
structure = Events.instances[structure];
}
if (!_onEvents || !_onEvents[event] || !event) {
return Promise.resolve().then(function() {
executeDone.call(self, event, structure);
});
}
var
listeners = _onEvents[event],
len = arguments.length,
args,
promises, ret,
i;
args = new Array(len);
args[0] = structure;
// TODO: remove???
args[1] = Events.meta(structure);
for (i = 2; i < len; i++) {
args[i] = arguments[i];
}
if ('function' === typeof listeners.fn) {
// one listener
if (listeners.once) Events.removeListener(event, listeners.fn, true);
// TODO: https://github.com/primus/EventEmitter3/blob/master/index.js#L70-L77
ret = listeners.fn.apply(listeners.context, args);
promises = [ret];
} else {
// multiple listeners
var
length = listeners.length;
promises = new Array(length);
for (i = 0; i < length; i++) {
if (listeners[i].once) Events.removeListener(event, listeners[i].fn, true);
promises[i] = listeners[i].fn.apply(listeners[i].context, args);
}
}
// resolve all promises
return Promise.settle(promises).then(function() {
return executeDone.call(self, event, structure);
});
}
};
/**
* Decorates the immstruct library with useful API for isomorphic react apps.
*
* The goal is to be able to associate a meta-structure (Immutable collection), populated with data/objects
* (such as actions or stores), to an immutable structure. In addition, the library
* enables us to lazily construct an immutable structure and its meta-structure
* by way of event hooks.
*
*
* EventEmitter-like code is adapted from EventEmitter3 v0.1.6 with the following
* modifications:
*
* - Listeners are functions that return promises.
* - Promises returned by each listener for an event are resolved together using
* Promise.settle(array) provided by the bluebird library.
* - When an event is executed for an immutable structure, each listener for that
* event is given that structure.
*
* TODO:
* - tests
*/
var
immstruct = require('immstruct'),
StructureMixin = require('./structure'),
EventsMixin = require('./events'),
ListenMixin = require('./listen');
var immstructor = module.exports = function() {
// TODO: Ideally immstruct would be instantiable. Right now immstruct.instances
// is globally polluted.
var structure = immstruct.apply(immstruct, arguments);
return structure;
};
Object.assign(immstructor, EventsMixin, StructureMixin, ListenMixin);
/**
* Listen utility mixin for immstructor
*
* Listen to key paths in a structure.
*/
var
Structure = require('immstruct/src/structure'),
Immutable = require('immutable');
// used for notSetValue for immutable library
var NOT_SET = {};
// Immutable structure of meta-attributes
var REGISTRY = new Structure();
var Listen = module.exports = {};
function fetchNode(rootNode, keyPath, create) {
var
current = rootNode,
i = 0;
for(i = 0; i < keyPath.length; i++) {
var children = current.get('children', NOT_SET);
if(children === NOT_SET) {
if(create) {
children = current.set('children', Immutable.Map()).get('children');
} else {
current = null;
break;
}
}
var key = keyPath[i];
current = children.get(key, NOT_SET);
if(current === NOT_SET) {
if(create) {
current = children.set(key, Immutable.Map()).get(key);
} else {
current = null;
break;
}
}
}
return current;
}
function processListeners(structure, keyPath, data) {
// fetch listeners
var rootNode = REGISTRY.cursor([structure, 'listenTo']).deref(NOT_SET);
if(rootNode === NOT_SET)
return;
var current = fetchNode(rootNode.cursor(), keyPath);
if(!current)
return;
var listeners = current.get('listeners', NOT_SET);
if(listeners === NOT_SET)
return;
// process listeners
listeners.forEach(function(fn) {
fn(data);
});
// TODO: remove this...
// var promises = [];
// listeners.forEach(function(fn) {
// var ret = fn(data);
// promises.push(Promise.resolve(ret));
// });
// return Promise.settle(promises).then(function() {
// // TODO: now what?
// });
}
// regiter change, add, delete events
function register(structure) {
structure.on('change', function(keyPath, newValue, oldValue) {
var results = {
event: 'change',
newValue: newValue,
oldValue: oldValue,
path: keyPath
};
processListeners(structure, keyPath, results);
});
structure.on('add', function(keyPath, newValue) {
var results = {
event: 'add',
newValue: newValue,
path: keyPath
};
processListeners(structure, keyPath, results);
});
structure.on('delete', function(keyPath, oldValue) {
var results = {
event: 'delete',
oldValue: oldValue,
path: keyPath
};
processListeners(structure, keyPath, results);
});
}
// TODO: merge this into an extension of Structure?
Listen.listenTo = function listenTo(structure, keyPath, listener) {
if(!structure.key) {
throw new Error('Given structure is not a structure');
}
var rootNode = REGISTRY.cursor([structure, 'listenTo']).deref(NOT_SET);
if(rootNode === NOT_SET) {
rootNode = new Structure();
REGISTRY.cursor([structure, 'listenTo']).update(function() {
return rootNode;
});
register(structure);
}
var current = fetchNode(rootNode.cursor(), keyPath, true);
// TODO: separate as a function
current.update('listeners', function(m) {
if(!m) {
return Immutable.List([listener]);
}
return m.push(listener);
});
};
/**
* Structure utility mixin for immstructor
*/
var
_ = require('lodash'),
immstruct = require('immstruct'),
Immutable = require('immutable'),
Cursor = require('immutable/contrib/cursor'),
Structure = require('immstruct/structure');
// Shared/static/global public structure
var _global = Immutable.fromJS({});
var structure = module.exports = {
// An object mapping structure keys to an Immutable collection (meta-structure).
chest: {},
exists: function exists(structureKey) {
// var obj = immstruct.instances;
// return obj ? hasOwnProperty.call(obj, structureKey) : false;
return _.has(immstruct.instances, structureKey);
},
global: function global(path) {
path = path || [];
var changeListener = function (newData, oldData, path) {
_global = _global.updateIn(path, function (data) {
return newData.getIn(path);
});
return _global;
};
return Cursor.from(_global, path, changeListener);
},
/**
* Retrieve meta-structure associated with structure from the chest, and get cursor at path.
*
* @param {Structure | key} structure Structure or structure key
* @param {Array} path Cursor path
* @returns {Cursor} Cursor of meta-structure at path.
* @api public
*/
// TODO: refactor all of this!
meta: function meta(structure, path) {
// path = path || [];
var
key = structure,
treasure;
if(structure.key) {
key = structure.key;
}
treasure = this.chest[key];
if(!_.has(this.chest, key)) {
treasure = this.chest[key] = new Structure({
key: key
});
}
if(!path) {
return treasure;
}
return treasure.cursor(path);
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment