feat(02-05): bound the Leaflet map's lifecycle across Mesh tab deactivation
Demo images / Build & push demo images (push) Failing after 3m45s
Demo images / Build & push demo images (push) Failing after 3m45s
MeshMap.vue's onMounted setup (window resize listener + ResizeObserver)
is refactored into an idempotent armMapVisibility()/disarmMapVisibility()
pair, dual-registered on both onMounted and onActivated (onActivated is a
documented no-op outside a KeepAlive boundary) and torn down on
onDeactivated, matching the arm/disarm idiom Mesh.vue itself already uses.
The Leaflet instance is never destroyed or recreated by this — initMap()'s
own `if (!mapContainer.value || map) return` guard already makes
construction idempotent, so exactly one map is built per session. On
reactivation the map's size is invalidated via nextTick so a map laid out
while off screen re-tiles at its real size instead of showing an unsized
or partially tiled canvas.
FLAGGED: RESEARCH.md's premise that Mesh.vue owns a live D3 force
simulation does not hold for this codebase — a grep for
d3/forceSimulation/simulation across neode-ui/src found nothing in
Mesh.vue's or MeshMap.vue's tree; the only D3 force simulation belongs to
NetworkMap.vue (Federation.vue's graph, out of this plan's scope). The
plan's D3-specific truths are therefore vacuously satisfied — see
02-05-SUMMARY.md for detail. Only the real Leaflet-map lifecycle work
landed here.
meshMapLifecycle.test.ts is a new, separate file (not appended to
meshTabCache.test.ts) because its vi.mock('@/stores/mesh')/vi.mock('leaflet')
hoist file-wide and would otherwise clobber meshTabCache.test.ts's need for
the real mesh/transport stores — mirrors the MarketplaceRefresh.test.ts
precedent from 02-02 for the same class of vi.mock-hoisting conflict.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
31389bcc3c
commit
abdfa07a77
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, watch, computed } from 'vue'
|
||||
import { ref, onMounted, onUnmounted, onActivated, onDeactivated, nextTick, watch, computed } from 'vue'
|
||||
import { useMeshStore } from '@/stores/mesh'
|
||||
import type { NodePosition } from '@/stores/mesh'
|
||||
import L from 'leaflet'
|
||||
@@ -361,34 +361,76 @@ watch(
|
||||
|
||||
let resizeObserver: ResizeObserver | null = null
|
||||
|
||||
onMounted(() => {
|
||||
// Only-while-visible: the window resize listener and ResizeObserver follow
|
||||
// the Mesh tab's activate/deactivate lifecycle now that this component
|
||||
// survives a tab switch under KeepAlive (T-02-03/D-03 — Mesh.vue joined
|
||||
// KEEP_ALIVE_PATHS in 02-04). The Leaflet instance itself is never destroyed
|
||||
// or recreated by any of this: initMap()'s own `if (!mapContainer.value ||
|
||||
// map) return` guard already makes construction idempotent, so exactly one
|
||||
// map is built per session no matter how many times armMapVisibility() runs.
|
||||
// This only quiesces/repairs the listener + observer and re-tiles the map
|
||||
// around it, so a map laid out while off screen doesn't paint at a stale or
|
||||
// partial size on return.
|
||||
//
|
||||
// Called from BOTH onMounted and onActivated: onActivated is a documented
|
||||
// no-op outside a <KeepAlive> boundary (see Mesh.vue's own armMeshLive for
|
||||
// the same idiom), so a bare mount (a unit test, or any future non-KeepAlive
|
||||
// usage) must not silently skip this setup.
|
||||
function armMapVisibility() {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
window.addEventListener('resize', handleResize)
|
||||
|
||||
if (mapContainer.value) {
|
||||
resizeObserver = new ResizeObserver((entries) => {
|
||||
const entry = entries[0]
|
||||
if (!entry) return
|
||||
const { height } = entry.contentRect
|
||||
if (!map && height > 10) {
|
||||
initMap()
|
||||
} else if (map) {
|
||||
try { map.invalidateSize() } catch { /* destroyed */ }
|
||||
}
|
||||
})
|
||||
if (!resizeObserver) {
|
||||
resizeObserver = new ResizeObserver((entries) => {
|
||||
const entry = entries[0]
|
||||
if (!entry) return
|
||||
const { height } = entry.contentRect
|
||||
if (!map && height > 10) {
|
||||
initMap()
|
||||
} else if (map) {
|
||||
try { map.invalidateSize() } catch { /* destroyed */ }
|
||||
}
|
||||
})
|
||||
}
|
||||
resizeObserver.observe(mapContainer.value)
|
||||
}
|
||||
|
||||
// Fallback init
|
||||
// Fallback init for the very first mount (idempotent no-op once `map` is
|
||||
// already set — mirrors initMap's own not-yet-laid-out retry loop).
|
||||
setTimeout(initMap, 300)
|
||||
|
||||
// A map laid out while the tab was off screen may have settled at a
|
||||
// zero/partial size; re-tile it at its real size now that it's back on
|
||||
// screen instead of showing an unsized or partially tiled canvas.
|
||||
if (map) {
|
||||
void nextTick(() => {
|
||||
try { map?.invalidateSize() } catch { /* destroyed */ }
|
||||
})
|
||||
}
|
||||
}
|
||||
function disarmMapVisibility() {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
if (resizeObserver) resizeObserver.disconnect()
|
||||
}
|
||||
|
||||
// Vue fires onActivated immediately after onMounted on a KeepAlive-wrapped
|
||||
// component's first mount, which would otherwise double the fallback
|
||||
// setTimeout(initMap, 300) call on every fresh page load (harmless since
|
||||
// initMap is idempotent, but redundant). This flag lets onMounted's call
|
||||
// count as the first activation's arm, so onActivated only re-arms on a
|
||||
// genuine later reactivation, matching Mesh.vue's own meshFreshMount idiom.
|
||||
let mapMountFresh = true
|
||||
onActivated(() => {
|
||||
if (mapMountFresh) { mapMountFresh = false; return }
|
||||
armMapVisibility()
|
||||
})
|
||||
onMounted(() => armMapVisibility())
|
||||
onDeactivated(() => disarmMapVisibility())
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
disarmMapVisibility()
|
||||
stopSharing()
|
||||
if (resizeObserver) {
|
||||
resizeObserver.disconnect()
|
||||
resizeObserver = null
|
||||
}
|
||||
if (map) {
|
||||
map.remove()
|
||||
map = null
|
||||
|
||||
Reference in New Issue
Block a user