├── .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 |
--------------------------------------------------------------------------------