diff --git a/src/annotations.js b/src/annotations.js index aabd957..23d2d68 100644 --- a/src/annotations.js +++ b/src/annotations.js @@ -13,9 +13,13 @@ import {isFunction} from './util'; // A class constructor can ask for this. class SuperConstructor {} +// All scopes must +// extend this +class ScopeAnnotation {} + // A built-in scope. // Never cache. -class TransientScope {} +class TransientScope extends ScopeAnnotation {} class Inject { constructor(...tokens) { @@ -96,7 +100,10 @@ function readAnnotations(fn) { // - token (anything) // - isPromise (boolean) // - isLazy (boolean) - params: [] + params: [], + + // List of Scopes + scopes: [] }; if (fn.annotations && fn.annotations.length) { @@ -111,6 +118,10 @@ function readAnnotations(fn) { collectedAnnotations.provide.token = annotation.token; collectedAnnotations.provide.isPromise = annotation.isPromise; } + + if (annotation instanceof ScopeAnnotation) { + collectedAnnotations.scopes.push(annotation); + } } } @@ -146,6 +157,7 @@ export { readAnnotations, SuperConstructor, + ScopeAnnotation, TransientScope, Inject, InjectPromise, diff --git a/src/injector.js b/src/injector.js index 04a1fae..17defb7 100644 --- a/src/injector.js +++ b/src/injector.js @@ -3,7 +3,8 @@ import { readAnnotations, hasAnnotation, Provide as ProvideAnnotation, - TransientScope as TransientScopeAnnotation + TransientScope as TransientScopeAnnotation, + ScopeAnnotation } from './annotations'; import {isFunction, toString} from './util'; import {profileInjector} from './profiler'; @@ -39,7 +40,22 @@ function constructResolvingMessage(resolving, token) { // - loading different "providers" and modules class Injector { - constructor(modules = [], parentInjector = null, providers = new Map(), scopes = []) { + constructor(modules = [], scopes = [], parentInjector = null, providers = new Map()) { + for (var Scope of scopes) { + if (!(Scope.prototype instanceof ScopeAnnotation)) { + throw new Error(`Cannot create injector, '${toString(Scope)}' is not a ScopeAnnotation`); + } + } + + // Always force new instance of TransientScope. + scopes.push(TransientScopeAnnotation); + + if (parentInjector) { + for (var scope of scopes) { + parentInjector._collectProvidersWithAnnotation(scope, providers); + } + } + this._cache = new Map(); this._providers = providers; this._parent = parentInjector; @@ -127,6 +143,16 @@ class Injector { return this._parent._instantiateDefaultProvider(provider, token, resolving, wantPromise, wantLazy); } + // returns true if the current injector can handle a given scope + _handlesScopes(scope) { + for(var handeldScope of this._scopes) { + if (scope instanceof handeldScope) { + return true; + } + } + return false; + } + // Return an instance for given token. get(token, resolving = [], wantPromise = false, wantLazy = false) { @@ -154,6 +180,7 @@ class Injector { return function createLazyInstance() { var lazyInjector = injector; + // TODO remove this if (arguments.length) { var locals = []; var args = arguments; @@ -198,7 +225,21 @@ class Injector { // No provider defined (overriden), use the default provider (token). if (!provider && isFunction(token) && !this._hasProviderFor(token)) { - provider = createProviderFromFnOrClass(token, readAnnotations(token)); + var annotations = readAnnotations(token); + var cantHandle = false; + for (var scope of annotations.scopes) { + if (!this._handlesScopes(scope)) { + cantHandle = scope; + } + } + if (cantHandle) { + if (this._parent) { + return this._parent.get(token, resolving, wantPromise, wantLazy); + } else { + throw new Error(`Can't instantiate service '${toString(token)}', ${toString(cantHandle.constructor)} not handled by this injector`); + } + } + provider = createProviderFromFnOrClass(token, annotations); return this._instantiateDefaultProvider(provider, token, resolving, wantPromise, wantLazy); } @@ -303,16 +344,7 @@ class Injector { // Create a child injector, which encapsulate shorter life scope. // It is possible to add additional providers and also force new instances of existing providers. createChild(modules = [], forceNewInstancesOf = []) { - var forcedProviders = new Map(); - - // Always force new instance of TransientScope. - forceNewInstancesOf.push(TransientScopeAnnotation); - - for (var annotation of forceNewInstancesOf) { - this._collectProvidersWithAnnotation(annotation, forcedProviders); - } - - return new Injector(modules, this, forcedProviders, forceNewInstancesOf); + return new Injector(modules, forceNewInstancesOf, this); } } diff --git a/src/testing.js b/src/testing.js index 0fe8bfa..452b2eb 100644 --- a/src/testing.js +++ b/src/testing.js @@ -82,7 +82,7 @@ function inject(...params) { } }); - currentSpec.$$injector = new Injector(modules, null, providers); + currentSpec.$$injector = new Injector(modules, [], null, providers); } currentSpec.$$injector.get(behavior); diff --git a/test/injector.spec.js b/test/injector.spec.js index 78d2047..9f014f5 100644 --- a/test/injector.spec.js +++ b/test/injector.spec.js @@ -1,5 +1,5 @@ import {Injector} from '../src/injector'; -import {Inject, Provide, SuperConstructor, InjectLazy, TransientScope} from '../src/annotations'; +import {Inject, Provide, SuperConstructor, InjectLazy, TransientScope, ScopeAnnotation} from '../src/annotations'; import {Car, CyclicEngine} from './fixtures/car'; import {module as houseModule} from './fixtures/house'; @@ -446,7 +446,7 @@ describe('injector', function() { it('should force new instances by annotation', function() { - class RouteScope {} + class RouteScope extends ScopeAnnotation {} class Engine { start() {} @@ -474,7 +474,7 @@ describe('injector', function() { it('should force new instances by annotation using overriden provider', function() { - class RouteScope {} + class RouteScope extends ScopeAnnotation {} class Engine { start() {} @@ -500,7 +500,7 @@ describe('injector', function() { it('should force new instance by annotation using the lowest overriden provider', function() { - class RouteScope {} + class RouteScope extends ScopeAnnotation {} @RouteScope class Engine { @@ -568,13 +568,13 @@ describe('injector', function() { it('should force new instance by annotation for default provider', function() { - class RequestScope {} + class RequestScope extends ScopeAnnotation {} @Inject @RequestScope class Foo {} - var parent = new Injector(); + var parent = new Injector([], [RequestScope]); var child = parent.createChild([], [RequestScope]); var fooFromChild = child.get(Foo); @@ -651,7 +651,7 @@ describe('injector', function() { }); - describe('with locals', function() { + describe('with locals', function() {5 it('should always create a new instance', function() { var constructorSpy = jasmine.createSpy('constructor'); @@ -684,4 +684,71 @@ describe('injector', function() { }); }); }); + + describe('Scopes', function () { + + it('should only accepts scopes that inherit ScopeAnnotation', function () { + class BadRouteScope{} + class RouteScope extends ScopeAnnotation {} + + expect(() => new Injector([], [BadRouteScope])) + .toThrowError("Cannot create injector, 'BadRouteScope' is not a ScopeAnnotation"); + + expect(() => new Injector([], [RouteScope])).not.toThrow(); + }); + + it('should only instantiate a scoped service if the injector can handle that scope', function () { + class RouteScope extends ScopeAnnotation {} + + @RouteScope + function Service(){} + + var injector = new Injector(), + child = injector.createChild([], [RouteScope]); + + expect(() => { child.get(Service); }).not.toThrow(); + expect(() => { injector.get(Service); }) + .toThrowError("Can't instantiate service 'Service', RouteScope not handled by this injector"); + + expect(injector._cache.has(Service)).toBe(false); + expect(child._cache.has(Service)).toBe(true); + }); + + it('should delegate the instantiation to the parent if it doesn\'t handle the scope', function () { + class RouteScope extends ScopeAnnotation {} + + @RouteScope + function Service(){} + + var injector = new Injector(), + child = injector.createChild([], [RouteScope]), + grantChild = child.createChild(); + + expect(() => { grantChild.get(Service); }).not.toThrow(); + + expect(grantChild.get(Service)).toBe(child.get(Service)); + + expect(injector._cache.has(Service)).toBe(false); + expect(child._cache.has(Service)).toBe(true); + expect(grantChild._cache.has(Service)).toBe(false); + }); + + describe('No Scopes', function () { + it('should be instantiated by the root injector if a service doesn\'t have a scope', function () { + function Service(){} + + var injector = new Injector(), + child = injector.createChild(), + grantChild = child.createChild(); + + expect(grantChild.get(Service)).toBe(child.get(Service)); + expect(child.get(Service)).toBe(injector.get(Service)); + expect(grantChild.get(Service)).toBe(injector.get(Service)); + + expect(injector._cache.has(Service)).toBe(true); + expect(child._cache.has(Service)).toBe(false); + expect(grantChild._cache.has(Service)).toBe(false); + }); + }); + }); });