/**
* Request deduplication
*
* Prevents concurrent duplicate requests by returning the same promise
* for identical in-flight requests. Useful for expensive operations
* like authentication or data fetching.
*/
import { createLogger } from './logger.js';
const logger = createLogger('dedup');
/**
* In-flight request tracker
*/
const inFlight = new Map<string, Promise<unknown>>();
/**
* Execute a function with deduplication
*
* If a request with the same key is already in flight, returns the
* existing promise instead of starting a new request.
*
* @example
* ```typescript
* // Only one API call will be made even if called 10 times concurrently
* const results = await Promise.all(
* Array(10).fill(null).map(() =>
* dedupe('user:123', () => fetchUser(123))
* )
* );
* ```
*/
export async function dedupe<T>(
key: string,
fn: () => Promise<T>
): Promise<T> {
// Check if request is already in flight
const existing = inFlight.get(key);
if (existing) {
logger.debug('Deduplicating request', { key });
return existing as Promise<T>;
}
// Start new request
const promise = fn()
.then((result) => {
inFlight.delete(key);
return result;
})
.catch((error) => {
inFlight.delete(key);
throw error;
});
inFlight.set(key, promise);
logger.debug('Started deduplicated request', { key });
return promise;
}
/**
* Create a deduplication wrapper for a function
*
* @example
* ```typescript
* const fetchUserDeduped = createDedupedFn(
* (userId: string) => `user:${userId}`,
* fetchUser
* );
*
* // Multiple calls with same userId share one request
* const user = await fetchUserDeduped('123');
* ```
*/
export function createDedupedFn<TArgs extends unknown[], TResult>(
keyFn: (...args: TArgs) => string,
fn: (...args: TArgs) => Promise<TResult>
): (...args: TArgs) => Promise<TResult> {
return (...args: TArgs) => dedupe(keyFn(...args), () => fn(...args));
}
/**
* Check if a request is currently in flight
*/
export function isInFlight(key: string): boolean {
return inFlight.has(key);
}
/**
* Get count of in-flight requests
*/
export function getInFlightCount(): number {
return inFlight.size;
}
/**
* Clear all in-flight tracking (for testing)
*/
export function clearInFlight(): void {
inFlight.clear();
}
/**
* Time-based deduplication with TTL
*
* Caches results for a specified duration, returning cached values
* for subsequent calls within the TTL window.
*/
const timedCache = new Map<string, { value: unknown; expiresAt: number }>();
/**
* Execute with time-based deduplication
*
* @example
* ```typescript
* // Cache result for 5 seconds
* const user = await dedupeWithTtl('user:123', 5000, () => fetchUser(123));
* ```
*/
export async function dedupeWithTtl<T>(
key: string,
ttlMs: number,
fn: () => Promise<T>
): Promise<T> {
const now = Date.now();
const cached = timedCache.get(key);
// Return cached value if still valid
if (cached && cached.expiresAt > now) {
logger.debug('Returning cached value', { key, remainingMs: cached.expiresAt - now });
return cached.value as T;
}
// Check for in-flight request
const existing = inFlight.get(key);
if (existing) {
logger.debug('Deduplicating request (TTL)', { key });
return existing as Promise<T>;
}
// Start new request
const promise = fn()
.then((result) => {
inFlight.delete(key);
timedCache.set(key, { value: result, expiresAt: now + ttlMs });
return result;
})
.catch((error) => {
inFlight.delete(key);
throw error;
});
inFlight.set(key, promise);
return promise;
}
/**
* Invalidate a cached entry
*/
export function invalidateCache(key: string): void {
timedCache.delete(key);
}
/**
* Clear all timed cache entries (for testing)
*/
export function clearTimedCache(): void {
timedCache.clear();
}