Skip to content

Redesign the VTK.wasm JavaScript API + docs overhaul#45

Open
jspanchu wants to merge 8 commits into
mainfrom
feat/js-loader-session-api
Open

Redesign the VTK.wasm JavaScript API + docs overhaul#45
jspanchu wants to merge 8 commits into
mainfrom
feat/js-loader-session-api

Conversation

@jspanchu

@jspanchu jspanchu commented Jun 13, 2026

Copy link
Copy Markdown
Member

Summary

Replaces the scattered, confusingly-named loader/session API with a single, lean entry point and adds clean teardown. Also drops legacy (<9.5) support, regenerates the docs around the new API, and wires up an auto-generated API reference.

⚠️ Breaking change — see Migration below.

Motivation

The previous API had three pain points:

  1. Namespace creation was scattered across createNamespace() (free function), VtkWASMLoader.createNamespace(), and VtkWASMLoader.createStandaloneSession() — no canonical path.
  2. Confusing names — the JS RemoteSession class shadowed the C++ vtkRemoteSession it wrapped (exposed as .sceneManager), and load() meant three different things.
  3. No clean teardown — C++ session objects were never deleted, and the remote path leaked a DOM container + canvas listeners.

What changed

Core API

  • loadVtkWasmAsync(options) → cached VtkWasmRuntime, the single factory for sessions.
  • runtime.createStandaloneSession() / runtime.createRemoteSession(); the C++ handle is private (#native), and session.vtk is the one way to the namespace.
  • Uniform dispose() / [Symbol.dispose] on runtime and both sessions for real teardown (using works).
  • RemoteSession now binds to user-provided canvases (bindCanvas) instead of a hard-coded internal selector, and never creates/removes canvas elements. A canvas element can be passed directly — when the build exposes specialHTMLTargets, the element is registered there, so it needs neither an id nor to be attached to the document (a CSS selector from the element id is the fallback). Render windows are bound lazily during update() once their state arrives, and specialHTMLTargets entries are cleaned up on unbindCanvas/dispose.
  • create/destroy through a remote session's vtk proxy are now guarded — the C++ vtkRemoteSession has no such methods, so they warn and return undefined/false instead of throwing. A remote session can only control server-owned objects via getVtkObject.
  • Legacy <9.5 support removed: sessionFactory, stateDecorators, and the dual-path branches in proxy/scriptLoader/remote are gone.

Naming — every promise-returning method/function under src/ is suffixed with Async; synchronous methods (e.g. VtkWasmRuntime.isAsync) are untouched. Docs and examples are updated to match.

flowchart TD
    ST["HTML script tag (global vtkWASM)"]
    BD["Bundler import"]
    AN["Annotation script tag"]
    ST --> LF["loadVtkWasmAsync(options)"]
    BD --> LF
    LF --> RT["VtkWasmRuntime — cached per url + config"]
    RT -->|createStandaloneSession| SS["StandaloneSession"]
    RT -->|createRemoteSession| RS["RemoteSession"]
    SS --> NS["session.vtk: create & render objects"]
    RS --> RW["bindNetwork + bindCanvas + update"]
    AN -.->|auto: loads + standalone session| NS
    NS --> DS["session.dispose()"]
    RW --> DS
    RT --> DR["runtime.dispose()"]
Loading

Packaging — exports collapse to . (index) + ./viewer; vite builds from src/index.js.

Docs

  • New conceptual guides: Loading VTK.wasm (with the path diagram above), Standalone Session, Remote Session — each links into the generated reference rather than restating signatures.
  • HTML Script Tag + Bundler Integration merged into Adding VTK.wasm to a Project; JS "Getting started" page removed; sidebar reordered.
  • Auto-generated API reference from source JSDoc (TypeDoc + markdown/vitepress theme), wired into the nav/sidebar (expanded by default); mermaid enabled.
  • README trimmed to onboarding essentials.

Migration

Before After
new VtkWASMLoader(); await l.load(url, cfg, name) await loadVtkWasmAsync({ url, ...cfg, wasmBaseName: name })
createNamespace(url, cfg) (await loadVtkWasmAsync({ url, ...cfg })).createStandaloneSession().vtk
new RemoteSession(); await r.load(url, cfg) (await loadVtkWasmAsync({ url, ...cfg })).createRemoteSession()
remote.sceneManager.xxx() remote.vtk / remote methods (#native is private)
import … from "@kitware/vtk-wasm/vtk" / /remote import { loadVtkWasmAsync } from "@kitware/vtk-wasm"
(none) session.dispose(), runtime.dispose(), using

Promise-returning APIs were also renamed to carry an Async suffix:

Before After
loadVtkWasm loadVtkWasmAsync
createViewer createViewerAsync
ExportViewer.load ExportViewer.loadAsync
RemoteSession.update / setSize / fetchState / fetchHash / pushHash updateAsync / setSizeAsync / fetchStateAsync / fetchHashAsync / pushHashAsync

Testing

  • npm run build — ESM + both UMD bundles build clean.
  • npm run lint — passes.
  • npm run docs:build — builds with no dead links (only the pre-existing mermaid chunk-size advisory).

jspanchu added 6 commits June 13, 2026 13:16
Replace the scattered VtkWASMLoader/createNamespace entry points with a
single loadVtkWasm() that returns a cached VtkWasmRuntime, the factory for
StandaloneSession and RemoteSession. Both sessions wrap their C++ handle as
a private #native and expose dispose()/[Symbol.dispose] for clean teardown.

- runtime.js: loadVtkWasm + VtkWasmRuntime (module cache, session factories)
- standaloneSession.js / remoteSession.js: session wrappers with lifecycle
- remoteSession: rely on user-provided canvas ids (bindCanvas) instead of a
  hard-coded internal selector; never create/remove canvas elements
- index.js: public exports + <script id="vtk-wasm"> bootstrap
- drop legacy (<9.5) support: sessionFactory, stateDecorators, dual-path
  branches in proxy/scriptLoader, and the old remote.js shims
- vite: build from src/index.js (esm "index" + umd "vtk")

BREAKING CHANGE: VtkWASMLoader, createNamespace, and the standalone/remote
entry modules are removed in favor of loadVtkWasm() and runtime.create*Session().
Update the simple-app example and the plain-javascript demos to use
loadVtkWasm({ url }).createStandaloneSession().vtk instead of the removed
VtkWASMLoader/createNamespace API.
- package.json: collapse exports to the new "." (index) + "./viewer" entry
  points; add docs:api script (run before docs:dev/docs:build)
- add TypeDoc (+ markdown/vitepress theme) and mermaid dev deps
- typedoc.json + tsconfig.json: generate the API reference from source JSDoc
  into docs/api (allowJs, no TS migration)
- gitignore the generated docs/api output
- new conceptual guides: Loading VTK.wasm (with a mermaid path diagram),
  Standalone Session, and Remote Session, each linking into the generated
  API reference rather than restating signatures
- merge HTML Script Tag + Bundler Integration into "Adding VTK.wasm to a
  Project"; remove the JS Getting started page
- reorder the "For JavaScript developers" sidebar and repoint inbound links
- wire the TypeDoc-generated API reference into the nav/sidebar (expanded by
  default) and enable mermaid via vitepress-plugin-mermaid
The C++ vtkRemoteSession has no create/destroy, so creating or deleting
objects through a remote session's vtk proxy would throw. Gate both on
typeof checks: warn and return undefined/false instead. Document that a
remote session can only control server-owned objects via getVtkObject.
@jspanchu jspanchu changed the title Feat/js loader session api Redesign the VTK.wasm JavaScript API + docs overhaul Jun 13, 2026
@jspanchu jspanchu requested a review from jourdain June 15, 2026 14:46
@jspanchu

Copy link
Copy Markdown
Member Author

fyi @ansbbrooks your input will be much appreciated.

Comment thread docs/guide/js/integration.md
Comment thread docs/.vitepress/config.mjs
Comment thread docs/guide/js/remote-session.md
Comment thread docs/guide/js/remote-session.md Outdated
Comment thread docs/guide/js/remote-session.md
Comment thread src/remoteSession.js Outdated
@ansbbrooks

ansbbrooks commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

I love that this refactor takes advantage of the new using declaration.

However, can we add the Async prefix to all public API methods that return a Promise (or any then-able for that matter)? It makes a huge difference for me when gaining intuition about a method's behavior when there is an Async suffix. I instantly know the method should be awaited without having to do a deep dive into its source to see if it returns a Promise or not. I'm sure you have seen that before in other libraries and frameworks. For example, it's actually a policy in ASP.NET that all async methods must have the Async suffix--I think it would be a good idea to do that here while there is a redesign underway. https://github.com/dotnet/aspnetcore/wiki/Engineering-guidelines#async-method-patterns

By default all async methods must have the Async suffix. There are some exceptional circumstances where a method name from a previous framework will be grandfathered in.

In Trame for example, there are several methods that return Promises that you wouldn't know unless you imported the type or your IDE happened to notice it:

Trame.connect(): Promise<void> // would be nice if it were "connectAsync()"
WebsocketSession.close() : Promise<void> // would be nice if it were "closeAsync()"

In this PR:

loadVtkWasm(): Promise<VtkWasmRuntime> // should be "loadVtkWasmAsync()"
createViewer(): Promise<ExportViewer> // should be "createViewerAsync()"
createScriptURL(): Promise<string> // should be "createScriptURLAsync()"

@jspanchu

Copy link
Copy Markdown
Member Author

I love that this refactor takes advantage of the new using declaration.

However, can we add the Async prefix to all public API methods that return a Promise (or any then-able for that matter)? It makes a huge difference for me when gaining intuition about a method's behavior when there is an Async suffix. I instantly know the method should be awaited without having to do a deep dive into its source to see if it returns a Promise or not. I'm sure you have seen that before in other libraries and frameworks. For example, it's actually a policy in ASP.NET that all methods must have the Async suffix--I think it would be a good idea to do that here while there is a redesign underway. https://github.com/dotnet/aspnetcore/wiki/Engineering-guidelines#async-method-patterns

By default all async methods must have the Async suffix. There are some exceptional circumstances where a method name from a previous framework will be grandfathered in.

In Trame for example, there are several methods that return Promises that you wouldn't know unless you imported the type or your IDE happened to notice it:

Trame.connect(): Promise<void> // would be nice if it were "connectAsync()"
WebsocketSession.close() : Promise<void> // would be nice if it were "closeAsync()"

In this PR:

loadVtkWasm(): Promise<VtkWasmRuntime> // should be "loadVtkWasmAsync()"
createViewer(): Promise<ExportViewer> // should be "createViewerAsync()"
createScriptURL(): Promise<string> // should be "createScriptURLAsync()"

sounds reasonable to me. i like this idea.

jspanchu added 2 commits June 15, 2026 19:01
RemoteSession can now bind a render window to a canvas element passed
directly, not just by DOM id. When the Emscripten build exposes
specialHTMLTargets, the element is registered there so it needs neither an
id nor to be attached to the document; otherwise a CSS selector built from
the element id is used as a fallback.

- patch the glue source to expose Module.specialHTMLTargets
- pass the module into RemoteSession and track the canvas->target mapping
  (canvasTargets) plus bound render windows (boundRenderWindows)
- bind render windows lazily during update() once their state arrives, and
  clean up specialHTMLTargets entries on unbindCanvas/dispose
Rename every method and function under src/ that returns a promise so its
name ends in `Async`, leaving synchronous methods (e.g. VtkWasmRuntime.isAsync)
untouched. Docs and examples are updated to match.

BREAKING CHANGE: the following promise-returning APIs were renamed:
- loadVtkWasm        -> loadVtkWasmAsync
- createViewer       -> createViewerAsync
- RemoteSession.update/setSize/fetchState/fetchHash/pushHash gain the
  `Async` suffix (updateAsync, setSizeAsync, fetchStateAsync, fetchHashAsync,
  pushHashAsync)
- ExportViewer.load  -> loadAsync

Callers must update to the new names.
@jspanchu

Copy link
Copy Markdown
Member Author

@ansbbrooks i pushed two more commits. one enables direct html canvas, second adds an "Async" suffix to promise returning methods.

@jourdain please review.

@jspanchu jspanchu requested review from ansbbrooks and jourdain June 15, 2026 23:05
Comment thread src/runtime.js
Comment on lines +37 to +42
function exposeSpecialHTMLTargets(buffer) {
const text = new TextDecoder().decode(buffer);
if (text.includes(SPECIAL_TARGETS_PATCH) || !text.includes(SPECIAL_TARGETS_DECL)) {
return buffer;
}
return new TextEncoder().encode(text.replace(SPECIAL_TARGETS_DECL, SPECIAL_TARGETS_PATCH)).buffer;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread src/viewer.js
this.remoting.sceneManager.bindRenderWindow(rwId, selector);
this.remoting.sceneManager.startEventLoop(rwId);
const target = this.remoting.bindCanvas(rwId, canvas);
this.remoting.native.bindRenderWindow(rwId, target);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shoud this.remoting.bindCanvas(rwId, canvas); and this.remoting.native.bindRenderWindow(rwId, target); be only one line?

Comment thread src/remoteSession.js

this.canvasTargets.set(rwId, { canvas, target });
addCanvasEventListeners(canvas);
return target;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why don't we bind the target on the native side right now? Is it because the rw may not be available yet?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants