diff --git a/packages/client/src/middleware/WaitMiddleware.test.ts b/packages/client/src/middleware/WaitMiddleware.test.ts new file mode 100644 index 0000000..e2240bd --- /dev/null +++ b/packages/client/src/middleware/WaitMiddleware.test.ts @@ -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) => { + 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([]) + }) + +}) diff --git a/packages/client/src/middleware/WaitMiddleware.ts b/packages/client/src/middleware/WaitMiddleware.ts new file mode 100644 index 0000000..67a59ab --- /dev/null +++ b/packages/client/src/middleware/WaitMiddleware.ts @@ -0,0 +1,61 @@ +import {TAsyncAction} from '../actions/TAsyncAction' +import {AnyAction, Middleware} from 'redux' + +export class WaitMiddleware { + protected notify?: (action: TAsyncAction) => void + + handle: Middleware = store => next => (action: AnyAction) => { + next(action) + if (this.notify && 'status' in action) { + this.notify(action as TAsyncAction) + } + } + + async wait(actions: string[], timeout = 10000): Promise { + if (this.notify) { + throw new Error('WaitMiddleware.wait - already waiting!') + } + + const actionsByName = actions.reduce((obj, type) => { + obj[type] = true + return obj + }, {} as Record) + // 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) => { + 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 + } + } + }) + } +}