@rezi-ui/node¶
Node/Bun backend package:
- configurable engine execution mode (
auto|worker|inline) - transfer of drawlists/events between core and native
- buffer pooling and scheduling
Install¶
What you get¶
- A backend implementation that satisfies the
@rezi-ui/coreruntime backend interface - Worker and inline execution paths for the native engine
- A stable message protocol for worker mode
- Integration with
@rezi-ui/native(prebuilt binaries when available)
Execution mode¶
Set config.executionMode on createNodeApp(...):
auto(default): inline whenfpsCap <= 30; otherwise prefer worker and fall back to inline when no TTY ornativeShimModuleis availableworker: always run the engine on a worker threadinline: run the engine inline on the main JS thread
Creating an app (recommended)¶
import { createNodeApp } from "@rezi-ui/node";
const app = createNodeApp({
initialState: { count: 0 },
config: {
executionMode: "auto",
fpsCap: 60,
maxEventBytes: 1 << 20,
},
});
createNodeApp is the default path because it keeps core/backend config in
lockstep:
- app/backend
maxEventBytes - app/backend
fpsCap
Hot State-Preserving Reload (HSR)¶
createNodeApp(...) has first-class HSR wiring through hotReload.
import { ui } from "@rezi-ui/core";
import { createNodeApp } from "@rezi-ui/node";
const app = createNodeApp({
initialState: { count: 0 },
hotReload: {
viewModule: new URL("./screens/main-screen.ts", import.meta.url),
moduleRoot: new URL("./src", import.meta.url),
},
});
app.view((state) => ui.text(`count=${String(state.count)}`));
await app.run(); // starts/stops hot reload watcher with app lifecycle
Route-managed apps use routesModule:
const app = createNodeApp({
initialState: { count: 0 },
routes,
initialRoute: "home",
hotReload: {
routesModule: new URL("./screens/index.ts", import.meta.url),
moduleRoot: new URL("./src", import.meta.url),
resolveRoutes: (moduleNs) => {
const routesExport = (moduleNs as { routes?: unknown }).routes;
if (!Array.isArray(routesExport)) {
throw new Error("Expected `routes` array export");
}
return routesExport;
},
},
});
app.hotReload exposes the controller (reloadNow(), isRunning()) when you need
manual trigger points. For advanced/custom lifecycle control, low-level
createHotStateReload(...) remains available.
What this does:
- watches source paths for changes
- re-imports the target module from a fresh module snapshot
- calls either
app.replaceView(...)orapp.replaceRoutes(...)without restarting the process
What stays intact across reload:
- app state (
app.update) - focused widget (when the same
idstill exists) - local widget hook state (
defineWidget) when keys/ids remain stable
Current scope:
- widget-mode apps (
app.view/app.replaceView) - route-managed apps (
createNodeApp({ routes, initialRoute })+app.replaceRoutes) - not raw draw mode
NO_COLOR behavior¶
createNodeApp(...) checks process.env.NO_COLOR at app construction time.
When present, Rezi forces a monochrome theme and exposes:
This supports CI and accessibility tooling that relies on the no-color.org convention.
Backend access (advanced)¶
Most apps should use createNodeApp() so app/core and backend settings stay
aligned automatically.
If you need direct backend access (benchmarks/custom runners), you can either:
- read it off
createNodeApp(...)viaapp.backend, or - construct it directly with
createNodeBackend()and pass it tocreateApp()from@rezi-ui/core.
Native engine config passthrough¶
createNodeApp({ config: { nativeConfig } }) forwards nativeConfig to the
native layer’s engine creation config.
Keys are forwarded as-is. If you want a close match to the engine’s public C structs, use snake_case field names:
import { createNodeApp } from "@rezi-ui/node";
const app = createNodeApp({
initialState: { count: 0 },
config: {
fpsCap: 60,
nativeConfig: {
target_fps: 60, // must match fpsCap when provided
limits: {
dl_max_total_bytes: 16 << 20,
},
},
},
});
See: