Repository navigation
Make the anywidget bundle safe to evaluate more than once per page - #3287
Conversation
anywidget evaluates a widget's `_esm` module once per widget model via a fresh blob-URL import, so module-scope side effects in `perspective-anywidget.js` run once per model while `customElements` and `globalThis` remain page-global. Two module-scope side effects threw on the second evaluation: * `import "regular-layout"` self-registers `regular-layout`, `regular-layout-frame` and `regular-layout-tab` unguarded. Replace the static import with a `customElements.get()` guard and skip-and-warn, matching the perspective-dev#3192 pattern for Perspective's own elements. * `init()` at module scope (since perspective-dev#3269) re-registered `viewer`/`server` keys against the page-wide wasm registry, which threw "already initialized". `register()` in `user` mode now treats re-registration of a consumed key that already has a source as idempotent: it keeps the existing runtime, warns once per key, and resolves. `replace_server` (`init_server()`) still throws for consumed keys, and keys consumed without ever being registered (e.g. `server32` after `server` was selected) still throw as before. Signed-off-by: Carson <cpsievert1@gmail.com>
5.8.0 and 5.9.0 (released after the !=5.7.0 exclusion) do not fix the WASM init regression: tests/playwright/render_perspective fails 6/6 on both with the same 'viewer wasm is already initialized' errors. Pin to <5.7 and reference the upstream fix proposal (perspective-dev/perspective#3287).
Instead of <5.7, exclude 5.7.0-5.9.0 specifically. Once the upstream fix (perspective-dev/perspective#3287) ships in a release, fresh CI runs resolve to it automatically and tests/playwright/render_perspective re-validates the fix against shinywidgets.
Signed-off-by: Andrew Stein <steinlink@gmail.com>
texodus
left a comment
There was a problem hiding this comment.
Thanks for the PR!
The await import makes the viewer ESM dist an async module (top-level await).
I've updated regular-layout texodus/regular-layout#20 and reverted this section 2739e9e
Semantics worth a look: re-registering an unconsumed key still overwrites its source
I don't know what the semantics of this should be. I'd prefer error, maybe with a "Reconsider your choices" message, except it seems many popular platforms do this. I did not realize Anywidget re-evaluates the module per model ... this library is ~400k with charts and datagrid that loads, parses, then subsequently ignored and leaks (I assume). I will do a follow-up that memoizes the bulk of Perspective's application code for anywidget to prevent this.
Fixes #3286.
anywidget evaluates a widget's
_esmmodule once per widget model (each model gets a fresh blob-URL import), so module-scope side effects inperspective-anywidget.jsrun once per model whilecustomElementsandglobalThisremain page-global. Two module-scope side effects threw on the second evaluation, so any page with twoPerspectiveWidgetmodels — or one re-created model, e.g. re-running a notebook cell — rendered nothing:import "regular-layout"self-registersregular-layout,regular-layout-frameandregular-layout-tabunguarded → second evaluation throwsNotSupportedErroroncustomElements.define.init#3269, module-scopeperspective.init({ viewer, server })re-registers against the page-wide wasm registry → second evaluation throws"viewer wasm is already initialized"(as an unhandled rejection, sinceindex.jsdoesn'tawaitit, leavingreadypermanently rejected).This PR completes the coexistence work started in #3192 (which guarded Perspective's own elements):
perspective-viewer.ts— replace the staticimport "regular-layout"with acustomElements.get()guard:await import("regular-layout")when unregistered (top-level await keeps registration synchronous with module evaluation, as the static import was), skip-and-warn otherwise, matching Add@perspective-dev/anywidgetfor vscode/jupyter #3192'sbootstrappattern. Done at the source rather than as an anywidget-build alias because the viewer build inlinesregular-layout— this covers every consumer of the viewer bundle.registry.ts— inusermode, re-registering aconsumedkey that already has a source is now idempotent: keep the existing runtime, warn once per key per page, resolve.replace_server(init_server()) still throws for consumed keys, and keys consumed without ever being registered (e.g.server32afterserverwas selected) still throw as before.Notes for reviewers
await importmakes the viewer ESM dist an async module (top-level await). All in-repo consumers are ESM/es2022 or webpack 5, so this is legal; a hypothetical classic-script consumer of the ESM dist would break (none exists in-repo).serversource while the consumedviewerkey skips+warns.Validation
registry.spec.js: 3 new tests (secondinit()resolves with an object-identical runtime; warn-once-per-key across three inits;init_server()still throws on a consumed server) — suite: 11 passed, 1 pre-existing skip.multi_panel/close_panel.spec.ts(exercises<regular-layout>/<regular-layout-frame>heavily): 6 passed.perspective-anywidget.jstwice via fresh blob URLs (mimicking two anywidget models) — both evaluations succeed, both exportedreadypromises resolve against the same runtime, zero console errors. Previously the second import threw withNotSupportedError: the name "regular-layout" has already been used with this registry.