aurelia-dependency-injection
Version:
A lightweight, extensible dependency injection container for JavaScript.
660 lines (575 loc) • 23.3 kB
text/typescript
import './internal';
import { metadata } from 'aurelia-metadata';
import { AggregateError } from 'aurelia-pal';
import { resolver, StrategyResolver, Resolver, Strategy, StrategyState, Factory, NewInstance, Lazy, Optional, All, Parent } from './resolvers';
import { Invoker } from './invokers';
import {
DependencyCtorOrFunctor,
DependencyCtor,
PrimitiveOrDependencyCtor,
PrimitiveOrDependencyCtorOrFunctor,
ImplOrAny,
Impl,
Args,
Primitive
} from './types';
let currentContainer: Container | null = null;
function validateKey(key: any) {
if (key === null || key === undefined) {
throw new Error(
'key/value cannot be null or undefined. Are you trying to inject/register something that doesn\'t exist with DI?'
);
}
}
/** @internal */
export const _emptyParameters = Object.freeze([]) as [];
metadata.registration = 'aurelia:registration';
metadata.invoker = 'aurelia:invoker';
const resolverDecorates = resolver.decorates!;
/**
* Stores the information needed to invoke a function.
*/
export class InvocationHandler<
TBase,
TImpl extends Impl<TBase>,
TArgs extends Args<TBase>
> {
/**
* The function to be invoked by this handler.
*/
public fn: DependencyCtorOrFunctor<TBase, TImpl, TArgs>;
/**
* The invoker implementation that will be used to actually invoke the function.
*/
public invoker: Invoker<TBase, TImpl, TArgs>;
/**
* The statically known dependencies of this function invocation.
*/
public dependencies: TArgs;
/**
* Instantiates an InvocationDescription.
* @param fn The Function described by this description object.
* @param invoker The strategy for invoking the function.
* @param dependencies The static dependencies of the function call.
*/
constructor(
fn: DependencyCtorOrFunctor<TBase, TImpl, TArgs>,
invoker: Invoker<TBase, TImpl, TArgs>,
dependencies: TArgs
) {
this.fn = fn;
this.invoker = invoker;
this.dependencies = dependencies;
}
/**
* Invokes the function.
* @param container The calling container.
* @param dynamicDependencies Additional dependencies to use during invocation.
* @return The result of the function invocation.
*/
public invoke(container: Container, dynamicDependencies?: TArgs[]): TImpl {
const previousContainer = currentContainer;
currentContainer = container;
try {
return dynamicDependencies !== undefined
? this.invoker.invokeWithDynamicDependencies(
container,
this.fn,
this.dependencies,
dynamicDependencies
)
: this.invoker.invoke(container, this.fn, this.dependencies);
} finally {
currentContainer = previousContainer;
}
}
}
/**
* Used to configure a Container instance.
*/
export interface ContainerConfiguration {
/**
* An optional callback which will be called when any function needs an
* InvocationHandler created (called once per Function).
*/
onHandlerCreated?: (
handler: InvocationHandler<any, any, any>
) => InvocationHandler<any, any, any>;
handlers?: Map<any, any>;
}
function invokeWithDynamicDependencies<
TBase,
TImpl extends Impl<TBase> = Impl<TBase>,
TArgs extends Args<TBase> = Args<TBase>
>(
container: Container,
fn: DependencyCtorOrFunctor<TBase, TImpl, TArgs>,
staticDependencies: TArgs[number][],
dynamicDependencies: TArgs[number][]
) {
let i = staticDependencies.length;
let args = new Array(i);
let lookup;
while (i--) {
lookup = staticDependencies[i];
if (lookup === null || lookup === undefined) {
throw new Error(
'Constructor Parameter with index ' +
i +
' cannot be null or undefined. Are you trying to inject/register something that doesn\'t exist with DI?'
);
} else {
args[i] = container.get(lookup);
}
}
if (dynamicDependencies !== undefined) {
args = args.concat(dynamicDependencies);
}
return Reflect.construct(fn, args);
}
const classInvoker: Invoker<any, any, any> = {
invoke(container, Type: DependencyCtor<any, any, any>, deps) {
const instances = deps.map((dep) => container.get(dep));
return Reflect.construct(Type, instances);
},
invokeWithDynamicDependencies
};
function getDependencies(f) {
if (!f.hasOwnProperty('inject')) {
return [];
}
if (typeof f.inject === 'function') {
return f.inject();
}
return f.inject;
}
/**
* A lightweight, extensible dependency injection container.
*/
export class Container {
/**
* The global root Container instance. Available if makeGlobal() has been
* called. Aurelia Framework calls makeGlobal().
*/
public static instance: Container;
/**
* The parent container in the DI hierarchy.
*/
public parent: Container | null;
/**
* The root container in the DI hierarchy.
*/
public root: Container;
/** @internal */
public _configuration: ContainerConfiguration;
/** @internal */
public _onHandlerCreated: (
handler: InvocationHandler<any, any, any>
) => InvocationHandler<any, any, any>;
/** @internal */
public _handlers: Map<any, any>;
/** @internal */
public _resolvers: Map<any, any>;
/**
* Creates an instance of Container.
* @param configuration Provides some configuration for the new Container instance.
*/
constructor(configuration?: ContainerConfiguration) {
if (configuration === undefined) {
configuration = {};
}
this._configuration = configuration;
this._onHandlerCreated = configuration.onHandlerCreated!;
this._handlers =
configuration.handlers || (configuration.handlers = new Map());
this._resolvers = new Map();
this.root = this;
this.parent = null;
}
/**
* Makes this container instance globally reachable through Container.instance.
*/
public makeGlobal(): Container {
Container.instance = this;
return this;
}
/**
* Sets an invocation handler creation callback that will be called when new
* InvocationsHandlers are created (called once per Function).
* @param onHandlerCreated The callback to be called when an
* InvocationsHandler is created.
*/
public setHandlerCreatedCallback<
TBase,
TImpl extends Impl<TBase> = Impl<TBase>,
TArgs extends Args<TBase> = Args<TBase>>(
onHandlerCreated: (
handler: InvocationHandler<TBase, TImpl, TArgs>
) => InvocationHandler<TBase, TImpl, TArgs>
) {
this._onHandlerCreated = onHandlerCreated;
this._configuration.onHandlerCreated = onHandlerCreated;
}
/**
* Registers an existing object instance with the container.
* @param key The key that identifies the dependency at resolution time;
* usually a constructor function.
* @param instance The instance that will be resolved when the key is matched.
* This defaults to the key value when instance is not supplied.
* @return The resolver that was registered.
*/
public registerInstance<TBase, TImpl extends Impl<TBase> = Impl<TBase>, TArgs extends Args<TBase> = Args<TBase>>(
key: PrimitiveOrDependencyCtor<TBase, TImpl, TArgs>,
instance?: TImpl): Resolver {
return this.registerResolver(
key,
new StrategyResolver(0, instance === undefined ? key : instance)
);
}
/**
* Registers a type (constructor function) such that the container always
* returns the same instance for each request.
* @param key The key that identifies the dependency at resolution time;
* usually a constructor function.
* @param fn The constructor function to use when the dependency needs to be
* instantiated. This defaults to the key value when fn is not supplied.
* @return The resolver that was registered.
*/
public registerSingleton<TBase, TImpl extends Impl<TBase> = Impl<TBase>, TArgs extends Args<TBase> = Args<TBase>>(
key: Primitive, fn: DependencyCtorOrFunctor<TBase, TImpl, TArgs>): Resolver;
public registerSingleton<TBase, TImpl extends Impl<TBase> = Impl<TBase>, TArgs extends Args<TBase> = Args<TBase>>(
key: DependencyCtor<TBase, TImpl, TArgs>, fn?: DependencyCtorOrFunctor<TBase, TImpl, TArgs>): Resolver;
public registerSingleton<TBase, TImpl extends Impl<TBase> = Impl<TBase>, TArgs extends Args<TBase> = Args<TBase>>(
key: PrimitiveOrDependencyCtor<TBase, TImpl, TArgs>, fn?: DependencyCtorOrFunctor<TBase, TImpl, TArgs>): Resolver {
return this.registerResolver(
key,
new StrategyResolver(Strategy.singleton, fn === undefined ? key as DependencyCtor<TBase, TImpl, TArgs> : fn)
);
}
/**
* Registers a type (constructor function) such that the container returns a
* new instance for each request.
* @param key The key that identifies the dependency at resolution time;
* usually a constructor function.
* @param fn The constructor function to use when the dependency needs to be
* instantiated. This defaults to the key value when fn is not supplied.
* @return The resolver that was registered.
*/
public registerTransient<TBase, TImpl extends Impl<TBase> = Impl<TBase>, TArgs extends Args<TBase> = Args<TBase>>(
key: Primitive, fn: DependencyCtorOrFunctor<TBase, TImpl, TArgs>): Resolver;
public registerTransient<TBase, TImpl extends Impl<TBase> = Impl<TBase>, TArgs extends Args<TBase> = Args<TBase>>(
key: DependencyCtor<TBase, TImpl, TArgs>, fn?: DependencyCtorOrFunctor<TBase, TImpl, TArgs>): Resolver;
public registerTransient<TBase, TImpl extends Impl<TBase> = Impl<TBase>, TArgs extends Args<TBase> = Args<TBase>>(
key: PrimitiveOrDependencyCtor<TBase, TImpl, TArgs>, fn?: DependencyCtorOrFunctor<TBase, TImpl, TArgs>): Resolver {
return this.registerResolver(
key,
new StrategyResolver(2, fn === undefined ? key as DependencyCtor<TBase, TImpl, TArgs> : fn)
);
}
/**
* Registers a custom resolution function such that the container calls this
* function for each request to obtain the instance.
* @param key The key that identifies the dependency at resolution time;
* usually a constructor function.
* @param handler The resolution function to use when the dependency is
* needed.
* @return The resolver that was registered.
*/
public registerHandler<TBase, TImpl extends Impl<TBase> = Impl<TBase>, TArgs extends Args<TBase> = Args<TBase>>(
key: PrimitiveOrDependencyCtor<TBase, TImpl, TArgs>,
handler: (container?: Container, key?: PrimitiveOrDependencyCtor<TBase, TImpl, TArgs>, resolver?: Resolver) => any
): Resolver {
return this.registerResolver(
key,
new StrategyResolver<TBase, TImpl, TArgs, Strategy.function>(Strategy.function, handler)
);
}
/**
* Registers an additional key that serves as an alias to the original DI key.
* @param originalKey The key that originally identified the dependency; usually a constructor function.
* @param aliasKey An alternate key which can also be used to resolve the same dependency as the original.
* @return The resolver that was registered.
*/
public registerAlias<TBase, TImpl extends Impl<TBase> = Impl<TBase>, TArgs extends Args<TBase> = Args<TBase>>(
originalKey: PrimitiveOrDependencyCtor<TBase, TImpl, TArgs>,
aliasKey: PrimitiveOrDependencyCtor<TBase, TImpl, TArgs>): Resolver {
return this.registerResolver(
aliasKey,
new StrategyResolver(5, originalKey)
);
}
/**
* Registers a custom resolution function such that the container calls this
* function for each request to obtain the instance.
* @param key The key that identifies the dependency at resolution time;
* usually a constructor function.
* @param resolver The resolver to use when the dependency is needed.
* @return The resolver that was registered.
*/
public registerResolver<TBase, TImpl extends Impl<TBase> = Impl<TBase>, TArgs extends Args<TBase> = Args<TBase>>(
key: PrimitiveOrDependencyCtor<TBase, TImpl, TArgs>,
resolver: Resolver
): Resolver {
validateKey(key);
const allResolvers = this._resolvers;
const result = allResolvers.get(key);
if (result === undefined) {
allResolvers.set(key, resolver);
} else if (result.strategy === 4) {
result.state.push(resolver);
} else {
allResolvers.set(key, new StrategyResolver(4, [result, resolver]));
}
return resolver;
}
/**
* Registers a type (constructor function) by inspecting its registration
* annotations. If none are found, then the default singleton registration is
* used.
* @param key The key that identifies the dependency at resolution time;
* usually a constructor function.
* @param fn The constructor function to use when the dependency needs to be
* instantiated. This defaults to the key value when fn is not supplied.
*/
public autoRegister<TBase, TImpl extends Impl<TBase> = Impl<TBase>, TArgs extends Args<TBase> = Args<TBase>>(
key: Primitive, fn: DependencyCtorOrFunctor<TBase, TImpl, TArgs>): Resolver;
public autoRegister<TBase, TImpl extends Impl<TBase> = Impl<TBase>, TArgs extends Args<TBase> = Args<TBase>>(
key: DependencyCtor<TBase, TImpl, TArgs>, fn?: DependencyCtorOrFunctor<TBase, TImpl, TArgs>): Resolver;
public autoRegister<TBase, TImpl extends Impl<TBase> = Impl<TBase>, TArgs extends Args<TBase> = Args<TBase>>(
key: PrimitiveOrDependencyCtor<TBase, TImpl, TArgs>, fn?: DependencyCtorOrFunctor<TBase, TImpl, TArgs>): Resolver {
fn = fn === undefined ? key as DependencyCtor<TBase, TImpl, TArgs> : fn;
if (typeof fn === 'function') {
const registration = metadata.get(metadata.registration, fn);
if (registration === undefined) {
return this.registerResolver(key, new StrategyResolver(1, fn));
}
return registration.registerResolver(this, key, fn);
}
return this.registerResolver(key, new StrategyResolver(0, fn));
}
/**
* Registers an array of types (constructor functions) by inspecting their
* registration annotations. If none are found, then the default singleton
* registration is used.
* @param fns The constructor function to use when the dependency needs to be instantiated.
*/
public autoRegisterAll(fns: DependencyCtor<any, any, any>[]): void {
let i = fns.length;
while (i--) {
this.autoRegister<any, any, any>(fns[i]);
}
}
/**
* Unregisters based on key.
* @param key The key that identifies the dependency at resolution time; usually a constructor function.
*/
public unregister(key: any): void {
this._resolvers.delete(key);
}
/**
* Inspects the container to determine if a particular key has been registred.
* @param key The key that identifies the dependency at resolution time; usually a constructor function.
* @param checkParent Indicates whether or not to check the parent container hierarchy.
* @return Returns true if the key has been registred; false otherwise.
*/
public hasResolver<TBase, TImpl extends Impl<TBase> = Impl<TBase>, TArgs extends Args<TBase> = Args<TBase>>(
key: PrimitiveOrDependencyCtor<TBase, TImpl, TArgs>, checkParent: boolean = false): boolean {
validateKey(key);
return (
this._resolvers.has(key) ||
(checkParent &&
this.parent !== null &&
this.parent.hasResolver(key, checkParent))
);
}
/**
* Gets the resolver for the particular key, if it has been registered.
* @param key The key that identifies the dependency at resolution time; usually a constructor function.
* @return Returns the resolver, if registred, otherwise undefined.
*/
public getResolver<
TStrategyKey extends keyof StrategyState<TBase, TImpl, TArgs>,
TBase,
TImpl extends Impl<TBase> = Impl<TBase>,
TArgs extends Args<TBase> = Args<TBase>
>(
key: PrimitiveOrDependencyCtorOrFunctor<TBase, TImpl, TArgs>
): StrategyResolver<TBase, TImpl, TArgs, TStrategyKey> {
return this._resolvers.get(key);
}
/**
* Resolves a single instance based on the provided key.
* @param key The key that identifies the object to resolve.
* @return Returns the resolved instance.
*/
public get<TBase, TResolver extends NewInstance<TBase> | Lazy<TBase> | Factory<TBase> | Optional<TBase> | Parent<TBase> | All<TBase>>(
key: TResolver): ResolvedValue<TResolver>;
public get<TBase, TImpl extends Impl<TBase> = Impl<TBase>, TArgs extends Args<TBase> = Args<TBase>>(
key: PrimitiveOrDependencyCtor<TBase, TImpl, TArgs>): ImplOrAny<TImpl>;
public get<TBase, TImpl extends Impl<TBase> = Impl<TBase>, TArgs extends Args<TBase> = Args<TBase>>(
key: typeof Container): Container;
public get<TBase, TImpl extends Impl<TBase> = Impl<TBase>, TArgs extends Args<TBase> = Args<TBase>>(
key: PrimitiveOrDependencyCtor<TBase, TImpl, TArgs> | typeof Container): ImplOrAny<TImpl> | Container {
validateKey(key);
if (key === Container) {
return this;
}
if (resolverDecorates(key)) {
return key.get(this, key);
}
const resolver = this._resolvers.get(key);
if (resolver === undefined) {
if (this.parent === null) {
return this.autoRegister(key as DependencyCtor<TBase, TImpl, TArgs>).get(this, key);
}
const registration = metadata.get(metadata.registration, key);
if (registration === undefined) {
return this.parent._get(key);
}
return registration.registerResolver(
this, key, key as DependencyCtorOrFunctor<TBase, TImpl, TArgs>).get(this, key);
}
return resolver.get(this, key);
}
public _get(key) {
const resolver = this._resolvers.get(key);
if (resolver === undefined) {
if (this.parent === null) {
return this.autoRegister(key).get(this, key);
}
return this.parent._get(key);
}
return resolver.get(this, key);
}
/**
* Resolves all instance registered under the provided key.
* @param key The key that identifies the objects to resolve.
* @return Returns an array of the resolved instances.
*/
public getAll<TBase, TImpl extends Impl<TBase> = Impl<TBase>, TArgs extends Args<TBase> = Args<TBase>>(
key: PrimitiveOrDependencyCtor<TBase, TImpl, TArgs>): ImplOrAny<TImpl>[] {
validateKey(key);
const resolver = this._resolvers.get(key);
if (resolver === undefined) {
if (this.parent === null) {
return _emptyParameters;
}
return this.parent.getAll(key);
}
if (resolver.strategy === 4) {
const state = resolver.state;
let i = state.length;
const results = new Array(i);
while (i--) {
results[i] = state[i].get(this, key);
}
return results;
}
return [resolver.get(this, key)];
}
/**
* Creates a new dependency injection container whose parent is the current container.
* @return Returns a new container instance parented to this.
*/
public createChild(): Container {
const child = new Container(this._configuration);
child.root = this.root;
child.parent = this;
return child;
}
/**
* Invokes a function, recursively resolving its dependencies.
* @param fn The function to invoke with the auto-resolved dependencies.
* @param dynamicDependencies Additional function dependencies to use during invocation.
* @return Returns the instance resulting from calling the function.
*/
public invoke<TBase, TImpl extends Impl<TBase> = Impl<TBase>, TArgs extends Args<TBase> = Args<TBase>>(
fn: DependencyCtorOrFunctor<TBase, TImpl, TArgs>,
dynamicDependencies?: TArgs[number][]
): ImplOrAny<TImpl> {
try {
let handler = this._handlers.get(fn);
if (handler === undefined) {
handler = this._createInvocationHandler(fn);
this._handlers.set(fn, handler);
}
return handler.invoke(this, dynamicDependencies);
} catch (e) {
// @ts-expect-error AggregateError returns an Error in its type hence it fails (new ...) but it's fine
throw new AggregateError(
`Error invoking ${fn.name}. Check the inner error for details.`,
e as Error | undefined,
true
);
}
}
public _createInvocationHandler
<TBase, TImpl extends Impl<TBase> = Impl<TBase>, TArgs extends Args<TBase> = Args<TBase>>(
fn: DependencyCtorOrFunctor<TBase, TImpl, TArgs> & { inject?: any; }
): InvocationHandler<TBase, TImpl, TArgs> {
let dependencies;
if (fn.inject === undefined) {
dependencies =
metadata.getOwn(metadata.paramTypes, fn) || _emptyParameters;
} else {
dependencies = [];
let ctor = fn;
while (typeof ctor === 'function') {
dependencies.push(...getDependencies(ctor));
ctor = Object.getPrototypeOf(ctor);
}
}
const invoker = metadata.getOwn(metadata.invoker, fn) || classInvoker;
const handler = new InvocationHandler(fn, invoker, dependencies);
return this._onHandlerCreated !== undefined
? this._onHandlerCreated(handler)
: handler;
}
}
export type ResolvedValue<T> =
T extends (new (...args: any[]) => infer R)
? R
: T extends (abstract new (...args: any[]) => infer R)
? R
: T extends Factory<infer R>
? (...args: unknown[]) => R
: T extends Lazy<infer R>
? () => R
: T extends NewInstance<infer R>
? R
: T extends Optional<infer R>
? R | null
: T extends All<infer R>
? R[]
: T extends Parent<infer R>
? R | null
: T extends [infer T1, ...infer T2]
? [ResolvedValue<T1>, ...ResolvedValue<T2>]
: T;
/**
* Resolve a key, or list of keys based on the current container.
*
* @example
* ```ts
* import { resolve } from 'aurelia-framework';
* // or
* // import { Container, resolve } from 'aurelia-dependency-injection';
*
* class MyCustomElement {
* someService = resolve(MyService);
* }
* ```
*/
export function resolve<K extends any>(key: K): ResolvedValue<K>;
export function resolve<K extends any[]>(...keys: K): ResolvedValue<K>
export function resolve<K extends any[]>(...keys: K) {
if (currentContainer == null) {
throw new Error(`There is not a currently active container to resolve "${String(keys)}". Are you trying to "new SomeClass(...)" that has a resolve(...) call?`);
}
return keys.length === 1
? currentContainer.get(keys[0])
: keys.map(containerGetKey, currentContainer);
}
function containerGetKey(this: Container, key: any) {
return this.get(key);
}