- whole porch track (2-8) now brainstormed and locked (all ready) - four decisions: three hooks (on-listen/on-shutdown/on-route-registered); healthcheck ships BOTH /livez + /readyz; directory listing off-by-default, documented; Last-Modified via a new small time.utc(ms)->TimeParts builtin - language enhancement: YES, one small builtin -- time.utc, a gmtime sibling of time.local (time.local is local-tz, time.iso is UTC-but-ISO); IMS by string-equality, no date parser. The track's third + smallest language touch - byte ranges/large files via fs.read_at + iteration 6 writer; not lang-41-exposed - track language bill now explicit: random_bytes (2), deflate+crc32 (7), time.utc (8) -- each a builtin with a named consumer, none decoration - validated against .dev/reference/fiber. Board: whole track marked ready (cherry picked from commit 9801fceade799e25718606177f09e4306a579e98)
10 KiB
| track | iteration | status | readiness |
|---|---|---|---|
| porch | 8 | pending | ready |
porch 8 — static files, lifecycle hooks, and the small middleware everyone ships
Part of Story —
porch, the writeonce web framework. Source: the Fiber parity study §3, §5, re-checked 2026-09-06 against.dev/reference/fiber(v3,3ca9a9d) and the existinghttp/files.wo. The static half needs iteration 6 and iteration 5'sDownloadhelper; theLast-Modifiedhalf needs one small language-track builtin (time.utc, decision 4). The rest is pure.wo. Not exposed to the lang-41 hang.The clean-up iteration, and the last in the track. Individually every item is small; together they are most of what makes a framework feel finished rather than adequate.
Goals
- Static files that can serve something large.
StaticFilesexists and is already careful — traversal is refused,max_bytesis a hard ceiling, and the site's/dldownloads run through it — but it reads the whole file withfs.read_alland so cannot serve a file it cannot hold in memory, resume a partial download, or let a browser cache correctly. Byte ranges are what make video and large downloads work at all. - Cache headers that let a client skip the request.
etag_for/with_etaggive conditional GETs already;Cache-Control,Last-ModifiedandIf-Modified-Sinceare the other half, andfs.statalready returns the mtime they need. - Lifecycle hooks. The ledger records "no user teardown hooks yet". porch needs a small handful; the shutdown one is the one that matters, because an app with its own resources currently has nowhere to close them.
- The small middleware every framework ships: healthcheck, favicon,
redirect, rewrite, and a
skipcombinator — each a handful of lines whose absence is felt immediately.
Decisions locked (brainstorm 2026-09-06)
- Three lifecycle hooks: on-listen, on-shutdown, on-route-registered. Hooks
are classes, like everything else. on-shutdown is the essential one, wired
into
env.stopping(), and runs exactly once even on a trapping path. on-listen runs app code after the port binds (post-bind warmup, a readiness signal). on-route-registered fires at registration time for plugins and logging — it composes with iteration 5's route introspection rather than duplicating it (a fire-at-registration hook versus a query of the table, two different timings). - The healthcheck ships both liveness and readiness as distinct endpoints.
/livezis a static 200 (the process is up, do not kill me);/readyzconsults an app-supplied readiness predicate (DB reachable, warmup done) and answers 503 until ready (do not send traffic yet). Conflating them is why deployments flap; shipping one endpoint that is silently only one of the two is the trap. - Directory listing ships, off by default, documented. Index resolution is
always on (a directory resolves to
index.htmland friends). A listing is available behind a config flag defaulting off, with the doc stating plainly what it exposes — the alternative is every app hand-rolling a worse, leakier one. Traversal refusal is re-proven against both the index-resolution and listing paths, which is exactly where traversal creeps back in. Last-Modifieduses a new smalltime.utc(ms) -> TimePartsbuiltin.time.localis localtime (wrong zone for an HTTP-date) andtime.isois UTC but ISO format (wrong shape).time.utcis the gmtime sibling oftime.local— the sameTimePartsrecord (includingdow) but in UTC — a tiny, broadly-reusable builtin that fills a genuine gap (any GMT timestamp or log line wants it). HTTP-date formatting is then pure.wo(static day/month name tables plus the record fields).If-Modified-Sinceis handled by string-equality against the formattedLast-Modified— the echo-back flow clients actually use — so no HTTP-date parser is needed. This is the track's third and smallest language touch, after iteration 2'srandom_bytesand iteration 7'sdeflate/crc32.
Phases
Phase A — ranges, cache headers, and the time.utc builtin
- Add
time.utc(ms) -> TimeParts(decision 4): a module builtin at the next freewob.hid (re-check — the runtime-v2 track has been consuming them),gmtime_r-based, mirroringtime.local's implementation. Document it in the builtin-surface contract in the same change. Rangerequest parsing (single range first; multi-range can wait),206 Partial Content,Content-Range, andAccept-Ranges. An unsatisfiable range is416, not a truncated200.Last-Modifiedfromfs.stat's mtime formatted withtime.utc,If-Modified-Sinceby string-equality, andCache-Controlwith a configurable max-age.- Serve the body through iteration 6's writer with
fs.read_at, so file size stops bounding what can be served (retiring thefs.read_allwhole-file read). - Verify: a ranged request returns exactly the requested bytes; a file much larger than the arena serves; a conditional request returns 304 with no body.
Phase B — directory behaviour
- Index-file resolution and an optional directory listing defaulting off (decision 3).
Download/Attachmentdisposition, reusing the helper iteration 5 added.- Re-verify the traversal refusal against every new path.
- Verify: index resolution works; listing is off unless asked for; traversal is still refused on all new paths.
Phase C — lifecycle hooks
- The three hook interfaces (decision 1). Wire the shutdown hook into the
existing
env.stopping()path so an app can flush and close before the process exits, guaranteed to run exactly once even on a trapping path. - Verify: the shutdown hook fires on SIGTERM before the listener closes, once; a trapping hook does not prevent shutdown; on-listen fires after bind; the on-route-registered hook fires per registration.
Phase D — the small middleware set
- Healthcheck (both endpoints, decision 2), favicon, redirect (permanent and
temporary), rewrite (internal, no round trip), and
skipwrapping another middleware with a predicate. - Verify: each behaves;
skipcomposes with the existing chain in registration order;/livezand/readyzanswer independently.
Phase E — the gate and the ledger
- Both serving gates. The site is the natural subject: it already serves
/favicon.svg,/health, and/dldownloads throughStaticFiles. - Close out the porch track's ledger rows and record what the whole track actually landed versus what the Fiber study predicted.
- Verify:
just web-app,just site,just linkcheckgreen.
Acceptance Criteria
- Given a
Range: bytes=a-brequest, when it is served, then the response is206with exactly those bytes and a correctContent-Range. - Given an unsatisfiable range, when it is served, then the response
is
416, never a truncated200. - Given a file larger than the heap, when it is requested, then it serves completely and peak memory does not track file size.
- Given
If-Modified-Sinceequal to the file's formattedLast-Modified, when the request arrives, then the response is304with no body. - Given a directory with an index file, when the directory is requested,
then the index is served; and with no index and listing disabled, the
response is
404, not a listing. - Given a traversal attempt through the index-resolution and listing paths, when it is served, then it is refused.
- Given a registered shutdown hook, when the process receives SIGTERM, then the hook runs exactly once before the listener closes, and in-flight requests still complete.
- Given a hook that traps, when shutdown runs, then shutdown still completes.
- Given
/livezand/readyz, when the app is up but not ready, then/livezis 200 and/readyzis 503. - Given
skipwrapping a middleware with a predicate, when the predicate matches, then the wrapped middleware does not run and the chain continues in order.
Out Of Scope
- Multi-range requests. A multipart byte-range response is a separate format; single ranges cover downloads and media seeking.
- Precompressed asset serving (
file.gzbesidefile). Composes with iteration 7; worth doing once, later, when both exist. - A file-watching or hot-reload story. Caching with invalidation is language iteration 18.
pprof,expvar, metrics endpoints. Language iteration 30. A healthcheck is one bit, not observability.- Fiber's fork, mount and prefork hooks. The shard runtime owns placement; there is no worker-pool to hook.
SendFilewith kernelsendfile(2). No such builtin exists and no iteration owns adding one; the streaming writer is the portable answer.- An HTTP-date parser.
If-Modified-Sinceis matched by string-equality against the formattedLast-Modified(decision 4); parsing arbitrary HTTP-date shapes is not needed for the echo-back flow.
Info
The one language dependency is small: time.utc, a gmtime_r sibling of the
existing time.local. Everything else is pure .wo — fs.read_at (id 44)
plus iteration 6's writer for ranges and large files, fs.stat (id 42) for
mtime, env.stopping() (id 50) for the shutdown hook, and the small middleware.
None of it touches actors or the per-key pool, so, like the rest of the track
since iteration 1, it is not exposed to the lang-41 hang.
With this iteration ready, the whole porch track (2–8) is brainstormed and
locked. The three language touches the track needs are now explicit and small:
random_bytes (2), deflate/crc32 (7), and time.utc (8) — each a builtin
with a named consumer, none of them a primitive shipped as decoration.