@myexamsai/revelio
v3.0.1
Published
Revelio — Server-Driven UI library for React and React Native. Your server defines the UI as JSON, Revelio renders it live with surgical per-node re-renders, Zod validation, real-time WebSocket updates, and cross-node subscriptions.
Maintainers
Readme
@myexamsai/revelio
Server-Driven UI for React and React Native.
Your server ships JSON. Revelio renders it live — with surgical per-node re-renders, offline caching, real-time SSE updates, network-aware asset resolution, and full TypeScript + Zod support.
Table of Contents
- Installation
- Quick Start
- Blueprint JSON Format
- Component Factories
- Architecture Overview
- API Reference
- Types Reference
Installation
npm install @myexamsai/revelio zodWeb peer deps (already a transitive dep of most React setups):
npm install react react-domReact Native peer deps:
npm install react-native react-native-sse @react-native-community/netinfoidb is a direct dependency (used by the Reel web backend) — it installs automatically.
Quick Start
Web
// app/page.tsx (Next.js App Router, or any React web app)
import { useBlueprint } from '@myexamsai/revelio/web'
import { createBroadcast } from '@myexamsai/revelio/web'
import { createReel } from '@myexamsai/revelio/web'
import { createSignal } from '@myexamsai/revelio/web'
import { createLens } from '@myexamsai/revelio/web'
import { useBlock } from '@myexamsai/revelio/web'
import { z } from 'zod'
// 1. Define your component factories
const Hero = (nodeId, parentPath, renderNode) => {
const { state } = useBlock({
nodeId,
schema: z.object({ title: z.string(), subtitle: z.string().optional() }),
})
return (
<section>
<h1>{state?.title}</h1>
{state?.subtitle && <p>{state.subtitle}</p>}
{renderNode(nodeId + '-cta', [...parentPath, nodeId])}
</section>
)
}
const components = { hero: Hero, button: Button, text: Text }
// 2. Create optional subsystems once (module scope or useMemo)
const reel = createReel({ ttl: 300_000 }) // 5-min IndexedDB cache
const signal = createSignal() // network-quality detection
const broadcast = createBroadcast({ // SSE real-time updates
url: 'https://api.example.com/sse/home-page',
})
const lens = createLens((asset, { dpr, tier }) => { // ImageKit resolver
const quality = tier === 'strong' ? 85 : tier === 'good' ? 70 : 50
const scale = Math.min(dpr, tier === 'strong' ? 2 : tier === 'good' ? 1.5 : 1)
return `https://ik.imagekit.io/myapp/${asset.id}?tr=q-${quality},dpr-${scale}`
})
// 3. Render
export default function Page() {
const { renderer, status, error } = useBlueprint({
url: 'https://api.example.com/blueprints/home-page',
components,
reel,
signal,
broadcast,
lens,
})
if (status === 'loading') return <Skeleton />
if (status === 'error') return <ErrorBanner error={error} />
return renderer
}React Native
import { useBlueprint } from '@myexamsai/revelio/native'
import { createBroadcast } from '@myexamsai/revelio/native'
import { createNativeReel } from '@myexamsai/revelio/native'
import { createNativeSignal } from '@myexamsai/revelio/native'
import NetInfo from '@react-native-community/netinfo'
import AsyncStorage from '@react-native-async-storage/async-storage'
const reel = createNativeReel(AsyncStorage, { ttl: 300_000 })
const signal = createNativeSignal(NetInfo)
const broadcast = createBroadcast({ url: 'https://api.example.com/sse/home' })
export default function HomeScreen() {
const { renderer, status } = useBlueprint({
url: 'https://api.example.com/blueprints/home',
components,
reel,
signal,
broadcast,
})
if (status === 'loading') return <LoadingSpinner />
return renderer
}Blueprint JSON Format
Revelio consumes Blueprint documents delivered as JSON from your server.
Structural Blueprint (full render)
{
"version": "1.0.0",
"metadata": {
"id": "home-page",
"kind": "structural",
"updatedAt": "2025-01-15T10:30:00Z"
},
"root": {
"id": "root",
"type": "page",
"state": { "title": "Home" },
"children": [
{
"id": "hero-1",
"type": "hero",
"state": {
"title": "Welcome back",
"subtitle": "Here is what's new today."
},
"children": [
{
"id": "cta-1",
"type": "button",
"state": { "label": "Get started", "variant": "primary" }
}
]
}
]
},
"variables": {
"userId": "u_abc123",
"theme": "dark"
}
}Delta Blueprint (incremental update)
Sent by the server over SSE to patch only changed nodes. kind: "update" is required, as is a monotonically increasing sequenceNumber. Stale or duplicate sequence numbers are discarded automatically.
{
"version": "1.0.0",
"metadata": {
"id": "home-page",
"kind": "update",
"sequenceNumber": 42
},
"root": { "id": "root", "type": "page", "state": {} },
"nodes": [
{
"id": "hero-1",
"type": "hero",
"state": {
"title": "New title from the server",
"subtitle": "Updated in real time."
}
}
]
}Only the nodes listed in nodes are updated. Every other node is untouched.
Metadata fields
| Field | Type | Description |
|---|---|---|
| id | string | Document identifier. |
| kind | 'structural' \| 'update' | Defaults to 'structural' if omitted. |
| sequenceNumber | number | Required for kind: 'update'. Duplicate/stale numbers are discarded. |
| updatedAt | string | ISO timestamp used as part of the identity check in Screen. |
Component Factories
A ComponentFactory is a function that receives a node ID and parent path and returns a React element (or null). It is called for every node in the Blueprint tree.
type ComponentFactory = (
id: string,
parentPath: ParentPath,
renderNode: RenderNodeFn,
) => React.ReactElement | nullimport { useBlock } from '@myexamsai/revelio'
import { z } from 'zod'
const CardSchema = z.object({
title: z.string(),
imageId: z.string().optional(),
})
const Card: ComponentFactory = (nodeId, parentPath, renderNode) => {
const { state, children } = useBlock({ nodeId, schema: CardSchema })
return (
<div className="card">
<h2>{state?.title}</h2>
{children.map((childId) => renderNode(childId, [...parentPath, nodeId]))}
</div>
)
}Register your factories in the components map and pass it to useBlueprint or Screen:
const components = {
card: Card,
hero: Hero,
button: Button,
text: Text,
}Architecture Overview
Server (JSON)
│ HTTP fetch + SSE stream
▼
useBlueprint
├── Reel.get() ── immediate render from cache (IndexedDB / AsyncStorage)
├── fetch(url) ── skipped when Signal tier is 'offline'
├── Stage.receive() ── normalize + store the Blueprint
├── Reel.store() ── persist to cache
└── Broadcast (SSE)
└── Stage.receive() + Reel.patch() ── live delta updates
Stage (store)
├── Ledger ── flat map of node id → node state (normalized)
├── Oracle ── per-node pub/sub for surgical re-renders
├── Archivist ── routing: structural → full rebuild, update → delta merge
└── Vars ── global Blueprint variables
React tree
└── RevelioProvider
└── ScreenInner
└── useRenderBlock
└── ComponentFactory → useBlock (per node)Every component subscribes only to its own node via useBlock. A delta update that changes one node triggers a re-render of exactly one component.
API Reference
useBlueprint
The primary integration hook. Fetches a Blueprint, manages offline caching, wires up SSE real-time updates, and returns a ready-to-render element.
import { useBlueprint } from '@myexamsai/revelio/web' // web
import { useBlueprint } from '@myexamsai/revelio/native' // React Nativefunction useBlueprint(options: UseBlueprintOptions): UseBlueprintReturnUseBlueprintOptions
| Prop | Type | Required | Description |
|---|---|---|---|
| url | string | ✓ | URL of the Blueprint endpoint. |
| components | Record<string, ComponentFactory> | ✓ | Map of node type → factory. |
| componentOverrides | { byNodeId?, byNodeType? } | | Override factories per node id or type. |
| fetchOptions | RequestInit | | Options forwarded to fetch(). |
| reel | Reel | | Offline cache. Created with createReel() / createNativeReel(). |
| signal | Signal | | Network quality detector. When tier is 'offline', the HTTP fetch is skipped and the cached Blueprint is used instead. |
| broadcast | BroadcastChannel | | SSE channel for real-time updates. Created with createBroadcast(). |
| lens | Lens | | Asset resolver for images. Created with createLens(). |
| onLayoutChange | (bp: Blueprint) => void | | Called when a new structural Blueprint is applied. |
| onError | (err: Error) => void | | Called on fetch/parse errors. |
UseBlueprintReturn
| Field | Type | Description |
|---|---|---|
| renderer | React.ReactElement \| null | The fully rendered UI tree. Render this directly. |
| status | RevelioStatus | 'idle' \| 'loading' \| 'success' \| 'error' |
| error | Error \| null | Set on fetch or validation failure. |
| stage | Stage | The underlying store for imperative operations. |
| dispatch | (action: RevelioAction) => Promise<RevelioActionResponse> | Fire an action to your server. |
Hydration sequence
- Reel.get() — If a cached Blueprint exists, render it immediately (instant first paint).
- fetch(url) — Unless
signal.getTier() === 'offline', fetch a fresh Blueprint from the server. - Stage.receive(fresh) — Replace or delta-merge the store.
- Reel.store(fresh) — Persist the fresh Blueprint to cache.
- Broadcast (SSE) — On every SSE message:
Stage.receive(delta)+Reel.patch(delta).
Screen
Standalone renderer for when you manage your own Stage instance. No internal store is created — the component identity-tracks the document via metadata.id + version + metadata.updatedAt.
import { Screen } from '@myexamsai/revelio'<Screen
document={blueprint}
components={components}
componentOverrides={{ byNodeId: { 'hero-1': CustomHero } }}
onLayoutChange={(bp) => console.log('new blueprint', bp)}
onError={(err) => console.error(err)}
/>Props
| Prop | Type | Required | Description |
|---|---|---|---|
| document | Blueprint | ✓ | The Blueprint to render. |
| components | Record<string, ComponentFactory> | | Custom component map. |
| componentOverrides | { byNodeId?, byNodeType? } | | Factory overrides per node. |
| onLayoutChange | (bp: Blueprint) => void | | Called when the document identity changes. |
| onError | (err: Error) => void | | Called on invalid Blueprint input. |
Stage
The central store. Holds all node state, manages subscriptions, and processes Blueprint updates.
import { Stage } from '@myexamsai/revelio'
const stage = new Stage()
stage.receive(blueprint)Constructor
new Stage(initialState?, options?)| Param | Type | Description |
|---|---|---|
| initialState | Partial<SduiLayoutStoreState> | Optional seed state (useful for SSR). |
| options | SduiLayoutStoreOptions | { componentOverrides? } |
Methods
| Method | Signature | Description |
|---|---|---|
| receive | (blueprint: Blueprint) => void | Apply a Blueprint. Routes to a full Ledger rebuild (kind: 'structural') or incremental delta merge (kind: 'update'). This is the only write method you need in normal operation. |
| updateNode | (id: string, state: Record<string, unknown>) => void | Imperatively update a single node's state and set the edited flag. |
| cancelEdit | () => void | Clear the edited flag without reverting state. |
| reset | () => void | Revert all node state to the last received structural Blueprint. |
| clear | () => void | Hard teardown — clears all state and subscriptions. |
| getBlueprint | () => Blueprint \| null | Returns the current Blueprint (denormalized from Ledger). |
| subscribeVersion | (cb: () => void) => () => void | Subscribe to any store change. Returns an unsubscribe function. |
| getSnapshot | () => number | Returns the current version counter (for useSyncExternalStore). |
| getServerSnapshot | () => number | Always returns 0 (for SSR hydration). |
State
stage.state // SduiLayoutStoreState| Field | Type | Description |
|---|---|---|
| rootId | string \| null | ID of the root node. |
| nodes | Record<string, SduiLayoutNode> | Flat map of all nodes (normalized). |
| variables | Record<string, unknown> | Global Blueprint variables. |
| version | number | Increments on every change. |
| isEdited | boolean | True after updateNode() until cancelEdit() or reset(). |
RevelioProvider / useRevelioContext
RevelioProvider makes a Stage (and optionally Lens, Signal, Reel) available to the component tree below it. useBlueprint sets this up automatically — use these directly only when you are managing your own Stage.
import { RevelioProvider, useRevelioContext } from '@myexamsai/revelio'
<RevelioProvider store={stage} lens={lens} signal={signal}>
<ScreenInner id={rootId} componentMap={components} />
</RevelioProvider>const { store, lens, signal, reel } = useRevelioContext()RevelioContextValue
| Field | Type |
|---|---|
| store | Stage |
| lens | Lens \| undefined |
| signal | Signal \| undefined |
| reel | Reel \| undefined |
useBlock
Subscribe to a single node. Re-renders only when that specific node's state changes (useSyncExternalStore under the hood — tearing-safe with React 18+ concurrent mode).
import { useBlock } from '@myexamsai/revelio'const MyButton: ComponentFactory = (nodeId) => {
const { state, children, type } = useBlock({
nodeId,
schema: z.object({ label: z.string(), variant: z.enum(['primary', 'secondary']) }),
})
return <button data-variant={state?.variant}>{state?.label}</button>
}UseBlockParams
| Param | Type | Required | Description |
|---|---|---|---|
| nodeId | string | ✓ | The node to subscribe to. |
| schema | ZodSchema<TSchema> | | Zod schema to validate and type state. |
UseBlockReturn
| Field | Type | Description |
|---|---|---|
| state | TSchema \| undefined | Validated node state. undefined if the node is absent or validation fails. |
| children | string[] | Ordered list of child node IDs. |
| type | string \| undefined | Node type string (e.g. 'hero', 'button'). |
| node | SduiLayoutNode \| undefined | The full raw node object. |
useNodeRef
Subscribe to one or more nodes by reference — useful for cross-node data (e.g. a tooltip that reads state from an unrelated node).
import { useNodeRef } from '@myexamsai/revelio'const Modal: ComponentFactory = (nodeId) => {
// source node has state.targetId = 'hero-1'
const { nodes } = useNodeRef({
nodeId,
refKey: 'targetId',
schema: z.object({ title: z.string() }),
})
const target = nodes[0]
return <div>{target?.state?.title}</div>
}UseNodeRefParams
| Param | Type | Required | Description |
|---|---|---|---|
| nodeId | string | ✓ | Source node that holds the reference. |
| refKey | string | ✓ | Key in state whose value is a node ID (or array of node IDs) to watch. |
| schema | ZodSchema | | Zod schema applied to each referenced node's state. |
Return
| Field | Type | Description |
|---|---|---|
| nodes | ReferencedNodeInfo[] | Array of { id, type, state, children } for each referenced node. |
useRenderBlock / useRenderNode
Low-level hook used internally to recursively render child nodes. You rarely call this directly — the renderNode argument passed to your factory already wraps it.
import { useRenderBlock } from '@myexamsai/revelio'
const { renderNode } = useRenderBlock({
nodeId: rootId,
componentMap: components,
parentPath: [],
})
return renderNode(rootId, [])useRenderNode is an alias for useRenderBlock.
useSduiLayoutAction
Returns the Stage store imperatively from within a component tree rendered by RevelioProvider. Useful for firing local mutations without going through dispatch.
import { useSduiLayoutAction } from '@myexamsai/revelio'
const stage = useSduiLayoutAction()
stage.updateNode('hero-1', { title: 'Edited locally' })Broadcast (SSE)
Real-time updates via Server-Sent Events. On disconnect, createBroadcast waits one second, re-fetches the full Blueprint first, then reconnects — so you never miss a structural change during an outage.
import { createBroadcast } from '@myexamsai/revelio/web' // uses native EventSource
import { createBroadcast } from '@myexamsai/revelio/native' // uses react-native-sseconst broadcast = createBroadcast({
url: 'https://api.example.com/sse/home-page',
eventName: 'blueprint', // default: 'message'
withCredentials: true,
onMessage: (blueprint) => {
stage.receive(blueprint)
},
onError: (err) => console.error('SSE error', err),
onStatusChange: (status) => console.log('SSE status:', status),
})
broadcast.connect()
// later:
broadcast.disconnect()When passed to useBlueprint, the connect() / disconnect() lifecycle and onMessage wiring are managed automatically.
BroadcastOptions
| Option | Type | Default | Description |
|---|---|---|---|
| url | string | required | SSE endpoint URL. |
| eventName | string | 'message' | SSE event type to listen for. |
| withCredentials | boolean | false | Set EventSource.withCredentials. |
| onMessage | (bp: Blueprint) => void | | Called for each inbound Blueprint. |
| onError | (err: Error) => void | | Called on SSE errors. |
| onStatusChange | (s: 'connecting' \| 'live' \| 'offline') => void | | Connection state change callback. |
BroadcastChannel
| Member | Type | Description |
|---|---|---|
| connect() | () => void | Open the SSE connection. |
| disconnect() | () => void | Close the SSE connection. |
| status | 'connecting' \| 'live' \| 'offline' | Current connection state. |
Signal (Network Quality)
Signal detects the user's network quality and exposes a SignalTier. Revelio uses it to skip HTTP fetches when offline and to cap image DPR/quality in Lens.
import { createSignal } from '@myexamsai/revelio/web' // web: navigator.connection
import { createNativeSignal } from '@myexamsai/revelio/native' // native: NetInfo// Web
const signal = createSignal()
// React Native
import NetInfo from '@react-native-community/netinfo'
const signal = createNativeSignal(NetInfo)
signal.getTier() // 'strong' | 'good' | 'weak' | 'offline'
const unsub = signal.subscribe((tier) => console.log('tier changed:', tier))
unsub() // unsubscribeSignalTier
| Tier | Meaning | Lens DPR cap | Lens quality |
|---|---|---|---|
| 'strong' | Fast WiFi / 5G | 2.0× | 85 |
| 'good' | LTE / decent WiFi | 1.5× | 70 |
| 'weak' | 3G or degraded | 1.0× | 50 |
| 'offline' | No connectivity | — | — (fetch skipped) |
When @react-native-community/netinfo or navigator.connection is not available, createSignal / createNativeSignal return a stub that always reports 'strong'.
Signal interface
interface Signal {
getTier(): SignalTier
subscribe(cb: (tier: SignalTier) => void): () => void
}Access inside components with useSignal():
import { useSignal } from '@myexamsai/revelio'
const tier = useSignal() // SignalTier | nullLens (Asset Resolution)
Lens resolves asset references (IDs or URLs) into final image URLs, DPR-capped and quality-adjusted for the current network tier.
import { createLens, useImageResolver } from '@myexamsai/revelio/web'const lens = createLens((asset, { containerWidth, dpr, tier }) => {
const quality = tier === 'strong' ? 85 : tier === 'good' ? 70 : 50
const cappedDpr = Math.min(dpr, tier === 'strong' ? 2 : tier === 'good' ? 1.5 : 1)
const w = Math.round(containerWidth * cappedDpr)
if ('url' in asset) return asset.url // passthrough for bare URLs
// Example: ImageKit
return `https://ik.imagekit.io/myapp/${asset.id}?tr=w-${w},q-${quality}`
})Pass lens to useBlueprint. Inside component factories, use useImageResolver:
import { useImageResolver } from '@myexamsai/revelio'
const HeroImage: ComponentFactory = (nodeId) => {
const { state } = useBlock({ nodeId })
const resolve = useImageResolver({ containerWidth: 800 })
if (!state?.image) return null
return <img src={resolve(state.image)} alt="" />
}createLens
function createLens(resolver?: LensResolver): LensCalled with no resolver, returns a passthrough lens (raw URL, or asset ID as a string).
LensResolver
type LensResolver = (asset: Asset, ctx: LensContext) => string
interface LensContext {
containerWidth: number
dpr: number // window.devicePixelRatio (read at call time, capped by Signal tier)
tier: SignalTier
}
type Asset = AssetRef | AssetUrl
interface AssetRef { id: string }
interface AssetUrl { url: string }useImageResolver
function useImageResolver(options: { containerWidth: number }): (asset: Asset) => stringReturns a resolver function that automatically reads the current dpr and tier from RevelioContext.
Reel (Offline Cache)
Reel persists Blueprints so the last-known UI is available instantly on the next load, before the network request completes.
import { createReel } from '@myexamsai/revelio/web' // IndexedDB via idb
import { createNativeReel } from '@myexamsai/revelio/native' // AsyncStorage// Web
const reel = createReel({ ttl: 300_000 }) // 5-minute TTL (optional, default: Infinity)
// React Native
import AsyncStorage from '@react-native-async-storage/async-storage'
const reel = createNativeReel(AsyncStorage, { ttl: 300_000 })Pass reel to useBlueprint and the full store → get → patch lifecycle is handled automatically.
Reel interface
interface Reel {
store(key: string, blueprint: Blueprint): Promise<void>
get(key: string): Promise<Blueprint | null>
patch(key: string, delta: Blueprint): Promise<void>
clear(key?: string): Promise<void>
}| Method | Description |
|---|---|
| store(key, blueprint) | Persist a full structural Blueprint under key. |
| get(key) | Retrieve a cached Blueprint. Returns null if absent or expired. |
| patch(key, delta) | Apply a delta Blueprint to the cached structural one in-place. |
| clear(key?) | Delete one entry (or all entries if key is omitted). |
The cache key is derived from the Blueprint URL. In SSR environments where IndexedDB is unavailable, Reel falls back to an in-memory store automatically — the API is always safe to call.
Types Reference
All types are re-exported from @myexamsai/revelio and the platform entry points.
// Blueprint document
import type { Blueprint, BlueprintNode, BlueprintMetadata } from '@myexamsai/revelio'
// Schema / node
import type { SduiLayoutDocument, SduiLayoutNode, SduiNode } from '@myexamsai/revelio'
// Store
import type { SduiLayoutStoreOptions, SduiLayoutStoreState } from '@myexamsai/revelio'
// Component system
import type {
ComponentFactory,
RenderNodeFn,
SduiComponentProps,
ParentPath,
} from '@myexamsai/revelio'
// Hooks
import type {
UseBlockParams,
UseBlockReturn,
UseNodeRefParams,
ReferencedNodeInfo,
UseRenderNodeParams,
UseRenderNodeReturn,
RevelioContextValue,
} from '@myexamsai/revelio'
// useBlueprint
import type {
UseBlueprintOptions,
UseBlueprintReturn,
RevelioAction,
RevelioActionResponse,
RevelioStatus,
} from '@myexamsai/revelio/web'
// Signal
import type { Signal, SignalTier } from '@myexamsai/revelio'
// Lens
import type { Lens, LensResolver, LensContext, Asset, AssetRef, AssetUrl } from '@myexamsai/revelio'
// Reel
import type { Reel } from '@myexamsai/revelio'
// Broadcast
import type { BroadcastChannel, BroadcastOptions } from '@myexamsai/revelio/web'
// Normalization utilities (advanced)
import {
normalizeSduiLayout,
normalizeSduiNode,
denormalizeSduiLayout,
denormalizeSduiNode,
} from '@myexamsai/revelio'
import type { NormalizedSduiEntities } from '@myexamsai/revelio'License
MIT © MyExamsAI
