import { IAnyStateTreeNode , isStateTreeNode , getSnapshot , applyAction , onSnapshot , getType , hasParent , getParent , onPatch , addMiddleware , IMiddlewareEvent , getPath , applySnapshot } from "mobx-state-tree" ; export param { * } model param { ... MiddleWare [ ] } middlewares returns { IReduxStore } export const asReduxStore = function ( model , ... middlewares ) { if ( ! isStateTreeNode ( model ) ) throw new Error ( "Expected model object" ) ; let store = { getState : ( ) => getSnapshot ( model ) , dispatch : ( action ) => { runMiddleWare ( action , runners . slice ( ) , ( newAction ) => applyAction ( model , reduxActionToAction ( newAction ) ) ) ; } , subscribe : ( listener ) => onSnapshot ( model , listener ) } ; let runners = middlewares . map ( ( mw ) => mw ( store ) ) ; return store ; } ; function reduxActionToAction ( action ) { const actionArgs = Object . assign ( { } , action ) ; delete actionArgs . type ; return { name : action . type , args : [ actionArgs ] } ; } function runMiddleWare ( action , runners , next ) { function n ( retVal ) { const f = runners . shift ( ) ; if ( f ) f ( n ) ( retVal ) ; else next ( retVal ) ; } n ( action ) ; } type ChangesMadeSetter = ( ) => void ; interface ActionContext { parent ? : ActionContext ; name : string ; targetTypePath : string ; id : number ; runningAsync : boolean ; errored : boolean ; errorReported : boolean ; step : number | undefined ; callArgs : any [ ] ; changesMadeSetter : ChangesMadeSetter | undefined ; } function getActionContextNameAndTypePath ( actionContext , logArgsNearName ) { let name = actionContext . name ; let targetTypePath = actionContext . targetTypePath ; if ( logArgsNearName ) { let args = actionContext . callArgs . map ( ( a ) => JSON . stringify ( a ) ) . join ( ", " ) ; if ( args . length > 64 ) { args = args . slice ( 0 , 64 ) + "..." ; } name += `(${ args })` ; } if ( actionContext . runningAsync ) { name += ` (${ actionContext . step !== undefined ? actionContext . step : "?" })` ; } if ( actionContext . errored ) { name += ` -error thrown-` ; } if ( actionContext . parent ) { const ret = getActionContextNameAndTypePath ( actionContext . parent , logArgsNearName ) ; if ( ret ) { name = `${ ret . name } >>> ${ name }` ; targetTypePath = `${ ret . targetTypePath } >>> ${ targetTypePath }` ; } } return { name , targetTypePath } ; } function getTypeName ( node ) { return getType ( node ) . name || "(UnnamedType)" ; } function getTargetTypePath ( node ) { let current = node ; const names = [ ] ; while ( current ) { names . unshift ( getTypeName ( current ) ) ; current = hasParent ( current ) ? getParent ( current ) : undefined ; } return names ; } export param { * } remoteDevDep param { IAnyStateTreeNode } model param { { logIdempotentActionSteps ? : boolean logChildActions ? : boolean logArgsNearName ? : boolean * } } options export function connectReduxDevtools ( remoteDevDep , model , options ? ) { const opts = { logIdempotentActionSteps : true , logChildActions : false , logArgsNearName : true , ... options } ; let handlingMonitorAction = 0 ; const remotedev = remoteDevDep . connectViaExtension ( { name : getType ( model ) . name } ) ; remotedev . subscribe ( ( message ) => { if ( message . type === "DISPATCH" ) { handleMonitorActions ( remotedev , model , message ) ; } } ) ; const initialState = getSnapshot ( model ) ; remotedev . init ( initialState ) ; const actionContexts = new Map < number , ActionContext > ( ) ; let changesMadeSetter = undefined ; if ( ! opts . logIdempotentActionSteps ) { onPatch ( model , ( ) => { if ( ! handlingMonitorAction && changesMadeSetter ) { changesMadeSetter ( ) ; } } ) ; } addMiddleware ( model , actionMiddleware , false ) ; function actionMiddleware ( call , next ) { if ( handlingMonitorAction ) { next ( call ) ; return ; } let context ! ; for ( let i = call . allParentIds . length - 1 ; i >= 0 ; i -- ) { const parentId = call . allParentIds [ i ] ; const foundContext = actionContexts . get ( parentId ) ; if ( foundContext ) { context = foundContext ; break ; } } if ( call . type === "action" || ! context ) { const targetTypePath = getTargetTypePath ( call . context ) . join ( "/" ) ; const parentContext = context ; const path = call . context ? `root${ getPath ( call . context ) }` : "*unknown*" ; context = { name : `[${ path }] ${ call . name || "*unknownAction*" }` , targetTypePath : targetTypePath , id : call . id , runningAsync : false , errored : false , errorReported : false , step : call . type === "action" ? 0 : undefined , callArgs : [ ] , changesMadeSetter : undefined } ; if ( call . type === "action" ) { if ( call . args ) { context . callArgs = [ ... call . args ] ; } if ( call . parentId ) { context . parent = parentContext ; } actionContexts . set ( call . id , context ) ; } } let changesMade = false ; context . changesMadeSetter = ( ) => { changesMade = true ; } ; let oldChangesMadeSetter = changesMadeSetter ; changesMadeSetter = context . changesMadeSetter ; let errorThrown ; try { next ( call ) ; } catch ( e ) { errorThrown = e ; context . errored = true ; } changesMadeSetter = oldChangesMadeSetter ; context . changesMadeSetter = undefined ; const changedTheModel = opts . logIdempotentActionSteps ? true : changesMade ; switch ( call . type ) { case "flow_spawn" : case "flow_resume" : case "flow_resume_error" : context . runningAsync = true ; let parent = context . parent ; while ( parent ) { parent . runningAsync = true ; parent = parent . parent ; } break ; case "flow_throw" : context . errored = true ; break ; } const syncAction = call . type === "action" && ! context . runningAsync ; let log = syncAction || call . type === "flow_resume" || ( call . type === "flow_throw" && ! context . errorReported ) ; if ( ! opts . logChildActions && context . parent && ! context . runningAsync ) { log = false ; if ( changesMade && context . parent . changesMadeSetter ) { context . parent . changesMadeSetter ( ) ; } } if ( log ) { const logStep = ( logContext ) => { const sn = getSnapshot ( model ) ; const names = getActionContextNameAndTypePath ( logContext , opts . logArgsNearName ! ) ; const copy = { type : names . name , targetTypePath : names . targetTypePath , args : logContext . callArgs } ; remotedev . send ( copy , sn ) ; if ( context . errored ) { context . errorReported = true ; } if ( context . step !== undefined ) { context . step ++ ; } let parent = context . parent ; while ( parent ) { if ( parent . step !== undefined ) { parent . step ++ ; } parent = parent . parent ; } } ; const logAsRoot = context . parent && ! opts . logChildActions ; if ( changedTheModel ) { let logContext = context ; if ( logAsRoot ) { while ( logContext . parent ) { logContext = logContext . parent ; } } logStep ( logContext ) ; } else if ( ! logAsRoot && context . errored && ! context . errorReported ) { logStep ( context ) ; } } if ( call . type === "flow_return" || call . type === "flow_throw" || ! context ! . runningAsync ) { actionContexts . delete ( context ! . id ! ) ; } if ( errorThrown ) { throw errorThrown ; } } function handleMonitorActions ( remotedev2 , model2 , message ) { try { handlingMonitorAction ++ ; switch ( message . payload . type ) { case "RESET" : applySnapshot ( model2 , initialState ) ; return remotedev2 . init ( initialState ) ; case "COMMIT" : return remotedev2 . init ( getSnapshot ( model2 ) ) ; case "ROLLBACK" : return remotedev2 . init ( remoteDevDep . extractState ( message ) ) ; case "JUMP_TO_STATE" : case "JUMP_TO_ACTION" : applySnapshot ( model2 , remoteDevDep . extractState ( message ) ) ; return ; case "IMPORT_STATE" : const nextLiftedState = message . payload . nextLiftedState ; const computedStates = nextLiftedState . computedStates ; applySnapshot ( model2 , computedStates [ computedStates . length - 1 ] . state ) ; remotedev2 . send ( null , nextLiftedState ) ; return ; default : } } finally { handlingMonitorAction -- ; } } }