feat(site): tutorial chapter for durable and resident storage modes

- new chapter 7, "Storage modes: durable and resident", covering what
  master gains with the databasev2 cherry-pick: durable: false for a
  RAM-only table, resident: keys for a table that outgrows RAM
- states the parts a reader would otherwise hit as surprises: a
  keys-resident table is REFUSED at startup without WO_DATA, an update
  appends a delta rather than rewriting the row, and the chain is
  bounded at 16 links so a hot row does not degrade reads or replay
- quotes the measured 2.55x smaller resident set, not an estimate
- actors/deps/serving shift to ord 8/9/10; seeding is ord-driven so an
  existing WO_DATA keeps its rows and only a fresh boot reseeds
- home card says a table can be RAM-only or outgrow RAM
- two gate legs pin the new chapter; site-accept 23 checks, 0 failures

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 3b503c0db50aa737dd06ef6d17151c654a42c59b)
This commit is contained in:
shoney.arickathil 2026-08-30 20:35:57 +02:00
parent 710325b94a
commit aee78c2296
5 changed files with 23 additions and 6 deletions

View file

@ -9,7 +9,7 @@ samples now read the same way.
| file | layer | what it owns | | file | layer | what it owns |
| --- | --- | --- | | --- | --- | --- |
| `types.wo` | MODEL | the `Chapter` `@table`, the `ChapterLink` projection, `Chapters.links()`, and `seed_if_empty()` | | `types.wo` | MODEL | the `Chapter` `@table`, the `ChapterLink` projection, `Chapters.links()`, and `seed_if_empty()` |
| `content.wo` | MODEL (content) | the nine chapter bodies as fragment-returning functions, plus `seed_chapters()` | | `content.wo` | MODEL (content) | the ten chapter bodies as fragment-returning functions, plus `seed_chapters()` |
| `layout/app.wo` | VIEW (chrome) | `AppShell` — the component that fills writeonce-view's `Layout` — the two named widths, and `html_error` | | `layout/app.wo` | VIEW (chrome) | `AppShell` — the component that fills writeonce-view's `Layout` — the two named widths, and `html_error` |
| `layout/header.wo`, `layout/footer.wo` | VIEW (chrome) | the shared nav bar (brand = mark + wordmark) and footer | | `layout/header.wo`, `layout/footer.wo` | VIEW (chrome) | the shared nav bar (brand = mark + wordmark) and footer |
| `layout/logo.wo` | VIEW (chrome) | the mark as inline SVG, plus the `<head>` links | | `layout/logo.wo` | VIEW (chrome) | the mark as inline SVG, plus the `<head>` links |

View file

@ -50,7 +50,7 @@ MVC, laid out exactly like the program template
| this app | layer | | this app | layer |
| --- | --- | | --- | --- |
| `types.wo` | MODEL — the `Chapter` `@table`, and seed-if-empty | | `types.wo` | MODEL — the `Chapter` `@table`, and seed-if-empty |
| `content.wo` | the nine chapter bodies + `seed_chapters()` | | `content.wo` | the ten chapter bodies + `seed_chapters()` |
| `layout/` | the chrome: `AppShell` (+ the two named widths), header, footer, `html_error` | | `layout/` | the chrome: `AppShell` (+ the two named widths), header, footer, `html_error` |
| `home/`, `chapter/`, `install/`, `packages/` | one module per feature: its `view.wo` (components: fields in, Text out) and its `controller.wo` (query the model, fill the components, answer a `Resp`) | | `home/`, `chapter/`, `install/`, `packages/` | one module per feature: its `view.wo` (components: fields in, Text out) and its `controller.wo` (query the model, fill the components, answer a `Resp`) |
| `admin/`, `health/`, `favicon/` | controller-only features — a redirect, a text probe, an SVG | | `admin/`, `health/`, `favicon/` | controller-only features — a redirect, a text probe, an SVG |

View file

@ -52,6 +52,20 @@ fn ch_tables() -> Text {
return b; return b;
} }
fn ch_storage() -> Text {
let b = el("p", "leading-relaxed mb-4",
"Two optional <code>@table</code> keys decide where a table's rows LIVE. Both default " .. "to today's behaviour, so every table above keeps working untouched. " .. "<code>durable: false</code> keeps a table entirely in RAM — a full table in-process, " .. "same indexes, same <code>@unique</code>, same queries — that is simply empty after a " .. "restart. That is what a session store or a rate-limit counter wants: losing it on " .. "reboot is correct, and paying to make it durable is waste.");
b = b .. code_block("@table(name: \"sessions\", durable: false)\nclass Session {\n sid: Text @unique\n hits: Int\n}\n\n@table(name: \"catalog\", resident: keys, index: [sku])\nclass Product {\n sku: Text @unique\n price: Int\n blurb: Text\n}");
b = b .. el("p", "leading-relaxed mt-4 mb-4",
"<code>resident: keys</code> is the other direction: only the KEY stays in memory. The " .. "row body lives in the write-ahead log and is read back on demand, so a table can be " .. "far larger than RAM without a page-fault cliff. Measured on a product catalogue, the " .. "resident set is <strong>2.55× smaller</strong> than the same table fully resident. " .. "Such a table has nowhere to keep its rows without a log, so declaring it and starting " .. "without <code>WO_DATA</code> is REFUSED at startup rather than silently ignored.");
b = b .. el("p", "leading-relaxed mb-4",
"Reads and writes are unchanged — the storage mode is not a different API:");
b = b .. code_block("let hits = from x in Product where x.sku == \"a-1\" take 1 select x;\nif len(hits) == 1 {\n hits[0].price = 1299; -- an ordinary assignment\n}");
b = b .. el("p", "leading-relaxed mt-4",
"Underneath, that assignment appends a <em>delta</em> — which field changed, plus a " .. "back-pointer to the row's previous record — instead of rewriting the whole row. A read " .. "walks that chain backward, newest wins. Left alone the chain would grow forever on a " .. "hot row, so it is BOUNDED: past 16 links an update writes a full row image and the " .. "chain restarts at zero. A row updated a million times still costs a bounded number of " .. "record reads, on read and on replay alike.");
return b;
}
fn ch_actors() -> Text { fn ch_actors() -> Text {
let b = el("p", "leading-relaxed mb-4", let b = el("p", "leading-relaxed mb-4",
"Concurrency is actors on fibers: <code>spawn</code> makes an actor from a class with a " .. "<code>receive</code> method, <code>send</code> delivers one message at a time, and " .. "ownership MOVES with the message — no locks, no shared mutable state, no data races " .. "by construction. Blocking calls park the fiber; the shard serves others meanwhile. " .. "One VM per core by default; mailboxes are bounded (a full one is a catchable trap)."); "Concurrency is actors on fibers: <code>spawn</code> makes an actor from a class with a " .. "<code>receive</code> method, <code>send</code> delivers one message at a time, and " .. "ownership MOVES with the message — no locks, no shared mutable state, no data races " .. "by construction. Blocking calls park the fiber; the shard serves others meanwhile. " .. "One VM per core by default; mailboxes are bounded (a full one is a catchable trap).");
@ -94,7 +108,8 @@ pub fn seed_chapters() {
insert Chapter { slug: "classes", ord: 4, title: "Classes and interfaces", body: ch_classes() }; insert Chapter { slug: "classes", ord: 4, title: "Classes and interfaces", body: ch_classes() };
insert Chapter { slug: "optionals", ord: 5, title: "Optionals and traps", body: ch_optionals() }; insert Chapter { slug: "optionals", ord: 5, title: "Optionals and traps", body: ch_optionals() };
insert Chapter { slug: "tables", ord: 6, title: "@table: the built-in database", body: ch_tables() }; insert Chapter { slug: "tables", ord: 6, title: "@table: the built-in database", body: ch_tables() };
insert Chapter { slug: "actors", ord: 7, title: "Actors and fibers", body: ch_actors() }; insert Chapter { slug: "storage", ord: 7, title: "Storage modes: durable and resident", body: ch_storage() };
insert Chapter { slug: "deps", ord: 8, title: "Dependencies", body: ch_deps() }; insert Chapter { slug: "actors", ord: 8, title: "Actors and fibers", body: ch_actors() };
insert Chapter { slug: "serving", ord: 9, title: "Serving the web (this site)", body: ch_serving() }; insert Chapter { slug: "deps", ord: 9, title: "Dependencies", body: ch_deps() };
insert Chapter { slug: "serving", ord: 10, title: "Serving the web (this site)", body: ch_serving() };
} }

View file

@ -24,7 +24,7 @@ pub class HomePage {
-- why-cards (2x2 grid, collapses on small screens) -- why-cards (2x2 grid, collapses on small screens)
let cards = card("One binary", "woc build emits a self-contained executable: VM, your bytecode, the database engine. Deploys are a file copy; the runtime swaps code in place."); let cards = card("One binary", "woc build emits a self-contained executable: VM, your bytecode, the database engine. Deploys are a file copy; the runtime swaps code in place.");
cards = cards .. card("Memory safety, no tax", "Rust-shaped ownership checked at compile time; where ownership cannot express the shape, the compiler infers GC — per shard, no global pause."); cards = cards .. card("Memory safety, no tax", "Rust-shaped ownership checked at compile time; where ownership cannot express the shape, the compiler infers GC — per shard, no global pause.");
cards = cards .. card("The database is built in", "Every class is a table. Inserts are WAL-logged before they acknowledge; restart replays. No server to operate, no connection string."); cards = cards .. card("The database is built in", "Every class is a table. Inserts are WAL-logged before they acknowledge; restart replays. No server to operate, no connection string — and a table can declare itself RAM-only, or keep only its keys in memory and outgrow RAM.");
cards = cards .. card("Actors on every core", "spawn returns an address, send moves ownership. Fibers park on io_uring instead of blocking threads — no async/await, ever."); cards = cards .. card("Actors on every core", "spawn returns an address, send moves ownership. Fibers park on io_uring instead of blocking threads — no async/await, ever.");
let grid = el("div", "grid grid-cols-2 gap-6 mb-8", cards); let grid = el("div", "grid grid-cols-2 gap-6 mb-8", cards);

View file

@ -119,6 +119,8 @@ expect "tailwind sheet inlined" "$(hit /)" 200 ".btn{"
expect "chapter renders a code sample" "$(hit /ch/hello)" 200 "fn main" expect "chapter renders a code sample" "$(hit /ch/hello)" 200 "fn main"
expect "escaped interpolation visible" "$(hit /ch/values)" 200 '${port}' expect "escaped interpolation visible" "$(hit /ch/values)" 200 '${port}'
expect "unknown chapter is a 404 page" "$(hit /ch/nope)" 404 "No such chapter" expect "unknown chapter is a 404 page" "$(hit /ch/nope)" 404 "No such chapter"
expect "storage chapter documents both modes" "$(hit /ch/storage)" 200 "resident: keys"
expect "storage chapter states the chain bound" "$(hit /ch/storage)" 200 "past 16 links"
expect "install guide renders" "$(hit /install)" 200 "tar -C /usr/local" expect "install guide renders" "$(hit /install)" 200 "tar -C /usr/local"
expect "packages index lists both" "$(hit /packages)" 200 "/packages/porch" expect "packages index lists both" "$(hit /packages)" 200 "/packages/porch"
expect "package detail shows its dep" "$(hit /packages/view)" 200 "writeonce-view" expect "package detail shows its dep" "$(hit /packages/view)" 200 "writeonce-view"