Every plugin ships as a self-contained IIFE, so library/utils/embedScript.js is compiled separately into each one: eleven bundles on main each carry their own copy. Its cache was a module-scoped const, which made "load exactly once" true per BUNDLE rather than per page — the opposite of what the file's own header claimed. Two shells nesting the same child (the Research Gallery and the Plugin Studio both mounting doxa-research-map) each injected it. Nothing failed, which is why it survived. The second request came from the HTTP cache, so no failed request and no extra network row; the bundle then executed in full a second time — a second Vue, a second vue-i18n, a second pinia, ~375 KiB gzip of work — and its guarded customElements.define quietly declined at the very end. Where the two URLs are different versions of one plugin, whichever executed first owns the tag and the other is discarded in silence. The cache now lives on globalThis, the only scope that matches the claim, under a versioned key so a page mixing old and new bundles falls back to a cache each rather than misreading a shape it does not expect. Entries are keyed by the resolved href, because the callers do not pass the same string for one file. A <script> already in the document is adopted, which is what covers bundles built before this and host pages that write their own tag; that resolves immediately, and it is safe only because every caller here follows with a wait on the element itself — the note in the file says so, since a new caller must keep that pairing. claimTag is a diagnostic, not a guard: by the time a second bundle arrives the define is long since won, so the thing that was missing was anyone saying so. The check imports the module twice under different URLs — two module instances sharing one globalThis, the same shape as two IIFEs on one page. It asserts the second caller receives the FIRST one's promise, not merely that one script tag exists: mutation-testing showed the tag count alone is satisfied by the adoption path, so it would have passed with the original bug restored. |
||
|---|---|---|
| .. | ||
| app-wrapper-material | ||
| pane-wrapper | ||
| README.md | ||
| StackLayout.vue | ||
| VerticalTabsLayout.vue | ||
layouts/ — app wrappers
Each folder here is an app wrapper: a host shell that composes doxa-maps plugin-designs (the independently-built custom elements) into one application.
layouts/<wrapper>/ ← the HOST page (not a bundle)
┌──────────┬──────────────────────────────┐
│ sidebar │ mounts an app-profile here │
│ (nav.js) │ ┌──────────────┐ │
│ ├ item │ │ <doxa-map> │ ← a │
│ ├ section│ │ (a bundle) │ micro- │
│ └ folder │ └──────────────┘ frontend │
└──────────┴──────────────────────────────┘
- app-profile = a self-contained custom element (the micro-frontend). app wrapper = the host that mounts them and provides nav/chrome. A wrapper is a host page, not a bundle-in-a-bundle.
- Wrappers today:
app-wrapper-material/,pane-wrapper/. - Icons are inline SVG only (no CDN) — an F-Droid/offline hard requirement.
Sidebar contract, target filtering, sidebar modes, staging: ../documentation/architecture/app-wrappers.md.