import { _interceptReads , action , IInterceptor , IKeyValueMap , IMapDidChange , IMapWillChange , intercept , Lambda , observable , ObservableMap , observe , values } from "mobx" ; import { ComplexType , createObjectNode , escapeJsonPath , fail , flattenTypeErrors , getContextForPath , getStateTreeNode , IAnyStateTreeNode , IAnyType , IChildNodesMap , IValidationContext , IJsonPatch , isMutable , isPlainObject , isStateTreeNode , isType , IType , IValidationResult , ModelType , ObjectNode , typecheckInternal , typeCheckFailure , TypeFlags , EMPTY_OBJECT , normalizeIdentifier , AnyObjectNode , AnyNode , IAnyModelType , asArray , cannotDetermineSubtype , getSnapshot , isValidIdentifier , ExtractCSTWithSTN , devMode , createActionInvoker , addHiddenFinalProp , addHiddenWritableProp , IHooksGetter } from "../../internal" ; hidden export interface IMapType < IT extends IAnyType > extends IType < IKeyValueMap < IT [ "CreationType" ] > | undefined , IKeyValueMap < IT [ "SnapshotType" ] > , IMSTMap < IT > > { hooks ( hooks ) : IMapType < IT > ; } hidden export interface IMSTMap < IT extends IAnyType > { clear ( ) : void ; delete ( key ) : boolean ; forEach ( callbackfn , thisArg ? ) : void ; get ( key ) : IT [ "Type" ] | undefined ; has ( key ) : boolean ; set ( key , value ) : this ; readonly size : number ; put ( value ) : IT [ "Type" ] ; keys ( ) : IterableIterator < string > ; values ( ) : IterableIterator < IT [ "Type" ] > ; entries ( ) : IterableIterator < [ string , IT [ "Type" ] ] > ; [ Symbol . iterator ] ( ) : IterableIterator < [ string , IT [ "Type" ] ] > ; /** Merge another object into this map, returns self. */ merge ( other ) : this ; replace ( values ) : this ; toJSON ( ) : IKeyValueMap < IT [ "SnapshotType" ] > ; toString ( ) : string ; [ Symbol . toStringTag ] : "Map" ; /**
     * Observes this object. Triggers for the events 'add', 'update' and 'delete'.
     * See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/observe
     * for callback details
     */ observe ( listener , fireImmediately ? ) : Lambda ; intercept ( handler ) : Lambda ; } const needsIdentifierError = `Map.put can only be used to store complex values that have an identifier type attribute` ; function tryCollectModelTypes ( type , modelTypes ) { const subtypes = type . getSubTypes ( ) ; if ( subtypes === cannotDetermineSubtype ) { return false ; } if ( subtypes ) { const subtypesArray = asArray ( subtypes ) ; for ( const subtype of subtypesArray ) { if ( ! tryCollectModelTypes ( subtype , modelTypes ) ) return false ; } } if ( type instanceof ModelType ) { modelTypes . push ( type ) ; } return true ; } internal hidden export enum MapIdentifierMode { UNKNOWN , YES , NO } class MSTMap < IT extends IAnyType > extends ObservableMap < string , any > { constructor ( initialData ? ) { super ( initialData , ( observable . ref as any ) . enhancer ) ; } get ( key ) { return super . get ( "" + key ) ; } has ( key ) { return super . has ( "" + key ) ; } delete ( key ) { return super . delete ( "" + key ) ; } set ( key , value ) { return super . set ( "" + key , value ) ; } put ( value ) { if ( ! value ) throw fail ( `Map.put cannot be used to set empty values` ) ; if ( isStateTreeNode ( value ) ) { const node = getStateTreeNode ( value ) ; if ( devMode ( ) ) { if ( ! node . identifierAttribute ) { throw fail ( needsIdentifierError ) ; } } if ( node . identifier === null ) { throw fail ( needsIdentifierError ) ; } this . set ( node . identifier , value ) ; return value as any ; } else if ( ! isMutable ( value ) ) { throw fail ( `Map.put can only be used to store complex values` ) ; } else { const mapNode = getStateTreeNode ( this as IAnyStateTreeNode ) ; const mapType = mapNode . type as MapType < any > ; if ( mapType . identifierMode !== MapIdentifierMode . YES ) { throw fail ( needsIdentifierError ) ; } const idAttr = mapType . mapIdentifierAttribute ! ; const id = ( value as any ) [ idAttr ] ; if ( ! isValidIdentifier ( id ) ) { const newNode = this . put ( mapType . getChildType ( ) . create ( value , mapNode . environment ) ) ; return this . put ( getSnapshot ( newNode ) ) ; } const key = normalizeIdentifier ( id ) ; this . set ( key , value ) ; return this . get ( key ) as any ; } } } internal hidden export class MapType < IT extends IAnyType > extends ComplexType < IKeyValueMap < IT [ "CreationType" ] > | undefined , IKeyValueMap < IT [ "SnapshotType" ] > , IMSTMap < IT > > { identifierMode : MapIdentifierMode = MapIdentifierMode . UNKNOWN ; mapIdentifierAttribute : string | undefined = undefined ; readonly flags = TypeFlags . Map ; private readonly hookInitializers : Array < IHooksGetter < IMSTMap < IT > > > = [ ] ; constructor ( name , private readonly _subType , hookInitializers = [ ] ) { super ( name ) ; this . _determineIdentifierMode ( ) ; this . hookInitializers = hookInitializers ; } hooks ( hooks ) { const hookInitializers = this . hookInitializers . length > 0 ? this . hookInitializers . concat ( hooks ) : [ hooks ] ; return new MapType ( this . name , this . _subType , hookInitializers ) ; } instantiate ( parent , subpath , environment , initialValue ) { this . _determineIdentifierMode ( ) ; return createObjectNode ( this , parent , subpath , environment , initialValue ) ; } private _determineIdentifierMode ( ) { if ( this . identifierMode !== MapIdentifierMode . UNKNOWN ) { return ; } const modelTypes = [ ] ; if ( tryCollectModelTypes ( this . _subType , modelTypes ) ) { const identifierAttribute = modelTypes . reduce ( ( current , type ) => { if ( ! type . identifierAttribute ) return current ; if ( current && current !== type . identifierAttribute ) { throw fail ( `The objects in a map should all have the same identifier attribute, expected '${ current }', but child of type '${ type . name }' declared attribute '${ type . identifierAttribute }' as identifier` ) ; } return type . identifierAttribute ; } , undefined as IAnyModelType [ "identifierAttribute" ] ) ; if ( identifierAttribute ) { this . identifierMode = MapIdentifierMode . YES ; this . mapIdentifierAttribute = identifierAttribute ; } else { this . identifierMode = MapIdentifierMode . NO ; } } } initializeChildNodes ( objNode , initialSnapshot = { } ) { const subType = ( objNode . type as this ) . _subType ; const result = { } ; Object . keys ( initialSnapshot ! ) . forEach ( ( name ) => { result [ name ] = subType . instantiate ( objNode , name , undefined , initialSnapshot [ name ] ) ; } ) ; return result ; } createNewInstance ( childNodes ) { return new MSTMap ( childNodes ) as any ; } finalizeNewInstance ( node , instance ) { _interceptReads ( instance , node . unbox ) ; const type = node . type as this ; type . hookInitializers . forEach ( ( initializer ) => { const hooks = initializer ( ( instance as unknown ) as IMSTMap < IT > ) ; Object . keys ( hooks ) . forEach ( ( name ) => { const hook = hooks [ name as keyof typeof hooks ] ! ; const actionInvoker = createActionInvoker ( instance as IAnyStateTreeNode , name , hook ) ; ( ! devMode ( ) ? addHiddenFinalProp : addHiddenWritableProp ) ( instance , name , actionInvoker ) ; } ) ; } ) ; intercept ( instance , this . willChange ) ; observe ( instance , this . didChange ) ; } describe ( ) { return "Map<string, " + this . _subType . describe ( ) + ">" ; } getChildren ( node ) { return values ( node . storedValue ) ; } getChildNode ( node , key ) { const childNode = node . storedValue . get ( "" + key ) ; if ( ! childNode ) throw fail ( "Not a child " + key ) ; return childNode ; } willChange ( change ) { const node = getStateTreeNode ( change . object as IAnyStateTreeNode ) ; const key = change . name ; node . assertWritable ( { subpath : key } ) ; const mapType = node . type as this ; const subType = mapType . _subType ; switch ( change . type ) { case "update" : { const { newValue } = change ; const oldValue = change . object . get ( key ) ; if ( newValue === oldValue ) return null ; typecheckInternal ( subType , newValue ) ; change . newValue = subType . reconcile ( node . getChildNode ( key ) , change . newValue , node , key ) ; mapType . processIdentifier ( key , change . newValue ) ; } break ; case "add" : { typecheckInternal ( subType , change . newValue ) ; change . newValue = subType . instantiate ( node , key , undefined , change . newValue ) ; mapType . processIdentifier ( key , change . newValue ) ; } break ; } return change ; } private processIdentifier ( expected , node ) { if ( this . identifierMode === MapIdentifierMode . YES && node instanceof ObjectNode ) { const identifier = node . identifier ! ; if ( identifier !== expected ) throw fail ( `A map of objects containing an identifier should always store the object under their own identifier. Trying to store key '${ identifier }', but expected: '${ expected }'` ) ; } } getSnapshot ( node ) { const res = { } ; node . getChildren ( ) . forEach ( ( childNode ) => { res [ childNode . subpath ] = childNode . snapshot ; } ) ; return res ; } processInitialSnapshot ( childNodes ) { const processed = { } ; Object . keys ( childNodes ) . forEach ( ( key ) => { processed [ key ] = childNodes [ key ] . getSnapshot ( ) ; } ) ; return processed ; } didChange ( change ) { const node = getStateTreeNode ( change . object as IAnyStateTreeNode ) ; switch ( change . type ) { case "update" : return void node . emitPatch ( { op : "replace" , path : escapeJsonPath ( change . name ) , value : change . newValue . snapshot , oldValue : change . oldValue ? change . oldValue . snapshot : undefined } , node ) ; case "add" : return void node . emitPatch ( { op : "add" , path : escapeJsonPath ( change . name ) , value : change . newValue . snapshot , oldValue : undefined } , node ) ; case "delete" : const oldSnapshot = change . oldValue . snapshot ; change . oldValue . die ( ) ; return void node . emitPatch ( { op : "remove" , path : escapeJsonPath ( change . name ) , oldValue : oldSnapshot } , node ) ; } } applyPatchLocally ( node , subpath , patch ) { const target = node . storedValue ; switch ( patch . op ) { case "add" : case "replace" : target . set ( subpath , patch . value ) ; break ; case "remove" : target . delete ( subpath ) ; break ; } } applySnapshot ( node , snapshot ) { typecheckInternal ( this , snapshot ) ; const target = node . storedValue ; const currentKeys = { } ; Array . from ( target . keys ( ) ) . forEach ( ( key ) => { currentKeys [ key ] = false ; } ) ; if ( snapshot ) { for ( let key in snapshot ) { target . set ( key , snapshot [ key ] ) ; currentKeys [ "" + key ] = true ; } } Object . keys ( currentKeys ) . forEach ( ( key ) => { if ( currentKeys [ key ] === false ) target . delete ( key ) ; } ) ; } getChildType ( ) { return this . _subType ; } isValidSnapshot ( value , context ) { if ( ! isPlainObject ( value ) ) { return typeCheckFailure ( context , value , "Value is not a plain object" ) ; } return flattenTypeErrors ( Object . keys ( value ) . map ( ( path ) => this . _subType . validate ( value [ path ] , getContextForPath ( context , path , this . _subType ) ) ) ) ; } getDefaultSnapshot ( ) { return EMPTY_OBJECT as this [ "C" ] ; } removeChild ( node , subpath ) { node . storedValue . delete ( subpath ) ; } } MapType . prototype . applySnapshot = action ( MapType . prototype . applySnapshot ) ; param subtype returns export function map < IT extends IAnyType > ( subtype ) { return new MapType < IT > ( `map<string, ${ subtype . name }>` , subtype ) ; } param type returns export function isMapType < Items extends IAnyType = IAnyType > ( type ) { return isType ( type ) && ( type . flags & TypeFlags . Map ) > 0 ; }