ui/phase-21: make MapView's mounted flag reactive

The renderer-mount effect in `lib/active-view/map.svelte` reads
`mounted` to gate the runSerializedMount call, but the variable was
declared as a plain `let`, not `$state`. On the first navigation to
/map this is benign: the effect's first pass returns early (gameState
still hydrating, `report` null), and once `report` arrives the
effect re-fires — by which point `onMount` has already flipped
`mounted = true`.

On every subsequent return to /map the report is already loaded by
the long-lived gameState in the layout. The effect therefore makes
exactly one pass on the freshly-mounted component, gates on
`mounted === false` (the brand-new instance has not run `onMount`
yet), and never wakes up again because no tracked state changes
afterwards. Symptom: black canvas — fresh DOM, no mount-error
overlay, but Pixi never rebuilt the world on the new canvas.

Convert `mounted` to `$state(false)` so flipping it true inside
`onMount` triggers the effect's second pass, which now finds all
preconditions satisfied and proceeds to `runSerializedMount`. The
detailed lifecycle reasoning is preserved as a code comment so the
next reader can see why this one variable must be reactive.

Add tests/e2e/map-roundtrip.spec.ts: navigates /map → {report,
ship-class designer, science designer, mail} → /map for each
non-map view, then asserts the renderer republished primitives onto
the DEV `__galaxyDebug.getMapPrimitives()` surface. The pre-fix
build failed every variant; the patch lands all four green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Ilia Denisov
2026-05-10 22:58:32 +02:00
parent 85ea6f413e
commit 9c29f03d66
2 changed files with 245 additions and 1 deletions
+11 -1
View File
@@ -96,7 +96,17 @@ preference the store already manages.
let detachClick: (() => void) | null = null;
let detachDebugProviders: (() => void) | null = null;
let detachDebugSurface: (() => void) | null = null;
let mounted = false;
// `mounted` must be `$state` so the renderer-mount effect re-runs
// once `onMount` flips it true. On the first map navigation the
// effect's initial pass returns early (gameState is still hydrating
// → `report` is null), and the subsequent server-driven `report`
// transition re-fires the effect after `onMount` has already
// completed. On a second navigation back to /map the report is
// already loaded — without reactivity here the effect's first
// pass would gate on `mounted === false`, and there would be no
// later state change to wake it up. The visible symptom is a
// black canvas (renderer never re-mounted on the new DOM).
let mounted = $state(false);
// Mount serialization. The `$effect` may re-fire while the
// async `mountRenderer` is mid-flight (e.g. report transitions
// from null → populated → overlay-mutated during boot). Without