All files / src/token registry.mts

98.11% Statements 52/53
90.24% Branches 37/41
90.9% Functions 10/11
98.07% Lines 51/52

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                          308x 308x   308x     1877x 1847x   30x 21x   9x           3050x 3050x 32x 29x   3x   3018x           8x 8x 2x 1x   1x     6x                   793x                 793x 793x 793x     793x 793x 734x         3x 3x 2x 2x 2x     2x 1x   2x     1x 1x     1x 1x                                     38x 38x   35x 36x     35x 35x 35x   35x   3x 1x   2x       19x  
import type { ClassType, InjectionToken } from './injection-token.mjs'
 
import { InjectableScope, InjectableType } from '../enums/index.mjs'
 
export type FactoryRecord<Instance = any, Schema = any> = {
  scope: InjectableScope
  originalToken: InjectionToken<Instance, Schema>
  target: ClassType
  type: InjectableType
  priority: number
}
 
export class Registry {
  private readonly factories = new Map<string, FactoryRecord[]>()
  private readonly highestPriority = new Map<string, FactoryRecord>()
 
  constructor(private readonly parent?: Registry) {}
 
  has(token: InjectionToken<any, any>): boolean {
    if (this.factories.has(token.id)) {
      return true
    }
    if (this.parent) {
      return this.parent.has(token)
    }
    return false
  }
 
  get<Instance, Schema>(
    token: InjectionToken<Instance, Schema>,
  ): FactoryRecord<Instance, Schema> {
    const factory = this.highestPriority.get(token.id)
    if (!factory) {
      if (this.parent) {
        return this.parent.get(token)
      }
      throw new Error(`[Registry] No factory found for ${token.toString()}`)
    }
    return factory
  }
 
  getAll<Instance, Schema>(
    token: InjectionToken<Instance, Schema>,
  ): FactoryRecord<Instance, Schema>[] {
    const records = this.factories.get(token.id)
    if (!records || records.length === 0) {
      if (this.parent) {
        return this.parent.getAll(token)
      }
      return []
    }
    // Return sorted by priority (highest first)
    return [...records].sort((a, b) => b.priority - a.priority)
  }
 
  set<Instance, Schema>(
    token: InjectionToken<Instance, Schema>,
    scope: InjectableScope,
    target: ClassType,
    type: InjectableType,
    priority: number = 0,
  ) {
    const record: FactoryRecord<Instance, Schema> = {
      scope,
      originalToken: token,
      target,
      type,
      priority,
    }
 
    // Add to factories array
    const existing = this.factories.get(token.id) || []
    existing.push(record)
    this.factories.set(token.id, existing)
 
    // Update highest priority cache if needed
    const currentHighest = this.highestPriority.get(token.id)
    if (!currentHighest || priority > currentHighest.priority) {
      this.highestPriority.set(token.id, record)
    }
  }
 
  delete(token: InjectionToken<any, any>) {
    const records = this.factories.get(token.id)
    if (records) {
      const deletedHighest = this.highestPriority.get(token.id)
      this.factories.delete(token.id)
      this.highestPriority.delete(token.id)
 
      // If we deleted the highest priority record, recalculate from remaining records
      if (deletedHighest && records.length > 1) {
        const remaining = records.filter(
          (r) =>
            r.originalToken.id !== deletedHighest.originalToken.id ||
            r.priority !== deletedHighest.priority,
        )
        Eif (remaining.length > 0) {
          const newHighest = remaining.reduce((max, current) =>
            current.priority > max.priority ? current : max,
          )
          this.highestPriority.set(token.id, newHighest)
          this.factories.set(token.id, remaining)
        }
      }
    }
  }
 
  /**
   * Updates the scope of an already registered factory.
   * This is useful when you need to dynamically change a service's scope
   * (e.g., when a singleton controller has request-scoped dependencies).
   *
   * @param token The injection token to update
   * @param scope The new scope to set
   * @returns true if the scope was updated, false if the token was not found
   */
  updateScope(
    token: InjectionToken<any, any>,
    scope: InjectableScope,
  ): boolean {
    const records = this.factories.get(token.id)
    if (records && records.length > 0) {
      // Update all records
      records.forEach((record) => {
        record.scope = scope
      })
      // Update highest priority cache if it exists
      const highest = this.highestPriority.get(token.id)
      Eif (highest) {
        highest.scope = scope
      }
      return true
    }
    if (this.parent) {
      return this.parent.updateScope(token, scope)
    }
    return false
  }
}
 
export const globalRegistry = /* #__PURE__ */ new Registry()