| new file mode 100644 |
| index 0000000..b696c6c |
| --- /dev/null |
| +++ b/apps/swift/README.md |
| @@ -0,0 +1,114 @@ |
| +# 🤖 todo-swift |
| + |
| +TodoMVC for the CARLOS bakeoff, written in Swift. It's live at |
| +[https://todo-swift.bfj.oncarlos.com](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](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 | |
| +| 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 | |
| +| Header types | [swift-http-types](https://github.com/apple/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. |