Created
February 2, 2015 23:18
-
-
Save dashed/e38d4184bc239268737e to your computer and use it in GitHub Desktop.
immstructor - wrapper around immstruct [WIP - very experimental]
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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); | |
}); | |
} | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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); | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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); | |
}); | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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