portable

js/ui/portable.ts

fino:ui/portable — host-neutral trees as transferable data.

A fino:ui tree is already plain-ish data, but nothing stops a component from putting a class instance, a closure, or a cycle in its props. This module defines the subset that survives leaving the isolate that built it, and rejects everything else at the boundary rather than at the far end.

That subset is what makes a component's location a deployment choice. The same tree crosses a realm port via structured clone, an SSE stream as JSON, or a postMessage to a browser, and the receiver routes type to whatever implementation it has for that name. Nothing in a portable tree names a server object, so nothing about where it was rendered leaks into it.

import { h, renderStatic } from 'fino:ui';
import { portableSink } from 'fino:ui/portable';

const tree = renderStatic(() => h('main', { id: 'root' }, 'Ready'), portableSink());

Types

type PortableValue = | null | boolean | number | string | PortableValue[] | { [key: string]: PortableValue }

JSON value allowed to cross a UI boundary.

Functions, class instances, non-finite numbers, and cyclic values are rejected before a tree is published.

Interfaces

interface PortableVNode {

Host-neutral component node in transferable form.

Receivers route type to their own named implementation. key is semantic instance identity for reconciliation; it is not a component implementation id.

Properties

type: string

Named component implementation requested from the receiver.

props: Record<string, PortableValue>

JSON props interpreted by that implementation.

children: Array<PortableVNode | string>

Ordered child components and text.

key: string | number | null

Stable instance identity, or null when the node is unkeyed.

Classes

class PortableValueError extends TypeError {

Value rejected while converting a tree to portable form.

The message names the property path so a component author can find the prop that cannot cross, rather than learning only that "something" failed.

Readonly Properties

readonly path: string

Dotted path from the tree root to the offending value.

Constructors

constructor(path: string, message: string)

Functions

function toPortable(tree: VNode): PortableVNode

Convert a rendered tree to its transferable form.

The result is structured-clone safe and JSON.stringify safe, and matches what a JSON round-trip would produce: an undefined prop is omitted, since a prop set to undefined and one never set are the same absent prop, and an undefined array element becomes null so positions are preserved.

A value that cannot cross at all — a function, a class instance, a cycle — throws PortableValueError naming its path rather than being dropped, because a receiver cannot tell a dropped prop from one that was never sent.

import { h } from 'fino:ui';
import { toPortable } from 'fino:ui/portable';

const tree = toPortable(h('button', { disabled: true }, 'Save'));

function portableSink(): Sink<PortableVNode>

Sink that converts each committed tree to its transferable form.

Use it wherever a rendered tree leaves the isolate that produced it: a realm publishing revisions to its parent, a server streaming semantic updates, or a build step recording a tree for a later host to interpret.

import { createRoot } from 'fino:ui';
import { portableSink } from 'fino:ui/portable';

const root = createRoot(App, portableSink());
root.subscribe((tree) => port.postMessage(tree));