@daisugi/kado
Version:
Kado is a minimal and unobtrusive inversion of control container.
204 lines (187 loc) • 5.47 kB
text/typescript
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,
};
}
}