Stop substituting synthetic market data when providers fail
- Replace synthetic fallback in getQuote()/getPriceHistory() with null returns
- Add QuoteResult/PriceHistoryResult types with { value, stale } structure
- Implement stale-while-revalidate: return cached value with stale=true on live fetch failure
- Cache failures for 30s to avoid hammering provider
- Update CompanyAnalysis type to use PriceData<T> wrapper
- Update task-processors to track failed/stale tickers explicitly
- Update price-history-card UI to show unavailable state and stale indicator
- Add comprehensive tests for failure cases
- Add e2e tests for null data, stale data, and live data scenarios
Resolves #14
This commit is contained in:
@@ -892,6 +892,8 @@ async function processRefreshPrices(task: Task) {
|
||||
const userHoldings = await listHoldingsForPriceRefresh(userId);
|
||||
const tickers = [...new Set(userHoldings.map((entry) => entry.ticker))];
|
||||
const quotes = new Map<string, number>();
|
||||
const failedTickers: string[] = [];
|
||||
const staleTickers: string[] = [];
|
||||
const baseContext = {
|
||||
counters: {
|
||||
holdings: userHoldings.length
|
||||
@@ -920,8 +922,15 @@ async function processRefreshPrices(task: Task) {
|
||||
);
|
||||
for (let index = 0; index < tickers.length; index += 1) {
|
||||
const ticker = tickers[index];
|
||||
const quote = await getQuote(ticker);
|
||||
quotes.set(ticker, quote);
|
||||
const quoteResult = await getQuote(ticker);
|
||||
if (quoteResult.value !== null) {
|
||||
quotes.set(ticker, quoteResult.value);
|
||||
if (quoteResult.stale) {
|
||||
staleTickers.push(ticker);
|
||||
}
|
||||
} else {
|
||||
failedTickers.push(ticker);
|
||||
}
|
||||
await setProjectionStage(
|
||||
task,
|
||||
'refresh.fetch_quotes',
|
||||
@@ -931,7 +940,9 @@ async function processRefreshPrices(task: Task) {
|
||||
total: tickers.length,
|
||||
unit: 'tickers',
|
||||
counters: {
|
||||
holdings: userHoldings.length
|
||||
holdings: userHoldings.length,
|
||||
failed: failedTickers.length,
|
||||
stale: staleTickers.length
|
||||
},
|
||||
subject: { ticker }
|
||||
})
|
||||
@@ -941,10 +952,12 @@ async function processRefreshPrices(task: Task) {
|
||||
await setProjectionStage(
|
||||
task,
|
||||
'refresh.persist_prices',
|
||||
`Writing refreshed prices for ${tickers.length} tickers across ${userHoldings.length} holdings`,
|
||||
`Writing refreshed prices for ${quotes.size} tickers across ${userHoldings.length} holdings`,
|
||||
{
|
||||
counters: {
|
||||
holdings: userHoldings.length
|
||||
holdings: userHoldings.length,
|
||||
failed: failedTickers.length,
|
||||
stale: staleTickers.length
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -952,12 +965,22 @@ async function processRefreshPrices(task: Task) {
|
||||
|
||||
const result = {
|
||||
updatedCount,
|
||||
totalTickers: tickers.length
|
||||
totalTickers: tickers.length,
|
||||
failedTickers,
|
||||
staleTickers
|
||||
};
|
||||
|
||||
const messageParts = [`Refreshed prices for ${quotes.size}/${tickers.length} tickers`];
|
||||
if (failedTickers.length > 0) {
|
||||
messageParts.push(`(${failedTickers.length} unavailable)`);
|
||||
}
|
||||
if (staleTickers.length > 0) {
|
||||
messageParts.push(`(${staleTickers.length} stale)`);
|
||||
}
|
||||
|
||||
return buildTaskOutcome(
|
||||
result,
|
||||
`Refreshed prices for ${tickers.length} tickers across ${userHoldings.length} holdings.`,
|
||||
`${messageParts.join(' ')} across ${userHoldings.length} holdings.`,
|
||||
{
|
||||
progress: {
|
||||
current: tickers.length,
|
||||
@@ -966,7 +989,9 @@ async function processRefreshPrices(task: Task) {
|
||||
},
|
||||
counters: {
|
||||
holdings: userHoldings.length,
|
||||
updatedCount
|
||||
updatedCount,
|
||||
failed: failedTickers.length,
|
||||
stale: staleTickers.length
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user