Replace hand-built JS DOM binding plumbing in blitz-vibey-script with typed class layers and a per-instance sized own-data registry - #814
Conversation
|
I am not sure what form Boa will take to enhance the ability to describe inheritance hierarchies on the Rust side, but this approach should facilitate future migration. |
|
|
|
And I'm also not entirely sure if this counts as being somewhat over-engineered. |
0386987 to
c3740aa
Compare
blitz-vibey-script DOM bindings onto the Extended<T> layer scheme with sized own-data slotsblitz-vibey-script with typed class layers and a per-instance sized own-data registry
|
|
|
|
…r scheme Port the `Extended<T>` inheritance design from blitz-boa-gui: each DOM interface is an `ExtendLayer` whose own data lives in a per-layer Symbol slot, prototypes are linked via `link_prototype`, and node wrappers are built from their layer chain with `from_chain!`. - Add `shared/` infrastructure: `extends.rs` (Extended<T>/Super/ Constructed/layer chains), error macros, member-definition macros and native-function helpers - Convert Node, CharacterData, Element, Document, Event, CSSStyleDeclaration and ComputedStyle to layers; `new Event()` is now a real constructor and dispatch state (currentTarget, flags) lives in the EventLayer own block - Replace the hand-built prototype objects (DomProtos, init_protos, NodeRef, define_method/define_accessor) with class registration; node_wrapper keeps its identity cache and builds via from_chain!
Replace the per-layer Symbol slots with a single per-instance `OwnDataRegistry`: one `GcRefCell` slot per real layer, addressed by the compile-time `OwnBlock::DEPTH`/`IDX` layout (ported from napi-blitz/crates/napi-inherit). The registry takes over the instance's native data slot; there are no Symbols and no per-layer JS objects. - `with_own`/`with_own_mut`/`set_own_block` become pure Rust-side slot borrows + `TypeId` downcasts and no longer take `&mut Context`; all DOM accessor call sites drop the context argument accordingly - `OwnSlot` carries `Any` downcasting on a blanket impl (not the trait itself) so `dyn OwnSlot: OwnSlot` holds; slot access goes through `Option::as_deref` to reach the trait object directly - taking `&Box<dyn OwnSlot>` would resolve `as_any_ref` to the blanket impl on the box itself and break every downcast - `Extended<T>` keeps only its class-handle role; `own_symbol` and `wrap_own` are gone
The slot list is sized once at attach time and never resized, so a boxed slice carries the same heap layout as a Vec while dropping the capacity field from the registry header. Also tune `#[inline]` placement: drop it from the generic accessors and drivers (monomorphized bodies are already visible to callers) and add it to the non-generic tiny members (`SuperDone::this`, the `RootLayer` chain terminators); `EmitOwn::Chain` now precedes its methods.
- Add the EventTarget layer as Node's parent; add/removeEventListener move there from Node - Add the event class layers (UIEvent, MouseEvent, PointerEvent, WheelEvent, KeyboardEvent, InputEvent) built per DomEventData variant - Split CharacterData/Text/Comment out of node.rs; text/comment wrappers now satisfy instanceof Text/Comment - Give ExtendLayer::build a default 'Failed to construct ...: Illegal constructor' implementation and drop the handwritten ones
- Add DispatchTarget (None / Direct / Callable with cached resolve): event construction no longer materializes target/currentTarget wrappers; they are built lazily on first getter read through the shared wrapper cache - Move per-event dispatch state (target, currentTarget, phase, canceled, stopPropagation flags) into EventLayer's GcRefCell<EventState> block - Store listeners in the EventTargetLayer own block instead of the global node_listeners map, making new EventTarget() a standard, working target; add the standard dispatchEvent method - Drive the DOM chain walk in three phases (capture / target / bubble) with real eventPhase values and reset the transient state afterwards - Report phase-plan-per-receiver via a DispatchStep; keep the vibey-side method bodies (options parsing, once handling, error reporting, on<event> handlers, change synthesis) unchanged - Add tests/events.rs for the event class layers (no DOM involvement, results reported via __blitz_send_message) and DOM-dispatch tests in tests/dom.rs
c3740aa to
2ae103c
Compare
- ListenerEntry.callback becomes ListenerCallback: Function,
HandlerObject { obj, handle_event } (an addEventListener'd event-listener
object), and AttributeFunction (the on<event> handler, always a function)
- add/removeEventListener register and match only the first two shapes,
comparing by shape + JS identity; invocation binds `this` per shape —
the listener object for handleEvent, currentTarget otherwise
- on<event> attribute handlers ride the same listener list through the
new set/remove_attribute_listener pair; the dispatcher drops its
dynamic on<event> read, and the property getter reflects the
registered handler (null once cleared)
…bleRef and GC-driven finalizers Track node wrappers in a NodeWrappers cache keyed by NodeId, holding each entry as a SwitchableRef enum of JsObject (strong, while the node is in the document) and WeakJsObject (weak, once detached), mirroring napi-blitz's NodeCache and SwitchableRef. A manual Finalize impl on NodeLayer carries the finalizer logic: each wrapper's native data holds Weak handles to the runtime state and base document (captured via Rc::downgrade at construction), so when the GC collects an unreachable wrapper the finalizer clears the cache entry and, if no live descendant keeps the subtree alive, drops the detached node storage via remove_and_drop_node. Wire strength switching into every DOM mutation entry point (append_child, insert_before, remove_child, remove, replace_child, replace_with, set_text_content, set_inner_html, append, prepend, before, after, replace_children) and port the SharedDocument methods as DomCtx helpers: is_in_document, make_subtree_strong, make_subtree_weak, make_in_document_subtree_strong, make_in_document_subtree_weak and detach_children. Add ScriptDocument::run_gc (boa_gc::force_collect), pin boa to 10ef23505ecae734a4984d7839fec99a7ca8482c via [patch.crates-io], and relax layer_chain!'s repetition operators so an empty layer list is accepted. Add tests/gc.rs covering both GC survival and reclamation: listeners and expando identity surviving forced GCs on in-document nodes, weak wrappers revived by re-attachment, detached subtrees kept alive by a live descendant, every mutation entry point switching entry strength both ways, and FinalizationRegistry cross-checks (the cleanup-callback test is #[ignore]d until the executor keeps its pending future alive).
2ae103c to
a56f7ed
Compare
# Conflicts: # packages/blitz-vibey-script/src/dom/mod.rs
07ded4c to
2ad0adf
Compare
|
Pinned Boa to 10ef23505ecae734a4984d7839fec99a7ca8482c |
…gation, once and handleEvent
- `node.dispatchEvent()` now walks the full capture/target/bubble chain over
the node's DOM ancestors (and the window listeners for bubbling events)
through the shared `dispatch_event_on_chain` walk; a plain `EventTarget`
still dispatches to itself only
- `stopImmediatePropagation()` takes effect everywhere: between the capture
and non-capture flavors on the same receiver, at the top of each listener
loop (a listener that halted the dispatch no longer lets the next one run),
and across the window-listener loop
- Capture-registered listeners on the target run before the non-capture ones
in `dispatchEvent`, matching the DOM-walk target phase
- Dispatching an event that is already being dispatched throws an
`InvalidStateError` DOMException; the previously write-only `dispatching`
flag is now read
- `once` listeners are removed right before their call instead of in a
pre-dispatch sweep, so a listener halted by `stopImmediatePropagation`
stays registered and fires on the next dispatch; `window.addEventListener`
now parses the `{ once }` option (it was hardcoded to `false`)
- A listener object's `handleEvent` is read off the object at dispatch time
instead of being cached at registration, so a replacement takes effect
- The dispatch walk no longer materializes wrappers for chain receivers
without a cached wrapper: listeners live in the wrapper's own block, so
such nodes have nothing to invoke
New tests cover chain propagation, flavor ordering, cross-flavor halting,
reentrant rejection, once survival, and handleEvent replacement.
# Conflicts: # packages/blitz-vibey-script/src/dom/node.rs # packages/blitz-vibey-script/src/state.rs
- The global object is born in the realm's host hooks (`ScriptHooks::create_global_object`) from pure layer data via the new `HostGlobal` trait: `WindowLayer::host_global` attaches the own-data registry and fills the Root -> EventTarget -> Window chain through `host_fill`. `Window` is now `Extended<WindowLayer>` with every member defined by `define_members` on the class prototype, replacing runtime.rs's flat `register_global_*` registry (timers, viewport, location/navigator, CSS, `__blitz_*`, etc.) - `HostGlobal` is an explicit opt-in for host-owned globals, implemented by `WindowLayer` only: it leaves `EmitOwn` untouched, and its provided `host_global` constructs the registry inside shared, so no private API leaks - `location`/`navigator` are [SameObject] getters backed by the window's own block, built from the runtime's base URL on first access in the current realm - `addEventListener`/`removeEventListener` follow WebIDL semantics: an interface operation called with no receiver (or detached) receives `undefined` as `this` and binds the global this (the window). The binding lives in the base class's `add_event_listener` / `remove_event_listener`, so the window inherits the methods instead of defining its own wrappers - The `on<event>` attribute definition moves from dom/node.rs into the generic `define_on_event_attributes(types)` in event_target.rs, each class supplying its own event-type set (Node, Window) - Drop the stale `window_listeners` bookkeeping from state.rs (window listeners live in its EventTarget own block) and the whole `register_global_*` surface in runtime.rs New tests cover the window as the global object and event target, prototype members, [SameObject] location/navigator, bare and detached listener binding, timers and window event dispatch.
7be4cb4 to
0ebc54c
Compare
A bare or detached dispatchEvent call sees a nullish this; substitute the global object per the ordinary-call this rule, as the listener registration/removal paths already do, instead of failing on `undefined`. Also drops the `crate::shared::` prefix on `native_error!` uses and adds a window.rs test for the detached dispatch.
make_weak no longer swaps the cache entry in place while a RuntimeState or document borrow may be alive; Strong entries are queued (pending_weak) and switched at flush_wrapper_switches, called at the end of each JS entry point (execute_scripts/eval/dispatch_dom_event/handle_ui_event/poll) where neither RefCell is borrowed. The checkpoint then runs force_collect and reclaim_detached_node, taking over the cleanup that used to run from NodeLayer's Finalize impl - now removed along with its state/doc back-refs. This keeps boa_gc's synchronous collections (triggered by WeakJsObject::new allocation or the checkpoint's force_collect) from re-entering borrowed RefCells, eliminating the "RefCell already borrowed" panics. WPT full run: crashes 386 -> 22, all remaining are the unrelated BorrowMutError family in boa's property setters.
Summary
This rewrites the JS DOM class definitions in
blitz-vibey-scriptonto a typed layer ("ExtendLayer") scheme: every DOM interface is a layer whose own data lives in a single per-instance sized-slot registry, prototypes are linked with ES class semantics, and own-block access is a plain Rust-side slot borrow plus aTypeIddowncast. Everything is internal toblitz-vibey-script; the DOM/JS behavior surface is unchanged (covered by the existingdom.rs/preact.rsintegration tests).Design
Layers
Each interface is an
ExtendLayerchained through a compile-timeParenttype, with its own data behind an accessor:Node=Extended<NodeLayer { node_id }>(root layer);CharacterData/Element/DocumentextendNodeLayerEvent=Extended<EventLayer>: configuration (type,target,bubbles,cancelable) and dispatch flags (prevented,stopped,currentTargetviaGcRefCell) live in the own blockCSSStyleDeclaration/ComputedStylecarry the styled node idPrototypes are wired with
link_prototype(child.prototype.__proto__ = parent.prototype,child.constructor.__proto__ = parent.constructor).Node,Document,Element,CharacterDataandEventare registered classes, sonew Event(type, init)is a working constructor,instanceof Node/Element/CharacterDataanswers truthfully through the linked prototype chain (HTMLElementaliasesElement), andon<event>IDL properties live on theNodeprototype.Node wrappers are built from their layer chain via
from_chain!, backed by theRuntimeState::node_wrappersidentity cache.OwnDataRegistryEach instance's native data slot holds an
OwnDataRegistry(Vec<GcRefCell<Option<Box<dyn OwnSlot>>>>), sized at attach time by the leaf layer's compile-timeOwnBlock::DEPTH; each layer addresses its slot withOwnBlock::IDX. The design is ported from blitz-boa-demo.with_own/with_own_mut/set_own_blockoperate purely on the registry: slot borrows +TypeIddowncasts, entirely on the Rust side. Wrapper construction fills the registry slots throughfrom_chain!.Box<dyn OwnSlot>withTraceon the trait, soJsValues inside layers (e.g.EventLayer.target) stay reachable. Slot access goes throughOption::as_derefto reach&dyn OwnSlotdirectly — resolvingas_any_refon&Box<dyn OwnSlot>would hit the blanket impl on the box itself and break every downcast (the blanket impl also applies to the box, sinceBox<dyn OwnSlot>: Any + Trace)Tested
cargo test -p blitz-vibey-script: 21/21dom.rs, 2/2preact.rs(real Preact render + todo interaction), doctests passcargo check --workspacecleancargo build -p browser --features javascriptbuildsWPT results
1 newly passing, 15 newly failing (net -14), 23 other status changes.
Full diff (39 changed tests)
Generated by the WPT workflow.