Repository navigation
Description
Summary
anywidget evaluates a widget's _esm module once per widget model (each model gets a fresh Blob URL import). Perspective's perspective-anywidget.js bundle performs page-global side effects at module scope that throw on the second evaluation, so any page with two Perspective widget models — or one re-created model — renders nothing. Two independent failure layers:
- Unguarded
customElements.define()forregular-layout,regular-layout-frame, andregular-layout-tab(bundledregular-layoutdependency). Second evaluation throwsNotSupportedError: ... the name "regular-layout" has already been used with this registry. (Long-standing; present in 5.6.1.) - New in 5.7.0 (via Better WASM
init#3269): module-scopeperspective.init({ viewer, server })registers into aglobalThis-shared WASM registry that throws"viewer wasm is already initialized"on the second registration.
Why this breaks
anywidget's runtime imports _esm through a fresh Blob URL per model (load.ts), so the browser's module cache never dedupes: module-scope code runs once per widget model, while customElements and globalThis are page-global. #3192 already applied skip-and-warn guards to Perspective's own custom elements for exactly this reason; the bundled regular-layout registration and the newer WASM registry are the two spots that work didn't cover.
Minimal reproduction (pure JavaScript, no Python)
Serve the shipped perspective/widget/static/perspective-anywidget.js (here from perspective-python==5.9.0) and import it the way anywidget's runtime does:
<script type="module">
const src = await (await fetch("/perspective-anywidget.js")).text();
async function importWidgetModule(label) {
const url = URL.createObjectURL(new Blob([src], { type: "text/javascript" }));
try {
await import(url);
console.log(`${label}: ok`);
} catch (e) {
console.error(`${label}: ${e.message}`);
} finally {
URL.revokeObjectURL(url);
}
}
await importWidgetModule("widget-1"); // ok
await importWidgetModule("widget-2"); // NotSupportedError: "regular-layout" already defined
</script>With the define collision patched over, the second evaluation then fails on 5.7.0+ with Error: viewer wasm is already initialized (as an unhandled rejection, leaving the exported ready promise permanently rejected).
Real-world impact
This affects any host that renders anywidget models through the generic runtime — JupyterLab's widget manager path, VS Code notebooks, marimo, Panel, etc. Verified with perspective-python 5.6.1 and 5.9.0 in a current JupyterLab:
import pandas as pd
from perspective.widget import PerspectiveWidget
df = pd.DataFrame({"a": [1, 2, 3], "b": ["x", "y", "z"]})
PerspectiveWidget(df) # cell 1 — fine on a fresh page
PerspectiveWidget(df) # cell 2 — "Error displaying widget"The same failure hits a single widget whose model is re-created — re-running the cell, or reopening a notebook whose saved widget output re-renders next to a freshly executed one. So "show two tables side by side" and "re-run this cell" are both broken on the anywidget path. (Related: with today's pip install jupyterlab perspective-python, the perspective labextension can't activate — it requires @jupyter-widgets/base ^2 || ^3 || ^4 while JupyterLab provides 6.x — so notebook users plausibly are on that path right now. Happy to file that separately.)
Fix
A PR implementing both fixes is open at #3287 — small patch (3 files, ~150 lines including tests), validated end-to-end: with it, both module evaluations above succeed and resolve to the same runtime, and single-evaluation behavior is unchanged.
Environment
perspective-python5.6.1 (define issue), 5.7.0–5.9.0 (define issue + WASM registry issue)anywidget0.11.0, Chromium via Playwright; reproduced in a bare HTML page and in JupyterLab
Activity
[-]anywidget front-end throws on the second widget instance in a page: module-scope `customElements.define` and (since 5.7.0) WASM registry `init()` are not re-evaluation safe[/-][+]anywidget front-end throws on the second widget instance in a page[/+]