typed-inject
Version:
Type safe dependency injection framework for TypeScript
220 lines (218 loc) • 7.63 kB
JavaScript
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-return */
import { INJECTOR_TOKEN, TARGET_TOKEN, } from './api/InjectionToken.js';
import { Scope } from './api/Scope.js';
import { InjectionError, InjectorDisposedError } from './errors.js';
import { isDisposable } from './utils.js';
const DEFAULT_SCOPE = Scope.Singleton;
/*
# Composite design pattern:
┏━━━━━━━━━━━━━━━━━━┓
┃ AbstractInjector ┃
┗━━━━━━━━━━━━━━━━━━┛
▲
┃
┏━━━━━━━━┻━━━━━━━━┓
┃ ┃
┏━━━━━━━━┻━━━━━┓ ┏━━━━━━━┻━━━━━━━┓
┃ RootInjector ┃ ┃ ChildInjector ┃
┗━━━━━━━━━━━━━━┛ ┗━━━━━━━━━━━━━━━┛
▲
┃
┏━━━━━━━━━━━━━┻━━━━━━━━━━━━━┓
┃ ChildWithProvidedInjector ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
▲
┃
┏━━━━━━━━━━━━━━━━━┻━┳━━━━━━━━━━━━━━━━┓
┏━━━━━━━━┻━━━━━━━━┓ ┏━━━━━━━━┻━━━━━━┓ ┏━━━━━━━┻━━━━━━━┓
┃ FactoryInjector ┃ ┃ ClassInjector ┃ ┃ ValueInjector ┃
┗━━━━━━━━━━━━━━━━━┛ ┗━━━━━━━━━━━━━━━┛ ┗━━━━━━━━━━━━━━━┛
*/
class AbstractInjector {
childInjectors = new Set();
injectClass(Class, providedIn) {
this.throwIfDisposed(Class);
try {
const args = this.resolveParametersToInject(Class, providedIn);
return new Class(...args);
}
catch (error) {
throw InjectionError.create(Class, error);
}
}
injectFunction(fn, providedIn) {
this.throwIfDisposed(fn);
try {
const args = this.resolveParametersToInject(fn, providedIn);
return fn(...args);
}
catch (error) {
throw InjectionError.create(fn, error);
}
}
resolveParametersToInject(injectable, target) {
const tokens = injectable.inject || [];
return tokens.map((key) => {
switch (key) {
case TARGET_TOKEN:
return target;
case INJECTOR_TOKEN:
return this;
default:
return this.resolveInternal(key, injectable);
}
});
}
provideValue(token, value) {
this.throwIfDisposed(token);
const provider = new ValueProvider(this, token, value);
this.childInjectors.add(provider);
return provider;
}
provideClass(token, Class, scope = DEFAULT_SCOPE) {
this.throwIfDisposed(token);
const provider = new ClassProvider(this, token, scope, Class);
this.childInjectors.add(provider);
return provider;
}
provideFactory(token, factory, scope = DEFAULT_SCOPE) {
this.throwIfDisposed(token);
const provider = new FactoryProvider(this, token, scope, factory);
this.childInjectors.add(provider);
return provider;
}
resolve(token, target) {
this.throwIfDisposed(token);
return this.resolveInternal(token, target);
}
throwIfDisposed(injectableOrToken) {
if (this.isDisposed) {
throw new InjectorDisposedError(injectableOrToken);
}
}
removeChild(child) {
this.childInjectors.delete(child);
}
isDisposed = false;
createChildInjector() {
return new ChildInjector(this);
}
async dispose() {
if (!this.isDisposed) {
this.isDisposed = true; // be sure new disposables aren't added while we're disposing
const promises = [];
for (const child of this.childInjectors) {
promises.push(child.dispose());
}
await Promise.all(promises);
await this.disposeInjectedValues();
}
}
}
class RootInjector extends AbstractInjector {
resolveInternal(token) {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`No provider found for "${token}"!.`);
}
disposeInjectedValues() {
return Promise.resolve();
}
}
class ChildInjector extends AbstractInjector {
parent;
async disposeInjectedValues() { }
resolveInternal(token, target) {
return this.parent.resolve(token, target);
}
constructor(parent) {
super();
this.parent = parent;
}
async dispose() {
this.parent.removeChild(this);
await super.dispose();
}
}
class ChildWithProvidedInjector extends ChildInjector {
token;
scope;
cached;
disposables = new Set();
constructor(parent, token, scope) {
super(parent);
this.token = token;
this.scope = scope;
}
resolveInternal(token, target) {
if (token === this.token) {
if (this.cached) {
return this.cached.value;
}
else {
try {
const value = this.result(target);
this.addToCacheIfNeeded(value);
return value;
}
catch (error) {
throw InjectionError.create(token, error);
}
}
}
else {
return this.parent.resolve(token, target);
}
}
addToCacheIfNeeded(value) {
if (this.scope === Scope.Singleton) {
this.cached = { value };
}
}
registerProvidedValue(value) {
if (isDisposable(value)) {
this.disposables.add(value);
}
return value;
}
async disposeInjectedValues() {
const promisesToAwait = [...this.disposables.values()].map((disposable) => disposable.dispose());
await Promise.all(promisesToAwait);
}
}
class ValueProvider extends ChildWithProvidedInjector {
value;
constructor(parent, token, value) {
super(parent, token, Scope.Transient);
this.value = value;
}
result() {
return this.value;
}
}
class FactoryProvider extends ChildWithProvidedInjector {
injectable;
constructor(parent, token, scope, injectable) {
super(parent, token, scope);
this.injectable = injectable;
}
result(target) {
return this.registerProvidedValue(this.parent.injectFunction(this.injectable, target));
}
}
class ClassProvider extends ChildWithProvidedInjector {
injectable;
constructor(parent, token, scope, injectable) {
super(parent, token, scope);
this.injectable = injectable;
}
result(target) {
return this.registerProvidedValue(this.parent.injectClass(this.injectable, target));
}
}
export function createInjector() {
return new RootInjector();
}
//# sourceMappingURL=InjectorImpl.js.map