fix(map): make installStyleSetup safe for mid-session mounts (#159)#161
Merged
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #159
What
installStyleSetup(app/src/lib/styleReload.ts) stranded mid-session installs: whenisStyleLoaded()read transiently false (basemap tiles streaming) it armedonce('load')— butloadfires exactly once per map lifetime and was long past for components mounting mid-session (ownship toggle / Live tab). On a static map nostyledatarescue ever came, so setup never ran: no accuracy circle, no #148 ownship vector, and a frozen stalesc-aissource on Live revisits.MapLibre semantics (researched against the installed maplibre-gl 5.24.0 sources)
Map.isStyleLoaded()delegates toStyle.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.addSource/addLayersafety gate isStyle._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).loadfires once per map lifetime on the first fully-loaded render (src/ui/map.ts:3698-3701);idlefires every time the render loop settles with!isMoving() && map.loaded()(src/ui/map.ts:3720-3723) — each condition that makesisStyleLoaded()read false schedules repaints on completion, so anidlealways follows a transient false window.styledata(relay of the Styledata/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)alongsideonce('load', setup); the persistentstyledatalistener and the immediateisStyleLoaded()fast path are unchanged; the disposer removes all three registrations.loadkeeps covering a genuinely-loading first install (the behavior the four component reload suites pin);idlerescues 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.tsunchanged (its generic event buckets already modelidle).Tests
styleReload.test.tscontract tests and the BoatMarker/AisLayer/RouteLayer/DataLayers suites (including BoatMarker's local fake, which has nogetStyle— one reason the probe was not switched to a new method).styleReload.test.ts,#159block): late install withisStyleLoaded()===falseandloadalready consumed runs setup exactly once on the nextidle(and a secondidledoes not stack a re-run); astyledataarriving beforeidlealso rescues; post-dispose, neitheridlenorstyledataresurrects.typecheck+lintclean; full vitest suite 93 files / 959 tests green (255 s), incl.realmask.repro.test.ts.Real-browser evidence (dev server, port 5182)
Patched
navigator.geolocationwith a moving fix, planned Langballigau→Sønderborg, then 6× loop:jumpToa fresh area at deep zoom, wait for the transientisStyleLoaded()===falsewindow, flip the ownship toggle ON (the mid-sessionBoatMarkermount). Result: 6/6 iterations showedsc-boat-accuracy-fill+sc-boat-vector-line+ the marker; in 5/6 the install happened withisStyleLoaded()===falseandloadlong 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 BEFOREload" 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