Represent market scan loading with RemoteData
Market scan state was still tracked as separate nullable payload and status strings. This moves the hook internals to RemoteData<MarketScan> so loading and error paths can preserve previous data while keeping the existing marketScan and marketStatus return values for current card consumers. Constraint: Preserve the existing useCityMarketScan public compatibility fields. Rejected: Update all card UI to consume marketRemote immediately | keeping the compatibility bridge avoids a broad rendering diff while the request state model lands. Confidence: high Scope-risk: narrow Reversibility: clean Tested: npm run build Not-tested: Forced live market-scan failure with stale previous quote.
This commit is contained in:
@@ -8,6 +8,10 @@ import type {
|
|||||||
import {
|
import {
|
||||||
scanTerminalClient,
|
scanTerminalClient,
|
||||||
type AiCityStreamProgress,
|
type AiCityStreamProgress,
|
||||||
|
type RemoteData,
|
||||||
|
toRemoteError,
|
||||||
|
toRemoteLoading,
|
||||||
|
toRemoteSuccess,
|
||||||
} from "@/components/dashboard/scan-terminal/scan-terminal-client";
|
} from "@/components/dashboard/scan-terminal/scan-terminal-client";
|
||||||
import {
|
import {
|
||||||
buildStorageKey,
|
buildStorageKey,
|
||||||
@@ -22,6 +26,7 @@ const AI_CITY_FORECAST_CACHE_PREFIX = "polyWeather_aiCityForecast_v6";
|
|||||||
const AI_CITY_FORECAST_CACHE_TTL_MS = 60 * 60 * 1000;
|
const AI_CITY_FORECAST_CACHE_TTL_MS = 60 * 60 * 1000;
|
||||||
const CITY_MARKET_SCAN_CACHE_PREFIX = "polyWeather_cityMarketScan_v3";
|
const CITY_MARKET_SCAN_CACHE_PREFIX = "polyWeather_cityMarketScan_v3";
|
||||||
const CITY_MARKET_SCAN_CACHE_TTL_MS = 10 * 60 * 1000;
|
const CITY_MARKET_SCAN_CACHE_TTL_MS = 10 * 60 * 1000;
|
||||||
|
type CityMarketScanStatus = "idle" | "loading" | "ready" | "failed";
|
||||||
const aiCityForecastStateCache = new Map<
|
const aiCityForecastStateCache = new Map<
|
||||||
string,
|
string,
|
||||||
{ state: AiCityForecastState; updatedAt: number }
|
{ state: AiCityForecastState; updatedAt: number }
|
||||||
@@ -349,17 +354,13 @@ export function useCityMarketScan({
|
|||||||
detailCityName: string;
|
detailCityName: string;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const [marketScan, setMarketScan] = useState<MarketScan | null>(
|
const [marketRemote, setMarketRemote] = useState<RemoteData<MarketScan>>(
|
||||||
detail?.market_scan || null,
|
detail?.market_scan ? toRemoteSuccess(detail.market_scan) : { status: "idle" },
|
||||||
);
|
);
|
||||||
const [marketStatus, setMarketStatus] = useState<
|
|
||||||
"idle" | "loading" | "ready" | "failed"
|
|
||||||
>(detail?.market_scan ? "ready" : "idle");
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!detail) {
|
if (!detail) {
|
||||||
setMarketScan(null);
|
setMarketRemote({ status: "idle" });
|
||||||
setMarketStatus("idle");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const cacheKey = buildStorageKey(CITY_MARKET_SCAN_CACHE_PREFIX, [
|
const cacheKey = buildStorageKey(CITY_MARKET_SCAN_CACHE_PREFIX, [
|
||||||
@@ -369,8 +370,7 @@ export function useCityMarketScan({
|
|||||||
]);
|
]);
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
if (detail.market_scan) {
|
if (detail.market_scan) {
|
||||||
setMarketScan(detail.market_scan);
|
setMarketRemote(toRemoteSuccess(detail.market_scan));
|
||||||
setMarketStatus("ready");
|
|
||||||
writeCachedPayload(cacheKey, detail.market_scan);
|
writeCachedPayload(cacheKey, detail.market_scan);
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
@@ -382,11 +382,9 @@ export function useCityMarketScan({
|
|||||||
CITY_MARKET_SCAN_CACHE_TTL_MS,
|
CITY_MARKET_SCAN_CACHE_TTL_MS,
|
||||||
);
|
);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
setMarketScan(cached);
|
setMarketRemote(toRemoteSuccess(cached));
|
||||||
setMarketStatus("ready");
|
|
||||||
} else {
|
} else {
|
||||||
setMarketScan(null);
|
setMarketRemote({ status: "idle" });
|
||||||
setMarketStatus("idle");
|
|
||||||
}
|
}
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
@@ -397,13 +395,12 @@ export function useCityMarketScan({
|
|||||||
CITY_MARKET_SCAN_CACHE_TTL_MS,
|
CITY_MARKET_SCAN_CACHE_TTL_MS,
|
||||||
);
|
);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
setMarketScan(cached);
|
setMarketRemote(toRemoteSuccess(cached));
|
||||||
setMarketStatus("ready");
|
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
setMarketStatus("loading");
|
setMarketRemote((current) => toRemoteLoading(current));
|
||||||
}
|
}
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
void scanTerminalClient.getMarketScan(detailCityName, {
|
void scanTerminalClient.getMarketScan(detailCityName, {
|
||||||
@@ -416,13 +413,20 @@ export function useCityMarketScan({
|
|||||||
if (payload) {
|
if (payload) {
|
||||||
writeCachedPayload(cacheKey, payload);
|
writeCachedPayload(cacheKey, payload);
|
||||||
}
|
}
|
||||||
setMarketScan(payload || detail.market_scan || null);
|
const nextPayload = payload || detail.market_scan || null;
|
||||||
setMarketStatus("ready");
|
if (nextPayload) {
|
||||||
|
setMarketRemote(toRemoteSuccess(nextPayload));
|
||||||
|
} else {
|
||||||
|
setMarketRemote({ status: "idle" });
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch((error) => {
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
setMarketScan(detail.market_scan || null);
|
if (detail.market_scan) {
|
||||||
setMarketStatus(detail.market_scan ? "ready" : "failed");
|
setMarketRemote(toRemoteSuccess(detail.market_scan));
|
||||||
|
} else {
|
||||||
|
setMarketRemote((current) => toRemoteError(error, current));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
@@ -430,5 +434,24 @@ export function useCityMarketScan({
|
|||||||
};
|
};
|
||||||
}, [detail, detailCityName, enabled]);
|
}, [detail, detailCityName, enabled]);
|
||||||
|
|
||||||
return { marketScan, marketStatus };
|
const previousMarketScan =
|
||||||
|
marketRemote.status === "loading" || marketRemote.status === "error"
|
||||||
|
? marketRemote.previous ?? null
|
||||||
|
: null;
|
||||||
|
const marketScan =
|
||||||
|
marketRemote.status === "success"
|
||||||
|
? marketRemote.data
|
||||||
|
: previousMarketScan ?? detail?.market_scan ?? null;
|
||||||
|
const marketStatus: CityMarketScanStatus =
|
||||||
|
marketRemote.status === "success"
|
||||||
|
? "ready"
|
||||||
|
: marketRemote.status === "loading"
|
||||||
|
? "loading"
|
||||||
|
: marketRemote.status === "error"
|
||||||
|
? marketScan
|
||||||
|
? "ready"
|
||||||
|
: "failed"
|
||||||
|
: "idle";
|
||||||
|
|
||||||
|
return { marketRemote, marketScan, marketStatus };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user