# 🤖 todo-zig

TodoMVC for the CARLOS bakeoff, in Zig. It's server-rendered and works
with JavaScript turned off; a small script makes it feel like the
reference TodoMVC when JavaScript is on. Live at
[https://todo-zig.bfj.oncarlos.com](https://todo-zig.bfj.oncarlos.com).

## The stack, and why

| Piece | Choice | Why |
|---|---|---|
| Language | Zig 0.17.0 | The setup under test |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |

The result is one static binary (musl, about 2 MB) with the CSS and
JavaScript embedded.

### How it fits together

- `src/main.zig` parses the flags, opens the database, listens on the unix
  socket (or `127.0.0.1:<port>`), and hands each connection to its own
  task. SIGTERM shuts the listening socket down; the accept loop ends,
  open connections are cancelled, and the database is closed.
- `src/app.zig` routes requests, reads the visitor's cookie, checks the
  `Origin` of every POST, and answers 303 (or, for the script, 200 with
  the fresh page).
- `src/db.zig` is the store: one SQLite connection in WAL mode behind a
  mutex, with migrations that only add. Every query that touches a todo is
  scoped by list.
- `src/view.zig` renders TodoMVC's own markup. Each action is a real
  form; the extra controls a page needs without JavaScript have a
  `nojs-` class, and `static/app.css` hides them once the page is marked
  `html.js`.

### Visitors and lists

The first request without a recognised cookie mints a 256-bit random
token, stores its SHA-256 as a new list, and sets the cookie `todo_list`
(`HttpOnly; SameSite=Lax; Path=/`, plus `Secure` on Carloku). A cookie
the app doesn't recognise, garbled or not, gets a fresh empty list. Item
ids are global, but every update is `WHERE id = ? AND list_id = ?`, so a
POST naming someone else's item answers 404 and changes nothing.

## Build, run, test, deploy

```
source ../../scripts/env.sh       # Zig 0.17.0 on PATH
make build                        # dist/app for this machine (static, musl)
make build-arm64                  # dist/app-linux-arm64, the shipped binary
make test                         # Zig unit tests (view, routing, store)
```

Run it locally:

```
dist/app --listen 127.0.0.1:8080 --db "$TMPDIR/zig.db"
```

The shared behaviour suite, from the repo root, with the app running:

```
scripts/test-app.sh http://127.0.0.1:8080 "$TMPDIR/zig-suite.json"
```

Before deploying, check the arm64 binary boots on a unix socket under qemu:

```
scripts/boot-check.sh apps/zig/dist/app-linux-arm64
```

Deploy (the version is the commit's short SHA):

```
make build-arm64 deploy MESSAGE="what changed"
```

The first deploy needed the app and its instance created once:

```
carlos apps create --account bfj --app todo-zig --build-probe /api/version
carlos instances enable --account bfj --app todo-zig
carlos instances create --account bfj --app todo-zig --host todo-zig.bfj.oncarlos.com --channel edge
```

### Caches

Every Zig cache lives under `.cache/`: compiled objects in
`.cache/zig` and `.cache/zig-local`, the downloaded SQLite archive in
`.cache/zig/p`, and its unpacked sources in `.cache/pkg`. `make clean`
removes build output; `make clean-deps` also removes every compiled
object but keeps the downloaded sources, so the next build compiles
SQLite and Zig's musl libc from scratch.
