- http/parse.wo: bounded-read buffering to the header terminator, then
exactly Content-Length body bytes; %XX decoding ('+' = space in query
strings only, malformed escapes pass through — parsing stays total);
path/query split with decoded pairs; header names lowercased; the
three-state Parsed record (closed / malformed / request) with keep-alive
carry-over — bytes past this request belong to the next one on the
connection.
- http/serve.wo: Dispatcher interface (the router's seam), status/reason
serialization with computed Content-Length, and the blocking loop:
malformed -> 400 + close; a trapping handler -> 500 AND the loop lives;
fds closed on every path; env.stopping() honored.
- Connection policy discovered by probing, not assumed: a parked keep-alive
connection BLOCKS accept on a single-threaded server (probe: client 1
idles open, client 2 starves). Policy: serve PIPELINED requests on one
connection (carry non-empty), close when the client would idle; a proxy
reconnects. README states it.
Verified against a throwaway echo app (not committed): %20 query decode;
two pipelined requests -> two responses on one connection; DIV0 handler ->
500 and the NEXT connection served; GARBAGE -> 400; SIGTERM stops clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
48 lines
2.3 KiB
Markdown
48 lines
2.3 KiB
Markdown
# 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.
|
|
|
|
```toml
|
|
[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-Length`
|
|
bodies, %-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 block `accept` and 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 `:param` captures into
|
|
`req.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 `Handler` interface
|
|
(`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, a `Resp`
|
|
short-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-Length` bodies 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.
|