-
Notifications
You must be signed in to change notification settings - Fork 95
feat(injector): implement a different approach to injector scopes #50
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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()) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This needs to be noted as a breaking change - in the commit message. |
||
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); | ||
} | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure I fully understand this "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"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's sufficient if there is a parent injector with that scope (it does not have to be this scope) so the error might say something like "... or parent injector" or Maybe "no injector for RouteScope"? Also, this exception should show the dependency path such as "(Foo -> Service)" (the same way other exceptions do). |
||
|
||
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(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd remove this assertion. If it throws, it will fail on line 729. I like it explicit in the previous test, where we say "this one throws, this one not" but I don't think we need it here. |
||
|
||
expect(grantChild.get(Service)).toBe(child.get(Service)); | ||
|
||
expect(injector._cache.has(Service)).toBe(false); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we assert this without using private API? Same for other tests using |
||
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(), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you change this fancy indentation to: var a = b;
var c = d;
var e = f; I know we use this in Angular codebase, but I prefer this old-school style, it's much easier to add new var or re-order... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same for other places.... |
||
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); | ||
}); | ||
}); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's call it
Scope
to make it consistent. OrDefaultScope
,BaseScope
or something?I think you are right and we might rename all of the annotations to
*Annotation
but that can be done in a separate commit. The reason why I didn't do that already was that then people have to import it asimport {InjectAnnotation as Inject} from 'di';
... and that kind of sucks....We should probably separate all these annotations to a separate module because often projects only need the annotations (so that their code can be used with DI) but they don't need the actual DI. Then, I think
Inject
,Scope
, etc. will be fine...What do you think?