API reference
The primary app-facing exports of @pocketjs/framework, grouped by import
path. Signatures are TypeScript-style; defaults are noted in parentheses.
Framework-internal and tests/debug helpers are not exhaustive here. For conceptual walkthroughs see Components,
Reactivity, Animation, and
Input & focus.
| Import path | Exports |
|---|---|
@pocketjs/framework |
mount, render, host/runtime helpers, types |
@pocketjs/framework/components |
View, Text, Image, Sprite, CompositorSurface, Screen, Focusable, FocusScope, FocusGrid, ActionHandler, Portal, AuxiliarySurface, AuxiliaryPortal, Modal, ActionBar, Named, Grid, Lazy, Gallery, DeepZoom (Solid) |
solid-js |
createSignal, createEffect, createMemo, onMount, onCleanup, batch, untrack, Show, For, Index, Switch, Match |
vue |
defineComponent, ref, computed, watchEffect, onMounted, onScopeDispose |
octane |
useState, useEffect, useMemo, useRef, useLayoutEffect, useEffectEvent |
@pocketjs/framework/animation |
animate, spring, jump, cancelAnim |
@pocketjs/framework/lifecycle |
onFrame, onButtonPress, analogX, analogY, analogRaw, createSpriteAnimation, pushButtonHandlerBlock (Octane builds: useFrame, useButtonPress, useSpriteAnimation) |
@pocketjs/framework/input |
BTN, touches, auxiliaryTouches, focusNode, getFocused, pressNode, setActiveNode, pushFocusScope, pushFocusGrid, pushFocusController, hitFocusable, hitNode, enableCursor, cursorX, cursorY |
@pocketjs/framework/gesture |
createGesture, attachGesture, pushTouchBlock, gesture types (Solid and Vue Vapor) |
@pocketjs/framework/kinetics |
createScroller, bindDpadScroll, Scroller / ScrollerOptions / ScrollerState types (Solid and Vue Vapor) |
@pocketjs/framework/osk |
Osk, TextField, createOsk, OSK_H, OSK_LAYERS (Solid) |
@pocketjs/framework/virtual-list |
VirtualList, VirtualListProps, VirtualListHandle (Solid) |
@pocketjs/framework/display |
auxiliaryViewport, hasAuxiliarySurface |
@pocketjs/framework/platform |
platform, hasFeature |
@pocketjs/framework/clock |
simulationHz, ticksPerFrame, virtualFrame, virtualNow, after |
@pocketjs/framework/effects |
installEffectDriver, runEffect, effect types |
@pocketjs/framework/net |
fetch, NetError, PocketResponse, FetchOptions |
@pocketjs/framework/db |
Database, Statement, SqlValue, SqlParams, RunResult |
@pocketjs/framework/fs |
file, write, usage, and the node:fs sync subset (readFileSync, writeFileSync, appendFileSync, mkdirSync, readdirSync, rmSync, renameSync, statSync, existsSync) |
@pocketjs/framework/audio |
decodeWav, createWavPlayer, WavPcm / WavPlayer types |
@pocketjs/framework/launcher |
launcherActive, appTable, launchApp, frozenShot |
@pocketjs/framework/devtools |
initDevtools, wrapFrameHandler, tape expanders — see DevTools |
@pocketjs/framework/hot |
text, prop |
@pocketjs/framework/manifest |
app and Pocket System schema/types/resolvers, extractHostBuildInputs, hostBuildEnvironment, vitaTitleId |
framework/compiler/subpaths.ts is the registry these paths are declared in —
one row per module, and a build resolves nothing that has no row. The rows it
carries beyond this table (/config, /host, /package, /prelude,
/renderer, /idf-host, /vita-package) are build and host-author surface
rather than app API.
@pocketjs/framework
The runtime entry point: mount an app, tear it down, and reach the lower-level host, sweep, style, and pack utilities.
mount
function mount(code: () => unknown, opts?: MountOptions): () => voidApp-level entry point for demo/application bundles. Resolves ops from opts.ops or globalThis.ui, loads opts.pak (when given), uploads the pack's images on injected hosts, feeds the default generated style table (opts.styles ?? STYLE_IDS), and mounts code. Returns a disposer that unmounts and destroys the app subtree. Throws if neither opts.ops nor globalThis.ui is present.
Solid entries pass a closure (mount(() => <App />)); Vue Vapor and Octane
entries pass the component itself (mount(App)). In Octane, JSX inside a
call-argument arrow is a compile error, so mount(App) is the only valid
entry shape.
render
function render(code: () => unknown, opts?: RenderOptions): () => voidLower-level mount: detects and installs the host, wires the style resolver, registers opts.styles, feeds styles/atlases from the pack on injected hosts, builds the app + overlay layers, installs the per-frame handler, and mounts code. Returns a disposer. mount calls render; call render directly when you supply your own ops/styles.
RenderOptions / MountOptions
MountOptions is an alias of RenderOptions.
| Field | Type | Description |
|---|---|---|
ops |
HostOps |
web/wasm/test hosts inject their op surface here; omit on native QuickJS hosts (globalThis.ui). |
styles |
Record<string, number> |
class-literal → styleId table (styles.generated.ts). |
pak |
ArrayBuffer |
app pack; defaults to globalThis.__pak when present. |
Host helpers
function detectHost(injected?: HostOps): Host
function installHost(host: Host): void
function getOps(): HostOpsdetectHost resolves the active host — injected ops win, otherwise
globalThis.ui (PSP/Vita QuickJS); throws when neither exists. installHost
sets the active host (called by render). getOps returns the installed op
surface. Manifest-built native bundles validate ui.__host and
ui.__hostAbi before mounting. See Native contract
for the full HostOps surface.
HostOps
The synchronous ui.* op surface a host installs. Node ids are
generation-tagged positive i32 values and reserve 0 for "none"; texture
handles have separate 0-based or generation-tagged contracts. Its TypeScript
declaration is HostOps in framework/src/host.ts, and the
Native contract page owns the wire: every op number,
signature, and which ops a host may omit.
Host
interface Host {
ops: HostOps;
kind: "native" | "injected";
target: string;
strict: boolean;
}kind describes who owns the transport, not the target. Native PSP and Vita
hosts are non-strict and count an unknown class or texture; injected
web/wasm/test hosts throw. target normally carries "psp", "vita", or
"injected"; a pre-contract native host reports "unknown", and a custom
injected host may supply another identifier.
End-of-frame sweep
function retain(node: NodeMirror): void
function release(node: NodeMirror): void
function runSweep(): voidretain keeps a detached subtree alive across frames (skips the sweep); release undoes it so a still-detached node re-enters the next sweep. runSweep destroys every subtree removed during the frame and still detached — the runtime calls it once per frame after user code and input, so remove-then-reinsert (Solid moves) never destroys live nodes. Reach for these only when hand-managing detached subtrees.
registerTexture
function registerTexture(key: string, handle: number): voidBind an image key (the src string) to an uploadTexture handle so <Image src="key"> resolves through the renderer's texture registry.
missCounters
const missCounters: { unknownClass: number; unknownTexture: number; unknownSurface: number }On a non-strict native host, an unknown class or texture increments a counter instead of throwing. Read it to diagnose missing styles/images without crashing hardware.
Styles
function registerStyles(table: Record<string, number>): void
function resolveStyle(cls: string): number | undefinedregisterStyles loads a class-literal → styleId table (the compiler's STYLE_IDS); it also registers a token-sorted alias so "a b" resolves the id for "b a". resolveStyle returns the styleId for a class string, or undefined if the compiler never saw it (or the token reordering is ambiguous). See Styling.
Data pack (pak)
function pakEntries(prefix?: string): string[]
function pakGet(key: string): Uint8Array
function loadPack(ab: ArrayBuffer): void
function resetPack(): voidpakEntries lists entry keys starting with prefix (default: all keys), sorted. pakGet returns a fresh copy of a blob's bytes, throwing on a missing key. loadPack explicitly loads a pack (web host after fetch, or tests), replacing any prior. resetPack drops the cached parsed pack. See Build pipeline.
NodeMirror
interface NodeMirror {
id: number; // native generation-tagged node id
type: number; // spec NODE_TYPE ordinal
parent: NodeMirror | null;
children: NodeMirror[];
text?: string; // text nodes only
focusable?: boolean; // focus traversal membership
onPress?: (() => void) | undefined; // activation: CIRCLE, tap, or cursor click
}The JS mirror of a native node. A ref receives one; animate, spring, focusNode, pushFocusScope, and pushFocusGrid all accept one.
@pocketjs/framework/components
Platform primitives and higher-level components. Solid control-flow components
(Show, For, Index, Switch, Match) are not exported here; import them
directly from solid-js.
Primitives
function View(props: ViewProps): JSX.Element
function Text(props: TextProps): JSX.Element
function Image(props: ImageProps): JSX.Element
function Sprite(props: SpriteProps): JSX.Element
function CompositorSurface(props: CompositorSurfaceProps): JSX.ElementThe host primitives, wrapped React Native-style. View is the flex container/box, Text renders baked-font text, Image draws an uploaded texture by src key, and Sprite draws an auto-playing animation from a baked sprite atlas by sprite key. CompositorSurface is reserved for System UI shells and places an installed package AppInstance into shell layout and painter order.
ViewProps
| Prop | Type | Description |
|---|---|---|
class |
string |
Tailwind-subset class literal. |
style |
Record<string, number | string> |
Inline spec props (escape hatch). |
onPress |
() => void |
Fired when the node is activated: CIRCLE while focused, a touch tap on the node, or a cursor click. See App shell. |
focusable |
boolean |
Joins d-pad focus traversal. |
ref |
(node: NodeMirror) => void | NodeMirror |
Node handle. |
children |
JSX.Element |
Child nodes. |
debugName |
string |
Semantic name in the DevTools tree (mirror-only, zero native cost). |
TextProps — class, style, ref, children, debugName.
ImageProps — class, src (string), style, ref, debugName.
SpriteProps — class, sprite (string — a ui:sprite.<name> atlas key), style, ref, debugName.
CompositorSurfaceProps — class, style, package (installed reverse-DNS package id), focused, ref, debugName.
Sprite is a native animated primitive: its atlas (a pow2 texture holding a grid of frames) is baked into the pak, and the Rust core advances the frame cell from its own tick counter — deterministic and with zero per-frame JS. It auto-plays from the first frame the moment it is displayed, so a sprite revealed by paging or a Show/Lazy starts animating on its own. Bake atlases by listing them in a demo's sprites.json ({ "<atlas>.png": { cols, rows, frames, step } }); step is core ticks per frame, so the sprite runs at tick rate / step fps (step: 2 is 30 fps on a 60 Hz bundle). See apps/gallery (its covers are shader-baked animated sprites).
Screen
function Screen(props: ScreenProps): JSX.Element // ScreenProps extends ViewPropsA full-screen root View. Defaults class to "relative flex-col w-full h-full bg-slate-50 overflow-hidden" when none is given.
Focusable
interface FocusableProps extends ViewProps { onPress?: () => void }
function Focusable(props: FocusableProps): JSX.ElementA View with focusable: true. onPress is the activation handler: CIRCLE
while the node is focused, a touch tap on the node, or a cursor click all
enter it through the same path. See App shell.
FocusScope
interface FocusScopeProps extends ViewProps, FocusScopeOptions {
active?: boolean | (() => boolean);
}
function FocusScope(props: FocusScopeProps): JSX.ElementRestricts d-pad traversal and CIRCLE to its subtree while active (default true). Adds autoFocus / restoreFocus from FocusScopeOptions. Internally pushes/pops via pushFocusScope.
FocusGrid
interface FocusGridProps extends ViewProps, FocusGridOptions {
active?: boolean | (() => boolean);
}
function FocusGrid(props: FocusGridProps): JSX.ElementGives its subtree row/column d-pad semantics while active. Requires columns; wrap (default false) wraps at row ends. Internally pushes/pops via pushFocusGrid.
ActionHandler
interface ActionHandlerProps extends ButtonPressOptions {
button: number; // BTN mask
onPress: (pressed: number, buttons: number) => void;
children?: JSX.Element;
}
function ActionHandler(props: ActionHandlerProps): JSX.ElementDeclarative wrapper over onButtonPress: fires onPress on the button edge.
Inherits allowWhenBlocked, active, and latched from
ButtonPressOptions. Renders children (or nothing).
Portal
interface PortalProps { children?: JSX.Element | (() => JSX.Element) }
function Portal(props: PortalProps): JSX.ElementRenders children into the full-screen overlay root (above the app layer, zIndex 1000) instead of the local tree. Cleans up its host node on unmount.
AuxiliarySurface
interface AuxiliarySurfaceProps { children?: JSX.Element | (() => JSX.Element) }
function AuxiliarySurface(props: AuxiliarySurfaceProps): JSX.ElementRenders children into the independent application layer of the resolved
auxiliary display. The component requires display.auxiliary; mounting it
without that capability throws. The application state and resource pool remain
shared with the primary tree.
AuxiliaryPortal
function AuxiliaryPortal(props: AuxiliarySurfaceProps): JSX.ElementRenders children into the auxiliary display's overlay layer. Use it for
content that must paint above the rest of <AuxiliarySurface>; ordinary
Portal always targets the primary display.
Modal
interface ModalProps {
class?: string;
panelClass?: string;
open?: boolean | (() => boolean);
children?: JSX.Element;
}
function Modal(props: ModalProps): JSX.ElementA portalled backdrop + focus-scoped panel. While open, it blocks background button handlers (pushButtonHandlerBlock) and fades/scales the panel in. class styles the centering frame; panelClass styles the panel.
Named
function Named(props: { name: string; children?: JSX.Element }): JSX.ElementTags the host nodes it renders with a semantic name for the
DevTools component tree (<Named name="MessageCard"><Card/></Named>).
Renders no node of its own; a child's explicit debugName prop wins.
ActionBar
function ActionBar(props: ActionBarProps): JSX.Element // ActionBarProps extends ViewPropsA portalled bottom bar. Defaults to a pinned left-3 right-3 bottom-3 row when no class is given.
Grid
interface GridProps extends ViewProps, Partial<FocusGridOptions> {
gap?: number; // cross-axis gap px (via style)
active?: boolean | (() => boolean); // enable FocusGrid traversal (needs columns)
}
function Grid(props: GridProps): JSX.ElementA wrapping tile layout (flex-row flex-wrap). With columns + active it delegates row/column d-pad traversal to FocusGrid; columns drives traversal only — layout stays flexbox. gap is a number so class can stay a single compiled literal.
Lazy
interface LazyProps {
when: boolean | (() => boolean); // mount while truthy; destroy when false
reveal?: number; // host frames to show fallback first (0)
fallback?: JSX.Element | (() => JSX.Element);
children: () => JSX.Element; // deferred content factory
}
function Lazy(props: LazyProps): JSX.ElementOn-demand mount: builds children only while when is truthy (the sweep destroys the subtree when it goes false). reveal shows fallback for N frames the first time it activates, then latches revealed for its lifetime (no replay). Models on-demand content build — textures are still uploaded eagerly at pak load.
Gallery
interface GalleryProps {
count: number; // total pages
page: () => number; // controlled current-page accessor
onPageChange?: (next: number) => void;
renderPage: (index: number) => JSX.Element; // called only for in-window pages
window?: number; // pages kept mounted each side (1)
duration?: number; // slide ms (300)
easing?: EasingName; // slide easing ("out")
bindTriggers?: boolean; // bind LTRIGGER/RTRIGGER (true)
wrap?: boolean; // wrap past the ends (false)
class?: string; // outer viewport class
}
function Gallery(props: GalleryProps): JSX.ElementA full-screen L/R-paged strip: LTRIGGER/RTRIGGER slide one whole screen at a time. Controlled (page + onPageChange); the slide is one native translateX tween per press (paint-only), and pages outside window are not built, keeping many-page galleries inside the draw budget. See apps/gallery.
DeepZoom (Solid)
function DeepZoom(props: DeepZoomProps): JSX.ElementA streamed tiled-canvas viewer. It selects from a baked TileDoc pyramid,
keeps an overview beneath the active level, loads a bounded number of tiles per
frame, and releases generation-tagged texture handles outside the viewport.
D-pad/left-analog panning and trigger zoom are built in; gestureSource can
supply logical-coordinate pan/pinch gestures with an anchored zoom point.
| Prop | Type | Description |
|---|---|---|
doc |
TileDoc |
Baked document dimensions, background, tile edge, and resolution levels. |
width / height |
number |
Logical viewport size (defaults to 480×272). |
loadBudget |
number |
Maximum textured-tile uploads per frame (default 2). |
prefetch |
number |
Extra mounted tile ring (default 1). |
bindInput |
boolean |
Bind controller pan/zoom internally (default true). |
gestureSource |
() => DeepZoomGesture | null |
Optional direct-manipulation input for touch hosts. |
onView |
(view: DeepZoomView) => void |
Observe deterministic center, zoom, and selected level. |
DeepZoom is currently exported by the Solid components adapter. The tile
wire format and texture-streaming HostOps remain framework-neutral.
solid-js
Import Solid's reactive primitives and control-flow components directly from
solid-js. PocketJS relies on the real Solid runtime rather than wrapping or
curating these exports. Full docs live at
solidjs.com; summary below.
Reactivity
| Export | Signature | Purpose |
|---|---|---|
createSignal |
createSignal<T>(value?, opts?) => [get: () => T, set: (v) => T] |
Reactive atom. |
createEffect |
createEffect(fn: (prev) => T, value?) => void |
Run on dependency change. |
createMemo |
createMemo(fn: (prev) => T, value?) => () => T |
Cached derived value. |
onMount |
onMount(fn: () => void) => void |
Run once after first render. |
onCleanup |
onCleanup(fn: () => void) => void |
Run on owner disposal. |
batch |
batch(fn: () => T) => T |
Coalesce updates. |
untrack |
untrack(fn: () => T) => T |
Read without tracking. |
See Reactivity.
Control flow
| Component | Usage | Purpose |
|---|---|---|
Show |
<Show when={cond} fallback={…}>…</Show> |
Conditional render. |
For |
<For each={list}>{(item, i) => …}</For> |
List keyed by reference. |
Index |
<Index each={list}>{(item, i) => …}</Index> |
List keyed by index. |
Switch / Match |
<Switch fallback={…}><Match when={c}>…</Match></Switch> |
Multi-branch. |
PocketJS's renderer maps these updates onto the native tree, but the component APIs and semantics are Solid's.
vue
Vue Vapor apps import Vue's Composition API directly from vue; PocketJS does
not wrap refs, computed values, effects, or component definitions. Use
@pocketjs/framework/vue-vapor/components explicitly, or set
framework: "vue-vapor" and import generic @pocketjs/framework/components.
octane
Octane apps import React-model hooks directly from octane (useState,
useEffect, useMemo, useRef, useLayoutEffect, useEffectEvent, …);
PocketJS does not wrap them. Dependency arrays may be omitted — the Octane
compiler infers them from captures — and hooks are tracked by call site
(conditional hooks in if blocks are fine, hooks in loops are not). Use
@pocketjs/framework/octane/components explicitly, or set
framework: "octane" and import generic @pocketjs/framework/components.
PocketJS's per-frame lifecycle hooks are use-prefixed in Octane builds —
useFrame, useButtonPress, and useSpriteAnimation from
@pocketjs/framework/octane/lifecycle — because the Octane compiler slot-keys
custom hooks by the use[A-Z] naming convention. Their signatures match
onFrame / onButtonPress / createSpriteAnimation below, except
useSpriteAnimation returns the current frame key as a plain string.
pushButtonHandlerBlock is not a hook and keeps its name.
@pocketjs/framework/animation
Typed motion over ops.animate. JS declares the tween once; the Rust core
advances it one fixed tick at a time, dt = 1 / tick rate for the rate the
bundle bakes (see clock). prop is a spec PROP name and must
be animatable (e.g. opacity, translateY, scale, and color props) —
non-animatable props throw. See Animation.
animate
function animate(
node: NodeMirror | number,
prop: PropName,
to: number | string,
opts?: AnimateOptions,
): number // returns animIdTweens prop from its current value to to. For color props, to is a packed u32 ABGR or a '#rrggbb' / '#rrggbbaa' string. Returns an animId for cancelAnim.
AnimateOptions
| Field | Type | Default | Description |
|---|---|---|---|
dur |
number |
200 |
Duration in ms (ignored by spring easings). |
easing |
EasingName | number |
"out" |
Named easing or raw ENUMS.Easing ordinal. |
delay |
number |
0 |
Delay in ms before the tween starts. |
EasingName — "linear" | "in" | "out" | "in-out" | "out-back" | "spring" | "spring-bouncy".
jump
function jump(node: NodeMirror | number, prop: PropName, value: number | string): voidSets an animatable prop for this frame with no tween. A jump on a transform
prop is paint-only — one setProp, no relayout — which is what a finger-follow
drag writes on every move frame.
spring
function spring(
node: NodeMirror | number,
prop: PropName,
to: number | string,
preset?: "default" | "bouncy",
): numberSprings prop to to; duration comes from the physics, not a timer. preset (default "default") selects the base or bouncy spring. Returns an animId.
cancelAnim
function cancelAnim(animId: number): voidStops a running animation by the id animate/spring returned.
@pocketjs/framework/lifecycle
Component-scoped per-frame hooks. Registrations clean up with the selected
framework owner (onCleanup in Solid, onScopeDispose in Vue Vapor, effect
cleanup in Octane). In Octane builds these exports are the use-prefixed hooks
useFrame, useButtonPress, and useSpriteAnimation (see
octane above); the signatures below are otherwise identical. See
Reactivity and Input & focus.
onFrame
function onFrame(callback: (buttons: number) => void): voidRegisters callback to run once per host frame with the current spec BTN bitmask.
Analog input
function analogX(): number
function analogY(): number
function analogRaw(): numberanalogX and analogY return the current left stick/nub axis in -1..1
after PocketJS's shared deadzone (right and down are positive).
analogRaw returns the host word (x << 8) | y, with each byte in 0..255
and 128 at center. Stickless hosts stay centered, so controller fallbacks do
not need target checks. Solid, Vue Vapor, and Octane expose the same functions
from their lifecycle subpaths.
onButtonPress
function onButtonPress(
mask: number,
callback: (pressed: number, buttons: number) => void,
opts?: ButtonPressOptions,
): voidEdge-detects a button: fires callback on the frame a button in mask transitions from up to down. pressed is the just-pressed bitmask; buttons is the full held mask.
ButtonPressOptions
| Field | Type | Default | Description |
|---|---|---|---|
allowWhenBlocked |
boolean |
false |
Keep firing while a modal/system block owns input. |
active |
boolean | (() => boolean) |
true |
Gate the handler on/off. |
latched |
boolean |
false |
Require the button to be observed released before the next edge can fire; prevents a held opener from re-triggering a newly mounted screen. |
createSpriteAnimation
function createSpriteAnimation(
frames: readonly string[],
opts?: SpriteAnimationOptions,
): Accessor<string> | ComputedRef<string>Cycles through frames (image src keys), returning a Solid Accessor or a
Vue Vapor ComputedRef for the current frame (the Octane hook,
useSpriteAnimation, returns the current frame key as a plain string).
Throws if frames is empty.
opts.frameStep (default 1, min 1) holds each sprite frame for that many
host frames.
pushButtonHandlerBlock
function pushButtonHandlerBlock(): () => voidPushes a global block so background onButtonPress handlers (those without allowWhenBlocked) stop firing; the returned disposer pops it. Modal uses this internally.
@pocketjs/framework/input
Programmatic focus, the button bitmask, the touch snapshots, the virtual
cursor, and the imperative focus-scope/grid/controller stack. Prefer the
FocusScope / FocusGrid components in app code. The behavior these
signatures produce — the traversal order, the press and bubble model, refocus
on removal — is on Input & focus.
BTN
PSP button bitmask (identical on every host; web/Bun hosts remap keys).
| Member | Value | Member | Value |
|---|---|---|---|
SELECT |
0x0001 |
LTRIGGER |
0x0100 |
START |
0x0008 |
RTRIGGER |
0x0200 |
UP |
0x0010 |
TRIANGLE |
0x1000 |
RIGHT |
0x0020 |
CIRCLE |
0x2000 |
DOWN |
0x0040 |
CROSS |
0x4000 |
LEFT |
0x0080 |
SQUARE |
0x8000 |
touches and auxiliaryTouches
interface TouchContact {
readonly id: number;
readonly x: number;
readonly y: number;
readonly surface: "primary" | "auxiliary";
readonly hit?: number;
}
function touches(): readonly TouchContact[]
function auxiliaryTouches(): readonly TouchContact[]Each function returns an immutable snapshot for one surface. Coordinates are
logical pixels in that surface, independent of the target raster density; at
most eight contacts are delivered across both snapshots. No active touch is an
empty snapshot, not an unavailable API. Declare input.touch for touches()
or input.touch.auxiliary plus display.auxiliary for
auxiliaryTouches(). Guard optional enhancements with hasFeature().
| Field | Type | Description |
|---|---|---|
id |
number |
Stable while the contact stays down; ids are reused after a release. |
x / y |
number |
Logical viewport position in this surface. |
surface |
"primary" | "auxiliary" |
Which output's coordinate space the contact belongs to. |
hit |
number | undefined |
Down-edge hit fact: the node id the host bounds-resolved under the contact when it landed, carried unchanged until the contact lifts. |
hit is 0 when the host resolved the position and no node claimed it (a
contact on bare background, or off-screen edge cases), and undefined when
the host has no hit-fact channel at all — an older host, or a DevTools replay.
Hosts derive the fact from the bounds hit test; see
Native contract for the ops and the frame()
argument that carries them.
focusNode
function focusNode(node: NodeMirror | null): voidProgrammatically focus a node (or clear focus with null). Applies the native focus: style variant.
getFocused
function getFocused(): NodeMirror | nullReturns the currently focused node, or null.
pushFocusScope
function pushFocusScope(node: NodeMirror, opts?: FocusScopeOptions): () => voidRestricts d-pad traversal and CIRCLE to node's subtree; returns a disposer that pops the scope and restores prior focus. Backs the FocusScope component.
FocusScopeOptions
| Field | Type | Default | Description |
|---|---|---|---|
autoFocus |
boolean |
true |
Focus the first focusable on push. |
restoreFocus |
boolean |
true |
Restore the previously focused node on pop. |
pushFocusGrid
function pushFocusGrid(node: NodeMirror, opts: FocusGridOptions): () => voidGives node's subtree row/column d-pad semantics; returns a disposer that pops the grid. Backs the FocusGrid component.
FocusGridOptions
| Field | Type | Default | Description |
|---|---|---|---|
columns |
number |
— | Grid column count (min 1). Required. |
wrap |
boolean |
false |
Wrap focus at row ends. |
Virtual cursor
function enableCursor(opts?: CursorOptions): () => void
function cursorX(): number
function cursorY(): numberenableCursor replaces the d-pad focus walk with a pointer the analog nub
steers, and returns a disposer that restores the d-pad model. Calling it again
while enabled updates the options in place, and an unchanged image keeps the
texture it uploaded. A host predating the cursor ops keeps the d-pad model.
cursorX / cursorY read the position in logical px, NaN while the cursor
is disabled or before its first frame. Declare input.cursor in requires or
enhances. What changes on screen while it runs — hover as focus, the
press-and-drag-off model, the suppressed d-pad traversal — is on
Input & focus.
CursorOptions
| Field | Type | Default | Description |
|---|---|---|---|
image |
string | Uint8Array |
built-in 16×16 arrow | A pak IMG entry key, or a raw IMG entry blob. |
hotspot |
[number, number] |
[0, 0] |
The sprite pixel the position points at. |
size |
[number, number] |
the texture's own size | Logical draw size. |
speed |
number |
240 |
Travel in px per virtual second at full nub deflection, so a tape replays the same path at every rate: 240 is 4 px per frame on a 60 Hz bundle. |
dpadSpeed |
number |
0 |
Steer with the d-pad at this px per virtual second while the nub is centered; 0 leaves the d-pad to the app. |
button |
number |
BTN.CIRCLE |
Mask that presses the hovered node. |
start |
[number, number] |
viewport center | Initial position. |
Hit testing and programmatic activation
function pressNode(node: NodeMirror): void
function setActiveNode(node: NodeMirror | null): void
function pushFocusController(
node: NodeMirror,
move: (direction: FocusDirection) => boolean,
): () => void
function hitFocusable(x: number, y: number): NodeMirror | null
function hitNode(x: number, y: number, surface?: "primary" | "auxiliary"): NodeMirror | nullpressNode focuses a node and fires its onPress, bubbling to the nearest
ancestor handler — the path a touch tap and a cursor click both take.
setActiveNode holds or clears the native active: variant through the one
latch every input mode writes, so a pressed look cannot strand when the mode
changes. pushFocusController gives a subtree custom d-pad traversal: while
focus is inside node, each navigation press calls move instead of grid or
linear traversal and a false return falls through to the default; the system
keyboard drives its variable-width key rows this way. hitFocusable resolves
a point to the nearest focusable inside the active focus scope — the filter
cursor hover and touch activation share. hitNode returns the topmost painted
node under a point with no focusable or scope filter, which is how the gesture
layer resolves region ownership. Both return null where the host mounts no
hitTest op.
@pocketjs/framework/gesture
Recognizers over the per-frame touch snapshot: tap, long press, axis-lockable
pan, and two-contact pinch, plus the ownership model that decides which
recognizer keeps a contact when several want it. The recognizer machinery is
framework-neutral; Solid and Vue Vapor resolve their own createGesture
shim over it, and Octane builds do not resolve this subpath. The pump runs
once per frame from the framework entry, after effect delivery and before app
frame hooks, so app code always reads this frame's completed output. On a host
that delivers no contacts the recognizers never fire and cost nothing. See
Touch & gestures.
createGesture and attachGesture
function createGesture(opts: GestureOptions): GestureHandle
function attachGesture(opts: GestureOptions): GestureHandleattachGesture registers a recognizer and returns its handle; it stays
registered until the caller calls handle.dispose(). createGesture makes the
same registration and hands disposal to the surrounding component scope —
onCleanup in Solid, onScopeDispose in Vue Vapor — so the recognizer
unregisters with the component that created it. Use createGesture inside
components and attachGesture when there is no component scope to own it.
GestureOptions
| Field | Type | Default | Description |
|---|---|---|---|
surface |
"primary" | "auxiliary" |
"primary" |
Output to observe. A recognizer only sees contacts on this surface. |
region |
GestureRegion |
— | Where the recognizer takes ownership. Omit for a whole-screen recognizer. |
axis |
"x" | "y" | "any" |
"any" |
Pan axis lock, and the axis a pinch measures its span on. |
tapSlop |
number |
8 |
Max total travel per axis, logical px, for the contact to count as a tap. |
panSlop |
number |
6 |
Total travel, logical px, that starts a pan. |
pinchSlop |
number |
10 |
Span change, logical px, that starts a pinch. |
longPressSeconds |
number |
0.5 |
Hold duration in virtual seconds. The deadline is max(1, round(longPressSeconds × simulationHz())) virtual frames, so it holds the same wall time at every rate. |
allowWhenBlocked |
boolean |
false |
Keep observing while a pushTouchBlock is held. |
onDown |
(c: GestureContact) => void |
— | Contact landed inside the region. |
onMove |
(c: GestureContact) => void |
— | Contact moved this frame, panning or not. |
onUp |
(c: GestureContact) => void |
— | Contact released. |
onCancel |
(c: GestureContact) => void |
— | Another owner claimed the contact, a touch block was pushed, or the handle was disposed or cancelled. |
onTap |
(c: GestureContact) => void |
— | Released within tapSlop with nothing claimed and no long press fired. |
onLongPress |
(c: GestureContact) => void |
— | Held past the deadline within tapSlop. Fires once, then claims. |
onPanStart |
(c: GestureContact) => void |
— | Travel crossed panSlop on the locked axis. Claims the contact. |
onPanMove |
(c: GestureContact) => void |
— | Every frame while panning, including hold frames where fdx/fdy are 0. |
onPanEnd |
(c: GestureContact) => void |
— | Released while panning; c.vx/c.vy is the fling velocity. |
onPinchStart |
(p: GesturePinch) => void |
— | Span change beat pinchSlop and dominated the centroid's travel. Claims both members. |
onPinchMove |
(p: GesturePinch) => void |
— | Every frame while pinching, including hold frames where fdspan is 0. |
onPinchEnd |
(p: GesturePinch) => void |
— | A member released, was cancelled, or the handle was disposed; p carries the final geometry. |
An axis lock rejects cross-axis movement rather than killing the contact:
while the recognizer is unclaimed the axis test is re-evaluated every frame, so
a thumb that lands with wobble still pans once its intended axis dominates, and
a drag whose dominant axis never matches never pans on that recognizer.
A pan and a pinch on the same recognizer both need handlers to run: the pan
pass skips a recognizer with no onPanStart/onPanMove/onPanEnd, and the
pinch pass skips one with no onPinchStart/onPinchMove/onPinchEnd.
GestureContact
| Field | Type | Description |
|---|---|---|
surface |
"primary" | "auxiliary" |
Output whose coordinate space holds this contact. |
id |
number |
Stable while the contact is down; ids are reused after a release. |
x / y |
number |
Current position, logical viewport px. |
startX / startY |
number |
Position at the down edge. |
dx / dy |
number |
Total travel since the down edge. |
fdx / fdy |
number |
Travel this frame — what a finger-follow drag consumes. |
vx / vy |
number |
Velocity in logical px per virtual second, estimated over a 3-frame window; on the release frame this is the fling velocity. |
downFrame |
number |
virtualFrame() at the down edge. |
frames |
number |
Frames since the down edge (0 on the down frame). |
hit |
number | undefined |
The down edge's TouchContact.hit fact, carried for the contact's lifetime. |
Velocity is an integer position delta over k fixed-length frames with one
IEEE division per axis, so the same contact path produces the same number on
every host.
GesturePinch
| Field | Type | Description |
|---|---|---|
ax / ay |
number |
First member's current position, logical viewport px. |
bx / by |
number |
Second member's current position. |
cx / cy |
number |
Centroid of the two members. |
span |
number |
Distance between the members, projected onto the recognizer's locked axis; Euclidean when axis is "any". |
startSpan |
number |
The span when the pair formed, before the slop was crossed. |
dspan |
number |
span - startSpan; positive when the members moved apart. |
fdspan |
number |
Span change this frame — what an opening gap consumes. |
GestureRegion
| Field | Type | Description |
|---|---|---|
node |
() => NodeMirror | null | undefined |
Own contacts whose down-edge hit lands inside this node's subtree. |
rect |
() => { x, y, w, h } | null | undefined |
Logical-px geometry: the fallback when the hit misses, and the whole test when no node is given. |
Both are getters, read at the down edge. Returning null from the getter (or
supplying neither) means the recognizer owns nothing that frame, which is how
a recognizer is switched off without disposing it.
The down edge is resolved once and shared across every recognizer: the host's
hit fact is used when the frame carried one, otherwise a single query through
hitTestBounds when the host has it and hitTest otherwise. A resolution that
lands outside the subtree fails the match and never falls through
to rect — ink painted above the region occludes it. A resolution that hits
nothing falls through to rect, so gaps between rows still reach a list's pan
recognizer.
GestureHandle
| Member | Type | Description |
|---|---|---|
dispose() |
() => void |
Cancel in-flight contacts, end an in-flight pinch, and unregister. |
cancel() |
() => void |
Force-cancel this recognizer's in-flight contacts (fires onCancel) without unregistering. |
panning |
boolean |
True while any contact is mid-pan under this recognizer. |
pinching |
boolean |
True while a pinch is in flight under this recognizer. |
pushTouchBlock
function pushTouchBlock(): () => voidThe touch mirror of pushButtonHandlerBlock.
Pushing cancels the in-flight contacts of every recognizer without
allowWhenBlocked inside the call, so those owners see onCancel on this
frame rather than a release later, and suppresses new downs for them while
the block is held; the returned disposer pops it. Recognizers with
allowWhenBlocked keep observing. Blocks nest, and the depth only reaches zero when every
disposer has run. The system keyboard pushes one for its own panel; Modal
pushes the button block only.
Ownership
These rules decide which recognizer keeps a contact when several observe it.
- Owners are resolved at the down edge. Every non-disposed recognizer whose
surfaceandregionmatch the contact becomes an owner and observesonDown,onMoveandonUp. - Priority is registration order, last registered first. Mount order is deterministic, so priority is too. A component registering a richer touch model after mount outranks anything registered before it.
- A claim cancels every other owner. A pan crossing
panSlop, a long press firing, or a pinch forming claims the contact for that recognizer, and every other owner of that contact receivesonCancel. A pinch claims both its members at once. - Cancellation is terminal for that contact. A cancelled recognizer
observes nothing further from it — no
onMove, noonUp, noonTap— until the contact lifts and a new one lands. - Tap and long press single-fire. Each resolves on the highest-priority
owner that carries the handler, and no lower-priority owner sees it. Tap
death is per-owner: each recognizer applies its own
tapSlop. - The pinch pass runs before the pan pass. Two diverging contacts become a pinch even when each alone would satisfy a pan. Two contacts travelling together, with the span steady and the centroid moving, stay available to the pan recognizers. The pair a recognizer measures is the first two unclaimed contacts it still observes.
- Contact and pinch objects are pooled and mutable. They are valid only for the duration of the callback. Never retain one across frames or store it in state; copy the numbers you need.
@pocketjs/framework/kinetics
A one-axis kinetic scroller: finger-follow tracking with a rubber-banded
overscroll, an exponential-decay fling, an edge spring that carries the
incoming velocity, a per-frame chase for d-pad and stick-to-bottom scrolling,
and a programmatic tween. The state machine is framework-neutral; the Solid
shim binds the offset to a signal and the Vue Vapor shim to a shallowRef, and
Octane builds do not resolve this subpath. See
Touch & gestures.
The physics are fixed platform literals, not app knobs: the decay rates
(0.998/ms and 0.99/ms, pre-baked per tick), the 0.55 rubber-band
coefficient, the K = 170 / C = 26 edge spring, and the chase pump's 0.3
rate all live in the module. Fling and spring integrate per core tick, so a
30 Hz host follows the 60 Hz trajectory subsampled; chase advances once per
frame by design. Every formula is + − * / over literals, so trajectories are
bit-identical on every host.
createScroller
function createScroller(opts: ScrollerOptions): ScrollerCreates a scroller whose offset is a reactive cell of the active framework.
Bind translateY: -s.offset() on the content canvas — a translate is
paint-only, one setProp per moving frame with no relayout — and call
s.step() once per frame from onFrame. Nothing advances without that pump.
ScrollerOptions
| Field | Type | Default | Description |
|---|---|---|---|
max |
() => number |
— | Range end; for a list that scrolls its content, max(0, contentH - viewH). Read on every step, so a growing list needs no re-registration. Required. |
extent |
() => number |
screen height | Viewport extent, which is the rubber band's asymptote. |
initial |
number |
0 |
Starting offset. |
overscroll |
number |
48 |
Cap on rubber-band travel in px; 0 hard-clamps at the edges. |
decay |
"normal" | "fast" |
"normal" |
Fling decay preset: "normal" is the iOS 0.998/ms rate, "fast" the 0.99/ms paging rate. |
snap |
((projectedRest: number, velocity: number) => number) | null |
null |
Applied at endDrag instead of a fling: receives the projected rest position and the release velocity, returns the position to tween to. This is how paging and row alignment are built. |
onSettle |
(offset: number) => void |
— | A moving state reached rest. The settled offset is rounded to 1/64 px so settled framebuffers hash to the same value. |
ScrollerState
type ScrollerState = "idle" | "tracking" | "fling" | "spring" | "chase" | "tween"tracking is finger-follow, fling the decay from a release velocity,
spring the edge bounce-back (entered mid-fling when a fling crosses an edge,
carrying its momentum), chase the per-frame ease toward a target, tween a
programmatic scrollTo, and idle at rest.
Scroller
| Member | Signature | Description |
|---|---|---|
offset |
() => number |
Current offset in logical px. Reactive. |
velocity |
() => number |
Instantaneous velocity in px per virtual second; meaningful in fling and spring. |
state |
() => ScrollerState |
The current state. |
beginDrag |
() => void |
Enter finger-follow. Wire to a pan start. |
drag |
(deltaPx: number) => void |
Content-space delta for this frame; a vertical list passes -c.fdy. |
endDrag |
(releaseVelocity: number) => void |
Release; a vertical list passes -c.vy. Decides between fling, edge spring, snap, and settling in place. |
scrollTo |
(to: number, opts?: { durMs?: number } | { immediate: true }) => void |
Programmatic scroll, tweening over 200 ms by default. |
scrollBy |
(delta: number, opts?: { durMs?: number } | { immediate: true }) => void |
Same, relative to the in-flight target when one exists. |
stop |
() => void |
Freeze in place. No settle callback. |
nudge |
(delta: number) => void |
Move the chase target by a delta — the d-pad and analog primitive. |
chaseTo |
(to: number) => void |
Chase an absolute target (focus-follow, stick-to-bottom). |
rebase |
(delta: number) => void |
Shift the offset and every in-flight anchor by delta. |
intent |
() => number |
Where the scroller is heading: the chase or tween target when one is in flight, the current offset otherwise. |
isAtEnd |
(slackPx?: number) => boolean |
Whether the range end is reached, judged on intent() rather than the current position. slackPx defaults to 1. |
projectFling |
(v: number) => number |
Rest position a fling from v would reach. Use it inside a snap function. |
step |
() => void |
Advance one frame. Call once per frame from onFrame. |
rebase exists for prepends. Shifting only the offset would leave a drag
position, a chase target, a spring bound, or a tween's endpoints pointing at
pre-prepend coordinates; rebase moves all of them together, so backfilling
content above the viewport never moves what the reader is looking at, even
mid-fling.
bindDpadScroll
function bindDpadScroll(s: Scroller, opts?: DpadScrollOptions): voidRegisters an onFrame hook that turns held UP/DOWN and the analog stick into
nudge calls against the scroller's chase target. The caller still owns
step().
| Field | Type | Default | Description |
|---|---|---|---|
stepPx |
number |
6 |
Px per held frame of UP/DOWN. |
nubPx |
number |
10 |
Px per frame at full analog deflection. |
active |
() => boolean |
always on | Gate the hook, for example () => !osk.isOpen(). Raw button reads are not muted by a touch or button block. |
@pocketjs/framework/platform
const platform: {
readonly target: string;
readonly pixelRatio: number;
readonly features: Readonly<Partial<Record<PocketCapabilityId, boolean>>>;
};
function hasFeature(feature: PocketCapabilityId): booleanThe manifest compiler defines the target-specific constants consumed by this
module. pixelRatio is
physical raster samples per logical pixel (1 on PSP, 2 on Vita), useful for
runtime-created textures; it never changes layout coordinates. Literal
hasFeature("…") calls are folded to booleans during the PocketJS compile, so
an unavailable enhancement branch can leave the bundle. Computed ids use the
frozen runtime map.
@pocketjs/framework/clock
| Export | Purpose |
|---|---|
simulationHz() |
Active virtual-frame rate selected by the host. |
ticksPerFrame() |
Exact core ticks advanced per virtual frame. |
virtualFrame() |
Deterministic frame index. |
virtualNow() |
Virtual seconds since boot. |
after(seconds, callback) |
Schedule on the virtual clock; returns a disposer. |
Use these instead of wall-clock timers for deterministic app behavior: the same elapsed virtual time produces the same trajectory at every rate.
The tick rate is declared per realm and baked per bundle, not fixed at 60.
tools/build.ts writes it in from --hz=N — an integer from 1 through 240,
default 60 — and the core takes it through set_tick_rate before the first
tick (MAX_TICK_HZ is 240 in engine/core/src/lib.rs). simulationHz() is
the host-selected virtual-frame rate and is a divisor of the baked rate, which
is what makes ticksPerFrame() a whole number. A native mount whose
ui.__tickHz disagrees with the baked rate throws before anything renders
(assertNativeHostContract, framework/src/host.ts): a bundle runs at the
rate it was built for and no other.
@pocketjs/framework/effects
type EffectDriver = (command: EffectCommand, deliver: (result: unknown) => void) => void;
function installEffectDriver(driver: EffectDriver): void
function runEffect<T>(kind: string, payload: unknown, onResult: (result: T) => void): numberrunEffect emits an outside-world command; its driver can complete at any
time, but PocketJS delivers the result only at the next frame boundary. This
callback surface keeps promise and microtask timing out of deterministic
journeys. A host-injected globalThis.__pocketEffectDriver overrides the app
driver for replay and simulation.
@pocketjs/framework/net
A bounded HTTP client over the host's net namespace. Declare net.http in
the manifest's requires; where no host mounts the module every call rejects
with code unavailable.
import { fetch } from "@pocketjs/framework/net";
const response = await fetch("https://api.example.com/items", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ name: "Pocket" }),
timeoutMs: 5_000,
maxBytes: 64 * 1024,
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();function fetch(url: string, options?: FetchOptions): Promise<PocketResponse>url must be an absolute http:// or https:// URL. options.method is one
of GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS (default GET;
GET and HEAD reject a body), options.body a string, Uint8Array, or
ArrayBuffer, and header names are lower-cased and rejected when malformed.
PocketResponse carries status, ok, url, headers, byteLength, and
the buffered reads text(), json(), bytes(), arrayBuffer().
The response is whole-body — the promise resolves once the body is complete
and holds it — so the omitted surface is streams, cookies, cache, Request,
Headers, AbortSignal, WebSocket, servers, and raw sockets.
| Resource | Limit |
|---|---|
| Concurrent requests | 2 |
| Request body | 64 KiB |
| Response body | 128 KiB default, 256 KiB maximum |
| Headers | 32 fields / 8 KiB |
| Timeout | 30 s default, 120 s maximum |
| Redirects | 3 |
A fetch settles at a tick boundary and never inside a native callback: the
first pending request registers a service pump that makes one net.poll()
call per tick and settles the whole batch that call returns, and the last
completion removes the pump. An HTTP status never rejects — a 404 resolves
with ok === false. A transport failure rejects with a NetError whose
code is one of unavailable, invalid_request, busy, dns, connect,
tls, timeout, redirect, response_too_large, protocol, cancelled,
other.
The two data modules mount their own namespaces beside ui (globalThis.db,
globalThis.fs). Every op is synchronous and completes inside the guest's
turn: no events, no clock, no promises. Names and paths resolve under the
app's own data root, and the vocabulary cannot spell another app's tree or an
absolute path. Absence is not a no-op — every entry point throws where the
namespace is unmounted, so declare data.sqlite or data.fs in the
manifest's requires and let admission catch a missing module before the app
runs.
@pocketjs/framework/db
SQLite in the bun:sqlite shape, so code written against Bun's built-in
driver runs against the mounted module unchanged.
import { Database } from "@pocketjs/framework/db";
const db = new Database("notes.sqlite");
db.exec("CREATE TABLE IF NOT EXISTS note (id INTEGER PRIMARY KEY, body TEXT)");
const insert = db.query("INSERT INTO note (body) VALUES (?)");
db.transaction(() => {
insert.run("first");
insert.run("second");
})();
for (const row of db.query("SELECT id, body FROM note").all()) {
console.log(row.id, row.body);
}| Member | Signature | Description |
|---|---|---|
new Database |
(name?: string) |
Open or create a database under the app's data root; the default is ":memory:" (DB_MEMORY). Throws where globalThis.db is unmounted or the host refuses the name. |
query |
(sql: string) => Statement |
A Statement cached on this Database by SQL text. The prepared handle is host-side and keyed by the same string, so there is nothing to finalize and no handle to leak. |
prepare |
(sql: string) => Statement |
The uncached spelling. |
run |
(sql: string, params?: SqlParams) => RunResult |
One statement for effect, through the cache. |
exec |
(sql: string) => void |
One or more statements with no parameters and no result rows — the schema and migration path. |
transaction |
(fn: (...args) => R) => (...args) => R |
Wraps fn in BEGIN/COMMIT, ROLLBACK on throw; a nested call becomes a SAVEPOINT. Returns the wrapped function — call it. |
close |
() => void |
Drop the cached statements and close the handle. |
A Statement takes positional values (stmt.all(1, "x")), one array, or one
named-parameter object, and reads back through get() (first row as a
column-keyed object, or null), all() (every row as objects), values()
(every row as an array in column order), or run()
({ changes, lastInsertRowid }). columnNames holds the last execution's
columns and is empty before the first run.
A cell or a bound value is null, a number, a string, a boolean, or a
Uint8Array for a BLOB. An integer whose magnitude exceeds
DB_MAX_SAFE_INTEGER (2^53 − 1) throws instead of losing precision, and a
non-finite number throws the same way — store money in cents.
@pocketjs/framework/fs
Files in the Bun shape — file(path) plus the node:fs sync subset Bun
implements — over the nine-op fs spec (read, write, remove, list,
stat, mkdir, rename, usage, lastError). Every call is synchronous,
and await on a plain value unwraps it, so await file(p).text() and
await write(p, data) both run here.
import { file, write, readdirSync } from "@pocketjs/framework/fs";
write("saves/slot1.json", JSON.stringify(state));
if (file("saves/slot1.json").exists()) restore(file("saves/slot1.json").json());
for (const name of readdirSync("saves")) console.log(name);| Export | Signature | Description |
|---|---|---|
file |
(path: string) => PocketFile |
A lazy handle; nothing is read until a method runs. |
PocketFile |
.size, .exists(), .bytes(), .text(), .json(), .delete() |
size is 0 for a missing file (Bun's behavior); exists() is true only for a file. |
write |
(path, data: string | Uint8Array) => number |
Replace the file, creating parent directories. Returns bytes written. |
usage |
() => { usedBytes: number; quotaBytes: number } |
The app's storage footprint and budget; quotaBytes: 0 is unmetered. |
readFileSync |
(path, encoding?: "utf8") => Uint8Array | string |
Bytes, or a string with "utf8". |
writeFileSync / appendFileSync |
(path, data) => void |
Truncating and appending writes. |
mkdirSync |
(path) => void |
Recursive and idempotent: every missing ancestor is created. |
readdirSync |
(path, options?: { withFileTypes: true }) => string[] | DirEntry[] |
Names, or { name, kind, size, isFile(), isDirectory() } entries. |
rmSync |
(path, options?: { recursive?: boolean; force?: boolean }) => void |
recursive removes a tree; force swallows "not found". |
renameSync |
(from, to) => void |
Move within the data root. |
statSync |
(path) => { size, isFile(), isDirectory() } |
Throws where the path does not exist. |
existsSync |
(path) => boolean |
True for a file or a directory. |
A path carries at most FS_MAX_DEPTH (8) segments, 64 bytes per segment and
160 bytes in total. The SDK chunks reads and writes at FS_MAX_IO_BYTES
(64 KiB), so a file's size is bounded by storage and any host quota rather
than by the marshaling ceiling.
@pocketjs/framework/audio
Streams s16 PCM to the host's audio namespace on a credit budget. Declare
audio.pcm in the manifest. Where no host mounts the module every player call
is a no-op and the app's tick-driven UI stays byte-identical, so an audio
enhancement needs no branch around it.
import { createWavPlayer } from "@pocketjs/framework/audio";
import { onFrame } from "@pocketjs/framework/lifecycle";
const player = createWavPlayer();
player.load("theme"); // the pak entry audio:wav.theme
player.play();
onFrame(() => player.pump());pump() must be called once per frame from the app's onFrame. It drains
the tick's event batch — the host's credit, underrun, and ended events — then
refills the ring with at most one writePcm inside the credit the host
granted, which is what keeps a frame's hot path free of host queries. Nothing
plays without it: a track played before its first feed opens the tap inside
the first pump() that pours into it.
| Export | Signature | Description |
|---|---|---|
decodeWav |
(bytes: Uint8Array) => WavPcm |
Parse RIFF/WAVE into { sampleRate, channels, frames, data }. Throws on anything but 16-bit PCM, mono or stereo, at a rate in AUDIO_RATES. |
createWavPlayer |
() => WavPlayer |
A player holding one track at a time. |
WavPlayer is load(name) (a pak audio:wav.<name> entry) or
loadPcm(pcm), play(), pause(), toggle(), stop() (flush the ring and
rewind to frame 0), setVolume(0..1), pump(), playing(),
positionFrames(), durationFrames(), stats() ({ underruns }), and
dispose().
@pocketjs/framework/launcher
The guest side of whole-app switching on hosts that embed several bundles,
over spec ops 39–41 (appTable, appLaunch, appShot). A single-app host
omits the ops and each function degrades, so a launcher bundle stays
admissible anywhere and renders its empty state.
| Export | Signature | Description |
|---|---|---|
launcherActive |
() => boolean |
Whether the active host can switch apps. |
appTable |
() => AppTable | null |
{ apps: { output, id, title }[], current, resume } in registry order; null without the op. current is the running bundle's output name; resume is the app the last SELECT summon interrupted, null after a cold boot or an explicit launch. |
launchApp |
(output: string) => boolean |
Request a whole-guest switch; the host swaps after the current frame presents. false for an unknown output or a host without switching. |
frozenShot |
() => number |
Texture handle of the frame the summon froze (256×128 PSM_8888), -1 when none was captured. Bind it with registerTexture(key, handle) and draw it as <Image src={key}>. |
Resuming is launchApp(resume), a fresh relaunch — this protocol has no
suspend.
@pocketjs/framework/hot
function text(node: NodeMirror | undefined, value: string | number): void
function prop(node: NodeMirror | undefined, name: PropName, value: number): voidAn imperative, last-value-gated escape hatch for high-frequency HUD updates. Do not bind the same value reactively as well. Prefer paint-only transforms to layout properties and keep hot text inside a fixed-size cell.
@pocketjs/framework/manifest
This is the stable build/custom-host boundary:
| Export | Purpose |
|---|---|
POCKET_MANIFEST_SCHEMA_ID / POCKET_MANIFEST_VERSION |
Canonical format-2 identity. |
pocketManifestV2Schema / validatePocketManifest |
Strict schema and structured diagnostics. |
extractHostBuildInputs(plan, options?) |
Verify a plan checksum and project it onto stable native-host inputs. |
hostBuildEnvironment(inputs, options) |
Produce the target-neutral Cargo environment for a custom host. |
vitaTitleId(applicationId) |
Deterministically derive a stable Vita Title ID. |
The complete ResolvedBuildPlan is internal build IR. Custom hosts should
consume HostBuildInputs rather than retaining or reinterpreting the plan.
See Platform contracts.
Try any of these live in the playground, or start from Getting started.