Skip to content
Permalink
Fetching contributors…
Cannot retrieve contributors at this time
304 lines (259 sloc) 9.93 KB
var mutate = require('xtend/mutable')
var nanotick = require('nanotick')
var assert = require('assert')
var xtend = require('xtend')
var applyHook = require('./apply-hook')
module.exports = dispatcher
// initialize a new barracks instance
// obj -> obj
function dispatcher (hooks) {
hooks = hooks || {}
assert.equal(typeof hooks, 'object', 'barracks: hooks should be undefined or an object')
var onStateChangeHooks = []
var onActionHooks = []
var onErrorHooks = []
var subscriptionWraps = []
var initialStateWraps = []
var reducerWraps = []
var effectWraps = []
use(hooks)
var reducersCalled = false
var effectsCalled = false
var stateCalled = false
var subsCalled = false
var stopped = false
var subscriptions = start._subscriptions = {}
var reducers = start._reducers = {}
var effects = start._effects = {}
var models = start._models = []
var _state = {}
var tick = nanotick()
start.model = setModel
start.state = getState
start.start = start
start.stop = stop
start.use = use
return start
// push an object of hooks onto an array
// obj -> null
function use (hooks) {
assert.equal(typeof hooks, 'object', 'barracks.use: hooks should be an object')
assert.ok(!hooks.onError || typeof hooks.onError === 'function', 'barracks.use: onError should be undefined or a function')
assert.ok(!hooks.onAction || typeof hooks.onAction === 'function', 'barracks.use: onAction should be undefined or a function')
assert.ok(!hooks.onStateChange || typeof hooks.onStateChange === 'function', 'barracks.use: onStateChange should be undefined or a function')
if (hooks.onStateChange) onStateChangeHooks.push(hooks.onStateChange)
if (hooks.onError) onErrorHooks.push(wrapOnError(hooks.onError))
if (hooks.onAction) onActionHooks.push(hooks.onAction)
if (hooks.wrapSubscriptions) subscriptionWraps.push(hooks.wrapSubscriptions)
if (hooks.wrapInitialState) initialStateWraps.push(hooks.wrapInitialState)
if (hooks.wrapReducers) reducerWraps.push(hooks.wrapReducers)
if (hooks.wrapEffects) effectWraps.push(hooks.wrapEffects)
if (hooks.models) hooks.models.forEach(setModel)
}
// push a model to be initiated
// obj -> null
function setModel (model) {
assert.equal(typeof model, 'object', 'barracks.store.model: model should be an object')
models.push(model)
}
// get the current state from the store
// obj? -> obj
function getState (opts) {
opts = opts || {}
assert.equal(typeof opts, 'object', 'barracks.store.state: opts should be an object')
var state = opts.state
if (!opts.state && opts.freeze === false) return xtend(_state)
else if (!opts.state) return Object.freeze(xtend(_state))
assert.equal(typeof state, 'object', 'barracks.store.state: state should be an object')
var namespaces = []
var newState = {}
// apply all fields from the model, and namespaced fields from the passed
// in state
models.forEach(function (model) {
var ns = model.namespace
namespaces.push(ns)
var modelState = model.state || {}
if (ns) {
newState[ns] = newState[ns] || {}
apply(ns, modelState, newState)
newState[ns] = xtend(newState[ns], state[ns])
} else {
mutate(newState, modelState)
}
})
// now apply all fields that weren't namespaced from the passed in state
Object.keys(state).forEach(function (key) {
if (namespaces.indexOf(key) !== -1) return
newState[key] = state[key]
})
var tmpState = xtend(_state, xtend(state, newState))
var wrappedState = wrapHook(tmpState, initialStateWraps)
return (opts.freeze === false)
? wrappedState
: Object.freeze(wrappedState)
}
// initialize the store hooks, get the send() function
// obj? -> fn
function start (opts) {
opts = opts || {}
assert.equal(typeof opts, 'object', 'barracks.store.start: opts should be undefined or an object')
// register values from the models
models.forEach(function (model) {
var ns = model.namespace
if (!stateCalled && model.state && opts.state !== false) {
var modelState = model.state || {}
if (ns) {
_state[ns] = _state[ns] || {}
apply(ns, modelState, _state)
} else {
mutate(_state, modelState)
}
}
if (!reducersCalled && model.reducers && opts.reducers !== false) {
apply(ns, model.reducers, reducers, function (cb) {
return wrapHook(cb, reducerWraps)
})
}
if (!effectsCalled && model.effects && opts.effects !== false) {
apply(ns, model.effects, effects, function (cb) {
return wrapHook(cb, effectWraps)
})
}
if (!subsCalled && model.subscriptions && opts.subscriptions !== false) {
apply(ns, model.subscriptions, subscriptions, function (cb, key) {
var send = createSend('subscription: ' + (ns ? ns + ':' + key : key))
cb = wrapHook(cb, subscriptionWraps)
cb(send, function (err) {
applyHook(onErrorHooks, err, _state, createSend)
})
return cb
})
}
})
// the state wrap is special because we want to operate on the full
// state rather than indvidual chunks, so we apply it outside the loop
if (!stateCalled && opts.state !== false) {
_state = wrapHook(_state, initialStateWraps)
}
if (!opts.noSubscriptions) subsCalled = true
if (!opts.noReducers) reducersCalled = true
if (!opts.noEffects) effectsCalled = true
if (!opts.noState) stateCalled = true
if (!onErrorHooks.length) onErrorHooks.push(wrapOnError(defaultOnError))
return createSend
// call an action from a view
// (str, bool?) -> (str, any?, fn?) -> null
function createSend (selfName, callOnError) {
assert.equal(typeof selfName, 'string', 'barracks.store.start.createSend: selfName should be a string')
assert.ok(!callOnError || typeof callOnError === 'boolean', 'barracks.store.start.send: callOnError should be undefined or a boolean')
return function send (name, data, cb) {
if (!cb && !callOnError) {
cb = data
data = null
}
data = (typeof data === 'undefined' ? null : data)
assert.equal(typeof name, 'string', 'barracks.store.start.send: name should be a string')
assert.ok(!cb || typeof cb === 'function', 'barracks.store.start.send: cb should be a function')
var done = callOnError ? onErrorCallback : cb
_send(name, data, selfName, done)
function onErrorCallback (err) {
err = err || null
if (err) {
applyHook(onErrorHooks, err, _state, function createSend (selfName) {
return function send (name, data) {
assert.equal(typeof name, 'string', 'barracks.store.start.send: name should be a string')
data = (typeof data === 'undefined' ? null : data)
_send(name, data, selfName, done)
}
})
}
}
}
}
// call an action
// (str, str, any, fn) -> null
function _send (name, data, caller, cb) {
if (stopped) return
assert.equal(typeof name, 'string', 'barracks._send: name should be a string')
assert.equal(typeof caller, 'string', 'barracks._send: caller should be a string')
assert.equal(typeof cb, 'function', 'barracks._send: cb should be a function')
;(tick(function () {
var reducersCalled = false
var effectsCalled = false
var newState = xtend(_state)
if (onActionHooks.length) {
applyHook(onActionHooks, _state, data, name, caller, createSend)
}
// validate if a namespace exists. Namespaces are delimited by ':'.
var actionName = name
if (/:/.test(name)) {
var arr = name.split(':')
var ns = arr.shift()
actionName = arr.join(':')
}
var _reducers = ns ? reducers[ns] : reducers
if (_reducers && _reducers[actionName]) {
if (ns) {
var reducedState = _reducers[actionName](_state[ns], data)
newState[ns] = xtend(_state[ns], reducedState)
} else {
mutate(newState, reducers[actionName](_state, data))
}
reducersCalled = true
if (onStateChangeHooks.length) {
applyHook(onStateChangeHooks, newState, data, _state, actionName, createSend)
}
_state = newState
cb(null, newState)
}
var _effects = ns ? effects[ns] : effects
if (!reducersCalled && _effects && _effects[actionName]) {
var send = createSend('effect: ' + name)
if (ns) _effects[actionName](_state[ns], data, send, cb)
else _effects[actionName](_state, data, send, cb)
effectsCalled = true
}
if (!reducersCalled && !effectsCalled) {
throw new Error('Could not find action ' + actionName)
}
}))()
}
}
// stop an app, essentially turns
// all send() calls into no-ops.
// () -> null
function stop () {
stopped = true
}
}
// compose an object conditionally
// optionally contains a namespace
// which is used to nest properties.
// (str, obj, obj, fn?) -> null
function apply (ns, source, target, wrap) {
if (ns && !target[ns]) target[ns] = {}
Object.keys(source).forEach(function (key) {
var cb = wrap ? wrap(source[key], key) : source[key]
if (ns) target[ns][key] = cb
else target[key] = cb
})
}
// handle errors all the way at the top of the trace
// err? -> null
function defaultOnError (err) {
throw err
}
function wrapOnError (onError) {
return function onErrorWrap (err, state, createSend) {
if (err) onError(err, state, createSend)
}
}
// take a apply an array of transforms onto a value. The new value
// must be returned synchronously from the transform
// (any, [fn]) -> any
function wrapHook (value, transforms) {
transforms.forEach(function (transform) {
value = transform(value)
})
return value
}
You can’t perform that action at this time.