Skip to content

fix(map): make installStyleSetup safe for mid-session mounts (#159)#161

Merged
DocGerd merged 1 commit into
developfrom
fix/159-late-install-style-setup
Jul 24, 2026
Merged

fix(map): make installStyleSetup safe for mid-session mounts (#159)#161
DocGerd merged 1 commit into
developfrom
fix/159-late-install-style-setup

Conversation

@DocGerd

@DocGerd DocGerd commented Jul 24, 2026

Copy link
Copy Markdown
Owner

Closes #159

What

installStyleSetup (app/src/lib/styleReload.ts) stranded mid-session installs: when isStyleLoaded() read transiently false (basemap tiles streaming) it armed once('load') — but load fires exactly once per map lifetime and was long past for components mounting mid-session (ownship toggle / Live tab). On a static map no styledata rescue ever came, so setup never ran: no accuracy circle, no #148 ownship vector, and a frozen stale sc-ais source on Live revisits.

MapLibre semantics (researched against the installed maplibre-gl 5.24.0 sources)

  • Map.isStyleLoaded() delegates to Style.loaded() (src/ui/map.ts:2242-2247), which returns false while the style JSON is unparsed, or source updates are pending, or any tile manager still has tiles in flight, or sprite/images are loading (src/style/style.ts:624-636) — it means "everything fully loaded", not "style present", so it is confounded by tile streaming.
  • The actual addSource/addLayer safety gate is Style._loaded (_checkLoaded() throws iff it is false, src/style/style.ts:715-719), which flips true synchronously when the style JSON is parsed (Style._load, src/style/style.ts:478) — long before tiles/sprite finish. There is no clean public probe for it (getStyle()'s undefined-when-unloaded contract exists but is a heavyweight serialize).
  • load fires once per map lifetime on the first fully-loaded render (src/ui/map.ts:3698-3701); idle fires every time the render loop settles with !isMoving() && map.loaded() (src/ui/map.ts:3720-3723) — each condition that makes isStyleLoaded() read false schedules repaints on completion, so an idle always follows a transient false window. styledata (relay of the Style data/dataType:'style' event, src/ui/map.ts:842) fires on the initial style parse (src/style/style.ts:501) and on every reload/mutation.

Mechanism

The deferral branch now arms once('idle', setup) alongside once('load', setup); the persistent styledata listener and the immediate isStyleLoaded() fast path are unchanged; the disposer removes all three registrations. load keeps covering a genuinely-loading first install (the behavior the four component reload suites pin); idle rescues the late install; every path fires only when the style JSON is in place, so setup never runs against a missing style. Idempotent setup absorbs the possible double one-shot on an initial load. The helper's contract doc is rewritten to match — installs are now safe at any point in the map lifetime.

No call-site changes; test/fakeMaplibre.ts unchanged (its generic event buckets already model idle).

Tests

  • All existing reload tests pass unmodified: the 5 direct styleReload.test.ts contract tests and the BoatMarker/AisLayer/RouteLayer/DataLayers suites (including BoatMarker's local fake, which has no getStyle — one reason the probe was not switched to a new method).
  • New hand-derived pins (styleReload.test.ts, #159 block): late install with isStyleLoaded()===false and load already consumed runs setup exactly once on the next idle (and a second idle does not stack a re-run); a styledata arriving before idle also rescues; post-dispose, neither idle nor styledata resurrects.
  • typecheck + lint clean; full vitest suite 93 files / 959 tests green (255 s), incl. realmask.repro.test.ts.

Real-browser evidence (dev server, port 5182)

Patched navigator.geolocation with a moving fix, planned Langballigau→Sønderborg, then 6× loop: jumpTo a fresh area at deep zoom, wait for the transient isStyleLoaded()===false window, flip the ownship toggle ON (the mid-session BoatMarker mount). Result: 6/6 iterations showed sc-boat-accuracy-fill + sc-boat-vector-line + the marker; in 5/6 the install happened with isStyleLoaded()===false and load long past — exactly the window that stranded forever pre-fix. Toggling OFF tore both layers down cleanly every time. Screenshot (boat triangle + accuracy circle + 045° COG vector after a false-window install) captured during the run.

No CHANGELOG entry

The stranding regression never shipped in a release — #151/#156, which composed into it, are both still in [Unreleased]; this fix corrects unreleased behavior, so there is no user-visible change to record.

Note (out of file fence)

DataLayers.tsx:238's comment still describes the old "install BEFORE load" contract. Its install timing (map arrival) remains correct and safe under the relaxed contract; the comment is now merely over-cautious. Left untouched per the #159 file fence — can be tidied opportunistically later.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UXvHWVP7iALk2rScg3CtkT

MapLibre's isStyleLoaded() is Style.loaded(), which ANDs "style JSON
parsed" with "all tiles/sprite/images finished loading" — it reads
transiently false mid-session while basemap tiles stream, and 'load'
fires exactly once per map lifetime. A mid-session install (ownship
toggle / Live tab) that hit that window armed once('load') and waited
forever: no accuracy circle, no #148 ownship vector, frozen stale
sc-ais source on Live revisits.

The deferral now arms once('idle') alongside once('load'): 'idle'
recurs every time the render loop settles with everything loaded, so
it always follows a transient false window; 'load' keeps covering a
first install while the initial style is genuinely loading (the
behavior the four component reload suites pin). The persistent
'styledata' listener is unchanged. The disposer removes all three
listeners, so a disposed install can never be resurrected.

Existing helper and component reload tests pass unmodified; new tests
pin the late-install rescue (idle and styledata paths) and the
post-unmount no-resurrection guarantee.

Closes #159

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXvHWVP7iALk2rScg3CtkT
@DocGerd
DocGerd merged commit cb4cef3 into develop Jul 24, 2026
5 checks passed
@DocGerd
DocGerd deleted the fix/159-late-install-style-setup branch July 24, 2026 00:17
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.

installStyleSetup strands mid-session mounts while tiles stream — ownship accuracy circle/vector can silently never appear; AIS layer can freeze stale

1 participant