import { illegalState } from './base/errors' ; import { Graph } from './graph' ; import { Descriptor } from './descriptors' ; import { ServiceIdentifier , IInstantiationService , ServicesAccessor , _util , optional } from './instantiation' ; import { ServiceCollection } from './serviceCollection' ; import { IdleValue } from './base/async' ; let _enableTracing = false ; let _logFunction ; export function setTracingEnabled ( enabled , logFunction ? ) { _enableTracing = enabled ; _logFunction = logFunction ?? ( ( msg ) => console . log ( msg ) ) ; } declare const Proxy ; const _canUseProxy = typeof Proxy === 'function' ; class CyclicDependencyError extends Error { constructor ( graph ) { super ( 'cyclic dependency between services' ) ; this . message = graph . toString ( ) ; } } internal export class InstantiationService implements IInstantiationService { _serviceBrand : undefined ; private readonly _services : ServiceCollection ; private readonly _strict : boolean ; private readonly _parent ? : InstantiationService ; constructor ( services = new ServiceCollection ( ) , strict = false , parent ? ) { this . _services = services ; this . _strict = strict ; this . _parent = parent ; this . _services . set ( IInstantiationService , this ) ; } createChild ( services ) { return new InstantiationService ( services , this . _strict , this ) ; } invokeFunction < R , TS extends any [ ] = [ ] > ( fn , ... args ) { let _trace = Trace . traceInvocation ( fn ) ; let _done = false ; try { const accessor = { get : < T > ( id , isOptional ? ) => { if ( _done ) { throw illegalState ( 'service accessor is only valid during the invocation of its target method' ) ; } const result = this . _getOrCreateServiceInstance ( id , _trace ) ; if ( ! result && isOptional !== optional ) { throw new Error ( `[invokeFunction] unknown service '${ id }'` ) ; } return result ; } } ; return fn ( accessor , ... args ) ; } finally { _done = true ; _trace . stop ( ) ; } } createInstance ( ctorOrDescriptor , ... rest ) { let _trace ; let result ; if ( ctorOrDescriptor instanceof Descriptor ) { _trace = Trace . traceCreation ( ctorOrDescriptor . ctor ) ; result = this . _createInstance ( ctorOrDescriptor . ctor , ctorOrDescriptor . staticArguments . concat ( rest ) , _trace ) ; } else { _trace = Trace . traceCreation ( ctorOrDescriptor ) ; result = this . _createInstance ( ctorOrDescriptor , rest , _trace ) ; } _trace . stop ( ) ; return result ; } private _createInstance < T > ( ctor , args = [ ] , _trace ) { let serviceDependencies = _util . getServiceDependencies ( ctor ) . sort ( ( a , b ) => a . index - b . index ) ; let serviceArgs = [ ] ; for ( const dependency of serviceDependencies ) { let service = this . _getOrCreateServiceInstance ( dependency . id , _trace ) ; if ( ! service && this . _strict && ! dependency . optional ) { throw new Error ( `[createInstance] ${ ctor . name } depends on UNKNOWN service ${ dependency . id }.` ) ; } serviceArgs . push ( service ) ; } let firstServiceArgPos = serviceDependencies . length > 0 ? serviceDependencies [ 0 ] . index : args . length ; if ( args . length !== firstServiceArgPos ) { console . warn ( `[createInstance] First service dependency of ${ ctor . name } at position ${ firstServiceArgPos + 1 } conflicts with ${ args . length } static arguments` ) ; let delta = firstServiceArgPos - args . length ; if ( delta > 0 ) { args = args . concat ( new Array ( delta ) ) ; } else { args = args . slice ( 0 , firstServiceArgPos ) ; } } return < T > new ctor ( ... [ ... args , ... serviceArgs ] ) ; } private _setServiceInstance < T > ( id , instance ) { if ( this . _services . get ( id ) instanceof Descriptor ) { this . _services . set ( id , instance ) ; } else if ( this . _parent ) { this . _parent . _setServiceInstance ( id , instance ) ; } else { throw new Error ( 'illegalState - setting UNKNOWN service instance' ) ; } } private _getServiceInstanceOrDescriptor < T > ( id ) { let instanceOrDesc = this . _services . get ( id ) ; if ( ! instanceOrDesc && this . _parent ) { return this . _parent . _getServiceInstanceOrDescriptor ( id ) ; } else { return instanceOrDesc ; } } private _getOrCreateServiceInstance < T > ( id , _trace ) { let thing = this . _getServiceInstanceOrDescriptor ( id ) ; if ( thing instanceof Descriptor ) { return this . _createAndCacheServiceInstance ( id , thing , _trace . branch ( id , true ) ) ; } else { _trace . branch ( id , false ) ; return thing ; } } private _createAndCacheServiceInstance < T > ( id , desc , _trace ) { type Triple = { id : ServiceIdentifier < any > ; desc : Descriptor < any > ; _trace : Trace ; } ; const graph = new Graph < Triple > ( data => data . id . toString ( ) ) ; let cycleCount = 0 ; const stack = [ { id , desc , _trace } ] ; while ( stack . length ) { const item = stack . pop ( ) ! ; graph . lookupOrInsertNode ( item ) ; if ( cycleCount ++ > 150 ) { throw new CyclicDependencyError ( graph ) ; } for ( let dependency of _util . getServiceDependencies ( item . desc . ctor ) ) { let instanceOrDesc = this . _getServiceInstanceOrDescriptor ( dependency . id ) ; if ( ! instanceOrDesc && ! dependency . optional ) { console . warn ( `[createInstance] ${ id } depends on ${ dependency . id } which is NOT registered.` ) ; } if ( instanceOrDesc instanceof Descriptor ) { const d = { id : dependency . id , desc : instanceOrDesc , _trace : item . _trace . branch ( dependency . id , true ) } ; graph . insertEdge ( item , d ) ; stack . push ( d ) ; } } } while ( true ) { const roots = graph . roots ( ) ; if ( roots . length === 0 ) { if ( ! graph . isEmpty ( ) ) { throw new CyclicDependencyError ( graph ) ; } break ; } for ( const { data } of roots ) { const instance = this . _createServiceInstanceWithOwner ( data . id , data . desc . ctor , data . desc . staticArguments , data . desc . supportsDelayedInstantiation , data . _trace ) ; this . _setServiceInstance ( data . id , instance ) ; graph . removeNode ( data ) ; } } return < T > this . _getServiceInstanceOrDescriptor ( id ) ; } private _createServiceInstanceWithOwner < T > ( id , ctor , args = [ ] , supportsDelayedInstantiation , _trace ) { if ( this . _services . get ( id ) instanceof Descriptor ) { return this . _createServiceInstance ( ctor , args , supportsDelayedInstantiation , _trace ) ; } else if ( this . _parent ) { return this . _parent . _createServiceInstanceWithOwner ( id , ctor , args , supportsDelayedInstantiation , _trace ) ; } else { throw new Error ( `illegalState - creating UNKNOWN service instance ${ ctor . name }` ) ; } } private _createServiceInstance < T > ( ctor , args = [ ] , _supportsDelayedInstantiation , _trace ) { if ( ! _supportsDelayedInstantiation || ! _canUseProxy ) { return this . _createInstance ( ctor , args , _trace ) ; } else { const idle = new IdleValue < any > ( ( ) => this . _createInstance < T > ( ctor , args , _trace ) ) ; return < T > new Proxy ( Object . create ( null ) , { get ( target , key ) { if ( key in target ) { return target [ key ] ; } let obj = idle . getValue ( ) ; let prop = obj [ key ] ; if ( typeof prop !== 'function' ) { return prop ; } prop = prop . bind ( obj ) ; target [ key ] = prop ; return prop ; } , set ( _target , p , value ) { idle . getValue ( ) [ p ] = value ; return true ; } } ) ; } } } const enum TraceType { Creation , Invocation , Branch } class Trace { private static readonly _None = new class extends Trace { constructor ( ) { super ( - 1 , null ) ; } stop ( ) { } branch ( ) { return this ; } } ; static traceInvocation ( ctor ) { return ! _enableTracing ? Trace . _None : new Trace ( TraceType . Invocation , ctor . name || ( ctor . toString ( ) as string ) . substring ( 0 , 42 ) . replace ( /\n/g , '' ) ) ; } static traceCreation ( ctor ) { return ! _enableTracing ? Trace . _None : new Trace ( TraceType . Creation , ctor . name ) ; } private static _totals : number = 0 ; private readonly _start : number = Date . now ( ) ; private readonly _dep : [ ServiceIdentifier < any > , boolean , Trace ? ] [ ] = [ ] ; private constructor ( readonly type , readonly name ) { } branch ( id , first ) { let child = new Trace ( TraceType . Branch , id . toString ( ) ) ; this . _dep . push ( [ id , first , child ] ) ; return child ; } stop ( ) { let dur = Date . now ( ) - this . _start ; Trace . _totals += dur ; let causedCreation = false ; function printChild ( n , trace ) { let res = [ ] ; let prefix = new Array ( n + 1 ) . join ( '\t' ) ; for ( const [ id , first , child ] of trace . _dep ) { if ( first && child ) { causedCreation = true ; res . push ( `${ prefix }CREATES -> ${ id }` ) ; let nested = printChild ( n + 1 , child ) ; if ( nested ) { res . push ( nested ) ; } } else { res . push ( `${ prefix }uses -> ${ id }` ) ; } } return res . join ( '\n' ) ; } let lines = [ `${ this . type === TraceType . Creation ? 'CREATE' : 'CALL' } ${ this . name }` , `${ printChild ( 1 , this ) }` , `DONE, took ${ dur . toFixed ( 2 ) }ms (grand total ${ Trace . _totals . toFixed ( 2 ) }ms)` ] ; if ( dur > 2 || causedCreation ) { _logFunction ( lines . join ( '\n' ) ) ; } } }