├── .babelrc ├── .gitignore ├── LICENSE ├── README.md ├── index.html ├── lib └── index.js ├── package.json ├── src └── index.js └── test └── index.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ "es2015" ] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Justin Hewlett 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ###Install 2 | 3 | `npm install --save redux-saga-combine-latest` 4 | 5 | ###Usage 6 | 7 | ```javascript 8 | import { createStore, applyMiddleware } from 'redux' 9 | import createSagaMiddleware from 'redux-saga' 10 | import * as effects from 'redux-saga/effects' 11 | import 'babel-polyfill' 12 | 13 | import createCombineLatest from 'redux-saga-combine-latest' 14 | 15 | const combineLatest = createCombineLatest(effects) 16 | 17 | function* handleActions(actions) { 18 | console.log(actions) 19 | } 20 | 21 | function* saga() { 22 | yield combineLatest(['type1', 'type2'], handleActions) 23 | } 24 | 25 | const sagaMiddleware = createSagaMiddleware(saga) 26 | 27 | const store = createStore( 28 | (state) => state, 29 | applyMiddleware(sagaMiddleware) 30 | ) 31 | 32 | store.dispatch({ type: 'type1', some: 'payload' }) //nothing logged 33 | store.dispatch({ type: 'type2', some: 'payload' }) //logs out "[{ type: 'type1', some: 'payload' }, { type: 'type2', some: 'payload' }]" 34 | store.dispatch({ type: 'type2', other: 'payload' }) //logs out "[{ type: 'type1', some: 'payload' }, { type: 'type2', other: 'payload' }]" 35 | ``` 36 | 37 | Notice that the handler saga does not get invoked until at least one action of each type as been received. From that point on, each time a new action is received that we care about, the handler is invoked with the latest action of each type. 38 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | App 6 | 7 | 8 |
9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.default = createCombineLatest; 7 | function createCombineLatest(_ref) { 8 | var take = _ref.take; 9 | 10 | return regeneratorRuntime.mark(function combineLatest(actionTypes, saga) { 11 | var actions, action; 12 | return regeneratorRuntime.wrap(function combineLatest$(_context) { 13 | while (1) { 14 | switch (_context.prev = _context.next) { 15 | case 0: 16 | actions = {}; 17 | 18 | case 1: 19 | if (!true) { 20 | _context.next = 11; 21 | break; 22 | } 23 | 24 | _context.next = 4; 25 | return take(actionTypes); 26 | 27 | case 4: 28 | action = _context.sent; 29 | 30 | actions[action.type] = action; 31 | 32 | if (!allActionsReady(actions, actionTypes)) { 33 | _context.next = 9; 34 | break; 35 | } 36 | 37 | _context.next = 9; 38 | return saga(actionTypes.map(function (t) { 39 | return actions[t]; 40 | })); 41 | 42 | case 9: 43 | _context.next = 1; 44 | break; 45 | 46 | case 11: 47 | case "end": 48 | return _context.stop(); 49 | } 50 | } 51 | }, combineLatest, this); 52 | }); 53 | } 54 | 55 | function allActionsReady(actions, actionTypes) { 56 | return Object.keys(actions).length === actionTypes.length; 57 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-saga-combine-latest", 3 | "version": "1.0.4", 4 | "description": "Wait for several action types to be dispatched before handling them", 5 | "main": "lib/index.js", 6 | "jsnext:main": "src/index.js", 7 | "scripts": { 8 | "compile": "babel src -d lib", 9 | "prepublish": "npm run test && npm run compile", 10 | "test": "BABEL_DISABLE_CACHE=1 mocha test/index.js --compilers js:babel-core/register", 11 | "test-watch": "BABEL_DISABLE_CACHE=1 mocha test/index.js -w --compilers js:babel-core/register" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/jhewlett/redux-saga-combine-latest.git" 16 | }, 17 | "keywords": [ 18 | "javascript", 19 | "redux", 20 | "saga", 21 | "frp", 22 | "stream", 23 | "generator" 24 | ], 25 | "author": "Justin Hewlett", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/jhewlett/redux-saga-combine-latest/issues" 29 | }, 30 | "homepage": "https://github.com/jhewlett/redux-saga-combine-latest#readme", 31 | "dependencies": {}, 32 | "devDependencies": { 33 | "babel-cli": "^6.6.5", 34 | "babel-core": "^6.7.2", 35 | "babel-polyfill": "^6.7.2", 36 | "babel-preset-es2015": "^6.6.0", 37 | "chai": "^3.5.0", 38 | "mocha": "^2.4.5", 39 | "redux": "^3.3.1", 40 | "redux-saga": "^0.9.4", 41 | "sinon": "^1.17.3", 42 | "sinon-chai": "^2.8.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export default function createCombineLatest({take}) { 2 | return function* combineLatest(actionTypes, saga) { 3 | let actions = {} 4 | while (true) { 5 | const action = yield take(actionTypes) 6 | actions[action.type] = action 7 | 8 | if (allActionsReady(actions, actionTypes)) 9 | yield saga(actionTypes.map(t => actions[t])) 10 | } 11 | } 12 | } 13 | 14 | function allActionsReady(actions, actionTypes) { 15 | return Object.keys(actions).length === actionTypes.length 16 | } 17 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import chai, { expect } from 'chai' 2 | import sinon from 'sinon' 3 | import sinonChai from 'sinon-chai' 4 | chai.use(sinonChai); 5 | 6 | import { createStore, applyMiddleware } from 'redux' 7 | import createSagaMiddleware from 'redux-saga' 8 | import * as effects from 'redux-saga/effects' 9 | import 'babel-polyfill' 10 | 11 | import createCombineLatest from '../src/index.js' 12 | 13 | const spy = sinon.spy() 14 | function* handleActions(actions) { 15 | spy(actions) 16 | } 17 | 18 | const combineLatest = createCombineLatest(effects) 19 | 20 | function* saga() { 21 | yield combineLatest(['type1', 'type2'], handleActions) 22 | } 23 | 24 | const sagaMiddleware = createSagaMiddleware(saga) 25 | 26 | const store = createStore( 27 | (state) => state, 28 | applyMiddleware(sagaMiddleware) 29 | ) 30 | 31 | describe('combineLatest', () => { 32 | const action1 = { type: 'type1', some: 'payload' } 33 | const action2 = { type: 'type2', some: 'payload' } 34 | const action3 = { type: 'type2', other: 'payload' } 35 | 36 | describe('when only one action type has been dispatched', () => { 37 | it('should not yield saga yet', () => { 38 | store.dispatch(action1) 39 | expect(spy).not.to.be.called 40 | }) 41 | }) 42 | 43 | describe('when all action types have been dispatched', () => { 44 | it('should yield saga with all actions', () => { 45 | store.dispatch(action2) 46 | expect(spy).to.be.calledWith([action1, action2]) 47 | }) 48 | }) 49 | 50 | describe('when a third action is dispatched', () => { 51 | it('should yield saga with latest actions of each type', () => { 52 | store.dispatch(action3) 53 | expect(spy).to.be.calledWith([action1, action3]) 54 | }) 55 | }) 56 | }) 57 | --------------------------------------------------------------------------------