* fix(panels): always fire background RPC refresh after bootstrap render Bootstrap hydration (getHydratedData) is one-shot — once rendered from it, panels never refresh and can show stale or partial data indefinitely. Affected panels: MacroSignals, ETFFlows, Stablecoins, FuelPrices, GulfEconomies, GroceryBasket, BigMac. Pattern: render from bootstrap immediately for fast first paint, then fire a background RPC call that silently updates the panel with live data. Errors during background refresh are suppressed when bootstrap data is already visible (no error flash over valid data). * fix(panels): guard background RPC refresh against empty response overwriting bootstrap Empty RPC responses (200 + empty array) no longer overwrite valid bootstrap data with error/unavailable state across all 7 affected panels: - ETFFlowsPanel, StablecoinPanel: wrap this.data assignment in `if (fresh.xxx?.length || !this.data)` guard - FuelPricesPanel, GulfEconomiesPanel, GroceryBasketPanel, BigMacPanel: add `!data.xxx?.length` check in background .then() before calling render - MacroSignalsPanel: return false early when error suppressed to skip redundant renderPanel() call * fix(hormuz): fix noUncheckedIndexedAccess TypeScript errors * fix(todos): add blank lines around headings (markdownlint MD022) * fix(hormuz): add missing hormuz-tracker service + fix implicit any in HormuzPanel * revert: remove HormuzPanel.ts from this branch (belongs in PR #2210)
2.7 KiB
status, priority, issue_id, tags
| status | priority | issue_id | tags | ||||
|---|---|---|---|---|---|---|---|
| pending | p2 | 014 |
|
Two performance issues in simulation package builders: new Set in filter predicate + intersectAny O(n²)
Problem Statement
Two patterns in the new simulation package code introduce unnecessary allocations and O(n×m) comparisons that will grow with the actor registry size.
Findings
CRITICAL-1: intersectAny uses Array.includes — O(n×m) in the actor registry loop
buildSimulationPackageEntities builds allForecastIds as a flat array via flatMap, then passes it to intersectAny which calls right.includes(item) for each item in actor.forecastIds. With a 200-actor registry and 8 forecast IDs each, this is ~38,400 comparisons per call.
const allForecastIds = candidates.flatMap((c) => c.sourceSituationIds || []);
for (const actor of (actorRegistry || [])) {
if (!intersectAny(actor.forecastIds || [], allForecastIds)) continue;
Fix: convert allForecastIds to a Set once before the loop.
CRITICAL-2: new Set(candidate.marketBucketIds || []) inside filter predicate
isMaritimeChokeEnergyCandidate constructs a new Set from marketBucketIds (typically 2-3 elements) on every candidate, then calls .has() twice. At 50 candidates this is 50 Set allocations for a 2-element array check. Array.includes is faster for arrays of this size.
const buckets = new Set(candidate.marketBucketIds || []);
return buckets.has('energy') || buckets.has('freight') || ...
Proposed Solutions
Fix both (Recommended)
// Fix 1: Set-based forecast ID lookup
const allForecastIdSet = new Set(candidates.flatMap((c) => c.sourceSituationIds || []));
for (const actor of (actorRegistry || [])) {
if (!(actor.forecastIds || []).some((id) => allForecastIdSet.has(id))) continue;
// Fix 2: Array.includes instead of Set for small arrays
const bucketArr = candidate.marketBucketIds || [];
return bucketArr.includes('energy') || bucketArr.includes('freight') || topBucket === 'energy' || topBucket === 'freight'
|| SIMULATION_ENERGY_COMMODITY_KEYS.has(candidate.commodityKey || '');
Effort: Tiny | Risk: Low
Acceptance Criteria
allForecastIdsis aSetbefore the actor registry loop inbuildSimulationPackageEntitiesisMaritimeChokeEnergyCandidateusesArray.includesorArray.someinstead ofnew SetformarketBucketIdscheck- All existing simulation package tests still pass
Technical Details
- File:
scripts/seed-forecasts.mjs—isMaritimeChokeEnergyCandidate,buildSimulationPackageEntities
Work Log
- 2026-03-24: Found by compound-engineering:review:performance-oracle in PR #2204 review