All files / src/internal/core name-resolver.mts

76.66% Statements 46/60
67.5% Branches 27/40
77.77% Functions 7/9
76.27% Lines 45/59

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 186 187 188 189 190 191 192 193 194 195 196 197                  287x       287x       2122x 2122x   1288x 1288x   2122x       834x   834x             834x                       21x 21x 21x 532x 532x 532x   21x                   287x   287x                                             2122x 2122x     2122x             2122x     2122x 2122x 1288x       834x     834x 156x       834x 21x 21x       834x   834x                                       33x                 33x 33x   33x           33x 33x   32x       1x 1x     1x         1x                                
import type { InjectionTokenType } from '../../token/injection-token.mjs'
 
import { InjectableScope } from '../../enums/index.mjs'
 
/**
 * Simple LRU cache for instance name generation.
 * Uses a Map which maintains insertion order for efficient LRU eviction.
 */
class InstanceNameCache {
  private readonly cache = new Map<string, string>()
  private readonly maxSize: number
 
  constructor(maxSize = 1000) {
    this.maxSize = maxSize
  }
 
  get(key: string): string | undefined {
    const value = this.cache.get(key)
    if (value !== undefined) {
      // Move to end (most recently used)
      this.cache.delete(key)
      this.cache.set(key, value)
    }
    return value
  }
 
  set(key: string, value: string): void {
    Iif (this.cache.has(key)) {
      this.cache.delete(key)
    I} else if (this.cache.size >= this.maxSize) {
      // Remove least recently used (first item)
      const firstKey = this.cache.keys().next().value
      if (firstKey !== undefined) {
        this.cache.delete(firstKey)
      }
    }
    this.cache.set(key, value)
  }
 
  clear(): void {
    this.cache.clear()
  }
}
 
/**
 * Simple hash function for deterministic hashing of arguments
 */
function hashArgs(args: any): string {
  const str = JSON.stringify(args, Object.keys(args || {}).sort())
  let hash = 0
  for (let i = 0; i < str.length; i++) {
    const char = str.charCodeAt(i)
    hash = (hash << 5) - hash + char
    hash = hash & hash // Convert to 32-bit integer
  }
  return Math.abs(hash).toString(36)
}
 
/**
 * Handles instance name generation with support for requestId and scope.
 *
 * Generates unique instance identifiers based on token, arguments, and scope.
 * Request-scoped services MUST include requestId in their name for proper isolation.
 */
export class NameResolver {
  private readonly instanceNameCache = new InstanceNameCache()
 
  constructor(private readonly logger: Console | null = null) {}
 
  /**
   * Generates a unique instance name based on token, arguments, requestId, and scope.
   *
   * Name formats:
   * - Singleton/Transient without args: `${tokenId}`
   * - Singleton/Transient with args: `${tokenId}:${argsHash}`
   * - Request without args: `${tokenId}:requestId=${requestId}`
   * - Request with args: `${tokenId}:requestId=${requestId}:${argsHash}`
   *
   * @param token The injection token
   * @param args Optional arguments
   * @param requestId Optional request ID (required for request-scoped services)
   * @param scope Optional scope (used to determine if requestId should be included)
   * @returns The generated instance name
   */
  generateInstanceName(
    token: InjectionTokenType,
    args?: any,
    requestId?: string,
    scope?: InjectableScope,
  ): string {
    const tokenStr = token.toString()
    const isRequest = scope === InjectableScope.Request
 
    // For request-scoped services, requestId is required
    Iif (isRequest && !requestId) {
      throw new Error(
        `[NameResolver] requestId is required for request-scoped services`,
      )
    }
 
    // Build cache key
    const cacheKey = `${tokenStr}:${scope}:${requestId || ''}:${args ? JSON.stringify(args) : ''}`
 
    // Check cache first
    const cached = this.instanceNameCache.get(cacheKey)
    if (cached !== undefined) {
      return cached
    }
 
    // Generate the instance name
    let result = tokenStr
 
    // Add requestId for request-scoped services
    if (isRequest && requestId) {
      result = `${result}:requestId=${requestId}`
    }
 
    // Add args hash if args are provided
    if (args) {
      const argsHash = hashArgs(args)
      result = `${result}:${argsHash}`
    }
 
    // Cache the result
    this.instanceNameCache.set(cacheKey, result)
 
    return result
  }
 
  /**
   * Upgrades an existing instance name to include requestId.
   * Preserves any args hash that might already be in the name.
   *
   * Examples:
   * - `TokenName` → `TokenName:requestId=req-123`
   * - `TokenName:abc123` → `TokenName:requestId=req-123:abc123`
   *
   * @param existingName The existing instance name (without requestId)
   * @param requestId The request ID to add
   * @returns The upgraded instance name with requestId
   */
  upgradeInstanceNameToRequest(
    existingName: string,
    requestId: string,
  ): string {
    // Check if requestId is already in the name
    Iif (existingName.includes(`:requestId=${requestId}`)) {
      return existingName
    }
 
    // Find where to insert requestId
    // Format: TokenName or TokenName:argsHash
    // We want: TokenName:requestId=req-123 or TokenName:requestId=req-123:argsHash
 
    // Check if there's an args hash (starts after first colon, but not requestId=)
    const requestIdPattern = /:requestId=/
    const hasRequestId = requestIdPattern.test(existingName)
 
    Iif (hasRequestId) {
      // Already has a requestId, don't upgrade
      return existingName
    }
 
    // Find the token part (everything before first colon, or entire string if no colon)
    const colonIndex = existingName.indexOf(':')
    if (colonIndex === -1) {
      // No colon, just token name: TokenName → TokenName:requestId=req-123
      return `${existingName}:requestId=${requestId}`
    }
 
    // Has colon, means there's an args hash: TokenName:abc123 → TokenName:requestId=req-123:abc123
    const tokenPart = existingName.substring(0, colonIndex)
    const argsPart = existingName.substring(colonIndex + 1)
 
    // Check if argsPart looks like an args hash (not requestId=)
    Iif (argsPart.startsWith('requestId=')) {
      // Already has requestId, return as is
      return existingName
    }
 
    return `${tokenPart}:requestId=${requestId}:${argsPart}`
  }
 
  /**
   * Formats a single argument value for instance name generation.
   */
  formatArgValue(value: any): string {
    if (typeof value === 'function') {
      return `fn_${value.name}(${value.length})`
    }
    if (typeof value === 'symbol') {
      return value.toString()
    }
    return JSON.stringify(value).slice(0, 40)
  }
}