Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

redux-saga

Package Overview
Dependencies
Maintainers
1
Versions
74
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

redux-saga - npm Package Compare versions

Comparing version 0.2.2 to 0.3.0

node_modules/history/CHANGES.md

54

lib/index.js

@@ -6,10 +6,51 @@ 'use strict';

});
exports.SAGA_NOT_A_GENERATOR_ERROR = undefined;
exports.SAGA_NOT_A_GENERATOR_ERROR = exports.join = exports.fork = exports.cps = exports.call = exports.race = exports.put = exports.take = undefined;
var _utils = require('./utils');
var _io = require('./io');
var _io2 = _interopRequireDefault(_io);
Object.defineProperty(exports, 'take', {
enumerable: true,
get: function get() {
return _io.take;
}
});
Object.defineProperty(exports, 'put', {
enumerable: true,
get: function get() {
return _io.put;
}
});
Object.defineProperty(exports, 'race', {
enumerable: true,
get: function get() {
return _io.race;
}
});
Object.defineProperty(exports, 'call', {
enumerable: true,
get: function get() {
return _io.call;
}
});
Object.defineProperty(exports, 'cps', {
enumerable: true,
get: function get() {
return _io.cps;
}
});
Object.defineProperty(exports, 'fork', {
enumerable: true,
get: function get() {
return _io.fork;
}
});
Object.defineProperty(exports, 'join', {
enumerable: true,
get: function get() {
return _io.join;
}
});
var _utils = require('./utils');
var _proc = require('./proc');

@@ -37,6 +78,3 @@

// wait for the current tick, to let other middlewares (e.g. logger) run
Promise.resolve(1).then(function () {
(0, _proc2.default)(saga(_io2.default, getState), subscribe, dispatch, saga.name);
});
(0, _proc2.default)(saga(getState), subscribe, dispatch, saga.name);
});

@@ -43,0 +81,0 @@

90

lib/io.js

@@ -1,2 +0,2 @@

'use strict';
"use strict";

@@ -6,10 +6,23 @@ Object.defineProperty(exports, "__esModule", {

});
exports.as = undefined;
exports.as = exports.JOIN_ARG_ERROR = exports.FORK_ARG_ERROR = exports.CPS_FUNCTION_ARG_ERROR = exports.CALL_FUNCTION_ARG_ERROR = undefined;
exports.matcher = matcher;
exports.take = take;
exports.put = put;
exports.race = race;
exports.call = call;
exports.cps = cps;
exports.fork = fork;
exports.join = join;
var _utils = require('./utils');
var _utils = require("./utils");
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; }
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 IO = Symbol('IO');
var TAKE = 'TAKE';

@@ -20,2 +33,4 @@ var PUT = 'PUT';

var CPS = 'CPS';
var FORK = 'FORK';
var JOIN = 'JOIN';

@@ -55,27 +70,46 @@ var effect = function effect(type, payload) {

exports.default = {
take: function take(pattern) {
return effect(TAKE, _utils.is.undef(pattern) ? '*' : pattern);
},
put: function put(ac) {
return effect(PUT, ac);
},
race: function race(effects) {
return effect(RACE, effects);
},
call: function call(fn) {
for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
args[_key - 1] = arguments[_key];
}
function take(pattern) {
return effect(TAKE, _utils.is.undef(pattern) ? '*' : pattern);
}
return effect(CALL, { fn: fn, args: args });
},
cps: function cps(fn) {
for (var _len2 = arguments.length, args = Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
args[_key2 - 1] = arguments[_key2];
}
function put(action) {
return effect(PUT, action);
}
return effect(CPS, { fn: fn, args: args });
function race(effects) {
return effect(RACE, effects);
}
function call(fn) {
for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
args[_key - 1] = arguments[_key];
}
};
(0, _utils.check)(fn, _utils.is.func, CALL_FUNCTION_ARG_ERROR);
return effect(CALL, { fn: fn, args: args });
}
function cps(fn) {
for (var _len2 = arguments.length, args = Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
args[_key2 - 1] = arguments[_key2];
}
(0, _utils.check)(fn, _utils.is.func, CPS_FUNCTION_ARG_ERROR);
return effect(CPS, { fn: fn, args: args });
}
function fork(task) {
for (var _len3 = arguments.length, args = Array(_len3 > 1 ? _len3 - 1 : 0), _key3 = 1; _key3 < _len3; _key3++) {
args[_key3 - 1] = arguments[_key3];
}
return effect(FORK, { task: task, args: args });
}
function join(taskDesc) {
if (!taskDesc[_utils.TASK]) throw new Error(JOIN_ARG_ERROR);
return effect(JOIN, taskDesc);
}
var as = exports.as = {

@@ -96,3 +130,9 @@ take: function take(effect) {

return effect && effect[IO] && effect[CPS];
},
fork: function fork(effect) {
return effect && effect[IO] && effect[FORK];
},
join: function join(effect) {
return effect && effect[IO] && effect[JOIN];
}
};

@@ -17,3 +17,3 @@ 'use strict';

var NOT_ITERATOR_ERROR = exports.NOT_ITERATOR_ERROR = "proc argument must be an iterator";
var NOT_ITERATOR_ERROR = exports.NOT_ITERATOR_ERROR = "proc first argument must be an iterator";

@@ -26,3 +26,3 @@ function proc(iterator) {

if (!_utils.is.iterator(iterator)) throw new Error(NOT_ITERATOR_ERROR);
(0, _utils.check)(iterator, _utils.is.iterator, NOT_ITERATOR_ERROR);

@@ -42,2 +42,3 @@ var deferredInput = undefined,

next();
iterator._isRunning = true;
return endP;

@@ -59,2 +60,4 @@

//console.log('return', name, result.value)
iterator._isRunning = false;
iterator._result = result.value;
unsubscribe();

@@ -65,2 +68,4 @@ deferredEnd.resolve(result.value);

//console.log('catch', name, err)
iterator._isRunning = false;
iterator._error = err;
unsubscribe();

@@ -72,6 +77,4 @@ deferredEnd.reject(err);

function runEffect(effect) {
var _data;
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)) ? Promise.resolve(dispatch(data)) : (data = _io.as.race(effect)) ? runRaceEffect(data) : (data = _io.as.call(effect)) ? (_data = data).fn.apply(_data, _toConsumableArray(data.args)) : (data = _io.as.cps(effect)) ? runCPSEffect(data) : /* resolve anything else */Promise.resolve(effect);
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)) ? Promise.resolve(dispatch(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);
}

@@ -85,10 +88,63 @@

function runCPSEffect(cps) {
function runCallEffect(fn, args) {
return !_utils.is.generator(fn) ? Promise.resolve(fn.apply(undefined, _toConsumableArray(args))) : proc(fn.apply(undefined, _toConsumableArray(args)), subscribe, dispatch);
}
function runCPSEffect(fn, args) {
return new Promise(function (resolve, reject) {
cps.fn.apply(cps, [].concat(_toConsumableArray(cps.args), [function (err, res) {
fn.apply(undefined, _toConsumableArray(args.concat(function (err, res) {
return _utils.is.undef(err) ? resolve(res) : reject(err);
}]));
})));
});
}
function runForkEffect(task, args) {
var _taskDesc;
var _generator = undefined,
_iterator = undefined;
if (_utils.is.generator(task)) {
_generator = task;
_iterator = _generator.apply(undefined, _toConsumableArray(args));
} else if (_utils.is.iterator(task)) {
// directly forking an iterator
_iterator = task;
} else {
//simple effect: wrap in a generator
_iterator = regeneratorRuntime.mark(function _callee() {
return regeneratorRuntime.wrap(function _callee$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
_context.next = 2;
return _utils.is.func(task) ? task.apply(undefined, _toConsumableArray(args)) : task;
case 2:
return _context.abrupt('return', _context.sent);
case 3:
case 'end':
return _context.stop();
}
}
}, _callee, this);
})();
}
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', _generator && _generator.name), _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);
}
function runJoinEffect(task) {
return task._done;
}
function runRaceEffect(effects) {

@@ -95,0 +151,0 @@ return Promise.race(Object.keys(effects).map(function (key) {

@@ -6,3 +6,5 @@ 'use strict';

});
exports.check = check;
exports.remove = remove;
var TASK = exports.TASK = Symbol('TASK');
var kTrue = exports.kTrue = function kTrue() {

@@ -13,2 +15,6 @@ return true;

function check(value, predicate, error) {
if (!predicate(value)) throw new Error(error);
}
var is = exports.is = {

@@ -26,3 +32,3 @@ undef: function undef(v) {

generator: function generator(g) {
return g.constructor.name === 'GeneratorFunction';
return is.func(g) && g.constructor.name === 'GeneratorFunction';
},

@@ -29,0 +35,0 @@ iterator: function iterator(it) {

{
"name": "redux-saga",
"version": "0.2.2",
"version": "0.3.0",
"description": "Saga middleware for Redux to handle Side Effects",

@@ -16,3 +16,6 @@ "main": "lib/index.js",

"test-shop": "babel-node examples/shopping-cart/test/sagas.js | tap-spec",
"build-async": "browserify examples/async/src/main.js -t babelify --outfile examples/async/build.js"
"build-async": "browserify 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"
},

@@ -19,0 +22,0 @@ "repository": {

@@ -9,12 +9,29 @@ An alternative Side Effect model for Redux applications. Instead of dispatching thunks

- Sagas are responsible of orchestrating complex/asynchronous operations (side effects or actions).
This includes simple side effects which react to one action (e.g. send a request on each button click),
but also complex operations that span across multiple actions (e.g. User onBoarding, Wizard
dialogs, asynchronous Game rules ...).
- Sagas are responsible of orchestrating complex/asynchronous operations.
Sagas are created using Generator functions.
A Saga is a generator function that takes user actions as inputs and may yield Side Effects
(e.g. server updates, navigation, store actions ...) as output.
>This middleware is not only about handling asynchronous flow; If all that matters is simplifying
asynchronous control flow, we could simply use async/await with some promise middleware.
What the The middleware provides is
- A composable abstraction **Effect**: Waiting for an action, triggering State updates (by dispatching
actions to the store), calling a remote service are all different forms of Effects. A Saga compose those
Effects using familiar control flow constructs (if, while, for, try/catch).
- The Saga is itself an Effect that can be combined with other Effects using Effect combinators (parallel or
race) and called from inside other Sagas, which all the power of Subroutines and
[Structured Programming](https://en.wikipedia.org/wiki/Structured_programming)
- Effects may be yielded declaratively, i.e. you yield a description of the Effect that
will executed by the middleware. This makes your operational logic inside Generators fully testable.
- You can implement complex operations with logic that spans across multiple actions (e.g. User onBoarding, Wizard
dialogs, complex Game rules ...), which are not trivial to express using redux-thunk or other effects
middlewares that can be only fired from Action Creators.
- [Getting started](#getting-started)
- [How is this different from other asynchronous middlewares](#how-does-it-works)
- [Declarative Effects](#declarative-effects)

@@ -25,2 +42,3 @@ - [Error handling](#error-handling)

- [Composing Sagas](#composing-sagas)
- [Concurrent tasks tasks with fork/join ](#concurrent-tasks-with-forkjoin)
- [Building from sources](#building-from-sources)

@@ -37,4 +55,5 @@

```javascript
import { take, call } from 'redux-saga'
// sagas/index.js
function* incrementAsync(io) {
function* incrementAsync() {

@@ -44,3 +63,3 @@ while(true) {

// wait for each INCREMENT_ASYNC action
const nextAction = yield io.take(INCREMENT_ASYNC)
const nextAction = yield take(INCREMENT_ASYNC)

@@ -51,3 +70,3 @@ // call delay : Number -> Promise

// dispatch INCREMENT_COUNTER
yield io.put( increment() )
yield put( increment() )
}

@@ -76,36 +95,50 @@

In the above example we created an `incrementAsync` Saga to handle all `INCREMENT_ASYNC` actions.
The Generator function uses `yield io.take(action)` to wait asynchronously for the next action.
The middleware handles action queries from the Saga by pausing the Generator until an action matching
the query happens. Then it resumes the Generator with the matching action.
#How is this different from other asynchronous middlewares
After receiving the wanted action, the Saga triggers a call to `delay(1000)`, which in our example
returns a Promise that will be resolved after 1 second. Again, the middleware will pause the Generator
until the yielded promise is resolved.
In the above example we created an `incrementAsync` Saga. The call `yield take(action)` is a
typical illustration on how Saga works.
After the 1 second delay, the Saga dispatches an `INCREMENT_COUNTER` action using the `io.put(action)`
method. And as for the 2 precedent statements, the Saga is resumed after resolving the result of the
action dispatch (which will happen immediately if the Store's dispatch function returns a normal value,
but may be later if the dispatch result is a Promise).
Typically, actual middlewares handle some Effects triggered by an Action Creator. for example,
redux-thunk handles *thunks* by calling the triggered thunk providing it with `(getState, dispatch)`,
redux-promise handles Promises by dispatching their resolved values, redux-gen handles generators by
dispatching all yielded actions to the store. The common thing is that all those middlewares shares the
same 'call on each action' pattern. They will be called again and again each time an action happens,
i.e. they are scoped by the *root action* which triggered them.
The Generator uses an infinite loop `while(true)` which means it will stay alive for all the application
lifetime. But you can also create Sagas that last only for a limited amount of time. For example, the
following Saga will wait for the first 3 `INCREMENT_COUNTER` actions, triggers a `showCongratulation()`
action and then finishes.
Sagas works differently, they are not fired from within Action Creators but are started with your
application and choose what user actions to watch for. They are a sort of daemon tasks that run in
the background and choose their own logic of progression. In the example above, `incrementAsync` *pulls*
the `INCREMENT_ASYNC` action, the `yield take(action)` is a *blocking call*, which means the Saga
will not progress until it receives a matching action.
After receiving the queried action, the Saga triggers a call to `delay(1000)`, which in our example
returns a Promise that will be resolved after 1 second. Again, this is a blocking call, so the Saga
will wait for 1 second before continuing on (a better way is `io.call(delay, 1000)`, see section
on declarative Effects).
After the 1 second delay, the Saga dispatches an `INCREMENT_COUNTER` action using the `put(action)`
function. Here also, the Saga will wait for the dispatch result. If the dispatch call returns
a normal value, the Saga resumes *immediately*, but if the result value is a Promise then the
Saga will wait until the Promise is resolved (or rejected).
To generalize, waiting for the next action (`yield take(MY_ACTION)`), for a the future result of
a function (`yield delay(1000)`) or for the result of a dispatch (`yield put(myAction())`) are all
different expressions of the same concept: *yielding a side effect*. Basically this is how Sagas
work.
Note also how `incrementAsync` uses an infinite loop `while(true)` which means it will stay alive
for all the application lifetime. You can also create Sagas that last only for a limited amount of
time. For example, the following Saga will wait for the first 3 `INCREMENT_COUNTER` actions,
triggers a `showCongratulation()` action and then finishes.
```javascript
function* onBoarding(io) {
function* onBoarding() {
for(let i = 0; i < 3; i++)
yield io.take(INCREMENT_COUNTER)
yield take(INCREMENT_COUNTER)
yield io.put( showCongratulation() )
yield put( showCongratulation() )
}
```
The basic idea, is that you use the `yield` operator every time you want to trigger a Side Effect. So
to be more accurate, A Saga is a Generator function that yields Side Effects, wait for their results
then resume with the responses. Everything you `yield` is considered an Effect: waiting for an action,
triggering a server request, dispatching an action to the store...
#Declarative Effects

@@ -116,9 +149,9 @@

```javascript
function* fetchSaga(io) {
function* fetchSaga() {
// returns a Promise that will resolve with the GET response
const products = yield fetch('/products')
// dispatch a RECEIVE_PRODUCTS action
yield io.put( receiveProducts(products) )
const products = yield fetch('/products')
// dispatch a RECEIVE_PRODUCTS action
yield put( receiveProducts(products) )
}

@@ -133,5 +166,3 @@ ```

```javascript
import io from 'redux-saga/io' // exposed for testing purposes
const iterator = fetchSaga(io)
const iterator = fetchSaga()
assert.deepEqual( iterator.next().value, ?? ) // what do we expect ?

@@ -142,7 +173,7 @@ ```

`fetch('/products')`. Executing the real service during tests is not a viable nor a practical approach, so we have to *mock* the
fetch service, i.e. we'll have to replace the real `fetch` method with a fake one which doesn't actually run the
GET request but only checks that w've called `fetch` with the right arguments (`'/products'` in our case).
fetch service, i.e. we'll have to replace the real `fetch` method with a fake one which doesn't actually run the
GET request but only checks that we've called `fetch` with the right arguments (`'/products'` in our case).
Mocks makes testing more difficult and less reliable. On the other hand, functions which returns simple values are
easier to test, we can use a simple `equal()` to check the result.This is the way to write the most relibale tests.
Mocks makes testing more difficult and less reliable. On the other hand, functions which returns simple values are
easier to test, we can use a simple `equal()` to check the result.This is the way to write the most reliable tests.

@@ -158,3 +189,3 @@ Not convinced ? I encourage you to read this [Eric Elliott' article]

What we need actually, is just to make sure the `fetchSaga` yields a call with the right function and the right
What we need actually, is just to make sure the `fetchSaga` yields a call with the right function and the right
arguments. For this reason, the library provides some declarative ways to yield Side Effects while still making it

@@ -164,11 +195,13 @@ easy to test the Saga logic

```javascript
function* fetchSaga(io) {
const products = yield io.call( fetch, '/products' ) // don't run the effect
import { call } from 'redux-saga'
function* fetchSaga() {
const products = yield call( fetch, '/products' ) // don't run the effect
}
```
We're using now `io.call(fn, ...args)` function. **The difference from the precedent example is that now we're not
executing the fetch call immedieately, instead, `io.call` creates a description of the effect**. Just as in
Redux you use action creators to create a plain object descibing the action that will get executed by the Store,
`io.call` creates a plain object describing the function call. The redux-saga middleware takes care of executing
We're using now `call(fn, ...args)` function. **The difference from the precedent example is that now we're not
executing the fetch call immediately, instead, `call` creates a description of the effect**. Just as in
Redux you use action creators to create a plain object describing the action that will get executed by the Store,
`io.call` creates a plain object describing the function call. The redux-saga middleware takes care of executing
the function call and resuming the generator with the resolved response.

@@ -180,6 +213,6 @@

```javascript
import io from 'redux-saga/io' // exposed for testing purposes
import { call } from 'redux-saga'
const iterator = fetchSaga(io)
assert.deepEqual(iterator.next().value, io.call(fetch, '/products') // expects a io.call(...) value
const iterator = fetchSaga()
assert.deepEqual(iterator.next().value, call(fetch, '/products') // expects a call(...) value
```

@@ -194,17 +227,19 @@

The `io.call` method is well suited for functions which return Promise results. Another function
`io.cps` can be used to handle Node style functions (e.g. `fn(...args, callback)` where `callback`
The `call` method is well suited for functions which return Promise results. Another function
`cps` can be used to handle Node style functions (e.g. `fn(...args, callback)` where `callback`
is of the form `(error, result) => ()`). For example
```javascript
const content = yield io.cps(readFile, '/path/to/file')
import { cps } from 'redux-saga'
const content = yield cps(readFile, '/path/to/file')
```
and of course you can test it just like you test io.call
and of course you can test it just like you test call
```javascript
import io from 'redux-saga/io' // exposed for testing purposes
import { cps } from 'redux-saga'
const iterator = fetchSaga(io)
assert.deepEqual(iterator.next().value, io.cps(readFile, '/path/to/file') )
const iterator = fetchSaga()
assert.deepEqual(iterator.next().value, cps(readFile, '/path/to/file') )
```

@@ -218,11 +253,11 @@

```javascript
function* checkout(io, getState) {
function* checkout(getState) {
while( yield io.take(types.CHECKOUT_REQUEST) ) {
while( yield take(types.CHECKOUT_REQUEST) ) {
try {
const cart = getState().cart
yield io.call(api.buyProducts, cart)
yield io.put(actions.checkoutSuccess(cart))
yield call(api.buyProducts, cart)
yield put(actions.checkoutSuccess(cart))
} catch(error) {
yield io.put(actions.checkoutFailure(error))
yield put(actions.checkoutFailure(error))
}

@@ -233,2 +268,26 @@ }

Of course you're not forced to handle you API errors inside try/catch blocks, you can also make
your API service return a normal value with some error flag on it
```javascript
function buyProducts(cart) {
return doPost(...)
.then(result => {result})
.catch(error => {error})
}
function* checkout(getState) {
while( yield take(types.CHECKOUT_REQUEST) ) {
try {
const cart = getState().cart
const {result, error} = yield call(api.buyProducts, cart)
if(!error)
yield put(actions.checkoutSuccess(result))
else
yield put(actions.checkoutFailure(error))
}
}
```
#Effect Combinators

@@ -241,4 +300,4 @@

// Wrong, effects will be executed in sequence
const users = yield io.call(fetch, '/users'),
repose = yield io.call(fetch, '/repose')
const users = yield call(fetch, '/users'),
repose = yield call(fetch, '/repose')
```

@@ -249,86 +308,60 @@

```javascript
import { call, race } from 'redux-saga'
// correct, effects will get executed in parallel
const [users, repose] = yield [
io.call(fetch, '/users'),
io.call(fetch, '/repose')
call(fetch, '/users'),
call(fetch, '/repose')
]
```
When we yield an array of effects, the Generator is paused until all the effects are resolved (or as soon as
one is rejected, just like specified by the `Promise.all` method).
When we yield an array of effects, the Generator is blocked until all the effects are resolved (or as soon as
one is rejected, just like how `Promise.all` behaves).
Sometimes we also need a behavior similar to `Promise.race`:getting the first resolved effect from
multiple ones. The method `io.race` offers a declarative way of triggering a race between effects.
Sometimes when to start multiple tasks in parallel, we don't want to wait for all of them, we just need
to get the *winner*: the first one that resolves (or rejects). The `race` function offers a way of
triggering a race between multiple effects.
The following shows a Saga that triggers a `showCongratulation` message if the user triggers 3
`INCREMENT_COUNTER` actions with less than 5 seconds between 2 actions.
The following sample shows a Saga that triggers a remote fetch request, and constrain the response with a
1 second timeout.
```javascript
function* onBoarding(io) {
let nbIncrements = 0
while(nbIncrements < 3) {
// wait for INCREMENT_COUNTER with a timeout of 5 seconds
const winner = yield io.race({
increment : io.take(INCREMENT_COUNTER),
timeout : io.call(delay, 5000)
function* fetchPostsWithTimeout() {
while( yield take(FETCH_POSTS) ) {
// starts a race between 2 effects
const {posts, timeout} = race({
posts : call(fetchApi, '/posts'),
timeout : call(delay, 1000)
})
if(winner.increment)
nbIncrements++
if(result)
put( actions.receivePosts(posts) )
else
nbIncrements = 0
put( actions.timeoutError() )
}
yield io.put(showCongratulation())
}
```
Note the Saga has an internal state, but that has nothing to do with the state inside the Store,
this is a local state to the Saga function used to control the flow of actions.
If by some means you want to view this state (e.g. shows the user progress) then you have
to move it into the store and create a dedicated action/reducer for it
```javascript
function* onBoarding(io, getState) {
while( getState().nbIncrements < 3 ) {
// wait for INCREMENT_COUNTER with a timeout of 5 seconds
const winner = yield io.race({
increment : io.take(INCREMENT_COUNTER),
timeout : io.call(delay, 5000)
})
if(winner.increment)
yield io.put( actions.incNbIncrements() )
else
yield io.put( actions.resetNbIncrements() )
}
yield io.put(showCongratulation())
}
```
#Sequencing Sagas via yield*
You can use the builtin `yield*` operator to compose multiple sagas in a sequential way.
This allows you to sequence your *macro-operations* in a simple procedural style.
This allows you to sequence your *macro-tasks* in a simple procedural style.
```javascript
function* playLevelOne(io, getState) { ... }
function* playLevelOne(getState) { ... }
function* playLevelTwo(io, getState) { ... }
function* playLevelTwo(getState) { ... }
function* playLevelThree(io, getState) { ... }
function* playLevelThree(getState) { ... }
function* game(io, getState) {
function* game(getState) {
const score1 = yield* playLevelOne(io, getState)
io.put(showScore(score1))
const score1 = yield* playLevelOne(getState)
put(showScore(score1))
const score2 = yield* playLevelTwo(io, getState)
io.put(showScore(score2))
const score2 = yield* playLevelTwo(getState)
put(showScore(score2))
const score3 = yield* playLevelThree(io, getState)
io.put(showScore(score3))
const score3 = yield* playLevelThree(getState)
put(showScore(score3))

@@ -338,5 +371,5 @@ }

Note that using `yield*` will cause the JavaScript runtime to *flatten* the whole sequence.
i.e. the resulting iterator (`game()` return value) will yield all values from the nested
iterators. A more powerful alternative is to use the generic middleware composition mechanism.
Note that using `yield*` will cause the JavaScript runtime to *spread* the whole sequence.
The resulting iterator (from `game()`) will yield all values from the nested
iterators. A more powerful alternative is to use the more generic middleware composition mechanism.

@@ -347,23 +380,39 @@ #Composing Sagas

- You'll likely to test nested generators separately. This leads to test duplication because when
- You'll likely want to test nested generators separately. This leads to some duplication because when
testing the main generator you'll have to iterate again on all the nested generators. This
can be achieved by making the nested tests reusable but the tests will still take more time to execute.
can be achieved by reusing the tests for sub-generators inside the main test but this introduce
more boilerplate inside your tests besides the overhead of the repeated execution. All we want is to
test that the main task yields to the correct subtask not actually execute it.
- More importantly, `yield*` allows only for sequential composition of generators, you can only
- More importantly, `yield*` allows only for sequential composition of tasks, you can only
yield* to one generator at a time. But there can be use cases when you want to launch multiple
operations in parallel. You spawn multiple Sagas in the background and resumes when all the spawned
Sagas are done.
tasks in parallel, and wait for them all to terminate in order to progress.
The Saga middleware offers an alternative way of composition using the simple `yield` statement.
When yielding a generator (more accurately an *iterator*) the middleware will convert the resulting
sub-iterator into a promise that will resolve to that sub-iterator return value (or a rejected with an
eventual error thrown from it).
You can simply use `yield subtask()`. When yielding a call to a generator, the Saga
will wait for the generator to terminate before progressing. And resumes then with the
returned value (or throws if an error propagates from the subtask).
This effectively lets you compose nested generators with other effects, like future actions,
timeouts, ...
```javascript
function* fetchPosts() {
yield put( actions.requestPosts() )
const products = yield call(fetchApi, '/products')
yield put( actions.receivePosts(products) )
}
function* watchFetch() {
while ( yield take(FETCH_POSTS) ) {
yield call(fetchPosts) // waits for the fetchPosts task to terminate
}
}
```
In fact, yielding Sagas is no more different than yielding other effects (future actions, timeouts ...).
It means you can combine those Sagas with all the other types of Effects using the effect combinators
(`[...effects]` and `race({...})`).
For example you may want the user finish some game in a limited amount of time
```javascript
function* game(io, getState) {
function* game(getState) {

@@ -373,5 +422,5 @@ let finished

// has to finish in 60 seconds
const {score, timeout} = yield io.race({
score : play(io, getState),
timeout : delay(60000)
const {score, timeout} = yield race({
score : call( play, getState),
timeout : call(delay, 60000)
})

@@ -381,3 +430,3 @@

finished = true
io.put( showScore(score) )
put( showScore(score) )
}

@@ -389,17 +438,102 @@ }

Or you may want to spawn multiple Sagas in parallel to monitor user actions and congratulate
the user when he accomplishes all the desired tasks.
Or you may want to start multiple Sagas in parallel to monitor user actions and congratulate
the user when he accomplished all the desired tasks.
```javascript
function* monitorTask1(io, getState) {...}
...
function* watchTask1() {...}
function* watchTask2() {...}
function* game(io, getState) {
function* game(getState) {
yield [ call(watchTask1), call(watchTask2)]
yield put( showCongratulation() )
}
```
#Concurrent tasks with fork/join
const tasks = yield [ monitorTask1(io, getState), ... ]
the `yield` statement causes the generator to pause until the yielded effect has resolved. If you look
closely at this example
yield io.put( showCongratulation() )
```javascript
function* watchFetch() {
while ( yield take(FETCH_POSTS) ) {
yield put( actions.requestPosts() )
const posts = yield call(fetchApi, '/posts') // will be blocked here
yield put( actions.receivePosts(posts) )
}
}
```
the `watchFetch` generator will wait until `yield call(fetchPosts)` terminates. Imagine that the
`FETCH_POSTS` action is fired from a `Refresh` button. If our application disables the button between
each fetch (no concurrent fecthes) then there is no issue, because we know that no `FETCH_POSTS` action
will occur until we get the response from the `fetchApi` call.
But what if the application allows the user to click on `Refresh` without waiting for the current request
to terminate ? What will happen ?
The following example illustrates a possible sequence of the events
```
UI watchFetch
--------------------------------------------------------
FETCH_POSTS.....................call fetchApi........... waiting to resolve
........................................................
........................................................
FETCH_POSTS............................................. missed
........................................................
FETCH_POSTS............................................. missed
................................fetchApi returned.......
........................................................
```
When `watchFetch` is blocked on the `fetchApi` call (waiting for the server response), all `FETCH_POSTS`
occurring in between the call and the response are missed.
To express non blocking call, we can use the `fork` function. A possible rewrite of the previous example
with `fork` can be
```javascript
import { fork } from 'redux-saga'
function* fetchPosts() {
yield put( actions.requestPosts() )
const posts = yield call(fetchApi, '/posts')
yield put( actions.receivePosts(posts) )
}
function* watchFetch() {
while ( yield take(FETCH_POSTS) ) {
yield fork(fetchPosts) // will not block here
}
}
```
`fork` accepts function/generator calls as well as simple effects
```javascript
yield fork(func, ...args) // simple async functions (...) -> Promise
yield fork(generator, ...args) // Generator functions
yield fork( put(someActions) ) // Simple effects
```
The result of `yield fork(api)` will be a *Task descriptor*. To get the
result of a forked Task in a later time, we use the `join` function
```javascript
// non blocking call
const task = yield fork(...)
// ... later
// now a blocking call, will resolve the outcome of task
const result = yield join(...)
```
You can also ask a Task if it's still running
```javascript
// attention, we don't use yield
const stillRunning = task.isRunning()
```
#Building from sources

@@ -406,0 +540,0 @@

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc