UNPKG

typed-inject

Version:

Type safe dependency injection framework for TypeScript

220 lines (218 loc) 7.63 kB
/* 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