YmerNode.References.Cache (Ymer Node v0.5.0)

Copy Markdown View Source

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"| Dir

Filling 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:

answerthe node
%{"unchanged" => true}, with an entry from that script to keepkeeps the entry, moves its checked_at
%{"content" => text, "format" => type, "reference_validator" => v} from a text-storing script, type a text/* oneguards the text and keeps it, stamping fetched_at and checked_at
%{"format" => type, "reference_validator" => v} from a file-storing script that wrote cache_pathkeeps the file, named for the reference
anything elserefuses 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/gif and image/webp, stored as a file and no larger than image_limit/0 once 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

dir()

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.

discard_file(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.

drop(reference_id)

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.

file_of(reference_id)

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.

image_limit()

The largest image, in base64-encoded bytes, served as image content.

read(reference, declarations, options \\ [])

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.

recipe(reference, declarations)

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.

reconcile()

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.

refresh(reference, declarations)

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.

text_limit()

The largest text, in bytes, a text-storing script's answer is kept at.