All files / src/internal/core service-initializer.mts

72.72% Statements 40/55
63.33% Branches 19/30
88.88% Functions 8/9
73.58% Lines 39/53

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185                            268x                           540x 540x   513x   27x                                                     513x 513x 651x     651x 651x 651x     513x 513x 138x 140x           138x 138x 138x     501x                             501x 21x   497x 32x 32x       497x   16x                                       27x 27x 27x 27x 27x 27x     27x 27x                         27x                           27x             27x 23x   4x                
import type { FactoryRecord } from '../../token/registry.mjs'
import type { Injectors } from '../../utils/index.mjs'
import type { ServiceInitializationContext } from '../context/service-initialization-context.mjs'
 
import { InjectableType } from '../../enums/index.mjs'
import { DIError } from '../../errors/index.mjs'
 
/**
 * Creates service instances from registry records.
 *
 * Handles both class-based (@Injectable) and factory-based (@Factory) services,
 * managing the instantiation lifecycle including lifecycle hook invocation.
 */
export class ServiceInitializer {
  constructor(private readonly injectors: Injectors) {}
 
  /**
   * Instantiates a service based on its registry record.
   * @param ctx The factory context for dependency injection
   * @param record The factory record from the registry
   * @param args Optional arguments for the service
   * @returns Promise resolving to [undefined, instance] or [error]
   */
  async instantiateService<T>(
    ctx: ServiceInitializationContext,
    record: FactoryRecord<T, any>,
    args: any = undefined,
  ): Promise<[undefined, T] | [DIError]> {
    try {
      switch (record.type) {
        case InjectableType.Class:
          return this.instantiateClass(ctx, record, args)
        case InjectableType.Factory:
          return this.instantiateFactory(ctx, record, args)
        default:
          throw DIError.unknown(
            `[ServiceInitializer] Unknown service type: ${record.type}`,
          )
      }
    } catch (error) {
      return [
        error instanceof DIError
          ? error
          : DIError.initializationError(record.target.name, error as Error),
      ]
    }
  }
 
  /**
   * Instantiates a class-based service (Injectable decorator).
   * @param ctx The factory context for dependency injection
   * @param record The factory record from the registry
   * @param args Optional arguments for the service constructor
   * @returns Promise resolving to [undefined, instance] or [error]
   */
  private async instantiateClass<T>(
    ctx: ServiceInitializationContext,
    record: FactoryRecord<T, any>,
    args: any,
  ): Promise<[undefined, T] | [DIError]> {
    try {
      const tryLoad = this.injectors.wrapSyncInit(() => {
        const original = this.injectors.provideFactoryContext(
          ctx as ServiceInitializationContext,
        )
        let result = new record.target(...(args ? [args] : []))
        this.injectors.provideFactoryContext(original)
        return result
      })
 
      let [instance, promises, injectState] = tryLoad()
      if (promises.length > 0) {
        const results = await Promise.allSettled(promises)
        Iif (results.some((result) => result.status === 'rejected')) {
          throw DIError.initializationError(
            record.target.name,
            new Error('Service cannot be instantiated'),
          )
        }
        const newRes = tryLoad(injectState)
        instance = newRes[0]
        promises = newRes[1]
      }
 
      Iif (promises.length > 0) {
        console.error(
          `[ServiceInitializer] ${record.target.name} has problem with it's definition.
 
       One or more of the dependencies are registered as a InjectableScope.Transient and are used with inject.
 
       Please use asyncInject instead of inject to load those dependencies.`,
        )
        throw DIError.initializationError(
          record.target.name,
          new Error('Service cannot be instantiated'),
        )
      }
 
      // Handle lifecycle hooks
      if ('onServiceInit' in instance) {
        await (instance as any).onServiceInit()
      }
      if ('onServiceDestroy' in instance) {
        ctx.addDestroyListener(async () => {
          await (instance as any).onServiceDestroy()
        })
      }
 
      return [undefined, instance]
    } catch (error) {
      return [
        error instanceof DIError
          ? error
          : DIError.initializationError(record.target.name, error as Error),
      ]
    }
  }
 
  /**
   * Instantiates a factory-based service (Factory decorator).
   * @param ctx The factory context for dependency injection
   * @param record The factory record from the registry
   * @param args Optional arguments for the factory
   * @returns Promise resolving to [undefined, instance] or [error]
   */
  private async instantiateFactory<T>(
    ctx: ServiceInitializationContext,
    record: FactoryRecord<T, any>,
    args: any,
  ): Promise<[undefined, T] | [DIError]> {
    try {
      const tryLoad = this.injectors.wrapSyncInit(() => {
        const original = this.injectors.provideFactoryContext(ctx)
        let result = new record.target()
        this.injectors.provideFactoryContext(original)
        return result
      })
 
      let [builder, promises, injectState] = tryLoad()
      Iif (promises.length > 0) {
        const results = await Promise.allSettled(promises)
        if (results.some((result) => result.status === 'rejected')) {
          throw DIError.initializationError(
            record.target.name,
            new Error('Service cannot be instantiated'),
          )
        }
        const newRes = tryLoad(injectState)
        builder = newRes[0]
        promises = newRes[1]
      }
 
      Iif (promises.length > 0) {
        console.error(
          `[ServiceInitializer] ${record.target.name} has problem with it's definition.
 
       One or more of the dependencies are registered as a InjectableScope.Transient and are used with inject.
 
       Please use asyncInject instead of inject to load those dependencies.`,
        )
        throw DIError.initializationError(
          record.target.name,
          new Error('Service cannot be instantiated'),
        )
      }
 
      Iif (typeof builder.create !== 'function') {
        throw DIError.initializationError(
          record.target.name,
          new Error('Factory does not implement the create method'),
        )
      }
 
      const instance = await builder.create(ctx, args)
      return [undefined, instance]
    } catch (error) {
      return [
        error instanceof DIError
          ? error
          : DIError.initializationError(record.target.name, error as Error),
      ]
    }
  }
}