carlos / todomvc-bakeoff Public

Clone
git clone https://amadan.net/carlos/todomvc-bakeoff

Plain git — no account needed to clone.

Download

Download this file

1# đŸ¤– todo-zig
2
3TodoMVC for the CARLOS bakeoff, in Zig. It's server-rendered and works
4with JavaScript turned off; a small script makes it feel like the
5reference TodoMVC when JavaScript is on. Live at
6[https://todo-zig.bfj.oncarlos.com](https://todo-zig.bfj.oncarlos.com).
7
8## The stack, and why
9
10| Piece | Choice | Why |
11|---|---|---|
12| Language | Zig 0.17.0 | The setup under test |
13| HTTP | `std.http.Server` on `std.Io.net` | Zig's standard library has a complete HTTP/1.1 server. A todo app needs no router or framework, and every outside dependency is one more thing to keep building on a language that still changes between releases |
14| Concurrency | `std.Io.Threaded`, one task per connection | It's what `std.process.Init` hands `main` by default. Blocking code, no event loop to reason about |
15| SQLite | The official amalgamation, fetched by `build.zig.zon` and compiled by Zig as C | Zig compiles C natively, so SQLite needs no binding library: `addTranslateC` turns `sqlite3.h` into a Zig module and `src/db.zig` wraps the dozen calls the app uses |
16| Templates | Hand-written functions in `src/view.zig` writing to a `std.Io.Writer` | One page with one loop. A template engine would be more code than the template |
17| Front end | `static/app.js`, a plain ES module, about 80 lines | No framework and no build step. It sends the page's own forms with `fetch` and swaps in the server's re-rendered list |
18
19The result is one static binary (musl, about 2 MB) with the CSS and
20JavaScript embedded.
21
22### How it fits together
23
24- `src/main.zig` parses the flags, opens the database, listens on the unix
25 socket (or `127.0.0.1:<port>`), and hands each connection to its own
26 task. SIGTERM shuts the listening socket down; the accept loop ends,
27 open connections are cancelled, and the database is closed.
28- `src/app.zig` routes requests, reads the visitor's cookie, checks the
29 `Origin` of every POST, and answers 303 (or, for the script, 200 with
30 the fresh page).
31- `src/db.zig` is the store: one SQLite connection in WAL mode behind a
32 mutex, with migrations that only add. Every query that touches a todo is
33 scoped by list.
34- `src/view.zig` renders TodoMVC's own markup. Each action is a real
35 form; the extra controls a page needs without JavaScript have a
36 `nojs-` class, and `static/app.css` hides them once the page is marked
37 `html.js`.
38
39### Visitors and lists
40
41The first request without a recognised cookie mints a 256-bit random
42token, stores its SHA-256 as a new list, and sets the cookie `todo_list`
43(`HttpOnly; SameSite=Lax; Path=/`, plus `Secure` on Carloku). A cookie
44the app doesn't recognise, garbled or not, gets a fresh empty list. Item
45ids are global, but every update is `WHERE id = ? AND list_id = ?`, so a
46POST naming someone else's item answers 404 and changes nothing.
47
48## Build, run, test, deploy
49
50```
51source ../../scripts/env.sh # Zig 0.17.0 on PATH
52make build # dist/app for this machine (static, musl)
53make build-arm64 # dist/app-linux-arm64, the shipped binary
54make test # Zig unit tests (view, routing, store)
55```
56
57Run it locally:
58
59```
60dist/app --listen 127.0.0.1:8080 --db "$TMPDIR/zig.db"
61```
62
63The shared behaviour suite, from the repo root, with the app running:
64
65```
66scripts/test-app.sh http://127.0.0.1:8080 "$TMPDIR/zig-suite.json"
67```
68
69Before deploying, check the arm64 binary boots on a unix socket under qemu:
70
71```
72scripts/boot-check.sh apps/zig/dist/app-linux-arm64
73```
74
75Deploy (the version is the commit's short SHA):
76
77```
78make build-arm64 deploy MESSAGE="what changed"
79```
80
81The first deploy needed the app and its instance created once:
82
83```
84carlos apps create --account bfj --app todo-zig --build-probe /api/version
85carlos instances enable --account bfj --app todo-zig
86carlos instances create --account bfj --app todo-zig --host todo-zig.bfj.oncarlos.com --channel edge
87```
88
89### Caches
90
91Every Zig cache lives under `.cache/`: compiled objects in
92`.cache/zig` and `.cache/zig-local`, the downloaded SQLite archive in
93`.cache/zig/p`, and its unpacked sources in `.cache/pkg`. `make clean`
94removes build output; `make clean-deps` also removes every compiled
95object but keeps the downloaded sources, so the next build compiles
96SQLite and Zig's musl libc from scratch.
97