container.ts 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. import { Service } from "./service"
  2. import { Observable, Subject } from 'rxjs'
  3. /**
  4. * Stores the current container instance in the current operating context.
  5. *
  6. * NOTE: This should not be used outside of dioc library code
  7. */
  8. export let currentContainer: Container | null = null
  9. /**
  10. * The events emitted by the container
  11. *
  12. * `SERVICE_BIND` - emitted when a service is bound to the container directly or as a dependency to another service
  13. * `SERVICE_INIT` - emitted when a service is initialized
  14. */
  15. export type ContainerEvent =
  16. | {
  17. type: 'SERVICE_BIND';
  18. /** The Service ID of the service being bounded (the dependency) */
  19. boundeeID: string;
  20. /**
  21. * The Service ID of the bounder that is binding the boundee (the dependent)
  22. *
  23. * NOTE: This will be undefined if the service is bound directly to the container
  24. */
  25. bounderID: string | undefined
  26. }
  27. | {
  28. type: 'SERVICE_INIT';
  29. /** The Service ID of the service being initialized */
  30. serviceID: string
  31. }
  32. /**
  33. * The dependency injection container, allows for services to be initialized and maintains the dependency trees.
  34. */
  35. export class Container {
  36. /** Used during the `bind` operation to detect circular dependencies */
  37. private bindStack: string[] = []
  38. /** The map of bound services to their IDs */
  39. protected boundMap = new Map<string, Service<unknown>>()
  40. /** The RxJS observable representing the event stream */
  41. protected event$ = new Subject<ContainerEvent>()
  42. /**
  43. * Returns whether a container has the given service bound
  44. * @param service The service to check for
  45. */
  46. public hasBound<
  47. T extends typeof Service<any> & { ID: string }
  48. >(service: T): boolean {
  49. return this.boundMap.has(service.ID)
  50. }
  51. /**
  52. * Returns the service bound to the container with the given ID or if not found, undefined.
  53. *
  54. * NOTE: This is an advanced method and should not be used as much as possible.
  55. *
  56. * @param serviceID The ID of the service to get
  57. */
  58. public getBoundServiceWithID(serviceID: string): Service<unknown> | undefined {
  59. return this.boundMap.get(serviceID)
  60. }
  61. /**
  62. * Binds a service to the container. This is equivalent to marking a service as a dependency.
  63. * @param service The class reference of a service to bind
  64. * @param bounder The class reference of the service that is binding the service (if bound directly to the container, this should be undefined)
  65. */
  66. public bind<T extends typeof Service<any> & { ID: string }>(
  67. service: T,
  68. bounder: ((typeof Service<T>) & { ID: string }) | undefined = undefined
  69. ): InstanceType<T> {
  70. // We need to store the current container in a variable so that we can restore it after the bind operation
  71. const oldCurrentContainer = currentContainer;
  72. currentContainer = this;
  73. // If the service is already bound, return the existing instance
  74. if (this.hasBound(service)) {
  75. this.event$.next({
  76. type: 'SERVICE_BIND',
  77. boundeeID: service.ID,
  78. bounderID: bounder?.ID // Return the bounder ID if it is defined, else assume its the container
  79. })
  80. return this.boundMap.get(service.ID) as InstanceType<T> // Casted as InstanceType<T> because service IDs and types are expected to match
  81. }
  82. // Detect circular dependency and throw error
  83. if (this.bindStack.findIndex((serviceID) => serviceID === service.ID) !== -1) {
  84. const circularServices = `${this.bindStack.join(' -> ')} -> ${service.ID}`
  85. throw new Error(`Circular dependency detected.\nChain: ${circularServices}`)
  86. }
  87. // Push the service ID onto the bind stack to detect circular dependencies
  88. this.bindStack.push(service.ID)
  89. // Initialize the service and emit events
  90. // NOTE: We need to cast the service to any as TypeScript thinks that the service is abstract
  91. const instance: Service<any> = new (service as any)()
  92. this.boundMap.set(service.ID, instance)
  93. this.bindStack.pop()
  94. this.event$.next({
  95. type: 'SERVICE_INIT',
  96. serviceID: service.ID,
  97. })
  98. this.event$.next({
  99. type: 'SERVICE_BIND',
  100. boundeeID: service.ID,
  101. bounderID: bounder?.ID
  102. })
  103. // Restore the current container
  104. currentContainer = oldCurrentContainer;
  105. // We expect the return type to match the service definition
  106. return instance as InstanceType<T>
  107. }
  108. /**
  109. * Returns an iterator of the currently bound service IDs and their instances
  110. */
  111. public getBoundServices(): IterableIterator<[string, Service<any>]> {
  112. return this.boundMap.entries()
  113. }
  114. /**
  115. * Returns the public container event stream
  116. */
  117. public getEventStream(): Observable<ContainerEvent> {
  118. return this.event$.asObservable()
  119. }
  120. }