- `readiness: ready | refine` is a SECOND axis, orthogonal to status.
`ready` = the brainstorm is complete and the decisions are LOCKED (a spec
approved, or the forks explicitly confirmed). `refine` = open forks remain
and it cannot be planned yet
- `status: refine` RETIRED because it carried both meanings at once, so a held
iteration with an approved spec (language 18, 26) was indistinguishable from
one nobody had thought about. status is now purely where the WORK is:
done | in-progress | pending | hold — `pending` was already the board's own
rendering word, so nothing new was invented
- all 47 iterations classified from EVIDENCE in their own text, not by guess:
"the four forks are SETTLED" / "spec + plan approved" / "Approved spec:" for
ready; "Forks the spec must settle" / "no spec exists yet" for refine. Every
shipped iteration is ready by definition. 19 done, 5 in-progress, 15
pending, 8 hold; 27 ready, 20 refine
- two iterations moved refine -> in-progress rather than -> pending: language
31 and 34 are absorbed into 24 and work on them is literally happening, which
the board already showed as 🔄 while their frontmatter said otherwise. That
disagreement is now gone
- board legend, board-views' frontmatter contract, and two new Dataview
queries updated — the useful one being `readiness: ready AND status:
pending`, the startable set
WHAT THE NEW AXIS IMMEDIATELY SURFACED: of 15 pending iterations, exactly ONE
is startable — databasev2 4, io_uring group-commit, whose forks were confirmed
settled 2026-08-20. Everything else pending needs a brainstorm first. That was
invisible while one key carried both meanings, and it is now on the board.
Also caught by the sweep, unrelated to readiness but found by cross-checking
frontmatter against the board: SIX duplicate rows. Every iteration moved into
databasev2 was still listed in the LANGUAGE pending table under its retired id
(23, 32, 33, 20, 21, 27) as well as its new one. Stale copies removed. And two
databasev2 rows made claims the sweep contradicts — iteration 1 was billed
"startable today" while its forks are open, and 6 still called itself the
ceiling-raiser after 2 took that role.
Docs only. linkcheck 0 broken / 0 anchors.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
154 lines
7.8 KiB
Markdown
154 lines
7.8 KiB
Markdown
---
|
|
track: porch
|
|
iteration: "8"
|
|
status: pending
|
|
readiness: refine
|
|
---
|
|
|
|
# porch 8 — static files, lifecycle hooks, and the small middleware everyone ships
|
|
|
|
> Part of [Story — `porch`, the writeonce web framework](00-story.md).
|
|
> Source: [the Fiber parity study](../../plan/exploration/fiber/00-fiber-parity.md) §3, §5.
|
|
> The static half needs [iteration 6](06-streaming-core.md); 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.** `StaticFiles` exists and is
|
|
already careful — traversal is refused rather than normalised, `max_bytes` is a
|
|
hard ceiling, and the site's `/dl` downloads 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 ships `ByteRange`, `MaxAge`,
|
|
`CacheDuration`, `IndexNames`, `Browse` and `Download`; 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_etag`
|
|
give conditional GETs; `Cache-Control`, `Last-Modified` and `If-Modified-Since`
|
|
are the other half, and `fs.stat` already 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 `skip` combinator. 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
|
|
|
|
- `Range` request parsing (single range first; multi-range is a multipart
|
|
response and can wait), `206 Partial Content`, `Content-Range`, and
|
|
`Accept-Ranges`. An unsatisfiable range is `416`, not a truncated `200`.
|
|
- `Last-Modified` from `fs.stat`'s mtime, `If-Modified-Since` handling, and
|
|
`Cache-Control` with 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.html` and 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`/`Attachment` disposition, reusing the helper iteration
|
|
[5](05-routing-response-ergonomics.md) 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 `skip` wrapping another middleware with a predicate.
|
|
- Verify: each behaves; `skip` composes 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 `/dl` downloads through `StaticFiles`, 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 linkcheck` green.
|
|
|
|
## Acceptance Criteria
|
|
|
|
- **Given** a `Range: bytes=a-b` request, **when** it is served, **then** the
|
|
response is `206` with exactly those bytes and a correct `Content-Range`.
|
|
- **Given** an unsatisfiable range, **when** it is served, **then** the response
|
|
is `416`, never a truncated `200`.
|
|
- **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-Since` matching the file's mtime, **when** the request
|
|
arrives, **then** the response is `304` with 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** `skip` wrapping 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.gz` beside `file`). Composes with
|
|
iteration [7](07-sse-and-compression.md); 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](../language-runtime-database/18-memory-db-features.md) 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.
|
|
- **`SendFile` with kernel `sendfile(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:
|
|
|
|
1. **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](05-routing-response-ergonomics.md) may already cover. Fewer is
|
|
better; each hook is a contract forever.
|
|
2. **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 `/health` should say which it is, and probably ships both.
|
|
3. **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.
|