* feat(supply-chain): Sprint E — scenario visual completion + service parity - E1: fetchSectorDependency exported from supply-chain service index - E2: PRO gate + all-renderer dispatch in MapContainer.activateScenario - E3: scenario summary banner in SupplyChainPanel (dismiss wired) - E4: "Simulate Closure" trigger button in expanded chokepoint cards - E5: affectedIso2s heat layer in DeckGLMap (GeoJsonLayer, red tint) - E6: SVG renderer setScenarioState (best-effort iso2 fill) - E7: Globe renderer scenario polygons via flushPolygons - E8: integration tests for scenario run/status endpoints * fix(supply-chain): address PR #2910 review findings (P1 + P2 + P3) - Wire setOnScenarioActivate + setOnDismissScenario in panel-layout.ts (todo #155) - Rename shadow variable t→tmpl in SCENARIO_TEMPLATES.find (todo #152) - Add statusResp.ok guard in scenario polling loop (todo #153) - Replace status.result! non-null assertion with shape guard (todo #154) - Add AbortController to prevent concurrent polling races (todo #162) - Add polygonStrokeColor scenario branch (transparent) in GlobeMap (todo #156) - Re-export SCENARIO_TEMPLATES via src/config/scenario-templates.ts (todo #157) - Cache affectedIso2Set in DeckGLMap.setScenarioState (todo #158) - Add scenario paths to PREMIUM_RPC_PATHS for auth injection (todo #160) - Show template name in scenario banner instead of raw ID (todo #163) * fix(supply-chain): address PR #2910 review findings - Add auth headers to scenario fetch calls in SupplyChainPanel - Reset button state on scenario dismiss - Poll status immediately on first iteration (no 2s delay) - Pre-compute scenario polygons in GlobeMap.setScenarioState - Use scenarioId for DeckGL updateTriggers precision * fix(supply-chain): wire panel instance to MapContainer, stop button click propagation - Call setSupplyChainPanel() in panel-layout.ts so scenario banner renders - Add stopPropagation() to Simulate Closure button to prevent card collapse
2.9 KiB
status, priority, issue_id, tags, dependencies
| status | priority | issue_id | tags | dependencies | ||||
|---|---|---|---|---|---|---|---|---|
| complete | p2 | 162 |
|
attachScenarioTriggers Has No In-Flight Guard — Concurrent Polling Possible
Problem Statement
SupplyChainPanel.attachScenarioTriggers() launches a polling loop but has no guard against being called while a previous poll is still running. If the user clicks "Simulate Closure" a second time before the first job completes (e.g., after navigating away and back to the chokepoint), two polling loops run concurrently. Both can call onScenarioActivate, resulting in a race between two job results applying visual state in an undefined order.
Findings
- File:
src/components/SupplyChainPanel.ts attachScenarioTriggers()is called from the "Simulate Closure" button click handler- No
isPollingflag or AbortController checked before starting a new poll - Existing guard:
if (!button.isConnected) breakonly exits on DOM removal, not on second click - Two concurrent polls can overlap and call
onScenarioActivatewith different results - Identified by kieran-typescript-reviewer during PR #2910 review
Proposed Solutions
Option A: AbortController + in-flight flag (Recommended)
private scenarioPollController: AbortController | null = null;
private async attachScenarioTriggers(button: HTMLButtonElement, cp: Chokepoint): Promise<void> {
// Cancel any in-flight poll
this.scenarioPollController?.abort();
this.scenarioPollController = new AbortController();
const { signal } = this.scenarioPollController;
// ... in the polling loop:
if (signal.aborted || !button.isConnected) break;
const statusResp = await fetch(`...`, { signal });
}
Pros: Clean cancellation, prevents concurrent polls, mirrors standard fetch abort patterns
Cons: Need to handle AbortError gracefully (not show error banner)
Effort: Small | Risk: Low
Option B: Simple boolean in-flight flag
private isScenarioPolling = false;
if (this.isScenarioPolling) return;
this.isScenarioPolling = true;
try { /* poll */ } finally { this.isScenarioPolling = false; }
Blocks second trigger rather than cancelling first. Simpler but less responsive (user can't restart a stalled poll). Effort: Small | Risk: None
Recommended Action
Apply Option A — AbortController for clean cancellation, matching the codebase's existing fetch patterns.
Technical Details
- Affected files:
src/components/SupplyChainPanel.ts - Add
private scenarioPollController: AbortController | null = null - Handle
AbortErrorin the catch block (do not show error banner on abort)
Acceptance Criteria
- Second click aborts previous polling loop
AbortErrornot surfaced to user as "Error — retry"npm run typecheckpasses
Work Log
- 2026-04-10: Identified by kieran-typescript-reviewer during PR #2910 review
Resources
- PR: #2910