Release the test hooks on unmount, all twelve of them
Twelve globals were set across three plugins and none was ever deleted. Each pins whatever it closes over — a map instance pins its canvas, its sources and through them the whole app — so every mount left another copy behind, measured elsewhere at roughly 10 MB each. Released as a set rather than one at a time. A block that sets several globals and clears the obvious one leaks the rest silently, and the obvious one is whichever the author happened to remember. Identity-checked, which is why the value is recorded as it is set rather than cleared by name. Two instances can share a page, and clearing by name alone would let the second unmount delete the first's live hooks — trading a leak for a harder bug in which a live map loses its test surface at a distance. A first attempt compared against a name that did not exist. Inside the try/catch that guards these, the ReferenceError was swallowed and every delete silently did nothing — a leak fix that looks exactly like a leak fix and leaks. The guard that makes the assignment safe is the same guard that hides its failure, which is the argument for recording what was set rather than inferring it at teardown.
This commit is contained in:
parent
82b2e2aa5a
commit
24b32e199f
6 changed files with 117 additions and 15 deletions
|
|
@ -1,4 +1,11 @@
|
|||
<script setup>
|
||||
// What this instance put on window, recorded AS IT IS SET so unmount can tell its own values
|
||||
// from another instance's. Two of these can share a page; clearing by NAME alone would let the
|
||||
// second unmount delete the first's live hooks.
|
||||
const ownGlobals = Object.create(null)
|
||||
function setGlobal(name, value) {
|
||||
try { window[name] = value; ownGlobals[name] = value } catch (_) {}
|
||||
}
|
||||
/**
|
||||
* connectivity-map.vue — THE CITIES-WITH-INTERNET GLOBAL MAP.
|
||||
*
|
||||
|
|
@ -193,14 +200,14 @@ function paintGroups(map) {
|
|||
map.on('mouseleave', 'ci-pg-dots', () => { map.getCanvas().style.cursor = '' })
|
||||
// Verification handle, same contract as gospel-resources' __grState: the counts the layer holds.
|
||||
if (typeof window !== 'undefined') {
|
||||
window.__ciState = () => ({
|
||||
setGlobal('__ciState', () => ({
|
||||
groups: groups.value.length,
|
||||
dots: dotCount.value,
|
||||
hasLayer: !!map.getLayer('ci-pg-dots'),
|
||||
rendered: map.queryRenderedFeatures({ layers: ['ci-pg-dots'] }).length,
|
||||
selected: uiStore?.selectedPeopleGroup?.properties?.slug || null,
|
||||
})
|
||||
window.__ciMap = map
|
||||
}))
|
||||
setGlobal('__ciMap', map)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -435,6 +442,16 @@ onMounted(async () => {
|
|||
} catch (e) { err.value = String(e.message || e) }
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
// ★ RELEASE THE TEST HOOKS. Each pins whatever it closes over — a map instance pins its
|
||||
// canvas, its sources, and through them the whole app — so every mount that leaves one behind
|
||||
// leaks another copy, measured at roughly 10 MB each.
|
||||
// Released as a SET, not just the obvious one: a block that sets several and clears one leaks
|
||||
// the rest silently, and the named one is the one you remember. One delete per line, so the
|
||||
// released set is readable and greppable.
|
||||
// Identity-checked: only this instance's own value is cleared, so a second instance sharing the
|
||||
// page does not delete the first's live hooks on its way out.
|
||||
try { if (window.__ciState === ownGlobals.__ciState) delete window.__ciState } catch (_) {}
|
||||
try { if (window.__ciMap === ownGlobals.__ciMap) delete window.__ciMap } catch (_) {}
|
||||
unmounted = true
|
||||
selectedPin.cleanup()
|
||||
uiStore?.selectPeopleGroup(null)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,11 @@
|
|||
<script setup>
|
||||
// What this instance put on window, recorded AS IT IS SET so unmount can tell its own values
|
||||
// from another instance's. Two of these can share a page; clearing by NAME alone would let the
|
||||
// second unmount delete the first's live hooks.
|
||||
const ownGlobals = Object.create(null)
|
||||
function setGlobal(name, value) {
|
||||
try { window[name] = value; ownGlobals[name] = value } catch (_) {}
|
||||
}
|
||||
/**
|
||||
* GiGrid.vue — the grid under the map: one row per people group in scope, sortable, selectable, with a
|
||||
* column picker and the CSV export.
|
||||
|
|
@ -64,6 +71,16 @@ onMounted(() => {
|
|||
}
|
||||
})
|
||||
onBeforeUnmount(() => { try { ro && ro.disconnect() } catch (_) {} })
|
||||
// ★ RELEASE THE TEST HOOKS. Each pins whatever it closes over — a map instance pins its
|
||||
// canvas, its sources, and through them the whole app — so every mount that leaves one behind
|
||||
// leaks another copy, measured at roughly 10 MB each.
|
||||
// Released as a SET, not just the obvious one: a block that sets several and clears one leaks
|
||||
// the rest silently, and the named one is the one you remember. One delete per line, so the
|
||||
// released set is readable and greppable.
|
||||
// Identity-checked: only this instance's own value is cleared, so a second instance sharing the
|
||||
// page does not delete the first's live hooks on its way out.
|
||||
try { if (window.__giLastCsv === ownGlobals.__giLastCsv) delete window.__giLastCsv } catch (_) {}
|
||||
try { if (window.__giExportStatus === ownGlobals.__giExportStatus) delete window.__giExportStatus } catch (_) {}
|
||||
// A new scope starts at the top: row 900 of the old scope means nothing in the new one.
|
||||
watch(() => board.filtered.value, () => { if (scroller.value) scroller.value.scrollTop = 0; scrollTop.value = 0 })
|
||||
|
||||
|
|
@ -105,7 +122,7 @@ function exportCsv() {
|
|||
document.body.appendChild(a); a.click(); downloaded = true
|
||||
setTimeout(() => { try { URL.revokeObjectURL(url); a.remove() } catch (_) {} }, 200)
|
||||
} catch (_) {}
|
||||
try { window.__giLastCsv = csv; window.__giExportStatus = { rows: n, clipboard, downloaded } } catch (_) {}
|
||||
try { setGlobal('__giLastCsv', csv); setGlobal('__giExportStatus', { rows: n, clipboard, downloaded }) } catch (_) {}
|
||||
status.value = T('gospelInternet.grid.exported', '{n} rows exported (copied and downloaded).', { n: n.toLocaleString() })
|
||||
setTimeout(() => { status.value = '' }, 4000)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,11 @@
|
|||
<script setup>
|
||||
// What this instance put on window, recorded AS IT IS SET so unmount can tell its own values
|
||||
// from another instance's. Two of these can share a page; clearing by NAME alone would let the
|
||||
// second unmount delete the first's live hooks.
|
||||
const ownGlobals = Object.create(null)
|
||||
function setGlobal(name, value) {
|
||||
try { window[name] = value; ownGlobals[name] = value } catch (_) {}
|
||||
}
|
||||
/**
|
||||
* GiMap.vue — the board's map: countries shaded by internet band, India's states and China's provinces
|
||||
* outlined (and shaded on request), and one pin per people group.
|
||||
|
|
@ -299,14 +306,14 @@ onMounted(async () => {
|
|||
else if (board.view.value === 'province') moveCamera({ bounds: [[68, 8], [123, 45]] })
|
||||
// test hook: what a probe needs to assert DATA drew, not just the basemap (PAINT GATES lesson)
|
||||
try {
|
||||
window.__giMap = map
|
||||
window.__giState = () => ({
|
||||
setGlobal('__giMap', map)
|
||||
setGlobal('__giState', () => ({
|
||||
provider: made.provider, pins: map.getSource('gi-pins') ? pinFC().features.length : 0,
|
||||
renderedPins: map.queryRenderedFeatures({ layers: ['gi-pins'] }).length,
|
||||
renderedCountries: map.queryRenderedFeatures({ layers: ['gi-adm0-fill'] }).length,
|
||||
provinces: provinceKeys.length, inScope: board.filtered.value.length,
|
||||
selected: [...bus.selectionSet], hover: bus.hoverKey.value,
|
||||
})
|
||||
}))
|
||||
} catch (_) {}
|
||||
} catch (e) { err.value = String(e && e.message || e) }
|
||||
})
|
||||
|
|
@ -320,6 +327,16 @@ onMounted(() => {
|
|||
}
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
// ★ RELEASE THE TEST HOOKS. Each pins whatever it closes over — a map instance pins its
|
||||
// canvas, its sources, and through them the whole app — so every mount that leaves one behind
|
||||
// leaks another copy, measured at roughly 10 MB each.
|
||||
// Released as a SET, not just the obvious one: a block that sets several and clears one leaks
|
||||
// the rest silently, and the named one is the one you remember. One delete per line, so the
|
||||
// released set is readable and greppable.
|
||||
// Identity-checked: only this instance's own value is cleared, so a second instance sharing the
|
||||
// page does not delete the first's live hooks on its way out.
|
||||
try { if (window.__giMap === ownGlobals.__giMap) delete window.__giMap } catch (_) {}
|
||||
try { if (window.__giState === ownGlobals.__giState) delete window.__giState } catch (_) {}
|
||||
try { ro && ro.disconnect() } catch (_) {}
|
||||
unmounted = true
|
||||
if (camTimer) clearTimeout(camTimer)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,11 @@
|
|||
<script setup>
|
||||
// What this instance put on window, recorded AS IT IS SET so unmount can tell its own values
|
||||
// from another instance's. Two of these can share a page; clearing by NAME alone would let the
|
||||
// second unmount delete the first's live hooks.
|
||||
const ownGlobals = Object.create(null)
|
||||
function setGlobal(name, value) {
|
||||
try { window[name] = value; ownGlobals[name] = value } catch (_) {}
|
||||
}
|
||||
/**
|
||||
* GiResources.vue — the second tab under the board: every gospel resource for the LANGUAGES of the people
|
||||
* groups in scope. Pick a family on the tree, a country on the map or a facet in the rail, and this lists
|
||||
|
|
@ -63,6 +70,15 @@ onMounted(() => {
|
|||
}
|
||||
})
|
||||
onBeforeUnmount(() => { try { ro && ro.disconnect() } catch (_) {} })
|
||||
// ★ RELEASE THE TEST HOOKS. Each pins whatever it closes over — a map instance pins its
|
||||
// canvas, its sources, and through them the whole app — so every mount that leaves one behind
|
||||
// leaks another copy, measured at roughly 10 MB each.
|
||||
// Released as a SET, not just the obvious one: a block that sets several and clears one leaks
|
||||
// the rest silently, and the named one is the one you remember. One delete per line, so the
|
||||
// released set is readable and greppable.
|
||||
// Identity-checked: only this instance's own value is cleared, so a second instance sharing the
|
||||
// page does not delete the first's live hooks on its way out.
|
||||
try { if (window.__giLastResourcesCsv === ownGlobals.__giLastResourcesCsv) delete window.__giLastResourcesCsv } catch (_) {}
|
||||
watch(() => board.filtered.value, () => { if (scroller.value) scroller.value.scrollTop = 0; scrollTop.value = 0 })
|
||||
|
||||
const status = ref('')
|
||||
|
|
@ -78,7 +94,7 @@ function exportCsv() {
|
|||
const a = document.createElement('a'); a.href = url; a.download = 'gospel-internet-resources.csv'; a.style.display = 'none'
|
||||
document.body.appendChild(a); a.click(); setTimeout(() => { try { URL.revokeObjectURL(url); a.remove() } catch (_) {} }, 200)
|
||||
} catch (_) {}
|
||||
try { window.__giLastResourcesCsv = csv } catch (_) {}
|
||||
try { setGlobal('__giLastResourcesCsv', csv )} catch (_) {}
|
||||
status.value = T('gospelInternet.grid.exported', '{n} rows exported (copied and downloaded).', { n: fmt(rows.value.length) })
|
||||
setTimeout(() => { status.value = '' }, 4000)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,11 @@
|
|||
<script setup>
|
||||
// What this instance put on window, recorded AS IT IS SET so unmount can tell its own values
|
||||
// from another instance's. Two of these can share a page; clearing by NAME alone would let the
|
||||
// second unmount delete the first's live hooks.
|
||||
const ownGlobals = Object.create(null)
|
||||
function setGlobal(name, value) {
|
||||
try { window[name] = value; ownGlobals[name] = value } catch (_) {}
|
||||
}
|
||||
/**
|
||||
* GiTree.vue — the LANGUAGE TREE, on a whiteboard: every family our people groups belong to, as a radial
|
||||
* tree (family -> branches -> languages -> dialects), the families packed like cells in a dish.
|
||||
|
|
@ -235,11 +242,11 @@ async function build() {
|
|||
try { map.fitBounds([[w, -lat], [e, lat]], { padding: 20, duration: 0 }) } catch (_) {}
|
||||
if (board.treeNode.value) { map.setFilter('gt-sel', ['==', ['get', 'id'], board.treeNode.value]); frame(subtreeCoords(board.treeNode.value)) }
|
||||
try {
|
||||
window.__giTree = map
|
||||
window.__giTreeState = () => ({ provider: made.provider, nodes: t.nodes.length,
|
||||
setGlobal('__giTree', map)
|
||||
setGlobal('__giTreeState', () => ({ provider: made.provider, nodes: t.nodes.length,
|
||||
renderedNodes: map.queryRenderedFeatures({ layers: ['gt-nodes'] }).length,
|
||||
renderedLabels: map.queryRenderedFeatures({ layers: ['gt-lab-f', 'gt-lab-l', 'gt-lab-b', 'gt-lab-d'] }).length,
|
||||
treeNode: board.treeNode.value })
|
||||
treeNode: board.treeNode.value }))
|
||||
} catch (_) {}
|
||||
}
|
||||
onMounted(() => {
|
||||
|
|
@ -251,6 +258,16 @@ onMounted(() => {
|
|||
build().catch((e) => { err.value = String(e && e.message || e) })
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
// ★ RELEASE THE TEST HOOKS. Each pins whatever it closes over — a map instance pins its
|
||||
// canvas, its sources, and through them the whole app — so every mount that leaves one behind
|
||||
// leaks another copy, measured at roughly 10 MB each.
|
||||
// Released as a SET, not just the obvious one: a block that sets several and clears one leaks
|
||||
// the rest silently, and the named one is the one you remember. One delete per line, so the
|
||||
// released set is readable and greppable.
|
||||
// Identity-checked: only this instance's own value is cleared, so a second instance sharing the
|
||||
// page does not delete the first's live hooks on its way out.
|
||||
try { if (window.__giTree === ownGlobals.__giTree) delete window.__giTree } catch (_) {}
|
||||
try { if (window.__giTreeState === ownGlobals.__giTreeState) delete window.__giTreeState } catch (_) {}
|
||||
unmounted = true
|
||||
if (camTimer) clearTimeout(camTimer)
|
||||
try { ro && ro.disconnect() } catch (_) {}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,11 @@
|
|||
<script setup>
|
||||
// What this instance put on window, recorded AS IT IS SET so unmount can tell its own values
|
||||
// from another instance's. Two of these can share a page; clearing by NAME alone would let the
|
||||
// second unmount delete the first's live hooks.
|
||||
const ownGlobals = Object.create(null)
|
||||
function setGlobal(name, value) {
|
||||
try { window[name] = value; ownGlobals[name] = value } catch (_) {}
|
||||
}
|
||||
/**
|
||||
* resources-map.vue — THE GOSPEL RESOURCES GLOBAL MAP.
|
||||
*
|
||||
|
|
@ -1562,9 +1569,9 @@ function paintPins(map) {
|
|||
// which is how a gate ends up reporting a product defect for its own bad arithmetic.
|
||||
// __grState() is the falsifiable one: it reports the counts the layer actually holds.
|
||||
if (typeof window !== 'undefined') {
|
||||
window.__grMap = map
|
||||
window.__grOpenPin = (i) => openPin(i)
|
||||
window.__grState = () => ({
|
||||
setGlobal('__grMap', map)
|
||||
setGlobal('__grOpenPin', (i) => openPin(i))
|
||||
setGlobal('__grState', () => ({
|
||||
pins: pinRows.value.length,
|
||||
codes: Object.keys(pinCodes.value).length,
|
||||
hasLayer: !!map.getLayer('gr-pins'),
|
||||
|
|
@ -1579,7 +1586,7 @@ function paintPins(map) {
|
|||
countryWithout: withoutResources.value.length,
|
||||
pinsInCountry: (pinsByIso.value[selected.value?.iso3] || []).length,
|
||||
group: selectedGroup.value,
|
||||
})
|
||||
}))
|
||||
}
|
||||
map.on('mouseenter', 'gr-pins', () => { map.getCanvas().style.cursor = 'pointer' })
|
||||
map.on('mouseleave', 'gr-pins', () => { map.getCanvas().style.cursor = '' })
|
||||
|
|
@ -1789,6 +1796,17 @@ onMounted(async () => {
|
|||
} catch (e) { err.value = String(e.message || e) }
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
// ★ RELEASE THE TEST HOOKS. Each pins whatever it closes over — a map instance pins its
|
||||
// canvas, its sources, and through them the whole app — so every mount that leaves one behind
|
||||
// leaks another copy, measured at roughly 10 MB each.
|
||||
// Released as a SET, not just the obvious one: a block that sets several and clears one leaks
|
||||
// the rest silently, and the named one is the one you remember. One delete per line, so the
|
||||
// released set is readable and greppable.
|
||||
// Identity-checked: only this instance's own value is cleared, so a second instance sharing the
|
||||
// page does not delete the first's live hooks on its way out.
|
||||
try { if (window.__grMap === ownGlobals.__grMap) delete window.__grMap } catch (_) {}
|
||||
try { if (window.__grOpenPin === ownGlobals.__grOpenPin) delete window.__grOpenPin } catch (_) {}
|
||||
try { if (window.__grState === ownGlobals.__grState) delete window.__grState } catch (_) {}
|
||||
try { mapRef.value?.remove() } catch (_) {}
|
||||
if (typeof window !== 'undefined') window.removeEventListener('resize', syncNarrow)
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in a new issue