import type { InternalAxiosRequestConfig } from 'axios'; declare module 'axios' { interface AxiosRequestConfig { dedupe?: boolean; } } const WRITE_METHODS = new Set(['POST', 'PUT', 'DELETE', 'PATCH']); type DedupableConfig = Pick & { dedupe?: boolean; }; function isFormDataLike(value: unknown): boolean { if (typeof FormData !== 'undefined' && value instanceof FormData) return true; if (typeof Blob !== 'undefined' && value instanceof Blob) return true; return false; } function stableJson(value: unknown): string { if (value === null || value === undefined) return ''; if (typeof value !== 'object') return JSON.stringify(value); if (Array.isArray(value)) return `[${value.map(stableJson).join(',')}]`; const obj = value as Record; const keys = Object.keys(obj).sort(); return `{${keys.map(k => `${JSON.stringify(k)}:${stableJson(obj[k])}`).join(',')}}`; } export function computeDedupeKey(config: DedupableConfig): string | null { const method = (config.method ?? 'GET').toUpperCase(); if (!WRITE_METHODS.has(method)) return null; if (config.dedupe === false) return null; if (isFormDataLike(config.data)) return null; const url = config.url ?? ''; const paramsPart = stableJson(config.params); const bodyPart = stableJson(config.data); return `${method}|${url}?${paramsPart}|${bodyPart}`; } const DEFAULT_TTL_MS = 30_000; export interface WithDedupeOptions { ttlMs?: number; now?: () => number; } type AnyRequestFn = (...args: any[]) => Promise; export function withDedupe(request: TFn, options: WithDedupeOptions = {}): TFn { const ttl = options.ttlMs ?? DEFAULT_TTL_MS; const now = options.now ?? Date.now; const pending = new Map; expiresAt: number }>(); return new Proxy(request, { apply(target, thisArg, args: Parameters) { const [config] = args; const key = computeDedupeKey(config as DedupableConfig); if (key === null) return Reflect.apply(target, thisArg, args); const cached = pending.get(key); if (cached && cached.expiresAt > now()) return cached.promise; if (cached) pending.delete(key); const promise = Promise.resolve() .then(() => Reflect.apply(target, thisArg, args)) .finally(() => { const current = pending.get(key); if (current && current.promise === promise) { pending.delete(key); } }); pending.set(key, { promise, expiresAt: now() + ttl }); return promise; } }) as TFn; }