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-swift
2
3TodoMVC for the CARLOS bakeoff, written in Swift. It's live at
4[https://todo-swift.bfj.oncarlos.com](https://todo-swift.bfj.oncarlos.com).
5
6The server renders every page, and every action is an ordinary HTML form,
7so the app works with JavaScript off. With JavaScript on, a small ES module
8sends the same forms with `fetch` and swaps in the new list, so it behaves
9like the TodoMVC reference apps. `AGENTS.md` has the rule the app lives by
10and its tenancy declaration; `CONTRACT.md` at the repo root has the routes
11and markup it shares with the other entries.
12
13## The stack, and why
14
15| Piece | Choice | Why |
16|---|---|---|
17| Language | Swift 6.2, strict concurrency | The setup under test. The Static Linux SDK links musl and the Swift runtime into one static binary |
18| HTTP server | [Hummingbird 2](https://github.com/hummingbird-project/hummingbird) | The lightweight server framework on SwiftNIO, async/await throughout. It binds a unix socket directly (`BindAddress.unixDomainSocket`), and its `runService()` handles SIGTERM with a graceful shutdown, which is exactly the platform's process contract. Vapor would also work, but it brings far more than a nine-route app needs |
19| SQLite | [SQLiteNIO](https://github.com/vapor/sqlite-nio) | It compiles SQLite (3.54) from source as part of the package, so nothing depends on a system `libsqlite3`, which the Static Linux SDK doesn't have. GRDB is the nicer API, but it expects a system SQLite on Linux |
20| Header types | [swift-http-types](https://github.com/apple/swift-http-types) | Already under Hummingbird; listed because the app names `HTTPField` itself |
21| HTML | String building in `Page.swift`, one `escape` function | Three small templates don't need a template engine. Every visitor-supplied string goes through `escape` |
22| CSS and JS | Embedded in the binary by `tools/embed.sh` | One file to ship. A test checks the embedded copies match `shared/` and `static/` byte for byte |
23| Tests | Swift Testing, with `HummingbirdTesting` driving the router | No server or port needed for the unit tests |
24| Arguments | Parsed by hand | Three flags didn't justify swift-argument-parser |
25
26Two size decisions: Hummingbird's default `FullFoundation` trait is
27switched off and the app only imports `FoundationEssentials`, so the ICU
28data that full Foundation carries stays out. That took the arm64 binary
29from 66 MB to 22 MB. The shipped binary is also stripped.
30
31### How it fits together
32
33- `Sources/todo/main.swift` reads the flags and starts the server.
34- `Server.swift` parses `--socket`/`--listen`/`--db`, works out the app's
35 own origin (`https://$CARLOS_HOST` on a socket, `http://127.0.0.1:<port>`
36 with `--listen`) and runs Hummingbird until SIGTERM.
37- `Routes.swift` holds every route, including the `GET /` handler.
38- `Visitors.swift` gives each visitor a list through a `todo_list` cookie
39 (128 random bits, `HttpOnly; SameSite=Lax; Path=/`, plus `Secure` on a
40 socket, where the edge serves https). It also holds the `Origin` check
41 that answers 403 to cross-site posts.
42- `Store.swift` is the SQLite layer: WAL mode, append-only migrations
43 tracked by `PRAGMA user_version`, and one SQL statement per change. An
44 action on an item that isn't in the caller's list changes no row and
45 answers 404.
46- `Page.swift` renders the page from `shared/reference.html`'s markup.
47- `static/app.js` is the JavaScript; `static/app.css` places the
48 no-JavaScript controls over the checkboxes they stand in for and hides
49 them when JavaScript runs.
50
51With JavaScript, an action sends the header `X-Todo-Partial: 1` and gets
52back the new `section.main` and `footer.footer` (200) instead of a 303.
53Without that header nothing changes.
54
55## Build, run, test, deploy
56
57Put the toolchain on `PATH` first: `source ../../scripts/env.sh`. The
58Makefile does that itself.
59
60| Command | Does |
61|---|---|
62| `make build` | Builds for this machine into `dist/app` (dynamically linked against the local Swift runtime) |
63| `make build-arm64` | Builds the shipped static binary, `dist/app-linux-arm64` |
64| `make test` | Runs the unit tests |
65| `make run` | Runs `dist/app` on `127.0.0.1:8080` (`PORT=` to change) |
66| `make clean` | Removes `dist/` |
67| `make clean-deps` | Also removes every compiled artifact under `.cache/build`, keeping the downloaded sources |
68| `make bump` / `make unbump` | Changes a string constant in `Routes.swift`, forcing that file to recompile |
69| `make deploy` | Deploys `dist/app-linux-arm64` to Carloku |
70
71SwiftPM's checkouts, repository cache and build output all live in
72`.cache/` (`--scratch-path` and `--cache-path`), so nothing is shared with
73other projects.
74
75The build version is the commit's short SHA. `make` writes it into
76`Sources/TodoApp/Version.swift` (git-ignored), with `-dirty` added when
77`apps/swift` has uncommitted changes. `make deploy` refuses a dirty build,
78or a binary built at another commit.
79
80The shared behaviour suite:
81
82```
83make build
84dist/app --listen 127.0.0.1:8080 --db $TMPDIR/swift.db &
85../../scripts/test-app.sh http://127.0.0.1:8080 $TMPDIR/swift-suite.json
86```
87
88Before a deploy, `../../scripts/boot-check.sh dist/app-linux-arm64` runs
89the arm64 binary under qemu on a unix socket and checks `/healthz`,
90`/api/version` and a clean exit on SIGTERM. Then:
91
92```
93git commit ...
94make build-arm64 deploy MSG="what changed"
95```
96
97The app was created on Carloku once with:
98
99```
100carlos apps create --account bfj --app todo-swift --build-probe /api/version
101carlos instances enable --account bfj --app todo-swift
102carlos instances create --account bfj --app todo-swift --host todo-swift.bfj.oncarlos.com --channel edge
103```
104
105## Trade-offs
106
107- Trust class C: titles are stored as plain text, and anyone holding the
108 database or a replica can read every list. See `AGENTS.md`.
109- A list belongs to a cookie. Clear it and the list is gone for good.
110 Abandoned lists are never erased.
111- Every visit without a valid cookie creates a list row, crawlers
112 included. The rows are tiny, and there's no cleanup yet.
113- The one SQLite connection serialises all queries. That's plenty for a
114 todo list. A busier app would add a read pool.
115