carlos / todomvc-bakeoff Public

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

Plain git — no account needed to clone.

Download

Download this file

đŸ¤– todo-swift

TodoMVC for the CARLOS bakeoff, written in Swift. It's live at https://todo-swift.bfj.oncarlos.com.

The server renders every page, and every action is an ordinary HTML form, so the app works with JavaScript off. With JavaScript on, a small ES module sends the same forms with fetch and swaps in the new list, so it behaves like the TodoMVC reference apps. AGENTS.md has the rule the app lives by and its tenancy declaration; CONTRACT.md at the repo root has the routes and markup it shares with the other entries.

The stack, and why

| Piece | Choice | Why | |---|---|---| | Language | Swift 6.2, strict concurrency | The setup under test. The Static Linux SDK links musl and the Swift runtime into one static binary | | HTTP server | Hummingbird 2 | 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 | | SQLite | SQLiteNIO | 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 | | Header types | swift-http-types | Already under Hummingbird; listed because the app names HTTPField itself | | 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 | | 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 | | Tests | Swift Testing, with HummingbirdTesting driving the router | No server or port needed for the unit tests | | Arguments | Parsed by hand | Three flags didn't justify swift-argument-parser |

Two size decisions: Hummingbird's default FullFoundation trait is switched off and the app only imports FoundationEssentials, so the ICU data that full Foundation carries stays out. That took the arm64 binary from 66 MB to 22 MB. The shipped binary is also stripped.

How it fits together

  • Sources/todo/main.swift reads the flags and starts the server.
  • Server.swift parses --socket/--listen/--db, works out the app's own origin (https://$CARLOS_HOST on a socket, http://127.0.0.1:<port> with --listen) and runs Hummingbird until SIGTERM.
  • Routes.swift holds every route, including the GET / handler.
  • Visitors.swift gives each visitor a list through a todo_list cookie (128 random bits, HttpOnly; SameSite=Lax; Path=/, plus Secure on a socket, where the edge serves https). It also holds the Origin check that answers 403 to cross-site posts.
  • Store.swift is the SQLite layer: WAL mode, append-only migrations tracked by PRAGMA user_version, and one SQL statement per change. An action on an item that isn't in the caller's list changes no row and answers 404.
  • Page.swift renders the page from shared/reference.html's markup.
  • static/app.js is the JavaScript; static/app.css places the no-JavaScript controls over the checkboxes they stand in for and hides them when JavaScript runs.

With JavaScript, an action sends the header X-Todo-Partial: 1 and gets back the new section.main and footer.footer (200) instead of a 303. Without that header nothing changes.

Build, run, test, deploy

Put the toolchain on PATH first: source ../../scripts/env.sh. The Makefile does that itself.

| Command | Does | |---|---| | make build | Builds for this machine into dist/app (dynamically linked against the local Swift runtime) | | make build-arm64 | Builds the shipped static binary, dist/app-linux-arm64 | | make test | Runs the unit tests | | make run | Runs dist/app on 127.0.0.1:8080 (PORT= to change) | | make clean | Removes dist/ | | make clean-deps | Also removes every compiled artifact under .cache/build, keeping the downloaded sources | | make bump / make unbump | Changes a string constant in Routes.swift, forcing that file to recompile | | make deploy | Deploys dist/app-linux-arm64 to Carloku |

SwiftPM's checkouts, repository cache and build output all live in .cache/ (--scratch-path and --cache-path), so nothing is shared with other projects.

The build version is the commit's short SHA. make writes it into Sources/TodoApp/Version.swift (git-ignored), with -dirty added when apps/swift has uncommitted changes. make deploy refuses a dirty build, or a binary built at another commit.

The shared behaviour suite:

make build
dist/app --listen 127.0.0.1:8080 --db $TMPDIR/swift.db &
../../scripts/test-app.sh http://127.0.0.1:8080 $TMPDIR/swift-suite.json

Before a deploy, ../../scripts/boot-check.sh dist/app-linux-arm64 runs the arm64 binary under qemu on a unix socket and checks /healthz, /api/version and a clean exit on SIGTERM. Then:

git commit ...
make build-arm64 deploy MSG="what changed"

The app was created on Carloku once with:

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

Trade-offs

  • Trust class C: titles are stored as plain text, and anyone holding the database or a replica can read every list. See AGENTS.md.
  • A list belongs to a cookie. Clear it and the list is gone for good. Abandoned lists are never erased.
  • Every visit without a valid cookie creates a list row, crawlers included. The rows are tiny, and there's no cleanup yet.
  • The one SQLite connection serialises all queries. That's plenty for a todo list. A busier app would add a read pool.