import { getStateTreeNode , isStateTreeNode , createScalarNode , IType , TypeFlags , IValidationContext , IValidationResult , typeCheckSuccess , typeCheckFailure , fail , IAnyType , IAnyStateTreeNode , IAnyComplexType , Hook , IDisposer , maybe , isModelType , IMaybe , NodeLifeCycle , ReferenceIdentifier , normalizeIdentifier , getIdentifier , applyPatch , AnyNode , AnyObjectNode , SimpleType , assertIsType , isValidIdentifier , IStateTreeNode , devMode } from "../../internal" ; export type OnReferenceInvalidatedEvent < STN extends IAnyStateTreeNode > = { parent : IAnyStateTreeNode ; invalidTarget : STN | undefined ; invalidId : ReferenceIdentifier ; replaceRef : ( newRef : STN | null | undefined ) => void ; removeRef : ( ) => void ; cause : "detach" | "destroy" | "invalidSnapshotReference" ; } ; export type OnReferenceInvalidated < STN extends IAnyStateTreeNode > = ( event : OnReferenceInvalidatedEvent < STN > ) => void ; function getInvalidationCause ( hook ) { switch ( hook ) { case Hook . beforeDestroy : return "destroy" ; case Hook . beforeDetach : return "detach" ; default : return undefined ; } } hidden export type ReferenceT < IT extends IAnyType > = IT [ "TypeWithoutSTN" ] & IStateTreeNode < IReferenceType < IT > > ; class StoredReference < IT extends IAnyType > { readonly identifier ! : ReferenceIdentifier ; node ! : AnyNode ; private resolvedReference ? : { node : AnyObjectNode ; lastCacheModification : string ; } ; constructor ( value , private readonly targetType ) { if ( isValidIdentifier ( value ) ) { this . identifier = value ; } else if ( isStateTreeNode ( value ) ) { const targetNode = getStateTreeNode ( value ) ; if ( ! targetNode . identifierAttribute ) throw fail ( `Can only store references with a defined identifier attribute.` ) ; const id = targetNode . unnormalizedIdentifier ; if ( id === null || id === undefined ) { throw fail ( `Can only store references to tree nodes with a defined identifier.` ) ; } this . identifier = id ; } else { throw fail ( `Can only store references to tree nodes or identifiers, got: '${ value }'` ) ; } } private updateResolvedReference ( node ) { const normalizedId = normalizeIdentifier ( this . identifier ) ; const root = node . root ; const lastCacheModification = root . identifierCache ! . getLastCacheModificationPerId ( normalizedId ) ; if ( ! this . resolvedReference || this . resolvedReference . lastCacheModification !== lastCacheModification ) { const { targetType } = this ; const target = root . identifierCache ! . resolve ( targetType , normalizedId ) ; if ( ! target ) { throw new InvalidReferenceError ( `[mobx-state-tree] Failed to resolve reference '${ this . identifier }' to type '${ this . targetType . name }' (from node: ${ node . path })` ) ; } this . resolvedReference = { node : target ! , lastCacheModification : lastCacheModification } ; } } get resolvedValue ( ) : ReferenceT < IT > { this . updateResolvedReference ( this . node ) ; return this . resolvedReference ! . node . value ; } } internal hidden export class InvalidReferenceError extends Error { constructor ( m ) { super ( m ) ; Object . setPrototypeOf ( this , InvalidReferenceError . prototype ) ; } } internal hidden export abstract class BaseReferenceType < IT extends IAnyComplexType > extends SimpleType < ReferenceIdentifier , ReferenceIdentifier , IT [ "TypeWithoutSTN" ] > { readonly flags = TypeFlags . Reference ; constructor ( protected readonly targetType , private readonly onInvalidated ? ) { super ( `reference(${ targetType . name })` ) ; } describe ( ) { return this . name ; } isAssignableFrom ( type ) { return this . targetType . isAssignableFrom ( type ) ; } isValidSnapshot ( value , context ) { return isValidIdentifier ( value ) ? typeCheckSuccess ( ) : typeCheckFailure ( context , value , "Value is not a valid identifier, which is a string or a number" ) ; } private fireInvalidated ( cause , storedRefNode , referenceId , refTargetNode ) { const storedRefParentNode = storedRefNode . parent ; if ( ! storedRefParentNode || ! storedRefParentNode . isAlive ) { return ; } const storedRefParentValue = storedRefParentNode . storedValue ; if ( ! storedRefParentValue ) { return ; } this . onInvalidated ! ( { cause , parent : storedRefParentValue , invalidTarget : refTargetNode ? refTargetNode . storedValue : undefined , invalidId : referenceId , replaceRef ( newRef ) { applyPatch ( storedRefNode . root . storedValue , { op : "replace" , value : newRef , path : storedRefNode . path } ) ; } , removeRef ( ) { if ( isModelType ( storedRefParentNode . type ) ) { this . replaceRef ( undefined as any ) ; } else { applyPatch ( storedRefNode . root . storedValue , { op : "remove" , path : storedRefNode . path } ) ; } } } ) ; } private addTargetNodeWatcher ( storedRefNode , referenceId ) { const refTargetValue = this . getValue ( storedRefNode ) ; if ( ! refTargetValue ) { return undefined ; } const refTargetNode = getStateTreeNode ( refTargetValue ) ; const hookHandler = ( _ , refTargetNodeHook ) => { const cause = getInvalidationCause ( refTargetNodeHook ) ; if ( ! cause ) { return ; } this . fireInvalidated ( cause , storedRefNode , referenceId , refTargetNode ) ; } ; const refTargetDetachHookDisposer = refTargetNode . registerHook ( Hook . beforeDetach , hookHandler ) ; const refTargetDestroyHookDisposer = refTargetNode . registerHook ( Hook . beforeDestroy , hookHandler ) ; return ( ) => { refTargetDetachHookDisposer ( ) ; refTargetDestroyHookDisposer ( ) ; } ; } protected watchTargetNodeForInvalidations ( storedRefNode , identifier , customGetSet ) { if ( ! this . onInvalidated ) { return ; } let onRefTargetDestroyedHookDisposer ; storedRefNode . registerHook ( Hook . beforeDestroy , ( ) => { if ( onRefTargetDestroyedHookDisposer ) { onRefTargetDestroyedHookDisposer ( ) ; } } ) ; const startWatching = ( sync ) => { if ( onRefTargetDestroyedHookDisposer ) { onRefTargetDestroyedHookDisposer ( ) ; } const storedRefParentNode = storedRefNode . parent ; const storedRefParentValue = storedRefParentNode && storedRefParentNode . storedValue ; if ( storedRefParentNode && storedRefParentNode . isAlive && storedRefParentValue ) { let refTargetNodeExists ; if ( customGetSet ) { refTargetNodeExists = ! ! customGetSet . get ( identifier , storedRefParentValue ) ; } else { refTargetNodeExists = storedRefNode . root . identifierCache ! . has ( this . targetType , normalizeIdentifier ( identifier ) ) ; } if ( ! refTargetNodeExists ) { if ( ! sync ) { this . fireInvalidated ( "invalidSnapshotReference" , storedRefNode , identifier , null ) ; } } else { onRefTargetDestroyedHookDisposer = this . addTargetNodeWatcher ( storedRefNode , identifier ) ; } } } ; if ( storedRefNode . state === NodeLifeCycle . FINALIZED ) { startWatching ( true ) ; } else { if ( ! storedRefNode . isRoot ) { storedRefNode . root . registerHook ( Hook . afterCreationFinalization , ( ) => { if ( storedRefNode . parent ) { storedRefNode . parent . createObservableInstanceIfNeeded ( ) ; } } ) ; } storedRefNode . registerHook ( Hook . afterAttach , ( ) => { startWatching ( false ) ; } ) ; } } } internal hidden export class IdentifierReferenceType < IT extends IAnyComplexType > extends BaseReferenceType < IT > { constructor ( targetType , onInvalidated ? ) { super ( targetType , onInvalidated ) ; } getValue ( storedRefNode ) { if ( ! storedRefNode . isAlive ) return undefined ; const storedRef = storedRefNode . storedValue ; return storedRef . resolvedValue as any ; } getSnapshot ( storedRefNode ) { const ref = storedRefNode . storedValue ; return ref . identifier ; } instantiate ( parent , subpath , environment , initialValue ) { const identifier = isStateTreeNode ( initialValue ) ? getIdentifier ( initialValue ) ! : initialValue ; const storedRef = new StoredReference ( initialValue , this . targetType as any ) ; const storedRefNode = createScalarNode ( this , parent , subpath , environment , storedRef as any ) ; storedRef . node = storedRefNode ; this . watchTargetNodeForInvalidations ( storedRefNode , identifier as string , undefined ) ; return storedRefNode ; } reconcile ( current , newValue , parent , subpath ) { if ( ! current . isDetaching && current . type === this ) { const compareByValue = isStateTreeNode ( newValue ) ; const ref = current . storedValue ; if ( ( ! compareByValue && ref . identifier === newValue ) || ( compareByValue && ref . resolvedValue === newValue ) ) { current . setParent ( parent , subpath ) ; return current ; } } const newNode = this . instantiate ( parent , subpath , undefined , newValue ) ; current . die ( ) ; return newNode ; } } internal hidden export class CustomReferenceType < IT extends IAnyComplexType > extends BaseReferenceType < IT > { constructor ( targetType , private readonly options , onInvalidated ? ) { super ( targetType , onInvalidated ) ; } getValue ( storedRefNode ) { if ( ! storedRefNode . isAlive ) return undefined as any ; const referencedNode = this . options . get ( storedRefNode . storedValue , storedRefNode . parent ? storedRefNode . parent . storedValue : null ) ; return referencedNode ; } getSnapshot ( storedRefNode ) { return storedRefNode . storedValue ; } instantiate ( parent , subpath , environment , newValue ) { const identifier = isStateTreeNode ( newValue ) ? this . options . set ( newValue as any , parent ? parent . storedValue : null ) : newValue ; const storedRefNode = createScalarNode ( this , parent , subpath , environment , identifier as any ) ; this . watchTargetNodeForInvalidations ( storedRefNode , identifier as string , this . options ) ; return storedRefNode ; } reconcile ( current , newValue , parent , subpath ) { const newIdentifier = isStateTreeNode ( newValue ) ? this . options . set ( newValue as any , current ? current . storedValue : null ) : newValue ; if ( ! current . isDetaching && current . type === this && current . storedValue === newIdentifier ) { current . setParent ( parent , subpath ) ; return current ; } const newNode = this . instantiate ( parent , subpath , undefined , newIdentifier ) ; current . die ( ) ; return newNode ; } } export interface ReferenceOptionsGetSet < IT extends IAnyComplexType > { get ( identifier , parent ) : ReferenceT < IT > ; set ( value , parent ) : ReferenceIdentifier ; } export interface ReferenceOptionsOnInvalidated < IT extends IAnyComplexType > { onInvalidated : OnReferenceInvalidated < ReferenceT < IT > > ; } export type ReferenceOptions < IT extends IAnyComplexType > = ReferenceOptionsGetSet < IT > | ReferenceOptionsOnInvalidated < IT > | ( ReferenceOptionsGetSet < IT > & ReferenceOptionsOnInvalidated < IT > ) ; hidden export interface IReferenceType < IT extends IAnyComplexType > extends IType < ReferenceIdentifier , ReferenceIdentifier , IT [ "TypeWithoutSTN" ] > { } /**
 * `types.reference` - Creates a reference to another type, which should have defined an identifier.
 * See also the [reference and identifiers](https://github.com/mobxjs/mobx-state-tree#references-and-identifiers) section.
 */ export function reference < IT extends IAnyComplexType > ( subType , options ? ) { assertIsType ( subType , 1 ) ; if ( devMode ( ) ) { if ( arguments . length === 2 && typeof arguments [ 1 ] === "string" ) { throw fail ( "References with base path are no longer supported. Please remove the base path." ) ; } } const getSetOptions = options ? ( options as ReferenceOptionsGetSet < IT > ) : undefined ; const onInvalidated = options ? ( options as ReferenceOptionsOnInvalidated < IT > ) . onInvalidated : undefined ; if ( getSetOptions && ( getSetOptions . get || getSetOptions . set ) ) { if ( devMode ( ) ) { if ( ! getSetOptions . get || ! getSetOptions . set ) { throw fail ( "reference options must either contain both a 'get' and a 'set' method or none of them" ) ; } } return new CustomReferenceType ( subType , { get : getSetOptions . get , set : getSetOptions . set } , onInvalidated ) ; } else { return new IdentifierReferenceType ( subType , onInvalidated ) ; } } param type returns export function isReferenceType < IT extends IReferenceType < any > > ( type ) { return ( type . flags & TypeFlags . Reference ) > 0 ; } export function safeReference < IT extends IAnyComplexType > ( subType , options ) ; export function safeReference < IT extends IAnyComplexType > ( subType , options ? ) ; param subType param options returns export function safeReference < IT extends IAnyComplexType > ( subType , options ? ) { const refType = reference ( subType , { ... options , onInvalidated ( ev ) { if ( options && options . onInvalidated ) { options . onInvalidated ( ev ) ; } ev . removeRef ( ) ; } } ) ; if ( options && options . acceptsUndefined === false ) { return refType ; } else { return maybe ( refType ) ; } }