Add PromiseMiddleware

This commit is contained in:
Jerko Steiner 2019-01-19 17:30:15 +01:00
parent eedd57c527
commit c1d21350a8
6 changed files with 138 additions and 2 deletions

View File

@ -0,0 +1,16 @@
module.exports = {
roots: [
'<rootDir>/src'
],
transform: {
'^.+\\.tsx?$': 'ts-jest'
},
testRegex: '(/__tests__/.*|\\.(test|spec))\\.tsx?$',
moduleFileExtensions: [
'ts',
'tsx',
'js',
'jsx'
],
setupFiles: ['<rootDir>/jest.setup.js']
}

View File

View File

@ -0,0 +1,76 @@
import {createStore, applyMiddleware, Store} from 'redux'
import {PromiseMiddleware} from './PromiseMiddleware'
describe('PromiseMiddleware', () => {
async function getError(promise: Promise<any>): Promise<Error> {
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`,
}])
})
})

View File

@ -0,0 +1,45 @@
import assert from 'assert'
import {AnyAction, Middleware} from 'redux'
function isPromise(value: any): value is Promise<any> {
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
}
}

View File

@ -1,5 +1,5 @@
{
"extends": "../tsconfig.common.json"
"extends": "../tsconfig.common.json",
"compilerOptions": {
"outDir": "lib",
"rootDir": "src"

View File

@ -1,6 +1,5 @@
{
"compilerOptions": {
// following settings required for referenced projects
"composite": true,
"declaration": true,
"declarationMap": true,