The cache: what the targets of this node's references say, fetched by each
reference's own script and kept here — one cache entry per reference, text
in the node database, files in the cache directory. What the cache is for,
and the rule that keeps a reference a pointer while its content lives here,
is YmerNode.References § The membrane.
The node never fetches. Filling a cache entry is running the reference's fetch
recipe — the URL action YmerNode.References.Sources derives for its uri at
this moment — through YmerNode.Scripts.run/3, the door every run takes, and
keeping what it answers. Read-only, never a working copy — how far an entry
may lag its target is the membrane's — and every answer about one says how
old it is.
flowchart TD
Cache[YmerNode.References.Cache]
subgraph owned
Entry[CacheEntry schema]
Window
Reconcile
end
subgraph external
Sources[References.Sources]
Scripts[YmerNode.Scripts]
Repo[(YmerNode.Repo)]
Dir[(cache directory)]
end
Cache -->|"the recipe, at this moment"| Sources
Cache -->|"run/3 on the URL action"| Scripts
Cache --> Entry
Cache -->|"a text entry's lines"| Window
Cache --> Repo
Cache -->|"file entries"| Dir
Reconcile -->|"at boot: unnamed files go"| DirFilling a cache entry
The URL action is called with the reference's uri under url, its fragment
under fragment where it has one, and — when the entry was filled by the
same script — the reference_validator that script answered last time. A
script that stores files (cache: :file in its declarations) is also
handed cache_path, an absolute path in the cache directory to write the
bytes to: a fresh name for every run, renamed into place when the answer is
kept and deleted when it is not, so two refreshes of one reference never
write one file.
What the action may answer, and what the node makes of it:
| answer | the node |
|---|---|
%{"unchanged" => true}, with an entry from that script to keep | keeps the entry, moves its checked_at |
%{"content" => text, "format" => type, "reference_validator" => v} from a text-storing script, type a text/* one | guards the text and keeps it, stamping fetched_at and checked_at |
%{"format" => type, "reference_validator" => v} from a file-storing script that wrote cache_path | keeps the file, named for the reference |
| anything else | refuses as :not_cache_contract, naming the script as one that predates the contract |
format is a media type and defaults to text/markdown; a script that
stores text answers a text/* one, because text is all a text entry can be
served as — anything else is converted first, or kept by a script that
stores files. Text is kept up to text_limit/0 bytes.
reference_validator is optional, the script's own string, and never read
here. The text guard is the runner's and one of its own: an answer that is
not valid UTF-8 never encodes and is refused by YmerNode.Scripts.Runner as
:result_not_encodable; text carrying a NUL byte is refused here as
:not_text. Every refusal — a script's error, a timeout, a guard — keeps
the entry that was there.
A cache entry records the target it was fetched for — the reference's uri
and fragment — and the reference is read again once the run is over: an
answer for a target the reference no longer names, because it was moved or
removed while the run was in flight, is refused as :reference_changed
and never kept.
A cache entry records the script that filled it. When the recipe names another script today — a new claim on the host, a script removed, the web fallback taking over — the entry is a miss, refetched by the new script with no validator, because a validator means something only to the script that issued it; the file the replaced entry named, if any, goes with it. An entry fetched for another target than the reference names now is a miss too, and so is a file entry whose file is gone. A reference no script fetches any more has its entry dropped.
Serving a cache entry
format alone decides how an entry is served, never where it is kept:
text/*by line window (YmerNode.References.Cache.Window); a file entry's text is guarded as it is read;image/png,image/jpeg,image/gifandimage/webp, stored as a file and no larger thanimage_limit/0once base64-encoded, as image content;- anything else — only ever a file entry, since text is kept only as
text/*— is described: its format, its size and its path relative to the cache directory, because a model cannot read it as it stands, and a script meant for one should convert it.
An image's size is taken from the file before it is read, so an image over the limit is never loaded to be refused.
The cache directory
dir/0. The node's, not the user's: a sibling of the files directory under
the mount, never inside it, and rebuildable together with the node database:
it follows the database. At boot YmerNode.References.Cache.Reconcile
removes every file in it that carries the node's naming and that no entry
names, so a fresh node database empties it and a run cut short leaves nothing
behind, and it removes every entry whose file is gone. Between boots an entry
whose file is gone is a miss, fetched again on the next read.
Summary
Functions
The cache directory, an absolute path — config :ymer_node, YmerNode.References.Cache, :dir, which config/runtime.exs writes from
CACHE_PATH in the prod environment and config/dev.exs and
config/test.exs name for their own. An absent key is a configuration
defect this refuses by name.
Removes a file from the cache directory by the name file_of/1 gave; nil is
nothing to remove. For the caller that deleted the entry's reference, whose
cascade took the row and left the file.
Drops a reference's entry and its file, if it has one. Answers :ok either
way: an entry that is not there is already dropped.
The name of the file a reference's cache entry keeps, relative to the cache directory — nil when the entry keeps text, or there is none.
The largest image, in base64-encoded bytes, served as image content.
Serves a reference's entry, fetching it once when there is none or when
another script than the recipe's filled it. options are the window's —
:offset, :limit and :column — for a text entry.
The script and action that fetch a reference right now, and how that script
stores — or {:error, {:no_recipe, detail}} naming the reference as a
pointer only.
Creates the cache directory, removes every file in it that carries the
node's naming — <reference id>.<extension> or <reference id>.part-<n> —
and that no entry names, and deletes every entry whose file is gone. Answers
the file names it removed. Nothing else in the directory is touched.
Runs the reference's recipe now and keeps what it answers, handing the
entry's reference_validator back when the same script filled it.
The largest text, in bytes, a text-storing script's answer is kept at.
Functions
The cache directory, an absolute path — config :ymer_node, YmerNode.References.Cache, :dir, which config/runtime.exs writes from
CACHE_PATH in the prod environment and config/dev.exs and
config/test.exs name for their own. An absent key is a configuration
defect this refuses by name.
Removes a file from the cache directory by the name file_of/1 gave; nil is
nothing to remove. For the caller that deleted the entry's reference, whose
cascade took the row and left the file.
Drops a reference's entry and its file, if it has one. Answers :ok either
way: an entry that is not there is already dropped.
The name of the file a reference's cache entry keeps, relative to the cache directory — nil when the entry keeps text, or there is none.
The largest image, in base64-encoded bytes, served as image content.
Serves a reference's entry, fetching it once when there is none or when
another script than the recipe's filled it. options are the window's —
:offset, :limit and :column — for a text entry.
Answers {:ok, %{entry: entry, fetched: boolean, served: served}} where
served is {:text, window}, {:image, base64, media_type} or
{:described, note}, or the refusal refresh/2 would give.
The script and action that fetch a reference right now, and how that script
stores — or {:error, {:no_recipe, detail}} naming the reference as a
pointer only.
Reads nothing: the declarations are the caller's one read of the seam.
Creates the cache directory, removes every file in it that carries the
node's naming — <reference id>.<extension> or <reference id>.part-<n> —
and that no entry names, and deletes every entry whose file is gone. Answers
the file names it removed. Nothing else in the directory is touched.
Runs the reference's recipe now and keeps what it answers, handing the
entry's reference_validator back when the same script filled it.
Answers {:ok, %{outcome: "fetched" | "unchanged", entry: entry}}, or the
refusal — the entry that was there stays. A reference no script fetches has
its entry dropped.
The largest text, in bytes, a text-storing script's answer is kept at.