Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/pocket-stack/pocketjs/llms.txt

Use this file to discover all available pages before exploring further.

The HostOps interface is the lowest layer of PocketJS: the synchronous ui.* op surface that every component tree eventually drives. Three design rules define the entire model. First, every op is a mutation — there are no queries, no getters, no tree-walking reads. The one read-shaped method, measureText, is a stateless convenience; layout still measures natively inside the Rust core. Second, every op is synchronous — each call is a single blocking FFI crossing, not a buffered command or async message. Batching happens one level up, where Solid only runs effects for changed signals. Third, the reconciler never reads across FFI — the renderer keeps a JS mirror tree (NodeMirror) so every structural read Solid performs (parent, first child, next sibling, type check) is a plain JS object walk. Only writes cross the boundary. App developers never call these ops directly — View, Text, and Image emit them — but this page is the authoritative reference for anyone implementing a new host or doing advanced debugging.
On PSP, globalThis.ui is the HostOps object, installed by the QuickJS native binding in native/src/ffi.rs. On browser, WASM, and headless-Bun hosts, you pass your own HostOps implementation explicitly to mount() or render() via opts.ops. The two paths are unified by detectHost: injected ops win, otherwise globalThis.ui is used, and if neither exists render() throws.

The HostOps interface

/** The `ui.*` op surface. Handles are generation-tagged positive i32 ids;
 *  0 means "none" (anchor 0 = append, setFocus 0 = clear). */
export interface HostOps {
  createNode(type: number): number;
  destroyNode(id: number): void;
  insertBefore(parent: number, child: number, anchorOr0: number): void;
  removeChild(parent: number, child: number): void;
  setStyle(id: number, styleId: number): void;
  setProp(id: number, propId: number, value: number): void;
  setText(id: number, str: string): void;
  replaceText(id: number, str: string): void;
  uploadTexture(buf: Uint8Array, w: number, h: number, psm: number): number;
  setImage(id: number, texHandle: number): void;
  setSprite(id: number, atlas: number, frames: number, cols: number, step: number): void;
  animate(id: number, propId: number, to: number, durMs: number, easing: number, delayMs: number): number;
  cancelAnim(animId: number): void;
  setFocus(idOr0: number): void;
  loadStyles?(buf: Uint8Array): void;
  loadFontAtlas?(buf: Uint8Array): void;
  measureText(str: string, fontSlot: number): number;
}

Method reference

MethodSignatureDescription
createNode(type: number) => numberCreates a new node. type is a NODE_TYPE code: 0 = view, 1 = text, 2 = image. Returns a fresh generation-tagged node id.
destroyNode(id: number) => voidDestroys the entire subtree rooted at id, frees all animation tracks it owns, and clears focus if the currently focused node was anywhere inside the subtree.
insertBefore(parent: number, child: number, anchorOr0: number) => voidInserts or moves child before anchor inside parent. DOM move semantics: if child is already attached anywhere it is unlinked from its current parent first (core tree, taffy layout, and JS mirror). anchor = 0 appends. Silently no-ops when the target parent is already at MAX_TREE_DEPTH (64) — same contract as a stale id.
removeChild(parent: number, child: number) => voidDetaches child from parent but keeps the node alive in memory. Solid may re-insert it later in the same frame. The end-of-frame renderer sweep calls destroyNode on any node still detached after frame() completes.
setStyle(id: number, styleId: number) => voidApplies a compiled style by its index in the style table. STYLE_ID_NONE (-1) clears the node back to default style. Triggers CSS-style transitions: the core diffs animatable props between the old and new style records and starts tween tracks for any that changed.
setProp(id: number, propId: number, value: number) => voidSets one dynamic prop on a node. propId is a value from the PROP enum (see Spec). Color props and enum props pass their u32 bits as the number argument; dimension and scalar props pass a plain f32.
setText(id: number, text: string) => voidSets the UTF-8 text content of a text node. Used at creation time when the initial string is known.
replaceText(id: number, text: string) => voidUpdates the UTF-8 text content of an existing text node. Solid’s universal renderer calls this method (not setText) on reactive text signal updates.
uploadTexture(buf: Uint8Array, w: number, h: number, psm: number) => numberUploads an image into the core’s texture arena. w and h must each be a power of two and no larger than 512. psm is a PSM format code from the spec (PSM_4444 = 2, PSM_8888 = 3). Bytes are copied and 16-byte aligned. Returns a 0-based texture handle, or -1 on failure.
setImage(id: number, texHandle: number) => voidBinds an uploaded texture to an image node. texHandle < 0 clears the image binding. Note: 0 is a valid handle (the first uploaded texture); only negative values clear. Also clears any active sprite binding on the same node.
setSprite(id: number, atlas: number, frames: number, cols: number, step: number) => voidBinds an animated sprite atlas to an image node. atlas is an uploaded texture holding a cols-wide grid of frames cells. The Rust core auto-plays the sprite by cycling cells at one cell per step vblanks — entirely in native code, with zero per-frame JS. frames <= 0 clears the sprite binding.
animate(id: number, propId: number, to: number, durMs: number, easing: number, delayMs: number) => numberStarts a tween on propId from its current value to to. durMs is the duration in milliseconds (ignored by spring easings, which are physics-driven). easing is an ENUMS.Easing ordinal. delayMs defers the start. Returns an animId for use with cancelAnim.
cancelAnim(animId: number) => voidStops a running tween or spring track by the id animate returned. The animated prop retains its value at the moment of cancellation.
setFocus(idOr0: number) => voidFocuses a node, applying its focus: style variant natively. Passing 0 clears focus. Only one node can hold focus at a time.
loadStyles(buf: Uint8Array) => voidWeb and test hosts only. Optional. Feeds the compiled styles.bin blob to the core. On PSP the native binary feeds core directly from the pak before JavaScript evaluation starts; this method exists on the interface so the full op surface is symmetric.
loadFontAtlas(buf: Uint8Array) => voidWeb and test hosts only. Optional. Feeds one baked font atlas blob. Call once per atlas slot; slot index is encoded in the atlas header. On PSP the pak walker feeds atlases natively.
measureText(str: string, fontSlot: number) => numberReturns the measured pixel width of str in the given font slot. This is a JavaScript-side convenience for dynamic layout calculations. The core’s layout engine always measures text natively; call this only when you need a width in JS for positioning logic.

Generation-tagged node handles

Node ids are not plain indices. Each id packs a slot and a generation counter:
id = (generation << ID_SLOT_BITS) | slot
// ID_SLOT_BITS = 20, ID_SLOT_MASK = 0xFFFFF
When a node is destroyed its arena slot returns to the free list and its generation increments. A stale id — a handle held by JS after the node was swept — decodes to a slot whose live generation no longer matches, so the core recognizes it and the op silently becomes a no-op instead of corrupting a reused node. Fixed invariants:
  • Bit 31 is always 0, so all valid ids are positive i32 values.
  • 0 is “no node” — insertBefore with anchor = 0 appends; setFocus(0) clears focus.
  • ROOT_ID = 1 (slot 1, generation 0) is the pre-created full-screen root, a flex column. App content mounts under it.
  • MAX_TREE_DEPTH = 64 bounds all recursive tree walks (layout, paint, subtree destroy) so a runaway tree cannot overflow the small PSP thread stacks.

The Host type

detectHost wraps a raw HostOps object in a Host struct that also records the kind and strictness. Strict hosts (web, WASM, Bun test) throw loudly on an unknown class string or unregistered texture src. The PSP host is non-strict and counts misses in missCounters instead of throwing, because a crash on hardware is worse than a slightly-wrong style.
export interface Host {
  ops: HostOps;
  kind: "psp" | "injected";
  /** Strict hosts throw on unknown class/src; PSP counts silently. */
  strict: boolean;
}

Host helper functions

These three functions from src/host.ts are re-exported by @pocketjs/framework.
/**
 * Resolve the active host.
 * - An injected HostOps wins over globalThis.ui.
 * - If the injected ops ARE globalThis.ui (PSP demo entries pass it explicitly),
 *   the host stays kind "psp" / non-strict — detected via the __textures marker
 *   set only by native/src/ffi.rs.
 * - Throws when neither exists.
 */
function detectHost(injected?: HostOps): Host

/**
 * Install the active host. Called by render(); tests may call directly.
 */
function installHost(host: Host): void

/**
 * Return the installed HostOps surface. Throws if installHost was never called.
 */
function getOps(): HostOps

NodeMirror

The JS-side mirror of a native node. Every reconciler read (parent, first child, next sibling, type check) operates on NodeMirror objects without crossing FFI. Only writes emit ops.
interface NodeMirror {
  id: number;                         // native generation-tagged node id
  type: number;                       // NODE_TYPE ordinal (0 view, 1 text, 2 image)
  parent: NodeMirror | null;
  children: NodeMirror[];
  text?: string;                      // text nodes only
  focusable?: boolean;                // joins d-pad focus traversal
  onPress?: (() => void) | undefined; // CIRCLE handler while focused
}
NodeMirror objects are what ref callbacks receive, and what animate(), spring(), focusNode(), pushFocusScope(), and pushFocusGrid() all accept as their first argument. The mirror tree is maintained entirely by the renderer — you never construct these objects directly.

Prop value encoding

setProp and animate both carry their value as a single number (a JavaScript f64 on the wire). The encoding depends on the prop’s VALUE_KIND:
KindProp examplesEncoding
f32width, height, opacity, translateX, rotate, scaleIEEE-754 float, passed through directly
colorbgColor, textColor, borderColor, gradFrom, gradToPacked u32 ABGR (0xAABBGGRR); or a '#rgb' / '#rrggbb' / '#rrggbbaa' string parsed by parseHexColor
intposType, overflow, zIndex, fontSlot, textAlign, flexDirEnum ordinal or integer, stored as u32 bits
The helper encodePropValue(prop, value) in src/host.ts is the single encoding choke point used by the renderer’s setProperty dispatch. A non-numeric string for a non-color prop throws. Full-string hex validation is applied to color strings (#ff00zz throws rather than silently painting a prefix color).

Build docs developers (and LLMs) love