Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wrap handlers #59

Merged
merged 6 commits into from
Sep 7, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,12 @@ hooks are possible:
- __onAction(data, state, name, caller, createSend):__ called when an `action`
is fired
- __onStateChange(data, state, prev, caller, createSend):__ called after a
reducer changes the `state`
reducer changes the `state`.
- __wrapSubscriptions(fn):__ wraps a `subscription` to add custom behavior
- __wrapReducers(fn):__ wraps a `reducer` to add custom behavior
- __wrapEffects(fn):__ wraps an `effect` to add custom behavior
- __wrapInitialState(fn):__ mutate the initial `state` to add custom
behavior - useful to mutate the state before starting up

`createSend()` is a special function that allows the creation of a new named
`send()` function. The first argument should be a string which is the name, the
Expand Down
122 changes: 81 additions & 41 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@ function dispatcher (hooks) {
const onActionHooks = []
const onErrorHooks = []

useHooks(hooks)
const subscriptionWraps = []
const initialStateWraps = []
const reducerWraps = []
const effectWraps = []

use(hooks)

var reducersCalled = false
var effectsCalled = false
Expand All @@ -32,20 +37,24 @@ function dispatcher (hooks) {
start.model = setModel
start.state = getState
start.start = start
start.use = useHooks
start.use = use
return start

// push an object of hooks onto an array
// obj -> null
function useHooks (hooks) {
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.onStateChange) onStateChangeHooks.push(hooks.onStateChange)
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)
}

// push a model to be initiated
Expand All @@ -60,26 +69,32 @@ function dispatcher (hooks) {
function getState (opts) {
opts = opts || {}
assert.equal(typeof opts, 'object', 'barracks.store.state: opts should be an object')
if (opts.state) {
const initialState = {}
const nsState = {}
models.forEach(function (model) {
const ns = model.namespace
const modelState = model.state || {}
if (ns) {
nsState[ns] = {}
apply(ns, modelState, nsState)
nsState[ns] = xtend(nsState[ns], opts.state[ns])
} else {
apply(model.namespace, modelState, initialState)
}
})
return xtend(_state, xtend(opts.state, nsState))
} else if (opts.freeze === false) {
return xtend(_state)
} else {
return Object.freeze(xtend(_state))
}

const 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')

const nsState = {}

models.forEach(function (model) {
const ns = model.namespace
const modelState = model.state || {}
if (ns) {
nsState[ns] = {}
apply(ns, modelState, nsState)
nsState[ns] = xtend(nsState[ns], state[ns])
} else {
mutate(nsState, modelState)
}
})

const tmpState = xtend(_state, xtend(state, nsState))
const wrappedState = wrapHook(tmpState, initialStateWraps)

return (opts.freeze === false)
? wrappedState
: Object.freeze(wrappedState)
}

// initialize the store hooks, get the send() function
Expand All @@ -92,25 +107,46 @@ function dispatcher (hooks) {
models.forEach(function (model) {
const ns = model.namespace
if (!stateCalled && model.state && opts.state !== false) {
apply(ns, model.state, _state)
const 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)
apply(ns, model.reducers, reducers, function (cb) {
return wrapHook(cb, reducerWraps)
})
}
if (!effectsCalled && model.effects && opts.effects !== false) {
apply(ns, model.effects, effects)
apply(ns, model.effects, effects, function (cb) {
return wrapHook(cb, effectWraps)
})
}
if (!subsCalled && model.subscriptions && opts.subscriptions !== false) {
apply(ns, model.subscriptions, subscriptions, createSend, function (err) {
applyHook(onErrorHooks, err)
apply(ns, model.subscriptions, subscriptions, function (cb, key) {
const send = createSend('subscription: ' + (ns ? ns + ':' + key : key))
cb = wrapHook(cb, subscriptionWraps)
cb(send, function (err) {
applyHook(onErrorHooks, err)
})
return cb
})
}
})

if (!opts.noState) stateCalled = true
// 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.noSubscriptions) subsCalled = true
if (!opts.noState) stateCalled = true

if (!onErrorHooks.length) onErrorHooks.push(wrapOnError(defaultOnError))

Expand Down Expand Up @@ -210,18 +246,12 @@ function dispatcher (hooks) {
// optionally contains a namespace
// which is used to nest properties.
// (str, obj, obj, fn?) -> null
function apply (ns, source, target, createSend, done) {
function apply (ns, source, target, wrap) {
if (ns && !target[ns]) target[ns] = {}
Object.keys(source).forEach(function (key) {
if (ns) {
target[ns][key] = source[key]
} else {
target[key] = source[key]
}
if (createSend && done) {
const send = createSend('subscription: ' + (ns ? ns + ':' + key : key))
source[key](send, done)
}
const cb = wrap ? wrap(source[key], key) : source[key]
if (ns) target[ns][key] = cb
else target[key] = cb
})
}

Expand All @@ -236,3 +266,13 @@ function wrapOnError (onError) {
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
}
55 changes: 55 additions & 0 deletions test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const barracks = require('./')
const xtend = require('xtend')
const noop = require('noop2')
const tape = require('tape')

Expand Down Expand Up @@ -547,3 +548,57 @@ tape('hooks: onError', (t) => {
t.test('should have a default err handler')
t.test('should not call itself')
})

tape('wrappers: wrapSubscriptions')
tape('wrappers: wrapReducers')
tape('wrappers: wrapEffects')

tape('wrappers: wrapInitialState', (t) => {
t.test('should wrap initial state in start', (t) => {
t.plan(2)
const store = barracks()
store.use({
wrapInitialState: (state) => {
t.deepEqual(state, { foo: 'bar' }, 'initial state is correct')
return xtend(state, { beep: 'boop' })
}
})

store.model({
state: { foo: 'bar' }
})

store.start()
process.nextTick(() => {
const state = store.state()
t.deepEqual(state, { foo: 'bar', beep: 'boop' }, 'wrapped state correct')
})
})

t.test('should wrap initial state in getState', (t) => {
t.plan(1)
const store = barracks()
store.use({
wrapInitialState: (state) => {
return xtend(state, { beep: 'boop' })
}
})

store.model({
state: { foo: 'bar' }
})

process.nextTick(() => {
const opts = {
state: { bin: 'baz' }
}
const expected = {
foo: 'bar',
beep: 'boop',
bin: 'baz'
}
const state = store.state(opts)
t.deepEqual(state, expected, 'wrapped state correct')
})
})
})
10 changes: 10 additions & 0 deletions wrap-hook.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module.exports = wrapHook

// fold an array of wrappers around a function recursively
// (fn, arr) -> fn
function wrapHook (fn, arr) {
arr.forEach(function (cb) {
fn = cb(fn)
})
return fn
}