/** * useSharedMapToken.js — ONE page, ONE map token, shared by every element on it. * * THE BUG THIS ENDS (the agent, 2026-09-23; the repo already names it * BUG-COMPOSITE-LEAVES-ARE-TOKEN-FREE-BUT-NEST-REAL-MAPS): * "the element gets the tk to the element itself and does not need to only gatekeep that to one * component only — all components in a plugin can share that tk" * * Inside ONE element that was already true: ProfileLoader provides 'mapboxToken' and eleven * components inject it. The gate is the ELEMENT BOUNDARY. Every custom element mounts its own Vue * app with its own ProfileLoader, which re-derives the token from ITS OWN profile-config — so a * composite that nests a real map hands the child no `tk` and the child starts from nothing. * * ★ AND THE FALLBACK THAT WAS SUPPOSED TO COVER THAT COULD NOT FIRE. ProfileLoader's ladder already * read `window.MAP_TOKEN` after config.tk. MEASURED on the built tree: window.MAP_TOKEN IS set on * the composite leaf — and the child STILL had no search bar. The reason is that the ladder lives * in a computed() and window.MAP_TOKEN is a PLAIN GLOBAL, not a reactive source. The generated * composite leaf FETCHES its token (one fetch( in the page), so the computed evaluates to '' long * before the token lands, caches that, and never re-runs — nothing reactive ever changed. * So the symptom looked like "no token" while the token was right there, seconds later. A grep * for the fallback would have said the fallback existed; only reading WHEN it ran explains it. * * THE FIX: make the page-level token a REACTIVE, CROSS-ELEMENT fact. * - publishMapToken(tk) stores it and announces it on `window`. * - useSharedMapToken() returns a ref that is seeded from whatever is already known and UPDATES * when a later publish happens — so an async token reaches elements that mounted before it. * - Assignments to `window.MAP_TOKEN` are adopted automatically (see installBridge), so every * ALREADY-GENERATED page keeps working untouched: the leaf pages assign that global today and * now that assignment broadcasts instead of landing silently. * * ★ WHY AN EVENT AND NOT A SHARED MODULE REF. Each bundle is its own IIFE with its own copy of this * module, so a module-level ref is NOT shared between and the * it nests — they are different bundles. The storage therefore lives on * `window` and changes travel as a DOM event, which is the only channel both copies can see. * * SECURITY: this shares a PUBLIC Mapbox token (pk.*) that is already in the page's HTML and in * every tile request. It does not widen exposure — it stops a second element on the SAME page from * being unable to use what that page was already given. Nothing is persisted or sent anywhere. */ import { ref, onBeforeUnmount } from 'vue' const KEY = '__doxaMapToken' // the value, on window, so every bundle sees one copy const EVENT = 'doxa:map-token' // fired when it changes const FLAG = '__doxaMapTokenBridge' const readStored = () => { if (typeof window === 'undefined') return '' const v = window[KEY] return typeof v === 'string' ? v : '' } /** Announce a token to every element on the page. No-ops on an empty or unchanged value. */ export function publishMapToken(tk) { if (typeof window === 'undefined') return '' const next = typeof tk === 'string' ? tk.trim() : '' if (!next || next === readStored()) return readStored() window[KEY] = next try { window.dispatchEvent(new CustomEvent(EVENT, { detail: next })) } catch { /* pre-CustomEvent host */ } return next } /** * Adopt `window.MAP_TOKEN` as a source, in BOTH directions and for the whole life of the page. * * Every generated leaf assigns `window.MAP_TOKEN`, some of them from an async fetch. Replacing that * property with an accessor means such an assignment publishes to every element automatically — * which is what makes this fix reach pages nobody regenerates. Installed once per bundle; guarded * because N bundles on one page would otherwise stack N accessors on the same property. */ function installBridge() { if (typeof window === 'undefined' || window[FLAG]) return // Seed from whatever the page already set before this bundle loaded. const existing = typeof window.MAP_TOKEN === 'string' ? window.MAP_TOKEN : '' if (existing && !readStored()) window[KEY] = existing try { Object.defineProperty(window, 'MAP_TOKEN', { configurable: true, get() { return readStored() }, set(v) { publishMapToken(v) }, }) window[FLAG] = true } catch { // A host may have made MAP_TOKEN non-configurable. Reading it still works through the ladder // below, so degrade to "no live adoption" rather than throwing during bundle init. } } /** * A reactive view of the page's shared map token. * @returns {import('vue').Ref} updates when any element publishes a token */ export function useSharedMapToken() { installBridge() const token = ref(readStored()) if (typeof window !== 'undefined') { const onChange = () => { token.value = readStored() } window.addEventListener(EVENT, onChange) // A composable may be called outside a component (rare here); guard so it cannot throw. try { onBeforeUnmount(() => window.removeEventListener(EVENT, onChange)) } catch { /* not in setup */ } } return token }