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-nim
2
3TodoMVC in Nim for the CARLOS bakeoff: server-rendered, works with
4JavaScript off, and keeps each visitor's list in SQLite. It runs at
5[https://todo-nim.bfj.oncarlos.com](https://todo-nim.bfj.oncarlos.com).
6
7## The stack, and why
8
9- **Nim 2.2, compiled to C.** Nim builds a small native binary, and its C
10 backend makes the static linux-arm64 build easy: `zig cc` targeting
11 musl acts as the C compiler and linker, and the result is a 1.7 MB
12 static executable that starts in milliseconds.
13- **HTTP: the standard library's `asynchttpserver`**, on `asyncdispatch`.
14 A todo list doesn't need more than a single-threaded event loop, and the
15 standard library means no framework to pin. The popular alternatives
16 don't fit this platform: Mummy and httpbeast (Jester's server) only
17 listen on TCP, and Carloku hands the app a unix socket.
18 `asynchttpserver` doesn't take a unix socket either, but it's a small
19 gap: `unixServer` in `src/app.nim` binds the socket itself and gives it
20 to the server through `std/importutils.privateAccess`. The server is a
21 copy of the standard library's (`src/httpserver.nim`) with one fix:
22 upstream doesn't limit chunked request bodies, so one POST could
23 exhaust the instance's memory. The copy holds chunked bodies to the same
24 1 MiB limit as `Content-Length` ones. The standard library's docs call
25 this server basic; it has no request timeouts or keep-alive limits.
26 Carloku's edge sits in front of it, and that's an acceptable trade for
27 an app this size.
28- **Routing: a `case` on the path.** Six actions and four pages don't need
29 a router.
30- **Templates: Nim's built-in `stdtmpl` source filter** (`src/page.nimf`).
31 It compiles the template into an ordinary Nim proc, so the markup is
32 type-checked and there's nothing to load at run time. Titles go through
33 `xmltree.escape`.
34- **SQLite: `db_connector`'s `sqlite3` binding** (the database module that
35 moved out of Nim's standard library) over the **SQLite 3.54.1
36 amalgamation, compiled into the binary.** The binding normally loads
37 `libsqlite3.so`; `--dynlibOverride:sqlite3` turns that off so it links
38 against the compiled-in copy. `src/store.nim` prepares every statement
39 once and binds parameters, so no user input is ever spliced into SQL.
40 WAL mode, `synchronous = NORMAL`, and numbered migrations that only add
41 (`PRAGMA user_version` records how far the file has got).
42- **JavaScript: one plain ES module, no framework** (`static/app.js`). It
43 sends the same form POSTs with an `X-Fragment: 1` header, and the server
44 answers with the re-rendered list section instead of a 303. Rendering
45 stays in one place, the server.
46- **The CSS** is `shared/base.css` and `shared/index.css`, embedded at
47 compile time with `staticRead` and served byte for byte.
48 `static/app.css` adds the no-JavaScript controls and hides them once
49 JavaScript is running.
50
51Lists are anonymous: the first visit gets a cookie holding 128 random bits
52from the OS (`std/sysrand`), and every query is scoped to it. See
53`AGENTS.md` for the tenancy declaration.
54
55## Build, run, test, deploy
56
57Put the toolchains on `PATH` first: `source scripts/env.sh` from the repo
58root. The first build fetches `db_connector` into `.cache/nimble` and the
59SQLite amalgamation into `.cache/dl`, checking it against a pinned SHA-256.
60
61```
62make build # dist/app for this machine
63make build-arm64 # dist/app-linux-arm64, static (zig cc, musl)
64make test # unit tests: the store, and the routes with fake requests
65make clean # removes dist/
66make clean-deps # also removes compiled C: .cache/nimcache-* and zig's caches
67```
68
69Run it locally and drive the shared behaviour suite against it:
70
71```
72dist/app --listen 127.0.0.1:8080 --db $TMPDIR/nim.db
73scripts/test-app.sh http://127.0.0.1:8080 $TMPDIR/nim-suite.json
74```
75
76On Carloku it runs as `app --socket <path> --db <path>`, reads
77`CARLOS_HOST` for its origin, and sets `Secure` on its cookie. It exits
78cleanly on SIGTERM.
79
80`/api/version` reports the git short SHA the binary was built from, so
81commit before building what you deploy. Then:
82
83```
84scripts/boot-check.sh dist/app-linux-arm64
85make deploy MESSAGE="what changed"
86```
87
88## Layout
89
90| Path | What |
91|---|---|
92| `src/app.nim` | Flags, the server, routes and cookies |
93| `src/httpserver.nim` | The standard library's HTTP server, with chunked bodies limited |
94| `src/store.nim` | SQLite: schema, migrations, every query |
95| `src/views.nim`, `src/page.nimf` | The page and the list fragment |
96| `static/app.js`, `static/app.css` | The JavaScript layer and the no-JS controls |
97| `tests/test_app.nim` | Unit tests |
98