Add WaitMiddleware.ts

This commit is contained in:
Jerko Steiner 2019-05-08 09:57:33 +12:00
parent 8c23b59ecd
commit 70c94c7c63
2 changed files with 170 additions and 0 deletions

View File

@ -0,0 +1,109 @@
import {WaitMiddleware} from './WaitMiddleware'
import {
IAction, IPendingAction, IResolvedAction, IRejectedAction,
} from '../actions'
import {applyMiddleware, createStore, AnyAction} from 'redux'
import {getError} from '../test-utils'
describe('WaitMiddleware', () => {
const getStore = (wm: WaitMiddleware) => createStore(
(state: string[] = [], action: IAction<any, string>) => {
return [...state, action.type]
},
[],
applyMiddleware(wm.handle),
)
it('waits for certain async actions to be resolved', async () => {
const wm = new WaitMiddleware()
const store = getStore(wm)
const promise = wm.wait(['B', 'C'])
store.dispatch({
payload: undefined,
type: 'A',
status: 'resolved',
})
store.dispatch({
payload: undefined,
type: 'B',
status: 'resolved',
})
store.dispatch({
payload: undefined,
type: 'C',
status: 'resolved',
})
await promise
expect(store.getState().slice(1)).toEqual(['A', 'B', 'C'])
})
it('times out when actions do not happen', async () => {
const wm = new WaitMiddleware()
const store = getStore(wm)
const promise = wm.wait(['B', 'C'], 5)
store.dispatch({
payload: undefined,
type: 'A',
status: 'resolved',
})
store.dispatch({
payload: undefined,
type: 'B',
status: 'resolved',
})
const error = await getError(promise)
expect(error.message).toMatch(/timed/)
})
it('errors out when a promise is rejected', async () => {
const wm = new WaitMiddleware()
const store = getStore(wm)
const promise = wm.wait(['B', 'C'])
store.dispatch({
payload: undefined,
type: 'A',
status: 'resolved',
})
store.dispatch({
payload: new Error('test'),
type: 'B',
status: 'rejected',
})
const error = await getError(promise)
expect(error.message).toMatch(/test/)
})
it('errors out when wait is called twice', async () => {
const wm = new WaitMiddleware()
const store = getStore(wm)
const promise = wm.wait(['B'])
const error = await getError(wm.wait(['B']))
expect(error.message).toMatch(/already waiting/)
store.dispatch({
payload: new Error('test'),
type: 'B',
status: 'resolved',
})
await promise
})
it('does nothing when pending is dispatched', async () => {
const wm = new WaitMiddleware()
const store = getStore(wm)
const promise = wm.wait(['B'], 1)
store.dispatch({
payload: undefined,
type: 'B',
status: 'pending',
})
const error = await getError(promise)
expect(error.message).toMatch(/timed/)
})
it('resolved immediately when no actions are defined', async () => {
const wm = new WaitMiddleware()
await wm.wait([])
})
})

View File

@ -0,0 +1,61 @@
import {TAsyncAction} from '../actions/TAsyncAction'
import {AnyAction, Middleware} from 'redux'
export class WaitMiddleware {
protected notify?: (action: TAsyncAction<any, string>) => void
handle: Middleware = store => next => (action: AnyAction) => {
next(action)
if (this.notify && 'status' in action) {
this.notify(action as TAsyncAction<any, string>)
}
}
async wait(actions: string[], timeout = 10000): Promise<void> {
if (this.notify) {
throw new Error('WaitMiddleware.wait - already waiting!')
}
const actionsByName = actions.reduce((obj, type) => {
obj[type] = true
return obj
}, {} as Record<string, boolean>)
// no duplicates here so we cannot use actions.length
let count = Object.keys(actionsByName).length
return new Promise((resolve, reject) => {
if (!actions.length) {
resolve()
this.notify = undefined
return
}
const t = setTimeout(() => {
reject(new Error('WaitMiddleware.wait - timed out!'))
this.notify = undefined
}, timeout)
this.notify = (action: TAsyncAction<any, string>) => {
if (!actionsByName[action.type]) {
return
}
switch (action.status) {
case 'pending':
return
case 'resolved':
actionsByName[action.type] = false
count--
if (count === 0) {
resolve()
this.notify = undefined
}
return
case 'rejected':
reject(action.payload)
this.notify = undefined
return
}
}
})
}
}