Repository navigation
Track upstream CM PR 686 (JS-API redesign): post-A10 alignment watchlist #115
Description
Upstream PR: WebAssembly/component-model#686 (eqrion; refs kept
non-linkifying in this issue deliberately — no backlink noise upstream).
Originally a WebIDL web-embedding draft. The 2026-08-21 iteration
("Redesign as a JS-API", commit 02b704ad) abandoned the WebIDL binding
approach — author: "I'm less convinced now that specifying this in terms
of WebIDL is the right approach" — and replaced it with a JS-API
(design/mvp/JS-Explainer.md + JS-Reference.md): imports/exports
defined in terms of JS values, with WebIDL-style conversion rules
designed so engines can fuse conversions without speculation. A Gecko
prototype sits behind it. It remains the presumptive shape of eventual
native support, so divergences between it and our embedder API are
migration surface.
This issue is tracking only. The PR is still a draft ("not quite
ready for review but is closer now") — no upstream feedback goes out from
here. Revisit when the author calls for review or the draft visibly
stabilizes.
Amendment A10 (#114) took the two cheap alignments against the old
draft: exception naming and { kind, value } variant dictionaries. The
redesign kept { kind, value } but renamed the exception class — row 2.
Closed by the redesign (resolved our way)
| Surface | polyengine | redesigned draft |
|---|---|---|
| s64/u64 (was row 4) | bigint |
BigInt, both directions |
list<u8> (was row 5) |
Uint8Array, always a copy |
Uint8Array on lift; lower iterates element-wise (no BufferSource fast path yet) |
own disposal (was row 6) |
[Symbol.dispose]/drop() + FinalizationRegistry backstop |
@@dispose on the prototype + FinalizationRegistry backstop; borrow invalidation via per-call scopes |
Watchlist
| # | Surface | polyengine (embedder-api.md) | JS-API draft | Disposition |
|---|---|---|---|---|
| 1 | option-typed values, none case | lift emits absent record field / undefined; lower accepts absent/undefined, rejects null (platform-class pin #2: null takes the some branch, inner conversion fails) |
lift emits null (record fields present, null-valued); lower accepts both null and undefined as none |
Divergence confirmed and hardened — upstream kept WebIDL-nullable semantics even after dropping WebIDL. 2026-08-30: rename-scale decisions deferred, including the independent option of accepting null at lower (would erase pin #2's footgun; host-ABI behavior change: conventions goldens + breaking/protocol). The ?? undefined bridge stays documented. |
| 2 | exception class | ComponentException<E> extends Error, payload, ctor (payload, message?) |
ComponentError : Error, payload attribute, ctor (message, payload), exposed as WebAssembly.ComponentError |
Shape converged to ours (DOMException derivation gone). The name churned away from the A10 alignment. 2026-08-30: rename deferred — breaking runtime+protocol event (brand key polyengine.componentException/1, isComponentException, consumer migration) and the name has churned once already. Take it when the draft stabilizes. |
| 3 | instance import/export shape | two-level record keyed by verbatim fully-versioned interface id | single-level objects keyed by camelCase JSName; interfacename imports (wasi:http/handler@1.0.0) rejected with TypeError; ESM-integration resolves specifiers via external-id |
Now concrete but plainly interim: upstream cannot express a wasi world through the imports object at all, so the eventual shape is unsettled. Member-level camelCase matches ours. Still the watch-closely row. |
| 4 | variant dictionary | { kind, value } (A10) |
{ kind, value } — survived the redesign; option<option<T>> boxes into the variant family, same nesting rule as ours |
Aligned, stronger than before. Residual nuance: draft lifts variants/records/flags as null-prototype objects; ours plain. Residual churn risk unchanged (re-migration accepted, A10 note). |
| 5 | streams/futures | Stream<T>/Future<T> handles (cancel/drop expressible) |
still TODO (future<T> → Promise, stream<T> → ReadableStream sketched) |
Nothing to align to. Our Stream.readable(): ReadableStream would not be alien to that direction. |
| 6 | error conversion at the import boundary | branded ComponentException required; unbranded throw = host bug = trap (Error model: deliberate inversion of jco) |
any thrown JS value is converted to E via ToComponentValue; unconvertible → trap |
New row. Class-level semantic divergence in the error model; a point to raise when upstream opens for review. Hold ours. |
| 7 | map<K, V> |
despecialized [K, V][] (C2 amendment) |
Map (SameValueZero, last-wins duplicate keys; lower accepts Map / iterable of pairs / plain object for string keys) |
New row. Wait — draft-unstable, and Map is a plausible landing spot we could follow later. |
| 8 | argument/value coercion | strict at lower (range-checked, type-checked) | WebIDL-loose: ToString/ToNumber-[EnforceRange]/ToBoolean coercions (greet(42) → "hello, 42"); extra args ignored, too-few TypeError |
New row. Pervasive divergence class across all primitives. Hold ours (strictness is a stated principle). |
| 9 | resource import protocol | platform-class pattern (non-normative subsection + runtime/tests/embedder/platform_class_test.ts): constructor satisfies type, per-call self[camelCase(member)] dispatch |
the same pattern, normative: constructor satisfies the type import, tagged [method]/[static] imports read off it and its .prototype, brand check via new well-known symbol @@isWasmResourceOf (synthesized instanceof fallback; WebIDL interface objects get the symbol) |
Convergence in our direction. @@isWasmResourceOf is a potential future adoption surface for our host-resource brand checks. Watch. |
| 10 | label mangling, acronym rule | camelCase members (get-resolution → getResolution); case labels kebab-case verbatim as data |
camelCase/PascalCase with acronym-preserving Capitalize (inner-HTML → innerHTML, XML-http-request → XMLHttpRequest); enum/variant labels verbatim as data |
Data-label treatment already aligned. To verify: our bindgen's kebab→camel on acronym-bearing labels vs the draft's rule. Minor. |
Re-check triggers
- The author calls for review, or new iterations push (still "needs a
couple more iterations"; the redesign itself is "not quite ready"). - The Mozilla prototype (rebased onto the JS-API) landing/shipping in
Nightly. - The
interfacename-imports rejection being replaced with a real shape
(row 3's structural risk). - The draft's "Fusing component value conversions" section (currently
TODO) being filled in — a signal the conversion semantics are freezing.
Refs: contracts/embedder-api.md (A10 status entry + terminology note +
Error model + platform-class subsection), PR #114. Empirical pins: see
the platform-class comment below.
Activity
lannbot commentedon Aug 12, 2026
Empirical input for the watchlist, now merged (#117): the zero-glue platform-class pattern is documented (contracts/embedder-api.md §Resources, non-normative subsection) and pinned executable (runtime/tests/embedder/platform_class_test.ts). Two pins bear directly on rows here:
nullwhere WIT saysoption<T>is NOT none; the call fails with the conversion layer's TypeError naming the import. The draft's nullable mapping (option<T>→T?) would accept it. This is the concrete interop cost if upstream keeps nullable and we keep undefined-for-none; the one-line bridge (?? undefined) is documented.[constructor]/[method]imports — whatever shape upstream's instance-import TODO lands on, this test is where the delta will surface first.Also pinned for future wat authors: wasmparser's
all_valtypes_named_in_funcrequires records referenced by imported functions to be named type exports of the importing instance.[-]Track WebAssembly/component-model#686 (web-embedding draft): post-A10 alignment watchlist[/-][+]Track upstream CM PR 686 (JS-API redesign): post-A10 alignment watchlist[/+]lannbot commentedon Aug 30, 2026
Re-check trigger fired: the upstream PR was redesigned (2026-08-21,
"Redesign as a JS-API",
02b704ad) — WebIDL binding approach abandonedfor a JS-API with fusable WebIDL-style conversion rules. Body rewritten
against the new draft; summary of the delta:
list<u8>→Uint8Array,disposal →
@@dispose+ FinalizationRegistry. Three of the old"hold ours, expect the draft to move" rows moved.
{ kind, value }variants survived the redesign, and itsnested-option boxing matches our option rule exactly.
WebAssembly.ComponentError : Errorwithpayload— our shape, no longer our name (A10 adoptedComponentExceptionfrom the old draft). Rename deferred(2026-08-30): breaking runtime+protocol, and the name has churned
once already.
nullon lift /null-or-undefinedon lower — WebIDL-nullable semantics kept even withoutWebIDL. Also deferred: independently accepting
nullat lower(would erase platform-class pin experiment-mosh composed-client deep E2E (the jco#14 row) #2's footgun; host-ABI behavior
change, conventions goldens +
breaking/protocol).concrete but interim — camelCase
JSNamekeys,interfacenameimports rejected outright, ESM
external-idfor specifiers. A wasiworld is currently inexpressible via the imports object upstream.
Econversion vs our branded-only errormodel;
map<K,V>→Mapvs our[K,V][]; WebIDL-loose coercion vsour strict lower;
@@isWasmResourceOf(our platform-class patternmade normative, with a brand-check symbol we could adopt later).
Still tracking only; no upstream feedback from here. Contract
annotation refresh in the companion docs PR.
contracts: refresh upstream JS-API draft annotations after its redesi…
Merge pull request #253 from polymorph-components/cm686/jsapi-redesig…