import type { AsyncAction , DecoratedActions , EffectsType , Middleware , MiddlewareFactory , MiddlewareStoreContext , } from './types' ; import isEqual from 'fast-deep-equal' ; import { CloneFunction , globalConfig , isSimpleSyncAction } from './impl' ; function getNoopMiddleware < S , A extends DecoratedActions , P > ( ) { const middleware = { init : ( ) => { } , destroy : null , effects : ( ) => null , } ; return middleware ; } export function logEffects < S , A extends DecoratedActions , P > ( log = console . log ) { const f = ( ) => { const logger = log ; let storeName = '' ; const middleware = { init : ( storeContext ) => { if ( process . env . NODE_ENV === 'development' || process . env . NODE_ENV === 'test' ) { storeName = storeContext . options ?. name ? `[${ storeContext . options ?. name }]` : '' ; const newState = storeContext . state ; logger ( storeName , 'initialState' , newState ) ; } } , destroy : null , effects : ( action , oldState , newState , loading ) => { if ( process . env . NODE_ENV === 'development' || process . env . NODE_ENV === 'test' ) { logger ( storeName , action . name , action . type , newState === null ? 'no effect' : newState , action . isAsync ? action . type === 'preEffects' && ! loading ? 'Promise cancelled' : `loading: ${ loading }` : '' ) ; } return null ; } , } ; return middleware ; } ; return f ; } export type Logger = { log : ( ... args : any [ ] ) => void ; group : ( ... args : any [ ] ) => void ; groupCollapsed : ( ... args : any [ ] ) => void ; groupEnd : ( ... args : any [ ] ) => void ; } ; export function logDetailedEffects < S , A extends DecoratedActions , P > ( logger = console ) { return ( ) => { let storeName = '' ; const console = logger ; if ( process . env . NODE_ENV === 'development' || process . env . NODE_ENV === 'test' ) { const computeDiffPropValue = ( oldValue , newValue ) => { let res ; if ( newValue !== oldValue ) { const type = oldValue != null ? typeof oldValue : typeof newValue ; if ( type === 'number' || type === 'string' || type === 'boolean' ) { res = `${ oldValue } => ${ newValue === '' ? '""' : newValue }` ; } else if ( ( oldValue && oldValue . length ) || ( newValue && newValue . length ) ) { if ( oldValue == null ) { res = `was ${ oldValue }, now contains ${ newValue . length } element(s)` ; } else if ( newValue == null ) { res = `contained ${ oldValue . length } element(s), now is ${ newValue }` ; } else if ( oldValue . length === 0 ) { res = `was empty, now contains ${ newValue . length } elements` ; } else if ( newValue . length === 0 ) { res = `contained ${ oldValue . length } elements, now is empty` ; } else { let addedValues = newValue . filter ( ( a ) => ! oldValue . find ( ( b ) => isEqual ( a , b ) ) ) ; let removedValues = oldValue . filter ( ( a ) => ! newValue . find ( ( b ) => isEqual ( a , b ) ) ) ; if ( addedValues . length > 10 ) { addedValues = `${ addedValues . length } element(s) added` ; } if ( removedValues . length > 10 ) { removedValues = `${ removedValues . length } element(s) removed` ; } res = { added : addedValues , removed : removedValues , } ; } } else { res = newValue ; } } return res ; } ; const buildDiff = < S > ( oldState , newState ) => { const res = { } ; if ( process . env . NODE_ENV === 'development' ) { if ( oldState ) { const keys = Object . keys ( oldState ) as ( keyof S ) [ ] ; keys . forEach ( ( k ) => { if ( newState . hasOwnProperty ( k ) ) { const oldValue = oldState [ k ] ; const newValue = newState [ k ] ; const diff = computeDiffPropValue ( oldValue , newValue ) ; if ( diff ) { res [ k as any ] = diff ; } } else { res [ k as any ] = 'was deleted' ; } } ) ; } const keys = Object . keys ( newState ) as ( keyof S ) [ ] ; keys . forEach ( ( k ) => { if ( oldState == null || ! oldState . hasOwnProperty ( k ) ) { const newValue = newState [ k ] ; res [ k as any ] = computeDiffPropValue ( undefined , newValue ) ; } } ) ; } return res ; } ; const getStr = ( v ) => { if ( typeof v === 'string' ) { return `"${ v }"` ; } return v ; } ; const middleware = { init : ( storeContext ) => { storeName = storeContext . options ?. name ? `[${ storeContext . options ?. name }]` : '' ; const newState = storeContext . state ; logger . group ( storeName , 'initialState' ) ; const keys = Object . keys ( newState ) as ( keyof S ) [ ] ; keys . forEach ( ( prop ) => void console . log ( prop , ':' , getStr ( newState [ prop ] ) ) ) ; logger . groupEnd ( ) ; } , destroy : null , effects : ( action , oldState , newState , loading ) => { const args = action . context ?. args ; const params = [ storeName , action . name . toString ( ) ] ; if ( action . isAsync ) { if ( action . type === 'preEffects' ) { params . push ( loading ? 'START' : 'CANCELLED' ) ; } else if ( action . type === 'effects' ) { params . push ( 'DONE' ) ; } else { params . push ( 'FAILED' ) ; } } if ( newState == null && ( ! args || Object . keys ( args ) . length === 0 ) ) { console . log ( ... params ) ; } else { console . group ( ... params ) ; } if ( args && Object . keys ( args ) . length > 0 ) { console . group ( 'Arguments' ) ; args . forEach ( ( arg ) => void console . log ( arg , ':' , getStr ( arg ) ) ) ; console . groupEnd ( ) ; } if ( newState != null ) { console . groupCollapsed ( 'Before' ) ; if ( oldState == null ) { console . log ( 'was null' ) ; } else { const keys = Object . keys ( oldState ) as ( keyof S ) [ ] ; keys . forEach ( ( prop ) => void console . log ( prop , ':' , getStr ( oldState [ prop ] ) ) ) ; } console . groupEnd ( ) ; console . groupCollapsed ( 'After' ) ; if ( newState == null ) { console . log ( 'was null' ) ; } else { const keys = Object . keys ( newState ) as ( keyof S ) [ ] ; keys . forEach ( ( prop ) => void console . log ( prop , ':' , getStr ( newState [ prop ] ) ) ) ; } console . groupEnd ( ) ; if ( newState != null ) { console . group ( 'Diff' ) ; const diff = buildDiff ( oldState , newState ) ; Object . keys ( diff ) . forEach ( ( prop ) => console . log ( prop , ':' , diff [ prop ] ) ) ; console . groupEnd ( ) ; } } console . groupEnd ( ) ; return null ; } , } ; return middleware ; } const middleware = getNoopMiddleware < S , A , P > ( ) ; return middleware ; } ; } type ActionHistory < S , A > = { actionName : keyof A ; promiseId : string ; effectType : EffectsType ; ctx : any ; beforeEffectState : S ; } ; type OptimisticActionsMap = { [ name ] : any ; } ; type OptimisticData < S , A > = { /** History of effects */ history : ActionHistory < S , A > [ ] ; /** Map of ongoing optimistic actions */ optimisticActions : OptimisticActionsMap ; /** Whether or not record all effects */ shouldRecordHistory : boolean ; } ; export function optimisticActions < S , A extends DecoratedActions , P > ( cloneFunc ? ) { function getMiddleWare ( ) { let storeContext = null ; const optimisticData = { history : null , optimisticActions : { } , shouldRecordHistory : false , } ; const clone = ( obj ) => { const f = cloneFunc || globalConfig . clone ; return f ( obj ) ; } ; const getActionId = ( name , promiseId ) => { return `${ name }_${ promiseId }` ; } ; param actionName param reducer param args param beforeState function pushActionToHistory ( actionName , effectType , ctx , beforeState = null ) { const { shouldRecordHistory } = optimisticData ; const { history } = optimisticData ; if ( shouldRecordHistory ) { if ( history === null ) { optimisticData . history = [ ] ; } const clonedCtx = clone ( ctx ) ; delete clonedCtx . state ; delete clonedCtx . s ; delete clonedCtx . abortSignal ; clonedCtx . error = ctx . err ; clonedCtx . err = ctx . err ; const newEntry = { effectType , actionName , promiseId : ctx . promiseId , ctx : clonedCtx , beforeEffectState : beforeState ? clone ( beforeState ) : null , } ; optimisticData . history . push ( newEntry ) ; } } param actionName function undoOptimisticAction ( actionName , promiseId ) { const { history } = optimisticData ; const index = history . findIndex ( ( a ) => a . effectType === 'optimisticEffects' && a . actionName === actionName && a . promiseId === promiseId ) ; let state = history [ index ] . beforeEffectState ; for ( let i = index + 1 ; i < history . length ; i ++ ) { const action = history [ i ] ; if ( action . beforeEffectState ) { action . beforeEffectState = state ; } const ctx = { ... action . ctx , state , s : state , } ; if ( action . actionName === 'onPropsChange' ) { const onPropsChanges = Array . isArray ( storeContext . options . onPropsChange ) ? storeContext . options . onPropsChange : [ storeContext . options . onPropsChange ] ; state = ( onPropsChanges [ ctx . index ] . effects as any ) ( ctx ) ; } else if ( isSimpleSyncAction ( storeContext . actions [ action . actionName ] ) ) { state = ( storeContext . actions [ action . actionName ] as any ) ( ctx ) ; } else { state = ( storeContext . actions [ action . actionName ] as any ) [ action . effectType ] ( ctx ) ; } } cleanHistoryAfterOptimistAction ( actionName , promiseId , index ) ; return state ; } function cleanHistoryAfterOptimistAction ( actionName , promiseId , indexInHistory = undefined ) { const { optimisticActions , history } = optimisticData ; const actionId = getActionId ( actionName , promiseId ) ; delete optimisticActions [ actionId ] ; const optiStateKeys = Object . keys ( optimisticActions ) as ( keyof A ) [ ] ; if ( optiStateKeys . length === 0 ) { optimisticData . history = null ; optimisticData . shouldRecordHistory = false ; } else { const index = indexInHistory === undefined ? history . findIndex ( ( a ) => getActionId ( a . actionName , a . promiseId ) === actionId ) : indexInHistory ; if ( index === 0 ) { const nextOptimisticIndex = history . slice ( 1 ) . findIndex ( ( a ) => a . beforeEffectState != null ) + 1 ; history . splice ( 0 , nextOptimisticIndex ) ; } else if ( indexInHistory === undefined ) { history [ index ] . beforeEffectState = null ; } } } const middleware = { init ( ctx ) { storeContext = ctx ; } , destroy ( ) { storeContext = null ; optimisticData . history = null ; optimisticData . optimisticActions = { } ; optimisticData . shouldRecordHistory = false ; } , effects ( { name , type , context , isAsync } , oldState , newState , loading ) { if ( ( newState != null && isSimpleSyncAction ( storeContext . actions [ name ] ) ) || name === 'onPropsChange' || ( storeContext . actions [ name ] as any ) [ type ] != null ) { pushActionToHistory ( name , type , context , null ) ; } if ( isAsync ) { const action = ( storeContext . actions [ name ] as any ) as AsyncAction < S , any , any , A , P > ; if ( loading && action . optimisticEffects != null && type === 'preEffects' ) { optimisticData . shouldRecordHistory = true ; optimisticData . optimisticActions [ getActionId ( name , context . promiseId ) ] = true ; const state = newState || oldState ; pushActionToHistory ( name , 'optimisticEffects' , context , state ) ; return { loading : false , state : action . optimisticEffects ( { ... ( context as any ) , state , s : state , } ) , } ; } if ( action . optimisticEffects != null && type === 'effects' ) { cleanHistoryAfterOptimistAction ( name , context . promiseId ) ; return { loading , state : newState , } ; } if ( action . optimisticEffects != null && type === 'errorEffects' ) { const state = undoOptimisticAction ( name , context . promiseId ) ; return { state , loading , } ; } } return null ; } , } ; return middleware ; } return getMiddleWare ; } export function devtools < S , A extends DecoratedActions , P > ( ) { const getMiddleware = ( ) => { let extension ; try { extension = ( window as any ) . __REDUX_DEVTOOLS_EXTENSION__ || ( window as any ) . top . __REDUX_DEVTOOLS_EXTENSION__ ; } catch { } if ( ! extension ) { return getNoopMiddleware < S , A , P > ( ) ; } let devtools ; let unsubscribe ; const middleware = { init ( storeContext ) { devtools = extension . connect ( { name : storeContext . options ?. name ?? 'StateDecorator' } ) ; devtools . init ( storeContext . state ) ; let savedState = null ; const initialState = storeContext . state ; unsubscribe = devtools . subscribe ( ( message ) => { if ( message . type === 'DISPATCH' && message . state && message . payload ?. type !== 'ROLLBACK' ) { const newState = JSON . parse ( message . state ) ; storeContext . setState ( newState ) ; } else if ( message . payload ?. type === 'COMMIT' ) { savedState = storeContext . state ; } else if ( message . payload ?. type === 'ROLLBACK' ) { if ( savedState != null ) { storeContext . setState ( savedState ) ; } } else if ( message . payload ?. type === 'RESET' ) { storeContext . setState ( initialState ) ; } } ) ; } , effects ( ctx , oldState , newState ) { if ( newState != null ) { const cleanCtx = { ... ctx . context , } ; delete cleanCtx . a ; delete cleanCtx . s ; delete cleanCtx . p ; delete cleanCtx . res ; delete cleanCtx . err ; delete cleanCtx . ds ; devtools . send ( { type : `${ ctx . name } ${ ctx . type }` , args : cleanCtx . args , context : cleanCtx , } , newState ) ; } return null ; } , destroy ( ) { unsubscribe ( ) ; devtools = null ; } , } ; return middleware ; } ; return getMiddleware ; }