- docs/stories/porch/ — a TRACK folder, not a status folder: status still
lives only in frontmatter. Adds `track: porch` so a query over
docs/stories/ can tell a porch 3 from a language 3
- 00-story.md carries the sequence, the dependency graph, and a table of
what the track explicitly does NOT own (binding -> 29, cache -> 18,
proxy -> 38, metrics -> 30, TLS/templates -> doctrine)
- eight iterations, each with phases, per-phase tasks, Given/When/Then
criteria, out-of-scope and the forks a spec must settle:
1 store-backed middleware (limiter + idempotency — needs nothing new,
first on purpose so the store pattern is proven cheaply)
2 randomness + cookies (phase A is language-track: a CSPRNG builtin;
`Resp.headers` being a map cannot emit two Set-Cookie lines)
3 sessions 4 CSRF 5 routing/response ergonomics (independent)
6 streaming core (the seam 7 and 8 wait on; chunked-request refusal
must survive) 7 SSE + compression 8 static + lifecycle hooks
- language iteration 39 -> status: hold, retitled superseded, with a row
mapping each of its goals to the porch iteration that took it. Kept, not
deleted: the Fiber study cites it and its randomness argument is what
this track is built on
- board gains a porch section; board-views gains porch and both-track
Dataview queries; porch README and the Fiber study §7 point at the track
- no code blocks in any story (plans carry concept and actions in words);
linkcheck 0 broken / 0 anchors
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
7.7 KiB
7.7 KiB
| track | iteration | status |
|---|---|---|
| porch | 8 | refine |
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. The static half needs iteration 6; the rest does not.The clean-up iteration. 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 rather than normalised,max_bytesis a hard ceiling, and the site's/dldownloads run through it. What it cannot do is serve a file it cannot hold in memory, resume a partial download, or let a browser cache correctly. Fiber's static shipsByteRange,MaxAge,CacheDuration,IndexNames,BrowseandDownload; the range support is what makes video and large downloads work at all. - Cache headers that let a client skip the request.
etag_for/with_etaggive conditional GETs;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" as a known gap. Fiber has eleven hook families; porch needs a small handful — on-listen, on-shutdown, and on-route-registered — and 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 is a handful of lines and their absence is felt immediately by anyone starting a new app.
Phases
Phase A — ranges and cache headers
Rangerequest parsing (single range first; multi-range is a multipart response and can wait),206 Partial Content,Content-Range, andAccept-Ranges. An unsatisfiable range is416, not a truncated200.Last-Modifiedfromfs.stat's mtime,If-Modified-Sincehandling, andCache-Controlwith a configurable max-age.- Serve the body through iteration 6's writer so file size stops bounding what can be served.
- 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 (
index.htmland friends) and an optional directory listing, defaulting off — a listing that is on by default is an information leak the first time someone points it at the wrong directory. Download/Attachmentdisposition, reusing the helper iteration 5 added.- Re-verify the traversal refusal against every new path (index resolution and listing both construct paths, which is exactly where traversal creeps back in).
- Verify: index resolution works; listing is off unless asked for; traversal is still refused on all new paths.
Phase C — lifecycle hooks
- Decide the minimal set (fork 1) and the interface — hooks are classes, like everything else here.
- Wire the shutdown hook into the existing
env.stopping()path so an app can flush and close before the process exits, and guarantee it runs 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.
Phase D — the small middleware set
- Healthcheck (liveness and readiness are different questions — say which),
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.
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, so these features have a real consumer rather than a synthetic one. - 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-Sincematching the file's mtime, 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 — the existing guarantee, re-proven against the new code paths.
- 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
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, which is what the workload needs.
- Precompressed asset serving (
file.gzbesidefile). Composes with iteration 7; worth doing once, later, when both exist. - A file-watching or hot-reload story. Assets are read from disk per request; a cache with invalidation is a different feature and language iteration 18 owns caching.
pprof,expvar, metrics endpoints. Language iteration 30 (no story file yet). A healthcheck is not observability — it is one bit.- 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 here.
Info
Forks the spec must settle:
- Which hooks, exactly? Fiber has eleven families and porch needs the fewest that are load-bearing. On-shutdown is clearly one — an app with open resources has nowhere to close them today. On-listen is convenient for a startup banner. On-route-registered is only useful for introspection, which iteration 5 may already cover. Fewer is better; each hook is a contract forever.
- Liveness or readiness for healthcheck? They answer different questions
and conflating them is why deployments flap: liveness says "do not kill me",
readiness says "do not send me traffic yet". A framework shipping one
endpoint called
/healthshould say which it is, and probably ships both. - Is directory listing available at all? Off-by-default is not the same as present-but-off. Shipping it at all means it will eventually be switched on somewhere it should not be. Leaning: ship it, off, with the doc saying plainly what it exposes — the alternative is every app hand-rolling a worse one.