{"version":3,"file":"request-deduplication.mjs","sources":["../../../../src/lib/api/advanced/request-deduplication.ts"],"sourcesContent":["/**\n * Request Deduplication\n *\n * Prevents duplicate concurrent API requests by caching in-flight\n * requests and returning the same promise for identical requests.\n *\n * @module api/advanced/request-deduplication\n */\n\n// =============================================================================\n// Types\n// =============================================================================\n\n/**\n * Deduplication configuration.\n */\nexport interface DeduplicationConfig {\n  /** How to generate cache keys */\n  keyGenerator?: (request: DeduplicationRequest) => string;\n  /** Time-to-live for cached promises (ms) */\n  ttl?: number;\n  /** Maximum number of cached entries */\n  maxSize?: number;\n  /** Whether to dedupe mutations (POST/PUT/PATCH/DELETE) */\n  dedupeMutations?: boolean;\n  /** Enable debug logging */\n  debug?: boolean;\n}\n\n/**\n * Request to potentially deduplicate.\n */\nexport interface DeduplicationRequest {\n  /** Request URL */\n  url: string;\n  /** HTTP method */\n  method: string;\n  /** Request body */\n  body?: unknown;\n  /** Request headers */\n  headers?: Record<string, string>;\n}\n\n/**\n * Cached request entry.\n */\ninterface CachedEntry<T> {\n  /** The promise for this request */\n  promise: Promise<T>;\n  /** When this entry was created */\n  createdAt: number;\n  /** Number of subscribers */\n  subscribers: number;\n  /** Request key */\n  key: string;\n}\n\n/**\n * Deduplication statistics.\n */\nexport interface DeduplicationStats {\n  /** Total requests processed */\n  totalRequests: number;\n  /** Requests that were deduplicated */\n  deduplicatedRequests: number;\n  /** Current cache size */\n  cacheSize: number;\n  /** Cache hit rate */\n  hitRate: number;\n  /** Average subscribers per deduplicated request */\n  avgSubscribers: number;\n}\n\n// =============================================================================\n// Request Deduplicator Class\n// =============================================================================\n\n/**\n * Request Deduplicator for preventing duplicate concurrent requests.\n *\n * @example\n * ```typescript\n * const dedup = new RequestDeduplicator({\n *   ttl: 5000,\n *   maxSize: 100,\n * });\n *\n * // Multiple calls with same request will share one actual fetch\n * const [result1, result2, result3] = await Promise.all([\n *   dedup.dedupe({ url: '/users', method: 'GET' }, () => fetch('/api/users')),\n *   dedup.dedupe({ url: '/users', method: 'GET' }, () => fetch('/api/users')),\n *   dedup.dedupe({ url: '/users', method: 'GET' }, () => fetch('/api/users')),\n * ]);\n * // Only one fetch was actually made!\n * ```\n */\nexport class RequestDeduplicator {\n  private config: Required<DeduplicationConfig>;\n  private cache: Map<string, CachedEntry<unknown>>;\n  private stats: {\n    totalRequests: number;\n    deduplicatedRequests: number;\n    totalSubscribers: number;\n  };\n\n  /**\n   * Create a new request deduplicator.\n   *\n   * @param config - Deduplication configuration\n   */\n  constructor(config: DeduplicationConfig = {}) {\n    this.config = {\n      keyGenerator: this.defaultKeyGenerator.bind(this),\n      ttl: 5000,\n      maxSize: 1000,\n      dedupeMutations: false,\n      debug: false,\n      ...config,\n    };\n\n    this.cache = new Map();\n    this.stats = {\n      totalRequests: 0,\n      deduplicatedRequests: 0,\n      totalSubscribers: 0,\n    };\n  }\n\n  // ===========================================================================\n  // Main Methods\n  // ===========================================================================\n\n  /**\n   * Deduplicate a request.\n   *\n   * @param request - Request to potentially deduplicate\n   * @param executor - Function to execute if not deduplicated\n   * @returns Promise resolving to the result\n   */\n  async dedupe<T>(request: DeduplicationRequest, executor: () => Promise<T>): Promise<T> {\n    this.stats.totalRequests++;\n\n    // Check if we should dedupe this request\n    if (!this.shouldDedupe(request)) {\n      return executor();\n    }\n\n    const key = this.config.keyGenerator(request);\n\n    // Check for existing in-flight request\n    const existing = this.cache.get(key) as CachedEntry<T> | undefined;\n\n    if (existing && this.isValid(existing)) {\n      this.stats.deduplicatedRequests++;\n      this.stats.totalSubscribers++;\n      existing.subscribers++;\n      this.log(`Deduped request: ${key} (${existing.subscribers} subscribers)`);\n      return existing.promise;\n    }\n\n    // Create new entry\n    const promise = this.executeAndCleanup<T>(key, executor);\n\n    const entry: CachedEntry<T> = {\n      promise,\n      createdAt: Date.now(),\n      subscribers: 1,\n      key,\n    };\n\n    // Enforce max size\n    this.enforceMaxSize();\n\n    this.cache.set(key, entry as CachedEntry<unknown>);\n    this.log(`New request: ${key}`);\n\n    return promise;\n  }\n\n  /**\n   * Check if a request is currently in flight.\n   *\n   * @param request - Request to check\n   * @returns Whether request is in flight\n   */\n  isInFlight(request: DeduplicationRequest): boolean {\n    const key = this.config.keyGenerator(request);\n    const entry = this.cache.get(key);\n    return entry !== undefined && this.isValid(entry);\n  }\n\n  /**\n   * Cancel a pending request.\n   *\n   * @param request - Request to cancel\n   * @returns Whether request was found and cancelled\n   */\n  cancel(request: DeduplicationRequest): boolean {\n    const key = this.config.keyGenerator(request);\n    return this.cache.delete(key);\n  }\n\n  /**\n   * Clear all cached requests.\n   */\n  clear(): void {\n    this.cache.clear();\n    this.log('Cache cleared');\n  }\n\n  /**\n   * Get deduplication statistics.\n   */\n  getStats(): DeduplicationStats {\n    return {\n      totalRequests: this.stats.totalRequests,\n      deduplicatedRequests: this.stats.deduplicatedRequests,\n      cacheSize: this.cache.size,\n      hitRate:\n        this.stats.totalRequests > 0\n          ? this.stats.deduplicatedRequests / this.stats.totalRequests\n          : 0,\n      avgSubscribers:\n        this.stats.deduplicatedRequests > 0\n          ? this.stats.totalSubscribers / this.stats.deduplicatedRequests\n          : 0,\n    };\n  }\n\n  /**\n   * Reset statistics.\n   */\n  resetStats(): void {\n    this.stats = {\n      totalRequests: 0,\n      deduplicatedRequests: 0,\n      totalSubscribers: 0,\n    };\n  }\n\n  // ===========================================================================\n  // Private Methods\n  // ===========================================================================\n\n  /**\n   * Check if request should be deduplicated.\n   */\n  private shouldDedupe(request: DeduplicationRequest): boolean {\n    const method = request.method.toUpperCase();\n\n    // Always dedupe GET and HEAD\n    if (method === 'GET' || method === 'HEAD') {\n      return true;\n    }\n\n    // Optionally dedupe mutations\n    return this.config.dedupeMutations;\n  }\n\n  /**\n   * Execute request and clean up cache.\n   */\n  private async executeAndCleanup<T>(key: string, executor: () => Promise<T>): Promise<T> {\n    try {\n\n      return await executor();\n    } finally {\n      // Remove from cache after completion\n      // Use setTimeout to allow other subscribers to get the result\n      setTimeout(() => {\n        this.cache.delete(key);\n        this.log(`Removed from cache: ${key}`);\n      }, 0);\n    }\n  }\n\n  /**\n   * Check if cache entry is still valid.\n   */\n  private isValid(entry: CachedEntry<unknown>): boolean {\n    return Date.now() - entry.createdAt < this.config.ttl;\n  }\n\n  /**\n   * Enforce maximum cache size.\n   */\n  private enforceMaxSize(): void {\n    if (this.cache.size >= this.config.maxSize) {\n      // Remove oldest entries\n      const entriesToRemove = Math.ceil(this.config.maxSize * 0.2); // Remove 20%\n      const entries = Array.from(this.cache.entries())\n        .sort((a, b) => a[1].createdAt - b[1].createdAt)\n        .slice(0, entriesToRemove);\n\n      for (const [key] of entries) {\n        this.cache.delete(key);\n      }\n\n      this.log(`Evicted ${entriesToRemove} entries`);\n    }\n  }\n\n  /**\n   * Default key generator.\n   */\n  private defaultKeyGenerator(request: DeduplicationRequest): string {\n    const { url, method, body } = request;\n    const bodyHash = body != null ? this.hashBody(body) : '';\n    return `${method.toUpperCase()}:${url}:${bodyHash}`;\n  }\n\n  /**\n   * Hash request body for cache key.\n   */\n  private hashBody(body: unknown): string {\n    try {\n      const str = JSON.stringify(body);\n      // Simple hash function\n      let hash = 0;\n      for (let i = 0; i < str.length; i++) {\n        const char = str.charCodeAt(i);\n        hash = (hash << 5) - hash + char;\n        hash = hash & hash;\n      }\n      return hash.toString(36);\n    } catch {\n      return '';\n    }\n  }\n\n  /**\n   * Log debug message.\n   */\n  private log(message: string): void {\n    if (this.config.debug) {\n      // eslint-disable-next-line no-console\n      console.log(`[RequestDeduplicator] ${message}`);\n    }\n  }\n}\n\n// =============================================================================\n// Factory Function\n// =============================================================================\n\n/**\n * Create a new request deduplicator.\n *\n * @param config - Deduplication configuration\n * @returns RequestDeduplicator instance\n */\nexport function createRequestDeduplicator(config?: DeduplicationConfig): RequestDeduplicator {\n  return new RequestDeduplicator(config);\n}\n\n// =============================================================================\n// Decorator / Wrapper\n// =============================================================================\n\n/**\n * Create a deduplicated fetch wrapper.\n *\n * @param config - Deduplication configuration\n * @returns Wrapped fetch function\n */\nexport function createDeduplicatedFetch(config?: DeduplicationConfig): typeof fetch {\n  const dedup = new RequestDeduplicator(config);\n\n  return async (input: RequestInfo | URL, init?: RequestInit) => {\n    let url: string;\n    if (typeof input === 'string') {\n      url = input;\n    } else if (input instanceof URL) {\n      url = input.toString();\n    } else {\n      ({ url } = input);\n    }\n\n    const method = init?.method ?? 'GET';\n\n    return dedup.dedupe(\n      { url, method, body: init?.body, headers: init?.headers as Record<string, string> },\n      async () => fetch(input, init)\n    );\n  };\n}\n\n/**\n * Higher-order function to deduplicate any async function.\n *\n * @param fn - Function to deduplicate\n * @param keyGenerator - Function to generate cache key from arguments\n * @param _config\n * @returns Deduplicated function\n */\nexport function deduplicateFunction<TArgs extends unknown[], TResult>(\n  fn: (...args: TArgs) => Promise<TResult>,\n  keyGenerator: (...args: TArgs) => string,\n\n  _config?: Omit<DeduplicationConfig, 'keyGenerator'>\n): (...args: TArgs) => Promise<TResult> {\n  const cache = new Map<string, Promise<TResult>>();\n\n  return async (...args: TArgs): Promise<TResult> => {\n    const key = keyGenerator(...args);\n\n    // Check existing\n    const existing = cache.get(key);\n    if (existing) {\n      return existing;\n    }\n\n    // Create new\n    const promise = fn(...args).finally(() => {\n      cache.delete(key);\n    });\n\n    cache.set(key, promise);\n    return promise;\n  };\n}\n\n// =============================================================================\n// React Hook Integration\n// =============================================================================\n\n/**\n * Request deduplication for React Query or SWR integration.\n */\nexport class ReactQueryDeduplicator extends RequestDeduplicator {\n  /**\n   * Create a query function with deduplication.\n   */\n  createQueryFn<T>(queryKey: unknown[], fetcher: () => Promise<T>): () => Promise<T> {\n    const key = JSON.stringify(queryKey);\n\n    return async () => this.dedupe({ url: key, method: 'GET' }, fetcher);\n  }\n}\n\n/**\n * Create a React Query compatible deduplicator.\n */\nexport function createReactQueryDeduplicator(config?: DeduplicationConfig): ReactQueryDeduplicator {\n  return new ReactQueryDeduplicator(config);\n}\n"],"names":["RequestDeduplicator","config","request","executor","key","existing","promise","entry","method","entriesToRemove","entries","a","b","url","body","bodyHash","str","hash","i","char","message","createRequestDeduplicator","createDeduplicatedFetch","dedup","input","init","deduplicateFunction","fn","keyGenerator","_config","cache","args","ReactQueryDeduplicator","queryKey","fetcher","createReactQueryDeduplicator"],"mappings":"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"}