UNPKG

@daisugi/kado

Version:

Kado is a minimal and unobtrusive inversion of control container.

204 lines (187 loc) 5.47 kB
import { Ayamari } from '@daisugi/ayamari'; import { urandom } from '@daisugi/kintsugi'; const { errFn } = new Ayamari(); interface Class { new (...args: any[]): unknown; } export type KadoToken = string | symbol | number; export type KadoScope = 'Transient' | 'Singleton'; export interface KadoManifestItem { token?: KadoToken; useClass?: Class; useValue?: any; useFnByContainer?(container: KadoContainer): any; useFn?(...args: any[]): any; params?: KadoParam[]; scope?: KadoScope; meta?: Record<string, any>; } export type KadoParam = KadoToken | KadoManifestItem; interface KadoContainerItem { manifestItem: KadoManifestItem; checkedForCircularDep: boolean; instance: any; } type KadoTokenToContainerItem = Map< KadoToken, KadoContainerItem >; export type KadoContainer = Container; export class Container { #tokenToContainerItem: KadoTokenToContainerItem; constructor() { this.#tokenToContainerItem = new Map(); } async resolve<T>(token: KadoToken): Promise<T> { const containerItem = this.#tokenToContainerItem.get(token); if (containerItem === undefined) { throw errFn.NotFound( `Attempted to resolve unregistered dependency token: "${token.toString()}".`, ); } const manifestItem = containerItem.manifestItem; if (manifestItem.useValue !== undefined) { return manifestItem.useValue; } if (containerItem.instance) { return containerItem.instance; } let resolve: ((value: any) => void) | undefined; if (manifestItem.scope !== Kado.scope.Transient) { containerItem.instance = new Promise((_resolve) => { resolve = _resolve; }); } let paramsInstances = null; if (manifestItem.params) { this.#checkForCircularDep(containerItem); paramsInstances = await Promise.all( manifestItem.params.map( this.#resolveParam.bind(this), ), ); } let instance: any; if (manifestItem.useFn) { instance = paramsInstances ? manifestItem.useFn(...paramsInstances) : manifestItem.useFn(); } else if (manifestItem.useFnByContainer) { instance = manifestItem.useFnByContainer(this); } else if (manifestItem.useClass) { instance = paramsInstances ? new manifestItem.useClass(...paramsInstances) : new manifestItem.useClass(); } if (manifestItem.scope === Kado.scope.Transient) { return instance; } // biome-ignore lint/style/noNonNullAssertion: We know that `resolve` is defined if the scope is not transient. resolve!(instance); return containerItem.instance; } async #resolveParam(param: KadoParam) { const token = typeof param === 'object' ? this.#registerItem(param) : param; return this.resolve(token); } register(manifestItems: KadoManifestItem[]) { for (const manifestItem of manifestItems) { this.#registerItem(manifestItem); } } #registerItem(manifestItem: KadoManifestItem): KadoToken { const token = manifestItem.token || urandom(); this.#tokenToContainerItem.set(token, { manifestItem: Object.assign(manifestItem, { token }), checkedForCircularDep: false, instance: null, }); return token; } list(): KadoManifestItem[] { return Array.from( this.#tokenToContainerItem.values(), ).map((containerItem) => containerItem.manifestItem); } get(token: KadoToken): KadoManifestItem { const containerItem = this.#tokenToContainerItem.get(token); if (containerItem === undefined) { throw errFn.NotFound( `Attempted to get unregistered dependency token: "${token.toString()}".`, ); } return containerItem.manifestItem; } #checkForCircularDep( containerItem: KadoContainerItem, tokens: KadoToken[] = [], ) { if (containerItem.checkedForCircularDep) { return; } const token = containerItem.manifestItem.token; if (!token) { return; } if (tokens.includes(token)) { const chainOfTokens = tokens .map((token) => `"${token.toString()}"`) .join(' ➡️ '); throw errFn.CircularDependencyDetected( `Attempted to resolve circular dependency: ${chainOfTokens} 🔄 "${token.toString()}".`, ); } if (containerItem.manifestItem.params) { for (const param of containerItem.manifestItem .params) { if (typeof param === 'object') { continue; } const paramContainerItem = this.#tokenToContainerItem.get(param); if (!paramContainerItem) { continue; } this.#checkForCircularDep(paramContainerItem, [ ...tokens, token, ]); paramContainerItem.checkedForCircularDep = true; } } } } export class Kado { static scope: Record<KadoScope, KadoScope> = { Transient: 'Transient', Singleton: 'Singleton', }; container: KadoContainer; constructor() { this.container = new Container(); } static value(value: unknown): KadoManifestItem { return { useValue: value }; } static map(params: KadoParam[]): KadoManifestItem { return { useFn(...args: unknown[]) { return args; }, params, }; } static flatMap(params: KadoParam[]): KadoManifestItem { return { useFn(...args: unknown[]) { return args.flat(); }, params, }; } }