Add WeatherNext2 worker and prevent empty scan cache
This commit is contained in:
@@ -30,6 +30,7 @@ export function runTests() {
|
||||
model_cluster_sources: {
|
||||
ECMWF: 28.8,
|
||||
GFS: 29.4,
|
||||
"WeatherNext 2": 29.8,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -144,8 +145,9 @@ export function runTests() {
|
||||
assert(
|
||||
MODEL_SUMMARY_MODEL_COLUMNS.map((column) => column.key).includes("AROME HD") &&
|
||||
MODEL_SUMMARY_MODEL_COLUMNS.map((column) => column.key).includes("HRRR") &&
|
||||
MODEL_SUMMARY_MODEL_COLUMNS.map((column) => column.key).includes("NAM"),
|
||||
"model summary must expose the fixed model columns including optional short-range models",
|
||||
MODEL_SUMMARY_MODEL_COLUMNS.map((column) => column.key).includes("NAM") &&
|
||||
MODEL_SUMMARY_MODEL_COLUMNS.map((column) => column.key).includes("WeatherNext 2"),
|
||||
"model summary must expose the fixed model columns including optional short-range and WeatherNext 2 models",
|
||||
);
|
||||
assert(summaryRows.length === 5, "model summary should keep one row per city");
|
||||
assert(summaryRows[0].cityName === "Beijing", "model summary should sort by resolved region then city name");
|
||||
@@ -179,6 +181,7 @@ export function runTests() {
|
||||
"model summary should aggregate Fahrenheit probabilities into two-degree market option labels",
|
||||
);
|
||||
assert(parisRow.marketMatches.length === 3, "model summary should keep every Polymarket tradable bucket");
|
||||
assert(beijingRow?.models["WeatherNext 2"] === 29.8, "model summary should preserve WeatherNext 2 model representative");
|
||||
assert(
|
||||
parisRow.marketMatches[0].label === "32°C" &&
|
||||
parisRow.marketMatches[0].modelProbability === null &&
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import {
|
||||
readScanCache,
|
||||
writeScanCache,
|
||||
} from "@/components/dashboard/scan-terminal/use-scan-terminal-query";
|
||||
|
||||
function assert(condition: unknown, message: string) {
|
||||
if (!condition) throw new Error(message);
|
||||
}
|
||||
|
||||
export function runTests() {
|
||||
const originalLocalStorage = globalThis.localStorage;
|
||||
const storage = new Map<string, string>();
|
||||
const memoryStorage = {
|
||||
getItem: (key: string) => storage.get(key) ?? null,
|
||||
removeItem: (key: string) => { storage.delete(key); },
|
||||
setItem: (key: string, value: string) => { storage.set(key, value); },
|
||||
} as Storage;
|
||||
|
||||
Object.defineProperty(globalThis, "localStorage", {
|
||||
configurable: true,
|
||||
value: memoryStorage,
|
||||
});
|
||||
|
||||
try {
|
||||
writeScanCache(
|
||||
{
|
||||
generated_at: "2026-07-02T12:00:00Z",
|
||||
rows: [{ city: "ankara", deb_prediction: 20.3 } as any],
|
||||
status: "ready",
|
||||
} as any,
|
||||
"all",
|
||||
"model-summary",
|
||||
);
|
||||
assert(
|
||||
readScanCache("all", "model-summary", { allowStale: true })?.rows?.length === 1,
|
||||
"non-empty scan cache should remain readable",
|
||||
);
|
||||
|
||||
writeScanCache(
|
||||
{
|
||||
generated_at: "2026-07-02T12:05:00Z",
|
||||
rows: [],
|
||||
status: "ready",
|
||||
} as any,
|
||||
"all",
|
||||
"model-summary",
|
||||
);
|
||||
assert(
|
||||
readScanCache("all", "model-summary", { allowStale: true }) === null,
|
||||
"empty scan cache responses must be ignored so model summary can revalidate rows",
|
||||
);
|
||||
} finally {
|
||||
Object.defineProperty(globalThis, "localStorage", {
|
||||
configurable: true,
|
||||
value: originalLocalStorage,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@ function scanCacheKey(tradingRegion: string, cacheScope: string): string {
|
||||
return `${SCAN_CACHE_PREFIX}:${cacheScope || "terminal"}:${tradingRegion || "all"}`;
|
||||
}
|
||||
|
||||
function readScanCache(
|
||||
export function readScanCache(
|
||||
tradingRegion: string,
|
||||
cacheScope: string,
|
||||
options?: { allowStale?: boolean },
|
||||
@@ -47,15 +47,27 @@ function readScanCache(
|
||||
const cached = JSON.parse(raw);
|
||||
const age = Date.now() - Number(cached.ts || 0);
|
||||
const maxAge = options?.allowStale ? MAX_STALE_SCAN_CACHE_MS : SCAN_CACHE_TTL_MS;
|
||||
if (cached.ts && age >= 0 && age < maxAge && cached.data?.rows) {
|
||||
if (
|
||||
cached.ts &&
|
||||
age >= 0 &&
|
||||
age < maxAge &&
|
||||
Array.isArray(cached.data?.rows) &&
|
||||
cached.data.rows.length > 0
|
||||
) {
|
||||
return cached.data;
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
return null;
|
||||
}
|
||||
|
||||
function writeScanCache(data: ScanTerminalResponse, tradingRegion: string, cacheScope: string) {
|
||||
try { localStorage.setItem(scanCacheKey(tradingRegion, cacheScope), JSON.stringify({ ts: Date.now(), data })); } catch { /* ignore */ }
|
||||
export function writeScanCache(data: ScanTerminalResponse, tradingRegion: string, cacheScope: string) {
|
||||
try {
|
||||
if (!Array.isArray(data.rows) || data.rows.length <= 0) {
|
||||
localStorage.removeItem(scanCacheKey(tradingRegion, cacheScope));
|
||||
return;
|
||||
}
|
||||
localStorage.setItem(scanCacheKey(tradingRegion, cacheScope), JSON.stringify({ ts: Date.now(), data }));
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
function normalizeCityKey(city: string | null | undefined) {
|
||||
|
||||
Reference in New Issue
Block a user