sourceafIoc::Scope.fan

using concurrent::AtomicBool

** (Service) -
** Creates and manages service instances, and performs dependency injection.
** Scopes may also create child scopes.
** 
** Scopes may be dependency injected. 
** Use standard injection to receive the Scope used to create the class instance.
** 
**   syntax: fantom
**   @Inject
**   private Scope scope
** 
** Or to always receive the current active Scope, use a Lazy Func.
**  
**   syntax: fantom
**   @Inject
**   private |->Scope| scope
** 
@Js
const mixin Scope {

    ** Returns the unique 'id' of this Scope. 
    abstract Str        id()
    
    ** Returns the parent scope.
    abstract Scope?     parent()
    
    ** Returns the registry instance this scope belongs to.
    abstract Registry   registry()
    
    ** Returns 'true' if this scope is threaded and may hold non-const services.
    abstract Bool isThreaded()
    
    ** Autobuilds an instance of the given type. Autobuilding performs the following:
    ** 
    **  - creates an instance via the ctor marked with '@Inject' or the *best* fitting ctor with the most parameters
    **  - inject dependencies into fields (of all visibilities)
    **  - calls any method on the class annotated with '@PostInjection'
    ** 
    ** 'ctorArgs' (if provided) will be passed as arguments to the constructor.
    ** Constructor parameters should be defined in the following order:
    ** 
    **   new make(<config>, <ctorArgs>, <dependencies>, <it-block>) { ... }
    ** 
    ** Note that 'fieldVals' are set by an it-block function, should the ctor define one.
    abstract Obj? build(Type type, Obj?[]? ctorArgs := null, [Field:Obj?]? fieldVals := null)
    
    ** Injects services and dependencies into fields of all visibilities and
    ** calls any method on the class annotated with '@PostInjection'.
    ** 
    ** Returns the object passed in for method chaining.
    abstract Obj inject(Obj obj)
    
    ** Calls the given method. Any method arguments not given are resolved as dependencies. 
    ** 'instance' may be 'null' if calling a static method.
    ** Method parameters should be defined in the following order:
    ** 
    ** 
    **   Void myMethod(<args>, <dependencies>, <default params>) { ... }
    ** 
    ** Note that nullable and default parameters are treated as optional dependencies.
    ** 
    ** Returns the result of calling the method.
    abstract Obj? callMethod(Method method, Obj? instance, Obj?[]? args := null)

    ** Calls the given func. Any func arguments not given are resolved as dependencies. 
    ** Func parameters should be defined in the following order:
    ** 
    **   |<args>, <dependencies>, <default params>| { ... }
    ** 
    ** Note that nullable and default parameters are treated as optional dependencies.
    ** 
    ** Returns the result of calling the func.
    abstract Obj? callFunc(Func func, Obj?[]? args := null)

    ** Resolves a service by its ID. Throws 'IocErr' if the service is not found, unless 'checked' is 'false'.
    abstract Obj? serviceById(Str serviceId, Bool checked := true)

    ** Resolves a service by its Type. Throws 'IocErr' if the service is not found, unless 'checked' is 'false'.
    abstract Obj? serviceByType(Type serviceType, Bool checked := true)
    
    ** Creates a nested child scope and makes it available to the given function,
    ** which is called straight away.
    ** The child scope also becomes the *active* scope for the duration of the function.
    ** 
    ** pre>
    ** syntax: fantom
    ** scope.createChild("childScopeId") |Scope childScope| {
    **     ...
    ** }
    ** <pre
    ** 
    ** When a function is passed in then 'null' is returned because the scope would not be valid 
    ** outside of the function.
    ** 
    ** Advanced users may create *non-active* scopes by **not** passing in a function and
    ** using the returned scope. Non-active scopes must be manually destroyed.
    ** 
    ** pre>
    ** syntax: fantom
    ** myScope := scope.createChild("myScope")
    ** 
    ** ... use myScope ...
    ** 
    ** myScope.destroy
    ** <pre
    ** 
    ** To create an active scope that remains active outside of the closure, use [jailBreak()]`jailBreak`. 
    abstract Scope? createChild(Str scopeId, |Scope|? f := null)

    ** *(Advanced Use Only)*
    ** 
    ** Jail breaks an active scope so it remains *active* outside its closure. 
    ** Jail broken scopes are not destroyed so you are responsible for calling 'destroy()' yourself.
    ** 
    ** pre>
    ** syntax: fantom
    ** childScope := (Scope?) null
    ** 
    ** scope.createChild("childScopeId") |childScopeInClosure| {
    **     childScope = childScopeInClosure.jailbreak
    ** }
    ** 
    ** // --> "childScopeId" is still active!
    ** echo(scope.registry.activeScope)
    ** 
    ** ... use childScope here ...
    ** childScope.serviceByType(...)
    ** 
    ** childScope.destroy
    ** <pre
    ** 
    ** Note that jail broken scopes are only active in the current thread, but will remain the active 
    ** scope even if the thread is re-entered. 
    abstract This jailBreak()
    
    ** *(Advanced Use Only)*
    ** 
    ** Destroys this scope and releases references to any services created. Calls any scope destroy hooks. 
    ** Pops this scope off the active stack.
    ** 
    ** 'destroy()' does nothing if called more than once.
    abstract Void destroy()
    
    ** *(Advanced Use Only)*
    ** 
    ** Runs the given function with this 'Scope' as the active one. Note this method does *not* destroy the scope. 
    ** 
    ** Under normal usage, consider using 'createChild(...)' instead.
    abstract Void asActive(|Scope| f)

    ** Returns 'true' if this 'Scope' has been destroyed.
    abstract Bool isDestroyed()

    ** Returns a recursive list of all the Scopes this Scope inherits from.
    ** The result list always starts with this Scope itself.
    ** 
    ** syntax: fantom
    ** scope.inheritance() // --> ui, root, builtIn
    abstract Scope[] inheritance()
}


@Js
internal const class ScopeImpl : Scope {
    private  const OneShotLock      destroyedLock
    private  const ServiceStore     serviceStore
    private  const AtomicBool       jailBroken      := AtomicBool(false)
    internal const ScopeDefImpl     scopeDef
    override const RegistryImpl     registry
    override const ScopeImpl?       parent
    
    internal new make(RegistryImpl registry, ScopeImpl? parent, ScopeDefImpl scopeDef) {
        this.registry       = registry
        this.scopeDef       = scopeDef
        this.parent         = parent
        this.serviceStore   = ServiceStore(registry, scopeDef.id)
        this.destroyedLock  = OneShotLock(ErrMsgs.scopeDestroyed(id), ScopeDestroyedErr#)       
    }

    override Str id() {
        scopeDef.id
    }
    
    override Bool isThreaded() {
        scopeDef.threaded
    }

    override Scope[] inheritance() {
        scope  := (Scope?) this
        scopes := Scope[,]
        while (scope != null) {
            scopes.add(scope)
            scope = scope.parent
        }
        return scopes
    }

    override Obj? build(Type type, Obj?[]? ctorArgs := null, [Field:Obj?]? fieldVals := null) {
        destroyedCheck

        registry.opStack.push("Building", type.qname)
        try return registry.autoBuilder.autobuild(this, type, ctorArgs, fieldVals, null)
        catch (IocErr ie)   throw ie
        catch (Err err)     throw IocErr(err.msg, err)
        finally registry.opStack.pop
    }

    override Obj inject(Obj instance) {
        destroyedCheck

        registry.opStack.push("Injecting", instance.typeof.qname)
        try {
            plan := registry.autoBuilder.findFieldVals(this, instance.typeof, instance, null, null)         
            plan.each |val, field| {
                field.set(instance, field.isConst ? val.toImmutable : val)
            }
            registry.autoBuilder.callPostInjectionMethods(this, null, instance, instance.typeof)
            return instance
        }
        catch (IocErr ie)   throw ie
        catch (Err err)     throw IocErr(err.msg, err)
        finally registry.opStack.pop
    }

    override Obj? callMethod(Method method, Obj? instance, Obj?[]? args := null) {
        destroyedCheck

        registry.opStack.push("Calling method", method.qname)
        try {
            methodArgs := registry.autoBuilder.findFuncArgs(this, method.func, args, instance, null)
            return method.callOn(instance, methodArgs)
        }
        catch (IocErr ie)   throw ie
        catch (Err err)     throw IocErr(err.msg, err)
        finally registry.opStack.pop
    }

    override Obj? callFunc(Func func, Obj?[]? args := null) {
        destroyedCheck
        if (func.typeof.isGeneric)
            throw ArgErr("Can not call generic functions: ${func.typeof.signature}")

        registry.opStack.push("Calling func", func.typeof.signature)
        try {
            funcArgs := registry.autoBuilder.findFuncArgs(this, func, args, null, null)
            return func.callList(funcArgs)
        }
        catch (IocErr ie)   throw ie
        catch (Err err)     throw IocErr(err.msg, err)
        finally registry.opStack.pop
    }
    
    override Obj? serviceById(Str serviceId, Bool checked := true) {
        destroyedCheck      
        
        registry.opStack.push("Resolving ID", serviceId)
        registry.opStack.setServiceId(serviceId)
        try     return serviceById_(serviceId, Str[,], checked)
        catch   (IocErr ie) throw ie
        catch   (Err err)   throw IocErr(err.msg, err)
        finally registry.opStack.pop
    }

    internal Obj? serviceById_(Str serviceId, Str[] scopes, Bool checked := true) {
        serviceInstance := serviceStore.instanceById(serviceId)
        if (serviceInstance == null)
            return parent?.serviceById_(serviceId, scopes.add(id), checked)
                ?: (checked ? throw ServiceNotFoundErr(ErrMsgs.scope_couldNotFindServiceById(serviceId, scopes.dup.add(id).reverse), services(scopes.dup.add(id).reverse)) : null)
        return serviceInstance.getOrBuild(this)
    }

    override Obj? serviceByType(Type serviceType, Bool checked := true) {
        destroyedCheck

        registry.opStack.push("Resolving Type", serviceType.qname)
        try     return serviceByType_(serviceType, Str[,], checked)
        catch   (IocErr ie) throw ie
        catch   (Err err)   throw IocErr(err.msg, err)
        finally registry.opStack.pop
    }

    internal Obj? serviceByType_(Type serviceType, Str[] scopes, Bool checked := true) {
        serviceInstance := serviceStore.instanceByType(serviceType)
        if (serviceInstance == null)
            return parent?.serviceByType_(serviceType, scopes.add(id), checked)
                ?: (checked ? throw ServiceNotFoundErr(ErrMsgs.scope_couldNotFindServiceByType(serviceType, scopes.dup.add(id).reverse), services(scopes.dup.add(id).reverse)) : null)
        registry.opStack.setServiceId(serviceInstance.def.id)
        return serviceInstance.getOrBuild(this)         
    }
    
    override Scope? createChild(Str scopeId, |Scope|? f := null) {
        destroyedCheck

        childScopeDef   := registry.findScopeDef(scopeId, this)
        childScope      := ScopeImpl(registry, this, childScopeDef)
        
        if (f != null)
            registry.activeScopeStack.push(childScope)
        else
            // this isn't strictly needed, for it will never be checked, but it doesn't hurt to keep everything in check
            childScope.jailBroken.val = true

        errors := null as Err[]
        try {
            // if createHooks errors, we should still call destroyHooks, because *some* create hooks may have succeeded
            errors = childScopeDef.callCreateHooks(childScope)

            if (f != null && (errors == null || errors.isEmpty))
                f.call(childScope)
        } finally {
            if (f != null) {
                errs := childScope.destroyInternal
                if (errs != null) {
                    if (errors == null)
                        errors = Err[,]
                    errors.addAll(errs)
                }
            }
        }
        
        if (errors != null && errors.size > 0)
            throw errors.first
        
        return f == null ? childScope : null
    }

    override This jailBreak() {
        destroyedCheck
        jailBroken.val = true
        return this
    }

    override Bool isDestroyed() {
        destroyedLock.locked
    }
    
    override Void destroy() {
        errors := _destroy
        if (errors != null && errors.size > 0)
            throw errors.first      
    }
    
    override Void asActive(|Scope| f) {
        registry.activeScopeStack.push(this)
        try     f(this)
        finally registry.activeScopeStack.pop(this)
    }

    internal Err[]? destroyInternal() {
        jailBroken.val ? null : _destroy
    }

    internal Err[]? _destroy() {
        // keeping a thread safe / synchronised list of active children is only achievable using 
        // Actors or a couple of locking flags. Either way, the contention overhead for dealing 
        // with multiple threads (e.g. BedSheet) makes it unrealistic for what little gain it 
        // gives - primarily ensuring child scopes are destroyed before their parents, which is
        // only of concern if someone has destroy hooks on both scopes.
        //
        // An IoC module could easily be created to compensate for this if absolutely needed. 
        if (destroyedLock.locked) return null

        try {
            return scopeDef.callDestroyHooks(this)

        } finally {
            destroyedLock.lock
    
            serviceStore.destroy
            
            // only pop ourselves off the end of the stack
            registry.activeScopeStack.pop(this)
        }
    }

    internal Void destroyedCheck() {
        registry.shutdownLock.check
        destroyedLock.check

        // just in case someone forgets to destroy a jail broken scope
        // also allows active threaded scopes to check the status of the root scope during registry shutdown
        try parent?.destroyedCheck
        catch (ScopeDestroyedErr err) {
            destroy
            throw err
        }
    }

    internal ServiceDefImpl? serviceDefById(Str serviceId, Bool checked) {
        instanceById(serviceId, Str[,], checked)?.def
    }

    internal ServiceDefImpl? serviceDefByType(Type serviceType, Bool checked) {
        instanceByType(serviceType, Str[,], checked)?.def
    }

    internal ServiceInstance? instanceById(Str serviceId, Str[] scopes, Bool checked) {
        serviceStore.instanceById(serviceId)
            ?: (parent?.instanceById(serviceId, scopes.add(id), checked)
                ?: (checked ? throw ServiceNotFoundErr(ErrMsgs.scope_couldNotFindServiceById(serviceId, scopes.dup.add(id).reverse), services(scopes.dup.add(id).reverse)) : null)
            )
    }

    internal ServiceInstance? instanceByType(Type serviceType, Str[] scopes, Bool checked) {
        serviceStore.instanceByType(serviceType)
            ?: (parent?.instanceByType(serviceType, scopes.add(id), checked)
                ?: (checked ? throw ServiceNotFoundErr(ErrMsgs.scope_couldNotFindServiceByType(serviceType, scopes.dup.add(id).reverse), services(scopes.dup.add(id).reverse)) : null)
            )
    }

    // just for the autobuild / service warning
    internal Bool containsServiceType(Type serviceType) {
        serviceStore.containsServiceType(serviceType) ? true : (parent?.containsServiceType(serviceType) ?: false)
    }
    
    private Str[] services(Str[] scopes) {
        scopes.map |scope| { registry.scopeIdLookup[scope].map { "$scope - $it" } }.flatten
    }
    
    override Str toStr() {
        "Scope: $id"
    }
}