unageanu/container-js

View on GitHub
src/containerjs/container.js

Summary

Maintainability
A
3 hrs
Test Coverage
define([
    "containerjs/loaders",
    "containerjs/binder",
    "containerjs/scope",
    "containerjs/utils/deferred"
], function( Loader, Binder, scope, Deferred ){
    
    "use strict";

    /**
     * @class container.Container
     * @constructor
     * @param {function(container.Binder):void} module 
     * @param {container.PackagingPolicy?} defaultPackagingPolicy
     */
    var Container = function Container ( module, defaultPackagingPolicy, loader ) {
        
        /** @type {Object.<string, Array.<container.Binding.<*>>>} */
        this.bindings = null;
        /** @type {Array.<container.Aspect>} */
        this.aspects = null;
        /** @type {Object.<number, {id:Number, name:string, parentId:number>} */
        this.creating = {};
        /** @type {Object.<string,*>} */
        this.singletonComponents = {};
        /** @type {container.Container} */
        this.chainedContainer = null;

        this.createRequestSerial = 1;
        
        this.load(module, defaultPackagingPolicy);
        this.loader = loader || Loader.AMD;
        this.onEagerSingletonConponentsInitialized =
            this.createEagerSingletonConponents(this);
        
        Object.seal(this);
    };
    var prototype = Container.prototype;
    
    /**
     * @public
     * @param {string} name
     * @param {number?} parentId
     * @return {Promise.<*,*>}
     */
    prototype.get = function( name, parentId ) {
        return Deferred.lazy( _get, this, arguments );
    };
    /** @private */
    var _get = function(name, parentId) {
        var binding = null;
        try {
           binding = this.getBinding(name);
        } catch ( e ) {
            return this.chainedContainer 
                ? this.chainedContainer.get(name)
                : Deferred.errorOf(e);
        }
        try {
            this.checkCyclicDependency( binding, parentId );
        } catch (e) {
            return Deferred.errorOf( e );
        }
        return binding.scope.retrievingStrategy( this, binding, parentId );
    };
    
    /**
     * @public
     * @param {string} name
     * @param {number?} parentId
     * @return {Promise.<Array.<*>,*>}
     */
    prototype.gets = function( name, parentId ) {
        return Deferred.lazy( _gets, this, arguments );
    };
    /** @private */
    var _gets = function(name, parentId) {
        var bindings = null;
        try {
            bindings = this.getBindings(name);
        } catch ( e ) {
            return this.chainedContainer 
                ? this.chainedContainer.gets(name)
                : Deferred.errorOf(e);
        }
        
        var deferreds = null;
        try {
            deferreds = bindings.map( function (binding) {
                this.checkCyclicDependency( binding, parentId );
                return binding.scope.retrievingStrategy( this, binding, parentId );
            }.bind(this));
        } catch (e) {
            return Deferred.errorOf( e );
        }
        
        if (this.chainedContainer && this.chainedContainer.hasBindings(name)) {
            var d = new Deferred();
            var errorback = function(e){ d.reject(e); };
            Deferred.when( deferreds ).then( function(components){
                this.chainedContainer.gets(name).then(function( parentComponent ){
                    d.resolve(components.concat(parentComponent));
                }, errorback);
            }.bind(this), errorback);
            return d;
        } else {
            return Deferred.when( deferreds );
        }
    };
    
    /**
     * @public
     * @param {string} name
     * @return {container.Binding}
     */
    prototype.getBinding = function( name ) {
        return this.getBindings(name)[0];
    };

    /**
     * @public
     * @param {string} name
     * @return {Array.<container.Binding>}
     */
    prototype.getBindings = function( name ) {
        if ( this.hasBindings(name) ) {
            return this.bindings[name];
        } else {
            throw new Error( "component is not binded. name=" + name ); 
        }
    };
    
    /**
     * @public
     * @param {string} name
     * @return {boolean}
     */
    prototype.hasBindings = function( name ) {
        var defintions = this.bindings[name];
        return defintions && defintions.length > 0;
    };
    
    /**
     * @public
     */
    prototype.destroy = function() {
        var destructor = function(binding){
            binding.scope.destructionStrategy( this, binding ); 
        }.bind(this);
        for ( var name in this.bindings ) {
            this.bindings[name].forEach(destructor);
            delete this.singletonComponents[name];
        }
    };
    
    /**
     * @public
     * @param {container.Container} container
     */
    prototype.chain = function( container ) {
        this.chainedContainer = container;
    };
    
    /** @private */
    prototype.createComponent = Deferred.defer( function( binding, parentId ) {

        var requestId = this.createRequestSerial++;
        this.creating[requestId] = {id:binding.id, name:binding.name, parentId:parentId};
        
        return binding.getInstance( this, requestId ).pipe( function(component){
            try { 
                this.aspects.forEach(function(aspect){
                    component = aspect.weave( binding, component );
                });
                binding.initialize(component, this);
                return component;
            } finally {
                delete this.creating[requestId];
            }
        }.bind(this), function(error) {
            delete this.creating[requestId];
            return error;
        }.bind(this));
    });
    
    /** @private */
    prototype.checkCyclicDependency = function( binding, parentId ) {
        var paths = [binding.name];
        while( parentId ) {
            paths.unshift( this.creating[parentId].name );
            if (this.creating[parentId].id === binding.id) {
                throw new Error("detect cyclic dependency.\n  -> " + 
                        paths.join("\n  -> "));
            }
            parentId = this.creating[parentId].parentId;
        }
    };
    
    /** @private */
    prototype.load = function( module, defaultPackagingPolicy ) {
        var binder = new Binder(defaultPackagingPolicy);
        module.call( null, binder );
        this.bindings = binder.buildBindings();
        this.aspects = binder.buildAspects();
    };
    
    /** @private */
    prototype.createEagerSingletonConponents = function() {
        var f = function( binding ){
            if (binding.scope.createOnStartUp) {
                return binding.scope.retrievingStrategy( this, binding );
            } else {
                return null;
            }
        }.bind(this);
        var deferreds = [];
        var notNull = function(x) { 
            return x != null;
        };
        for ( var i in this.bindings ) {
            deferreds = deferreds.concat(this.bindings[i].map(f).filter(notNull));
        }
        return Deferred.when(deferreds);
    };

    Object.freeze(prototype);
    return Container;
});