import { runInAction } from "mobx" ; import { getStateTreeNode , isStateTreeNode , addMiddleware , tryResolve , applyPatch , getType , applySnapshot , isRoot , isProtected , fail , isPlainObject , isPrimitive , IDisposer , isArray , asArray , getRelativePathBetweenNodes , IAnyStateTreeNode , warnError , AnyNode , assertIsStateTreeNode , devMode , assertArg , IActionContext , getRunningActionContext } from "../internal" ; export interface ISerializedActionCall { name : string ; path ? : string ; args ? : any [ ] ; } export interface IActionRecorder { actions : ReadonlyArray < ISerializedActionCall > ; readonly recording : boolean ; stop ( ) : void ; resume ( ) : void ; replay ( target ) : void ; } function serializeArgument ( node , actionName , index , arg ) { if ( arg instanceof Date ) return { $MST_DATE : arg . getTime ( ) } ; if ( isPrimitive ( arg ) ) return arg ; if ( isStateTreeNode ( arg ) ) return serializeTheUnserializable ( `[MSTNode: ${ getType ( arg ) . name }]` ) ; if ( typeof arg === "function" ) return serializeTheUnserializable ( `[function]` ) ; if ( typeof arg === "object" && ! isPlainObject ( arg ) && ! isArray ( arg ) ) return serializeTheUnserializable ( `[object ${ ( arg && ( arg as any ) . constructor && ( arg as any ) . constructor . name ) || "Complex Object" }]` ) ; try { JSON . stringify ( arg ) ; return arg ; } catch ( e ) { return serializeTheUnserializable ( "" + e ) ; } } function deserializeArgument ( adm , value ) { if ( value && typeof value === "object" && "$MST_DATE" in value ) return new Date ( value [ "$MST_DATE" ] ) ; return value ; } function serializeTheUnserializable ( baseType ) { return { $MST_UNSERIALIZABLE : true , type : baseType } ; } param target param actions export function applyAction ( target , actions ) { assertIsStateTreeNode ( target , 1 ) ; assertArg ( actions , ( a ) => typeof a === "object" , "object or array" , 2 ) ; runInAction ( ( ) => { asArray ( actions ) . forEach ( ( action ) => baseApplyAction ( target , action ) ) ; } ) ; } function baseApplyAction ( target , action ) { const resolvedTarget = tryResolve ( target , action . path || "" ) ; if ( ! resolvedTarget ) throw fail ( `Invalid action path: ${ action . path || "" }` ) ; const node = getStateTreeNode ( resolvedTarget ) ; if ( action . name === "@APPLY_PATCHES" ) { return applyPatch . call ( null , resolvedTarget , action . args ! [ 0 ] ) ; } if ( action . name === "@APPLY_SNAPSHOT" ) { return applySnapshot . call ( null , resolvedTarget , action . args ! [ 0 ] ) ; } if ( ! ( typeof resolvedTarget [ action . name ] === "function" ) ) throw fail ( `Action '${ action . name }' does not exist in '${ node . path }'` ) ; return resolvedTarget [ action . name ] . apply ( resolvedTarget , action . args ? action . args . map ( ( v ) => deserializeArgument ( node , v ) ) : [ ] ) ; } param subject returns export function recordActions ( subject , filter ? ) { assertIsStateTreeNode ( subject , 1 ) ; const actions = [ ] ; const listener = ( call ) => { const recordThis = filter ? filter ( call , getRunningActionContext ( ) ) : true ; if ( recordThis ) { actions . push ( call ) ; } } ; let disposer ; const recorder = { actions , get recording ( ) { return ! ! disposer ; } , stop ( ) { if ( disposer ) { disposer ( ) ; disposer = undefined ; } } , resume ( ) { if ( disposer ) return ; disposer = onAction ( subject , listener ) ; } , replay ( target ) { applyAction ( target , actions ) ; } } ; recorder . resume ( ) ; return recorder ; } param target param listener param attachAfter returns export function onAction ( target , listener , attachAfter = false ) { assertIsStateTreeNode ( target , 1 ) ; if ( devMode ( ) ) { if ( ! isRoot ( target ) ) warnError ( "Warning: Attaching onAction listeners to non root nodes is dangerous: No events will be emitted for actions initiated higher up in the tree." ) ; if ( ! isProtected ( target ) ) warnError ( "Warning: Attaching onAction listeners to non protected nodes is dangerous: No events will be emitted for direct modifications without action." ) ; } return addMiddleware ( target , function handler ( rawCall , next ) { if ( rawCall . type === "action" && rawCall . id === rawCall . rootId ) { const sourceNode = getStateTreeNode ( rawCall . context ) ; const info = { name : rawCall . name , path : getRelativePathBetweenNodes ( getStateTreeNode ( target ) , sourceNode ) , args : rawCall . args . map ( ( arg , index ) => serializeArgument ( sourceNode , rawCall . name , index , arg ) ) } ; if ( attachAfter ) { const res = next ( rawCall ) ; listener ( info ) ; return res ; } else { listener ( info ) ; return next ( rawCall ) ; } } else { return next ( rawCall ) ; } } ) ; }