From c1d21350a88670521e97c9494ea680c6148165da Mon Sep 17 00:00:00 2001 From: Jerko Steiner Date: Sat, 19 Jan 2019 17:30:15 +0100 Subject: [PATCH] Add PromiseMiddleware --- packages/client/jest.config.js | 16 ++++ packages/client/jest.setup.js | 0 .../src/middleware/PromiseMiddleware.test.ts | 76 +++++++++++++++++++ .../src/middleware/PromiseMiddleware.ts | 45 +++++++++++ packages/client/tsconfig.json | 2 +- packages/tsconfig.common.json | 1 - 6 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 packages/client/jest.config.js create mode 100644 packages/client/jest.setup.js create mode 100644 packages/client/src/middleware/PromiseMiddleware.test.ts create mode 100644 packages/client/src/middleware/PromiseMiddleware.ts diff --git a/packages/client/jest.config.js b/packages/client/jest.config.js new file mode 100644 index 0000000..737dab0 --- /dev/null +++ b/packages/client/jest.config.js @@ -0,0 +1,16 @@ +module.exports = { + roots: [ + '/src' + ], + transform: { + '^.+\\.tsx?$': 'ts-jest' + }, + testRegex: '(/__tests__/.*|\\.(test|spec))\\.tsx?$', + moduleFileExtensions: [ + 'ts', + 'tsx', + 'js', + 'jsx' + ], + setupFiles: ['/jest.setup.js'] +} diff --git a/packages/client/jest.setup.js b/packages/client/jest.setup.js new file mode 100644 index 0000000..e69de29 diff --git a/packages/client/src/middleware/PromiseMiddleware.test.ts b/packages/client/src/middleware/PromiseMiddleware.test.ts new file mode 100644 index 0000000..e94fe4f --- /dev/null +++ b/packages/client/src/middleware/PromiseMiddleware.test.ts @@ -0,0 +1,76 @@ +import {createStore, applyMiddleware, Store} from 'redux' +import {PromiseMiddleware} from './PromiseMiddleware' + +describe('PromiseMiddleware', () => { + + async function getError(promise: Promise): Promise { + let error: Error + try { + await promise + } catch (err) { + error = err + } + expect(error!).toBeTruthy() + return error! + } + + describe('constructor', () => { + it('throws an error when action types are the same', () => { + expect(() => new PromiseMiddleware('a', 'a', 'a')).toThrowError() + expect(new PromiseMiddleware('a', 'b', 'c')).toBeTruthy() + }) + }) + + let store!: Store + beforeEach(() => { + const middleware = new PromiseMiddleware() + store = createStore((state: any[] = [], action) => { + state.push(action) + return state + }, applyMiddleware(middleware.handle)) + }) + + it('does nothing when payload is not a promise', () => { + const action = {type: 'test'} + store.dispatch(action) + expect(store.getState().slice(1)).toEqual([action]) + }) + + it('dispatches pending and fulfilled action', async () => { + const value = 123 + const type = 'TEST' + const action = { + payload: Promise.resolve(value), + type, + } + const result = store.dispatch(action) + expect(result).toBe(action) + await result.payload + expect(store.getState().slice(1)).toEqual([{ + type: `${type}_PENDING`, + }, { + payload: value, + type, + }]) + }) + + it('dispatches pending and rejected action on error', async () => { + const error = new Error('test') + const type = 'TEST' + const action = { + payload: Promise.reject(error), + type, + } + const result = store.dispatch(action) + expect(result).toBe(action) + const err = await getError(result.payload) + expect(err).toBe(error) + expect(store.getState().slice(1)).toEqual([{ + type: `${type}_PENDING`, + }, { + error, + type: `${type}_REJECTED`, + }]) + }) + +}) diff --git a/packages/client/src/middleware/PromiseMiddleware.ts b/packages/client/src/middleware/PromiseMiddleware.ts new file mode 100644 index 0000000..aab2d35 --- /dev/null +++ b/packages/client/src/middleware/PromiseMiddleware.ts @@ -0,0 +1,45 @@ +import assert from 'assert' +import {AnyAction, Middleware} from 'redux' + +function isPromise(value: any): value is Promise { + return value && typeof value === 'object' && + typeof (value as any).then === 'function' +} + +export class PromiseMiddleware { + constructor( + readonly pendingExtension = '_PENDING', + readonly fulfilledExtension = '', + readonly rejectedExtension = '_REJECTED', + ) { + assert( + this.pendingExtension !== this.fulfilledExtension && + this.fulfilledExtension !== this.rejectedExtension && + this.pendingExtension !== this.rejectedExtension, + 'Pending, fulfilled and rejected extensions must be unique') + } + handle: Middleware = store => next => (action: AnyAction) => { + const {payload, type} = action + if (!isPromise(payload)) { + next(action) + return + } + store.dispatch({type: type + this.pendingExtension}) + + payload + .then(result => { + store.dispatch({ + payload: result, + type, + }) + }) + .catch(err => { + store.dispatch({ + error: err, + type: type + this.rejectedExtension, + }) + }) + + return action + } +} diff --git a/packages/client/tsconfig.json b/packages/client/tsconfig.json index 56fff90..caad41d 100644 --- a/packages/client/tsconfig.json +++ b/packages/client/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../tsconfig.common.json" + "extends": "../tsconfig.common.json", "compilerOptions": { "outDir": "lib", "rootDir": "src" diff --git a/packages/tsconfig.common.json b/packages/tsconfig.common.json index a003e86..9997d22 100644 --- a/packages/tsconfig.common.json +++ b/packages/tsconfig.common.json @@ -1,6 +1,5 @@ { "compilerOptions": { - // following settings required for referenced projects "composite": true, "declaration": true, "declarationMap": true,