- router/router.wo: Handler (handle(req) -> Resp) and Middleware (before(req) -> ?Resp; nil = continue) structural interfaces; Route/Mw record classes built as ctor literals at the registration site — the ownership shape the corpus pins (run/container-owned-move); route_match with :param captures over '/'-split segments (empties dropped, first mismatch wins). - app.wo: App holds the middleware chain + route table (take-push), satisfies http's Dispatcher, dispatches middleware-then-first-match, fills the captures onto the borrowed request in place (Dispatcher takes `mut req` — the borrow checker rightly refused rebuilding a Req from borrowed maps; captures collect locally so a failed match never touches the request), 404 fallback, serve(host, port) delegation. Verified against a throwaway app (not committed): middleware 401 short-circuit without the token; /things/:id captures 42 into the body; unknown path 404; a DIV0 handler answers 500 and the next request is served; SIGTERM clean. Framework image emits at 12161 bytes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| .. | ||
| http | ||
| router | ||
| app.wo | ||
| README.md | ||
| wo.toml | ||
writeonce-framework
A web framework written in writeonce, consumed as a [deps] dependency
(iteration 15). Spec: docs/superpowers/specs/2026-08-18-web-framework-design.md §B.
[deps]
writeonce-framework = { git = "https://github.com/shoneyj/writeonce-framework", rev = "v0.1.0" }
What it is
- HTTP/1.1 server core (
http/): request parsing (Content-Lengthbodies, %-decoded paths and query strings), response serialization, a blocking serve loop that answers 400 to malformed requests, 500 to trapping handlers (and survives), closes every fd, and honors SIGTERM. Connection policy: pipelined requests are served on one connection; idle connections close after the response — on a single-threaded server a parked keep-alive connection would blockacceptand starve every other client, so closing is the correct shape until shards/fibers (8/11). A proxy in front simply reconnects. - Router (
router/): method + path table with:paramcaptures intoreq.params; first match wins; no match is the framework's 404. - Handlers without closures: the language has no function values by
doctrine, so a route handler is a class satisfying the
Handlerinterface (fn handle(req: Req) -> Resp), dispatched structurally — a non-conforming handler is a compile error (WO-E205). Middleware is its own interface (fn before(req: Req) -> ?Resp; nil = continue, aRespshort-circuits). - Data layer for free: handlers use
@table+ the query surface directly — durable, compiler-checked persistence in the same binary. No ORM, no database server.
Honest limits (v1, all deliberate)
- Single-threaded, blocking — one request at a time. Concurrency arrives underneath this same surface with the shard/fiber iterations (8/11).
- TLS: none, anywhere. Deploy behind nginx/caddy; the proxy terminates TLS+ALPN and gives browsers HTTP/2 while this backend speaks HTTP/1.1 keep-alive. See the web-app sample's README for the nginx sketch.
Content-Lengthbodies only (no chunked encoding), no WebSockets/SSE, JSON-first (no templates).
The consuming sample
docs/examples/web-app — a small storefront importing this framework
through [deps]. Its acceptance (just web-app) exercises the whole chain:
fetch → lock → build → serve → durable restart.