3

I have, a set of action grouped in redux-saga channel. Each of them (or many simultaneously) can fail because of token expiration. How can I suspend future actions to prevent next failures, retry already catched action after token refresh and unsuspend channel.

function* watchRequests() {
    // a set of simultaneous actions which utilize access token from store
    const requestChannel = yield actionChannel('*_REQUEST')
    while (true) {
        try {
            const { payload } = yield take(requestChannel)

            yield fork(handleRequest, payload)
        } catch (error) {
            if (error === 'EXPIRED_ACCESS_TOKEN') {
                const newToken = yield call(refreshToken)
                yield call(setAccessToken, newToken)
            } else {
                throw error
            }
        } finally {
            // after success token refresh
            // 1. retry catched action
            // 2. unsuspend rest actions from channel
        }
    }
}
Szopinski
  • 790
  • 1
  • 10
  • 18

1 Answers1

3

Working on the same concept here but using a different recipe.

Below are the key parts of the code but please note that it is still a work in progress.

The key is that the "api.refreshSession" is forked and while this task is still running, every REFRESH_SESSION action joins the already running task instead of launching a new one.

function* refreshSessionListener() {
    let task;
    while (true) {
        const action = yield take(REFRESH_SESSION);
        if (task === undefined || !task.isRunning()) {
            task = yield fork(api.refreshSession);
        }
        // non-blocking join
        yield fork(joinRefreshSessionTask, task, action);
    }
}

function* joinRefreshSessionTask(task, action) {
    const response = yield join(task);
    if (response.status === 200) {
        yield fork(action.onSuccess);
    } else {
        yield fork(action.onError, response);
    }
}

export function* apiSaga(...args) {
    const [fn, { type, payload, meta = {} }] = args;
    try {
        const data = yield call(fn, payload);
        yield* okSaga(type, e);
    } catch (e) {
        if (e.tokenExpired) {
            yield put({
                type: REFRESH_SESSION,
                onSuccess: apiSaga.bind(null, ...args),
                onError: errorSaga.bind(null, type)
            });
            return;
        } else {
            yield* errorSaga(type, e);
        }
    }
}

runSaga(refreshSessionListener)

I use the above in other part of the application like that:

import {apiSaga} from ...

function* listenToActions () {
    yield takeEvery(POST_SOMETHING, apiSaga.bind(null, api.postSomething));
    yield takeLatest(GET_SOMETHING, apiSaga.bind(null, api.getSomething));
}

runSaga(listenToActions);
  • You can use `takeLeading` which basically blocks every subsequent tank until the first one is done: https://redux-saga.js.org/docs/api/#takeleadingpattern-saga-args – Tasos Oct 15 '21 at 12:19