Not Everything Worth Reading Is a File
Loomis does not only want thumbnails. It wants Eagle's folder tree, an item list, an item's metadata. Soul Vault does not only want note bytes; it wants a manifest of every note with its hash, a vault's change events after a cursor, a note's history. These are questions, not paths, and the root class has a verb for them: api(op, params), a read-only operation that returns JSON. A caller sends GET /v1/api/<share>/<op>?…; the server finds the share and the operation name, checks can_read exactly as for a file, and hands the rest to the child.
Read-only is a hard line. Every operation behind api is a GET, its answer may be cached on the receiving Mac like a file's bytes, and nothing about it may change the owner's data. The one way a share's owner writes on another Mac's behalf is a different verb with different rules entirely (call, track 7).
Two Ways to Answer "Nothing Changed"
An API answer can be large, and most of the time the caller already holds the current one. HTTP's conditional requests exist for that: the caller sends the ETag it holds in If-None-Match, and the owner answers 304 Not Modified with no body when it still matches. Delta's serve_api gets a 304 out of a child in one of two ways, and the order matters:
- The child's validator, before any work. The server first calls
validator(op, params). A child that can tell cheaply whether an answer changed returns a tag; if it matches what the caller sent, the server answers 304 andapiis never called. Loomis' child does this foritem-info: the answer changes only when Eagle rewrites that item'smetadata.json, so the tag is that file's mtime and size, and a revalidation costs onestatand no Eagle API call. Soul Vault's child returns the store's own change tag forvaults,manifestandnotes: no note is read to decide. - The answer's own content, after the work. When the child has no cheap tag, the server runs
api, serializes the JSON, and uses the first 32 hex digits of its SHA-256 as the ETag. If that matches, it still answers 304: the owner did the work, but the bytes do not cross the network again.
Both answers carry Cache-Control: private, no-cache, which in HTTP means you may keep this, but ask me before you use it. That is the contract Delta's receiving cache follows (track 5), and it is why validators come from the owner instead of a guessed time-to-live.
Derived Outputs: Build Once Per Source Version
Some reads want a file that does not exist yet: a thumbnail of an image that was never given one, a PNG of a TIFF a browser cannot show. Building it on every request would be wasteful; building it once and forgetting when the source changes would be wrong. The root class reserves a hook for exactly this, derive(entry, kind, params), and Delta's serving side owns the machinery around it:
- The child supplies only a recipe: a file suffix and an argument list with
{src}and{out}placeholders, orNonewhen that kind is not derivable. A caller asks for it with?derive=<kind>on an ordinary file read. - The key is the source's version: SHA-256 over the share id, path, kind, parameters, the source's ETag (size, mtime, inode) and the recipe itself. Edit the source, or change the recipe, and the key changes, so a stale output is never served; it simply stops being asked for.
- Two callers build it once: a per-key lock makes the second caller wait for the first one's result.
- It never fills the disk: before building, Delta checks the volume against the family's disk floor and refuses with a named
Unavailablewhen it is under. - It is built whole or not at all: the recipe writes to a temporary file, runs with a 60-second limit, and a non-zero exit or an empty output is a named
derive_failederror carrying the tail of the tool's stderr. Only a good result is moved into place with an atomicos.replace. - It is bounded: outputs live under Delta's state root, never beside the source, capped at 4 GiB by default (
derived_cap_gibin the host config). Past the cap, the least recently used outputs are deleted until the total is under 90% of it.
The served output gets its own ETag, the source's tag with the kind appended, so a receiving cache keeps the original and its thumbnail as two different things that each revalidate against the same source.
A Hook That Is Ready Before Its First User
Honesty about the present: as this quest is written, no shipped child overrides derive. Loomis' Eagle child answers a thumbnail request with Eagle's own generated thumbnail and falls back to the original when Eagle made none; the docstring of Delta's serving module names "an Eagle thumbnail when Eagle made none, a PNG of a TIFF" as the cases the hook exists for. The machinery (keys, locks, the floor, the cap, the eviction) is built and tested in the root, so the first child that needs it writes a two-line recipe, not a cache.