( endpoint: string, params: Record<string, string | number | string[] | undefined> )
| 64 | * Example: prices/AAPL_a1b2c3d4e5f6.json |
| 65 | */ |
| 66 | export function buildCacheKey( |
| 67 | endpoint: string, |
| 68 | params: Record<string, string | number | string[] | undefined> |
| 69 | ): string { |
| 70 | // Build a canonical string from sorted, non-empty params |
| 71 | const sortedParams = Object.entries(params) |
| 72 | .filter(([, v]) => v !== undefined && v !== null) |
| 73 | .sort(([a], [b]) => a.localeCompare(b)) |
| 74 | .map(([k, v]) => `${k}=${Array.isArray(v) ? [...v].sort().join(',') : v}`) |
| 75 | .join('&'); |
| 76 | |
| 77 | const raw = `${endpoint}?${sortedParams}`; |
| 78 | const hash = createHash('md5').update(raw).digest('hex').slice(0, 12); |
| 79 | |
| 80 | // Turn "/prices/" → "prices" |
| 81 | const cleanEndpoint = endpoint |
| 82 | .replace(/^\//, '') |
| 83 | .replace(/\/$/, '') |
| 84 | .replace(/\//g, '_'); |
| 85 | |
| 86 | // Prefix with ticker when available for human-readable filenames (optional) |
| 87 | const ticker = typeof params.ticker === 'string' ? params.ticker.toUpperCase() : null; |
| 88 | const prefix = ticker ? `${ticker}_` : ''; |
| 89 | |
| 90 | return `${cleanEndpoint}/${prefix}${hash}.json`; |
| 91 | } |
| 92 | |
| 93 | /** |
| 94 | * Validate that a parsed object has the shape of a CacheEntry. |
no test coverage detected