philk philk - 3 months ago 30
Javascript Question

How to structure sagas depending on an API accessToken

I have a saga

loginSaga
that performs the api call to get an
accessToken
.
Once this succeeds I'd like to have bunch of other sagas to spin up that download data with this
accessToken


Those sagas depend on the
accessToken
, so they should only run until
LOGOUT_SUCCESS
appears. Then they should finish. If they were currently in the process of downloading something this should be canceled, the result ignored.

This is what I have now:

function *authorize(credentials) {
try {
const { email, password, device_name, device_family, device_token } = credentials
const { access_token, user } = yield call(api.loginAsync, email, password, { device_name, device_family, device_token })
yield put(actions.loginSuccess(access_token, user))
} catch(error) {
console.error(error)
if (!isCancelError(error)) {
if (error.response && error.response.status == 401) {
error.message = "E-Mail or Password wrong"
}
yield put(actions.loginError(error))
}
}
}

function *loginFlow() {
while(true) {
console.info("Waiting for account/LOGIN")
const credentials = yield take(actions.LOGIN)
const authorizeTask = yield fork(authorize, credentials)

console.info("Waiting for account/LOGOUT")
const { type } = yield take([actions.LOGOUT, actions.LOGIN_ERROR])
cancel(authorizeTask)
if (type == actions.LOGOUT) {
yield call(api.logoutAsync)
yield put(actions.logoutSuccess())
}
}
}


Then the on
LOGIN_SUCESS
depending sagas:

function *refreshUser() {
while (true) {
const result = yield take([actions.LOGIN_SUCCESS, actions.REFRESH_USER])
try {
const user = yield call(api.getUserAsync)
yield put(actions.userRefreshed(user))
} catch (error) {
}
}
}


function *getActivities(access_token) {
console.info("fetch activities")
try {
const activities = yield call(api.getActivitiesAsync, access_token)
console.info("activities fetched")
yield put(activitiesUpdated(activities))
} catch (error) {
}
}

function *updateActivities() {
while (true) {
const { access_token } = yield take(actions.LOGIN_SUCCESS)
console.info("Calling getActivities")
yield call(getActivities, access_token)
while (true) {
const {type } = yield take([actions.REFRESH_ACTIVITIES, actions.LOGOUT])
if (type == actions.LOGOUT) {
break
}
yield call(getActivities, access_token)
}
}
}


And since they all need the
accessToken
they all have to wait until the
LOGIN_FINISHED
because this carries it in the payload.

Also my sagas are all started at the beginning and wait.

I thought maybe, when the
authorize
flow finishs it could then spin up the
loginDependingSagas
with the
accessToken
as a param.

like so:

function forkLoginDependingSagas(accessToken) {
fork refreshUser(accessToken)
fork updateActivities(accessToken)
}

Is that a good pattern?

Answer

I solved this problem using a separate saga that waits for actions that carry the access_token and then fork sagas depending on this access_token.

function* authorizedFlow() {
  let payload
  while (({ payload } = yield take([actions.LOGIN_SUCCESS, actions.START_SIGNUP_SUCCESS]))) {
    const { access_token } = payload
    yield fork(logoutFlow, access_token)
    yield race({
      watchers: [
        call(refreshUser, access_token),
        call(refreshInvoices, access_token),
        call(refreshPendingTransactions, access_token),
      ],
      logout: take(actions.LOGOUT_SUCCESS),
    })
  }
}

function* logoutFlow(access_token) {
  while (yield take(actions.LOGOUT)) {
    try {
      yield call(api.logoutAsync, access_token)
      yield put(actions.logoutSuccess())
    } catch (error) {
      yield put(actions.logoutError(error))
    }
  }
}

The logoutFlow would ensure that the race between the access_token dependent sagas would lead to cancellation of them in case the logout is succcessful.