← the tour · concept
concept i
The one loop
A user acts; the store changes; every watcher re-reads it and re-renders. Everything else is a refinement of this.
orion has exactly one round-trip, and the whole framework is shaped to serve it. A user acts. A command validates the request and writes to the store, then publishes a topic that says only “something here changed.” Every live view subscribed to that topic re-reads the store and re-renders its region; the server streams the fresh HTML over SSE and the browser morphs it in place. No client cache, no diff protocol, no second copy of the truth.
POST /todos → command
validate · write → bus
publish("todos") → live
store → Html → fat morph
SSE · brotli
CQRS, the Datastar way
Reads and writes are different route kinds with different types — and keeping them apart is what makes the loop hold:
- live — a long-lived GET returning an SSE stream. The read side: a pure function
store → Html, re-run on every relevant event and morphed into the viewer's DOM. - command — a short-lived POST/PATCH/DELETE. The write side: validate signals, mutate the store, publish. Shared state never comes back in the response — it flows to everyone (including the requester) through their live streams.
The read: a pure function, run forever
A view is a function of the store, nothing else. There is no client state to drift, no optimistic update to reconcile — the server re-renders the whole region and the browser morphs the difference. Data lives in the feature, as named SQL:
// features/todos/todos.view.ts — the read side
import { html, type Html } from "../../belt/html.ts" // belt is vendored in your repo
export function TodoList(todos: Todo[]): Html {
const remaining = todos.filter((t) => !t.done).length
return html`
<section id="todos">
<p role="status">${remaining} of ${todos.length} remaining</p>
<ul>${todos.map(TodoItem)}</ul>
</section>`
}
The write: validate, mutate, publish
A command answers the requester with only requester-scoped feedback — a form reset, a validation error, a toast. The shared change it made flows back to every open page (the requester's included) through their live streams. That discipline is the whole reason single-player and multiplayer are the same code.
// features/todos/todos.commands.ts — the write side
import { command } from "../../belt/http.ts"
import { parse } from "../../belt/shape.ts"
import { formErrors } from "../../belt/ds.ts"
import { toast } from "../patterns/toast.ts" // a vendored pattern, also yours
export const add = command(async (ctx, respond) => {
const r = await parse(todoShape, ctx.signals)
if (!r.ok) return respond.patch(formErrors("todo", todoShape, r.issues))
todos.insert(ctx.store, r.value.title)
ctx.bus.publish("todos") // → every watcher re-renders
toast(respond, "Added", { tone: "success" })
})
The load-bearing trick: events carry no state. A published topic just means “re-read the store.” Because every render is the complete current truth, a dropped, coalesced, or out-of-order render can't leave the screen wrong — the next one heals it, and every viewer is consistent by construction.
Why this scales down to one person
There's no WebSocket protocol to design, no client store to keep in sync, no reconciliation layer. The work you'd normally spend wiring real-time goes away, because real-time is just the read side doing its ordinary job. This shape — server-rendered HTML, morphed over SSE, one SQLite node as the source of truth — is the one Anders Murphy's Hyperlith proves out; orion carries it into a Node toolbelt.