123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147 |
- import { Service } from "./service"
- import { Observable, Subject } from 'rxjs'
- /**
- * Stores the current container instance in the current operating context.
- *
- * NOTE: This should not be used outside of dioc library code
- */
- export let currentContainer: Container | null = null
- /**
- * The events emitted by the container
- *
- * `SERVICE_BIND` - emitted when a service is bound to the container directly or as a dependency to another service
- * `SERVICE_INIT` - emitted when a service is initialized
- */
- export type ContainerEvent =
- | {
- type: 'SERVICE_BIND';
- /** The Service ID of the service being bounded (the dependency) */
- boundeeID: string;
- /**
- * The Service ID of the bounder that is binding the boundee (the dependent)
- *
- * NOTE: This will be undefined if the service is bound directly to the container
- */
- bounderID: string | undefined
- }
- | {
- type: 'SERVICE_INIT';
- /** The Service ID of the service being initialized */
- serviceID: string
- }
- /**
- * The dependency injection container, allows for services to be initialized and maintains the dependency trees.
- */
- export class Container {
- /** Used during the `bind` operation to detect circular dependencies */
- private bindStack: string[] = []
- /** The map of bound services to their IDs */
- protected boundMap = new Map<string, Service<unknown>>()
- /** The RxJS observable representing the event stream */
- protected event$ = new Subject<ContainerEvent>()
- /**
- * Returns whether a container has the given service bound
- * @param service The service to check for
- */
- public hasBound<
- T extends typeof Service<any> & { ID: string }
- >(service: T): boolean {
- return this.boundMap.has(service.ID)
- }
- /**
- * Returns the service bound to the container with the given ID or if not found, undefined.
- *
- * NOTE: This is an advanced method and should not be used as much as possible.
- *
- * @param serviceID The ID of the service to get
- */
- public getBoundServiceWithID(serviceID: string): Service<unknown> | undefined {
- return this.boundMap.get(serviceID)
- }
- /**
- * Binds a service to the container. This is equivalent to marking a service as a dependency.
- * @param service The class reference of a service to bind
- * @param bounder The class reference of the service that is binding the service (if bound directly to the container, this should be undefined)
- */
- public bind<T extends typeof Service<any> & { ID: string }>(
- service: T,
- bounder: ((typeof Service<T>) & { ID: string }) | undefined = undefined
- ): InstanceType<T> {
- // We need to store the current container in a variable so that we can restore it after the bind operation
- const oldCurrentContainer = currentContainer;
- currentContainer = this;
- // If the service is already bound, return the existing instance
- if (this.hasBound(service)) {
- this.event$.next({
- type: 'SERVICE_BIND',
- boundeeID: service.ID,
- bounderID: bounder?.ID // Return the bounder ID if it is defined, else assume its the container
- })
- return this.boundMap.get(service.ID) as InstanceType<T> // Casted as InstanceType<T> because service IDs and types are expected to match
- }
- // Detect circular dependency and throw error
- if (this.bindStack.findIndex((serviceID) => serviceID === service.ID) !== -1) {
- const circularServices = `${this.bindStack.join(' -> ')} -> ${service.ID}`
- throw new Error(`Circular dependency detected.\nChain: ${circularServices}`)
- }
- // Push the service ID onto the bind stack to detect circular dependencies
- this.bindStack.push(service.ID)
- // Initialize the service and emit events
- // NOTE: We need to cast the service to any as TypeScript thinks that the service is abstract
- const instance: Service<any> = new (service as any)()
- this.boundMap.set(service.ID, instance)
- this.bindStack.pop()
- this.event$.next({
- type: 'SERVICE_INIT',
- serviceID: service.ID,
- })
- this.event$.next({
- type: 'SERVICE_BIND',
- boundeeID: service.ID,
- bounderID: bounder?.ID
- })
- // Restore the current container
- currentContainer = oldCurrentContainer;
- // We expect the return type to match the service definition
- return instance as InstanceType<T>
- }
- /**
- * Returns an iterator of the currently bound service IDs and their instances
- */
- public getBoundServices(): IterableIterator<[string, Service<any>]> {
- return this.boundMap.entries()
- }
- /**
- * Returns the public container event stream
- */
- public getEventStream(): Observable<ContainerEvent> {
- return this.event$.asObservable()
- }
- }
|