rega's heap snapshots (leak-722ac7789): Simple Research and All People Groups mounted twice went 54.2 MB to 65.1 MB, with live Vue apps 6 to 8. Every unmounted application was still there, one copy per mount, and the retainers were all the same shape -- a window key set during setup and never cleared. The closure keeps its component, the component keeps its shadow root, and a whole app survives its own unmount. An element property dies with the element; a window key does not. The named one was window.__doxaLogoOwner -> Map -> div.rm-map-canvas -> ShadowRoot -> app, released only when the map's 'remove' event fires. That listener cannot be the guarantee the global is cleared, because it only runs if remove() is reached; it is a consequence of the guarantee, not the guarantee itself. useMapInstance now drops the claim inside destroy(), before removing, so it goes even if remove() throws part-way. As for why remove() was never reached: research-map's unmount hook ran ten teardown steps and called destroy() last, and two of those steps were unguarded calls into other composables. A single throw anywhere above stranded the map, and with it the app. I could not reproduce which step threw without a browser, so the fix does not depend on knowing: each step is isolated and destroy() runs from a finally, where nothing above it can skip it. Ordering teardown so the most important step is last, behind nine chances to fail, was the real defect. The globals themselves are released by the components that set them -- four verify hooks in research-map, three in the dashboard map panel, the vocabulary hook in the grid, which had no unmount hook at all. Released as SETS: __rdashPinAt was the one the snapshot named, but __rdashMap holds the map and retains strictly more, so clearing only the reported one would have looked fixed and leaked nearly as much. The deletes are written one per line instead of looped over an array of names, so that the check can read them. A release a checker cannot see is worth no more than no release, the day someone removes three of the four. The check flags only values that actually retain. A warn-once boolean latch and a page-wide cache keyed off itself are deliberate and must not be reported, or it becomes noise and gets switched off. It is NOT in gates:pre: 19 sites remain in other owners' plugins, and this is a fleet pattern rather than one plugin's bug. |
||
|---|---|---|
| .. | ||
| api | ||
| charts | ||
| colors | ||
| components | ||
| composables | ||
| constants | ||
| data | ||
| elements | ||
| i18n | ||
| layouts | ||
| lists | ||
| maps | ||
| packet | ||
| packets | ||
| scripts | ||
| stores | ||
| styles | ||
| utils | ||
| vendor/cldr | ||
| index.js | ||
| ProfileLoader.vue | ||
| README.md | ||
library/ (@map) — the shared map template
The other half of the architecture (see ../plugin-designs/README.md):
every map in plugin-designs/<your-map>/ composes the reusable, parameterized pieces that live here.
Fix something here and every map benefits on the next build; break something here and every map
breaks — read components/WARNING.md before touching shared pieces.
| Folder | What lives there |
|---|---|
ProfileLoader.vue |
The root of every bundle: parses profile-config, loads the profile, provides the stores + config |
components/ |
Shared UI — map, legends, controls, popup (PeopleGroupDetail), tables, charts, hints (README) |
composables/ |
Shared logic — map engine (useMapInstance), provider switch, shadow-DOM styles, theming |
stores/ |
Per-instance Pinia stores (ui / map / data) — provided by ProfileLoader, injected by children |
api/ |
Data-source registry (sources.json) + fetch layer — the JAMstack runtime wiring |
colors/ |
Color sets maps opt into via profile-config.colorSet |
i18n/ |
The shared string catalogs (all maps speak the same languages) |
data/ |
Small shared datasets baked at build time (big served data lives in ../data/) |
constants/ |
Shared constants |
scripts/ |
Maintainer pipelines (posters, research) — not part of any bundle |
The rule of thumb (same as WARNING.md): custom code in your app-profile, reusable code here — and things only move from the first to the second once they've earned it.