import { action , computed , IComputedValue , reaction , _allowStateChangesInsideComputed } from "mobx" ; import { addHiddenFinalProp , ComplexType , convertChildNodesToArray , createActionInvoker , EMPTY_OBJECT , extend , fail , freeze , IAnyType , IdentifierCache , IDisposer , IJsonPatch , IMiddleware , IMiddlewareHandler , IReversibleJsonPatch , NodeLifeCycle , resolveNodeByPathParts , splitJsonPath , splitPatch , toJSON , EventHandlers , Hook , BaseNode , getLivelinessChecking , normalizeIdentifier , ReferenceIdentifier , IMiddlewareEvent , escapeJsonPath , getPath , warnError , AnyNode , IStateTreeNode , ArgumentTypes , IType , devMode , getCurrentActionContext } from "../../internal" ; let nextNodeId = 1 ; const enum ObservableInstanceLifecycle { UNINITIALIZED , CREATING , CREATED } const enum InternalEvents { Dispose = "dispose" , Patch = "patch" , Snapshot = "snapshot" } internal hidden export interface IChildNodesMap { [ key ] : AnyNode ; } const snapshotReactionOptions = { onError ( e ) { throw e ; } } ; type InternalEventHandlers < S > = { [ InternalEvents . Dispose ] : IDisposer ; [ InternalEvents . Patch ] : ( patch : IJsonPatch , reversePatch : IJsonPatch ) => void ; [ InternalEvents . Snapshot ] : ( snapshot : S ) => void ; } ; internal hidden export class ObjectNode < C , S , T > extends BaseNode < C , S , T > { declare readonly type : ComplexType < C , S , T > ; declare storedValue : T & IStateTreeNode < IType < C , S , T > > ; readonly nodeId = ++ nextNodeId ; readonly identifierAttribute ? : string ; readonly identifier : string | null ; readonly unnormalizedIdentifier : ReferenceIdentifier | null ; identifierCache ? : IdentifierCache ; isProtectionEnabled = true ; middlewares ? : IMiddleware [ ] ; private _applyPatches ? : ( patches : IJsonPatch [ ] ) => void ; applyPatches ( patches ) { this . createObservableInstanceIfNeeded ( ) ; this . _applyPatches ! ( patches ) ; } private _applySnapshot ? : ( snapshot : C ) => void ; applySnapshot ( snapshot ) { this . createObservableInstanceIfNeeded ( ) ; this . _applySnapshot ! ( snapshot ) ; } private _autoUnbox = true ; _isRunningAction = false ; private _hasSnapshotReaction = false ; private _observableInstanceState = ObservableInstanceLifecycle . UNINITIALIZED ; private _childNodes : IChildNodesMap ; private _initialSnapshot : C ; private _cachedInitialSnapshot ? : S ; private _cachedInitialSnapshotCreated = false ; private _snapshotComputed : IComputedValue < S > ; constructor ( complexType , parent , subpath , environment , initialValue ) { super ( complexType , parent , subpath , environment ) ; this . _snapshotComputed = computed < S > ( ( ) => freeze ( this . getSnapshot ( ) ) ) ; this . unbox = this . unbox . bind ( this ) ; this . _initialSnapshot = freeze ( initialValue ) ; this . identifierAttribute = complexType . identifierAttribute ; if ( ! parent ) { this . identifierCache = new IdentifierCache ( ) ; } this . _childNodes = complexType . initializeChildNodes ( this , this . _initialSnapshot ) ; this . identifier = null ; this . unnormalizedIdentifier = null ; if ( this . identifierAttribute && this . _initialSnapshot ) { let id = ( this . _initialSnapshot as any ) [ this . identifierAttribute ] ; if ( id === undefined ) { const childNode = this . _childNodes [ this . identifierAttribute ] ; if ( childNode ) { id = childNode . value ; } } if ( typeof id !== "string" && typeof id !== "number" ) { throw fail ( `Instance identifier '${ this . identifierAttribute }' for type '${ this . type . name }' must be a string or a number` ) ; } this . identifier = normalizeIdentifier ( id ) ; this . unnormalizedIdentifier = id ; } if ( ! parent ) { this . identifierCache ! . addNodeToCache ( this ) ; } else { parent . root . identifierCache ! . addNodeToCache ( this ) ; } } createObservableInstanceIfNeeded ( ) { if ( this . _observableInstanceState === ObservableInstanceLifecycle . UNINITIALIZED ) { this . createObservableInstance ( ) ; } } createObservableInstance ( ) { if ( devMode ( ) ) { if ( this . state !== NodeLifeCycle . INITIALIZING ) { throw fail ( "assertion failed: the creation of the observable instance must be done on the initializing phase" ) ; } } this . _observableInstanceState = ObservableInstanceLifecycle . CREATING ; const parentChain = [ ] ; let parent = this . parent ; while ( parent && parent . _observableInstanceState === ObservableInstanceLifecycle . UNINITIALIZED ) { parentChain . unshift ( parent ) ; parent = parent . parent ; } for ( const p of parentChain ) { p . createObservableInstanceIfNeeded ( ) ; } const type = this . type ; try { this . storedValue = type . createNewInstance ( this . _childNodes ) ; this . preboot ( ) ; this . _isRunningAction = true ; type . finalizeNewInstance ( this , this . storedValue ) ; } catch ( e ) { this . state = NodeLifeCycle . DEAD ; throw e ; } finally { this . _isRunningAction = false ; } this . _observableInstanceState = ObservableInstanceLifecycle . CREATED ; ( this . _snapshotComputed as any ) . trackAndCompute ( ) ; if ( this . isRoot ) this . _addSnapshotReaction ( ) ; this . _childNodes = EMPTY_OBJECT ; this . state = NodeLifeCycle . CREATED ; this . fireHook ( Hook . afterCreate ) ; this . finalizeCreation ( ) ; } get root ( ) : AnyObjectNode { const parent = this . parent ; return parent ? parent . root : this ; } clearParent ( ) { if ( ! this . parent ) return ; this . fireHook ( Hook . beforeDetach ) ; const previousState = this . state ; this . state = NodeLifeCycle . DETACHING ; const root = this . root ; const newEnv = root . environment ; const newIdCache = root . identifierCache ! . splitCache ( this ) ; try { this . parent . removeChild ( this . subpath ) ; this . baseSetParent ( null , "" ) ; this . environment = newEnv ; this . identifierCache = newIdCache ; } finally { this . state = previousState ; } } setParent ( newParent , subpath ) { const parentChanged = newParent !== this . parent ; const subpathChanged = subpath !== this . subpath ; if ( ! parentChanged && ! subpathChanged ) { return ; } if ( devMode ( ) ) { if ( ! subpath ) { throw fail ( "assertion failed: subpath expected" ) ; } if ( ! newParent ) { throw fail ( "assertion failed: new parent expected" ) ; } if ( this . parent && parentChanged ) { throw fail ( `A node cannot exists twice in the state tree. Failed to add ${ this } to path '${ newParent . path }/${ subpath }'.` ) ; } if ( ! this . parent && newParent . root === this ) { throw fail ( `A state tree is not allowed to contain itself. Cannot assign ${ this } to path '${ newParent . path }/${ subpath }'` ) ; } if ( ! this . parent && ! ! this . environment && this . environment !== newParent . root . environment ) { throw fail ( `A state tree cannot be made part of another state tree as long as their environments are different.` ) ; } } if ( parentChanged ) { this . environment = undefined ; newParent . root . identifierCache ! . mergeCache ( this ) ; this . baseSetParent ( newParent , subpath ) ; this . fireHook ( Hook . afterAttach ) ; } else if ( subpathChanged ) { this . baseSetParent ( this . parent , subpath ) ; } } protected fireHook ( name ) { this . fireInternalHook ( name ) ; const fn = this . storedValue && typeof this . storedValue === "object" && ( this . storedValue as any ) [ name ] ; if ( typeof fn === "function" ) { if ( _allowStateChangesInsideComputed ) { _allowStateChangesInsideComputed ( ( ) => { fn . apply ( this . storedValue ) ; } ) ; } else { fn . apply ( this . storedValue ) ; } } } private _snapshotUponDeath ? : S ; get snapshot ( ) : S { return this . _snapshotComputed . get ( ) ; } getSnapshot ( ) { if ( ! this . isAlive ) return this . _snapshotUponDeath ! ; return this . _observableInstanceState === ObservableInstanceLifecycle . CREATED ? this . _getActualSnapshot ( ) : this . _getCachedInitialSnapshot ( ) ; } private _getActualSnapshot ( ) { return this . type . getSnapshot ( this ) ; } private _getCachedInitialSnapshot ( ) { if ( ! this . _cachedInitialSnapshotCreated ) { const type = this . type ; const childNodes = this . _childNodes ; const snapshot = this . _initialSnapshot ; this . _cachedInitialSnapshot = type . processInitialSnapshot ( childNodes , snapshot ) ; this . _cachedInitialSnapshotCreated = true ; } return this . _cachedInitialSnapshot ! ; } private isRunningAction ( ) { if ( this . _isRunningAction ) return true ; if ( this . isRoot ) return false ; return this . parent ! . isRunningAction ( ) ; } assertAlive ( context ) { const livelinessChecking = getLivelinessChecking ( ) ; if ( ! this . isAlive && livelinessChecking !== "ignore" ) { const error = this . _getAssertAliveError ( context ) ; switch ( livelinessChecking ) { case "error" : throw fail ( error ) ; case "warn" : warnError ( error ) ; } } } private _getAssertAliveError ( context ) { const escapedPath = this . getEscapedPath ( false ) || this . pathUponDeath || "" ; const subpath = ( context . subpath && escapeJsonPath ( context . subpath ) ) || "" ; let actionContext = context . actionContext || getCurrentActionContext ( ) ; if ( actionContext && actionContext . type !== "action" && actionContext . parentActionEvent ) { actionContext = actionContext . parentActionEvent ; } let actionFullPath = "" ; if ( actionContext && actionContext . name != null ) { const actionPath = ( actionContext && actionContext . context && getPath ( actionContext . context ) ) || escapedPath ; actionFullPath = `${ actionPath }.${ actionContext . name }()` ; } return `You are trying to read or write to an object that is no longer part of a state tree. (Object type: '${ this . type . name }', Path upon death: '${ escapedPath }', Subpath: '${ subpath }', Action: '${ actionFullPath }'). Either detach nodes first, or don't use objects after removing / replacing them in the tree.` ; } getChildNode ( subpath ) { this . assertAlive ( { subpath } ) ; this . _autoUnbox = false ; try { return this . _observableInstanceState === ObservableInstanceLifecycle . CREATED ? this . type . getChildNode ( this , subpath ) : this . _childNodes ! [ subpath ] ; } finally { this . _autoUnbox = true ; } } getChildren ( ) { this . assertAlive ( EMPTY_OBJECT ) ; this . _autoUnbox = false ; try { return this . _observableInstanceState === ObservableInstanceLifecycle . CREATED ? this . type . getChildren ( this ) : convertChildNodesToArray ( this . _childNodes ) ; } finally { this . _autoUnbox = true ; } } getChildType ( propertyName ? ) { return this . type . getChildType ( propertyName ) ; } get isProtected ( ) : boolean { return this . root . isProtectionEnabled ; } assertWritable ( context ) { this . assertAlive ( context ) ; if ( ! this . isRunningAction ( ) && this . isProtected ) { throw fail ( `Cannot modify '${ this }', the object is protected and can only be modified by using an action.` ) ; } } removeChild ( subpath ) { this . type . removeChild ( this , subpath ) ; } unbox ( childNode ) { if ( ! childNode ) return childNode ; this . assertAlive ( { subpath : childNode . subpath || childNode . subpathUponDeath } ) ; return this . _autoUnbox ? childNode . value : childNode ; } toString ( ) { const path = ( this . isAlive ? this . path : this . pathUponDeath ) || "<root>" ; const identifier = this . identifier ? `(id: ${ this . identifier })` : "" ; return `${ this . type . name }@${ path }${ identifier }${ this . isAlive ? "" : " [dead]" }` ; } finalizeCreation ( ) { this . baseFinalizeCreation ( ( ) => { for ( let child of this . getChildren ( ) ) { child . finalizeCreation ( ) ; } this . fireInternalHook ( Hook . afterCreationFinalization ) ; } ) ; } detach ( ) { if ( ! this . isAlive ) throw fail ( `Error while detaching, node is not alive.` ) ; this . clearParent ( ) ; } private preboot ( ) { const self = this ; this . _applyPatches = createActionInvoker ( this . storedValue , "@APPLY_PATCHES" , ( patches ) => { patches . forEach ( ( patch ) => { if ( ! patch . path ) { self . type . applySnapshot ( self , patch . value ) ; return ; } const parts = splitJsonPath ( patch . path ) ; const node = resolveNodeByPathParts ( self , parts . slice ( 0 , - 1 ) ) as AnyObjectNode ; node . applyPatchLocally ( parts [ parts . length - 1 ] , patch ) ; } ) ; } ) ; this . _applySnapshot = createActionInvoker ( this . storedValue , "@APPLY_SNAPSHOT" , ( snapshot ) => { if ( snapshot === ( self . snapshot as any ) ) return ; return self . type . applySnapshot ( self , snapshot as any ) ; } ) ; addHiddenFinalProp ( this . storedValue , "$treenode" , this ) ; addHiddenFinalProp ( this . storedValue , "toJSON" , toJSON ) ; } die ( ) { if ( ! this . isAlive || this . state === NodeLifeCycle . DETACHING ) return ; this . aboutToDie ( ) ; this . finalizeDeath ( ) ; } aboutToDie ( ) { if ( this . _observableInstanceState === ObservableInstanceLifecycle . UNINITIALIZED ) { return ; } this . getChildren ( ) . forEach ( ( node ) => { node . aboutToDie ( ) ; } ) ; this . baseAboutToDie ( ) ; this . _internalEventsEmit ( InternalEvents . Dispose ) ; this . _internalEventsClear ( InternalEvents . Dispose ) ; } finalizeDeath ( ) { this . getChildren ( ) . forEach ( ( node ) => { node . finalizeDeath ( ) ; } ) ; this . root . identifierCache ! . notifyDied ( this ) ; const snapshot = this . snapshot ; this . _snapshotUponDeath = snapshot ; this . _internalEventsClearAll ( ) ; this . baseFinalizeDeath ( ) ; } onSnapshot ( onChange ) { this . _addSnapshotReaction ( ) ; return this . _internalEventsRegister ( InternalEvents . Snapshot , onChange ) ; } protected emitSnapshot ( snapshot ) { this . _internalEventsEmit ( InternalEvents . Snapshot , snapshot ) ; } onPatch ( handler ) { return this . _internalEventsRegister ( InternalEvents . Patch , handler ) ; } emitPatch ( basePatch , source ) { if ( this . _internalEventsHasSubscribers ( InternalEvents . Patch ) ) { const localizedPatch = extend ( { } , basePatch , { path : source . path . substr ( this . path . length ) + "/" + basePatch . path } ) ; const [ patch , reversePatch ] = splitPatch ( localizedPatch ) ; this . _internalEventsEmit ( InternalEvents . Patch , patch , reversePatch ) ; } if ( this . parent ) this . parent . emitPatch ( basePatch , source ) ; } hasDisposer ( disposer ) { return this . _internalEventsHas ( InternalEvents . Dispose , disposer ) ; } addDisposer ( disposer ) { if ( ! this . hasDisposer ( disposer ) ) { this . _internalEventsRegister ( InternalEvents . Dispose , disposer , true ) ; return ; } throw fail ( "cannot add a disposer when it is already registered for execution" ) ; } removeDisposer ( disposer ) { if ( ! this . _internalEventsHas ( InternalEvents . Dispose , disposer ) ) { throw fail ( "cannot remove a disposer which was never registered for execution" ) ; } this . _internalEventsUnregister ( InternalEvents . Dispose , disposer ) ; } private removeMiddleware ( middleware ) { if ( this . middlewares ) { const index = this . middlewares . indexOf ( middleware ) ; if ( index >= 0 ) { this . middlewares . splice ( index , 1 ) ; } } } addMiddleWare ( handler , includeHooks = true ) { const middleware = { handler , includeHooks } ; if ( ! this . middlewares ) this . middlewares = [ middleware ] ; else this . middlewares . push ( middleware ) ; return ( ) => { this . removeMiddleware ( middleware ) ; } ; } applyPatchLocally ( subpath , patch ) { this . assertWritable ( { subpath } ) ; this . createObservableInstanceIfNeeded ( ) ; this . type . applyPatchLocally ( this , subpath , patch ) ; } private _addSnapshotReaction ( ) { if ( ! this . _hasSnapshotReaction ) { const snapshotDisposer = reaction ( ( ) => this . snapshot , ( snapshot ) => this . emitSnapshot ( snapshot ) , snapshotReactionOptions ) ; this . addDisposer ( snapshotDisposer ) ; this . _hasSnapshotReaction = true ; } } private _internalEvents ? : EventHandlers < InternalEventHandlers < S > > ; private _internalEventsHasSubscribers ( event ) { return ! ! this . _internalEvents && this . _internalEvents . hasSubscribers ( event ) ; } private _internalEventsRegister < IE extends InternalEvents > ( event , eventHandler , atTheBeginning = false ) { if ( ! this . _internalEvents ) { this . _internalEvents = new EventHandlers ( ) ; } return this . _internalEvents . register ( event , eventHandler , atTheBeginning ) ; } private _internalEventsHas < IE extends InternalEvents > ( event , eventHandler ) { return ! ! this . _internalEvents && this . _internalEvents . has ( event , eventHandler ) ; } private _internalEventsUnregister < IE extends InternalEvents > ( event , eventHandler ) { if ( this . _internalEvents ) { this . _internalEvents . unregister ( event , eventHandler ) ; } } private _internalEventsEmit < IE extends InternalEvents > ( event , ... args ) { if ( this . _internalEvents ) { this . _internalEvents . emit ( event , ... args ) ; } } private _internalEventsClear ( event ) { if ( this . _internalEvents ) { this . _internalEvents . clear ( event ) ; } } private _internalEventsClearAll ( ) { if ( this . _internalEvents ) { this . _internalEvents . clearAll ( ) ; } } } ObjectNode . prototype . createObservableInstance = action ( ObjectNode . prototype . createObservableInstance ) ; ObjectNode . prototype . detach = action ( ObjectNode . prototype . detach ) ; ObjectNode . prototype . die = action ( ObjectNode . prototype . die ) ; internal hidden export type AnyObjectNode = ObjectNode < any , any , any > ; internal hidden export interface AssertAliveContext { subpath ? : string ; actionContext ? : IMiddlewareEvent ; }