|
import { |
|
DependencyContainer, |
|
InjectionToken, |
|
RegistrationOptions, |
|
baseContainer, |
|
constructor, |
|
delay, |
|
injectable, |
|
registry, |
|
} from "./driver"; |
|
import { DependencySpec, getDecorationParams } from "./core"; |
|
|
|
// #region Dependency Acquisition |
|
/** |
|
* Gets a named dependency from the container. See also |
|
* {@link acquire.all} and {@link acquire.many} |
|
*/ |
|
export interface IAcquireFunction<TDeps> { |
|
<K extends DepId<TDeps>>(id: K): TDeps[K]; |
|
/** Gets all of the named dependencies from the container. */ |
|
all: IAcquireAllFunction<TDeps>; |
|
/** |
|
* Gets the registered implementations of the given dependency id. |
|
*/ |
|
many: IAcquireManyFunction<TDeps>; |
|
} |
|
/** Gets all of the named dependencies from the container. */ |
|
export interface IAcquireAllFunction<TDeps> { |
|
<K extends DepId<TDeps>>( |
|
ids: Array<K>, |
|
): { |
|
[E in Array<K>[number]]: TDeps[E]; |
|
}; |
|
} |
|
/** |
|
* Gets the registered implementations of the given dependency id. |
|
*/ |
|
export interface IAcquireManyFunction<TDeps> { |
|
<K extends DepId<TDeps>>(id: K): Array<TDeps[K]>; |
|
} |
|
// #endregion |
|
|
|
// #region Loaders |
|
/** |
|
* A {@link target} class to load by making it {@link injectable} along with |
|
* it's {@link dependencies}. |
|
*/ |
|
export interface PlatformTypeLoad<TDeps, T = any> { |
|
target: constructor<T>; |
|
dependencies?: Dep<TDeps>[]; |
|
} |
|
/** Function that produces a {@link PlatformTypeLoad}. */ |
|
export type PlatformTypeLoader<TDeps, T = any> = () => PlatformTypeLoad< |
|
TDeps, |
|
T |
|
>; |
|
// #endregion |
|
|
|
// #region Utility |
|
// - Types that couldn't be nested inside the generic DIContainer. |
|
// |
|
/** Key of `TDeps` restricted to `string` keys only. */ |
|
type DepId<TDeps> = Extract<keyof TDeps, string>; |
|
/** Dependency key or type or a specification for loading one by key or type. */ |
|
type Dep<TDeps> = DependencySpec<DepId<TDeps>>; |
|
// #endregion |
|
|
|
/** |
|
* A Dependency Injection container with automatic dependency id and TypeScript |
|
* type lookup in registration and resolution methods. |
|
* |
|
* Pass a blank interface type as `TDeps` and then let your services modules |
|
* use TypeScript module augmentation to add things to that interface, e.g. |
|
* `declare module "@/my/container" { interface MyDeps { ... } }`. |
|
* See examples for more details. |
|
*/ |
|
export class DIContainer<TDeps> { |
|
private impl: DependencyContainer = baseContainer; |
|
|
|
constructor() { |
|
this.acquire.all = this.acquireAll; |
|
this.acquire.many = this.acquireMany; |
|
} |
|
|
|
/** |
|
* Makes a {@link DIContainer} a child of this one. The given `container` |
|
* MUST NOT have had any types registered with it before adoption. |
|
* |
|
* This pattern simplifies the work that the TypeScript parser must do to |
|
* track the source and types of the `DIContainer` methods. |
|
*/ |
|
adoptChild(container: DIContainer<unknown>) { |
|
const child = this.impl.createChildContainer(); |
|
container.impl = child; |
|
} |
|
|
|
// #region Dependency Acquisition |
|
/** |
|
* Gets a named dependency from the container. See also |
|
* {@link acquire.all} and {@link acquire.many} |
|
*/ |
|
public readonly acquire = ((id) => { |
|
return this.impl.resolve(id); |
|
}) as IAcquireFunction<TDeps>; |
|
|
|
public readonly acquireAll: IAcquireAllFunction<TDeps> = (ids) => { |
|
const acquired: ReturnType<IAcquireAllFunction<TDeps>> = {} as any; |
|
|
|
for (const id of ids) { |
|
// Guard against acquiring anything twice |
|
acquired[id] = acquired[id] ?? this.impl.resolve(id); |
|
// CONSIDER: Throwing an error for duplicate ids at DEV time. |
|
// NOTE: TypeScript can't EASILY be used to type an array of unique ids. |
|
} |
|
|
|
return acquired; |
|
}; |
|
|
|
public readonly acquireMany: IAcquireManyFunction<TDeps> = (id) => { |
|
return this.impl.resolveAll(id); |
|
}; |
|
|
|
/** |
|
* Resolves a string, symbol, class or delayed dependency from the container. |
|
*/ |
|
public readonly resolve = <T = any>(token: InjectionToken<T>) => { |
|
return this.impl.resolve<T>(token); |
|
}; |
|
/** |
|
* Resolves all registered implementations of the given string, symbol, class |
|
* or delayed constructor dependency token. |
|
*/ |
|
public readonly resolveMany = <T = any>(token: InjectionToken<T>): T[] => { |
|
return this.impl.resolveAll<T>(token); |
|
}; |
|
// #endregion |
|
|
|
// #region Register Types |
|
/** |
|
* Registers an anonymous `class` which is **transient** by default, i.e. it |
|
* will be newly created for every dependent. Optionally pass a different |
|
* {@link Lifecycle} in the 3rd argument. |
|
* @param target The class to register with container. |
|
* @param dependencies The container items to be injected this class's |
|
* constructor. See {@link PlatformDependency} for examples. |
|
*/ |
|
public readonly registerClass = <T>( |
|
target: constructor<T>, |
|
dependencies: Dep<TDeps>[] = [], |
|
options?: RegistrationOptions, |
|
) => { |
|
this.registerInjectable(target, dependencies); |
|
this.impl.register<T>( |
|
target, |
|
{ |
|
useClass: target, |
|
}, |
|
options, |
|
); |
|
}; |
|
/** |
|
* Registers an injectable class by applying the tsyringe {@link injectable} |
|
* decorator to the given `target` class including the `@inject("dep")` |
|
* decorator for each dependency given. |
|
* See https://github.com/microsoft/tsyringe#injectable |
|
* @param target The class to register with container. |
|
* @param dependencies The container items to be injected this class's |
|
* constructor. See {@link PlatformDependency} for examples. |
|
*/ |
|
public readonly registerInjectable = <T = any>( |
|
target: constructor<T>, |
|
dependencies: Dep<TDeps>[] = [], |
|
) => { |
|
const { delayed, params, paramtypes } = getDecorationParams(dependencies); |
|
if (params.length > 0) { |
|
// const x = delayed ? registry(delayed) : undefined; |
|
Reflect.decorate( |
|
[ |
|
injectable() as ClassDecorator, |
|
...(delayed ? [registry(delayed)] : []), |
|
...params, |
|
Reflect.metadata("design:paramtypes", paramtypes), |
|
], |
|
target, |
|
); |
|
} |
|
}; |
|
/** |
|
* Registers a **singleton** class. It will ONLY be created ONCE and the same |
|
* instance will be given to all dependents. |
|
* @param target The class to register with container. |
|
* @param dependencies The container items to be injected this class's |
|
* constructor. See {@link PlatformDependency} for examples. |
|
*/ |
|
public readonly registerSingleton = <T = any>( |
|
// TODO: target should be constructor<any> | InjectionToken<any>... |
|
target: constructor<T>, |
|
dependencies: Dep<TDeps>[] = [], |
|
) => { |
|
this.registerInjectable(target, dependencies); |
|
this.impl.registerSingleton(target); |
|
}; |
|
/** |
|
* Register a token which is a redirect or an alias. It simply states that |
|
* given token `x`, resolve using token `y`. |
|
* See https://github.com/microsoft/tsyringe#token-provider |
|
*/ |
|
public readonly registerToken = <T = any>( |
|
target: InjectionToken<T>, |
|
token: InjectionToken<T>, |
|
options?: RegistrationOptions, |
|
) => { |
|
this.impl.register<T>( |
|
target, |
|
{ |
|
useToken: token, |
|
}, |
|
options, |
|
); |
|
}; |
|
// #endregion |
|
|
|
// #region Register Factories, Loaders |
|
|
|
/** Registers a named factory *(of any type)* or a `class` factory. */ |
|
public readonly registerFactory = <T = any>( |
|
idOrClass: DepId<TDeps> | constructor<T>, |
|
factory: (dependencyContainer: DependencyContainer) => T, |
|
) => { |
|
this.impl.register<T>(idOrClass, { |
|
useFactory: factory, |
|
}); |
|
}; |
|
/** |
|
* Creates a named, *delayed loader* for a *dynamic* `class`. |
|
* @param id Id of the loaded type as it will be used by dependents. |
|
* @param loader Function to return target class and dependencies. |
|
* @example |
|
* registerLoader( |
|
* "thing", |
|
* () => { |
|
* console.log("Now setting up type DynamicProvider..."); |
|
* class DynamicProvider { |
|
* constructor(private logger: ILogger) {} |
|
* getConfig(name: string) { |
|
* this.logger.info(`Getting "${name}" config...`); |
|
* return `${name}.config`; |
|
* } |
|
* } |
|
* return { |
|
* target: import.meta.env.DEV ? DynamicProvider : StandardProvider, |
|
* dependencies: ["logger"], |
|
* }; |
|
* }, |
|
* { lifecycle: Lifecycle.Singleton }, |
|
* ); |
|
*/ |
|
public readonly registerLoader = <T = any>( |
|
id: DepId<TDeps>, |
|
loader: PlatformTypeLoader<TDeps, T>, |
|
options?: RegistrationOptions, |
|
) => { |
|
this.impl.register<T>( |
|
id, |
|
{ |
|
useToken: delay(() => { |
|
const { target, dependencies } = loader(); |
|
this.registerInjectable<T>(target, dependencies); |
|
return target; |
|
}), |
|
}, |
|
options, |
|
); |
|
}; |
|
// #endregion |
|
|
|
// #region Register Providers |
|
/** |
|
* Registers a named provider class which is **transient** by default, i.e. |
|
* it will be newly created for every dependent. Optionally pass a different |
|
* {@link Lifecycle} in the 4th argument. |
|
* @param id Id of the provider as it will be used by dependents. |
|
* @param target The class to register with container. |
|
* @param dependencies The container items to be injected this class's |
|
* constructor. See {@link PlatformDependency} for examples. |
|
*/ |
|
public readonly registerProvider = <T>( |
|
id: DepId<TDeps>, |
|
target: constructor<T>, |
|
dependencies: Dep<TDeps>[] = [], |
|
options?: RegistrationOptions, |
|
) => { |
|
this.registerInjectable(target, dependencies); |
|
this.impl.register<T>( |
|
id, |
|
{ |
|
useClass: target, |
|
}, |
|
options, |
|
); |
|
}; |
|
// #endregion |
|
|
|
// #region Register Services |
|
/** |
|
* Registers a **singleton** service class. The {@link target} `class` will |
|
* ONLY be created ONCE. |
|
* @param id Id of the service as it will be used by dependents. |
|
* @param target The class that will be instantiated by the platform container. |
|
* @param dependencies The container items to be injected this class's |
|
* constructor. See {@link PlatformDependency} for examples. |
|
*/ |
|
public readonly registerService = <T = any>( |
|
id: DepId<TDeps>, |
|
target: constructor<T>, |
|
dependencies: Dep<TDeps>[] = [], |
|
) => { |
|
this.registerInjectable(target, dependencies); |
|
this.impl.registerSingleton(id, target); |
|
}; |
|
// #endregion |
|
|
|
// #region Register Values |
|
/** Registers named `value` *(of any type)* or a `class` instance. */ |
|
public readonly registerValue = <K extends DepId<TDeps>, T = any>( |
|
id: K | constructor<T>, |
|
value: TDeps[K] | T, |
|
) => { |
|
this.impl.register(id, { |
|
useValue: value, |
|
}); |
|
}; |
|
// #endregion |
|
} |