fix(url-sync): don't overwrite URL position params on initial load#2580
fix(url-sync): don't overwrite URL position params on initial load#2580koala73 merged 8 commits intokoala73:mainfrom
Conversation
Race condition: setupUrlStateSync() called debouncedUrlSync() immediately, but applyInitialUrlState() had already started an async flyTo via setCenter(). At ~250ms the debounce fired while the flyTo was still running, reading the map's default center (lat=20, lon=0, zoom=1) and writing it back over the URL-specified params. Fix: skip the immediate debouncedUrlSync() call when URL position params are present. onStateChanged fires on moveend (after flyTo completes) and handles the first URL write at that point with the correct position. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@rayanhabib07 is attempting to deploy a commit to the Elie Team on Vercel. A member of the Team first needs to authorize it. |
Greptile SummaryThis PR fixes a race condition in Key changes:
Minor edge case (P2): When Confidence Score: 5/5Safe to merge — the fix is narrowly scoped, correctly handles the race condition, and only skips an erroneous initial sync that was clobbering URL params. The single changed file makes a minimal, well-targeted fix. The logic is sound: No files require special attention beyond the one P2 edge case noted on Important Files Changed
Sequence DiagramsequenceDiagram
participant App
participant PanelLayout
participant EventHandlers
participant Map
participant URL
App->>PanelLayout: applyInitialUrlState()
PanelLayout->>Map: setCenter(lat, lon, zoom)
Map-->>Map: flyTo animation starts (async)
App->>EventHandlers: setupUrlStateSync()
EventHandlers->>EventHandlers: urlHasPosition = (lat/lon/zoom in initialUrlState)?
alt urlHasPosition = false (no position params in URL)
EventHandlers->>EventHandlers: debouncedUrlSync() fires after 250ms
EventHandlers->>Map: getCenter() returns default (lat=20, lon=0, zoom=1)
EventHandlers->>URL: replaceState(default position) BUG
else urlHasPosition = true (position params present) NEW BEHAVIOUR
EventHandlers->>EventHandlers: skip debouncedUrlSync()
Note over Map,URL: flyTo animation runs...
Map-->>EventHandlers: onStateChanged fires on moveend
EventHandlers->>EventHandlers: debouncedUrlSync() fires after 250ms
EventHandlers->>Map: getCenter() returns correct position
EventHandlers->>URL: replaceState(correct position)
end
Reviews (1): Last reviewed commit: "fix(url-sync): don't overwrite URL posit..." | Re-trigger Greptile |
| const urlHasPosition = | ||
| this.ctx.initialUrlState?.lat !== undefined || | ||
| this.ctx.initialUrlState?.lon !== undefined || | ||
| this.ctx.initialUrlState?.zoom !== undefined; | ||
| if (!urlHasPosition) { | ||
| this.debouncedUrlSync(); | ||
| } |
There was a problem hiding this comment.
Silent gap when
zoom ≤ 2 with lat/lon present
In applyInitialUrlState() there is an existing guard that skips setCenter() when effectiveZoom <= 2:
if (lat !== undefined && lon !== undefined) {
const effectiveZoom = zoom ?? this.ctx.map.getState().zoom;
if (effectiveZoom > 2) this.ctx.map.setCenter(lat, lon, zoom); // ← skipped when zoom≤2
}When a URL like ?lat=41&lon=0&zoom=1 is loaded:
urlHasPositionistrue(lat/lon/zoom are all defined), sodebouncedUrlSync()is correctly skipped by the new guard.- However, because
effectiveZoom <= 2,setCenter()is never called — no flyTo animation is started, andmoveendnever fires. onStateChangedtherefore never fires from the animation, meaning the first URL write is silently dropped. The URL retains the original params indefinitely until the user manually interacts with the map.
Before this PR the old debouncedUrlSync() call would at least update the URL to the actual map state (even if with the wrong position). After this PR the URL is preserved but permanently out of sync with the map's actual position until the next user interaction.
The practical impact is low — a URL with zoom=1 or zoom=2 alongside explicit lat/lon is an unusual combination, and the user interacting with the map immediately restores consistency. But if precise URL-shareability on initial load is important, you may want to call debouncedUrlSync() explicitly after applyInitialUrlState() when setCenter() was skipped for this reason.
When the URL contains ?view=global&zoom=1.00, setView was called without a zoom override, so flyTo animated to the preset zoom (1.5) instead. The subsequent moveend/onStateChange cycle then wrote zoom=1.50 back to the URL, overwriting the correct zoom=1.00. Pass the URL zoom as the second argument to setView so flyTo targets the correct zoom level. Also remove the effectiveZoom > 2 guard from the lat/lon branch so URL lat/lon is always honoured regardless of zoom level. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tations DeckGLMap, GlobeMap, Map, and MapContainer setView() only accepted a single view argument — the zoom passed from panel-layout was silently ignored, so ?view=X&zoom=Y still wrote the preset zoom back to the URL after moveend. - DeckGLMap: pass zoom ?? preset.zoom into flyTo - GlobeMap: apply same deck.gl→altitude mapping as setCenter when zoom provided - Map (SVG): use zoom ?? settings.zoom for the SVG map zoom state - MapContainer: forward the optional zoom to all underlying implementations
|
Pushed a fix on top of your branch for commit 2.
Updated all four:
|
P1: MapContainer was forwarding zoom to DeckGL/Globe but not to the SVG fallback renderer — ?view=X&zoom=Y was still loading at preset zoom on mobile/WebGL-fallback. P2: urlHasPosition fired on any single URL param including lone ?lat or ?lon. applyInitialUrlState only calls setCenter when both are present, so malformed links like ?lat=41 skipped canonicalization and left stale params in the URL until the user moved the map. Fix: rename to urlHasAsyncFlyTo and only suppress the initial debounced sync when applyInitialUrlState will actually trigger an async operation: - view is set (setView → flyTo) - both lat AND lon are set (setCenter → flyTo) - zoom is set (setZoom → animated zoom)
|
Pushed two more fixes: P1 — SVG/mobile path dropped the zoom ( P2 — partial URL params skipped canonicalization Renamed to |
view was included in the suppression condition but this breaks two renderer paths: - SVG/Map: setView() is synchronous and fires emitStateChange before the URL-sync listener is installed (App.ts wires listeners after applyInitialUrlState runs). No async callback ever fires to drive the URL write when suppressed. - GlobeMap: onStateChanged() is a no-op stub — suppressing the initial debounce leaves /?view=X deep links without any URL sync on the globe renderer. view state is written synchronously at the top of every setView() implementation (this.state.view = view), so the 250ms debounced read is always correct regardless of renderer. Remove view from the guard. Only suppress for the two cases where an in-flight flyTo makes getCenter() return stale intermediate coordinates: (a) lat+lon pair present → setCenter() flyTo (b) zoom-only without a view preset → setZoom() animated zoom
|
Pushed the P1 fix. Removed
The guard now only suppresses in the two cases where an in-flight flyTo makes
|
DeckGLMap.setView() started a flyTo without updating this.state.zoom
or this.pendingCenter. The 250ms URL debounce would then read:
- state.zoom → old cached value (wrong zoom written to URL)
- getCenter() → maplibreMap.getCenter() mid-animation (intermediate
coords written to URL, e.g. lat=22.1 instead of 25.0)
Fix: at the top of setView(), before flyTo starts:
- Set this.state.zoom = zoom ?? preset.zoom (zoom correct immediately)
- Set this.pendingCenter = preset lat/lon (center correct immediately)
- getCenter() returns pendingCenter when set, falls through to live
maplibre coords after moveend clears it
This ensures any URL sync that fires during the flyTo animation (whether
the initial 250ms debounce or an onStateChanged triggered by the sync
setView call itself) reads the correct destination values.
|
Pushed the DeckGL center fix.
Added
Any URL sync that fires during the flyTo — whether the initial 250ms debounce or an |
…+ DeckGLMap.pendingCenter 20 tests across three suites: - urlHasAsyncFlyTo guard: 10 cases covering cold load, view-only, partial lat/lon, full lat+lon, bare zoom, view+zoom, and the setCenter override path. - DeckGLMap.pendingCenter: 8 cases verifying eager center/zoom cache on setView, override zoom, consecutive calls, and moveend clearing the cache. - Integration regression: view=mena path confirms suppression is skipped and pendingCenter holds correct preset coords for the URL builder.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Root cause
Race condition in setupUrlStateSync(). applyInitialUrlState() calls
map.setCenter() which starts an async flyTo animation. setupUrlStateSync()
was then immediately calling debouncedUrlSync() — 250ms later the debounce
fired while flyTo was still running, reading the default center
(lat=20, lon=0, zoom=1) and writing it back over the URL params.
Fix
Skip the immediate debouncedUrlSync() call when URL position params
(lat/lon/zoom) are present. onStateChanged fires on moveend after flyTo
completes, at which point getCenter() returns the correct position and
handles the first URL write.
Test plan