ts-ioc-container
Version:
Fast, lightweight TypeScript dependency injection container with a clean API, scoped lifecycles, decorators, tokens, hooks, lazy injection, customizable providers, and no global container objects.
183 lines (182 loc) • 6.49 kB
JavaScript
import { EmptyContainer } from './EmptyContainer';
import { ContainerDisposedError } from '../errors/ContainerDisposedError';
import { MetadataInjector } from '../injector/MetadataInjector';
import { AliasMap } from './AliasMap';
import { DependencyNotFoundError } from '../errors/DependencyNotFoundError';
import { ContainerNotFoundError } from '../errors/ContainerNotFoundError';
import { Is } from '../utils/basic';
import { Filter as F } from '../utils/array';
export class Container {
isDisposed = false;
parent;
scopes = [];
instances = [];
registrations = [];
tags;
providers = new Map();
aliases = new AliasMap();
injector;
onConstructHookList = [];
onDisposeHookList = [];
constructor(options = {}) {
this.injector = options.injector ?? new MetadataInjector();
this.parent = options.parent ?? new EmptyContainer();
this.tags = new Set(options.tags ?? []);
}
register(key, provider, { aliases = [] } = {}) {
this.validateContainer();
this.providers.set(key, provider);
this.aliases.setAliasesByKey(key, aliases);
return this;
}
resolve(target, { args = [], child = this, lazy } = {}) {
this.validateContainer();
if (Is.constructor(target)) {
return this.injector.resolve(this, target, { args, lazy });
}
const provider = this.providers.get(target);
return provider?.hasAccess({ invocationScope: child, providerScope: this, args })
? provider.resolve(this, { args, lazy })
: this.parent.resolve(target, { args, child, lazy });
}
resolveByAlias(alias, { args = [], child = this, lazy, excludedKeys = [] } = {}) {
this.validateContainer();
const keys = [];
const deps = [];
for (const key of this.aliases.getKeysByAlias(alias).filter(F.exclude(excludedKeys))) {
const provider = this.findProviderByKeyOrFail(key);
if (!provider.hasAccess({ invocationScope: child, providerScope: this, args })) {
continue;
}
keys.push(key);
deps.push(provider.resolve(this, { args, lazy }));
}
const parentDeps = this.parent.resolveByAlias(alias, {
args,
child,
lazy,
excludedKeys: [...excludedKeys, ...keys],
});
return [...deps, ...parentDeps];
}
resolveOneByAlias(alias, { args = [], child = this, lazy } = {}) {
this.validateContainer();
const [key, ..._] = this.aliases.getKeysByAlias(alias);
const provider = key ? this.findProviderByKeyOrFail(key) : undefined;
return provider?.hasAccess({ invocationScope: child, providerScope: this, args })
? provider.resolve(this, { args, lazy })
: this.parent.resolveOneByAlias(alias, { args, child, lazy });
}
createScope({ tags } = {}) {
this.validateContainer();
const scope = new Container({ injector: this.injector, parent: this, tags })
.addOnConstructHook(...this.onConstructHookList)
.addOnDisposeHook(...this.onDisposeHookList);
for (const registration of [...this.parent.getRegistrations(), ...this.registrations]) {
registration.applyTo(scope);
}
this.scopes.push(scope);
return scope;
}
dispose() {
this.validateContainer();
this.isDisposed = true;
// Execute onDispose hooks
while (this.onDisposeHookList.length) {
const onDispose = this.onDisposeHookList.shift();
onDispose(this);
}
// Detach from parent
this.parent.removeScope(this);
this.parent = new EmptyContainer();
// Reset the state
for (const [_, provider] of this.providers) {
provider.dispose();
}
this.providers.clear();
this.aliases.destroy();
this.instances = [];
this.registrations = [];
// Clear hooks
this.onConstructHookList.splice(0, this.onConstructHookList.length);
}
addRegistration(registration) {
this.registrations.push(registration);
registration.applyTo(this);
return this;
}
getRegistrations() {
return [...this.parent.getRegistrations(), ...this.registrations];
}
hasRegistration(key) {
return this.registrations.some((r) => r.getKeyOrFail() === key) || this.parent.hasRegistration(key);
}
addOnConstructHook(...hooks) {
this.onConstructHookList.push(...hooks);
return this;
}
addOnDisposeHook(...hooks) {
this.onDisposeHookList.push(...hooks);
return this;
}
addInstance(instance) {
this.instances.push(instance);
// Execute onConstruct hooks
for (const onConstruct of this.onConstructHookList) {
onConstruct(instance, this);
}
}
getScopes() {
return [...this.scopes];
}
hasInstance(instance) {
return this.instances.includes(instance);
}
getScopeByInstanceOrFail(instance) {
this.validateContainer();
const queue = [this];
while (queue.length > 0) {
const scope = queue.shift();
if (scope.hasInstance(instance)) {
return scope;
}
queue.push(...scope.getScopes());
}
throw new ContainerNotFoundError('Cannot find scope for the given instance');
}
removeScope(child) {
this.scopes = this.scopes.filter((s) => s !== child);
}
useModule(module) {
module.applyTo(this);
return this;
}
getParent() {
return this.parent;
}
getInstances(cascade = false) {
if (!cascade) {
return [...this.instances];
}
return [...this.instances, ...this.scopes.flatMap((s) => s.getInstances(true))];
}
hasTag(tag) {
return this.tags.has(tag);
}
addTags(...tags) {
for (const tag of tags) {
this.tags.add(tag);
}
}
validateContainer() {
if (this.isDisposed) {
throw new ContainerDisposedError('Container is already disposed');
}
}
findProviderByKeyOrFail(key) {
if (!this.providers.has(key)) {
throw new DependencyNotFoundError(`Provider ${key.toString()} does not exist`);
}
return this.providers.get(key);
}
}