redux-saga
Advanced tools
Comparing version 0.3.3 to 0.4.0
@@ -6,4 +6,31 @@ 'use strict'; | ||
}); | ||
exports.join = exports.fork = exports.cps = exports.call = exports.race = exports.put = exports.take = undefined; | ||
exports.storeIO = exports.runSaga = exports.as = exports.cancel = exports.join = exports.fork = exports.cps = exports.call = exports.race = exports.put = exports.take = exports.MANUAL_CANCEL = exports.PARALLEL_AUTO_CANCEL = exports.RACE_AUTO_CANCEL = exports.SagaCancellationException = undefined; | ||
var _proc = require('./proc'); | ||
Object.defineProperty(exports, 'SagaCancellationException', { | ||
enumerable: true, | ||
get: function get() { | ||
return _proc.SagaCancellationException; | ||
} | ||
}); | ||
Object.defineProperty(exports, 'RACE_AUTO_CANCEL', { | ||
enumerable: true, | ||
get: function get() { | ||
return _proc.RACE_AUTO_CANCEL; | ||
} | ||
}); | ||
Object.defineProperty(exports, 'PARALLEL_AUTO_CANCEL', { | ||
enumerable: true, | ||
get: function get() { | ||
return _proc.PARALLEL_AUTO_CANCEL; | ||
} | ||
}); | ||
Object.defineProperty(exports, 'MANUAL_CANCEL', { | ||
enumerable: true, | ||
get: function get() { | ||
return _proc.MANUAL_CANCEL; | ||
} | ||
}); | ||
var _io = require('./io'); | ||
@@ -53,43 +80,36 @@ | ||
}); | ||
Object.defineProperty(exports, 'cancel', { | ||
enumerable: true, | ||
get: function get() { | ||
return _io.cancel; | ||
} | ||
}); | ||
Object.defineProperty(exports, 'as', { | ||
enumerable: true, | ||
get: function get() { | ||
return _io.as; | ||
} | ||
}); | ||
var _utils = require('./utils'); | ||
var _runSaga = require('./runSaga'); | ||
var _proc = require('./proc'); | ||
var _proc2 = _interopRequireDefault(_proc); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
exports.default = function () { | ||
for (var _len = arguments.length, sagas = Array(_len), _key = 0; _key < _len; _key++) { | ||
sagas[_key] = arguments[_key]; | ||
Object.defineProperty(exports, 'runSaga', { | ||
enumerable: true, | ||
get: function get() { | ||
return _runSaga.runSaga; | ||
} | ||
}); | ||
Object.defineProperty(exports, 'storeIO', { | ||
enumerable: true, | ||
get: function get() { | ||
return _runSaga.storeIO; | ||
} | ||
}); | ||
return function (_ref) { | ||
var getState = _ref.getState; | ||
var dispatch = _ref.dispatch; | ||
var _middleware = require('./middleware'); | ||
var cbs = []; | ||
var _middleware2 = _interopRequireDefault(_middleware); | ||
sagas.forEach(function (saga) { | ||
(0, _proc2.default)(saga(getState), subscribe, dispatch, saga.name); | ||
}); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
return function (next) { | ||
return function (action) { | ||
var result = next(action); // hit reducers | ||
cbs.forEach(function (cb) { | ||
return cb(action); | ||
}); | ||
return result; | ||
}; | ||
}; | ||
function subscribe(cb) { | ||
cbs.push(cb); | ||
return function () { | ||
return (0, _utils.remove)(cbs, cb); | ||
}; | ||
} | ||
}; | ||
}; | ||
exports.default = _middleware2.default; |
@@ -6,3 +6,3 @@ "use strict"; | ||
}); | ||
exports.as = exports.JOIN_ARG_ERROR = exports.FORK_ARG_ERROR = exports.CPS_FUNCTION_ARG_ERROR = exports.CALL_FUNCTION_ARG_ERROR = undefined; | ||
exports.as = exports.CANCEL_ARG_ERROR = exports.JOIN_ARG_ERROR = exports.FORK_ARG_ERROR = exports.CPS_FUNCTION_ARG_ERROR = exports.CALL_FUNCTION_ARG_ERROR = undefined; | ||
exports.matcher = matcher; | ||
@@ -13,5 +13,7 @@ exports.take = take; | ||
exports.call = call; | ||
exports.apply = apply; | ||
exports.cps = cps; | ||
exports.fork = fork; | ||
exports.join = join; | ||
exports.cancel = cancel; | ||
@@ -22,9 +24,9 @@ var _utils = require("./utils"); | ||
var CALL_FUNCTION_ARG_ERROR = exports.CALL_FUNCTION_ARG_ERROR = "io.call first argument must be a function"; | ||
var CPS_FUNCTION_ARG_ERROR = exports.CPS_FUNCTION_ARG_ERROR = "io.cps first argument must be a function"; | ||
var FORK_ARG_ERROR = exports.FORK_ARG_ERROR = "io.fork first argument must be a generator function or an iterator"; | ||
var JOIN_ARG_ERROR = exports.JOIN_ARG_ERROR = "io.join argument must be a valid task (a result of io.fork)"; | ||
var CALL_FUNCTION_ARG_ERROR = exports.CALL_FUNCTION_ARG_ERROR = "call first argument must be a function"; | ||
var CPS_FUNCTION_ARG_ERROR = exports.CPS_FUNCTION_ARG_ERROR = "cps first argument must be a function"; | ||
var FORK_ARG_ERROR = exports.FORK_ARG_ERROR = "fork first argument must be a generator function or an iterator"; | ||
var JOIN_ARG_ERROR = exports.JOIN_ARG_ERROR = "join argument must be a valid task (a result of a fork)"; | ||
var CANCEL_ARG_ERROR = exports.CANCEL_ARG_ERROR = "cancel argument must be a valid task (a result of a fork)"; | ||
var IO = Symbol('IO'); | ||
var TAKE = 'TAKE'; | ||
@@ -37,2 +39,3 @@ var PUT = 'PUT'; | ||
var JOIN = 'JOIN'; | ||
var CANCEL = 'CANCEL'; | ||
@@ -89,4 +92,10 @@ var effect = function effect(type, payload) { | ||
return apply(null, fn, args); | ||
} | ||
function apply(context, fn) { | ||
var args = arguments.length <= 2 || arguments[2] === undefined ? [] : arguments[2]; | ||
(0, _utils.check)(fn, _utils.is.func, CALL_FUNCTION_ARG_ERROR); | ||
return effect(CALL, { fn: fn, args: args }); | ||
return effect(CALL, { context: context, fn: fn, args: args }); | ||
} | ||
@@ -111,4 +120,8 @@ | ||
var isForkedTask = function isForkedTask(task) { | ||
return task[_utils.TASK]; | ||
}; | ||
function join(taskDesc) { | ||
if (!taskDesc[_utils.TASK]) throw new Error(JOIN_ARG_ERROR); | ||
if (!isForkedTask(taskDesc)) throw new Error(JOIN_ARG_ERROR); | ||
@@ -118,2 +131,8 @@ return effect(JOIN, taskDesc); | ||
function cancel(taskDesc) { | ||
if (!isForkedTask(taskDesc)) throw new Error(CANCEL_ARG_ERROR); | ||
return effect(CANCEL, taskDesc); | ||
} | ||
var as = exports.as = { | ||
@@ -140,3 +159,6 @@ take: function take(effect) { | ||
return effect && effect[IO] && effect[JOIN]; | ||
}, | ||
cancel: function cancel(effect) { | ||
return effect && effect[IO] && effect[CANCEL]; | ||
} | ||
}; |
225
lib/proc.js
@@ -6,3 +6,3 @@ 'use strict'; | ||
}); | ||
exports.NOT_ITERATOR_ERROR = undefined; | ||
exports.SagaCancellationException = exports.CANCEL = exports.MANUAL_CANCEL = exports.RACE_AUTO_CANCEL = exports.PARALLEL_AUTO_CANCEL = exports.NOT_ITERATOR_ERROR = undefined; | ||
exports.default = proc; | ||
@@ -14,2 +14,8 @@ | ||
var _monitorActions = require('./monitorActions'); | ||
var monitorActions = _interopRequireWildcard(_monitorActions); | ||
function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } | ||
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } | ||
@@ -19,69 +25,133 @@ | ||
var NOT_ITERATOR_ERROR = exports.NOT_ITERATOR_ERROR = "proc first argument (Saga function result) must be an iterator"; | ||
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } | ||
var NOT_ITERATOR_ERROR = exports.NOT_ITERATOR_ERROR = 'proc first argument (Saga function result) must be an iterator'; | ||
var PARALLEL_AUTO_CANCEL = exports.PARALLEL_AUTO_CANCEL = 'PARALLEL_AUTO_CANCEL'; | ||
var RACE_AUTO_CANCEL = exports.RACE_AUTO_CANCEL = 'RACE_AUTO_CANCEL'; | ||
var MANUAL_CANCEL = exports.MANUAL_CANCEL = 'MANUAL_CANCEL'; | ||
var nextEffectId = (0, _utils.autoInc)(); | ||
var CANCEL = exports.CANCEL = Symbol('@@redux-saga/cancelPromise'); | ||
var SagaCancellationException = exports.SagaCancellationException = function SagaCancellationException(type, saga, origin) { | ||
_classCallCheck(this, SagaCancellationException); | ||
this.type = type; | ||
this.saga = saga; | ||
this.origin = origin; | ||
}; | ||
function proc(iterator) { | ||
var subscribe = arguments.length <= 1 || arguments[1] === undefined ? function () { | ||
return function () {}; | ||
return _utils.noop; | ||
} : arguments[1]; | ||
var dispatch = arguments.length <= 2 || arguments[2] === undefined ? function () {} : arguments[2]; | ||
var dispatch = arguments.length <= 2 || arguments[2] === undefined ? _utils.noop : arguments[2]; | ||
var monitor = arguments.length <= 3 || arguments[3] === undefined ? _utils.noop : arguments[3]; | ||
var parentEffectId = arguments.length <= 4 || arguments[4] === undefined ? 0 : arguments[4]; | ||
var name = arguments.length <= 5 || arguments[5] === undefined ? 'anonymous' : arguments[5]; | ||
(0, _utils.check)(iterator, _utils.is.iterator, NOT_ITERATOR_ERROR); | ||
var deferredInput = undefined, | ||
deferredEnd = undefined; | ||
var deferredInputs = []; | ||
var canThrow = _utils.is.throw(iterator); | ||
var deferredEnd = (0, _utils.deferred)(); | ||
var endP = new Promise(function (resolve, reject) { | ||
return deferredEnd = { resolve: resolve, reject: reject }; | ||
}); | ||
var unsubscribe = subscribe(function (input) { | ||
if (deferredInput && deferredInput.match(input)) deferredInput.resolve(input); | ||
deferredInputs.forEach(function (def) { | ||
if (def.match(input)) def.resolve(input); | ||
}); | ||
}); | ||
iterator._isRunning = true; | ||
next(); | ||
iterator._isRunning = true; | ||
return endP; | ||
return newTask(parentEffectId, name, iterator, deferredEnd.promise); | ||
function next(arg, isError) { | ||
//console.log('next', arg, isError) | ||
deferredInput = null; | ||
if (!iterator._isRunning) return; | ||
try { | ||
if (isError && !canThrow) throw arg; | ||
var result = isError ? iterator.throw(arg) : iterator.next(arg); | ||
if (!result.done) { | ||
//console.log('yield', name, result.value) | ||
runEffect(result.value).then(next, function (err) { | ||
var currentEffect = runEffect(result.value, parentEffectId); | ||
deferredEnd.promise[CANCEL] = currentEffect[CANCEL]; | ||
currentEffect.then(next, function (err) { | ||
return next(err, true); | ||
}); | ||
} else { | ||
//console.log('return', name, result.value) | ||
iterator._isRunning = false; | ||
iterator._result = result.value; | ||
unsubscribe(); | ||
deferredEnd.resolve(result.value); | ||
end(result.value); | ||
} | ||
} catch (err) { | ||
//console.log('catch', name, err) | ||
iterator._isRunning = false; | ||
iterator._error = err; | ||
unsubscribe(); | ||
deferredEnd.reject(err); | ||
} catch (error) { | ||
/*eslint-disable no-console*/ | ||
console.warn(name + ': uncaught', error); | ||
end(error, true); | ||
} | ||
} | ||
function runEffect(effect) { | ||
function end(result, isError) { | ||
iterator._isRunning = false; | ||
if (!isError) { | ||
iterator._result = result; | ||
deferredEnd.resolve(result); | ||
} else { | ||
iterator._error = result; | ||
deferredEnd.reject(result); | ||
} | ||
unsubscribe(); | ||
} | ||
function runEffect(effect, parentEffectId) { | ||
var label = arguments.length <= 2 || arguments[2] === undefined ? '' : arguments[2]; | ||
var effectId = nextEffectId(); | ||
monitor(monitorActions.effectTriggered(effectId, parentEffectId, label, effect)); | ||
var data = undefined; | ||
return _utils.is.array(effect) ? Promise.all(effect.map(runEffect)) : _utils.is.iterator(effect) ? proc(effect, subscribe, dispatch) : (data = _io.as.take(effect)) ? runTakeEffect(data) : (data = _io.as.put(effect)) ? runPutEffect(data) : (data = _io.as.race(effect)) ? runRaceEffect(data) : (data = _io.as.call(effect)) ? runCallEffect(data.fn, data.args) : (data = _io.as.cps(effect)) ? runCPSEffect(data.fn, data.args) : (data = _io.as.fork(effect)) ? runForkEffect(data.task, data.args) : (data = _io.as.join(effect)) ? runJoinEffect(data) : /* resolve anything else */Promise.resolve(effect); | ||
var promise = _utils.is.array(effect) ? runParallelEffect(effect, effectId) : _utils.is.iterator(effect) ? proc(effect, subscribe, dispatch, monitor, effectId).done : (data = _io.as.take(effect)) ? runTakeEffect(data) : (data = _io.as.put(effect)) ? runPutEffect(data) : (data = _io.as.race(effect)) ? runRaceEffect(data, effectId) : (data = _io.as.call(effect)) ? runCallEffect(data.context, data.fn, data.args, effectId) : (data = _io.as.cps(effect)) ? runCPSEffect(data.fn, data.args) : (data = _io.as.fork(effect)) ? runForkEffect(data.task, data.args, effectId) : (data = _io.as.join(effect)) ? runJoinEffect(data) : (data = _io.as.cancel(effect)) ? runCancelEffect(data) : /* resolve anything else */Promise.resolve(effect); | ||
var def = (0, _utils.deferred)(); | ||
var isRunning = true; | ||
var completeWith = function completeWith(fn) { | ||
return function (outcome) { | ||
if (isRunning) { | ||
isRunning = false; | ||
fn(outcome); | ||
} | ||
}; | ||
}; | ||
promise.then(completeWith(def.resolve), completeWith(def.reject)); | ||
def.promise[CANCEL] = function (_ref) { | ||
var type = _ref.type; | ||
var origin = _ref.origin; | ||
if (isRunning) { | ||
isRunning = false; | ||
var error = new SagaCancellationException(type, name, origin); | ||
cancelPromise(promise, error); | ||
def.reject(error); | ||
} | ||
}; | ||
def.promise.then(function (result) { | ||
return monitor(monitorActions.effectResolved(effectId, result)); | ||
}, function (error) { | ||
return monitor(monitorActions.effectRejected(effectId, error)); | ||
}); | ||
return def.promise; | ||
} | ||
function runTakeEffect(pattern) { | ||
return new Promise(function (resolve) { | ||
deferredInput = { resolve: resolve, match: (0, _io.matcher)(pattern), pattern: pattern }; | ||
}); | ||
var def = (0, _utils.deferred)({ match: (0, _io.matcher)(pattern), pattern: pattern }); | ||
deferredInputs.push(def); | ||
var done = function done() { | ||
return (0, _utils.remove)(deferredInputs, def); | ||
}; | ||
def.promise.then(done, done); | ||
def.promise[CANCEL] = done; | ||
return def.promise; | ||
} | ||
function runPutEffect(action) { | ||
return Promise.resolve(1).then(function () { | ||
return (0, _utils.asap)(function () { | ||
return dispatch(action); | ||
@@ -91,5 +161,5 @@ }); | ||
function runCallEffect(fn, args) { | ||
var result = fn.apply(undefined, _toConsumableArray(args)); | ||
return !_utils.is.iterator(result) ? Promise.resolve(result) : proc(result, subscribe, dispatch); | ||
function runCallEffect(context, fn, args, effectId) { | ||
var result = fn.apply(context, args); | ||
return !_utils.is.iterator(result) ? Promise.resolve(result) : proc(result, subscribe, dispatch, monitor, effectId, fn.name).done; | ||
} | ||
@@ -105,7 +175,4 @@ | ||
function runForkEffect(task, args) { | ||
var _taskDesc; | ||
function runForkEffect(task, args, effectId) { | ||
var result = undefined, | ||
_generator = undefined, | ||
_iterator = undefined; | ||
@@ -120,3 +187,2 @@ var isFunc = _utils.is.func(task); | ||
if (_utils.is.iterator(result)) { | ||
_generator = task; | ||
_iterator = result; | ||
@@ -150,21 +216,38 @@ } | ||
var _done = proc(_iterator, subscribe, dispatch); | ||
var taskDesc = (_taskDesc = {}, _defineProperty(_taskDesc, _utils.TASK, true), _defineProperty(_taskDesc, '_generator', _generator), _defineProperty(_taskDesc, '_iterator', _iterator), _defineProperty(_taskDesc, '_done', _done), _defineProperty(_taskDesc, 'name', isFunc ? task.name : 'anonymous'), _defineProperty(_taskDesc, 'isRunning', function isRunning() { | ||
return _iterator._isRunning; | ||
}), _defineProperty(_taskDesc, 'result', function result() { | ||
return _iterator._result; | ||
}), _defineProperty(_taskDesc, 'error', function error() { | ||
return _iterator._error; | ||
}), _taskDesc); | ||
return Promise.resolve(taskDesc); | ||
var name = isFunc ? task.name : 'anonymous'; | ||
return Promise.resolve(proc(_iterator, subscribe, dispatch, monitor, effectId, name, true)); | ||
} | ||
function runJoinEffect(task) { | ||
return task._done; | ||
return task.done; | ||
} | ||
function runRaceEffect(effects) { | ||
return Promise.race(Object.keys(effects).map(function (key) { | ||
return runEffect(effects[key]).then(function (result) { | ||
function runCancelEffect(task) { | ||
task.done[CANCEL](new SagaCancellationException(MANUAL_CANCEL, '', name)); | ||
return Promise.resolve(); | ||
} | ||
function runParallelEffect(effects, effectId) { | ||
var promises = effects.map(function (eff) { | ||
return runEffect(eff, effectId); | ||
}); | ||
var ret = Promise.all(promises); | ||
ret[CANCEL] = function (error) { | ||
promises.forEach(function (p) { | ||
return cancelPromise(p, error); | ||
}); | ||
}; | ||
ret.catch(function () { | ||
ret[CANCEL](new SagaCancellationException(PARALLEL_AUTO_CANCEL, name, name)); | ||
}); | ||
return ret; | ||
} | ||
function runRaceEffect(effects, effectId) { | ||
var promises = []; | ||
var retP = Promise.race(Object.keys(effects).map(function (key) { | ||
var promise = runEffect(effects[key], effectId, key); | ||
promises.push(promise); | ||
return promise.then(function (result) { | ||
return _defineProperty({}, key, result); | ||
@@ -175,3 +258,31 @@ }, function (error) { | ||
})); | ||
retP[CANCEL] = function (error) { | ||
promises.forEach(function (p) { | ||
return cancelPromise(p, error); | ||
}); | ||
}; | ||
var done = function done() { | ||
return retP[CANCEL](new SagaCancellationException(RACE_AUTO_CANCEL, name, name)); | ||
}; | ||
retP.then(done, done); | ||
return retP; | ||
} | ||
function newTask(id, name, iterator, done, forked) { | ||
var _ref3; | ||
return _ref3 = {}, _defineProperty(_ref3, _utils.TASK, true), _defineProperty(_ref3, 'id', id), _defineProperty(_ref3, 'name', name), _defineProperty(_ref3, 'done', done), _defineProperty(_ref3, 'forked', forked), _defineProperty(_ref3, 'isRunning', function isRunning() { | ||
return iterator._isRunning; | ||
}), _defineProperty(_ref3, 'getResult', function getResult() { | ||
return iterator._result; | ||
}), _defineProperty(_ref3, 'getError', function getError() { | ||
return iterator._error; | ||
}), _ref3; | ||
} | ||
function cancelPromise(promise, error) { | ||
if (promise[CANCEL]) promise[CANCEL](error); | ||
} | ||
} |
'use strict'; | ||
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; | ||
Object.defineProperty(exports, "__esModule", { | ||
@@ -8,2 +10,6 @@ value: true | ||
exports.remove = remove; | ||
exports.deferred = deferred; | ||
exports.arrayOfDeffered = arrayOfDeffered; | ||
exports.autoInc = autoInc; | ||
exports.asap = asap; | ||
@@ -52,2 +58,5 @@ var _marked = [sampleGen].map(regeneratorRuntime.mark); | ||
return it && typeof it.throw === 'function'; | ||
}, | ||
task: function task(it) { | ||
return it && it[TASK]; | ||
} | ||
@@ -59,2 +68,36 @@ }; | ||
if (index >= 0) array.splice(index, 1); | ||
} | ||
function deferred() { | ||
var props = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0]; | ||
var def = _extends({}, props); | ||
var promise = new Promise(function (resolve, reject) { | ||
def.resolve = resolve; | ||
def.reject = reject; | ||
}); | ||
def.promise = promise; | ||
return def; | ||
} | ||
function arrayOfDeffered(length) { | ||
var arr = []; | ||
for (var i = 0; i < length; i++) { | ||
arr.push(deferred()); | ||
} | ||
return arr; | ||
} | ||
function autoInc() { | ||
var seed = arguments.length <= 0 || arguments[0] === undefined ? 0 : arguments[0]; | ||
return function () { | ||
return ++seed; | ||
}; | ||
} | ||
function asap(action) { | ||
return Promise.resolve(1).then(function () { | ||
return action(); | ||
}); | ||
} |
{ | ||
"name": "redux-saga", | ||
"version": "0.3.3", | ||
"version": "0.4.0", | ||
"description": "Saga middleware for Redux to handle Side Effects", | ||
@@ -11,18 +11,16 @@ "main": "lib/index.js", | ||
"compile": "rimraf lib && babel -d lib/ src/", | ||
"prepublish": "npm run check && npm run compile", | ||
"build:umd:dev": "webpack src/index.js dist/redux-saga.js --config webpack.config.dev.js", | ||
"build:umd:prod": "webpack src/index.js dist/redux-saga.min.js --config webpack.config.prod.js", | ||
"build:umd": "rimraf dist && npm run build:umd:dev && npm run build:umd:prod", | ||
"prepublish": "npm run check && npm run compile && npm run build:umd", | ||
"counter": "budo examples/counter/src/main.js:build.js --dir examples/counter --verbose --live -- -t babelify", | ||
"build-counter": "browserify --debug examples/counter/src/main.js -t babelify --outfile examples/counter/build.js", | ||
"test-counter": "babel-node examples/counter/test/sagas.js | tap-spec", | ||
"shop": "budo examples/shopping-cart/src/main.js:build.js --dir examples/shopping-cart --verbose --live -- -t babelify", | ||
"build-shop": "browserify --debug examples/shopping-cart/src/main.js -t babelify --outfile examples/shopping-cart/build.js", | ||
"test-shop": "babel-node examples/shopping-cart/test/sagas.js | tap-spec", | ||
"async": "budo examples/async/src/main.js:build.js --dir examples/async --verbose --live -- -t babelify", | ||
"build-async": "browserify --debug examples/async/src/main.js -t babelify --outfile examples/async/build.js", | ||
"build-examples": "npm run build-counter && npm run build-shop && npm run build-async", | ||
"test-examples": "npm run test-counter && npm run test-shop", | ||
"real-world": "node examples/real-world/server.js" | ||
@@ -51,4 +49,6 @@ }, | ||
"babel-cli": "^6.1.18", | ||
"babel-core": "6.4.0", | ||
"babel-eslint": "^4.1.5", | ||
"babel-polyfill": "^6.2.0", | ||
"babel-loader": "6.2.1", | ||
"babel-polyfill": "6.3.14", | ||
"babel-preset-es2015": "^6.1.18", | ||
@@ -67,4 +67,14 @@ "babel-preset-react": "^6.1.18", | ||
"tap-spec": "^4.1.1", | ||
"tape": "^4.2.2" | ||
} | ||
"tape": "^4.2.2", | ||
"webpack": "1.12.10" | ||
}, | ||
"npmName": "redux-saga", | ||
"npmFileMap": [ | ||
{ | ||
"basePath": "/dist/", | ||
"files": [ | ||
"*.js" | ||
] | ||
} | ||
] | ||
} |
175
README.md
@@ -0,1 +1,5 @@ | ||
# redux-saga | ||
[![npm version](https://img.shields.io/npm/v/redux-saga.svg?style=flat-square)](https://www.npmjs.com/package/redux-saga) | ||
An alternative Side Effect model for Redux applications. Instead of dispatching thunks | ||
@@ -41,3 +45,5 @@ which get handled by the redux-thunk middleware. You create *Sagas* to gather all your | ||
- [Non blocking calls with fork/join](#non-blocking-calls-with-forkjoin) | ||
- [Task cancellation](#task-cancellation) | ||
- [Building examples from sources](#building-examples-from-sources) | ||
- [Using umd build in the browser](#using-umd-build-in-the-browser) | ||
@@ -207,3 +213,3 @@ #Getting started | ||
const iterator = fetchSaga() | ||
assert.deepEqual(iterator.next().value, call(fetch, '/products') // expects a call(...) value | ||
assert.deepEqual(iterator.next().value, call(fetch, '/products')) // expects a call(...) value | ||
``` | ||
@@ -501,18 +507,159 @@ | ||
// non blocking call | ||
const task = yield fork(subtask, ...args) | ||
function* child() { ... } | ||
// ... later | ||
// now a blocking call, will resume with the outcome of task | ||
const result = yield join(task) | ||
function *parent() { | ||
// non blocking call | ||
const task = yield fork(subtask, ...args) | ||
// ... later | ||
// now a blocking call, will resume with the outcome of task | ||
const result = yield join(task) | ||
} | ||
``` | ||
You can also ask a Task if it's still running | ||
the task object exposes some useful methods | ||
<table> | ||
<tr> | ||
<th>method</th> | ||
<th>return value</th> | ||
</tr> | ||
<tr> | ||
<td>task.isRunning()</td> | ||
<td>true if the task hasn't yet returned or throwed an error</td> | ||
</tr> | ||
<tr> | ||
<td>task.result()</td> | ||
<td>task return value. `undefined` if task is still running</td> | ||
</tr> | ||
<tr> | ||
<td>task.error()</td> | ||
<td>task thrown error. `undefined` if task is still running</td> | ||
</tr> | ||
<tr> | ||
<td>task.done</td> | ||
<td> | ||
a Promise which is either | ||
<ul> | ||
<li>resolved with task's return value</li> | ||
<li>rejected with task's thrown error</li> | ||
</ul> | ||
</td> | ||
</tr> | ||
</table> | ||
#Task cancellation | ||
Once a task is forked, you can abort its execution using `yield cancel(task)`. Cancelling | ||
a running task will throw a `SagaCancellationException` inside it. | ||
To see how it works, let's consider a simple example. A background sync which can be | ||
started/stopped by some UI commands. Upon receiving a `START_BACKGROUND_SYNC` action, | ||
we fork a background task that will periodically sync some data from a remote server. | ||
The task will execute continually until a `STOP_BACKGROUND_SYNC` action is triggered. | ||
Then we cancel the background task and wait again for the next `START_BACKGROUND_SYNC` action. | ||
```javascript | ||
// attention, we don't use yield | ||
const stillRunning = task.isRunning() | ||
import { take, put, call, fork, cancel, SagaCancellationException } from 'redux-saga' | ||
import actions from 'somewhere' | ||
import { someApi, delay } from 'somewhere' | ||
function* bgSync() { | ||
try { | ||
while(true) { | ||
yield put(actions.requestStart()) | ||
const result = yield call(someApi) | ||
yield put(actions.requestSuccess(result)) | ||
yield call(delay, 5000) | ||
} | ||
} catch(error) { | ||
if(error instanceof SagaCancellationException) | ||
yield put(actions.requestFailure('Sync cancelled!')) | ||
} | ||
} | ||
function* main() { | ||
while( yield take(START_BACKGROUND_SYNC) ) { | ||
// starts the task in the background | ||
const bgSyncTask = yield fork(bgSync) | ||
// wait for the user stop action | ||
yield take(STOP_BACKGROUND_SYNC) | ||
// user clicked stop. cancel the background task | ||
// this will throw a SagaCancellationException into task | ||
yield cancel(bgSyncTask) | ||
} | ||
} | ||
``` | ||
`yield cancel(bgSyncTask)` will throw a `SagaCancellationException` | ||
inside the currently running task. In the above example, the exception is caught by | ||
`bgSync`. Otherwise, it will propagate up to `main`. And it if `main` doesn't handle it | ||
then it will bubble up the call chain, just as normal JavaScript errors bubble up the | ||
call chain of synchronous functions. | ||
Cancelling a running task will also cancel the current effect where the task is blocked | ||
at the moment of cancellation. | ||
For example, suppose that at a certain point in application lifetime, we had this pending call chain | ||
```javascript | ||
function* main() { | ||
const task = yield fork(subtask) | ||
... | ||
// later | ||
yield cancel(task) | ||
} | ||
function* subtask() { | ||
... | ||
yield call(subtask2) // currently blocked on this call | ||
... | ||
} | ||
function* subtask2() { | ||
... | ||
yield call(someApi) // currently blocked on this all | ||
... | ||
} | ||
``` | ||
`yield cancel(task)` will trigger a cancellation on `subtask`, which in turn will trigger | ||
a cancellation on `subtask2`. A `SagaCancellationException` will be thrown inside `subtask2`, | ||
then another `SagaCancellationException` will be thrown inside `subtask`. If `subtask` | ||
omits to handle the cancellation exception, it will propagate up to `main`. | ||
The main purpose of the cancellation exception is to allow cancelled tasks to perform any | ||
cleanup logic. So we wont leave the application in an inconsistent state. In the above example | ||
of background sync, by catching the cancellation exception, `bgSync` is able to dispatch a | ||
`requestFailure` action to the store. Otherwise, the store could be left in a inconsistent | ||
state (e.g. waiting for the result of a pending request) | ||
>It's important to remember that `yield cancel(task)` doesn't wait for the cancelled task | ||
to finish (i.e. to perform its catch block). The cancel effect behave like fork. It returns | ||
as soon as the cancel was initiated. | ||
>Once cancelled, a task should normally return as soon as it finishes its cleanup logic. | ||
In some cases, the cleanup logic could involve some async operations, but the cancelled | ||
task lives now as a separate process, and there is no way for it to rejoin the main | ||
control flow (except dispatching actions other tasks via the Redux store. However | ||
this will lead to complicated control flows that ae hard to reason about. It's always preferable | ||
to terminate a cancelled task asap). | ||
##Automatic cancellation | ||
Besides manual cancellation. There are cases where cancellation is triggered automatically | ||
1- In a `race` effect. All race competitors, except the winner, are automatically cancelled. | ||
2- In a parallel effect (`yield [...]`). The parallel effect is rejected as soon as one of the | ||
sub-effects is rejected (as implied by Promise.all). In this case, all the other sub-effects | ||
are automatically cancelled. | ||
Unlike in manual cancellations, unhandled cancellation exceptions are not propagated to the actual | ||
saga running the race/parallel effect. Nevertheless, a warning is logged into the console in case | ||
a cancelled task omitted to handle a cancellation exception. | ||
#Building examples from sources | ||
@@ -578,1 +725,11 @@ | ||
``` | ||
#Using umd build in the browser | ||
There's an **umd** build of `redux-saga` available in `dist/` folder. Using the umd build `redux-saga` is available as `ReduxSaga` in the window object. | ||
The umd version is useful if you don't use webpack or browserify, you can access it directly from [npmcdn](npmcdn.com). | ||
The following builds are available: | ||
[https://npmcdn.com/redux-saga/dist/redux-saga.js](https://npmcdn.com/redux-saga/dist/redux-saga.js) | ||
[https://npmcdn.com/redux-saga/dist/redux-saga.min.js](https://npmcdn.com/redux-saga/dist/redux-saga.min.js) | ||
**Important!** If the browser you are targeting doesn't support _es2015 generators_ you must provide a valid polyfill, for example the one provided by *babel*: [browser-polyfill.min.js](https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.25/browser-polyfill.min.js). The polyfill must be imported before **redux-saga**. |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
108723
25
1909
732
20
1