Lumen
Server-driven reactive UI — no client framework, no hydration mismatch, no JS in the browser.
Lumen (vn_modules/lumen.vn, vn_modules/lumen_ui.vn, and the LumenJS client) compiles .lumen files into component structs at runtime via lumen_compile_source. The compiler generates WebSocket route handlers that serve HTML and morph DOM diffs — no build step, no JavaScript framework, no hydration. The same vn_modules/ prelude that auto-imports ZenithApp also makes lumen_component, lumen_mount, and the entire Lumen UI registry available in every .lumen file.
auto_awesome Why Lumen?
Lumen's pitch is not "another JavaScript framework" (the browser already has one). It's zero JS on the client, zero build steps, zero hydration bugs — just server-side state and DOM morphing over a WebSocket.
HTML-over-WebSocket
A 5 KB client sends events; the server re-renders and morphs only the changed DOM nodes. No Varian in the browser, no VDOM, no serialization — just raw HTML diffs over a persistent connection.
No hydration mismatch
The server owns state and rendering. There is one source of truth — the string of HTML it last sent. The client never tries to guess or reconcile state.
Scoped CSS
Each .lumen file's <style> block is automatically scoped to that component. Selectors are rewritten to include a unique data-lumen-css attribute — zero class-name collisions.
Live reload, zero config
vn dev watches your pages/ directory, recompiles .lumen files on change, and hot-pushes the update over the existing WebSocket — no browser refresh, no build pipeline.
File-based routing
Drop a .lumen file in pages/ and it becomes a route — pages/hello.lumen → /hello. Static files from public/ are served automatically. No config file needed.
Batteries included
Lumen UI registry (dropdowns, modals, toasts), LumenJS with event modifiers (debounce, prevent, key, confirm), test helpers — all built in through vn_modules/.
Import system
Call lumen_import("other_component") from a render string to mount a child component. The parent passes data via {{ slots }}; children get their own state and lifecycle — true composability.
Headless test helpers
lumen_click, lumen_input, lumen_assert_html, lumen_snapshot — test components with zero browser, zero WebSocket. Assert rendered output directly without Playwright or Selenium.
This page has two halves:
- The developer guide —
.lumenfiles,vn dev, the dev console, file routing, and the batteries every project ships with. Start here. - The runtime reference — the underlying
lumen_component/lumen_mountAPI the compiler targets. Read this if you're embedding Lumen by hand or hacking on it.
code The developer guide (Lumen + vn dev)
rocket_launch From zero to live
play_arrow Quick start
vn lumen new myapp # scaffold pages/ + public/ + starter component cd myapp vn dev # serve ./pages with live reload + dev console (:8090)
Open http://localhost:8090/ and click the logo — its colour is reactive server state.
draft The .lumen file format
A component is up to three blocks in one file:
<template> <!-- {{ }} bindings + @event hooks --> <button @click="pulse">{{ count }}</button> </template> <script> <!-- plain Varian: state() + handlers --> fn state() { return { count: 0 } } fn pulse(s, v) { return s.set("count", s.get("count") + 1) } </script> <client> <!-- OPTIONAL: browser JS island --> /* runs once on first paint; untouched by DOM morph */ </client>
How it compiles (lumen_compile_source in vn_modules/lumen.vn):
<template>is escaped and embedded as the component's render string.- Every
fnin<script>exceptstateis collected as a handler (so helper functions like_hueare in scope for your handlers too — they just never fire as events).state()supplies the initial state. - The result is a generated
_lumen_init_component_<Name>()that callslumen_component(...)and registers it. The language has noeval, sovn devdoes this in a build pass that emits one runnable.vn, then serves it.
import_export Import system
.lumen files can import other components, utilities, and Lumen UI components
using a standard ES-module-like syntax:
import UserCard from "./components/UserCard" import { Button, Card } from "lumen-ui" import { format } from "./helpers"
Three kinds of imports are supported:
- Relative paths (
"./components/X") — resolved relative to the importing file. The imported.lumenfile is compiled as a child component and registered under its PascalCased name. - Bare package names (
"lumen-ui") — resolved fromvn_modules/or$VARIAN_HOME/vn_modules/. The package manifest declares which components it exports, and they are registered automatically. - Direct imports (
import { fn } from "./lib") — import plain Varian functions from.vnfiles (not.lumen). This lets you share helper logic across components without duplicating code.
Imports are resolved at compile time by _lumen_extract_imports and
_lumen_collect_pages, which perform full transitive dependency resolution.
Circular imports are detected and produce a build error.
palette Scoped CSS
Lumen supports both global and scoped styles in .lumen files. A
<style scoped> block is rewritten so that every selector is prefixed with
[data-lumen-css="ComponentName"], isolating the component's styles from
the rest of the page:
<style scoped> .card { background: var(--lumen-surface); border-radius: 8px; } .card h3 { color: var(--lumen-primary); } </style> <style> body { font-family: var(--lumen-font); } </style>
The scoping is implemented by _lumen_scope_css which rewrites the CSS
selectors, and _lumen_scope_html which injects data-lumen-css attributes
onto the component's root elements. This means zero class-name collisions
between components — each component's styles are sealed behind its own
attribute selector.
bolt Reactivity model
State is plain Varian data on the server. There is no useState, no signals, no
effect dependency arrays, no virtual DOM in the browser. The whole loop is:
click ─▶ data-lumen-click ──WS──▶ run handler ▶ new state ▶ re-render ▶ diff ──WS──▶ morph
A handler takes (state, value) and returns the new state. State helpers are immutable
and arena-safe — s.set(k, v) returns a new struct, and they chain:
fn pulse(s, v) { let n = s.get("count") + 1 return s.set("count", n).set("color", _hue(n)) // immutable, chainable }
The scaffolded starter wires {{ color }} into an SVG fill, so each click recomputes a
colour server-side and Lumen morphs only the changed attribute into the DOM — a live
demonstration of the model with zero client code.
signpost_2 File-based routing
vn dev <dir> scans <dir> for .lumen files and maps each to a route (_lumen_route):
| File | Route |
|---|---|
pages/index.lumen |
/ |
pages/about.lumen |
/about |
pages/user-card.lumen |
/user-card (component name PascalCased to UserCard) |
terminal The interactive dev console
vn dev prints a Nuxt/Next-style console and then watches for changes:
LUMEN v0.1.0 the Varian frontend framework
➜ Local http://localhost:8090/
➜ Pages 2 in pages/
● / index.lumen
● /about about.lumen
✔ ready in 142 ms · watching pages/ — edit a page to hot-reload
- Live reload. Save any
.lumenfile → rebuild + restart; the browser's runtime auto-reconnects and re-renders. A build that fails mid-edit keeps the last good server up and prints✖ build error — keeping the last good page up. - Colour is emitted only to a TTY and is suppressed by
NO_COLOR. - Implemented in
src/main.c(lumen_dev,lumen_print_banner); the framework's ownhttp.servestartup line is silenced via theLUMEN_QUIETenv var so the console is the single source of truth.
inventory_2 Batteries included (no extra deps)
Every page Lumen serves gets, with zero setup, the same defaults create-next-app /
nuxi init / create-vite give you — injected by _lumen_head():
- Favicon set +
manifest.json.vn lumen newcopies a full icon set (favicon.ico,favicon-16/32,apple-touch-icon,android-chrome-192/512) and a themed web-app manifest intopublic/. The generated app servespublic/viaapp.serve_static("/", "public")— consulted only after routing, so page routes always win and a missing asset just 404s. The bundled source lives invn_modules/lumen_assets/so an installed binary finds it too. - Responsive meta —
<meta viewport>,theme-color,lang="en". - The Degular typeface, loaded via Adobe Fonts, wired through a CSS variable with a full system-font fallback so a page looks right even before the webfont arrives.
- Escape-by-default rendering and the branded error overlay (below).
bug_report Error overlay
A runtime error inside a handler is caught in _lumen_live_loop and pushed to the browser
as a Lumen-branded overlay (navy/amber, inline-SVG logo) carrying the friendly
errors.explain() what/fix text. It auto-clears on the next successful render. Dismissable;
dev-only by nature (it only appears when a live handler throws).
merge DOM patch protocol (M4)
After the first full render per connection, the server sends a minimal splice patch
instead of full HTML: it computes the longest common ASCII prefix/suffix between the last
HTML and the new HTML and sends only the changed middle as {"t":"p","s":p,"e":q,"d":mid}.
Prefix/suffix are kept ASCII so server byte offsets equal the client's UTF-16 string
indices (Unicode in the changed middle is sent verbatim and stays safe). The client
reconstructs prev[0:s] + d + prev[len-e:] and morphs. Invisible to authors.
widgets Composition Composition & slotsamp; slots
- Child components.
<UserCard id="u1" name="Ada"/>renders a child with props and its own server-side event scope — events are namespacedid:handlerso two instances of the same component never collide. - Slots.
<Card> inner markup </Card>projects into the card's{{! children }}; interactive components nested inside a slot keep their own scope.
data_object Triple-brace Triple-brace & JSX-style propsamp; JSX-style props
Beyond standard {{ var }} interpolation, Lumen supports two additional template
syntaxes for special cases:
- Triple-brace
{{{ raw }}}— unescaped HTML interpolation. Use this when you need to inject raw HTML from a variable (e.g. a rendered markdown string or an SVG). You are responsible for escaping — Lumen does not escape the contents of triple braces. The{{! children }}syntax is a slot for child content, distinct from raw interpolation. - JSX-style prop binding
<Card title={ expr }>— when passing dynamic values to child component props, you can use single-brace syntax instead oftitle="{{ expr }}". The compiler rewritestitle={ expr }totitle="{{ expr }}"via_lumen_rewrite_prop_braces, so both forms work identically. This is purely syntactic sugar for authors who prefer the JSX aesthetic.
lan Client islands (M8)
For a genuinely client-only widget (chart, canvas, map), add a <client> block of real
browser JS. It's embedded as a <script> that runs once on first paint and is left
untouched by the morph (cloneNode/innerHTML never re-run scripts). This is the honest
island — real client code where you ask for it, the rest still server-driven. Lumen
deliberately does not compile Varian to a browser bundle; that's exactly what
reintroduces hydration-mismatch bugs.
javascript LumenJS LumenJS & event modifiersamp; event modifiers
Lumen ships a tiny (~2.5 KB gzipped) client-side JavaScript runtime called LumenJS. It is not a framework you write against — it is a transport layer that Lumen emits on your behalf. The user never writes JS; they write Varian. LumenJS handles WebSocket reconnect with exponential backoff, event delegation, DOM morphing, and a small set of opt-in browser API shims.
Event modifiers can be attached to any @event handler using the
data-lumen-mod attribute:
<!-- Prevent default + stop propagation --> <form @submit="save" data-lumen-mod="prevent stop">...</form> <!-- Debounce input by 300ms --> <input @input="search" data-lumen-mod="debounce:300" /> <!-- Confirmation dialog --> <button @click="deleteItem" data-lumen-mod="confirm:Delete?"> Delete </button> <!-- Only fire on Enter --> <input @keydown="submitSearch" data-lumen-mod="key:Enter" />
Available modifiers: prevent, stop, once, debounce(ms),
throttle(ms), confirm("msg"), key(KeyName). All are handled
client-side without a server round-trip.
Opt-in client modules are scanned from the template at compile time and only shipped if the page uses them:
| Module | Attribute | What it does |
|---|---|---|
clipboard | data-lumen-copy | Copy element content to clipboard on click |
focus | data-lumen-focus | Autofocus an element on mount |
scroll | data-lumen-scroll | Scroll into view, lock body scroll, restore position |
toast | data-lumen-toast | Display a toast notification |
key | data-lumen-key | Global keyboard shortcut map |
A page that only uses @click handlers ships core LumenJS and nothing else.
A page that uses data-lumen-copy ships core + clipboard. The bundling
is automatic — no configuration, no manual tree-shaking.
widgets Lumen UI (Component Registry)
Lumen ships with an official, Shadcn-inspired component registry called Lumen UI. Instead of an external black-box package, Lumen UI provides beautifully designed, accessible components that are copied directly into your codebase (in pages/components/) so you fully own and customize the code.
Add components via the CLI:
vn lumen add button
vn lumen add card
Features:
- Zero Client JS: Components like Accordion, Dialog, and Select are driven entirely by server-side Varian state and scoped CSS.
- Theme-Aware: Lumen is "Light Mode First" but ships with native Dark Mode. Global CSS variables (var(--lumen-bg), var(--lumen-primary), etc.) adapt automatically when the dark class is toggled on the <html> element. Use vn lumen add theme-toggle for a pre-built switcher.
- Inline SVG Icons: Icons are pure, inline Lucide SVG vectors baked directly into the .lumen files. They inherit currentColor, cause zero layout shift, and require no external CDN scripts.
- Batteries Included: Available components: button, card, input, dialog, badge, accordion, alert, progress, select, checkbox, switch, separator, and theme-toggle.
dashboard Built-in layout components
Beyond the copyable Lumen UI registry, Lumen ships a set of built-in layout
and content components that are available globally — no vn lumen add needed.
They form a complete page-building vocabulary:
| Component | Purpose |
|---|---|
<Page> | Root page wrapper — emits DOCTYPE, <html>, <head> with SEO meta, <body> |
<Container> | Width-limited centered container (max-width via w prop) |
<Section> | Page section with optional top border + padding |
<Stack> | Vertical flex stack with configurable gap and align |
<Row> | Horizontal flex row with gap, align, justify, and wrap |
<Grid> | CSS grid with cols and gap props |
<Card> | Card surface container with tone, shadow, border |
<Heading> | Heading with size (1-6) and as (semantic tag override) |
<Text> | Paragraph with size, muted, align |
<Eyebrow> | Small uppercase label (section eyebrow) |
<Button> | Button or link with variant, href, on (event handler) |
<Badge> | Inline badge with tone (primary, success, danger, info) |
<Feature> | Marketing feature card with icon slot, title, description |
<Divider> | Horizontal rule with optional label |
<Spacer> | Vertical spacer using the spacing scale |
<Hero> | Full-width marketing hero with eyebrow, title, description, actions |
<Nav> | Sticky navigation bar with logo slot + link array |
<Footer> | Page footer with link columns |
<Split> | Two-column responsive split with ratio and reverse |
<Field> | Form field wrapping label, hint, error, slot |
<Stat> | KPI stat display (value, label, trend) |
<Tag> | Slim tag/badge with tone |
<Avatar> | Avatar with image or initials, configurable size |
<Alert> | Alert callout with tone, icon, title, and dismiss |
<Empty> | Empty state placeholder with icon, title, description |
<Skeleton> | Shimmer loading placeholder with variant (text, card, avatar) |
All built-in components accept the B-style layout props (see below), making them composable without writing custom CSS.
B-style layout props (shared vocabulary)
Every Lumen built-in component accepts the same set of layout and spacing props, inspired by Tailwind's utility model but expressed as component attributes rather than class names:
| Prop | CSS | Values |
|---|---|---|
pad / padx / pady | padding / padding-inline / padding-block | Space scale: 0–16 (0.25rem units) |
m / mx / my / mt / mb | margin variants | Space scale |
gap | gap | Space scale (flex/grid only) |
align | align-items | start / center / end / stretch |
justify | justify-content | start / center / end / between / around |
w / maxw | width / max-width | sm / md / lg / xl / full |
bg | background | surface / surface-2 / muted / primary |
tone | color + background pair | default / muted / primary / success / danger / info |
radius | border-radius | none / sm / md / lg / full |
shadow | box-shadow | Flag (present = shadow) |
border | 1px border | Flag (present = border) |
Example usage:
<!-- Card with padding, border, and primary tone -->
<Card pad="6" border tone="primary">
<Heading size="3">Welcome</Heading>
<Text muted>Your dashboard is ready.</Text>
</Card>
<!-- Two-column hero split -->
<Split ratio="1:1" align="center" gap="8">
<Stack gap="4">
<Eyebrow>Features</Eyebrow>
<Heading size="2">Build faster</Heading>
<Text>With Lumen's built-in vocabulary.</Text>
</Stack>
<!-- image slot -->
</Split>
These props are resolved through _ui_style_props, which converts them to
inline CSS declarations. Every built-in component threads this function into
its style attribute, so author-set props always apply.
image Icons
Lumen ships with an inline SVG icon system. Use lumen_icon(name) in your
handler or template to render an SVG string:
<button @click="submit">
<!-- literal call in the template -->
{{ lumen_icon("check") }} Save
</button>
Available icons: zap/bolt, bars, code, lock, clock, box,
layers, rocket, check, shield. Icons are Lucide-style SVG vectors
baked directly into the runtime — zero CDN dependencies, zero layout shift
(they use currentColor and fixed viewBox).
terminal CLI reference
| Command | Effect |
|---|---|
vn lumen new <name> |
Scaffold pages/ + public/ (favicons + manifest) + a starter component |
vn dev [dir] [port] |
Serve a pages dir with live reload + the dev console (default ./pages :8090) |
vn lumen build <dir> <out.vn> [port] |
Compile a pages dir into one runnable Varian app |
static Static Site Generation (SSG)
Lumen can also generate a fully static HTML site — no server, no WebSocket, no runtime required:
vn lumen build --static ./pages ./out_dir --base-url https://example.com
This produces:
- Standalone
.htmlfiles for every.lumenpage, rendered against the component's initial state (no interactivity — just HTML). sitemap.xmlwith all page routes and last-modified timestamps.robots.txtallowing all crawlers.- Static assets (
public/) copied verbatim to the output directory.
Use SSG for documentation sites, blogs, marketing pages, or any content that doesn't require per-connection server state. The generated output can be served by any HTTP server or CDN.
layers Advanced client-server APIs
Beyond the core lumen_component / lumen_mount API, Lumen provides a set
of higher-level primitives for data loading, shared state, cross-connection
communication, form validation, and SEO.
publish lumen_mount_dynamic(app, path, component, param)
Mount a component at a dynamic route, e.g. /product/:id. The param
argument is the URL parameter name (e.g. "id"). The parameter value is
extracted from req.params and merged into the component's initial state:
let detail = lumen_component(
| | { return { id: null, product: null } },
|s| { return lumen_render("<h1>Product {{ id }}</h1>", s) },
[], []
)
lumen_mount_dynamic(app, "/product/:id", detail, "id")
File-based routing also supports this: a file named [id].lumen in the
pages/ directory automatically maps to a :id route segment.
publish lumen_mount_data(app, path, component, provider)
Mount a component with a data provider callback that runs before each render. The provider receives the request and can access databases, APIs, or authentication before the component renders:
let profile = lumen_component(
| | { return { user: null, loading: true } },
|s| { return lumen_render("<h1>{{ user.name }}</h1>", s) },
[], []
)
lumen_mount_data(app, "/profile", profile, |req| {
let user = db.query("SELECT * FROM users WHERE id = ?", [req.params.id])
return { user: user, loading: false }
})
The provider's return value is shallow-merged into the component's state before the initial render.
storage lumen_store(initial)
A Zustand-style shared mutable store. Created inside state() it is
per-connection; created at the module level it is app-global:
// Module-level (shared across all connections)
let cart = lumen_store({ items: [], total: 0 })
fn state() {
return { store: cart, localFlag: false }
}
fn addItem(s, v) {
cart.set("items", cart.get("items").push(v))
return s
}
Returns { get(key), set(key, value), all() }. set mutates in place
(suitable for shared state where immutability would be expensive).
data_fetch lumen_resource(fetcher)
A React-Query-style async data container. The fetcher function is called
once on construction and returns { data, error }. The resource provides
state() for reactive access in templates:
let users = lumen_resource(| | {
return db.query("SELECT * FROM users")
})
// In a template: {{ users.state().loading }}, {{ users.state().data }}
Returns { state (fn), refetch (fn) }. state() returns
{ loading, error, data } — bind it in your template for live updates.
sync lumen_async_resource(fetcher)
An off-thread version of lumen_resource that uses task.spawn() and
task.channel() to fetch data in a background task. When the fetch
completes, it pushes a signal onto a global channel
(_lumen_async_updates), which triggers all active live loop connections
to re-render. Use this for expensive queries or external API calls that
should not block the event loop:
let stats = lumen_async_resource(| | {
return external_api.fetch("/analytics/dashboard")
})
campaign lumen_publish(topic, key, value) / lumen_subscribe(topic)
Cross-connection pub-sub. lumen_publish broadcasts a key-value update
to all WebSocket connections subscribed to a topic. lumen_subscribe
returns the current data for a topic as a struct:
// In one connection:
lumen_publish("chat", "message", "Hello everyone!")
// In another connection (polled on each render):
let chat = lumen_subscribe("chat") // { message: "Hello everyone!" }
Data is stored in _lumen_pubsub_data, a global struct shared across
all connections. This enables real-time features like chat, notifications,
and live dashboards without a separate message broker.
rss_feed lumen_broadcast_store(topic, initial)
Combines lumen_store with lumen_publish. Creates a store that
automatically publishes every set() call to all subscribers on the
topic:
let notif = lumen_broadcast_store("notifications", { count: 0 })
// Any connection can call notif.set("count", n) and all
// subscribed connections see the update on next render.
fact_check lumen_form(field_schemas)
Zod-inspired form validation. Define a schema for each field and call
validate(values) to get a result:
let loginForm = lumen_form({
email: { required: true, type: "email" },
password: { required: true, min_len: 8 }
})
fn submit(s, v) {
let result = loginForm.validate({ email: s.get("email"), password: s.get("password") })
if result.ok {
// result.values — validated and sanitized
} else {
// result.errors — { email: "invalid email", password: "too short" }
}
return s
}
Returns { validate(values) -> { ok, values, errors }, schema }.
Integrates with vn_modules/validate.vn for rule parsing and error
message generation.
search lumen_meta(title, description, og)
Declare SEO metadata for a page. Call this in a component's handler or
state() to set the document title, meta description, Open Graph tags,
and Twitter Card tags:
fn state() {
lumen_meta("About Us", "Learn about the Varian team", {
image: "https://varian-lang.org/og-image.png"
})
return { page: "about" }
}
Generates <title>, <meta name="description">, og:title,
og:description, og:image, twitter:card, and twitter:title tags.
The <Page> built-in component calls _lumen_meta_tags() to inject
these into the document <head>.
api Public API
component lumen_component(state_fn, render_fn, handler_names, handler_fns)
Create a component struct from four pieces:
| Argument | Description |
|---|---|
state_fn |
\| \| { return <initial state> } — zero-arg closure returning the initial state |
render_fn |
\|state\| { return lumen_render(template, state) } — renders state to HTML |
handler_names |
Array of handler name strings, e.g. ["inc", "dec"] |
handler_fns |
Array of handler closures \|state, value\| { ...; return state } |
Handler names and functions are parallel arrays (same index = same handler). Handler
closures receive the current state and the event value (the element's .value, or null)
and must return the new state (mutation works but the returned value is what's used).
Returns a struct (built with http.create_struct so there's no parse-order dependency
on zenith.vn's struct declarations).
publish lumen_mount(app, path, component)
Mount a live component on a Zenith app at path. Registers two routes:
GET <path>— renders the component's initial HTML wrapped in the live shell (data-lumen-rootcontainer + embedded client script) and returnstext/html.GET <path>/live— WebSocket upgrade endpoint. Upgrades to RFC 6455, then enters the per-connection event loop until the socket closes.
camera lumen_snapshot(component)
Render a component against its initial state and return the HTML string. Useful for snapshot testing and SSG:
let html = lumen_snapshot(counter)
// html = "<div><p>Count: 0</p>..."
Test helpers
Lumen provides a set of test helpers for component testing without a browser or WebSocket connection:
| Function | Description |
|---|---|
lumen_test_render(comp, state) | Render a component with a given state, return HTML string |
lumen_test_event(comp, state, hname, value) | Fire a handler event against the state, return new state |
lumen_test_contains(html, substr) | Check if HTML string contains a substring |
lumen_test_attr(html, attr_name) | Extract an attribute value from an HTML element |
lumen_test_count(html, substr) | Count occurrences of a substring in HTML |
lumen_test_render_response(comp, state) | Render as a zenith Response struct (for HTTP test assertions) |
Example test:
let html = lumen_test_render(counter, { count: 5 })
assert(lumen_test_contains(html, "Count: 5"))
let newState = lumen_test_event(counter, { count: 0 }, "inc", null)
assert(newState.count == 1)
wifi Wire Protocol
Both directions are JSON text frames over a single WebSocket.
Client → Server (an event happened):
{"t":"event","h":"<handlerName>","v":"<value-or-null>"}
h= thedata-lumen-<event>attribute value (originallydata-lumen-click="handlerName").v= the element's.valuefor input-ish elements, elsenull.
Server → Client (re-rendered HTML):
{"t":"html","html":"<full component HTML>"}
The client morphs (not replaces) the new HTML onto the live DOM, preserving focus and input values in untouched fields.
memory Per-Connection State
Each WebSocket connection gets its own independent state held in a local variable in the
_lumen_live_loop function. State lives exactly as long as the socket. Shared/multi-user
state via actors is a future milestone.
shield Security Rules
- Handler names from the wire are resolved only against the registered set. An unknown name is silently dropped — never evaluated.
- Re-rendering goes through
lumen_render, which HTML-escapes{{ }}by default. Never bypass it.
schedule Scheduler Model
The Varian VM is a single-threaded cooperative round-robin scheduler.
ws.read() is non-blocking: it returns "" (empty string) when no data is available
(EAGAIN), not null. The per-connection live loop calls task.yield() when there's no
message, allowing the scheduler to service other connections/tasks on the next tick.
play_circle Example
let counter = lumen_component( | | { return { count: 0 } }, |s| { return lumen_render("<div><p>Count: {{ count }}</p>" + "<button @click=\\"inc\\">+</button>" + "<button @click=\\"dec\\">-</button></div>", s) }, ["inc", "dec"], [ |s, v| { s.count = s.count + 1; return s }, |s, v| { s.count = s.count - 1; return s } ] ) let app = new_app() lumen_mount(app, "/", counter) app.listen(8090)
Run it: ./vn run examples/lumen_counter.vn, open http://localhost:8090.