@akala/core
Version:
336 lines • 13.5 kB
JavaScript
import { each, map } from "../each.js";
import { logger } from "../logging/index.browser.js";
import { isPromiseLike } from "../promiseHelpers.js";
import { getParamNames } from "../reflect.js";
export const injectorLog = logger.use('akala:core:injector');
/**
* Converts a constructor to a function that creates new instances.
* @template T - The parameter types of the constructor.
* @template TResult - The instance type created by the constructor.
* @param {new (...args: T) => TResult} ctor - The constructor to convert.
* @returns {(...parameters: T) => TResult} A function that creates new instances of the constructor.
*/
export function ctorToFunction(ctor) {
return (...parameters) => {
return new ctor(...parameters);
};
}
export const customResolve = Symbol('custom resolve symbol');
export function isCustomResolver(x) {
return x && typeof x == 'object' && customResolve in x;
}
/**
* The `Injector` abstract class provides a framework for dependency injection,
* allowing the resolution and injection of parameters, functions, and constructors.
* It supports synchronous and asynchronous resolution of dependencies, as well as
* advanced features like nested property resolution and fallback mechanisms.
*
* Key Features:
* - Resolves parameters and injects them into functions or constructors.
* - Supports both synchronous and asynchronous dependency resolution.
* - Handles nested property resolution and fallback mechanisms for unresolved keys.
* - Provides utilities for merging arguments, collecting parameter maps, and applying them.
* - Allows injection of new instances of constructors or asynchronous functions.
*
* Usage:
* Extend this class to implement custom dependency injection logic by overriding
* the abstract methods `resolve`, `onResolve`, and `inspect`.
*
* Example:
* ```typescript
* class MyInjector extends Injector {
* resolve<T>(param: Resolvable): T {
* // Custom resolution logic
* }
* onResolve<T>(name: Resolvable): PromiseLike<T> {
* // Custom asynchronous resolution logic
* }
* inspect(): void {
* // Custom inspection logic
* }
* }
* ```
*
* @template T - The type of the resolved value.
*/
export class Injector {
static customResolve = customResolve;
[customResolve](param) {
return this.resolve(param);
}
/**
* Applies a collected map to resolved values.
* @param {InjectMap<T>} param - The parameter map.
* @param {{ [k: string | symbol]: any }} resolved - The resolved values.
* @returns {T} The applied map.
*/
static applyCollectedMap(param, resolved) {
let promises = [];
const result = map(param, (value, key) => {
if (typeof value == 'object') {
const subResult = Injector.applyCollectedMap(value, resolved);
if (isPromiseLike(subResult))
promises.push(subResult.then(r => { result[key] = r; }));
return subResult;
}
const subResult = resolved[value];
if (isPromiseLike(subResult))
promises.push(subResult.then(r => { result[key] = r; }));
return subResult;
});
if (promises.length)
return Promise.all(promises).then(() => result);
return result;
}
/**
* Collects a map of parameters.
* @param {InjectMap} param - The parameter map.
* @returns {(string | symbol)[]} The collected map.
*/
static collectMap(param) {
let result = [];
if (param)
each(param, value => {
if (typeof value == 'object')
result = result.concat(Injector.collectMap(param));
else
result.push(value);
});
return result;
}
/**
* Merges arrays of resolved arguments and other arguments.
* @param {InjectedParameter<unknown>[]} resolvedArgs - The resolved arguments.
* @param {...unknown[]} otherArgs - The other arguments.
* @returns {unknown[]} The merged array.
*/
static mergeArrays(resolvedArgs, ...otherArgs) {
const args = [];
let unknownArgIndex = 0;
let hasPromise = [];
for (const arg of resolvedArgs.sort((a, b) => a.index - b.index)) {
if (isPromiseLike(arg.value))
hasPromise.push(arg.value.then(v => { args[arg.index] = v; }));
if (arg.index === args.length)
args[args.length] = arg.value;
else if (typeof (otherArgs[unknownArgIndex]) != 'undefined')
args[args.length] = otherArgs[unknownArgIndex++];
}
if (hasPromise.length)
return { promisedArgs: Promise.all(hasPromise).then(() => args.concat(otherArgs.slice(unknownArgIndex))), args: args.concat(otherArgs.slice(unknownArgIndex)) };
return { args: args.concat(otherArgs.slice(unknownArgIndex)) };
}
/**
* Gets the arguments to inject.
* @param {(Resolvable)[]} toInject - The resolvable parameters to inject.
* @returns {InjectedParameter<unknown>[]} The injected parameters.
*/
getArguments(toInject) {
return toInject.map((p, i) => ({ index: i, value: this.resolve(p) }));
}
/**
* Resolves a series of keys on a given source object, traversing through nested properties.
* If the resolution encounters an `Injector` instance, it delegates the resolution to the `Injector`.
* If the resolution encounters a promise-like object, it resolves the promise and continues the resolution.
* If a key cannot be resolved and a fallback function is provided, the fallback is invoked with the remaining keys.
*
* @template T - The type of the resolved value.
* @param source - The initial object to resolve the keys from.
* @param keys - An array of keys (strings or symbols) to resolve on the source object.
* @param fallback - A function to handle unresolved keys, invoked with the remaining keys if resolution fails.
* @returns The resolved value of type `T`, or the result of the fallback function if provided.
*/
static resolveKeys(source, keys, fallback) {
let result = source;
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (isCustomResolver(key))
return key[customResolve](keys.slice(i + 1));
if (isCustomResolver(result))
return result[customResolve](keys.slice(i));
if (isPromiseLike(result))
return result.then((result) => this.resolveKeys(result, keys.slice(i), fallback));
if (result === source && (result === null || typeof key !== 'object' && typeof (result[key]) == 'undefined') && fallback)
return fallback(keys.slice(i));
if (typeof key !== 'object')
result = result?.[key];
else {
const x = Injector.collectMap(key);
result = Injector.applyCollectedMap(key, Object.fromEntries(x.map(x => [x, this.resolveKeys(source, [x], fallback)])));
}
}
return result;
}
inject(a, ...b) {
if (typeof a == 'function')
return this.injectWithName(a['$inject'] || getParamNames(a), a);
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self = this;
return function (c) {
if (typeof b == 'undefined')
b = [];
b.unshift(a);
const oldf = self.injectWithName(b, c.value);
c.value = function (...args) {
return oldf.apply(this, Array.from(args));
};
};
}
injectAsync(a, ...b) {
if (typeof a == 'function')
return this.injectWithNameAsync(a['$inject'] || getParamNames(a), a);
if (typeof b == 'undefined')
b = [];
b.unshift(a);
return (c) => {
const f = c.value;
c.value = function () {
return this.injectWithNameAsync(b, f);
};
};
}
/**
* Injects a new instance of a constructor.
* @param {InjectableConstructor<T, TArgs>} ctor - The constructor to inject.
* @returns {Injected<T>} The injected instance.
*/
injectNew(ctor) {
return this.inject(ctorToFunction(ctor));
}
/**
* Injects a new instance of a constructor with specified parameters.
* @param {Resolvable[]} toInject - The parameters to inject.
* @param {InjectableConstructor<T, TArgs>} ctor - The constructor to inject.
* @returns {Injected<T>} The injected instance.
*/
injectNewWithName(toInject, ctor) {
return this.injectWithName(toInject, ctorToFunction(ctor));
}
/**
* Injects an asynchronous function with specified parameters.
* @param {Resolvable[]} toInject - The parameters to inject.
* @param {InjectableAsync<T, TArgs> | Injectable<T, TArgs>} a - The function to inject.
* @returns {Promise<T>} The injected function.
*/
injectWithNameAsync(toInject, a) {
if (!toInject || toInject.length == 0)
return instance => a.call(instance);
return (instance, ...otherArgs) => {
const args = Injector.mergeArrays(this.getArguments(toInject), ...otherArgs);
if (args.promisedArgs)
return args.promisedArgs.then(args => a.apply(instance, args));
return a.apply(instance, args.args);
};
}
/**
* Injects a function with specified parameters.
* @param {Resolvable[]} toInject - The parameters to inject.
* @param {Injectable<T, TArgs>} a - The function to inject.
* @returns {Injected<T>} The injected function.
*/
injectWithName(toInject, a) {
if (toInject?.length === 0)
return (instance) => a.call(instance);
return (instance, ...otherArgs) => {
const args = Injector.mergeArrays(this.getArguments(toInject), ...otherArgs);
return a.apply(instance, args.args);
};
}
/**
* Executes a function with specified parameters.
* @param {...Resolvable[]} toInject - The parameters to inject.
* @returns {(f: Injectable<T, TArgs>) => T} The executed function.
*/
exec(...toInject) {
return (f) => {
return this.injectWithName(toInject, f)(this);
};
}
}
export class LocalInjector extends Injector {
parent;
constructor(parent) {
super();
this.parent = parent;
}
/**
* Registers a parameter with a value.
* @param {string | symbol} name - The name of the parameter to register.
* @param {T} value - The value to register.
* @param {boolean} [override] - Whether to override the existing value.
* @returns {T} The registered value.
*/
register(name, value, override) {
if (typeof (value) != 'undefined' && value !== null)
this.registerDescriptor(name, { value: value, enumerable: true, configurable: true }, override);
return value;
}
/**
* Registers a factory function.
* @param {string} name - The name of the factory.
* @param {(() => unknown)} value - The factory function.
* @param {boolean} [override] - Whether to override the existing value.
* @returns {(() => unknown)} The registered factory function.
*/
registerFactory(name, value, override) {
if (typeof name == 'string')
this.register(name + 'Factory', value, override);
this.registerDescriptor(name, {
get: function () {
return value();
}, enumerable: true, configurable: true
}, override);
return value;
}
/**
* Creates a factory function.
* @param {string} name - The name of the factory.
* @param {boolean} [override] - Whether to override the existing value.
* @returns {(fact: (() => unknown)) => void} The factory function.
*/
factory(name, override) {
return (fact) => {
this.registerFactory(name, fact, override);
};
}
service(name, override, ...toInject) {
let singleton;
if (typeof toInject == 'undefined')
toInject = [];
if (typeof override !== 'boolean' && typeof override !== 'undefined') {
toInject.unshift(override);
override = false;
}
return (fact) => {
this.registerDescriptor(name, {
get: () => {
if (singleton)
return singleton;
return singleton = this.injectNewWithName(toInject, fact)();
}
}, override);
};
}
}
export class InjectorMap extends Injector {
map;
constructor(map) {
super();
this.map = map;
}
onResolve(name, handler) {
throw new Error('Method not implemented.');
}
resolve(param) {
if (typeof param == 'object') {
if (Array.isArray(param))
return Injector.resolveKeys({}, param);
const x = Injector.collectMap(param);
return Injector.applyCollectedMap(param, Object.fromEntries(x.map(x => [x, this.resolve(x)])));
}
return this.map(param);
}
inspect() {
}
}
//# sourceMappingURL=shared.js.map