Runtime DOM renderer for Constela UI framework
npm install @constela/runtimeExecutes Constela JSON programs in the browser with fine-grained reactivity.
``bash`
npm install @constela/runtime
Your JSON program:
`json`
{
"version": "1.0",
"state": {
"count": { "type": "number", "initial": 0 }
},
"actions": [
{
"name": "increment",
"steps": [{ "do": "update", "target": "count", "operation": "increment" }]
}
],
"view": {
"kind": "element",
"tag": "button",
"props": { "onClick": { "event": "click", "action": "increment" } },
"children": [{ "kind": "text", "value": { "expr": "state", "name": "count" } }]
}
}
Becomes an interactive app with:
- Reactive state management - Signal-based updates without virtual DOM
- Efficient DOM updates - Fine-grained reactivity
- Event handling - Declarative action binding
Update nested values without replacing entire arrays:
`json`
{
"do": "setPath",
"target": "posts",
"path": [5, "liked"],
"value": { "expr": "lit", "value": true }
}
Dynamic path with variables:
`json`
{
"do": "setPath",
"target": "posts",
"path": { "expr": "var", "name": "payload", "path": "index" },
"field": "liked",
"value": { "expr": "lit", "value": true }
}
Build dynamic strings from multiple expressions:
`json`
{
"expr": "concat",
"items": [
{ "expr": "lit", "value": "/users/" },
{ "expr": "var", "name": "userId" },
{ "expr": "lit", "value": "/profile" }
]
}
Useful for:
- Dynamic URLs: /api/posts/{id}btn btn-{variant}
- CSS class names: Hello, {name}!
- Formatted messages:
Pass multiple values to actions with object-shaped payloads:
`json`
{
"kind": "element",
"tag": "button",
"props": {
"onClick": {
"event": "click",
"action": "toggleLike",
"payload": {
"index": { "expr": "var", "name": "index" },
"postId": { "expr": "var", "name": "post", "path": "id" },
"currentLiked": { "expr": "var", "name": "post", "path": "liked" }
}
}
}
}
Each expression field in the payload is evaluated when the event fires. The action receives the evaluated object:
`json`
{ "index": 5, "postId": "abc123", "currentLiked": true }
Efficient list updates - only changed items re-render:
`json`
{
"kind": "each",
"items": { "expr": "state", "name": "posts" },
"as": "post",
"key": { "expr": "var", "name": "post", "path": "id" },
"body": { ... }
}
Benefits:
- Add/remove items: Only affected DOM nodes change
- Reorder: DOM nodes move without recreation
- Update item: Only that item re-renders
- Input state preserved during updates
Real-time data with declarative WebSocket:
`json`
{
"connections": {
"chat": {
"type": "websocket",
"url": "wss://api.example.com/ws",
"onMessage": { "action": "handleMessage" },
"onOpen": { "action": "connectionOpened" },
"onClose": { "action": "connectionClosed" }
}
}
}
Send messages:
`json`
{ "do": "send", "connection": "chat", "data": { "expr": "state", "name": "inputText" } }
Close connection:
`json`
{ "do": "close", "connection": "chat" }
Call methods on arrays, strings, Math, and Date:
`json
// Filter completed todos
{
"expr": "call",
"target": { "expr": "state", "name": "todos" },
"method": "filter",
"args": [{
"expr": "lambda",
"param": "todo",
"body": { "expr": "get", "base": { "expr": "var", "name": "todo" }, "path": "completed" }
}]
}
// Get array length
{ "expr": "call", "target": { "expr": "state", "name": "items" }, "method": "length" }
// Math.max
{
"expr": "call",
"target": { "expr": "var", "name": "Math" },
"method": "max",
"args": [{ "expr": "lit", "value": 10 }, { "expr": "state", "name": "count" }]
}
`
Supported methods:
- Array: length, at, includes, slice, indexOf, join, filter, map, find, findIndex, some, every
- String: length, charAt, substring, slice, split, trim, toUpperCase, toLowerCase, replace, includes, startsWith, endsWith, indexOf
- Math: min, max, round, floor, ceil, abs, sqrt, pow, random, sin, cos, tan
- Date: now, parse, toISOString, getTime, getFullYear, getMonth, getDate, getHours, getMinutes, getSeconds, getMilliseconds
Construct arrays dynamically from expressions:
`json`
{
"expr": "array",
"elements": [
{ "expr": "var", "name": "basicSetup" },
{ "expr": "call", "target": { "expr": "var", "name": "json" }, "method": "apply", "args": [] }
]
}
Use cases:
- CodeMirror extensions: [basicSetup, json()]
- Dynamic configuration arrays
- Combining variables, literals, and call results in a single array
`json`
{
"kind": "markdown",
"content": { "expr": "state", "name": "markdownContent" }
}
Rendered with marked and sanitized with DOMPurify.
`json`
{
"kind": "code",
"code": { "expr": "lit", "value": "const x: number = 42;" },
"language": { "expr": "lit", "value": "typescript" }
}
Features:
- Syntax highlighting with Shiki
- Dual theme support (light/dark)
- Built-in copy button
Components can have their own independent local state and actions:
`json`
{
"components": {
"Counter": {
"localState": {
"count": { "type": "number", "initial": 0 }
},
"localActions": [
{
"name": "increment",
"steps": [{ "do": "update", "target": "count", "operation": "increment" }]
}
],
"view": {
"kind": "element",
"tag": "button",
"props": { "onClick": { "event": "click", "action": "increment" } },
"children": [
{ "kind": "text", "value": { "expr": "state", "name": "count" } }
]
}
}
}
}
Features:
- Each instance has independent state
- Local actions operate on local state only
- state expressions check local state first, then fall back to globalset
- Supported steps: , update, setPath
Use cases: Accordions, dropdowns, form fields, toggles, tooltips
Server-rendered HTML is hydrated on the client without DOM reconstruction:
`json`
{
"version": "1.0",
"state": { "theme": { "type": "string", "initial": "light" } },
"lifecycle": {
"onMount": "loadTheme"
},
"actions": [
{
"name": "loadTheme",
"steps": [
{
"do": "storage",
"operation": "get",
"key": { "expr": "lit", "value": "theme" },
"storage": "local",
"result": "savedTheme",
"onSuccess": [
{ "do": "set", "target": "theme", "value": { "expr": "var", "name": "savedTheme" } }
]
}
]
}
],
"view": { ... }
}
Manage application theming with reactive CSS variables:
`typescript
import { createThemeProvider } from '@constela/runtime';
const theme = createThemeProvider({
config: {
mode: 'system',
colors: {
primary: 'hsl(220 90% 56%)',
background: 'hsl(0 0% 100%)',
},
darkColors: {
background: 'hsl(222 47% 11%)',
},
},
storageKey: 'app-theme',
useCookies: true,
});
// Get current theme
const current = theme.getTheme();
console.log(current.resolvedMode); // 'light' or 'dark'
// Switch theme
theme.setMode('dark');
// Subscribe to changes
const unsubscribe = theme.subscribe((theme) => {
console.log('Theme changed:', theme.resolvedMode);
});
// Cleanup
theme.destroy();
`
Features:
- System preference detection via prefers-color-scheme:root
- CSS variable application to document.documentElement
- Dark class management on
- Persistence via localStorage with optional cookies (for SSR)
- Subscription-based change notifications
Establish Server-Sent Events connections:
`json`
{
"actions": [
{
"name": "connectToNotifications",
"steps": [
{
"do": "sseConnect",
"connection": "notifications",
"url": { "expr": "lit", "value": "/api/events" },
"eventTypes": ["message", "update", "delete"],
"reconnect": {
"enabled": true,
"strategy": "exponential",
"maxRetries": 5,
"baseDelay": 1000,
"maxDelay": 30000
},
"onMessage": [
{ "do": "update", "target": "messages", "operation": "push", "value": { "expr": "var", "name": "payload" } }
]
}
]
}
]
}
Reconnection Strategies:
| Strategy | Description |
|----------|-------------|
| exponential | Exponential backoff (1s, 2s, 4s, 8s...) |linear
| | Linear backoff (1s, 2s, 3s, 4s...) |none
| | No automatic reconnection |
Apply UI changes immediately with automatic rollback on failure:
`json`
{
"actions": [
{
"name": "likePost",
"steps": [
{
"do": "optimistic",
"target": "posts",
"path": { "expr": "var", "name": "payload", "path": "index" },
"value": { "expr": "lit", "value": { "liked": true } },
"result": "updateId",
"timeout": 5000
},
{
"do": "fetch",
"url": { "expr": "concat", "items": [
{ "expr": "lit", "value": "/api/posts/" },
{ "expr": "var", "name": "payload", "path": "id" },
{ "expr": "lit", "value": "/like" }
]},
"method": "POST",
"onSuccess": [
{ "do": "confirm", "id": { "expr": "var", "name": "updateId" } }
],
"onError": [
{ "do": "reject", "id": { "expr": "var", "name": "updateId" } }
]
}
]
}
]
}
Bind SSE messages directly to state:
`json`
{
"actions": [
{
"name": "bindNotifications",
"steps": [
{
"do": "bind",
"connection": "notifications",
"eventType": "update",
"target": "items",
"transform": { "expr": "get", "base": { "expr": "var", "name": "payload" }, "path": "data" }
}
]
}
]
}
Hydrate interactive islands with the appropriate strategy:
`typescript
import { hydrateIsland } from '@constela/runtime';
// Hydrate an island when it becomes visible
hydrateIsland({
id: 'interactive-chart',
strategy: 'visible',
strategyOptions: { threshold: 0.5 },
program: compiledIslandProgram,
mount: document.querySelector('[data-island="interactive-chart"]'),
});
`
Supported Strategies:
- load - Hydrate immediatelyidle
- - Hydrate when browser is idle (requestIdleCallback)visible
- - Hydrate when element enters viewport (IntersectionObserver)interaction
- - Hydrate on first user interaction (click, focus, mouseover)media
- - Hydrate when media query matches (matchMedia)never
- - Never hydrate (static content only)
The runtime includes security measures:
- Prototype Pollution Prevention - Blocks __proto__, constructor, prototypeJSON
- Safe Globals - Only exposes , Math, Date, Object, Array, String, Number, Boolean, console
- HTML Sanitization - DOMPurify for Markdown content
> For framework developers only. End users should use the CLI.
`typescript
import { createApp } from '@constela/runtime';
const app = createApp(compiledProgram, document.getElementById('app'));
// Cleanup
app.destroy();
`
`typescript
import { hydrateApp } from '@constela/runtime';
const app = hydrateApp({
program: compiledProgram,
mount: document.getElementById('app'),
route: { params: { id: '123' }, query: new URLSearchParams(), path: '/users/123' },
imports: { config: { apiUrl: 'https://api.example.com' } }
});
`
`typescript`
interface AppInstance {
destroy(): void;
setState(name: string, value: unknown): void;
getState(name: string): unknown;
subscribe(name: string, fn: (value: unknown) => void): () => void;
}
`typescript
import { createSignal, createEffect, createComputed } from '@constela/runtime';
const count = createSignal(0);
count.get(); // Read
count.set(1); // Write
// Computed values with automatic dependency tracking
const doubled = createComputed(() => count.get() * 2);
doubled.get(); // Returns memoized value
const cleanup = createEffect(() => {
console.log(Count: ${count.get()});`
});
Type-safe state access for TypeScript developers:
`typescript
import { createTypedStateStore } from '@constela/runtime';
interface AppState {
posts: { id: number; liked: boolean }[];
filter: string;
}
const state = createTypedStateStore
posts: { type: 'list', initial: [] },
filter: { type: 'string', initial: '' },
});
state.get('posts'); // Type: { id: number; liked: boolean }[]
state.set('filter', 'recent'); // OK
state.set('filter', 123); // TypeScript error
``
MIT