Skip to content

Commit

Permalink
feat: add bindAsyncAction helper for redux-saga
Browse files Browse the repository at this point in the history
  • Loading branch information
aikoven committed Feb 7, 2017
1 parent a022f66 commit b887e31
Show file tree
Hide file tree
Showing 17 changed files with 2,418 additions and 39 deletions.
3 changes: 0 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@
# NPM
node_modules
npm-*.log
yarn.lock

tests-build

# OS X
.DS_Store
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ node_js:

script:
- npm run lint
- npm run build
- npm test
- npm run build
File renamed without changes.
37 changes: 37 additions & 0 deletions es/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
export function isType(action, actionCreator) {
return action.type === actionCreator.type;
}
export default function actionCreatorFactory(prefix, defaultIsError = p => p instanceof Error) {
const actionTypes = {};
const base = prefix ? `${prefix}/` : "";
function actionCreator(type, commonMeta, isError = defaultIsError) {
const fullType = base + type;
if (process.env.NODE_ENV !== 'production') {
if (actionTypes[fullType])
throw new Error(`Duplicate action type: ${fullType}`);
actionTypes[fullType] = true;
}
return Object.assign((payload, meta) => {
const action = {
type: fullType,
payload,
};
if (commonMeta || meta) {
action.meta = Object.assign({}, commonMeta, meta);
}
if (isError && (typeof isError === 'boolean' || isError(payload))) {
action.error = true;
}
return action;
}, { type: fullType });
}
function asyncActionCreators(type, commonMeta) {
return {
type: base + type,
started: actionCreator(`${type}_STARTED`, commonMeta, false),
done: actionCreator(`${type}_DONE`, commonMeta, false),
failed: actionCreator(`${type}_FAILED`, commonMeta, true),
};
}
return Object.assign(actionCreator, { async: asyncActionCreators });
}
15 changes: 15 additions & 0 deletions es/sagas.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { AsyncActionCreators } from "./index";
import { SagaIterator } from "redux-saga";
export declare function bindAsyncAction<R>(actionCreators: AsyncActionCreators<void, R, any>): {
(worker: () => Promise<R> | SagaIterator): () => SagaIterator;
(worker: (params: void) => Promise<R> | SagaIterator): (params: void) => SagaIterator;
<A1>(worker: (params: void, arg1: A1) => Promise<R> | SagaIterator): (params: void, arg1: A1) => SagaIterator;
<A1, A2>(worker: (params: void, arg1: A1, arg2: A2) => Promise<R> | SagaIterator): (params: void, arg1: A1, arg2: A2) => SagaIterator;
<A1, A2, A3>(worker: (params: void, arg1: A1, arg2: A2, arg3: A3, ...rest: any[]) => Promise<R> | SagaIterator): (params: void, arg1: A1, arg2: A2, arg3: A3, ...rest: any[]) => SagaIterator;
};
export declare function bindAsyncAction<P, R>(actionCreators: AsyncActionCreators<P, R, any>): {
(worker: (params: P) => Promise<R> | SagaIterator): (params: P) => SagaIterator;
<A1>(worker: (params: P, arg1: A1) => Promise<R> | SagaIterator): (params: P, arg1: A1) => SagaIterator;
<A1, A2>(worker: (params: P, arg1: A1, arg2: A2) => Promise<R> | SagaIterator): (params: P, arg1: A1, arg2: A2) => SagaIterator;
<A1, A2, A3>(worker: (params: P, arg1: A1, arg2: A2, arg3: A3, ...rest: any[]) => Promise<R> | SagaIterator): (params: P, arg1: A1, arg2: A2, arg3: A3, ...rest: any[]) => SagaIterator;
};
42 changes: 42 additions & 0 deletions es/sagas.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { put, call, cancelled } from "redux-saga/effects";
export function bindAsyncAction(actionCreator) {
return (worker) => {
function* boundAsyncActionSaga(params, ...args) {
yield put(actionCreator.started(params));
try {
const result = yield call(worker, params, ...args);
yield put(actionCreator.done({ params, result }));
return result;
}
catch (error) {
yield put(actionCreator.failed({ params, error }));
throw error;
}
finally {
if (yield cancelled()) {
yield put(actionCreator.failed({ params, error: 'cancelled' }));
}
}
}
const capName = worker.name.charAt(0).toUpperCase() +
worker.name.substring(1);
return setFunctionName(boundAsyncActionSaga, `bound${capName}(${actionCreator.type})`);
};
}
/**
* Set function name.
*
* Note that this won't have effect on built-in Chrome stack traces, although
* useful for traces generated by `redux-saga`.
*/
function setFunctionName(func, name) {
try {
Object.defineProperty(func, 'name', {
value: name,
configurable: true,
});
}
catch (e) {
}
return func;
}
37 changes: 37 additions & 0 deletions lib/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Action as ReduxAction } from "redux";
export interface Action<P> extends ReduxAction {
type: string;
payload: P;
error?: boolean;
meta?: Object;
}
export interface Success<P, S> {
params: P;
result: S;
}
export interface Failure<P, E> {
params: P;
error: E;
}
export declare function isType<P>(action: ReduxAction, actionCreator: ActionCreator<P>): action is Action<P>;
export interface ActionCreator<P> {
type: string;
(payload: P, meta?: Object): Action<P>;
}
export interface EmptyActionCreator extends ActionCreator<undefined> {
(payload?: undefined, meta?: Object): Action<undefined>;
}
export interface AsyncActionCreators<P, S, E> {
type: string;
started: ActionCreator<P>;
done: ActionCreator<Success<P, S>>;
failed: ActionCreator<Failure<P, E>>;
}
export interface ActionCreatorFactory {
(type: string, commonMeta?: Object, error?: boolean): EmptyActionCreator;
<P>(type: string, commonMeta?: Object, isError?: boolean): ActionCreator<P>;
<P>(type: string, commonMeta?: Object, isError?: (payload: P) => boolean): ActionCreator<P>;
async<P, S>(type: string, commonMeta?: Object): AsyncActionCreators<P, S, any>;
async<P, S, E>(type: string, commonMeta?: Object): AsyncActionCreators<P, S, E>;
}
export default function actionCreatorFactory(prefix?: string, defaultIsError?: (payload: any) => boolean): ActionCreatorFactory;
38 changes: 22 additions & 16 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,32 @@
"use strict";
'use strict';

Object.defineProperty(exports, "__esModule", {
value: true
});
exports.isType = isType;
exports.default = actionCreatorFactory;
function isType(action, actionCreator) {
return action.type === actionCreator.type;
}
exports.isType = isType;
function actionCreatorFactory(prefix, defaultIsError) {
if (defaultIsError === void 0) { defaultIsError = function (p) { return p instanceof Error; }; }
function actionCreatorFactory(prefix) {
var defaultIsError = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : function (p) {
return p instanceof Error;
};

var actionTypes = {};
var base = prefix ? prefix + "/" : "";
function actionCreator(type, commonMeta, isError) {
if (isError === void 0) { isError = defaultIsError; }
var base = prefix ? prefix + '/' : "";
function actionCreator(type, commonMeta) {
var isError = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : defaultIsError;

var fullType = base + type;
if (process.env.NODE_ENV !== 'production') {
if (actionTypes[fullType])
throw new Error("Duplicate action type: " + fullType);
if (actionTypes[fullType]) throw new Error('Duplicate action type: ' + fullType);
actionTypes[fullType] = true;
}
return Object.assign(function (payload, meta) {
var action = {
type: fullType,
payload: payload,
payload: payload
};
if (commonMeta || meta) {
action.meta = Object.assign({}, commonMeta, meta);
Expand All @@ -32,12 +40,10 @@ function actionCreatorFactory(prefix, defaultIsError) {
function asyncActionCreators(type, commonMeta) {
return {
type: base + type,
started: actionCreator(type + "_STARTED", commonMeta, false),
done: actionCreator(type + "_DONE", commonMeta, false),
failed: actionCreator(type + "_FAILED", commonMeta, true),
started: actionCreator(type + '_STARTED', commonMeta, false),
done: actionCreator(type + '_DONE', commonMeta, false),
failed: actionCreator(type + '_FAILED', commonMeta, true)
};
}
return Object.assign(actionCreator, { async: asyncActionCreators });
}
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = actionCreatorFactory;
}
15 changes: 15 additions & 0 deletions lib/sagas.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { AsyncActionCreators } from "./index";
import { SagaIterator } from "redux-saga";
export declare function bindAsyncAction<R>(actionCreators: AsyncActionCreators<void, R, any>): {
(worker: () => Promise<R> | SagaIterator): () => SagaIterator;
(worker: (params: void) => Promise<R> | SagaIterator): (params: void) => SagaIterator;
<A1>(worker: (params: void, arg1: A1) => Promise<R> | SagaIterator): (params: void, arg1: A1) => SagaIterator;
<A1, A2>(worker: (params: void, arg1: A1, arg2: A2) => Promise<R> | SagaIterator): (params: void, arg1: A1, arg2: A2) => SagaIterator;
<A1, A2, A3>(worker: (params: void, arg1: A1, arg2: A2, arg3: A3, ...rest: any[]) => Promise<R> | SagaIterator): (params: void, arg1: A1, arg2: A2, arg3: A3, ...rest: any[]) => SagaIterator;
};
export declare function bindAsyncAction<P, R>(actionCreators: AsyncActionCreators<P, R, any>): {
(worker: (params: P) => Promise<R> | SagaIterator): (params: P) => SagaIterator;
<A1>(worker: (params: P, arg1: A1) => Promise<R> | SagaIterator): (params: P, arg1: A1) => SagaIterator;
<A1, A2>(worker: (params: P, arg1: A1, arg2: A2) => Promise<R> | SagaIterator): (params: P, arg1: A1, arg2: A2) => SagaIterator;
<A1, A2, A3>(worker: (params: P, arg1: A1, arg2: A2, arg3: A3, ...rest: any[]) => Promise<R> | SagaIterator): (params: P, arg1: A1, arg2: A2, arg3: A3, ...rest: any[]) => SagaIterator;
};
97 changes: 97 additions & 0 deletions lib/sagas.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
'use strict';

Object.defineProperty(exports, "__esModule", {
value: true
});
exports.bindAsyncAction = bindAsyncAction;

var _effects = require('redux-saga/effects');

function bindAsyncAction(actionCreator) {
return function (worker) {
var _marked = [boundAsyncActionSaga].map(regeneratorRuntime.mark);

function boundAsyncActionSaga(params) {
var _len,
args,
_key,
result,
_args = arguments;

return regeneratorRuntime.wrap(function boundAsyncActionSaga$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
_context.next = 2;
return (0, _effects.put)(actionCreator.started(params));

case 2:
_context.prev = 2;

for (_len = _args.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
args[_key - 1] = _args[_key];
}

_context.next = 6;
return _effects.call.apply(undefined, [worker, params].concat(args));

case 6:
result = _context.sent;
_context.next = 9;
return (0, _effects.put)(actionCreator.done({ params: params, result: result }));

case 9:
return _context.abrupt('return', result);

case 12:
_context.prev = 12;
_context.t0 = _context['catch'](2);
_context.next = 16;
return (0, _effects.put)(actionCreator.failed({ params: params, error: _context.t0 }));

case 16:
throw _context.t0;

case 17:
_context.prev = 17;
_context.next = 20;
return (0, _effects.cancelled)();

case 20:
if (!_context.sent) {
_context.next = 23;
break;
}

_context.next = 23;
return (0, _effects.put)(actionCreator.failed({ params: params, error: 'cancelled' }));

case 23:
return _context.finish(17);

case 24:
case 'end':
return _context.stop();
}
}
}, _marked[0], this, [[2, 12, 17, 24]]);
}
var capName = worker.name.charAt(0).toUpperCase() + worker.name.substring(1);
return setFunctionName(boundAsyncActionSaga, 'bound' + capName + '(' + actionCreator.type + ')');
};
}
/**
* Set function name.
*
* Note that this won't have effect on built-in Chrome stack traces, although
* useful for traces generated by `redux-saga`.
*/
function setFunctionName(func, name) {
try {
Object.defineProperty(func, 'name', {
value: name,
configurable: true
});
} catch (e) {}
return func;
}
26 changes: 18 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,37 @@
"action creator"
],
"main": "lib/index.js",
"typings": "./index.d.ts",
"jsnext:main": "es/index.js",
"typings": "lib/index.d.ts",
"files": [
"lib",
"index.d.ts"
"es",
"lib"
],
"repository": "aikoven/redux-typescript-actions",
"scripts": {
"clean": "rimraf lib",
"clean": "rimraf lib es",
"lint": "tslint -c tslint.json src/**/*.ts tests/**/*.ts",
"test": "tsc -p tsconfig.tests.json && tape tests-build/tests/*.js",
"build": "tsc",
"prepublish": "npm run clean && npm run build"
"test": "ts-node -P tsconfig.tests.json node_modules/.bin/tape tests/*.ts",
"build:es": "tsc",
"build:lib": "babel es --out-dir lib && cpx \"es/*.d.ts\" lib",
"build": "npm run build:es && npm run build:lib",
"prepublish": "npm run clean && npm run lint && npm run test && npm run build"
},
"author": "Daniel Lytkin <dan.lytkin@gmail.com>",
"license": "MIT",
"devDependencies": {
"@types/tape": "^4.2.28",
"babel-core": "^6.22.1",
"babel-preset-es2015": "^6.22.0",
"cpx": "^1.5.0",
"redux": "^3.6.0",
"rimraf": "^2.5.4",
"tape": "^4.6.2",
"ts-node": "^2.0.0",
"tslint": "^4.1.0",
"typescript": "^2.0.3"
"typescript": "^2.1.5"
},
"optionalDependencies": {
"redux-saga": "^0.14.3"
}
}
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export default function actionCreatorFactory(
prefix?: string,
defaultIsError: (payload: any) => boolean = p => p instanceof Error,
): ActionCreatorFactory {
const actionTypes = {};
const actionTypes: {[type: string]: boolean} = {};

const base = prefix ? `${prefix}/` : "";

Expand Down

0 comments on commit b887e31

Please sign in to comment.