From Generators to Sagas

ian metcalf

ES6 Refresher

Arrow Functions

const fn = (param) => { console.log(param); }; fn('hi'); [1,2,3].map(n => n + 2).filter(n => n % 2);

Object Shorthand

const value = 'some value'; const obj = { method(param) { console.log(param); }, value, }; obj.method('hi'); obj;

Destructuring

const obj = { prop: 'some value', nested: { prop: 'nested value', }, }; const { prop: value1, nested: { prop: value2, }, value3 = 'default value', } = obj; ({value1, value2, value3});

Rest

function fn(...args) { return args; } fn(1, 2, 3)

Spread

const values = [4, 8, 2, 5]; Math.min(...values);

ES6 Generators

Generator Function

Factory for generator objects

function* numbers(n) { for (let i = 1; i <= n; i += 1) { yield i; } return 'Completed'; } const it = numbers(2);

Generator Objects

function* numbers(n) { for (let i = 1; i <= n; i += 1) { yield i; } return 'Completed'; } const it = numbers(2); it.next(); // it.next(); // it.next(); // it.next();

Generator Objects As Iterables

function* numbers(n) { for (let i = 1; i <= n; i += 1) { yield i; } return 'Completed'; } for (let n of numbers(2)) { console.log(n); } [...numbers(3)];

Let's Create a Runner

Simple Runner

function* numbers(n) { for (let i = 1; i <= n; i += 1) { console.log(yield i); } } function runner(fn, ...args) { const it = fn(...args); function step(value) { const result = it.next(value); if (!result.done) { setImmediate(() => step(result.value)); } } setImmediate(step); } // runner(numbers, 3);

Promise Based Runner

function runner(fn, ...args) { return new Promise(resolve => { const it = fn(...args); function step(value) { const result = it.next(value); if (!result.done) { Promise.resolve(result.value).then(step); } else { resolve(result.value); } } setImmediate(step); }); }

Promise Based Runner in Action

function* main(steps) { for (let i = 1; i <= steps; i += 1) { const start = now(); yield delay(Math.random() * 5); console.log(`Completed step ${ i } in ${ now() - start } secs`); } } function now() { return Math.floor(Date.now() / 1000); } function delay(sec) { return new Promise(resolve => setTimeout(resolve, sec * 1000)); } // runner(main, 3);

Effect Based Runner

function effect(type, ...args) { return { effect: type, args, }; }

Effect Based Runner

function runEffect(value) { if (!value || !value.effect) return Promise.resolve(value); if (value.effect === 'CALL') { const [fn, ...fnArgs] = value.args; return runEffect(fn(...fnArgs)); } if (value.effect === 'FORK') { const [fn, ...fnArgs] = value.args; setImmediate(() => runEffect(fn(...fnArgs))); return Promise.resolve(); } throw new Error('Unknown effect'); }

Effect Based Runner

function runner(fn, ...args) { return new Promise(resolve => { const it = fn(...args); function step(value) { const result = it.next(value); if (!result.done) { runEffect(result.value).then(step); } else { resolve(result.value); } } setImmediate(step); }); }

Effect Based Runner in Action

function* main(workers) { for (let i = 1; i <= workers; i += 1) { yield effect('FORK', runner, worker, i); } } function* worker(id) { const start = now(); console.log(`Worker ${ id } started`); yield effect('CALL', delay, Math.random() * 5); console.log(`Worker ${ id } completed in ${ now() - start } secs`); } // runner(main, 2);

Communicating Sequential Processes

CSP Channel

const CLOSED = null; function channel() { let closed = false; const takers = []; const putters = []; return { put(value, callback = () => {}) { if (closed) return callback(false); if (takers.length) { const {fn} = takers.shift(); setImmediate(() => fn(value)); callback(true); } else { putters.push({fn: callback, value}); } }, take(callback = () => {}) { if (closed) return callback(CLOSED); if (putters.length) { const {fn, value} = putters.shift(); setImmediate(() => fn(true)); callback(value); } else { takers.push({fn: callback}); } }, close() { if (closed) return; closed = true; while (takers.length) { const {fn} = takers.shift(); setImmediate(() => fn(CLOSED)); } while (putters.length) { const {fn} = putters.shift(); setImmediate(() => fn(false)); } }, isClosed() { return closed; }, }; }

CSP Effects

function runEffect(value) { // ... if (value.effect === 'PUT') { const [chan, val] = value.args; return new Promise(resolve => chan.put(val, resolve)); } if (value.effect === 'TAKE') { const [chan] = value.args; return new Promise(resolve => chan.take(resolve)); } // ... }

CSP Example

function* main(workers, duration) { const ch = channel(); for (let i = 1; i <= workers; i += 1) { yield effect('FORK', runner, worker, i, ch); } setTimeout(() => ch.close(), duration * 1000); while (!ch.isClosed()) { const work = effect('CALL', delay, Math.random() * 5); yield effect('PUT', ch, work); } console.log('Main done'); } function* worker(id, ch) { while (true) { const work = yield effect('TAKE', ch); if (work === CLOSED) break; const start = now(); yield work; console.log(`Worker ${ id } finished in ${ now() - start } secs`); } console.log(`Worker ${ id } done`); } // runner(main, 3, 10);

CSP Example 2

function* main(count) { const ch = channel(); document.addEventListener('click', e => ch.put(e)); for (let i = 1; i <= count; i += 1) { const result = yield effect('TAKE', ch); if (result !== CLOSED) { console.log(`click ${ i }`); } } ch.close(); } // runner(main, 3);

Redux Saga

Uses es6 generators to orchestrate side effects in redux

Redux Architecture

Redux Architecture

Saga Middleware

function configureStore(initialState) { const sagaMiddleware = createSagaMiddleware(); const enhancer = applyMiddleware(sagaMiddleware); const store = createStore(rootReducer, initialState, enhancer); sagaMiddleware.run(rootSaga); }

Saga Example

function* saga() { const action = yield take('SOME_ACTION'); yield put({ type: 'OTHER_ACTION', }); yield call(console.log, 'completed', action); } // [...saga()];

Easy to test

What about thunks

Limitations

Channels

Saga Effects API

take( pattern | matcher | channel )

Stop generator until matching action is dispatched

function* saga() { const action = yield take('SOME_ACTION'); }

put( action )

Dispatch action and continue

function* saga() { yield put({ type: 'SOME_ACTION', }); }

call( saga | function , ...args )

Run saga or function and wait until completed

function* saga() { yield call(fn, ...args); }

fork( saga | function , ...args )

Run saga or function without waiting

function* saga() { yield fork(fn, ...args); }

select( selector , ...args )

Return state from redux store

function* saga() { const state = yield select(); }

Effect Combinators

[ ...effects ]

Run effects and wait until all have completed

function* saga() { const results = yield [ call(fn, ...args), take('SOME_ACTION'), take('OTHER_ACTION'), ]; }

race({ ...effects })

Run effects and wait until one has completed

function* saga() { const results = yield race({ response: call(fn, ...args), someAction: take('SOME_ACTION'), otherAction: take('OTHER_ACTION'), }); }

Higher Order Effects

takeEvery( pattern | matcher , saga , ...args )

Run saga for each matching action

function* saga() { yield takeEvery('SOME_ACTION', someSaga, ...args); }

takeLatest( pattern | matcher , saga , ...args )

Run saga for most recent matching action

function* saga() { yield takeLatest('SOME_ACTION', someSaga, ...args); }

Demo time

Questions