Files
worldmonitor/todos/162-complete-p2-attach-scenario-triggers-no-in-flight-guard.md
Elie Habib 60e727679c feat(supply-chain): Sprint E — scenario visual completion + service parity (#2910)
* 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
2026-04-10 21:31:26 +04:00

2.9 KiB

status, priority, issue_id, tags, dependencies
status priority issue_id tags dependencies
complete p2 162
code-review
quality
supply-chain
reliability

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 isPolling flag or AbortController checked before starting a new poll
  • Existing guard: if (!button.isConnected) break only exits on DOM removal, not on second click
  • Two concurrent polls can overlap and call onScenarioActivate with different results
  • Identified by kieran-typescript-reviewer during PR #2910 review

Proposed Solutions

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

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 AbortError in the catch block (do not show error banner on abort)

Acceptance Criteria

  • Second click aborts previous polling loop
  • AbortError not surfaced to user as "Error — retry"
  • npm run typecheck passes

Work Log

  • 2026-04-10: Identified by kieran-typescript-reviewer during PR #2910 review

Resources

  • PR: #2910