All files / src/utils fetchDecorator.ts

100% Statements 49/49
94.11% Branches 16/17
100% Functions 3/3
100% Lines 49/49

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 1151x                     1x 18x 18x 18x 1518x 1518x 1518x 1518x 18x 18x   1x                                       1x 1x 18x 18x 18x 18x 18x 18x 18x 18x 18x   18x 18x 18x   1x                                                     1x 1x 7x 7x   7x   7x   7x 1x 1x   7x   7x 2x 2x 2x   7x 1x 1x 1x   2x 2x 2x 2x  
/**
 * Generates a simple non-cryptographic hash from a string using the djb2 algorithm.
 * Produces a hexadecimal string suitable for use as a cache key or request identifier.
 *
 * @remarks
 * This is intentionally a fast, non-cryptographic hash. It is not suitable for
 * security-sensitive use cases but works well for deduplicating HTTP requests.
 *
 * @param str - The input string to hash
 * @returns A hexadecimal string representation of the hash
 * @source
 */
function generateSimpleHash(str: string): string {
  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 32bit integer
  }
  return Math.abs(hash).toString(16);
}
 
/**
 * Generates a deterministic hash for an HTTP request based on its URL, method,
 * headers, body, and content type. Two requests with identical parameters will
 * always produce the same hash, making this suitable for cache keying and
 * request deduplication.
 *
 * @category Utils
 * @param url - The request URL
 * @param options - The request options (method, headers, body, etc.)
 * @returns A hexadecimal hash string uniquely identifying the request
 *
 * @example
 * ```typescript
 * const hash = await generateRequestHash('https://api.example.com/data', {
 *   method: 'POST',
 *   headers: { 'content-type': 'application/json' },
 *   body: JSON.stringify({ query: 'borohydride' }),
 * });
 * ```
 * @source
 */
export async function generateRequestHash(url: string, options: RequestInit): Promise<string> {
  const data = {
    url,
    method: options.method || 'GET',
    headers: options.headers || {},
    body: options.body || '',
    // headers may be a Headers instance or string[][]; only plain-object headers
    // expose content-type by index, matching the previous behavior.
    contentType: (options.headers as Record<string, string> | undefined)?.['content-type'] || '',
  };
 
  const dataString = JSON.stringify(data);
  return generateSimpleHash(dataString);
}
 
/**
 * A lightweight fetch wrapper that automatically parses response bodies based on
 * their `Content-Type` header and attaches a deterministic request hash to each
 * response. Supports JSON, text, and binary (blob) content types.
 *
 * @category Utils
 * @remarks
 * This is the utility-layer fetch decorator used in `src/utils/`. For the
 * primary application fetch decorator with LRU caching, response aggregation,
 * and richer error handling, see the `fetchDecorator` in `src/helpers/fetch.ts`.
 *
 * @param url - The URL to fetch
 * @param options - Standard `RequestInit` options forwarded to `fetch()`
 * @returns An object spreading the original response with `data` (parsed body)
 *          and `requestHash` (hex hash of the request parameters)
 * @throws If the response status is not OK (non-2xx)
 *
 * @example
 * ```typescript
 * const result = await fetchDecorator('https://api.example.com/compounds', {
 *   method: 'GET',
 *   headers: { Accept: 'application/json' },
 * });
 * console.log(result.data);        // parsed JSON body
 * console.log(result.requestHash); // e.g. "a1b2c3d4"
 * ```
 * @source
 */
export async function fetchDecorator(
  url: string,
  options: RequestInit = {},
): Promise<Response & { data: unknown; requestHash: string }> {
  const requestHash = await generateRequestHash(url, options);
 
  const response = await fetch(url, options);
 
  if (!response.ok) {
    throw new Error(`HTTP Error: ${response.status} ${response.statusText}`);
  }
 
  const contentType = response.headers.get('content-type') || '';
 
  if (contentType.includes('application/json')) {
    const data = await response.json();
    return { ...response, data, requestHash };
  }
 
  if (contentType.includes('text/')) {
    const data = await response.text();
    return { ...response, data, requestHash };
  }
 
  // For binary data (images, files, etc)
  const data = await response.blob();
  return { ...response, data, requestHash };
}