Skip to content

Kernel

wybthon.kernel

kernel

Batched DOM command buffer and rendering backends.

This module is the single point of contact between Wybthon's renderer and the real DOM. Instead of calling DOM APIs directly (each call crosses the Python-to-JS bridge in Pyodide), the reconciler and prop appliers emit compact operations into a buffer. At well-defined commit points (end of a render, end of an effect flush) the whole buffer is serialized once and handed to a small JavaScript kernel that applies every operation natively. A mount of a 1,000-row table becomes one bridge crossing instead of tens of thousands.

Core concepts:

  • Node handles. DOM nodes are referred to by integer ids allocated on the Python side. The kernel keeps an id -> Node registry, so no JsProxy objects flow through the hot path.
  • Ops. Each operation is a small tuple, (opcode, ...args), serialized as a JSON array. See the OP_* constants.
  • Backends. BrowserBackend drives the real DOM through the embedded JS kernel. PythonBackend is a reference interpreter that applies the same ops to any DOM-like stub document; it backs the unit tests and the stubbed benchmark so both exercise the exact protocol the browser sees.
  • Hydration. CLAIM_* ops adopt server-rendered nodes in document order instead of creating them. Claims are tolerant: a node that doesn't match is created in place, and HYDRATE_END removes server nodes nobody claimed.
  • Events. Event delegation lives in the kernel: one native listener per event type walks the ancestor chain natively and calls into Python once for the matching bubbling route with a JSON payload. See wybthon.events for the Python half.

Application code never imports this module directly; it's plumbing for the reconciler, wybthon._dom_props, and wybthon.events.

Classes:

Name Description
BrowserBackend

Backend that drives the real DOM through the embedded JS kernel.

PythonBackend

Reference op interpreter over a DOM-like stub document.

Functions:

Name Description
commit

Flush all queued ops to the backend in one crossing.

set_backend

Install a rendering backend (tests pass a PythonBackend).

reset

Test helper: clear the op buffer, id counters, and template registry.

BrowserBackend

BrowserBackend()

Backend that drives the real DOM through the embedded JS kernel.

Created automatically on first use inside Pyodide. Every apply serializes the op list to JSON and makes exactly one call across the bridge.

Methods:

Name Description
apply

Serialize ops to JSON and apply them in one kernel call.

get_node

Return the raw DOM node registered under node_id.

adopt

Return an existing handle or register the node under node_id.

query

Return the canonical handle of a selector match, or None.

supports_html

The browser can always parse template HTML.

set_dispatcher

Install the Python event dispatcher as the kernel's callback proxy.

current_event

Return the native event currently being dispatched, or None.

take_state

Remove and return the text of the server state script under node_id.

replay

Replay input the bootstrap recorded before hydration.

apply

apply(ops: List[Any]) -> None

Serialize ops to JSON and apply them in one kernel call.

get_node

get_node(node_id: int) -> Any

Return the raw DOM node registered under node_id.

adopt

adopt(node_id: int, node: Any) -> int

Return an existing handle or register the node under node_id.

query

query(node_id: int, selector: str) -> int | None

Return the canonical handle of a selector match, or None.

supports_html

supports_html() -> bool

The browser can always parse template HTML.

set_dispatcher

set_dispatcher(fn: Callable[[int, str, str], int]) -> None

Install the Python event dispatcher as the kernel's callback proxy.

current_event

current_event() -> Any

Return the native event currently being dispatched, or None.

take_state

take_state(node_id: int) -> Any

Remove and return the text of the server state script under node_id.

replay

replay() -> int

Replay input the bootstrap recorded before hydration.

PythonBackend

PythonBackend(document: Any)

Reference op interpreter over a DOM-like stub document.

Mirrors the JS kernel's semantics against plain Python objects that quack like DOM nodes (the test suite's StubNode and the benchmark runner's _Node). Unit tests and the stubbed benchmark run the real wire protocol through this class, so protocol bugs surface without a browser.

Methods:

Name Description
apply

Interpret a batch of ops against the stub document.

get_node

Return the stub node registered under node_id, or None.

adopt

Return an existing handle or register this stub node.

query

Return the canonical handle of a selector match, or None.

supports_html

Whether the stub document parses <template> innerHTML.

set_dispatcher

Install the Python event dispatcher used by dispatch.

current_event

Return the raw event passed to the in-flight dispatch, or None.

take_state

Remove and return the text of the server state script under node_id.

replay

Replay queued_events (recorded "before hydration") through delegation.

roots

Return the registered delegation roots (test helper).

dispatch

Simulate a bubbling native event dispatch for tests.

apply

apply(ops: List[Any]) -> None

Interpret a batch of ops against the stub document.

get_node

get_node(node_id: int) -> Any

Return the stub node registered under node_id, or None.

adopt

adopt(node_id: int, node: Any) -> int

Return an existing handle or register this stub node.

query

query(node_id: int, selector: str) -> int | None

Return the canonical handle of a selector match, or None.

supports_html

supports_html() -> bool

Whether the stub document parses <template> innerHTML.

set_dispatcher

set_dispatcher(fn: Callable[[int, str, str], int]) -> None

Install the Python event dispatcher used by dispatch.

current_event

current_event() -> Any

Return the raw event passed to the in-flight dispatch, or None.

take_state

take_state(node_id: int) -> Optional[str]

Remove and return the text of the server state script under node_id.

replay

replay() -> int

Replay queued_events (recorded "before hydration") through delegation.

roots

roots() -> list[Any]

Return the registered delegation roots (test helper).

dispatch

dispatch(event_type: str, target: Any, raw_event: Any = None, payload: Optional[dict] = None) -> None

Simulate a bubbling native event dispatch for tests.

Walks the stub-node ancestor chain from target, invoking the Python dispatcher for every registered (node, event_type) handler, exactly like the JS kernel's root listener.

alloc_id

alloc_id() -> int

Allocate one fresh node id.

alloc_ids

alloc_ids(count: int) -> int

Allocate a dense block of count ids; returns the first id.

register_template

register_template(shape: Any) -> int

Register shape's skeleton with the backend and return its template id.

The registration op travels in the same batch as the clone that needs it, so no extra bridge crossing occurs. The oldest template is retired once more than _TEMPLATE_LIMIT are live; its shape registers again on its next mount.

emit

emit(op: Any) -> None

Queue one op tuple for the next commit.

commit

commit() -> None

Flush all queued ops to the backend in one crossing.

No-op when the buffer is empty. Safe to call at any time; the renderer calls it at the end of render, at the end of every effect flush, and before any synchronous DOM read.

stats

stats() -> dict[str, int]

Return node, template, listener, root, and hydration-mismatch counts.

take_state

take_state(node_id: int) -> Optional[str]

Remove and return the serialized server state inside node_id, if any.

Server renders end with a <script type="application/json" data-wyb-state> element. Commits pending ops first so the node exists.

replay_events

replay_events() -> int

Replay input recorded before hydration through the delegated handlers.

Returns the number of events replayed.

get_node

get_node(node_id: int) -> Any

Return the raw DOM node for node_id, committing pending ops first.

adopt

adopt(node: Any) -> int

Register an existing raw DOM node and return its new id.

query

query(selector: str) -> Optional[int]

Resolve a CSS selector to a node id, or None when nothing matches.

Commits pending ops first so the selector can match nodes created earlier in the same logical update.

supports_html

supports_html() -> bool

Return whether the backend can parse templates (OP_REGISTER_TPL).

current_event

current_event() -> Any

Return the native event being dispatched right now, or None.

Only valid synchronously inside an event handler; used as the escape hatch behind DomEvent.raw.

set_event_dispatcher

set_event_dispatcher(fn: Callable[[int, str, str], int]) -> None

Install the Python-side event dispatcher (called by wybthon.events).

set_backend

set_backend(backend: Any) -> None

Install a rendering backend (tests pass a PythonBackend).

Clears the template registry: a fresh backend has no registered skeletons, so they must be re-sent on next use.

reset

reset(backend: Optional[Any] = None) -> None

Test helper: clear the op buffer, id counters, and template registry.

kernel_source

kernel_source() -> str

Return the JavaScript kernel's source.

What's in this module

kernel is the single point of contact between the renderer and the real DOM. The reconciler, the compiled template mounts, the prop appliers, and the event system emit compact ops (JSON-serializable tuples against integer node ids) into a buffer; commit() hands the whole buffer to the active backend in one Python-to-JS bridge crossing. The browser half lives in _kernel.js, shipped with the package. Application code never imports this module; it matters when you're writing low-level tests against the stub backend or debugging the wire protocol. For component tests, use wybthon.testing.

Name Description
commit Flush every queued op to the backend in one crossing (no-op when empty).
BrowserBackend Drives the real DOM through the JS kernel; created automatically in Pyodide.
PythonBackend Reference interpreter applying the same ops to a DOM-like stub document (used by wybthon.testing, the unit tests, and the stubbed benchmark).
set_backend Install a backend (tests pass a PythonBackend).
reset Test helper: clear the op buffer, id counters, and template registry, optionally installing a backend.

Two more module-level hooks matter to tests. register_template(shape) registers the static skeleton of a compiled html template in the same batch as its first clone. html_templates (True, False, or None for "ask the backend") gates template mounting; set it to False to force per-node commands.

Wire protocol

Each op is a JSON array whose first element is the opcode. Node ids are allocated on the Python side, so no JsProxy objects flow through the hot path; None anchors mean "append".

Op Payload Effect
CREATE_ELEMENT id, tag document.createElement
CREATE_ELEMENT_NS id, namespace, tag document.createElementNS (SVG, MathML)
CREATE_TEXT id, text document.createTextNode
CREATE_COMMENT id[, data] Comment marker (fragment and hole anchors); Loading boundaries carry a key in data during server rendering and hydration
REGISTER_TPL tpl_id, html, count, [text offsets], [[offset, event_type], ...] Parse a template's or shape's skeleton once via <template>, and record its text slots and delegated listeners
CLONE first_id, tpl_id, parent_id, anchor_id, *texts Clone the prototype, register a dense id block in pre-order, fill the text slots, mark the delegated listeners, and insert before the anchor
INSERT parent_id, id, anchor_id insertBefore (None anchor appends)
REMOVE id Detach from the parent; the node stays registered
MOVE_RANGE parent_id, first_id, last_id, anchor_id Move a contiguous mounted range
DISPOSE_RANGE first_id, last_id Remove a contiguous range of siblings and release every registered node inside it, walking the removed nodes natively
DISPOSE id Remove one node and release every registered node inside it
HOLE_TEXT id, text Reuse a hole's placeholder as a visible text node
RELEASE_TPL tpl_id Evict a native template prototype
SET_TEXT id, text nodeValue assignment
SET_ATTR id, name, value setAttribute, or removeAttribute when value is None
SET_PROP id, name, value DOM property assignment (value, checked, selectedValues, innerHTML)
SET_STYLE id, decls style.setProperty / removeProperty per kebab-case declaration
LISTEN / UNLISTEN id, event_type[, options] Delegated bookkeeping for nodes outside a template, or a direct native listener with options; unlisten omits options
RELEASE [ids] Drop registry entries and listener sets (used for a disposed root's container)
ROOT / UNROOT id Start or stop delegating events from this container instead of document
HYDRATE root_id Start claiming server-rendered nodes under root_id
CLAIM_ELEMENT id, parent_id, tag, namespace Adopt the parent's next element, or create one in place on a mismatch
CLAIM_TEXT id, parent_id, text Adopt the next text node, splitting text the HTML parser merged; empty text is created
CLAIM_COMMENT id, parent_id, data Adopt the next comment; a keyed end marker (/...) resynchronizes by searching forward
CLAIM_STATIC parent_id, end_marker Keep the server nodes before the marker as static DOM (a NoHydration region)
HYDRATE_END none Remove server nodes nobody claimed and stop claiming

A row of the js-framework-benchmark table costs one command, a CLONE that carries the row's text and its label's first value, when it's written as an html template. Clearing a list is one DISPOSE_RANGE. Unmounting never sends lists of every node id: the kernel releases the removed nodes itself. See Virtual DOM for how compiled templates, precompiled mount functions, and lazy node ids fit together.

hydrate emits the claim ops; see Server rendering. stats() reports node, template, listener, and root counts plus hydration_mismatches, and take_state() and replay_events() read the server state and replay input recorded before hydration.

Events travel the other way: the JS kernel installs one native listener per event type on each render root, walks the ancestor chain natively, and calls the Python dispatcher once for the matching bubbling route with a small JSON payload (see events).

from wybthon import kernel

# In a CPython test, install the stub backend over any DOM-like document
# (wybthon.testing and the test suite's conftest do this for you).
kernel.reset(kernel.PythonBackend(stub_document))
kernel.html_templates = False  # optional: force per-node commands

See also