├── .babelrc ├── .gitignore ├── .npmignore ├── 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 -------------------------------------------------------------------------------- /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 | 2 | # redux-gen 3 | 4 | [![Codeship Status for weo-edu/redux-gen](https://img.shields.io/codeship/816e83f0-3e69-0133-9f44-5a0949beaeb8/master.svg)](https://codeship.com/projects/102760) [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](https://github.com/feross/standard) 5 | 6 | Generator middleware for redux. Allows you to write action creators that return generators, enabling apps to push all side effects into a small set of effects middleware. 7 | 8 | ## Installation 9 | 10 | $ npm install redux-gen 11 | 12 | ## Usage 13 | 14 | Push side effects to edges by putting all io in middleware. 15 | 16 | ```js 17 | import { createStore, applyMiddleware } from 'redux' 18 | import gen from 'redux-gen' 19 | import rootReducer from './reducers/index' 20 | import fetch from 'isomorphic-fetch' 21 | 22 | // create a store that has redux-thunk middleware enabled 23 | const createStoreWithMiddleware = applyMiddleware( 24 | gen() 25 | fetch 26 | )(createStore); 27 | 28 | const store = createStoreWithMiddleware(rootReducer); 29 | 30 | // returns [ 31 | // {username: "josh", id: 1}, 32 | // {username: "tio", id: 2}, 33 | // {username: "shasta", id: 3} 34 | // ] 35 | store.dispatch(getUsers()) 36 | 37 | // Side Effects Middleware 38 | 39 | function fetch ({dispatch, getState}) { 40 | return next => action => 41 | action.type === 'FETCH' 42 | ? fetch(action.payload.url, action.payload.params).then(res => res.json()) 43 | : next(action) 44 | } 45 | 46 | // Actions 47 | 48 | function getUsers * () { 49 | let userIds = yield {url: '/users', method: 'GET', type: 'FETCH'} 50 | return yield userIds.map(userId => ({url: '/user/' + userId, method: 'GET', type: 'FETCH'})) 51 | } 52 | 53 | ``` 54 | 55 | 56 | ## License 57 | 58 | MIT 59 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-gen", 3 | "description": "Generator middleware for redux.", 4 | "repository": "git://github.com/weo-edu/redux-gen.git", 5 | "version": "0.2.0", 6 | "license": "MIT", 7 | "main": "lib/index.js", 8 | "scripts": { 9 | "prepublish": "make build", 10 | "postpublish": "make clean" 11 | }, 12 | "dependencies": { 13 | "@weo-edu/is": "^0.2.0", 14 | "yio": "^0.2.4" 15 | }, 16 | "devDependencies": { 17 | "babel-cli": "^6.1.2", 18 | "babel-preset-es2015": "^6.1.2", 19 | "standard": "^5.1.0", 20 | "tape": "^4.2.0", 21 | "test-console": "^1.0.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Imports 3 | */ 4 | 5 | import assert from 'assert' 6 | import yio from 'yio' 7 | import is from '@weo-edu/is' 8 | 9 | 10 | /** 11 | * Exports 12 | */ 13 | 14 | export default function genMiddleware(errorHandler=defaultErrorHandler, successHandler=identity) { 15 | return ({dispatch}) => next => action => 16 | is.generator(action) || is.generatorFunction(action) 17 | ? yio(dispatch, action).then(successHandler, errorHandler) 18 | : next(action) 19 | } 20 | 21 | function identity (v) { 22 | return v 23 | } 24 | 25 | function defaultErrorHandler (err) { 26 | assert(err instanceof Error, 'non-error thrown: ' + err) 27 | 28 | var msg = err.stack || err.toString() 29 | console.error() 30 | console.error(msg.replace(/^/gm, ' ')) 31 | console.error() 32 | throw err 33 | } 34 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Imports 3 | */ 4 | 5 | import test from 'tape' 6 | import reduxGen from '../src' 7 | import is from '@weo-edu/is' 8 | import {stderr} from 'test-console' 9 | import {AssertionError} from 'assert' 10 | 11 | 12 | /** 13 | * Tests 14 | */ 15 | 16 | var log = [] 17 | const doDispatch = (v) => { 18 | log.push(v) 19 | } 20 | const nextHandler = reduxGen()({dispatch: doDispatch}) 21 | 22 | const wrapEach = function(fn) { 23 | return function(t) { 24 | // before 25 | log = [] 26 | fn(t) 27 | //after 28 | } 29 | } 30 | 31 | test('must return a function to handle next', t => { 32 | t.plan(2) 33 | t.ok(is.function(nextHandler)) 34 | t.equal(nextHandler.length, 1) 35 | }) 36 | 37 | test('handle next must return a function to handle action', t => { 38 | t.plan(2) 39 | 40 | const actionHandler = nextHandler() 41 | t.ok(is.function(actionHandler)) 42 | t.equal(actionHandler.length, 1) 43 | }) 44 | 45 | test('must run the given action generator function with dispatch', wrapEach(t => { 46 | t.plan(1) 47 | 48 | const actionHandler = nextHandler(); 49 | 50 | actionHandler(function *() { 51 | yield 'foo' 52 | t.deepEqual(log, ['foo']) 53 | }) 54 | 55 | })) 56 | 57 | test('must run the given action generator with dispatch', wrapEach(t => { 58 | t.plan(1) 59 | 60 | const actionHandler = nextHandler(); 61 | 62 | actionHandler((function *() { 63 | yield 'foo' 64 | t.deepEqual(log, ['foo']) 65 | })()) 66 | 67 | })) 68 | 69 | test('must pass acton to next if not a generator', wrapEach(t => { 70 | t.plan(1) 71 | 72 | const actionObj = {} 73 | 74 | const actionHandler = nextHandler(action => { 75 | t.equal(action, actionObj) 76 | }) 77 | 78 | actionHandler(actionObj) 79 | })) 80 | 81 | test('must return the return value if not a generator', wrapEach(t => { 82 | t.plan(1) 83 | 84 | const expected = 'foo' 85 | const actionHandler = nextHandler(() => expected) 86 | 87 | let outcome = actionHandler() 88 | t.equal(outcome, expected) 89 | })) 90 | 91 | test('must return promise if a generator', wrapEach(t => { 92 | t.plan(1) 93 | 94 | const expected = 'foo' 95 | const actionHandler = nextHandler() 96 | 97 | let promise = actionHandler(function *() { 98 | return expected 99 | }) 100 | promise.then(function(outcome) { 101 | t.equal(outcome, expected) 102 | }) 103 | })) 104 | 105 | test('must throw error if argument is non-object', wrapEach(t => { 106 | t.plan(1) 107 | 108 | t.throws(() => reduxGen()()) 109 | })) 110 | 111 | test('must log errors to stderr', t => { 112 | t.plan(1) 113 | 114 | const dispatch = () => { 115 | var err = new Error() 116 | err.stack = 'Foo' 117 | throw err 118 | } 119 | const nextHandler = reduxGen()({dispatch: dispatch}) 120 | const actionHandler = nextHandler() 121 | 122 | let inspect = stderr.inspect() 123 | 124 | actionHandler(function *() { 125 | yield 'foo' 126 | }).catch(function() { 127 | t.deepEqual(inspect.output, ["\n", " Foo\n", "\n"]) 128 | inspect.restore() 129 | }) 130 | 131 | }) 132 | 133 | test('must allow custom error handler', t => { 134 | t.plan(2) 135 | 136 | 137 | const dispatch = () => { 138 | var err = new Error() 139 | err.stack = 'Foo' 140 | throw err 141 | } 142 | 143 | let handlerCalled = false 144 | const errorHandler = () => { 145 | console.log('error handler') 146 | handlerCalled = true 147 | } 148 | 149 | const nextHandler = reduxGen(errorHandler)({dispatch: dispatch}) 150 | const actionHandler = nextHandler() 151 | 152 | let inspect = stderr.inspect() 153 | 154 | actionHandler(function *() { 155 | yield 'foo' 156 | }).then(function() { 157 | t.deepEqual(inspect.output, []) 158 | t.equal(handlerCalled, true) 159 | inspect.restore() 160 | }) 161 | }) 162 | 163 | test('must throw error when non error given', t => { 164 | t.plan(2) 165 | 166 | const dispatch = () => { 167 | throw 'foo' 168 | } 169 | const nextHandler = reduxGen()({dispatch: dispatch}) 170 | const actionHandler = nextHandler() 171 | 172 | actionHandler(function *() { 173 | yield 'foo' 174 | }).catch(function(err) { 175 | t.ok(err instanceof AssertionError) 176 | t.equal(err.message, 'non-error thrown: foo') 177 | }) 178 | }) 179 | --------------------------------------------------------------------------------