Skip to content

Instantly share code, notes, and snippets.

@bradennapier
Last active February 26, 2019 21:12
Show Gist options
  • Save bradennapier/af12f1ee3b31ccc934e5011ec1f43897 to your computer and use it in GitHub Desktop.
Save bradennapier/af12f1ee3b31ccc934e5011ec1f43897 to your computer and use it in GitHub Desktop.
Saga Defer Example
import _ from 'lodash';
import {
take,
call,
put,
fork,
spawn,
delay,
cancel
} from 'redux-saga/effects';
import { eventChannel } from 'redux-saga';
import defer from 'sagas/defer';
import connecting from './messages/connecting';
import critical from './messages/critical';
// import { subscribeToMarket } from 'client/wsActions';
let notifications = [];
let i = 0;
function createNotification(payload) {
i += 1;
return {
key: i,
props: {},
...payload,
created: Date.now()
};
}
function actions(n) {
return eventChannel(emitter => {
n.emit = (type, payload) => emitter({ type, payload });
return () => n.emit('close');
});
}
function* removeNotification(n) {
const cancelling = [];
const nextNotifications = notifications.reduce((arr, notification) => {
if (notification.key === n.key) {
cancelling.push(n.task);
return arr;
}
arr.push(notification);
return arr;
}, []);
if (cancelling.length) {
notifications = nextNotifications;
yield put({
type: 'COMMIT_NOTIFICATIONS',
notifications
});
yield cancel(cancelling);
}
}
const UPDATE_N = Symbol('@@notifications/UPDATE_NOTIFICATION_EVENT');
/**
* Each notification has an `emit` function
* which will emit events to control the
* notification `notification.emit('click')`
*/
function* watchNotification(chan, n) {
let notification = n;
let prevTask;
try {
while (true) {
const { type, payload } = yield take(chan);
switch (type) {
case UPDATE_N: {
notification = payload;
break;
}
case 'click': {
if (prevTask && prevTask.isRunning) {
yield cancel(prevTask);
}
if (!notification.onClick && !notification.props.sticky) {
prevTask = yield defer(fork, removeNotification, notification);
} else if (notification.onClick) {
prevTask = yield defer(spawn, notification.onClick, notification);
}
break;
}
case 'close': {
yield defer(fork, removeNotification, notification);
break;
}
case 'update': {
yield defer(fork, handleNotification, {
...payload,
key: n.key
});
break;
}
}
}
} finally {
if (prevTask && prevTask.isRunning) {
yield cancel(prevTask);
}
yield call(notification.chan.close);
}
}
/**
* For now we are not doing much here, but normalizing to ACCOUNT_LOGOUT
* to have a unified simple event pushed.
*
* We also dispatch the account to logout of for use by the callers if
* necessary (so it is not removed before they get it).
*/
function* handleNotification(payload) {
const currentIndex = payload.key
? notifications.findIndex(notification => notification.key === payload.key)
: -1;
const n = createNotification(payload);
if (currentIndex !== -1) {
// update notification with given key - we rebuild the
// object to ensure rendering can be intelligent if it
// has a given notification
const currentNotification = notifications[currentIndex];
const nextNotification = _.merge({}, currentNotification, n);
notifications.splice(currentIndex, 1, nextNotification);
yield call(currentNotification.emit, UPDATE_N, nextNotification);
} else {
const chan = yield call(actions, n);
n.chan = chan;
n.task = yield spawn(watchNotification, chan, n);
notifications.unshift(n);
notifications = notifications.sort((a, b) =>
a.sticky && b.sticky
? a.created - b.created
: a.sticky && !b.sticky
? 1
: -1
);
}
yield put({
type: 'COMMIT_NOTIFICATIONS',
notifications: notifications.slice()
});
}
function* handleAdd() {
yield put({
type: 'NOTIFICATION',
payload: critical.blockedRegion
});
yield delay(1000);
yield put({
type: 'NOTIFICATION',
payload: connecting.connecting
});
yield delay(5000);
yield put({
type: 'NOTIFICATION',
payload: connecting.connected
});
}
export default function* notificationSaga() {
yield fork(handleAdd);
while (true) {
const action = yield take(['NOTIFICATION']);
yield fork(handleNotification, action.payload);
}
}
import { call, cancel } from 'redux-saga/effects';
let nextTickProm;
function* deferredExecute(fn, ...args) {
if (!nextTickProm) {
nextTickProm = Promise.resolve().then(() => {
nextTickProm = undefined;
});
}
yield call(() => nextTickProm);
if (typeof fn !== 'function') {
return;
}
return yield call(fn, ...args);
}
export default function* defer(effect, ...args) {
if ([cancel].includes(effect) || !effect) {
// deferred effects without fnDescriptor
// footprint
yield call(deferredExecute);
if (!effect) {
return;
}
return yield effect(...args);
}
return yield effect(deferredExecute, ...args);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment