esp-js-di
Version:
A tiny DI container (formally microdi-js)
357 lines (353 loc) • 17.9 kB
JavaScript
/* notice_start
* Copyright 2016 Dev Shop Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
notice_end */
import * as utils from './utils';
import ResolverContext from './resolverContext';
import InstanceLifecycleType from './instanceLifecycleType';
import RegistrationModifier from './registrationModifier';
import Guard from './guard';
import EspDiConsts from './espDiConsts';
import ResolverNames from './resolverNames';
import {DefaultIsRegisteredQueryOptions} from './isRegisteredQueryOptions';
export default class Container {
constructor() {
this._isChildContainer = false;
this._parent = undefined;
this._registrations = {};
this._registrationGroups = {};
this._instanceCache = {};
this._resolverContext = new ResolverContext();
this._resolvers = this._createDefaultResolvers();
this._isDisposed = false;
this._childContainers = [];
this._containerEventHandlers = new Map();
this._registerSelf();
}
createChildContainer() {
this._throwIfDisposed();
// The child prototypically inherits some but not all props from its parent.
// Below we override the ones it doesn't inherit.
var child = Object.create(this);
child._parent = this;
child._isChildContainer = true;
child._registrations = Object.create(this._registrations);
child._registrationGroups = Object.create(this._registrationGroups);
child._instanceCache = Object.create(this._instanceCache);
child._resolvers = Object.create(this._resolvers);
child._isDisposed = false;
child._childContainers = [];
child._registerSelf();
this._childContainers.push(child);
return child;
}
register(name, proto) {
this._throwIfDisposed();
Guard.isNonEmptyString(name, 'Error calling register(name, proto). The name argument must be a string and can not be \'\'');
Guard.isNotNullOrUndefined(proto, `Error calling register(name, proto). Registered item for [${name}] can not be null or undefined`);
Guard.isTrue(!utils.isString(proto), `Error calling register(name, proto). Can not register a string instance against key [${name}], use registerInstance(name, instance)`);
Guard.isTrue(!utils.isNumber(proto), `Error calling register(name, proto). Can not register a number instance against key [${name}], use registerInstance(name, instance)`);
var registration = {
name: name,
proto: proto,
dependencyList: [],
instanceLifecycleType: InstanceLifecycleType.singleton
};
this._registrations[name] = registration;
return new RegistrationModifier(registration, this._instanceCache, this._registrationGroups);
}
registerFactory(name, factory) {
this._throwIfDisposed();
Guard.isNonEmptyString(name, 'Error calling registerFactory(name, factory). The name argument must be a string and can not be \'\'');
Guard.isFunction(factory, `Error calling registerFactory(name, factory). Provided factory for [${name}] must be a function`);
let dependencyKey = {
resolver: 'externalFactory',
factory : factory,
isResolverKey: true
};
return this.register(name, dependencyKey);
}
registerInstance(name, instance, isExternallyOwned = true) {
this._throwIfDisposed();
Guard.isNonEmptyString(name, 'Error calling register(name, instance, isExternallyOwned = true). The name argument must be a string and can not be \'\'');
Guard.isNotNullOrUndefined(instance, `Error calling registerInstance(name, instance, isExternallyOwned = true). Provided instance for [${name}] can not be null or undefined`);
this._registrations[name] = {
name: name,
instanceLifecycleType: isExternallyOwned
? InstanceLifecycleType.external
: InstanceLifecycleType.singleton
};
this._instanceCache[name] = instance;
this._raiseContainerEvent('instanceRegistered', name, instance)
}
isRegistered(name, options = DefaultIsRegisteredQueryOptions) {
this._throwIfDisposed();
Guard.isNonEmptyString(name, 'Error calling isRegistered(name). The name argument must be a string and can not be \'\'');
const isRegistered = options.searchParentContainers
? !!this._registrations[name]
: this._registrations.hasOwnProperty(name);
return isRegistered;
}
isGroupRegistered(groupName, options = DefaultIsRegisteredQueryOptions) {
this._throwIfDisposed();
Guard.isNonEmptyString(groupName, 'Error calling isGroupRegistered(groupName). The groupName argument must be a string and can not be \'\'');
const isGroupRegistered = options.searchParentContainers
? !!this._registrationGroups[groupName]
: this._registrationGroups.hasOwnProperty(groupName);
return isGroupRegistered;
}
resolve(name, ...additionalDependencies) {
this._throwIfDisposed();
Guard.isNonEmptyString(name, 'Error calling resolve(name, ...additionalDependencies). The name argument must be a string and can not be \'\'');
var registration = this._registrations[name],
dependency,
instance,
error;
if (!registration) {
error = utils.sprintf('Nothing registered for dependency [%s]', name);
throw new Error(error);
}
instance = this._tryRetrieveFromCache(name);
if (!instance) {
instance = this._buildInstance(name, additionalDependencies);
if (registration.instanceLifecycleType === InstanceLifecycleType.singleton || registration.instanceLifecycleType === InstanceLifecycleType.singletonPerContainer) {
this._instanceCache[name] = instance;
}
this._raiseContainerEvent('instanceCreated', name, instance)
} else if(additionalDependencies.length > 0) {
throw new Error("The provided additional dependencies can't be used to construct the instance as an existing instance was found in the container");
}
return instance;
}
resolveGroup(groupName, ...additionalDependencies) {
this._throwIfDisposed();
Guard.isNonEmptyString(groupName, 'Error calling resolveGroup(groupName). The groupName argument must be a string and can not be \'\'');
var items = [],
mapings,
error;
mapings = this._registrationGroups[groupName];
if (!mapings) {
error = utils.sprintf('No group with name [%s] registered', groupName);
throw new Error(error);
}
for (let i = 0, len = mapings.length; i < len; i++) {
items.push(this.resolve(mapings[i], ...additionalDependencies));
}
return items;
}
addResolver(name, resolver) {
this._throwIfDisposed();
Guard.isNonEmptyString(name, 'Error calling addResolver(name, resolver). The name argument must be a string and can not be \'\'');
Guard.isNotNullOrUndefined(resolver, `Error calling addResolver(name, resolver). Provided resolver for [${name}] can not be null or undefined`);
this._resolvers[name] = resolver;
}
on(eventType, eventHandler) {
Guard.isNonEmptyString(eventType, 'Error calling on(eventType, eventHandler). The eventType argument must be a string and can not be \'\'');
Guard.isFunction(eventHandler, 'Error calling on(eventType, eventHandler). The eventHandler argument must be a function can not be null or undefined');
let handlers = this._containerEventHandlers.get(eventType);
if (!handlers) {
handlers = [];
this._containerEventHandlers.set(eventType, handlers);
}
let handlerExists = handlers.some(handler => handler === eventHandler);
Guard.isFalsey(handlerExists, 'Error calling on(eventType, eventHandler). The eventHandler passed is already registered');
handlers.push(eventHandler);
}
off(eventType, eventHandler){
Guard.isNonEmptyString(eventType, 'Error calling off(eventType, eventHandler). The eventType argument must be a string and can not be \'\'');
Guard.isFunction(eventHandler, 'Error calling off(eventType, eventHandler). The eventHandler argument must be a function can not be null or undefined');
let handlers = this._containerEventHandlers.get(eventType);
if (!handlers) {
return;
}
const removeAtIndex = handlers.indexOf(eventHandler);
if (removeAtIndex > -1) {
handlers.splice(removeAtIndex, 1);
}
}
dispose() {
this._disposeContainer();
}
_tryRetrieveFromCache(name) {
var registration = this._registrations[name],
instance = this._instanceCache[name],
thisContainerOwnsRegistration,
thisContainerOwnsInstance,
typeIsSingleton,
childHasOverriddenRegistration,
parentRegistrationIsSingletonPerContainer;
if (this._isChildContainer) {
thisContainerOwnsRegistration = this._registrations.hasOwnProperty(name);
if (instance === undefined) {
typeIsSingleton = registration.instanceLifecycleType === InstanceLifecycleType.singleton;
// do we have the right to create it, or do we need to defer to the parent?
if (!thisContainerOwnsRegistration && typeIsSingleton) {
// singletons always need to be resolved and stored with the container that owns the
// registration, otherwise the cached instance won't live in the right place
instance = this._parent.resolve(name);
}
} else {
thisContainerOwnsInstance = this._instanceCache.hasOwnProperty(name);
if (!thisContainerOwnsInstance) {
childHasOverriddenRegistration = thisContainerOwnsRegistration && !thisContainerOwnsInstance;
parentRegistrationIsSingletonPerContainer = !thisContainerOwnsRegistration && registration.instanceLifecycleType === InstanceLifecycleType.singletonPerContainer;
if (childHasOverriddenRegistration || parentRegistrationIsSingletonPerContainer) {
instance = undefined;
}
}
}
}
return instance;
}
_buildInstance(name, additionalDependencies) {
var registration = this._registrations[name],
dependencies = [],
dependency,
dependencyKey,
context,
instance,
resolver;
context = this._resolverContext.beginResolve(name);
try {
if (registration.dependencyList !== undefined) {
for (let i = 0, len = registration.dependencyList.length; i < len; i++) {
dependencyKey = registration.dependencyList[i];
if (utils.isString(dependencyKey)) {
if(this.isGroupRegistered(dependencyKey)) {
dependency = this.resolveGroup(dependencyKey);
} else {
dependency = this.resolve(dependencyKey);
}
} else if (dependencyKey.hasOwnProperty('resolver') && utils.isString(dependencyKey.resolver)) {
resolver = this._resolvers[dependencyKey.resolver];
if (resolver === undefined) {
throw new Error(utils.sprintf('Error resolving [%s]. No resolver registered to resolve dependency key for resolver [%s]', name, dependencyKey.resolver));
}
dependency = resolver.resolve(this, dependencyKey);
} else {
throw new Error(utils.sprintf('Error resolving [%s]. It\'s dependency at index [%s] had an unknown resolver', name, i));
}
dependencies.push(dependency);
}
}
for(let j = 0, len = additionalDependencies.length; j < len; j ++) {
dependencies.push(additionalDependencies[j]);
}
if(registration.proto.isResolverKey) {
if(registration.proto.resolver) {
resolver = this._resolvers[registration.proto.resolver];
instance = resolver.resolve(this, registration.proto, ...dependencies);
}
else {
throw new Error('Registered resolverKey is missing it\'s resolver property');
}
} else if (typeof registration.proto === 'function') {
var Ctor = registration.proto.bind.apply(
registration.proto,
[null].concat(dependencies)
);
instance = new Ctor();
} else {
instance = Object.create(registration.proto);
if (instance.init !== undefined) {
instance = instance.init.apply(instance, dependencies) || instance;
}
}
} finally {
context.endResolve();
}
return instance;
}
_createDefaultResolvers() {
return {
// A resolvers that delegates to the dependency keys resolve method to perform the resolution.
// It expects a dependency key in format:
// { resolver: 'factory', resolve: function(container) { return someInstance } }
[ResolverNames.delegate]: {
resolve: (container, dependencyKey) => {
return dependencyKey.resolve(container);
}
},
// A resolvers that returns a factory that when called will resolve the dependency from the container.
// Any arguments passed at runtime will be passed to resolve as additional dependencies
// It expects a dependency key in format:
// { resolver: 'factory', name: "aDependencyName" }
[ResolverNames.factory]: {
resolve: (container, dependencyKey) => {
return function() { // using function here as I don't want babel to re-write the arguments var
var args = [].slice.call(arguments);
args.unshift(dependencyKey.key);
return container.resolve.apply(container, args );
};
}
},
// A resolver that invokes an external factory to resolve the dependency from the container.
[ResolverNames.externalFactory]: {
resolve: function(container, dependencyKey, ...additionalDeps) {
return dependencyKey.factory.apply(null, [container, ...additionalDeps]);
}
},
// A resolver that take a literal value
[ResolverNames.literal]: {
resolve: function(container, dependencyKey) {
Guard.isNotNullOrUndefined(dependencyKey.value, 'Invalid container configuration. A literal resolver key is missing the \'value\' property. That property should hold the value to be resolved.');
return dependencyKey.value;
}
}
};
}
_registerSelf() {
// register the child with itself so any dependency that wants to resolve a container get's the container at the same scope as itself (i.e. the container that built it).
this.registerInstance(EspDiConsts.owningContainer, this);
}
_raiseContainerEvent(eventType, name, instance) {
let handlers = this._containerEventHandlers.get(eventType);
if (!handlers) {
return;
}
let notification = {
name,
instance,
eventType
};
// copy the list as we're about to call external code which could be reentrant
let copy = handlers.slice(0);
for (let i = 0; i < copy.length; i ++) {
copy[i](notification)
}
}
_throwIfDisposed() {
if (this._isDisposed) throw new Error("Container has been disposed");
}
_disposeContainer() {
if (!this._isDisposed) {
this._isDisposed = true;
for (var prop in this._instanceCache) {
if (this._instanceCache.hasOwnProperty(prop)) {
var registration = this._registrations[prop];
if (registration.instanceLifecycleType !== InstanceLifecycleType.external) {
var instance = this._instanceCache[prop];
if (instance.dispose) {
instance.dispose();
}
}
}
}
for (var i = 0, len = this._childContainers.length; i < len; i++) {
var child = this._childContainers[i];
child._disposeContainer();
}
}
}
}