├── .babelrc ├── .eslintrc.js ├── .gitignore ├── LICENSE ├── README.md ├── index.js ├── package.json ├── src └── index.js └── test └── index.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"], 3 | "plugins": [ 4 | "transform-async-to-generator" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: 'babel-eslint', 3 | extends: 'airbnb-base', 4 | }; 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Jacob Parker 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **I'd strongly advise against using this. Redux is not good with handling asynchronous actions, and this will not change that. There's almost always better options.** 2 | 3 | # redux-enqueue 4 | 5 | Simple queue system for redux. Use with redux-thunk. 6 | 7 | ``` 8 | npm install --save redux-enqueue 9 | ``` 10 | 11 | # Create Store 12 | 13 | Create a redux store using the enqueue middleware. 14 | 15 | ```js 16 | // createStore.js 17 | import { createStore, applyMiddleware } from 'redux'; 18 | import thunkMiddleware from 'redux-thunk'; 19 | import enqueueMiddleware from 'redux-enqueue'; 20 | 21 | const middlewares = applyMiddleware( 22 | thunkMiddleware, 23 | enqueueMiddleware 24 | ); 25 | 26 | return createStore(reducers, initialState, middlewares); 27 | ``` 28 | 29 | # Integrate into Actions 30 | 31 | Call `await dispatch(enqueue(queueId))`, which will return a function that you must call when the action is finished. This will ensure that actions sharing a `queueId`s will not be run concurrently, but actions with other `queueId`s may be. 32 | 33 | Note that we're using try/finally. This is recommended: the code in the finally block is **guaranteed** to be run. 34 | 35 | ```js 36 | // authentication.js 37 | import { enqueue } from 'redux-enqueue'; 38 | 39 | export const login = (username, password) => async dispatch => { 40 | const completionHandler = await dispatch(enqueue('authentication:login')); // arbitrary id 41 | 42 | try { 43 | api.login(username, password); 44 | } finally { 45 | completionHandler(); 46 | } 47 | }; 48 | 49 | export const logout = () => async dispatch => { 50 | const completionHandler = await dispatch(enqueue('authentication:login')); // don't log out whilst logging in 51 | try { 52 | api.login(); 53 | } finally { 54 | completionHandler(); 55 | } 56 | }; 57 | ``` 58 | 59 | # Cookbook 60 | 61 | ## Fetching data 62 | 63 | The below example will load messages for an id unless they've already been loaded. Fetching messages for different ids can happen concurrently. Fetching for the same id will not be able to happen in parallel, and no subsequent calls for this id will be executed after the first successful run. 64 | 65 | ```js 66 | // messages.js 67 | import { enqueue } from 'redux-enqueue'; 68 | 69 | export const loadMessages = id => async (dispatch, getState) => { 70 | const completionHandler = await dispatch(enqueue(`messages:loadMessages:${id}`)); 71 | 72 | try { 73 | const { currentMessages } = getState().messages; 74 | if (currentMessages[id]) return; 75 | const messages = await api.loadMessages(id); 76 | dispatch({ type: SET_MESSAGES, id, messages }); 77 | } finally { 78 | completionHandler(); 79 | } 80 | }; 81 | ``` 82 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; }; 8 | 9 | var ENQUEUE = '@@middleware/queue/ENQUEUE'; 10 | 11 | exports.default = function () { 12 | var promisesPerId = {}; 13 | 14 | return function (next) { 15 | return function (action) { 16 | if (action && action.type === ENQUEUE) { 17 | var _ret = function () { 18 | var id = action.id; 19 | 20 | var existingPromise = promisesPerId[id] || Promise.resolve(); 21 | 22 | var completionHandler = void 0; 23 | var continuationPromise = new Promise(function (res) { 24 | completionHandler = res; 25 | }); 26 | var queuePromise = existingPromise.then(function () { 27 | return completionHandler; 28 | }); 29 | promisesPerId[id] = queuePromise.then(function () { 30 | return continuationPromise; 31 | }); 32 | 33 | return { 34 | v: queuePromise 35 | }; 36 | }(); 37 | 38 | if ((typeof _ret === 'undefined' ? 'undefined' : _typeof(_ret)) === "object") return _ret.v; 39 | } 40 | 41 | return next(action); 42 | }; 43 | }; 44 | }; 45 | 46 | var enqueue = exports.enqueue = function enqueue(id) { 47 | return { 48 | type: ENQUEUE, 49 | id: id 50 | }; 51 | }; 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-enqueue", 3 | "version": "1.0.0", 4 | "description": "Simple queue system for redux", 5 | "main": "index.js", 6 | "dependencies": {}, 7 | "devDependencies": { 8 | "ava": "^0.15.2", 9 | "babel": "^6.5.2", 10 | "babel-cli": "^6.10.1", 11 | "babel-eslint": "^6.0.4", 12 | "babel-plugin-transform-async-to-generator": "^6.8.0", 13 | "babel-preset-es2015": "^6.9.0", 14 | "eslint": "^2.13.0", 15 | "eslint-config-airbnb-base": "^3.0.1", 16 | "eslint-plugin-import": "^1.8.1", 17 | "redux": "^3.5.2", 18 | "redux-thunk": "^2.1.0" 19 | }, 20 | "scripts": { 21 | "test": "ava", 22 | "build": "babel ./src/index.js -o index.js" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/jacobp100/redux-enqueue.git" 27 | }, 28 | "keywords": [], 29 | "author": "", 30 | "license": "ISC", 31 | "bugs": { 32 | "url": "https://github.com/jacobp100/redux-enqueue/issues" 33 | }, 34 | "homepage": "https://github.com/jacobp100/redux-enqueue#readme", 35 | "ava": { 36 | "files": [ 37 | "test/**/*.js" 38 | ], 39 | "require": [ 40 | "babel-register" 41 | ], 42 | "babel": "inherit" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const ENQUEUE = '@@middleware/queue/ENQUEUE'; 2 | 3 | 4 | /* 5 | If you look at the api docs, you queue an item by calling 6 | 7 | const completionHandler = await dispatch(enqueue('id')); 8 | 9 | So what this means is that the return value of dispatch(enqueue('id')) is a promise that resolves 10 | to give the function completionHandler. 11 | 12 | Firstly, we store a promise per id, and have a default value of Promise.resolve(). 13 | 14 | When we dispatch an item to enqueue, we make a new promise, continuationPromise, that will resolve 15 | when completionHandler is called. 16 | 17 | We get then the previous promise for the id, and append a new promise to create queuePromise. 18 | queuePromise immediately resolves with the completionHandler function. This is the promise that is 19 | returned from dispatch. 20 | 21 | We then extend queuePromise by appending the continuationPromise, so that we have a promise that 22 | will be resolved once completionHandler is called (assuming we didn't attempt to enqueue anything 23 | in-between). We use this new promise as the promise for the id, and all subsequent calls to enqueue 24 | will use this promise as the starting point. This means that every time you call enqueue, you're 25 | just awaiting and appending to a long list of promises. Every time you call it, you'll create two 26 | promises. 27 | */ 28 | export default () => { 29 | const promisesPerId = {}; 30 | 31 | return next => action => { 32 | if (action && action.type === ENQUEUE) { 33 | const { id } = action; 34 | const existingPromise = promisesPerId[id] || Promise.resolve(); 35 | 36 | let completionHandler; 37 | const continuationPromise = new Promise(res => { 38 | completionHandler = res; 39 | }); 40 | const queuePromise = existingPromise.then(() => completionHandler); 41 | promisesPerId[id] = queuePromise.then(() => continuationPromise); 42 | 43 | return queuePromise; 44 | } 45 | 46 | return next(action); 47 | }; 48 | }; 49 | 50 | export const enqueue = id => ({ 51 | type: ENQUEUE, 52 | id, 53 | }); 54 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { createStore, applyMiddleware } from 'redux'; 3 | import thunk from 'redux-thunk'; 4 | import queue, { enqueue } from '../src'; 5 | 6 | const createReduxStore = () => { 7 | const middlewares = applyMiddleware( 8 | thunk, 9 | queue 10 | ); 11 | 12 | return createStore(x => x, {}, middlewares); 13 | }; 14 | 15 | test('simple queue', t => { 16 | t.plan(1); 17 | 18 | const store = createReduxStore(); 19 | return store.dispatch(async dispatch => { 20 | const completionHandler = await dispatch(enqueue('id')); 21 | completionHandler(); 22 | t.pass(); 23 | }); 24 | }); 25 | 26 | test('multiple queues', t => { 27 | t.plan(2); 28 | 29 | let currentValue; 30 | 31 | const store = createReduxStore(); 32 | return Promise.all([ 33 | store.dispatch(async dispatch => { 34 | currentValue = 1; 35 | const completionHandler = await dispatch(enqueue('id')); 36 | 37 | await new Promise(res => { setTimeout(res, 200); }); 38 | 39 | t.is(currentValue, 1); 40 | completionHandler(); 41 | }), 42 | store.dispatch(async dispatch => { 43 | const completionHandler = await dispatch(enqueue('id')); 44 | currentValue = 2; 45 | completionHandler(); 46 | t.is(currentValue, 2); 47 | }), 48 | ]); 49 | }); 50 | 51 | test('tangent queues', t => { 52 | t.plan(2); 53 | 54 | let currentValue; 55 | 56 | const store = createReduxStore(); 57 | return Promise.all([ 58 | store.dispatch(async dispatch => { 59 | currentValue = 1; 60 | const completionHandler = await dispatch(enqueue('id1')); 61 | 62 | await new Promise(res => { setTimeout(res, 200); }); 63 | 64 | t.is(currentValue, 2); 65 | completionHandler(); 66 | }), 67 | store.dispatch(async dispatch => { 68 | const completionHandler = await dispatch(enqueue('id2')); 69 | currentValue = 2; 70 | completionHandler(); 71 | t.is(currentValue, 2); 72 | }), 73 | ]); 74 | }); 75 | --------------------------------------------------------------------------------