author import { Fetcher } from "./Fetcher" ; import { FetchableType } from "./Fetchable" ; export class DependencyManager { private rootTypeResourceMap = new Map < string , Resources > ( ) ; private fieldResourceMap = new Map < string , Map < string , Resources > > ( ) ; private _idGetter : ( obj : any ) => any ; constructor ( idGetter ? ) { ( window as any ) . dependencyManager = this ; this . _idGetter = idGetter ?? getDefaultId ; } register ( resource , fetcher , fieldDependencies ? ) { if ( fieldDependencies !== undefined ) { this . registerTypes ( resource , [ fetcher , ... fieldDependencies ] ) ; this . registerFields ( resource , fieldDependencies ) ; } else { this . registerTypes ( resource , [ fetcher ] ) ; } } unregister ( resource ) { removeResource ( this . rootTypeResourceMap , resource ) ; removeResource ( this . fieldResourceMap , resource ) ; } resources < TObject extends object > ( fetcher , oldObject , newObject ) { const resources = new Set < string > ( ) ; this . collectResources ( fetcher , nullToUndefined ( oldObject ) , nullToUndefined ( newObject ) , resources ) ; return Array . from ( resources ) ; } allResources ( fetcher ) { const resources = new Set < string > ( ) ; this . collectAllResources ( fetcher , resources ) ; return Array . from ( resources ) ; } private registerTypes ( resource , fetchers ) { for ( const fetcher of fetchers ) { const isOperation = isOperationFetcher ( fetcher ) ; for ( const [ fieldName , field ] of fetcher . fieldMap ) { if ( ! fieldName . startsWith ( "..." ) ) { const declaringTypeNames = getDeclaringTypeNames ( fieldName , fetcher ) ; for ( const declaringTypeName of declaringTypeNames ) { compute ( this . rootTypeResourceMap , declaringTypeName , ( ) => new Resources ( ) ) . retain ( resource ) ; } } if ( field . childFetchers !== undefined ) { this . registerTypes ( resource , field . childFetchers ) ; } } } } private registerFields ( resource , fetchers ) { for ( const fetcher of fetchers ) { const isOperation = isOperationFetcher ( fetcher ) ; for ( const [ fieldName , field ] of fetcher . fieldMap ) { if ( ! isOperation && ! fieldName . startsWith ( "..." ) ) { const subMap = compute ( this . fieldResourceMap , fieldName , ( ) => new Map < string , Resources > ( ) ) ; const declaringTypeNames = getDeclaringTypeNames ( fieldName , fetcher ) ; for ( const declaringTypeName of declaringTypeNames ) { compute ( subMap , declaringTypeName , ( ) => new Resources ( ) ) . retain ( resource ) ; } } if ( field . childFetchers !== undefined ) { this . registerFields ( resource , field . childFetchers ) ; } } } } private collectResources ( fetcher , oldObject , newObject , output ) { if ( oldObject === newObject ) { return ; } if ( oldObject === undefined || newObject === undefined ) { this . rootTypeResourceMap . get ( fetcher . fetchableType . name ) ?. copyTo ( output ) ; } else if ( ! isOperationFetcher ( fetcher ) ) { const oldId = this . _idGetter ( oldObject ) ; const newId = this . _idGetter ( newObject ) ; if ( oldId !== newId ) { this . rootTypeResourceMap . get ( fetcher . fetchableType . name ) ?. copyTo ( output ) ; } } for ( const [ fieldName , field ] of fetcher . fieldMap ) { if ( fieldName . startsWith ( "..." ) ) { if ( field . childFetchers !== undefined ) { for ( const childFetcher of field . childFetchers ) { this . collectResources ( childFetcher , oldObject , newObject , output ) ; } } } else { const oldValue = nullToUndefined ( oldObject !== undefined ? oldObject [ fieldName ] : undefined ) ; const newValue = nullToUndefined ( newObject !== undefined ? newObject [ fieldName ] : undefined ) ; if ( field . childFetchers !== undefined && field . childFetchers . length !== 0 ) { if ( oldValue !== newValue ) { for ( const childFetcher of field . childFetchers ) { this . collectResourcesByAssocaiton ( fetcher , fieldName , childFetcher , oldValue , newValue , output ) ; } } } else if ( ! scalarEqual ( oldValue , newValue ) ) { const subMap = this . fieldResourceMap . get ( fieldName ) ; if ( subMap !== undefined ) { const declaringTypeNames = getDeclaringTypeNames ( fieldName , fetcher ) ; for ( const declaringTypeName of declaringTypeNames ) { subMap . get ( declaringTypeName ) ?. copyTo ( output ) ; } } } } } } private collectResourcesByAssocaiton ( parentFetcher , fieldName , childFetcher , oldAssociation , newAssociation , output ) { if ( oldAssociation === undefined || newAssociation === undefined || Array . isArray ( oldAssociation ) !== Array . isArray ( newAssociation ) ) { if ( Array . isArray ( oldAssociation ) ) { for ( const element of oldAssociation ) { this . collectResources ( childFetcher , nullToUndefined ( element ) , undefined , output ) ; } } else if ( typeof oldAssociation === 'object' ) { this . collectResources ( childFetcher , oldAssociation , undefined , output ) ; } if ( Array . isArray ( newAssociation ) ) { for ( const element of newAssociation ) { this . collectResources ( childFetcher , undefined , nullToUndefined ( element ) , output ) ; } } else if ( typeof newAssociation === 'object' ) { this . collectResources ( childFetcher , undefined , newAssociation , output ) ; } } else if ( Array . isArray ( oldAssociation ) && Array . isArray ( newAssociation ) ) { const map1 = associatedBy ( oldAssociation , this . _idGetter ) ; const map2 = associatedBy ( newAssociation , this . _idGetter ) ; for ( const [ k , o1 ] of map1 ) { const o2 = map2 . get ( k ) ; this . collectResources ( childFetcher , nullToUndefined ( o1 ) , nullToUndefined ( o2 ) , output ) ; } for ( const [ k , o2 ] of map2 ) { if ( ! map2 . has ( k ) ) { this . collectResources ( childFetcher , undefined , nullToUndefined ( o2 ) , output ) ; } } } else if ( typeof oldAssociation === 'object' && typeof newAssociation === 'object' ) { this . collectResources ( childFetcher , oldAssociation , newAssociation , output ) ; } else { const declaringType = getDeclaringTypeNames ( fieldName , parentFetcher ) [ 0 ] ; console . warn ( `Illegal data, cannot compare the data ${ oldAssociation } and ${ newAssociation } for the assocaiton ${ declaringType }.${ fieldName }` ) ; } } private collectAllResources ( fetcher , output ) { this . rootTypeResourceMap . get ( fetcher . fetchableType . name ) ?. copyTo ( output ) ; for ( const [ fieldName , field ] of fetcher . fieldMap ) { if ( ! fieldName . startsWith ( "..." ) ) { const declaringTypes = getDeclaringTypeNames ( fieldName , fetcher ) ; for ( const declaringType of declaringTypes ) { this . rootTypeResourceMap . get ( declaringType ) ?. copyTo ( output ) ; this . fieldResourceMap . get ( fieldName ) ?. get ( declaringType ) ?. copyTo ( output ) ; } } if ( field . childFetchers !== undefined ) { for ( const childFetcher of field . childFetchers ) { this . collectAllResources ( childFetcher , output ) ; } } } } } class Resources { private refCountMap = new Map < string , number > ( ) ; retain ( resource ) { this . refCountMap . set ( resource , ( this . refCountMap . get ( resource ) ?? 0 ) + 1 ) ; } release ( resource ) { const refCount = this . refCountMap . get ( resource ) ; if ( refCount !== undefined ) { if ( refCount > 1 ) { this . refCountMap . set ( resource , refCount - 1 ) ; } else { this . refCountMap . delete ( resource ) ; } } return this . refCountMap . size ; } copyTo ( output ) { for ( const [ resource ] of this . refCountMap ) { output . add ( resource ) ; } } } function isOperationFetcher ( fetcher ) { const fetcherName = fetcher . fetchableType . name ; return fetcherName === "Query" || fetcherName === 'Mutation' ; } function getDeclaringTypeNames ( fieldName , fetcher ) { const declaringTypeNames = new Set < string > ( ) ; if ( fieldName !== '' && fieldName !== '...' ) { collectDeclaringTypeNames ( fieldName , fetcher . fetchableType , declaringTypeNames ) ; } return declaringTypeNames ; } function collectDeclaringTypeNames ( fieldName , fetchableType , output ) { if ( fetchableType . declaredFields . has ( fieldName ) ) { output . add ( fetchableType . name ) ; } else { for ( const superType of fetchableType . superTypes ) { collectDeclaringTypeNames ( fieldName , superType , output ) ; } } } function getDefaultId ( value ) { const id = value . id ?? value . _id ; if ( id === undefined ) { throw Error ( `There is no id/_id in the object ${ JSON . stringify ( value ) }` ) ; } return id ; } function compute < K , V > ( map , key , valueSupplier ) { let value = map . get ( key ) ; if ( value === undefined ) { map . set ( key , value = valueSupplier ( key ) ) ; } return value ; } type RecursiveResourceMap = Map < string , Resources | RecursiveResourceMap > ; function removeResource ( recursiveResourceMap , resource ) { const deletedKeys = new Set < string > ( ) ; for ( const [ key , deeperValue ] of recursiveResourceMap ) { if ( deeperValue instanceof Resources ) { if ( deeperValue . release ( resource ) === 0 ) { deletedKeys . add ( key ) ; } } else { removeResource ( deeperValue , resource ) ; if ( deeperValue . size === 0 ) { deletedKeys . add ( key ) ; } } } for ( const deletedKey of deletedKeys ) { recursiveResourceMap . delete ( deletedKey ) ; } } function associatedBy < K , V > ( values , keyGetter ) { const map = new Map < K , V > ( ) ; for ( const value of values ) { if ( value !== undefined && value !== null ) { const key = keyGetter ( value ) ; map . set ( key , value ) ; } } return map ; } function scalarEqual ( left , right ) { if ( Array . isArray ( left ) && Array . isArray ( right ) ) { if ( left . length !== right . length ) { return false ; } for ( let i = left . length - 1 ; i >= 0 ; -- i ) { if ( nullToUndefined ( left [ i ] ) !== nullToUndefined ( right [ i ] ) ) { return false ; } } return true ; } return left === right ; } function nullToUndefined < T > ( value ) { return value === null ? undefined : value ; }