UNPKG

sahara

Version:

An inversion-of-control container for managing dependencies. Supports constructor, property and method injection

363 lines 13.4 kB
"use strict"; 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