Skip to content

Commit

Permalink
Adds fetching groups (#13)
Browse files Browse the repository at this point in the history
  • Loading branch information
zepatrik authored and arekkas committed Jan 30, 2018
1 parent 274b3b6 commit d6568d8
Show file tree
Hide file tree
Showing 3 changed files with 141 additions and 38 deletions.
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,15 @@ const sagaFetcher = createSagaFetcher({
},
article: {
// The action payload (see below) will be passed as the first argument.
fetcher: (id) => getUsersFromAPI(id)
fetcher: (id) => getUsersFromAPI(id),
// If any key of the group is fetching (in this case 'article' and 'createArticle') and
// the other one is requested, the first one has to finish first before the second one gets fetched.
group: 'article'
},
createArticle: {
// This works with POST/PUT/DELETE/... methods as well
fetcher: (payload) => createArticleAtAPI(payload)
fetcher: (payload) => createArticleAtAPI(payload),
group: 'article'
}
})

Expand Down
115 changes: 79 additions & 36 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,29 @@
// @flow
// createRegistry, createRequestAction, createRequestFailureAction, createRequestSuccessAction, createRootReducer, createRootSaga
import { createAction, handleActions } from 'redux-actions'
import { all, call, put, takeLatest } from 'redux-saga/effects'
import { all, call, put, takeLatest, select, take } from 'redux-saga/effects'
import 'babel-core/register'
import 'babel-polyfill'

type OptionsType = {
fetcher: () => Promise<*>,
takeStrategy?: Function,
group?: string,
}

type Registry = {
[key: string]: {
fetcher: () => Promise<*>,
takeStrategy: Function,
},
[key: string]: OptionsType,
}

type StateItem = {
status: string,
payload: mixed,
errorPayload: mixed,
}

type State = {
reduxSagaFetch: {
[key: string]: {
status: string,
payload: mixed,
},
[key: string]: StateItem,
},
}

Expand All @@ -40,22 +46,71 @@ export const hasFetchFailures = (state: State) => {
Object.keys(states).find(key => states[key].status === STATE_FAILURE)
)
}
export const selectErrorPayloads = (state: State) => {

// mapStates filters all the states by status and then maps with the custom map function
const mapStates = (
state: State,
status: string,
mapFunc: (curr: StateItem, key: string) => mixed
) => {
const states = pathOr(['reduxSagaFetch'], state, {})
return Object.keys(states)
.filter(key => states[key].status === STATE_FAILURE)
.map(key => ({
key,
error: states[key].errorPayload,
}))
.filter(key => states[key].status === status)
.map((key: string) => mapFunc(states[key], key))
}

const createDefaultWorker = (fetcher, successAction, failureAction) =>
export const selectErrorPayloads = (state: State) =>
mapStates(state, STATE_FAILURE, (current, key) => ({
key,
error: current.errorPayload,
}))

const fetchingActionsInGroup = (
state: State,
registry: Registry,
group: ?string,
key: string
) =>
group
? mapStates(
state,
STATE_FETCHING,
(current, key: string) => (registry[key].group === group ? key : undefined)
).filter(k => k && k !== key)
: []

const getFinishedActions = (keys: string[]) =>
keys
.map(k => createRequestSuccessAction(k).toString())
.concat(keys.map(k => createRequestFailureAction(k).toString()))

const createDefaultWorker = (key: string, registry: Registry) =>
function*(action) {
try {
const { group, fetcher } = registry[key]
let blockedInGroup = yield select(
fetchingActionsInGroup,
registry,
group,
key
)

// wait for group to get unblocked
while (blockedInGroup.length > 0) {
yield take(getFinishedActions(blockedInGroup))
blockedInGroup = yield select(
fetchingActionsInGroup,
registry,
group,
key
)
}

const successAction = createRequestSuccessAction(key)
const response = yield call(fetcher, action.payload)
yield put(successAction({ response, request: action.payload }))
} catch (error) {
const failureAction = createRequestFailureAction(key)
yield put(failureAction({ error, request: action.payload }))
}
}
Expand All @@ -67,13 +122,9 @@ const createWatcher = (action, worker, takeStrategy) =>

const createWatchers = (registry: Registry) =>
Object.keys(registry).map(key => {
const { fetcher, takeStrategy } = registry[key]
const { takeStrategy = takeLatest } = registry[key]
const action = createRequestAction(key)
const worker = createDefaultWorker(
fetcher,
createRequestSuccessAction(key),
createRequestFailureAction(key)
)
const worker = createDefaultWorker(key, registry)

return createWatcher(action, worker, takeStrategy)()
})
Expand All @@ -87,10 +138,7 @@ class SagaFetcher {

constructor(
config: {
[key: string]: {
fetcher: () => Promise<*>,
takeStrategy: any,
},
[key: string]: OptionsType,
} = {}
) {
if (typeof config !== 'object') {
Expand All @@ -99,24 +147,19 @@ class SagaFetcher {

Object.keys(config).forEach(key => {
const options = config[key]
this.add(key, options.fetcher, options.takeStrategy)
this.add(key, options)
})
}

add = (
key: string,
fetcher: (arg: any) => Promise<*>,
takeStrategy: Function = takeLatest
) => {
if (typeof fetcher !== 'function') {
add = (key: string, options: OptionsType) => {
if (typeof options.fetcher !== 'function') {
throw new Error(
`Expected a function for key ${key} but got ${typeof fetcher}`
`Expected a function for key ${key} but got ${typeof options.fetcher}`
)
}

this.registry[key] = {
fetcher,
takeStrategy: takeStrategy,
...options,
}
}

Expand Down
56 changes: 56 additions & 0 deletions src/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ import {
selectErrorPayload,
hasFetchFailures,
selectErrorPayloads,
createRequestSuccessAction,
} from './index'

import { applyMiddleware, combineReducers, createStore } from 'redux'
import createSagaMiddleware from 'redux-saga'
import { combineActions, handleActions } from 'redux-actions'

const internalServerError = new Error('Internal server error')

Expand Down Expand Up @@ -123,4 +125,58 @@ describe('The public api of redux-saga-fetch', () => {
})
})
})

it('grouping of requests works', async () => {
const group = 'group'

const registry = createSagaFetcher({
request1: {
fetcher: () => delay(50).then(() => Promise.resolve('')),
group,
},
request2: {
fetcher: () => delay(10).then(() => Promise.resolve('')),
group,
},
})

const request1 = createRequestAction('request1')
const request2 = createRequestAction('request2')

const rootReducer = combineReducers(registry.wrapRootReducer())

const initialState = {}
const sagaMiddleware = createSagaMiddleware()
const store = createStore(
rootReducer,
initialState,
applyMiddleware(sagaMiddleware)
)

sagaMiddleware.run(registry.createRootSaga())

const expectFetching = (r1, r2) => {
expect(isFetching('request1')(store.getState())).toBe(r1)
expect(isFetching('request2')(store.getState())).toBe(r2)
}

const expectSuccess = (r1, r2) => {
expect(isFetchSuccess('request1')(store.getState())).toBe(r1)
expect(isFetchSuccess('request2')(store.getState())).toBe(r2)
}

store.dispatch(request1())
store.dispatch(request2())

expectFetching(true, true)
expectSuccess(false, false)

await delay(55)
expectFetching(false, true)
expectSuccess(true, false)

await delay(10)
expectFetching(false, false)
expectSuccess(true, true)
})
})

0 comments on commit d6568d8

Please sign in to comment.