| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322 |
- <!doctype html>
- <html lang="en-nz">
- <head>
- <meta charset="utf-8">
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <title>Statum — Astrologue Developer</title>
- <meta name="description" content="Statum is the page rendering and state management layer for Astrologue web applications — server-rendered pages, signed and encrypted client-carried state slots, GUID actions and realtime channels, bound by a small client script. Free software, LGPL-3.0-or-later.">
- <meta name="color-scheme" content="light dark">
- <meta name="theme-color" content="#f2ebda" media="(prefers-color-scheme: light)">
- <meta name="theme-color" content="#0a0a12" media="(prefers-color-scheme: dark)">
- <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Ccircle cx='32' cy='32' r='27' fill='none' stroke='%23a8801f' stroke-width='4'/%3E%3Cpath d='M32 8l6 18 18 6-18 6-6 18-6-18-18-6 18-6z' fill='%23a8801f'/%3E%3Ccircle cx='32' cy='32' r='4' fill='%23003078'/%3E%3C/svg%3E">
- <meta property="og:title" content="Statum — Astrologue Developer">
- <meta property="og:description" content="Page rendering and state management for web applications: server-rendered pages, signed and encrypted state slots, directives and actions, bound by a small client script.">
- <meta property="og:type" content="website">
- <link rel="stylesheet" href="../../assets/css/style.css">
- </head>
- <body>
- <a class="skip-link" href="#main">Skip to content</a>
- <header class="masthead">
- <div class="container masthead__inner">
- <a class="brand" href="../../">
- <img class="brand__logo brand__logo--light" src="../../assets/img/astrologue-light.png" width="392" height="89" alt="Astrologue">
- <img class="brand__logo brand__logo--dark" src="../../assets/img/astrologue-dark.png" width="392" height="89" alt="Astrologue">
- </a>
- <nav class="site-nav" aria-label="Primary">
- <ul class="site-nav__list">
- <li><a href="../../">Home</a></li>
- <li><a href="../../#news">News</a></li>
- <li><a href="../../#download">Download</a></li>
- <li><a href="../" aria-current="page">Developer</a></li>
- <li><a class="nav-cta" href="../../#download">Get Astrologue</a></li>
- </ul>
- </nav>
- </div>
- </header>
- <main id="main">
- <nav class="breadcrumb" aria-label="Breadcrumb">
- <div class="container">
- <ol>
- <li><a href="../../">Astrologue</a></li>
- <li><a href="../">Developer</a></li>
- <li aria-current="page">Statum</li>
- </ol>
- </div>
- </nav>
- <section class="docs-header">
- <div class="container">
- <p class="kicker"><span>Web stack · Pages and state</span></p>
- <h1>Statum</h1>
- <p class="lede">Page rendering and state management for web applications. HTML pages are composed and precompressed at build time; application state is signed, sealed in the browser and carried back with each request — so the server stays stateless between requests, with no session store and no database for ephemeral state. A small client script binds it all to the DOM.</p>
- <dl class="docs-facts">
- <div><dt>Version</dt> <dd><code>0.1</code></dd></div>
- <div><dt>Licence</dt> <dd>LGPL-3.0-or-later</dd></div>
- <div><dt>Package</dt> <dd><code>usm install statum</code></dd></div>
- <div><dt>Language</dt> <dd>Vala</dd></div>
- </dl>
- </div>
- </section>
- <section class="band band--plain" id="docs">
- <div class="container docs-layout">
- <nav class="docs-toc" aria-label="On this page">
- <h2>On this page</h2>
- <ul>
- <li><a href="#overview">Overview</a></li>
- <li><a href="#features">Features</a></li>
- <li><a href="#installation">Installation</a></li>
- <li><a href="#usage">Usage & documentation</a></li>
- <li><a href="#package">Package & source</a></li>
- </ul>
- </nav>
- <div class="docs-prose">
- <header class="section-head" id="overview">
- <p class="kicker"><span>Overview</span></p>
- <h2>The request, drawn out</h2>
- </header>
- <p>Statum sits on <a href="../astralis/">Astralis</a> and turns an HTTP server into an application framework. A request follows one shape, from first byte to bound DOM:</p>
- <figure class="code-plate">
- <pre><code> Browser Server
- ─────── ──────
- GET / ──────────────────────► StatumPage (precompressed HTML)
- page loads, statum.js boots
- GET /_statum/entrypoint ────► StatumEntrypoint → directives
- client applies directives (set/clear/…)
- client binds stm-* attributes
- POST /_statum/action/{guid} ► StatumAction → directives
- (with X-Statum-Slot headers) (mutate state, push to channels)</code></pre>
- <figcaption>The architecture at a glance, from <code>getting-started.md</code>.</figcaption>
- </figure>
- <p>The trick is where state lives. The server mints <em>slots</em> — units of application state, identified by a type name and a random key — signs them, and hands them to the browser. The client holds them and sends them back on every subsequent request. There is no session table to scale, no sticky routing to arrange; a restarted server with the same static keys verifies and continues where it left off.</p>
- <p>A page's route is declared in its own HTML, its handler pairs with it by type, and the wiring is three lines:</p>
- <figure class="code-plate">
- <pre><code>void main(string[] args) {
- var app = new WebApplication(8080);
- app.add_module<StatumModule>();
- var statum = app.configure_with<StatumConfigurator>();
- statum.add_page<HomePage, HomeEntrypoint>(); // route from <pstm-uri>
- statum.action<BumpAction>(); // GUID auto-assigned
- app.run();
- }</code></pre>
- <figcaption>From <code>getting-started.md</code> and <code>example/Main.vala</code>.</figcaption>
- </figure>
- <header class="section-head" id="features">
- <p class="kicker"><span>Features</span></p>
- <h2>What it hands you</h2>
- </header>
- <ul class="docs-features">
- <li><strong>Server-rendered pages.</strong> HTML composed from templates and fragments at build time by <code>statum-mkpstm</code>, precompressed with gzip, zstd and brotli and served with encoding negotiation and ETags.</li>
- <li><strong>Client-carried state.</strong> Signed snapshots — and for private data, encrypted ones — that the browser holds and returns, keeping the server stateless between requests.</li>
- <li><strong>Entrypoint hydration.</strong> After a page loads, the client fetches directives that set the initial slot data and bind the DOM.</li>
- <li><strong>GUID actions.</strong> POST and GET handlers auto-mapped to <code>/_statum/action/{guid}</code>, invoked from <code>stm-action</code> attributes, carrying strongly-typed private data.</li>
- <li><strong>Realtime updates.</strong> A SharedWorker-backed SSE channel pushes slot changes to every subscribed tab.</li>
- <li><strong>A topic system.</strong> External services — workers, webhooks, timers — trigger updates that load an entity and derive slot state for watching clients.</li>
- <li><strong>Three build tools.</strong> <code>statum-mkpstm</code> for pages, <code>statum-mkres</code> for precompressed resources, <code>statum-genkeys</code> for static signing and encryption keys.</li>
- </ul>
- <header class="section-head" id="installation">
- <p class="kicker"><span>Installation</span></p>
- <h2>Getting the library</h2>
- </header>
- <p>With USM on your machine, one command acquires, builds and installs the library, its three command-line tools, C header, Vala binding and introspection:</p>
- <figure class="code-plate">
- <pre><code>usm install statum</code></pre>
- <figcaption>Installs from the Astrologue package repository.</figcaption>
- </figure>
- <p>From source, the build lives at the repository root:</p>
- <figure class="code-plate">
- <pre><code>git clone <the Statum repository>
- cd Statum
- meson setup . build
- ninja -C build
- ninja -C build install</code></pre>
- <figcaption>The same meson invocation the package's own build script uses.</figcaption>
- </figure>
- <p>Then compile against it with Vala's usual package mechanism:</p>
- <figure class="code-plate">
- <pre><code>valac --pkg statum-0.1 my-app.vala</code></pre>
- <figcaption>The installed <code>statum-0.1.deps</code> resolves the Astralis, Invercargill and Inversion bindings for you.</figcaption>
- </figure>
- <header class="section-head" id="usage">
- <p class="kicker"><span>Usage & documentation</span></p>
- <h2>Pages, slots and actions</h2>
- </header>
- <p>Statum's documentation lives in its repository: <code>getting-started.md</code> is a full walkthrough of templates, slots, actions, the client script and the topic system, and <code>model.md</code> records the design. What follows is that guide in miniature.</p>
- <h3>A page, a template, an entrypoint</h3>
- <p>A page is an HTML file whose <code><head></code> declares its own route and the layout it wraps in — all <code>pstm-*</code> directives are build-time, processed by <code>statum-mkpstm</code> into a Vala class:</p>
- <figure class="code-plate">
- <pre><code><!-- home.html -->
- <pstm-uri>/</pstm-uri>
- <pstm-template>main</pstm-template>
- <p>Counter: <span stm-text="counter.value">0</span></p>
- <button stm-action="counter.bump">Bump</button></code></pre>
- <figcaption>From <code>getting-started.md</code> — the route comes from <code><pstm-uri></code>, never passed by hand.</figcaption>
- </figure>
- <p>Templates name themselves with <code><pstm-name></code> and receive child content at <code><pstm-content></code>; fragments are inlined by name with <code><pstm-fragment></code>; and when a page is composed into its template the child's head tags merge into the parent's, so every layer can declare its own styles and scripts.</p>
- <p>After the page loads, an entrypoint hydrates it — a handler that returns directives:</p>
- <figure class="code-plate">
- <pre><code>public class HomeEntrypoint : StatumEntrypoint {
- public override async DirectiveBuilder handle() throws GLib.Error {
- var counter = new CounterPublic();
- counter.value = 0;
- counter.bump = action_registry
- .author_private<BumpAction, BumpPrivate>(new BumpPrivate());
- return directives().set_typed<CounterPublic>("counter", Scope.PAGE, counter);
- }
- }</code></pre>
- <figcaption>From <code>getting-started.md</code> — the counter slot is page-scoped; the bump action reference carries sealed private data.</figcaption>
- </figure>
- <h3>Slots and scopes</h3>
- <p>A slot's data is a snapshot — public plus private properties — wrapped in a frame the server signs. The client holds frames and returns them; private data is encrypted under a namespace (X25519 seal plus Ed25519 signature), so an action's private blob cannot be read by another action, and a stolen copy of public data cannot impersonate anyone. Four scopes decide how long a slot lives:</p>
- <ul>
- <li><strong><code>Scope.PAGE</code></strong> — in-memory, gone on navigate; transient page data.</li>
- <li><strong><code>Scope.FLOW</code></strong> — per tab via sessionStorage; a multi-step flow.</li>
- <li><strong><code>Scope.SESSION</code></strong> — per browser via localStorage; login state, preferences.</li>
- <li><strong><code>Scope.DEVICE</code></strong> — per device, long-lived settings.</li>
- </ul>
- <h3>Actions</h3>
- <p>Actions are not addressed by URL. The server authors a reference, embeds it in slot public data, and the client invokes it through an <code>stm-action</code> attribute. A typed action decrypts its private data before <code>handle()</code> runs:</p>
- <figure class="code-plate">
- <pre><code>public class BumpAction : TypedStatumAction<BumpPrivate> {
- public override async DirectiveBuilder handle() throws GLib.Error {
- var delta = request_private != null ? ((!)request_private).delta : 1;
- return directives()
- .update_held<CounterPublic>("counter", c => c.value += delta);
- }
- }</code></pre>
- <figcaption>From <code>getting-started.md</code> — <code>update_held</code> reads, mutates and re-signs the held slot, preserving action references.</figcaption>
- </figure>
- <p>Directives are a small, fluent vocabulary: <code>set</code> and <code>set_typed</code> sign state into the client, <code>update_held</code> mutates what is already held, <code>clear</code> drops it, <code>navigate</code> redirects, <code>notify</code> surfaces a message, <code>subscribe</code> joins a realtime channel — and terminal directives like <code>form_submit</code> finish a round trip by submitting a form by id.</p>
- <h3>The client script</h3>
- <p><code>statum.js</code> is embedded in the library and served from <code>/_statum/resource/statum.js</code> — one script tag in the template, auto-initialising on DOMContentLoaded. Binding attributes do the rest:</p>
- <figure class="code-plate">
- <pre><code><p>Counter: <span stm-text="counter.value">0</span></p>
- <button stm-action="counter.bump">Bump</button>
- <div stm-if="auth != null"> … </div>
- <div stm-else> … </div>
- <div stm-class.active="cart.count > 0"> … </div>
- <div stm-markdown="post.body"></div></code></pre>
- <figcaption>From <code>getting-started.md</code> — text, HTML, CommonMark markdown, conditionals, class toggles and action calls.</figcaption>
- </figure>
- <p>Its markdown renderer targets the CommonMark 0.31.2 specification — all source HTML escaped first — and its JavaScript test suite runs under a vendored QuickJS, reporting conformance against the official example suite and failing below a 95% target.</p>
- <h3>Topics and global slots</h3>
- <p>A topic loads an entity from the database when triggered; a state modifier derives slot types from the payload and pushes them to subscribed clients over SSE. The example registers a topic that cycles an announcement every ten seconds, so two tabs side by side show the broadcast arriving in both:</p>
- <figure class="code-plate">
- <pre><code>statum.global<AnnouncementPublic>("announcement", announcement);
- statum.topic<AnnouncementTopic>("announcement");
- statum.topic_modifier<AnnouncementPreviewModifier, AnnouncementPreview>("announcement");
- // trigger from anywhere — a worker, a webhook, a timer:
- yield topic_registry.trigger("announcement");</code></pre>
- <figcaption>From <code>example/Main.vala</code> and <code>getting-started.md</code> — global slots use deterministic keys for broadcast.</figcaption>
- </figure>
- <h3>Keys and models</h3>
- <p>Signed frames and encrypted blobs need keys that outlive a restart. <code>statum-genkeys</code> prints a ready-to-paste <code>statum</code> section for <code>web-config.json</code>; without it the server generates random keys per restart, which is fine for development and logged with a warning.</p>
- <p>Typed models serialise by GObject property introspection — scalars including <code>int64</code> and <code>bool</code> round-trip as type-correct JSON primitives on both the public and the encrypted path, <code>string[]</code> properties come back as arrays, and Invercargill collections serialise as JSON arrays. The test suite pins exactly this:</p>
- <figure class="code-plate">
- <pre><code>var model = new LoginPrivate() {
- user_id = (int64)9007199254740993,
- is_admin = true,
- attempts = 3,
- score = 0.5,
- username = "bbarrow",
- session_no = (long)123456789012345
- };
- var wire = new JsonElement.from_properties(
- GObjectMapping.to_properties(model)).stringify();
- check(wire.contains("\"user_id\":9007199254740993"),
- "public path: int64 serialises as an unquoted JSON number");</code></pre>
- <figcaption>From <code>tests/TestMain.vala</code> — the suite runs through <code>meson test</code> and USM's test exec.</figcaption>
- </figure>
- <header class="section-head" id="package">
- <p class="kicker"><span>Package & source</span></p>
- <h2>The ledger</h2>
- </header>
- <dl class="docs-package">
- <div><dt>Version</dt><dd><code>0.1</code> (from <code>MANIFEST.usm</code>)</dd></div>
- <div><dt>Licence</dt><dd>LGPL-3.0-or-later — free to run, study, share and improve; the LGPL keeps it free to link from any program.</dd></div>
- <div><dt>Package</dt><dd><code>usm install statum</code></dd></div>
- <div><dt>Provides</dt><dd><code>libstatum-0.1.so</code>, <code>statum-mkpstm</code>, <code>statum-mkres</code>, <code>statum-genkeys</code>, <code>statum-0.1.h</code>, <code>statum-0.1.vapi</code> + <code>.deps</code>, <code>statum-0.1.gir</code>, <code>statum-0.1.typelib</code>, <code>statum-0.1.pc</code></dd></div>
- <div><dt>Needs at runtime</dt><dd>GLib and GObject, libsodium, libxml2, json-glib via the stack beneath — <code>libinvercargill-1.so</code>, <code>libinvercargill-json.so</code>, <code>libinversion-0.1.so</code> and <code>libastralis-0.1.so</code>.</dd></div>
- <div><dt>Source</dt><dd>The repository <code>Tilo15/Statum</code> on the Astrologue forge (git over ssh, port 222). Free software: the licence guarantees your right to the source.</dd></div>
- </dl>
- <nav class="docs-siblings" aria-label="Neighbours in the catalogue">
- <p>Its neighbours:</p>
- <ul>
- <li><a href="../astralis/">Astralis</a> — the server beneath it</li>
- <li><a href="../spry/">Spry</a> — authentication and user management on top of it</li>
- </ul>
- </nav>
- <nav class="docs-pager" aria-label="Project pages">
- <a class="docs-pager__link docs-pager__link--prev" href="../astralis/">
- <span>Previous</span>
- <strong>Astralis</strong>
- </a>
- <p class="docs-pager__up"><a href="../">All eleven</a></p>
- <a class="docs-pager__link docs-pager__link--next" href="../spry/">
- <span>Next in the catalogue</span>
- <strong>Spry</strong>
- </a>
- </nav>
- </div>
- </div>
- </section>
- </main>
- <footer class="site-footer">
- <div class="container">
- <div class="footer-grid">
- <div class="foot-brand">
- <img class="brand__logo brand__logo--dark" src="../../assets/img/astrologue-dark.png" width="392" height="89" alt="Astrologue">
- <p>An operating system for change — kept for self sufficiency, resilience, and software freedom, so regular people can hold the power of general purpose computers in their own hands.</p>
- </div>
- <nav aria-label="The four freedoms">
- <p class="foot-title">The Four Freedoms</p>
- <ol class="freedoms">
- <li><strong>Run</strong> the software as you wish, for any purpose.</li>
- <li><strong>Study</strong> the source and change it to do your computing as you wish.</li>
- <li><strong>Share</strong> copies so you can help others.</li>
- <li><strong>Modify</strong> and publish your versions so everyone benefits.</li>
- </ol>
- </nav>
- <nav aria-label="Site">
- <p class="foot-title">Site</p>
- <ul class="foot-list">
- <li><a href="../../">Home</a></li>
- <li><a href="../../#news">News</a></li>
- <li><a href="../../#download">Download</a></li>
- <li><a href="../">Developer</a></li>
- </ul>
- </nav>
- </div>
- <p class="colophon">Set in Cinzel and EB Garamond (SIL Open Font License). Astrologue is free software, licensed GPL-3.0-or-later: take it, tend it, pass it on. The libraries are LGPL-3.0-or-later, so they remain free to link.</p>
- </div>
- </footer>
- </body>
- </html>
|