Add a 30-minute in-memory cache keyed by the complete Financial Modeling Prep upstream URL so repeated profile, metrics, earnings, and screener requests do not burn the free-tier quota. The cache keeps non-OK responses out of storage so transient rate-limit or provider errors can recover on the next request. Refs: docs/AUDIT_REDESIGN.md item C2. Co-Authored-By: GPT-5 Codex <noreply@openai.com>
73 lines
1.7 KiB
TypeScript
73 lines
1.7 KiB
TypeScript
export const FMP_CACHE_TTL_MS = 30 * 60 * 1000;
|
|
|
|
export class FmpFetchError extends Error {
|
|
constructor(public readonly status: number) {
|
|
super(`FMP fetch failed with status ${status}`);
|
|
this.name = 'FmpFetchError';
|
|
}
|
|
}
|
|
|
|
type FetchJson = (url: string) => Promise<{
|
|
ok: boolean;
|
|
status: number;
|
|
json: () => Promise<unknown>;
|
|
}>;
|
|
|
|
interface FmpCacheEntry {
|
|
expiresAt: number;
|
|
data: unknown;
|
|
}
|
|
|
|
const fmpCache = new Map<string, FmpCacheEntry>();
|
|
const inFlightFmpRequests = new Map<string, Promise<unknown>>();
|
|
|
|
let now = () => Date.now();
|
|
|
|
export async function fetchFmpJson(url: string, fetchJson: FetchJson = fetch): Promise<unknown> {
|
|
const cached = fmpCache.get(url);
|
|
const currentTime = now();
|
|
|
|
if (cached && cached.expiresAt > currentTime) {
|
|
return cached.data;
|
|
}
|
|
|
|
if (cached) {
|
|
fmpCache.delete(url);
|
|
}
|
|
|
|
const inFlight = inFlightFmpRequests.get(url);
|
|
if (inFlight) {
|
|
return inFlight;
|
|
}
|
|
|
|
const request = fetchJson(url)
|
|
.then(async (response) => {
|
|
if (!response.ok) {
|
|
throw new FmpFetchError(response.status);
|
|
}
|
|
|
|
const data = await response.json();
|
|
fmpCache.set(url, {
|
|
data,
|
|
expiresAt: now() + FMP_CACHE_TTL_MS,
|
|
});
|
|
return data;
|
|
})
|
|
.finally(() => {
|
|
inFlightFmpRequests.delete(url);
|
|
});
|
|
|
|
inFlightFmpRequests.set(url, request);
|
|
return request;
|
|
}
|
|
|
|
export function clearFmpCacheForTests() {
|
|
fmpCache.clear();
|
|
inFlightFmpRequests.clear();
|
|
now = () => Date.now();
|
|
}
|
|
|
|
export function setFmpCacheNowForTests(nowProvider: () => number) {
|
|
now = nowProvider;
|
|
}
|