├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── apply-hook.js ├── index.js ├── package.json ├── script └── test-size └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | # files 2 | *.log 3 | *.pid 4 | *.seed 5 | 6 | # folders 7 | logs/ 8 | pids/ 9 | build/ 10 | coverage/ 11 | node_modules/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | node_js: 2 | - "4" 3 | - "6" 4 | sudo: false 5 | language: node_js 6 | script: "npm run test:cov" 7 | after_script: "npm install coveralls@2 && cat ./coverage/lcov.info | coveralls" 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Yoshua Wuyts 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # barracks 2 | [![NPM version][npm-image]][npm-url] 3 | [![build status][travis-image]][travis-url] 4 | [![Test coverage][coveralls-image]][coveralls-url] 5 | [![Downloads][downloads-image]][downloads-url] 6 | 7 | Action dispatcher for unidirectional data flows. Creates tiny models of data 8 | that can be accessed with actions through a small API. 9 | 10 | ## Usage 11 | ````js 12 | const barracks = require('barracks') 13 | 14 | const store = barracks() 15 | 16 | store.use({ 17 | onError: (err, state, createSend) => { 18 | console.error(`error: ${err}`) 19 | }, 20 | onAction: (state, data, name, caller, createSend) => { 21 | console.log(`data: ${data}`) 22 | }, 23 | onStateChange: (state, data, prev, caller, createSend) => { 24 | console.log(`state: ${prev} -> ${state}`) 25 | } 26 | }) 27 | 28 | store.model({ 29 | namespace: 'cakes', 30 | state: {}, 31 | effects: {}, 32 | reducers: {}, 33 | subscriptions: {} 34 | }) 35 | 36 | const createSend = store.start({ subscriptions: true }) 37 | const send = createSend('myDispatcher', true) 38 | document.addEventListener('DOMContentLoaded', () => { 39 | store.start() // fire up subscriptions 40 | const state = store.state() 41 | send('foo:start', { name: 'Loki' }) 42 | }) 43 | ```` 44 | 45 | ## API 46 | ### store = barracks(hooks?) 47 | Initialize a new `barracks` instance. Takes an optional object of hooks which 48 | is passed to `.use()`. 49 | 50 | ### store.use(hooks) 51 | Register new hooks on the store. Hooks are little plugins that can extend 52 | behavior or perform actions at specific points in the life cycle. The following 53 | hooks are possible: 54 | - __models:__ an array of models that will be merged with the store. 55 | - __onError(err, state, createSend):__ called when an `effect` or 56 | `subscription` emit an error; if no hook is passed, the default hook will 57 | `throw` on each error 58 | - __onAction(state, data, name, caller, createSend):__ called when an `action` 59 | is fired 60 | - __onStateChange(state, data, prev, caller, createSend):__ called after a 61 | reducer changes the `state`. 62 | - __wrapSubscriptions(fn):__ wraps a `subscription` to add custom behavior 63 | - __wrapReducers(fn):__ wraps a `reducer` to add custom behavior 64 | - __wrapEffects(fn):__ wraps an `effect` to add custom behavior 65 | - __wrapInitialState(obj):__ mutate the initial `state` to add custom 66 | behavior - useful to mutate the state before starting up 67 | 68 | `createSend()` is a special function that allows the creation of a new named 69 | `send()` function. The first argument should be a string which is the name, the 70 | second argument is a boolean `callOnError` which can be set to `true` to call 71 | the `onError` hook instead of a provided callback. It then returns a 72 | `send(actionName, data?)` function. 73 | 74 | The `wrap*` hooks are synchronously resolved when the `store.start()` method is 75 | called, and the corresponding values from the models are loaded. All wrap hooks 76 | (or wrappers) are passed the argument that would usually be called, so it can 77 | be wrapped or modified. Say we want to make all our `reducers` print `'golden 78 | pony'` every time they're run, we'd do: 79 | ```js 80 | const barracks = require('barracks') 81 | const store = barracks() 82 | 83 | store.use({ 84 | wrapReducers: function wrapConstructor (reducer) { 85 | return function wrapper (state, data) { 86 | console.log('golden pony') 87 | return reducer(state, data) 88 | } 89 | } 90 | }) 91 | ``` 92 | 93 | Hooks should be used with care, as they're the most powerful interface into 94 | the state. For application level code, it's generally recommended to delegate to 95 | actions inside models using the `send()` call, and only shape the actions 96 | inside the hooks. 97 | 98 | ### store.model() 99 | Register a new model on the store. Models are optionally namespaced objects 100 | with an initial `state` and handlers for dealing with data: 101 | - __namespace:__ namespace the model so that it cannot access any properties 102 | and handlers in other models 103 | - __state:__ initial values of `state` inside the model 104 | - __reducers:__ synchronous operations that modify state; triggered by `actions` 105 | - __effects:__ asynchronous operations that don't modify state directly; 106 | triggered by `actions`, can call `actions` 107 | - __subscriptions:__ asynchronous read-only operations that don't modify state 108 | directly; can call `actions` 109 | 110 | `state` within handlers is immutable through `Object.freeze()` and thus cannot 111 | be modified. Return data from `reducers` to modify `state`. See [handler 112 | signatures](#handler-signatures) for more info on the handlers. 113 | 114 | For debugging purposes, internal references to values can be inspected through a 115 | series of private accessors: 116 | - `store._subscriptions` 117 | - `store._reducers` 118 | - `store._effects` 119 | - `store._models` 120 | 121 | ### state = store.state(opts) 122 | Get the current state from the store. Opts can take the following values: 123 | - __freeze:__ default: true; set to false to not freeze state in handlers 124 | using `Object.freeze()`; useful for optimizing performance in production 125 | builds 126 | - __state:__ pass in a state object that will be merged with the state returned 127 | from the store; useful for rendering in Node 128 | 129 | ### send = createSend(name) = store.start(opts) 130 | Start the store and get a `createSend(name)` function. Pass a unique `name` to 131 | `createSend()` to get a `send()` function. Opts can take the following values: 132 | - __subscriptions:__ default: true; set to false to not register 133 | `subscriptions` when starting the application; useful to delay `init` 134 | functions until the DOM has loaded 135 | - __effects:__ default: true; set to `false` to not register `effects` when 136 | starting the application; useful when only wanting the initial `state` 137 | - __reducers:__ default: true; set to false to not register `reducers` when 138 | starting the application; useful when only wanting the initial `state` 139 | 140 | If the store has disabled any of the handlers (e.g. `{ reducers: false }`), 141 | calling `store.start()` a second time will register the remaining values. This 142 | is useful if not everything can be started at the same time (e.g. have 143 | `subscriptions` wait for the `DOMContentLoaded` event). 144 | 145 | ### send(name, data?) 146 | Send a new action to the models with optional data attached. Namespaced models 147 | can be accessed by prefixing the name with the namespace separated with a `:`, 148 | e.g. `namespace:name`. 149 | 150 | ### store.stop() 151 | 152 | After an app is "stopped" all subsequent `send()` calls become no-ops. 153 | 154 | ```js 155 | store.stop() 156 | send('trimBeard') // -> does not call a reducer/effect 157 | ``` 158 | 159 | ## Handler signatures 160 | These are the signatures for the properties that can be passed into a model. 161 | 162 | ### namespace 163 | An optional string that causes `state`, `effects` and `reducers` to be 164 | prefixed. 165 | 166 | ```js 167 | app.model({ 168 | namespace: 'users' 169 | }) 170 | ``` 171 | 172 | ### state 173 | State can either be a value or an object of values that is used as the initial 174 | state for the application. If namespaced the values will live under 175 | `state[namespace]`. 176 | ```js 177 | app.model({ 178 | namespace: 'hey', 179 | state: { foo: 'bar' } 180 | }) 181 | app.model({ 182 | namespace: 'there', 183 | state: { bin: [ 'beep', 'boop' ] } 184 | }) 185 | app.model({ 186 | namespace: 'people', 187 | state: 'oi' 188 | }}) 189 | ``` 190 | 191 | ### reducers 192 | Reducers are synchronous functions that return a value synchronously. No 193 | eventual values, just values that are relevant for the state. It takes two 194 | arguments of `data` and `state`. `data` is the data that was emitted, and 195 | `state` is the current state. Each action has a name that can be accessed 196 | through `send(name)`, and when under a namespace can be accessed as 197 | `send(namespace:name)`. When operating under a namespace, reducers only have 198 | access to the state within the namespace. 199 | ```js 200 | // some model 201 | app.model({ 202 | namespace: 'plantcake', 203 | state: { 204 | enums: [ 'veggie', 'potato', 'lettuce' ] 205 | paddie: 'veggie' 206 | } 207 | }) 208 | 209 | // so this model can't access anything in the 'plantcake' namespace 210 | app.model({ 211 | namespace: 'burlybeardos', 212 | state: { count: 1 }, 213 | reducers: { 214 | feedPlantcake: (state, data) => { 215 | return { count: state.count + 1 } 216 | }, 217 | trimBeard: (state, data) => ({ count: state.count - 1 }) 218 | } 219 | }) 220 | ``` 221 | 222 | ### effects 223 | `effects` are asynchronous methods that can be triggered by `actions` in 224 | `send()`. They never update the state directly, but can instead do thing 225 | asynchronously, and then call `send()` again to trigger a `reducer` that can 226 | update the state. `effects` can also trigger other `effects`, making them fully 227 | composable. Generally, it's recommended to only have `effects` without a 228 | `namespace` call other `effects`, as to keep namespaced models as isolated as 229 | possible. 230 | 231 | When an `effect` is done executing, or encounters an error, it should call the 232 | final `done(err)` callback. If the `effect` was called by another `effect` it 233 | will call the callback of the caller. When an error propagates all the way to 234 | the top, the `onError` handler will be called, registered in 235 | `barracks(handlers)`. If no callback is registered, errors will `throw`. 236 | 237 | Having callbacks in `effects` means that error handling can be formalized 238 | without knowledge of the rest of the application leaking into the model. This 239 | also causes `effects` to become fully composable, which smooths parallel 240 | development in large teams, and keeps the mental overhead low when developing a 241 | single model. 242 | 243 | ```js 244 | const http = require('xhr') 245 | const app = barracks({ 246 | onError: (state, data, prev, send) => send('app:error', data) 247 | }) 248 | 249 | app.model({ 250 | namespace: 'app', 251 | effects: { 252 | error: (state, data, send, done) => { 253 | // if doing http calls here be super sure not to get lost 254 | // in a recursive error handling loop: remember this IS 255 | // the error handler 256 | console.error(data.message) 257 | done() 258 | } 259 | } 260 | }) 261 | 262 | app.model({ 263 | namespace: 'foo', 264 | state: { foo: 1 }, 265 | reducers: { 266 | moreFoo: (state, data) => ({ foo: state.foo + data.count }) 267 | } 268 | effects: { 269 | fetch: (state, data, send, done) => { 270 | http('foobar.com', function (err, res, body) { 271 | if (err || res.statusCode !== 200) { 272 | return done(new Error({ 273 | message: 'error accessing server', 274 | error: err 275 | })) 276 | } else { 277 | send('moreFoo', { count: foo.count }, done) 278 | } 279 | }) 280 | } 281 | } 282 | }) 283 | ``` 284 | 285 | ### subscriptions 286 | `subscriptions` are read-only sources of data. This means they cannot be 287 | triggered by actions, but can emit actions themselves whenever they want. This 288 | is useful for stuff like listening to keyboard events or incoming websocket 289 | data. They should generally be started when the application is loaded, using 290 | the `DOMContentLoaded` listener. 291 | 292 | ```js 293 | app.model({ 294 | subscriptions: { 295 | emitWoofs: (send, done) => { 296 | // emit a woof every second 297 | setInterval(() => send('printWoofs', { woof: 'meow?' }, done), 1000) 298 | } 299 | }, 300 | effects: { 301 | printWoofs: (state, data) => console.log(data.woof) 302 | } 303 | }) 304 | ``` 305 | `done()` is passed as the final argument so if an error occurs in a subscriber, 306 | it can be communicated to the `onError` hook. 307 | 308 | ## FAQ 309 | ### What is an "action dispatcher"? 310 | An action dispatcher gets data from one place to another without tightly 311 | coupling code. The best known use case for this is in the `flux` pattern. Say 312 | you want to update a piece of data (for example a user's name), instead of 313 | directly calling the update logic inside the view, the action calls a function 314 | that updates the user's name for you. Now all the views that need to update a 315 | user's name can call the same action and pass in the relevant data. This 316 | pattern tends to make views more robust and easier to maintain. 317 | 318 | ### Why did you build this? 319 | Passing messages around should not be complicated. Many `flux` implementations 320 | casually throw restrictions at users without having a clear architecture. I 321 | don't like that. `barracks` is a package that creates a clear flow of data within an 322 | application, concerning itself with state, code separation, and data flow. I 323 | believe that having strong opinions and being transparent in them makes for 324 | better architectures than sprinkles of opinions left and right, without a cohesive 325 | story as to _why_. 326 | 327 | ### How is this different from choo? 328 | `choo` is a framework that handles views, data and all problems related to 329 | that. This is a package that only concerns itself with data flow, without being 330 | explicitly tied to the DOM. 331 | 332 | ### This looks like more than five functions! 333 | Welllll, no. It's technically five functions with a high arity, hah. Nah, 334 | you're right - but five functions _sounds_ good. Besides: you don't need to 335 | know all options and toggles to get this working; that only relevant once you 336 | start hitting edge cases like we did in `choo` :sparkles: 337 | 338 | ## See Also 339 | - [choo](https://github.com/yoshuawuyts/choo) - sturdy frontend framework 340 | - [sheet-router](https://github.com/yoshuawuyts/sheet-router) - fast, modular 341 | client-side router 342 | - [yo-yo](https://github.com/maxogden/yo-yo) - template string based view 343 | framework 344 | - [send-action](https://github.com/sethvincent/send-action) - unidirectional 345 | action emitter 346 | 347 | ## Installation 348 | ```sh 349 | $ npm install barracks 350 | ``` 351 | 352 | ## License 353 | [MIT](https://tldrlegal.com/license/mit-license) 354 | 355 | [npm-image]: https://img.shields.io/npm/v/barracks.svg?style=flat-square 356 | [npm-url]: https://npmjs.org/package/barracks 357 | [travis-image]: https://img.shields.io/travis/yoshuawuyts/barracks/master.svg?style=flat-square 358 | [travis-url]: https://travis-ci.org/yoshuawuyts/barracks 359 | [coveralls-image]: https://img.shields.io/coveralls/yoshuawuyts/barracks.svg?style=flat-square 360 | [coveralls-url]: https://coveralls.io/r/yoshuawuyts/barracks?branch=master 361 | [downloads-image]: http://img.shields.io/npm/dm/barracks.svg?style=flat-square 362 | [downloads-url]: https://npmjs.org/package/barracks 363 | 364 | [flux]: http://facebook.github.io/react/blog/2014/05/06/flux.html 365 | [browserify]: https://github.com/substack/node-browserify 366 | -------------------------------------------------------------------------------- /apply-hook.js: -------------------------------------------------------------------------------- 1 | module.exports = applyHook 2 | 3 | // apply arguments onto an array of functions, useful for hooks 4 | // (arr, any?, any?, any?, any?, any?) -> null 5 | function applyHook (arr, arg1, arg2, arg3, arg4, arg5) { 6 | arr.forEach(function (fn) { 7 | fn(arg1, arg2, arg3, arg4, arg5) 8 | }) 9 | } 10 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var mutate = require('xtend/mutable') 2 | var nanotick = require('nanotick') 3 | var assert = require('assert') 4 | var xtend = require('xtend') 5 | 6 | var applyHook = require('./apply-hook') 7 | 8 | module.exports = dispatcher 9 | 10 | // initialize a new barracks instance 11 | // obj -> obj 12 | function dispatcher (hooks) { 13 | hooks = hooks || {} 14 | assert.equal(typeof hooks, 'object', 'barracks: hooks should be undefined or an object') 15 | 16 | var onStateChangeHooks = [] 17 | var onActionHooks = [] 18 | var onErrorHooks = [] 19 | 20 | var subscriptionWraps = [] 21 | var initialStateWraps = [] 22 | var reducerWraps = [] 23 | var effectWraps = [] 24 | 25 | use(hooks) 26 | 27 | var reducersCalled = false 28 | var effectsCalled = false 29 | var stateCalled = false 30 | var subsCalled = false 31 | var stopped = false 32 | 33 | var subscriptions = start._subscriptions = {} 34 | var reducers = start._reducers = {} 35 | var effects = start._effects = {} 36 | var models = start._models = [] 37 | var _state = {} 38 | 39 | var tick = nanotick() 40 | 41 | start.model = setModel 42 | start.state = getState 43 | start.start = start 44 | start.stop = stop 45 | start.use = use 46 | 47 | return start 48 | 49 | // push an object of hooks onto an array 50 | // obj -> null 51 | function use (hooks) { 52 | assert.equal(typeof hooks, 'object', 'barracks.use: hooks should be an object') 53 | assert.ok(!hooks.onError || typeof hooks.onError === 'function', 'barracks.use: onError should be undefined or a function') 54 | assert.ok(!hooks.onAction || typeof hooks.onAction === 'function', 'barracks.use: onAction should be undefined or a function') 55 | assert.ok(!hooks.onStateChange || typeof hooks.onStateChange === 'function', 'barracks.use: onStateChange should be undefined or a function') 56 | 57 | if (hooks.onStateChange) onStateChangeHooks.push(hooks.onStateChange) 58 | if (hooks.onError) onErrorHooks.push(wrapOnError(hooks.onError)) 59 | if (hooks.onAction) onActionHooks.push(hooks.onAction) 60 | if (hooks.wrapSubscriptions) subscriptionWraps.push(hooks.wrapSubscriptions) 61 | if (hooks.wrapInitialState) initialStateWraps.push(hooks.wrapInitialState) 62 | if (hooks.wrapReducers) reducerWraps.push(hooks.wrapReducers) 63 | if (hooks.wrapEffects) effectWraps.push(hooks.wrapEffects) 64 | if (hooks.models) hooks.models.forEach(setModel) 65 | } 66 | 67 | // push a model to be initiated 68 | // obj -> null 69 | function setModel (model) { 70 | assert.equal(typeof model, 'object', 'barracks.store.model: model should be an object') 71 | models.push(model) 72 | } 73 | 74 | // get the current state from the store 75 | // obj? -> obj 76 | function getState (opts) { 77 | opts = opts || {} 78 | assert.equal(typeof opts, 'object', 'barracks.store.state: opts should be an object') 79 | 80 | var state = opts.state 81 | if (!opts.state && opts.freeze === false) return xtend(_state) 82 | else if (!opts.state) return Object.freeze(xtend(_state)) 83 | assert.equal(typeof state, 'object', 'barracks.store.state: state should be an object') 84 | 85 | var namespaces = [] 86 | var newState = {} 87 | 88 | // apply all fields from the model, and namespaced fields from the passed 89 | // in state 90 | models.forEach(function (model) { 91 | var ns = model.namespace 92 | namespaces.push(ns) 93 | var modelState = model.state || {} 94 | if (ns) { 95 | newState[ns] = newState[ns] || {} 96 | apply(ns, modelState, newState) 97 | newState[ns] = xtend(newState[ns], state[ns]) 98 | } else { 99 | mutate(newState, modelState) 100 | } 101 | }) 102 | 103 | // now apply all fields that weren't namespaced from the passed in state 104 | Object.keys(state).forEach(function (key) { 105 | if (namespaces.indexOf(key) !== -1) return 106 | newState[key] = state[key] 107 | }) 108 | 109 | var tmpState = xtend(_state, xtend(state, newState)) 110 | var wrappedState = wrapHook(tmpState, initialStateWraps) 111 | 112 | return (opts.freeze === false) 113 | ? wrappedState 114 | : Object.freeze(wrappedState) 115 | } 116 | 117 | // initialize the store hooks, get the send() function 118 | // obj? -> fn 119 | function start (opts) { 120 | opts = opts || {} 121 | assert.equal(typeof opts, 'object', 'barracks.store.start: opts should be undefined or an object') 122 | 123 | // register values from the models 124 | models.forEach(function (model) { 125 | var ns = model.namespace 126 | if (!stateCalled && model.state && opts.state !== false) { 127 | var modelState = model.state || {} 128 | if (ns) { 129 | _state[ns] = _state[ns] || {} 130 | apply(ns, modelState, _state) 131 | } else { 132 | mutate(_state, modelState) 133 | } 134 | } 135 | if (!reducersCalled && model.reducers && opts.reducers !== false) { 136 | apply(ns, model.reducers, reducers, function (cb) { 137 | return wrapHook(cb, reducerWraps) 138 | }) 139 | } 140 | if (!effectsCalled && model.effects && opts.effects !== false) { 141 | apply(ns, model.effects, effects, function (cb) { 142 | return wrapHook(cb, effectWraps) 143 | }) 144 | } 145 | if (!subsCalled && model.subscriptions && opts.subscriptions !== false) { 146 | apply(ns, model.subscriptions, subscriptions, function (cb, key) { 147 | var send = createSend('subscription: ' + (ns ? ns + ':' + key : key)) 148 | cb = wrapHook(cb, subscriptionWraps) 149 | cb(send, function (err) { 150 | applyHook(onErrorHooks, err, _state, createSend) 151 | }) 152 | return cb 153 | }) 154 | } 155 | }) 156 | 157 | // the state wrap is special because we want to operate on the full 158 | // state rather than indvidual chunks, so we apply it outside the loop 159 | if (!stateCalled && opts.state !== false) { 160 | _state = wrapHook(_state, initialStateWraps) 161 | } 162 | 163 | if (!opts.noSubscriptions) subsCalled = true 164 | if (!opts.noReducers) reducersCalled = true 165 | if (!opts.noEffects) effectsCalled = true 166 | if (!opts.noState) stateCalled = true 167 | 168 | if (!onErrorHooks.length) onErrorHooks.push(wrapOnError(defaultOnError)) 169 | 170 | return createSend 171 | 172 | // call an action from a view 173 | // (str, bool?) -> (str, any?, fn?) -> null 174 | function createSend (selfName, callOnError) { 175 | assert.equal(typeof selfName, 'string', 'barracks.store.start.createSend: selfName should be a string') 176 | assert.ok(!callOnError || typeof callOnError === 'boolean', 'barracks.store.start.send: callOnError should be undefined or a boolean') 177 | 178 | return function send (name, data, cb) { 179 | if (!cb && !callOnError) { 180 | cb = data 181 | data = null 182 | } 183 | data = (typeof data === 'undefined' ? null : data) 184 | 185 | assert.equal(typeof name, 'string', 'barracks.store.start.send: name should be a string') 186 | assert.ok(!cb || typeof cb === 'function', 'barracks.store.start.send: cb should be a function') 187 | 188 | var done = callOnError ? onErrorCallback : cb 189 | _send(name, data, selfName, done) 190 | 191 | function onErrorCallback (err) { 192 | err = err || null 193 | if (err) { 194 | applyHook(onErrorHooks, err, _state, function createSend (selfName) { 195 | return function send (name, data) { 196 | assert.equal(typeof name, 'string', 'barracks.store.start.send: name should be a string') 197 | data = (typeof data === 'undefined' ? null : data) 198 | _send(name, data, selfName, done) 199 | } 200 | }) 201 | } 202 | } 203 | } 204 | } 205 | 206 | // call an action 207 | // (str, str, any, fn) -> null 208 | function _send (name, data, caller, cb) { 209 | if (stopped) return 210 | 211 | assert.equal(typeof name, 'string', 'barracks._send: name should be a string') 212 | assert.equal(typeof caller, 'string', 'barracks._send: caller should be a string') 213 | assert.equal(typeof cb, 'function', 'barracks._send: cb should be a function') 214 | 215 | ;(tick(function () { 216 | var reducersCalled = false 217 | var effectsCalled = false 218 | var newState = xtend(_state) 219 | 220 | if (onActionHooks.length) { 221 | applyHook(onActionHooks, _state, data, name, caller, createSend) 222 | } 223 | 224 | // validate if a namespace exists. Namespaces are delimited by ':'. 225 | var actionName = name 226 | if (/:/.test(name)) { 227 | var arr = name.split(':') 228 | var ns = arr.shift() 229 | actionName = arr.join(':') 230 | } 231 | 232 | var _reducers = ns ? reducers[ns] : reducers 233 | if (_reducers && _reducers[actionName]) { 234 | if (ns) { 235 | var reducedState = _reducers[actionName](_state[ns], data) 236 | newState[ns] = xtend(_state[ns], reducedState) 237 | } else { 238 | mutate(newState, reducers[actionName](_state, data)) 239 | } 240 | reducersCalled = true 241 | if (onStateChangeHooks.length) { 242 | applyHook(onStateChangeHooks, newState, data, _state, actionName, createSend) 243 | } 244 | _state = newState 245 | cb(null, newState) 246 | } 247 | 248 | var _effects = ns ? effects[ns] : effects 249 | if (!reducersCalled && _effects && _effects[actionName]) { 250 | var send = createSend('effect: ' + name) 251 | if (ns) _effects[actionName](_state[ns], data, send, cb) 252 | else _effects[actionName](_state, data, send, cb) 253 | effectsCalled = true 254 | } 255 | 256 | if (!reducersCalled && !effectsCalled) { 257 | throw new Error('Could not find action ' + actionName) 258 | } 259 | }))() 260 | } 261 | } 262 | 263 | // stop an app, essentially turns 264 | // all send() calls into no-ops. 265 | // () -> null 266 | function stop () { 267 | stopped = true 268 | } 269 | } 270 | 271 | // compose an object conditionally 272 | // optionally contains a namespace 273 | // which is used to nest properties. 274 | // (str, obj, obj, fn?) -> null 275 | function apply (ns, source, target, wrap) { 276 | if (ns && !target[ns]) target[ns] = {} 277 | Object.keys(source).forEach(function (key) { 278 | var cb = wrap ? wrap(source[key], key) : source[key] 279 | if (ns) target[ns][key] = cb 280 | else target[key] = cb 281 | }) 282 | } 283 | 284 | // handle errors all the way at the top of the trace 285 | // err? -> null 286 | function defaultOnError (err) { 287 | throw err 288 | } 289 | 290 | function wrapOnError (onError) { 291 | return function onErrorWrap (err, state, createSend) { 292 | if (err) onError(err, state, createSend) 293 | } 294 | } 295 | 296 | // take a apply an array of transforms onto a value. The new value 297 | // must be returned synchronously from the transform 298 | // (any, [fn]) -> any 299 | function wrapHook (value, transforms) { 300 | transforms.forEach(function (transform) { 301 | value = transform(value) 302 | }) 303 | return value 304 | } 305 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "barracks", 3 | "version": "9.3.2", 4 | "description": "Action dispatcher for unidirectional data flows", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "standard && NODE_ENV=test node test", 8 | "test:cov": "standard && NODE_ENV=test istanbul cover test.js", 9 | "watch": "watch 'npm t'" 10 | }, 11 | "repository": "yoshuawuyts/barracks", 12 | "keywords": [ 13 | "action", 14 | "dispatcher" 15 | ], 16 | "license": "MIT", 17 | "dependencies": { 18 | "nanotick": "^1.1.2", 19 | "xtend": "^4.0.1" 20 | }, 21 | "devDependencies": { 22 | "bundle-collapser": "^1.2.1", 23 | "coveralls": "~2.11.12", 24 | "es2020": "^1.1.6", 25 | "istanbul": "~0.4.5", 26 | "noop2": "^2.0.0", 27 | "standard": "^8.0.0", 28 | "tape": "^4.6.0", 29 | "uglifyify": "^3.0.2", 30 | "unassertify": "^2.0.3", 31 | "watch": "^1.0.0" 32 | }, 33 | "files": [ 34 | "LICENSE", 35 | "README.md", 36 | "index.js", 37 | "apply-hook.js" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /script/test-size: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | usage () { 4 | cat << USAGE 5 | script/test-size 6 | Commands: 7 | g, gzip (default) Output gzip size 8 | m, minified Output size minified 9 | d, discify Run discify 10 | USAGE 11 | } 12 | 13 | gzip_size () { 14 | browserify index.js \ 15 | -g unassertify \ 16 | -g es2020 \ 17 | -g uglifyify \ 18 | -p bundle-collapser/plugin \ 19 | | uglifyjs \ 20 | | gzip-size \ 21 | | pretty-bytes 22 | } 23 | 24 | min_size () { 25 | browserify index.js \ 26 | -g unassertify \ 27 | -g es2020 \ 28 | -g uglifyify \ 29 | -p bundle-collapser/plugin \ 30 | | uglifyjs \ 31 | | wc -c \ 32 | | pretty-bytes 33 | } 34 | 35 | run_discify () { 36 | browserify index.js --full-paths \ 37 | -g unassertify \ 38 | -g es2020 \ 39 | -g uglifyify \ 40 | | uglifyjs \ 41 | | discify --open 42 | } 43 | 44 | # set CLI flags 45 | getopt -T > /dev/null 46 | if [ "$?" -eq 4 ]; then 47 | args="$(getopt --long help discify minified --options hmdg -- "$*")" 48 | else args="$(getopt h "$*")"; fi 49 | [ ! $? -eq 0 ] && { usage && exit 2; } 50 | eval set -- "$args" 51 | 52 | # parse CLI flags 53 | while true; do 54 | case "$1" in 55 | -h|--help) usage && exit 1 ;; 56 | -- ) shift; break ;; 57 | * ) break ;; 58 | esac 59 | done 60 | 61 | case "$1" in 62 | d|discify) shift; run_discify "$@" ;; 63 | m|minified) shift; printf "min: %s\n" "$(min_size)" "$@" ;; 64 | g|gzip|*) shift; printf "gzip: %s\n" "$(gzip_size)" "$@" ;; 65 | esac 66 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const barracks = require('./') 2 | const xtend = require('xtend') 3 | const noop = require('noop2') 4 | const tape = require('tape') 5 | 6 | tape('api: store = barracks(handlers)', (t) => { 7 | t.test('should validate input types', (t) => { 8 | t.plan(3) 9 | t.doesNotThrow(barracks, 'no args does not throw') 10 | t.doesNotThrow(barracks.bind(null, {}), 'object does not throw') 11 | t.throws(barracks.bind(null, 123), 'non-object throws') 12 | }) 13 | 14 | t.test('should validate hook types', (t) => { 15 | t.plan(3) 16 | t.throws(barracks.bind(null, { onError: 123 }), /function/, 'onError throws') 17 | t.throws(barracks.bind(null, { onAction: 123 }), /function/, 'onAction throws') 18 | t.throws(barracks.bind(null, { onStateChange: 123 }), /function/, 'onStateChange throws') 19 | }) 20 | }) 21 | 22 | tape('api: store.model()', (t) => { 23 | t.test('should validate input types', (t) => { 24 | t.plan(2) 25 | const store = barracks() 26 | t.throws(store.model.bind(null, 123), /object/, 'non-obj should throw') 27 | t.doesNotThrow(store.model.bind(null, {}), 'obj should not throw') 28 | }) 29 | }) 30 | 31 | tape('api: store.use()', (t) => { 32 | t.test('should allow model injection', (t) => { 33 | t.plan(1) 34 | const store = barracks() 35 | store.model({ 36 | state: { accessed: false } 37 | }) 38 | store.use({ 39 | models: [{ 40 | namespace: 'namespaced', 41 | state: { 'accessed': false }, 42 | reducers: { update: (state, data) => data } 43 | }, { 44 | state: {}, 45 | reducers: { update: (state, data) => data } 46 | }] 47 | }) 48 | const createSend = store.start() 49 | const send = createSend('test', true) 50 | send('namespaced:update', { accessed: true }) 51 | send('update', { accessed: true }) 52 | 53 | setTimeout(function () { 54 | const expected = { accessed: true, namespaced: { accessed: true } } 55 | t.deepEqual(store.state(), expected, 'models can be injected') 56 | }, 100) 57 | }) 58 | 59 | t.test('should call multiples', (t) => { 60 | t.plan(1) 61 | const store = barracks() 62 | const called = { first: false, second: false } 63 | 64 | store.use({ 65 | onAction: (state, data, name, caller, createSend) => { 66 | called.first = true 67 | } 68 | }) 69 | 70 | store.use({ 71 | onAction: (state, data, name, caller, createSend) => { 72 | called.second = true 73 | } 74 | }) 75 | 76 | store.model({ 77 | state: { 78 | count: 0 79 | }, 80 | reducers: { 81 | foo: (state, data) => ({ count: state.count + 1 }) 82 | } 83 | }) 84 | 85 | const createSend = store.start() 86 | const send = createSend('test', true) 87 | send('foo', { count: 3 }) 88 | 89 | setTimeout(function () { 90 | const expected = { first: true, second: true } 91 | t.deepEqual(called, expected, 'all hooks were called') 92 | }, 100) 93 | }) 94 | }) 95 | 96 | tape('api: createSend = store.start(opts)', (t) => { 97 | t.test('should validate input types', (t) => { 98 | t.plan(3) 99 | const store = barracks() 100 | t.throws(store.start.bind(null, 123), /object/, 'non-obj should throw') 101 | t.doesNotThrow(store.start.bind(null, {}), /object/, 'obj should not throw') 102 | t.doesNotThrow(store.start.bind(null), 'undefined should not throw') 103 | }) 104 | 105 | t.test('opts.state = false should not register state', (t) => { 106 | t.plan(1) 107 | const store = barracks() 108 | store.model({ state: { foo: 'bar' } }) 109 | store.start({ state: false }) 110 | const state = store.state() 111 | t.deepEqual(state, {}, 'no state returned') 112 | }) 113 | 114 | t.test('opts.effects = false should not register effects', (t) => { 115 | t.plan(1) 116 | const store = barracks() 117 | store.model({ effects: { foo: noop } }) 118 | store.start({ effects: false }) 119 | const effects = Object.keys(store._effects) 120 | t.deepEqual(effects.length, 0, 'no effects registered') 121 | }) 122 | 123 | t.test('opts.reducers = false should not register reducers', (t) => { 124 | t.plan(1) 125 | const store = barracks() 126 | store.model({ reducers: { foo: noop } }) 127 | store.start({ reducers: false }) 128 | const reducers = Object.keys(store._reducers) 129 | t.deepEqual(reducers.length, 0, 'no reducers registered') 130 | }) 131 | 132 | t.test('opts.subscriptions = false should not register subs', (t) => { 133 | t.plan(1) 134 | const store = barracks() 135 | store.model({ subscriptions: { foo: noop } }) 136 | store.start({ subscriptions: false }) 137 | const subscriptions = Object.keys(store._subscriptions) 138 | t.deepEqual(subscriptions.length, 0, 'no subscriptions registered') 139 | }) 140 | }) 141 | 142 | tape('api: state = store.state()', (t) => { 143 | t.test('should return initial state', (t) => { 144 | t.plan(1) 145 | const store = barracks() 146 | store.model({ state: { foo: 'bar' } }) 147 | store.start() 148 | const state = store.state() 149 | t.deepEqual(state, { foo: 'bar' }) 150 | }) 151 | 152 | t.test('should initialize state with empty namespace object', (t) => { 153 | t.plan(1) 154 | const store = barracks() 155 | store.model({ 156 | namespace: 'beep', 157 | state: {} 158 | }) 159 | store.start() 160 | const state = store.state() 161 | const expected = { 162 | beep: {} 163 | } 164 | t.deepEqual(expected, state, 'has initial empty namespace object') 165 | }) 166 | 167 | t.test('should return the combined state', (t) => { 168 | t.plan(1) 169 | const store = barracks() 170 | store.model({ 171 | namespace: 'beep', 172 | state: { foo: 'bar', bin: 'baz' } 173 | }) 174 | store.model({ 175 | namespace: 'boop', 176 | state: { foo: 'bar', bin: 'baz' } 177 | }) 178 | store.model({ 179 | state: { hello: 'dog', welcome: 'world' } 180 | }) 181 | store.start() 182 | const state = store.state() 183 | const expected = { 184 | beep: { foo: 'bar', bin: 'baz' }, 185 | boop: { foo: 'bar', bin: 'baz' }, 186 | hello: 'dog', 187 | welcome: 'world' 188 | } 189 | t.deepEqual(expected, state, 'initial state of models is combined') 190 | }) 191 | 192 | t.test('object should be frozen by default', (t) => { 193 | t.plan(1) 194 | const store = barracks() 195 | store.model({ state: { foo: 'bar' } }) 196 | store.start() 197 | const state = store.state() 198 | state.baz = 'bin' 199 | const expected = { foo: 'bar' } 200 | t.deepEqual(state, expected, 'state was frozen') 201 | }) 202 | 203 | t.test('freeze = false should not freeze objects', (t) => { 204 | t.plan(1) 205 | const store = barracks() 206 | store.model({ state: { foo: 'bar' } }) 207 | store.start() 208 | const state = store.state({ freeze: false }) 209 | state.baz = 'bin' 210 | const expected = { foo: 'bar', baz: 'bin' } 211 | t.deepEqual(state, expected, 'state was not frozen') 212 | }) 213 | 214 | t.test('passing a state opts should merge state', (t) => { 215 | t.plan(1) 216 | const store = barracks() 217 | store.model({ state: { foo: 'bar' } }) 218 | store.model({ 219 | namespace: 'beep', 220 | state: { foo: 'bar', bin: 'baz' } 221 | }) 222 | store.start() 223 | 224 | const extendedState = { 225 | woof: 'dog', 226 | beep: { foo: 'baz' }, 227 | barp: { bli: 'bla' } 228 | } 229 | const state = store.state({ state: extendedState }) 230 | const expected = { 231 | foo: 'bar', 232 | woof: 'dog', 233 | beep: { foo: 'baz', bin: 'baz' }, 234 | barp: { bli: 'bla' } 235 | } 236 | t.deepEqual(state, expected, 'state was merged') 237 | }) 238 | }) 239 | 240 | tape('api: send(name, data?)', (t) => { 241 | t.test('should validate input types', (t) => { 242 | t.plan(1) 243 | const store = barracks() 244 | const createSend = store.start() 245 | const send = createSend('test') 246 | t.throws(send.bind(null, 123), /string/, 'non-string should throw') 247 | }) 248 | }) 249 | 250 | tape('api: stop()', (t) => { 251 | t.test('should stop executing send() calls', (t) => { 252 | t.plan(1) 253 | const store = barracks() 254 | var count = 0 255 | store.model({ reducers: { foo: (state, action) => { count += 1 } } }) 256 | const createSend = store.start() 257 | const send = createSend('test', true) 258 | send('foo') 259 | store.stop() 260 | send('foo') 261 | setTimeout(() => t.equal(count, 1, 'no actions after stop()'), 10) 262 | }) 263 | }) 264 | 265 | tape('handlers: reducers', (t) => { 266 | t.test('should be able to be called', (t) => { 267 | t.plan(6) 268 | const store = barracks() 269 | store.model({ 270 | namespace: 'meow', 271 | state: { beep: 'boop' }, 272 | reducers: { 273 | woof: (state, data) => t.pass('meow.woof called') 274 | } 275 | }) 276 | 277 | store.model({ 278 | state: { 279 | foo: 'bar', 280 | beep: 'boop' 281 | }, 282 | reducers: { 283 | foo: (state, data) => { 284 | t.deepEqual(data, { foo: 'baz' }, 'action is equal') 285 | t.equal(state.foo, 'bar', 'state.foo = bar') 286 | return { foo: 'baz' } 287 | }, 288 | sup: (state, data) => { 289 | t.equal(data, 'nope', 'action is equal') 290 | t.equal(state.beep, 'boop', 'state.beep = boop') 291 | return { beep: 'nope' } 292 | } 293 | } 294 | }) 295 | const createSend = store.start() 296 | const send = createSend('tester', true) 297 | send('foo', { foo: 'baz' }) 298 | send('sup', 'nope') 299 | send('meow:woof') 300 | setTimeout(function () { 301 | const state = store.state() 302 | const expected = { 303 | foo: 'baz', 304 | beep: 'nope', 305 | meow: { beep: 'boop' } 306 | } 307 | t.deepEqual(state, expected, 'state was updated') 308 | }, 10) 309 | }) 310 | }) 311 | 312 | tape('handlers: effects', (t) => { 313 | t.test('should be able to be called', (t) => { 314 | t.plan(5) 315 | const store = barracks() 316 | 317 | store.model({ 318 | namespace: 'meow', 319 | effects: { 320 | woof: (state, data, send, done) => { 321 | t.pass('woof called') 322 | } 323 | } 324 | }) 325 | 326 | store.model({ 327 | state: { bin: 'baz', beep: 'boop' }, 328 | reducers: { 329 | bar: (state, data) => { 330 | t.pass('reducer was called') 331 | return { beep: data.beep } 332 | } 333 | }, 334 | effects: { 335 | foo: (state, data, send, done) => { 336 | t.pass('effect was called') 337 | send('bar', { beep: data.beep }, () => { 338 | t.pass('effect callback was called') 339 | done() 340 | }) 341 | } 342 | } 343 | }) 344 | const createSend = store.start() 345 | const send = createSend('tester', true) 346 | send('foo', { beep: 'woof' }) 347 | send('meow:woof') 348 | 349 | setTimeout(function () { 350 | const state = store.state() 351 | const expected = { bin: 'baz', beep: 'woof' } 352 | t.deepEqual(state, expected, 'state was updated') 353 | }, 10) 354 | }) 355 | 356 | t.test('should be able to nest effects and return data', (t) => { 357 | t.plan(12) 358 | const store = barracks() 359 | store.model({ 360 | effects: { 361 | foo: (state, data, send, done) => { 362 | t.pass('foo was called') 363 | send('bar', { beep: 'boop' }, () => { 364 | t.pass('foo:bar effect callback was called') 365 | send('baz', (err, res) => { 366 | t.ifError(err, 'no error') 367 | t.equal(res, 'yay', 'res is equal') 368 | t.pass('foo:baz effect callback was called') 369 | done() 370 | }) 371 | }) 372 | }, 373 | bar: (state, data, send, done) => { 374 | t.pass('bar was called') 375 | t.deepEqual(data, { beep: 'boop' }, 'action is equal') 376 | send('baz', (err, res) => { 377 | t.ifError(err, 'no error') 378 | t.equal(res, 'yay', 'res is equal') 379 | t.pass('bar:baz effect callback was called') 380 | done() 381 | }) 382 | }, 383 | baz: (state, data, send, done) => { 384 | t.pass('baz effect was called') 385 | done(null, 'yay') 386 | } 387 | } 388 | }) 389 | const createSend = store.start() 390 | const send = createSend('tester', true) 391 | send('foo') 392 | }) 393 | 394 | t.test('should be able to propagate nested errors', (t) => { 395 | t.plan(7) 396 | const store = barracks() 397 | store.model({ 398 | effects: { 399 | foo: (state, data, send, done) => { 400 | t.pass('foo was called') 401 | send('bar', (err, res) => { 402 | t.ok(err, 'error detected') 403 | t.pass('foo:bar effect callback was called') 404 | done() 405 | }) 406 | }, 407 | bar: (state, data, send, done) => { 408 | t.pass('bar was called') 409 | send('baz', (err, res) => { 410 | t.ok(err, 'error detected') 411 | t.pass('bar:baz effect callback was called') 412 | done(err) 413 | }) 414 | }, 415 | baz: (state, data, send, done) => { 416 | t.pass('baz effect was called') 417 | done(new Error('oh noooo')) 418 | } 419 | } 420 | }) 421 | const createSend = store.start() 422 | const send = createSend('tester', true) 423 | send('foo') 424 | }) 425 | }) 426 | 427 | tape('handlers: subscriptions', (t) => { 428 | t.test('should be able to call', (t) => { 429 | t.plan(9) 430 | const store = barracks() 431 | store.model({ 432 | namespace: 'foo', 433 | subscriptions: { 434 | mySub: (send, done) => { 435 | t.pass('namespaced sub initiated') 436 | } 437 | } 438 | }) 439 | 440 | store.model({ 441 | reducers: { 442 | bar: () => t.pass('reducer called') 443 | }, 444 | effects: { 445 | foo: (state, data, send, done) => { 446 | t.pass('foo was called') 447 | done(new Error('oh no!'), 'hello') 448 | } 449 | }, 450 | subscriptions: { 451 | mySub: (send, done) => { 452 | t.pass('mySub was initiated') 453 | send('foo', (err, res) => { 454 | t.ok(err, 'error detected') 455 | t.equal(res, 'hello', 'res was passed') 456 | t.pass('mySub:foo effect callback was called') 457 | send('bar', (err, res) => { 458 | t.error(err, 'no error detected') 459 | t.pass('mySub:bar effect callback was called') 460 | }) 461 | }) 462 | } 463 | } 464 | }) 465 | store.start() 466 | }) 467 | 468 | t.test('should be able to emit an error', (t) => { 469 | t.plan(4) 470 | const store = barracks({ 471 | onError: (err, state, createSend) => { 472 | t.equal(err.message, 'oh no!', 'err was received') 473 | t.equal((state || {}).a, 1, 'state was passed') 474 | t.equal(typeof createSend, 'function', 'createSend is a function') 475 | } 476 | }) 477 | 478 | store.model({ 479 | state: { a: 1 }, 480 | subscriptions: { 481 | mySub: (send, done) => { 482 | t.pass('sub initiated') 483 | done(new Error('oh no!')) 484 | } 485 | } 486 | }) 487 | store.start() 488 | }) 489 | }) 490 | 491 | tape('hooks: onStateChange', (t) => { 492 | t.test('should be called whenever state changes', (t) => { 493 | t.plan(4) 494 | const store = barracks({ 495 | onStateChange: (state, data, prev, caller, createSend) => { 496 | t.deepEqual(data, { count: 3 }, 'action is equal') 497 | t.deepEqual(state, { count: 4 }, 'state is equal') 498 | t.deepEqual(prev, { count: 1 }, 'prev is equal') 499 | t.equal(caller, 'increment', 'caller is equal') 500 | } 501 | }) 502 | 503 | store.model({ 504 | state: { count: 1 }, 505 | reducers: { 506 | increment: (state, data) => ({ count: state.count + data.count }) 507 | } 508 | }) 509 | 510 | const createSend = store.start() 511 | const send = createSend('test', true) 512 | send('increment', { count: 3 }) 513 | }) 514 | 515 | t.test('should allow triggering other actions', (t) => { 516 | t.plan(2) 517 | const store = barracks({ 518 | onStateChange: function (state, data, prev, caller, createSend) { 519 | t.pass('onStateChange called') 520 | const send = createSend('test:onStateChange', true) 521 | send('foo') 522 | } 523 | }) 524 | 525 | store.model({ 526 | state: { count: 1 }, 527 | effects: { 528 | foo: (state, data, send, done) => { 529 | t.pass('called') 530 | done() 531 | } 532 | }, 533 | reducers: { 534 | increment: (state, data) => ({ count: state.count + data.count }) 535 | } 536 | }) 537 | 538 | const createSend = store.start() 539 | const send = createSend('test', true) 540 | send('increment', { count: 3 }) 541 | }) 542 | 543 | t.test('previous state should not be mutated', (t) => { 544 | t.plan(2) 545 | const storeNS = barracks({ 546 | onStateChange: (state, data, prev, caller, createSend) => { 547 | t.equal(state.ns.items.length, 3, 'state was updated') 548 | t.equal(prev.ns.items.length, 0, 'prev was left as-is') 549 | } 550 | }) 551 | 552 | storeNS.model({ 553 | namespace: 'ns', 554 | state: { items: [] }, 555 | reducers: { 556 | add: (_, state) => ({ items: [1, 2, 3] }) 557 | } 558 | }) 559 | 560 | const createSendNS = storeNS.start() 561 | const sendNS = createSendNS('testNS', true) 562 | sendNS('ns:add') 563 | }) 564 | }) 565 | 566 | tape('hooks: onAction', (t) => { 567 | t.test('should be called whenever an action is emitted', (t) => { 568 | t.plan(5) 569 | const store = barracks({ 570 | onAction: (state, data, actionName, caller, createSend) => { 571 | t.deepEqual(data, { count: 3 }, 'action is equal') 572 | t.deepEqual(state, { count: 1 }, 'state is equal') 573 | t.deepEqual(actionName, 'foo', 'actionName is equal') 574 | t.equal(caller, 'test', 'caller is equal') 575 | } 576 | }) 577 | 578 | store.model({ 579 | state: { count: 1 }, 580 | effects: { 581 | foo: (state, data, send, done) => { 582 | t.pass('effect called') 583 | done() 584 | } 585 | } 586 | }) 587 | 588 | const createSend = store.start() 589 | const send = createSend('test', true) 590 | send('foo', { count: 3 }) 591 | }) 592 | }) 593 | 594 | tape('hooks: onError', (t) => { 595 | t.test('should have a default err handler') 596 | t.test('should not call itself') 597 | }) 598 | 599 | tape('wrappers: wrapSubscriptions') 600 | tape('wrappers: wrapReducers') 601 | tape('wrappers: wrapEffects') 602 | 603 | tape('wrappers: wrapInitialState', (t) => { 604 | t.test('should wrap initial state in start', (t) => { 605 | t.plan(2) 606 | const store = barracks() 607 | store.use({ 608 | wrapInitialState: (state) => { 609 | t.deepEqual(state, { foo: 'bar' }, 'initial state is correct') 610 | return xtend(state, { beep: 'boop' }) 611 | } 612 | }) 613 | 614 | store.model({ 615 | state: { foo: 'bar' } 616 | }) 617 | 618 | store.start() 619 | process.nextTick(() => { 620 | const state = store.state() 621 | t.deepEqual(state, { foo: 'bar', beep: 'boop' }, 'wrapped state correct') 622 | }) 623 | }) 624 | 625 | t.test('should wrap initial state in getState', (t) => { 626 | t.plan(1) 627 | const store = barracks() 628 | store.use({ 629 | wrapInitialState: (state) => { 630 | return xtend(state, { beep: 'boop' }) 631 | } 632 | }) 633 | 634 | store.model({ 635 | state: { foo: 'bar' } 636 | }) 637 | 638 | process.nextTick(() => { 639 | const opts = { 640 | state: { bin: 'baz' } 641 | } 642 | const expected = { 643 | foo: 'bar', 644 | beep: 'boop', 645 | bin: 'baz' 646 | } 647 | const state = store.state(opts) 648 | t.deepEqual(state, expected, 'wrapped state correct') 649 | }) 650 | }) 651 | }) 652 | --------------------------------------------------------------------------------