sahara
Version:
An inversion-of-control container for managing dependencies. Supports constructor, property and method injection
363 lines • 13.4 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.Container = exports.DelegateRegistration = exports.FactoryRegistration = exports.InstanceRegistration = exports.TypeRegistration = exports.Registration = void 0;
const tarjan_graph_1 = require("tarjan-graph");
const event_emitter_1 = require("./event-emitter");
const lifetime_1 = require("./lifetime");
const object_builder_1 = require("./object-builder");
const utils = require("./util");
const getHistoryString = (context, current) => {
const history = context.history.concat([]);
if (current) {
history.push({ name: current, type: null });
}
return history.map(registration => `"${registration.name}"`).join(' -> ');
};
function getUnregisteredErrorMessage(key, context) {
let message = `Nothing with key "${key}" is registered in the container`;
if (context && context.history.length) {
message += '; error occurred while resolving ' + getHistoryString(context, key);
}
if (key.startsWith(utils.argPrefix)) {
const argName = key.substring(utils.argPrefix.length);
message += `; you are probably missing a registration for arg alias "${argName}", e.g. ` +
`container.registerType(MyObject, { argAlias: '${argName}' })`;
}
return message;
}
function createResolverContext() {
return {
history: []
};
}
function resolveSignatureToOptions(keyOrOptions) {
const options = {};
if (typeof (keyOrOptions) === 'string') {
options.key = keyOrOptions;
}
else {
options.key = (keyOrOptions === null || keyOrOptions === void 0 ? void 0 : keyOrOptions.key) || undefined;
options.injections = (keyOrOptions === null || keyOrOptions === void 0 ? void 0 : keyOrOptions.injections) || [];
options.lifetime = (keyOrOptions === null || keyOrOptions === void 0 ? void 0 : keyOrOptions.lifetime) || undefined;
options.argAlias = (keyOrOptions === null || keyOrOptions === void 0 ? void 0 : keyOrOptions.argAlias) || undefined;
}
return options;
}
function getKeyFromCtor(ctor) {
return ctor.name;
}
function getKeyFromInstance(instance) {
if (instance && typeof (instance) === 'object') {
return getKeyFromCtor(instance.constructor);
}
return null;
}
class Registration {
constructor(name, lifetime, injections) {
this.name = name;
this.lifetime = lifetime || new lifetime_1.TransientLifetime();
this.injections = injections || [];
}
}
exports.Registration = Registration;
class TypeRegistration extends Registration {
constructor(name, lifetime, injections, typeInfo) {
super(name, lifetime, injections);
this.typeInfo = typeInfo;
}
}
exports.TypeRegistration = TypeRegistration;
class InstanceRegistration extends Registration {
constructor(name, lifetime, injections, instance) {
super(name, lifetime, injections);
this.instance = instance;
}
}
exports.InstanceRegistration = InstanceRegistration;
class FactoryRegistration extends Registration {
constructor(name, lifetime, injections, factory) {
super(name, lifetime, injections);
this.factory = factory;
}
}
exports.FactoryRegistration = FactoryRegistration;
class DelegateRegistration extends Registration {
constructor(alias, delegateKey) {
super(alias, null, null);
this.delegateKey = delegateKey;
}
}
exports.DelegateRegistration = DelegateRegistration;
class Container extends event_emitter_1.EventEmitter {
constructor(parent) {
super();
this.parent = parent || null;
this.registrations = {};
this.graph = new tarjan_graph_1.default();
this.builder = new object_builder_1.ObjectBuilder(this);
this.registerInstance(this);
}
addDependencyToGraph(key, dependencies) {
try {
this.graph.addAndVerify(key, dependencies);
}
catch (e) {
throw new Error(`${key}'s dependencies create a cycle: ${e.message}`);
}
}
registerType(ctor, options) {
const resolvedOptions = resolveSignatureToOptions(options);
const typeInfo = utils.getTypeInfo(ctor, resolvedOptions.key);
const key = typeInfo.name;
this.emit('registering', key, 'type');
this.registrations[key] = new TypeRegistration(key, resolvedOptions.lifetime, resolvedOptions.injections, typeInfo);
if (resolvedOptions.argAlias) {
this.registerArgAlias(key, resolvedOptions.argAlias);
}
this.addDependencyToGraph(key, typeInfo.args.map(info => info.type));
return this;
}
registerTypeAndArgAlias(ctor, key, alias) {
if (typeof (alias) === 'undefined') {
alias = key;
key = null;
}
this.registerType(ctor, {
key,
argAlias: alias,
});
return this;
}
registerAlias(key, alias) {
if (typeof (key) !== 'string') {
throw new Error('key must be a string');
}
if (typeof (alias) !== 'string') {
throw new Error('alias must be a string');
}
// this alias has a dependency on the thing it's aliasing
this.graph.addAndVerify(alias, [key]);
this.registrations[alias] = new DelegateRegistration(alias, key);
return this;
}
registerArgAlias(key, alias) {
return this.registerAlias(key, utils.argPrefix + alias);
}
/**
* Registers a specific object instance
*/
registerInstance(instance, options) {
const resolvedOptions = resolveSignatureToOptions(options);
const key = resolvedOptions.key || getKeyFromInstance(instance);
if (!key) {
throw new Error('Key not provided while registering instance');
}
resolvedOptions.key = key;
this.emit('registering', resolvedOptions.key, 'instance');
this.registrations[resolvedOptions.key] = new InstanceRegistration(resolvedOptions.key, resolvedOptions.lifetime, resolvedOptions.injections, instance);
if (resolvedOptions.argAlias) {
this.registerArgAlias(resolvedOptions.key, resolvedOptions.argAlias);
}
return this;
}
registerInstanceAndArgAlias(instance, key, alias) {
if (typeof (alias) === 'undefined') {
alias = key;
key = null;
}
this.registerInstance(instance, {
key,
argAlias: alias,
});
return this;
}
/**
* Registers a factory function for a type that will create
* the object
*/
registerFactory(factory, options) {
const resolvedOptions = resolveSignatureToOptions(options);
if (!resolvedOptions.key) {
throw new Error('"options.key" must be passed to registerFactory()');
}
this.emit('registering', resolvedOptions.key, 'factory');
this.registrations[resolvedOptions.key] = new FactoryRegistration(resolvedOptions.key, resolvedOptions.lifetime, resolvedOptions.injections, factory);
if (resolvedOptions.argAlias) {
this.registerArgAlias(resolvedOptions.key, resolvedOptions.argAlias);
}
return this;
}
registerFactoryAndArgAlias(factory, key, alias) {
this.registerFactory(factory, {
key,
argAlias: alias,
});
return this;
}
/**
* Determines if something is registered with the given key
*/
isRegistered(key) {
const keyStr = typeof (key) === 'function' ? getKeyFromCtor(key) : key;
return !!this.registrations[keyStr];
}
resolveExisting(key, context) {
const keyStr = typeof (key) === 'function' ? getKeyFromCtor(key) : String(key);
const registration = this.registrations[keyStr];
this.emit('resolving', keyStr);
if (!registration) {
throw new Error(getUnregisteredErrorMessage(keyStr, context));
}
const existing = registration.lifetime.fetch();
if (existing) {
this.emit('resolved', keyStr, existing);
return [existing, keyStr, registration];
}
context.history.push({ name: registration.name, type: null });
return [undefined, keyStr, registration];
}
/**
* Resolve a type to an instance synchronously
*/
resolveSync(key, context) {
context = context || createResolverContext();
let [existing, keyStr, registration] = this.resolveExisting(key, context);
if (existing) {
return existing;
}
let instance;
if (registration instanceof InstanceRegistration) {
instance = registration.instance;
}
else if (registration instanceof TypeRegistration) {
instance = this.builder.newInstanceSync(registration.typeInfo, context);
}
else if (registration instanceof FactoryRegistration) {
instance = registration.factory(this);
}
else if (registration instanceof DelegateRegistration) {
instance = this.resolveSync(registration.delegateKey, context);
}
else {
throw new Error('Unknown registration: ' + registration.constructor.name);
}
context.history.pop();
this.injectSync(instance, keyStr);
registration.lifetime.store(instance);
this.emit('resolved', keyStr, instance);
return instance;
}
/**
* Resolve a type to an instance asynchronously
*/
async resolve(key, context) {
context = context || createResolverContext();
let [existing, keyStr, registration] = this.resolveExisting(key, context);
if (existing) {
return existing;
}
let instance;
if (registration instanceof InstanceRegistration) {
instance = registration.instance;
}
else if (registration instanceof TypeRegistration) {
instance = await this.builder.newInstance(registration.typeInfo, context);
}
else if (registration instanceof FactoryRegistration) {
instance = await registration.factory(this);
}
else if (registration instanceof DelegateRegistration) {
instance = await this.resolve(registration.delegateKey, context);
}
context.history.pop();
await this.inject(instance, keyStr);
registration.lifetime.store(instance);
this.emit('resolved', keyStr, instance);
return instance;
}
/**
* Same as resolve(), but won't ever reject
*/
async tryResolve(key) {
try {
return await this.resolve(key);
}
catch (e) {
return;
}
}
/**
* Same as resolveSync(), but won't ever throw
*/
tryResolveSync(key) {
try {
return this.resolveSync(key);
}
catch (e) {
return;
}
}
/**
* Performs injection on an object asynchronously
*/
async inject(instance, key) {
const keyStr = key || getKeyFromInstance(instance);
if (!keyStr) {
throw new Error('Invalid registration key');
}
const registration = this.registrations[keyStr];
if (!registration) {
return Promise.reject(new Error(getUnregisteredErrorMessage(keyStr)));
}
await Promise.all(registration.injections.map(injection => injection.inject(instance, this)));
}
/**
* Performs injection on an object synchronously
*/
injectSync(instance, key) {
const keyStr = key || getKeyFromInstance(instance);
if (!keyStr) {
throw new Error('Invalid registration key');
}
const registration = this.registrations[keyStr];
if (!registration) {
throw new Error(getUnregisteredErrorMessage(keyStr));
}
registration.injections.forEach(injection => injection.injectSync(instance, this));
}
/**
* Creates a clone of the container in its current state
*/
createChildContainer(withEvents = false) {
const childContainer = new Container(this);
Object.keys(this.registrations).forEach((key) => {
childContainer.registrations[key] = this.registrations[key];
});
childContainer.registerInstance(childContainer);
childContainer.graph = this.graph.clone();
if (withEvents) {
const containerEvents = {
registering: 1,
resolving: 1,
resolved: 1,
};
Object.keys(containerEvents).forEach((event) => {
this.listeners(event).forEach((listener) => {
childContainer.on(event, listener);
});
});
const builderEvents = {
building: 1,
built: 1,
};
Object.keys(builderEvents).forEach((event) => {
this.builder.listeners(event).forEach((listener) => {
childContainer.builder.on(event, listener);
});
});
}
return childContainer;
}
}
exports.Container = Container;
//# sourceMappingURL=container.js.map