├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── index.js ├── package.json └── test └── tests.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2017 rt2zz and contributors 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Redux Action Buffer 2 | A middleware for [redux](https://github.com/reactjs/redux) that buffers all actions into a queue until a breaker condition is met, at which point the queue is released (i.e. actions are triggered). 3 | 4 | One potential use case for this is to buffer any actions that occur before you are finished initializing your app. For example in conjunction with [redux-persist](https://github.com/rt2zz/redux-persist). 5 | 6 | ### Usage 7 | ```js 8 | import createActionBuffer from 'redux-action-buffer' 9 | let breaker = BREAKER_ACTION_TYPE 10 | let actionBuffer = createActionBuffer( 11 | BREAKER_ACTION_TYPE, 12 | (err, data) => { 13 | // callback fired immediately after releasing the buffer 14 | // data: { results: [actionReturnValue], queue: [rawAction] } 15 | } 16 | ) 17 | ``` 18 | 19 | #### Redux Persist Example 20 | 21 | **In short:** 22 | 23 | `createActionBuffer({ breaker: REHYDRATE, passthrough: [PERSIST] })` 24 | 25 | **Full example:** 26 | ```js 27 | import { REHYDRATE } from 'redux-persist' 28 | import createActionBuffer from 'redux-action-buffer' 29 | import { createStore, compose } from 'redux' 30 | 31 | let enhancer = compose( 32 | autoRehydrate(), 33 | applyMiddleware( 34 | createActionBuffer(REHYDRATE) //make sure to apply this after redux-thunk et al. 35 | ) 36 | ) 37 | 38 | createStore( 39 | reducer, 40 | {}, 41 | enhancer 42 | ) 43 | ``` 44 | 45 | ### Notes 46 | Delaying actions can be tricky because many actions depend on having a return value, and buffering breaks that. To help catch this scenario the return value from all buffered actions is a string indicating the action has been buffered. 47 | 48 | ### API 49 | actionBuffer(options, callback) 50 | - **options** (string | function | object): Either a action type string that will break the buffer or a function that takes an action as the argument and returns true when the buffer should be broken. An object is interpreted as **{ breaker: (string | function), passthrough: (Array) }** where **breaker** functions as before and **passthrough** is an array of actions not to buffer. 51 | - **callback** (function): A function that is invoked after the buffer is broken. 52 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var BUFFERED_ACTION_RETURN = 'redux-action-buffer: buffered action' 2 | 3 | var setImmediate = typeof global !== 'undefined' && 4 | typeof global.setImmediate !== 'undefined' 5 | ? global.setImmediate 6 | : setTimeout 7 | 8 | module.exports = function bufferActions(options, cb) { 9 | var active = true 10 | var queue = [] 11 | var passthrough = (options && options.passthrough) || [] 12 | var breaker = typeof options === 'object' ? options.breaker : options 13 | 14 | var breakerType = typeof breaker 15 | 16 | if (breakerType === 'string' || breakerType === 'symbol') { 17 | var actionType = breaker 18 | breaker = function(action) { 19 | if (action.type === actionType) return true 20 | else return false 21 | } 22 | } 23 | 24 | return function(store) { 25 | return function(next) { 26 | return function(action) { 27 | // console.log("next", next, action); 28 | if (!active || passthrough.includes(action.type)) return next(action) 29 | if (breaker(action)) { 30 | active = false 31 | var result = next(action) 32 | setImmediate(function() { 33 | var queueResults = [] 34 | queue.forEach(function(queuedAction) { 35 | var queuedActionResult = next(queuedAction) 36 | queueResults.push(queuedActionResult) 37 | }) 38 | cb && 39 | cb(null, { 40 | results: queueResults, 41 | queue: queue 42 | }) 43 | }) 44 | return result 45 | } else { 46 | queue.push(action) 47 | // @TODO consider returning a dummy action, or maybe null for cleanliness 48 | return BUFFERED_ACTION_RETURN 49 | } 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-action-buffer", 3 | "version": "1.2.0", 4 | "description": "Buffer redux actions until a breaker condition is met.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "ava" 8 | }, 9 | "author": "rt2zz ", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "ava": "^0.13.0", 13 | "redux": "^3.3.1" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/tests.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | 3 | import { createStore, applyMiddleware } from 'redux' 4 | import actionBuffer from '../index' 5 | 6 | const BREAKER = 'BREAKER' 7 | 8 | // @TODO split into multiple atomic tests 9 | test('buffers actions', t => { 10 | var actionHistory = [] 11 | 12 | function breakCallback(err, { results, queue }) { 13 | // result is an object with results and queue arrays 14 | t.is(results.length, 2) 15 | t.is(queue.length, 2) 16 | } 17 | 18 | let store = createStore( 19 | (state, action) => { 20 | if (action.type.indexOf('@@') !== 0) actionHistory.push(action) 21 | return {} 22 | }, 23 | null, 24 | applyMiddleware(actionBuffer(BREAKER, breakCallback)) 25 | ) 26 | 27 | let action1 = { type: 'ACTION1' } 28 | let action2 = { type: 'ACTION2' } 29 | let action3 = { type: 'ACTION3' } 30 | let breaker = { type: 'BREAKER' } 31 | 32 | let r1 = store.dispatch(action1) 33 | let r2 = store.dispatch(action2) 34 | let rB = store.dispatch(breaker) 35 | let r3 = store.dispatch(action3) 36 | 37 | // buffered actions return strings, other actions return themselves 38 | t.ok(typeof r1 === 'string') 39 | t.ok(typeof r2 === 'string') 40 | t.same(rB, breaker) 41 | t.same(r3, action3) 42 | 43 | setTimeout(function() { 44 | // history is re-ordered as expected 45 | t.is(actionHistory.indexOf(breaker), 0) 46 | t.is(actionHistory.indexOf(action1), 1) 47 | t.is(actionHistory.indexOf(action2), 2) 48 | t.is(actionHistory.indexOf(action3), 3) 49 | t.pass() 50 | }, 1000) 51 | }) 52 | 53 | test('buffers actions except passthrough', t => { 54 | var actionHistory = [] 55 | 56 | function breakCallback(err, { results, queue }) { 57 | // result is an object with results and queue arrays 58 | t.is(results.length, 3) 59 | t.is(queue.length, 2) 60 | } 61 | 62 | let store = createStore( 63 | (state, action) => { 64 | if (action.type.indexOf('@@') !== 0) actionHistory.push(action) 65 | return {} 66 | }, 67 | null, 68 | applyMiddleware( 69 | actionBuffer( 70 | { breaker: BREAKER, passthrough: ['PASS_THROUGH'] }, 71 | breakCallback 72 | ) 73 | ) 74 | ) 75 | 76 | let actionp = { type: 'PASS_THROUGH' } 77 | let action1 = { type: 'ACTION1' } 78 | let action2 = { type: 'ACTION2' } 79 | let action3 = { type: 'ACTION3' } 80 | let breaker = { type: 'BREAKER' } 81 | 82 | let rp = store.dispatch(actionp) 83 | let r1 = store.dispatch(action1) 84 | let r2 = store.dispatch(action2) 85 | let rB = store.dispatch(breaker) 86 | let r3 = store.dispatch(action3) 87 | 88 | // buffered actions return strings, other actions return themselves 89 | t.same(rp, actionp) 90 | t.ok(typeof r1 === 'string') 91 | t.ok(typeof r2 === 'string') 92 | t.same(rB, breaker) 93 | t.same(r3, action3) 94 | 95 | setTimeout(function() { 96 | // history is re-ordered as expected 97 | t.is(actionHistory.indexOf(actionp), 0) 98 | t.is(actionHistory.indexOf(breaker), 1) 99 | t.is(actionHistory.indexOf(action1), 2) 100 | t.is(actionHistory.indexOf(action2), 3) 101 | t.is(actionHistory.indexOf(action3), 4) 102 | t.pass() 103 | }, 1000) 104 | }) 105 | --------------------------------------------------------------------------------