fluxcapacitor
Version:
A bunch of tools to implement apps the Flux way
387 lines (366 loc) • 10.8 kB
JavaScript
;
var _ = require('lodash');
var Q = require('q');
var debug = false;
function capitalize(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
var pending = {};
function Dispatcher(log) {
var callbacks = {};
var api = {
on: function(name, token, callback) {
if (!callback) {
callback = token;
if (!callback.__uuid) callback.__uuid = uuid();
} else {
callback.__uuid = token;
}
var events = callbacks[name] || (callbacks[name] = []);
events.push(callback);
return function() {
api.off(name, callback);
};
},
off: function(name, callback) {
var events = callbacks[name] || [];
callbacks[name] = _.filter(events, function(cb) {
return cb !== callback;
});
},
triggerAsync: function(name, payload) {
var deferred = Q.defer();
var cb = this.trigger;
_.defer(function() {
try {
cb(name, payload);
if (payload.__response) {
deferred.resolve(payload.__response);
} else {
deferred.resolve({});
}
} catch(e) {
console.error(e);
deferred.reject(new Error(e));
}
});
return deferred.promise;
},
trigger: function(name, payload) {
if (log || debug) console.log('[FLUX CAPACITOR] Notify "' + name + '" with payload : ' + JSON.stringify(payload));
var current;
var done = {};
var events = callbacks[name] || [];
var call = function(cb, waiting) {
if (cb && !done[cb.__uuid]) {
if (waiting) {
invariant(waiting && cb.__uuid !== current, 'Cyclic dependency detected, current action directly wait for itself : %s', cb.__uuid);
invariant(waiting && !pending[cb.__uuid], 'Cyclic dependency detected, you are already waiting for %s', cb.__uuid);
}
current = cb.__uuid;
pending[cb.__uuid] = true;
try {
cb(payload, waitFor);
} finally {
delete pending[cb.__uuid];
done[cb.__uuid] = true;
}
}
}
var waitFor = function(arr) {
_.each(arr, function(k) {
if (k.__store && k.token) k = k.token;
call(_.findWhere(events, { __uuid: k }), true);
});
};
_.each(events, function(callback) {
call(callback);
});
var all = callbacks['*'] || [];
_.each(all, function(callback) {
callback(name, payload);
});
}
};
return api;
};
function MultiDispatcher(arr, dispatcherName, log) {
var dispatcher = Dispatcher(log);
function action(name) {
var ret = function() {
var arg = arguments[0] || {};
dispatcher.trigger(dispatcherName + "." + name, arg);
};
ret.triggerAsync = function() {
var arg = arguments[0] || {};
return dispatcher.triggerAsync(dispatcherName + "." + name, arg);
};
ret.listen = function(token, callback) {
if (!callback) {
callback = token;
} else {
callback.__uuid = token;
}
dispatcher.on(dispatcherName + "." + name, callback);
return function() {
ret.off(callback);
};
};
ret.off = function(callback) {
dispatcher.off(dispatcherName + "." + name, callback);
};
ret.__name = name;
ret.__action = true;
return ret;
}
if (!_.isArray(arr) && _.isObject(arr)) {
arr = _.keys(arr);
}
var api = {};
_.each(arr, function(name) {
api[name] = action(name);
});
api.__actions = true;
api.bindTo = function(obj, config) {
var token = uuid();
if (arguments.length >= 2 && _.isString(arguments[0])) {
token = arguments[0];
obj = arguments[1];
config = arguments[2];
}
var subscriptions = [];
if (config) {
_.chain(_.keys(config)).filter(function(key) {
var actualFuncName = config[key];
return _.isFunction(obj[actualFuncName]) || _.isFunction(actualFuncName);
}).each(function(key) {
var actualFuncName = config[key];
subscriptions.push(api[key].listen(token, _.isFunction(actualFuncName) ? actualFuncName.bind(obj) : obj[actualFuncName].bind(obj)));
}).value();
} else {
_.chain(_.keys(api)).filter(function(key) {
if (key === 'bindTo') return false;
return true;
}).map(function(key) {
return {
f: obj['on' + capitalize(key)],
key: key,
handler: 'on' + capitalize(key)
};
}).filter(function(struct) {
return _.isFunction(struct.f);
}).each(function(struct) {
subscriptions.push(api[struct.key].listen(token, struct.f.bind(obj)));
}).value();
}
return function() {
_.each(subscriptions, function(unsubscribe) {
unsubscribe();
});
};
};
return api;
}
function Events(arr, log) {
return MultiDispatcher(arr, "Events", log);
}
function Actions(arr, log) {
return MultiDispatcher(arr, "Actions", log);
}
function keyMirror(obj, prefix) {
if (!prefix) {
prefix = '';
}
var ret = {};
var key;
if (!(obj instanceof Object && !Array.isArray(obj))) {
throw new Error('mirror(...): Argument must be an object.');
}
for (key in obj) {
if (!obj.hasOwnProperty(key)) {
continue;
}
if (obj[key] instanceof Object) {
ret[key] = keyMirror(obj[key], key + '.');
} else {
ret[key] = prefix + key;
}
}
return ret;
}
function uuid() {
var d = Date.now();
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = (d + Math.random()*16)%16 | 0;
d = Math.floor(d/16);
return (c=='x' ? r : (r&0x7|0x8)).toString(16);
});
}
function invariant(condition, message, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p) {
if (!condition) {
var args = [a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p];
var argIndex = 0;
throw new Error("Violation : " + message.replace(/%s/g, function() { return args[argIndex++]; }));
}
}
function invariantLog(condition, message, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p) {
if (!condition) {
var args = [a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p];
var argIndex = 0;
console.error("Violation : " + message.replace(/%s/g, function() { return args[argIndex++]; }));
}
}
function createStore(actions, store) {
var token = uuid();
if (_.isFunction(store)) {
var api = {};
api.token = token;
store(api);
store = api;
}
store.token = token;
store.__store = true;
if (_.isArray(actions)) {
var unsubscribe = function() {};
store.init = store.init || function() {
var subs = [];
_.each(actions, function(a) {
subs.push(a.bindTo(token, store));
});
unsubscribe = function() {
_.each(subs, function(s) { s(); });
}
store.shutdown = unsubscribe;
};
store.init();
store.shutdown = store.shutdown || unsubscribe;
return store;
} else {
var unsubscribe = actions.bindTo(token, store);
store.init = store.init || function() {
store.shutdown = actions.bindTo(token, store);
};
store.shutdown = store.shutdown || unsubscribe;
return store;
}
}
exports.createAction = function(name) {
return Actions([name])[name];
};
exports.createEvent = function(name) {
return Events([name])[name];
};
var reactMixins = {
AutoListen: {
componentDidMount: function() {
var that = this;
var subs = [];
var eventsArr = that.listenTo || [];
eventsArr.forEach(function(e) {
subs.push(e.bindTo(that));
});
that.__unsubscribe = function() {
subs.forEach(function(s) { s(); });
};
},
componentWillUnmount: function() {
this.__unsubscribe();
}
},
AutoListenAt: function(event, name) {
var regName = '__registrationAutoListenAt' + name + Date.now();
return {
componentDidMount: function() {
this[regName] = event.listen(this[name] || function() { console.log('Missing function "' + name + '"'); });
},
componentWillUnmount: function() {
this[regName]();
}
};
},
AutoStates: {
componentDidMount: function() {
var that = this;
var subs = [];
var eventsArr = that.stateFrom || [];
_.each(eventsArr, function(event) {
var actions = [];
if (event.__actions) {
_.each(event, function(value, key) {
if (value.__action) actions.push(value);
});
} else if (event.__action) {
actions.push(event);
}
_.each(actions, function(action) {
subs.push(action.listen(function(payload) {
var name = action.__name;
if (_.startsWith(name, 'on')) {
name = name.slice(2);
}
if (_.startsWith(name, 'set')) {
name = name.slice(3);
}
if (_.startsWith(name, 'notify')) {
name = name.slice(6);
}
if (_.endsWith(name, 'Change')) {
name = name.slice(0, name.length - 6);
}
if (_.endsWith(name, 'Changed')) {
name = name.slice(0, name.length - 7);
}
if (_.endsWith(name, 'Updated')) {
name = name.slice(0, name.length - 7);
}
name = name.charAt(0).toLowerCase() + name.slice(1);
var newState = {};
newState[name] = payload;
that.setState(newState);
}));
});
});
that.__unsubscribe = function() {
subs.forEach(function(s) { s(); });
};
},
componentWillUnmount: function() {
this.__unsubscribe();
}
},
AutoState: function(event, name) {
var regName = '__registrationAutoState' + name + Date.now();
return {
componentDidMount: function() {
var that = this;
that[regName] = event.listen(function(payload) {
var newState = {};
newState[name] = payload;
that.setState(newState);
});
},
componentWillUnmount: function() {
this[regName]();
}
};
}
};
exports.invariant = invariant;
exports.invariantLog = invariantLog;
exports.uuid = uuid;
exports.mirror = keyMirror;
exports.keyMirror = keyMirror;
exports.createDispatcher = Dispatcher;
exports.createActions = Actions;
exports.createEvents = Events;
exports.createStore = createStore;
exports.lodash = _;
exports.q = Q;
exports.Mixins = reactMixins;
exports.Store = function(actions) {
FluxCapacitor.createStore(actions, this);
};
exports.withDebug = function(d) {
debug = d;
return exports;
};