Skip to content

fix(map+panels): swallow deck.gl render race + viewport-gate tech-readiness refresh#3833

Merged
koala73 merged 3 commits into
mainfrom
worktree-shimmying-wandering-moon
May 19, 2026
Merged

fix(map+panels): swallow deck.gl render race + viewport-gate tech-readiness refresh#3833
koala73 merged 3 commits into
mainfrom
worktree-shimmying-wandering-moon

Conversation

@koala73

@koala73 koala73 commented May 19, 2026

Copy link
Copy Markdown
Owner

Summary

Two independent fixes bundled into one PR — both are surgical, single-file, low-blast-radius.

#1 — deck.gl/maplibre interleaved-mode render race (src/components/DeckGLMap.ts)

Module-scoped installDeckInterleavedRaceFilter() adds a one-shot capture-phase window.addEventListener('error', …) that suppresses

Uncaught TypeError: Cannot read properties of null (reading 'id')
  at DeckRenderer._drawLayers  (deck-stack-*.js)
  at LayerManager.renderLayers
  at MapLibre painter.renderLayer (maplibre-*.js)

Trigger: deck setProps({layers}) finalizes a layer between _resolveLayers and renderLayers; the next maplibre repaint hits the freed slot. MapboxOverlay.onError can't catch this — maplibre, not deck, owns the throwing callstack.

  • Called from initDeck() — idempotent on recreateWithFallback / HMR.
  • Filter narrows on BOTH the message shape and the deck-stack-*.js chunk filename so a first-party .id crash still surfaces.
  • Sentry's beforeSend in main.ts:313-315 already drops this exact pattern for telemetry, so the change is pure console-noise removal.

#5 — tech-readiness refresh storm on energy/finance/commodity variants (src/app/data-loader.ts)

Replaced

if (SITE_VARIANT !== 'happy') {  (this.ctx.panels['tech-readiness'] as TechReadinessPanel)?.refresh()  }

with

if (SITE_VARIANT !== 'happy' && shouldLoad('tech-readiness')) {  }

matching the thermal-escalation gate one line below.

Why this fixes the 5 s timeout: App.ts:576-583 merges ALL_PANELS into panelSettings on every variant for cross-variant pref carryover, so shouldCreatePanel('tech-readiness') is true everywhere — but the bootstrap seed key only exists on full + tech variants. The unconditional refresh() at services/economic/index.ts:694 therefore times out on every other variant's data-loader cycle. Viewport-gating prevents the fan-out from firing on variants whose panel will never be visible.

Test plan

  • Build passes (npm run typecheck:api, boundaries, premium-fetch parity — already green from push hooks)
  • Manual: load any non-happy variant (energy, finance, commodity) → confirm no 5 s timeout entry for techReadiness in data-loader logs
  • Manual: open map with chokepoint pulse layer → trigger a layer swap (e.g. flip a chokepoint filter) → confirm no Cannot read properties of null in DevTools console
  • Sentry: confirm the deck-stack-* .id event continues to be dropped client-side (no console error) AND continues to be filtered in telemetry

koala73 added 2 commits May 19, 2026 09:57
Module-scoped installDeckInterleavedRaceFilter() adds a one-shot
capture-phase window error listener that suppresses
  TypeError: Cannot read properties of null (reading 'id')
when the throwing file matches /deck-stack-…\.js/. Called from
initDeck() — idempotent across recreateWithFallback / HMR.

Trigger is the deck.gl 9.x + maplibre-gl 5.x interleaved-mode race:
setProps({layers}) finalizes a layer while maplibre's painter is
mid-render, so the painter callback iterates a list that just had
a slot nulled. MapboxOverlay.onError can't see this — maplibre, not
deck, owns the throwing callstack.

Sentry's beforeSend in main.ts:313-315 already drops this pattern
for telemetry, so impact is purely console-noise removal. First-
party .id crashes still surface because the filter requires BOTH
the message shape AND the deck-stack chunk filename.
…iants

Replace `if (SITE_VARIANT !== 'happy')` with `… && shouldLoad('tech-readiness')`,
matching the thermal-escalation gate one line below.

Why this fixes the energy/finance/commodity-variant 5s timeout:
App.ts:576-583 merges ALL_PANELS into panelSettings on every variant
for cross-variant pref carryover, so shouldCreatePanel('tech-readiness')
returns true everywhere — but the bootstrap seed key only exists on
full + tech variants, so the unconditional refresh() at
services/economic/index.ts:694 times out on every other variant's
data-loader cycle. Viewport-gating prevents the refresh fan-out from
firing on variants whose panel will never be visible.
@vercel

vercel Bot commented May 19, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
worldmonitor Ready Ready Preview, Comment May 19, 2026 9:32am

Request Review

@greptile-apps

greptile-apps Bot commented May 19, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Two targeted fixes: a capture-phase window error listener in DeckGLMap.ts that swallows the well-known deck.gl 9.x / maplibre-gl 5.x interleaved render race (narrowed to both message shape and deck-stack-*.js filename), and a viewport gate in data-loader.ts that prevents the tech-readiness refresh() from being dispatched on variants where the panel's bootstrap key is absent.

  • DeckGLMap.ts: installDeckInterleavedRaceFilter() is module-scoped and idempotent, installed once from initDeck(), and intentionally mirrors the existing Sentry beforeSend filter at main.ts:313-315 for client-side console suppression.
  • data-loader.ts: Adds shouldLoad('tech-readiness') to match the existing thermal-escalation and cross-source-signals gates, preventing a repeated 5 s timeout on energy/finance/commodity variants where the panel exists in settings but has no seeded bootstrap data.

Confidence Score: 4/5

Both changes are surgical and low blast-radius; the recurring tech-readiness timeout storm is resolved and the deck.gl console noise is suppressed, but the initial forced-load path and the broad error-listener suppression warrant a quick look before merging.

The data-loader fix correctly eliminates repeated timeouts during normal refresh cycles, but the initial loadAllData(true) call at startup bypasses the viewport gate — meaning one timeout can still appear in logs on first page load for affected variants. The error filter in DeckGLMap is well-scoped but calls stopImmediatePropagation beyond what is needed to suppress console noise, which could silently block future error handlers registered after initDeck().

Both changed files are worth a second read: data-loader.ts for the forceAll=true initial-load gap, and DeckGLMap.ts for the stopImmediatePropagation side-effect.

Important Files Changed

Filename Overview
src/app/data-loader.ts Adds shouldLoad('tech-readiness') viewport gate to prevent repeated 5s refresh timeouts on energy/finance/commodity variants; the gate is correctly bypassed by forceAll=true on initial load (unchanged from pre-fix behavior), but this means one timeout still appears in logs on first page load.
src/components/DeckGLMap.ts Adds a module-scoped, idempotent capture-phase window error listener that swallows the deck.gl/maplibre interleaved-mode race error; filter is well-narrowed to both message shape AND deck-stack chunk filename, with DEV logging and Sentry-aligned logic.

Sequence Diagram

sequenceDiagram
    participant App as App.ts
    participant DL as DataLoaderManager
    participant PL as PanelLayoutManager
    participant TRP as TechReadinessPanel
    participant DGM as DeckGLMap
    participant Win as window (error events)

    App->>PL: await panelLayout.init()
    PL-->>TRP: lazyPanel() dynamic import (async, not awaited)
    TRP-->>PL: ctx.panels set after import resolves

    App->>DL: "loadAllData(forceAll=true)"
    Note over DL: shouldLoad = forceAll || isPanelNearViewport
    Note over DL: forceAll=true, task always queued
    DL->>TRP: refresh() batch N after 300ms delays

    App->>DL: loadAllData() subsequent refresh cycles
    Note over DL: forceAll=false, shouldLoad gates on viewport
    Note over DL: isPanelNearViewport returns false on energy/finance/commodity
    DL-->>DL: task NOT queued, fix takes effect

    App->>DGM: initDeck()
    DGM->>Win: installDeckInterleavedRaceFilter() once
    Win-->>Win: capture-phase listener registered
    Note over Win: Matches msg and deck-stack filename
    Win-->>Win: preventDefault plus stopImmediatePropagation
Loading

Reviews (1): Last reviewed commit: "fix(panels): gate tech-readiness refresh..." | Re-trigger Greptile

Comment thread src/app/data-loader.ts Outdated
Comment on lines 602 to 603
if (SITE_VARIANT !== 'happy' && shouldLoad('tech-readiness')) {
tasks.push({ name: 'techReadiness', task: runGuarded('techReadiness', () => (this.ctx.panels['tech-readiness'] as TechReadinessPanel)?.refresh()) });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 forceAll=true bypasses the viewport gate on initial load

shouldLoad is defined as forceAll || isPanelNearViewport(id). On the initial startup call (App.ts:1227 calls loadAllData(true)), forceAll=true short-circuits the check and the tech-readiness refresh() is still dispatched. By the time the batch containing this task runs (after at least one 300 ms batch delay), the lazyPanel async import has already resolved and this.ctx.panels['tech-readiness'] is populated on energy/finance/commodity variants — so ?.refresh() is not a no-op. The behavior for the initial load is unchanged from before this fix; subsequent loadAllData() (forceAll=false) cycles are correctly gated. Worth noting if the PR test plan includes checking initial-load logs, as one timeout entry will still appear on first page load.

Comment on lines +451 to +452
ev.preventDefault();
ev.stopImmediatePropagation();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 stopImmediatePropagation silences all subsequent capture-phase error listeners

ev.preventDefault() is sufficient to suppress the browser's default console logging of uncaught errors. The additional ev.stopImmediatePropagation() call prevents any other window.addEventListener('error', {capture:true}) handler registered AFTER this one from seeing the event. Since installDeckInterleavedRaceFilter is called from initDeck() — which runs after app startup — any error monitoring tool or debug hook wired up later would be silently blocked for this error class. Dropping stopImmediatePropagation would still achieve the stated goal while leaving the event visible to later handlers.

Reviewer found that PR #3833's first cut was incomplete — the
`shouldLoad('tech-readiness')` gate I added in data-loader was
silently bypassed on initial boot, and a parallel auto-refresh path
in panel-layout was never gated at all.

P1 #1 — data-loader.ts:602
  `shouldLoad(id)` returns `forceAll || isPanelNearViewport(id)` at
  data-loader.ts:444. App.ts:1226 calls `loadAllData(true)` on startup,
  forcing `shouldLoad` to true on every variant — so commodity/finance/
  energy were still enqueueing `TechReadinessPanel.refresh()` at boot.

P1 #2 — panel-layout.ts:1278
  The `lazyPanel('tech-readiness', ...)` factory calls `void p.refresh()`
  unconditionally inside the dynamic import. Because App.ts:577-583
  merges ALL_PANELS into panelSettings on every variant for cross-variant
  pref carryover, `shouldCreatePanel('tech-readiness')` (just a key-
  existence check at panel-layout.ts:854) is true everywhere — and the
  hide-disabled path at panel-layout.ts:2028-2030 runs AFTER the import's
  refresh has already fired the 5s `/api/bootstrap?keys=techReadiness`
  fetch. So the variant gate has to live inside the factory, not after.

Fix — introduce `isPanelInVariantDefaults(key)` helper in src/config/panels.ts
that returns `VARIANT_DEFAULTS[SITE_VARIANT].includes(key)`. Use it in
both auto-refresh paths:

  data-loader.ts:602
    if (isPanelInVariantDefaults('tech-readiness') && shouldLoad('tech-readiness'))

  panel-layout.ts:1278 (factory body)
    if (isPanelInVariantDefaults('tech-readiness')) {
      void p.refresh();
    }

The panel is still CREATED on all variants so users who opt-in via
settings can still see and use it — but the eager fetch only fires
where the bootstrap seeder actually populates the key (full + tech).

Regression test — tests/tech-readiness-variant-gate.test.mts uses a
line-walker (not a strict regex) to assert:
  1. data-loader's techReadiness task is gated by isPanelInVariantDefaults
  2. panel-layout's lazyPanel factory wraps p.refresh() in the same gate
  3. the helper is exported from the @/config barrel

Mutation-tested locally: reverting either gate makes the test fail
with a clear diagnostic naming the bypassed location.

Note — national-debt at panel-layout.ts:1286 has the same factory
shape (eager `void p.refresh()` on a variant-restricted panel that's
only in FULL_PANELS). Left for a follow-up so this PR stays scoped
to the reviewer's findings.
@koala73

koala73 commented May 19, 2026

Copy link
Copy Markdown
Owner Author

Pushed 1374cec9 addressing both P1 findings.

P1 #1shouldLoad(id) = forceAll || isPanelNearViewport(id) at data-loader.ts:444, and App.ts:1226 calls loadAllData(true) on boot. The viewport-only gate was bypassed at startup on every variant. Re-gated on a new isPanelInVariantDefaults('tech-readiness') helper so the task only enqueues on full + tech.

P1 #2lazyPanel('tech-readiness', ...) at panel-layout.ts:1278 calls void p.refresh() inside the dynamic import. The hide-disabled path at panel-layout.ts:2028-2030 only runs after the import resolves, so the eager refresh has already fired. Wrapped p.refresh() in if (isPanelInVariantDefaults('tech-readiness')) so the factory still creates the panel on every variant (for users who opt-in via settings) but only fires the eager fetch where the bootstrap seeder populates the key.

Helper — added isPanelInVariantDefaults(key) in src/config/panels.ts + re-exported from the @/config barrel. Returns VARIANT_DEFAULTS[SITE_VARIANT].includes(key).

Regression testtests/tech-readiness-variant-gate.test.mts line-walks both gates and asserts the variant helper is present. Mutation-tested locally: reverting either gate fails the test with a clear diagnostic.

Noted, not fixednational-debt at panel-layout.ts:1286 has the same factory shape (eager void p.refresh() on a panel that's only in FULL_PANELS). Same class of bug; left for a follow-up to keep this PR scoped.

@koala73
koala73 merged commit 8e4e25a into main May 19, 2026
11 checks passed
@koala73
koala73 deleted the worktree-shimmying-wandering-moon branch May 19, 2026 10:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant