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>
82 lines
2.5 KiB
TypeScript
82 lines
2.5 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import {
|
|
clearFmpCacheForTests,
|
|
fetchFmpJson,
|
|
FMP_CACHE_TTL_MS,
|
|
FmpFetchError,
|
|
setFmpCacheNowForTests,
|
|
} from './src/services/fmpCache.js';
|
|
|
|
type MockResponse = {
|
|
ok: boolean;
|
|
status: number;
|
|
json: () => Promise<unknown>;
|
|
};
|
|
|
|
function createFetch(payloads: unknown[]) {
|
|
let calls = 0;
|
|
const fetchJson = async (): Promise<MockResponse> => {
|
|
const payload = payloads[calls] ?? payloads[payloads.length - 1];
|
|
calls += 1;
|
|
return {
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => payload,
|
|
};
|
|
};
|
|
|
|
return {
|
|
fetchJson,
|
|
get calls() {
|
|
return calls;
|
|
},
|
|
};
|
|
}
|
|
|
|
async function main() {
|
|
clearFmpCacheForTests();
|
|
let now = 1_000;
|
|
setFmpCacheNowForTests(() => now);
|
|
|
|
const url = 'https://financialmodelingprep.com/api/v3/profile/AAPL?apikey=test';
|
|
const otherUrl = 'https://financialmodelingprep.com/api/v3/profile/MSFT?apikey=test';
|
|
const fetcher = createFetch([{ symbol: 'AAPL' }, { symbol: 'AAPL-refresh' }]);
|
|
|
|
assert.deepEqual(await fetchFmpJson(url, fetcher.fetchJson), { symbol: 'AAPL' });
|
|
assert.deepEqual(await fetchFmpJson(url, fetcher.fetchJson), { symbol: 'AAPL' });
|
|
assert.equal(fetcher.calls, 1, 'same full URL should be served from cache before TTL expiry');
|
|
|
|
assert.deepEqual(await fetchFmpJson(otherUrl, fetcher.fetchJson), { symbol: 'AAPL-refresh' });
|
|
assert.equal(fetcher.calls, 2, 'different full URL should get its own cache entry');
|
|
|
|
now += FMP_CACHE_TTL_MS + 1;
|
|
assert.deepEqual(await fetchFmpJson(url, fetcher.fetchJson), { symbol: 'AAPL-refresh' });
|
|
assert.equal(fetcher.calls, 3, 'expired cache entry should be refetched');
|
|
|
|
clearFmpCacheForTests();
|
|
let failingCalls = 0;
|
|
const failingFetch = async (): Promise<MockResponse> => {
|
|
failingCalls += 1;
|
|
return {
|
|
ok: false,
|
|
status: 429,
|
|
json: async () => ({ error: 'rate limit' }),
|
|
};
|
|
};
|
|
|
|
await assert.rejects(
|
|
() => fetchFmpJson(url, failingFetch),
|
|
(error) => error instanceof FmpFetchError && error.status === 429,
|
|
);
|
|
await assert.rejects(() => fetchFmpJson(url, failingFetch));
|
|
assert.equal(failingCalls, 2, 'failed upstream responses should not be cached');
|
|
|
|
clearFmpCacheForTests();
|
|
console.log('FMP cache tests passed');
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(error);
|
|
process.exit(1);
|
|
});
|