├── .babelrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── Makefile ├── Readme.md ├── package.json ├── src └── index.js └── test └── index.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | lib -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4" 4 | - "5" 5 | sudo: false 6 | script: "make test" 7 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Vars 3 | # 4 | 5 | BIN = ./node_modules/.bin 6 | .DEFAULT_GOAL := all 7 | 8 | # 9 | # Tasks 10 | # 11 | 12 | node_modules: package.json 13 | @npm install 14 | @touch node_modules 15 | 16 | test: node_modules 17 | ${BIN}/babel-node test/*.js 18 | 19 | validate: node_modules 20 | @${BIN}/standard 21 | 22 | clean: 23 | @rm -rf lib 24 | 25 | build: clean 26 | @${BIN}/babel src --out-dir lib 27 | 28 | all: validate test 29 | 30 | # 31 | # Phony 32 | # 33 | 34 | .PHONY: test validate clean build 35 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # redux-flo 2 | 3 | [![Build status][travis-image]][travis-url] 4 | [![Git tag][git-image]][git-url] 5 | [![NPM version][npm-image]][npm-url] 6 | [![Code style][standard-image]][standard-url] 7 | 8 | Redux style control flow middleware - inspired by haskel's free monad approach to io and [co](//github.com/tj/co). 9 | 10 | ## Installation 11 | 12 | $ npm install redux-flo 13 | 14 | ## Usage 15 | 16 | ```js 17 | import flow from 'redux-flo' 18 | import fetchMiddleware, {fetch} from 'redux-effects-fetch' 19 | import {createStore, applyMiddleware} from 'redux' 20 | 21 | const store = createStore(identity, applyMiddleware(flo(), fetchMiddleware)) 22 | const dispatch = store.dispatch 23 | 24 | // simple parallel 25 | 26 | dispatch([ 27 | fetch('google.com'), 28 | fetch('facebook.com') 29 | ]).then(res => res /* [google, facebook] */) 30 | 31 | // simple serial 32 | 33 | dispatch(function * () { 34 | yield fetch('google.com') // google 35 | return yield fetch('facebook.com') 36 | }).then(res => res /* facebook */) 37 | 38 | // complex 39 | dispatch(function * () { 40 | //sync 41 | yield fetch('google.com') // google 42 | yield fetch('facebook.com') // facebook 43 | //parallel 44 | yield [fetch('heroku.com'), fetch('segment.io')] // [heroku, segment] 45 | return 'done' 46 | }).then(res => res /* 'done' */) 47 | ``` 48 | 49 | ## API 50 | 51 | ### flow (errorHandler, successHandler) 52 | FLO middleWare. 53 | 54 | - `errorHandler` - handles errors in flows (defualts to throws) 55 | - `successHandler` - handles successes in flow (defaults to identity function) 56 | 57 | **Returns:** redux style middleware 58 | 59 | Flo is simple and powerful: 60 | 61 | **Functors and generators** will be mapped and converted to a promise (basically a map-reduce). 62 | ```js 63 | toPromise(map(dispatch, action)).then(successHandler, errorHandler) 64 | ``` 65 | 66 | **Promises and thunks** are converted to a promise. 67 | ```js 68 | toPromise(action).then(successHandler, errorHandler) 69 | ``` 70 | 71 | **All other types** (mostly we are talking about plain objects here) are passed down the middleware stack. 72 | 73 | ### Functors 74 | Functors implement map. An array is a functor. A plain object is not. This is good, because we don't want Flo to handle plain objects. We can however coerce plain objects into functors, letting you define custome behavior for Flo. Here's an example: 75 | 76 | ```js 77 | import flow from 'redux-flo' 78 | import fetchMiddleware, {fetch} from 'redux-effects-fetch' 79 | import bind from '@f/bind-middleware' 80 | import ObjectF from '@f/obj-functor' 81 | 82 | let dispatch = bind([flow(), fetchMiddleware]) 83 | 84 | dispatch(function * () { 85 | yield ObjectF({ 86 | google: fetch('google.com'), 87 | facebook: fetch('facebook.com') 88 | }) // => {google: google, facebook: facebook} 89 | }) 90 | ``` 91 | 92 | ## License 93 | 94 | MIT 95 | 96 | [travis-image]: https://img.shields.io/travis/redux-effects/redux-flo.svg?style=flat-square 97 | [travis-url]: https://travis-ci.org/redux-effects/redux-flo 98 | [git-image]: https://img.shields.io/github/tag/redux-effects/redux-flo.svg 99 | [git-url]: https://github.com/redux-effects/redux-flo 100 | [standard-image]: https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat 101 | [standard-url]: https://github.com/feross/standard 102 | [npm-image]: https://img.shields.io/npm/v/redux-flo.svg?style=flat-square 103 | [npm-url]: https://npmjs.org/package/redux-flo 104 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-flo", 3 | "description": "Flow control middleware.", 4 | "repository": "git://github.com/weo-edu/redux-flo.git", 5 | "version": "2.3.0", 6 | "license": "MIT", 7 | "main": "lib/index.js", 8 | "scripts": { 9 | "prepublish": "make build", 10 | "postpublish": "make clean", 11 | "postversion": "git push && git push --tags && npm publish --access=public" 12 | }, 13 | "dependencies": { 14 | "@f/identity": "^1.1.1", 15 | "@f/is-functor": "^1.0.0", 16 | "@f/is-generator": "^1.3.2", 17 | "@f/is-iterable": "^1.0.1", 18 | "@f/is-iterator": "^1.0.1", 19 | "@f/is-promise": "^1.1.1", 20 | "@f/log-error": "^1.0.0", 21 | "@f/map": "^1.5.2", 22 | "@f/to-promise": "^1.1.1" 23 | }, 24 | "devDependencies": { 25 | "@f/bind-middleware": "^1.1.1", 26 | "@f/obj-functor": "^1.0.0", 27 | "babel-cli": "^6.0.15", 28 | "babel-preset-es2015": "^6.1.2", 29 | "redux-log": "^2.0.0", 30 | "rlog": "0.0.2", 31 | "standard": "^5.1.0", 32 | "tape": "^4.2.0", 33 | "test-console": "^1.0.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Imports 3 | */ 4 | 5 | import toPromise from '@f/to-promise' 6 | import map from '@f/map' 7 | import identity from '@f/identity' 8 | import isIterator from '@f/is-iterator' 9 | import isGenerator from '@f/is-generator' 10 | import isPromise from '@f/is-promise' 11 | import isFunctor from '@f/is-functor' 12 | import logError from '@f/log-error' 13 | 14 | /** 15 | * Flo middleWare 16 | * @param {Function} errorHandler=defaultErrorHandler 17 | * @param {Function} successHandler=identity 18 | * @return {Function} Redux middleware 19 | */ 20 | 21 | function flow (errorHandler = defaultErrorHandler, successHandler = identity) { 22 | return ({dispatch}) => next => action => { 23 | let promise 24 | if (isFunctor(action) || isGenerator(action) || isIterator(action)) { 25 | promise = toPromise(map(action => action && dispatch(action), action)) 26 | } else if (isPromise(action)) { 27 | promise = toPromise(action) 28 | } else { 29 | return next(action) 30 | } 31 | return promise.then(successHandler, errorHandler) 32 | } 33 | } 34 | 35 | /** 36 | * Default error handler 37 | * 38 | * Logs the error and then throws it again to pass it back 39 | * to the calling code 40 | */ 41 | 42 | function defaultErrorHandler (err) { 43 | logError(err) 44 | throw err 45 | } 46 | 47 | /** 48 | * Exports 49 | */ 50 | 51 | export default flow 52 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Imports 3 | */ 4 | 5 | import test from 'tape' 6 | import flow from '../src' 7 | import {stderr} from 'test-console' 8 | import isFunction from '@f/is-function' 9 | import compose from '@f/bind-middleware' 10 | import isGenerator from '@f/is-generator' 11 | import identity from '@f/identity' 12 | import map from '@f/map' 13 | import rlog from 'redux-log' 14 | import ObjectF from '@f/obj-functor' 15 | 16 | /** 17 | * Tests 18 | */ 19 | 20 | var log = [] 21 | const doDispatch = (v) => { 22 | log.push(v) 23 | } 24 | const nextHandler = flow()({dispatch: doDispatch}) 25 | 26 | const wrapEach = (fn) => { 27 | return (t) => { 28 | // before 29 | log = [] 30 | fn(t) 31 | // after 32 | } 33 | } 34 | 35 | test('must return a function to handle next', t => { 36 | t.plan(2) 37 | t.ok(isFunction(nextHandler)) 38 | t.equal(nextHandler.length, 1) 39 | }) 40 | 41 | test('handle next must return a function to handle action', t => { 42 | t.plan(2) 43 | 44 | const actionHandler = nextHandler() 45 | t.ok(isFunction(actionHandler)) 46 | t.equal(actionHandler.length, 1) 47 | }) 48 | 49 | test('must run the given action generator function with dispatch', wrapEach(t => { 50 | t.plan(1) 51 | 52 | const actionHandler = nextHandler() 53 | 54 | actionHandler(function * () { 55 | yield 'foo' 56 | t.deepEqual(log, ['foo']) 57 | }) 58 | })) 59 | 60 | test('must run the given action array with dispatch', wrapEach(t => { 61 | t.plan(1) 62 | 63 | const actionHandler = nextHandler() 64 | 65 | actionHandler(['foo']).then(function () { 66 | t.deepEqual(log, ['foo']) 67 | }) 68 | 69 | })) 70 | 71 | test('must run the given nested action array with dispatch', wrapEach(t => { 72 | t.plan(1) 73 | 74 | let l = [] 75 | let dispatch = compose([flow(), rlog(l)]) 76 | 77 | dispatch(function * () { 78 | yield ['foo', 'bar'] 79 | yield 'qux' 80 | }).then(function () { 81 | t.deepEqual(l, ['foo', 'bar', 'qux']) 82 | }) 83 | 84 | })) 85 | 86 | test('must run the given nested action functor with dispatch', wrapEach(t => { 87 | t.plan(1) 88 | 89 | let l = [] 90 | let dispatch = compose([flow(), rlog(l)]) 91 | 92 | dispatch(ObjectF({foo: 'bar'})).then(function (res) { 93 | console.log('res', res) 94 | t.deepEqual(l, ['bar']) 95 | }) 96 | 97 | })) 98 | 99 | test('must run the given action generator object with dispatch', wrapEach(t => { 100 | t.plan(1) 101 | 102 | const actionHandler = nextHandler() 103 | 104 | actionHandler((function * () { 105 | yield 'foo' 106 | t.deepEqual(log, ['foo']) 107 | })()) 108 | })) 109 | 110 | test('must pass action to next if not iterable', wrapEach(t => { 111 | const actionObj = {type: 'action', payload: 'foo'} 112 | 113 | const actionHandler = nextHandler(action => { 114 | t.equal(action, actionObj) 115 | t.end() 116 | }) 117 | 118 | actionHandler(actionObj) 119 | })) 120 | 121 | test('must return the return value if not mappable', wrapEach(t => { 122 | t.plan(1) 123 | 124 | const expected = 'foo' 125 | const actionHandler = nextHandler(() => expected) 126 | 127 | let outcome = actionHandler() 128 | t.equal(outcome, expected) 129 | })) 130 | 131 | test('must return promise if a generator', wrapEach(t => { 132 | t.plan(1) 133 | 134 | const expected = 'foo' 135 | const actionHandler = nextHandler() 136 | 137 | let promise = actionHandler(function * () { 138 | return expected 139 | }) 140 | promise.then(outcome => { 141 | t.equal(outcome, expected) 142 | }) 143 | })) 144 | 145 | test('must throw error if argument is non-object', wrapEach(t => { 146 | t.plan(1) 147 | 148 | t.throws(() => flow()()) 149 | })) 150 | 151 | test('must allow custom error handler', t => { 152 | t.plan(2) 153 | 154 | const dispatch = () => { 155 | var err = new Error() 156 | err.stack = 'Foo' 157 | throw err 158 | } 159 | 160 | let handlerCalled = false 161 | const errorHandler = () => { 162 | handlerCalled = true 163 | } 164 | 165 | const nextHandler = flow(errorHandler)({dispatch: dispatch}) 166 | const actionHandler = nextHandler() 167 | 168 | let inspect = stderr.inspect() 169 | 170 | actionHandler(function * () { 171 | yield 'foo' 172 | }).then(() => { 173 | t.deepEqual(inspect.output, []) 174 | t.equal(handlerCalled, true) 175 | inspect.restore() 176 | }) 177 | }) 178 | 179 | test('should dispatch nested flos', wrapEach(t => { 180 | t.plan(3) 181 | 182 | const dispatch = compose([ 183 | flow(), 184 | ctx => next => action => action.type === 'fetch' ? 200 : next(action), 185 | ctx => next => action => 'foo' 186 | ]) 187 | 188 | dispatch(function * () { 189 | let [google, facebook] = yield [{type: 'fetch', payload: 'google'}, {type: 'fetch', payload: 'facebook'}] 190 | 191 | t.deepEqual(google, 200) 192 | t.deepEqual(facebook, 200) 193 | t.deepEqual('foo', yield {type: 'bar'}) 194 | t.end() 195 | }) 196 | })) 197 | --------------------------------------------------------------------------------