@daisugi/kado
Version:
Kado is a minimal and unobtrusive inversion of control container.
145 lines • 4.92 kB
JavaScript
import { Ayamari } from '@daisugi/ayamari';
import { urandom } from '@daisugi/kintsugi';
const { errFn } = new Ayamari();
export class Container {
#tokenToContainerItem;
constructor() {
this.#tokenToContainerItem = new Map();
}
async resolve(token) {
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;
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;
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) {
const token = typeof param === 'object'
? this.#registerItem(param)
: param;
return this.resolve(token);
}
register(manifestItems) {
for (const manifestItem of manifestItems) {
this.#registerItem(manifestItem);
}
}
#registerItem(manifestItem) {
const token = manifestItem.token || urandom();
this.#tokenToContainerItem.set(token, {
manifestItem: Object.assign(manifestItem, { token }),
checkedForCircularDep: false,
instance: null,
});
return token;
}
list() {
return Array.from(this.#tokenToContainerItem.values()).map((containerItem) => containerItem.manifestItem);
}
get(token) {
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, tokens = []) {
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 = {
Transient: 'Transient',
Singleton: 'Singleton',
};
container;
constructor() {
this.container = new Container();
}
static value(value) {
return { useValue: value };
}
static map(params) {
return {
useFn(...args) {
return args;
},
params,
};
}
static flatMap(params) {
return {
useFn(...args) {
return args.flat();
},
params,
};
}
}
//# sourceMappingURL=kado.js.map