YmerNode.Script.Context (Ymer Node v0.2.1)

Copy Markdown View Source

What the node hands every run — Req, JSON, the notebook, secrets, the throttles it declares, the node's time zone, the files directory and the Typst renders — provided by the node rather than declared by the script.

A script's code carries logic, not plumbing. It never opens a connection pool, never reads a config file and never learns where the node keeps anything: it calls the functions here and the node decides what they reach. That is what makes a script small enough to read before accepting it, and it is why the boundary is which scripts are accepted rather than what a running one can touch — see YmerNode's Scripts section.

HTTP

request/2 is Req with the node's own options merged underneath the script's, so the node can set a policy the script does not have to remember and a test can stub every outbound call at one seam. Two of the policy's options matter: retry: false, because a run is already bounded by a timeout and Req's default retry would spend most of it sleeping; and a receive timeout well inside the run's own. A script that wants retries asks for them.

Secrets

secret/2 resolves a name through YmerNode.Secrets — but only a name the script declared. An undeclared name is refused rather than resolved, so declarations/0 is a real list of what a script can reach and not a comment: a reader accepting the code sees every secret it can ask for. The value is read from disk at the call, so a secret set while the node runs is seen by the next run.

Every refusal carries the name that could not be resolved, which is what lets a run that propagates it name the secret and the verb that sets it rather than answering a bare atom nobody can act on. A machine with no secrets file at all reads the same way as a file that does not set the name: for a resolution both mean "not set here", and the caller needs the name either way.

Throttles

A system that punishes bursts or repeated failures — an account that locks after a run of failed logins, an API with a budget of requests — is protected by a throttle. Declare it under throttles in declarations/0, where YmerNode.Script.throttle/0 lists the parameters, and name it on every request that should pass through it with the throttle: option of request/2. The node keeps one throttle per name, shared by every script that declares the name and by every run of them, which is why two accepted scripts may declare one name only with the same parameters.

A request waits for a token from the throttle's bucket at most until the run's deadline, and is refused at once when the requests already waiting and the rate say it would not get one in time. Every attempt pays a token, a retry included, and every response's status reaches the breaker. A refused request is never sent: request/2 answers {:error, exception}, and the exception's message names the throttle, what stopped the request and — for an open breaker — the verb a person runs on this machine to reset it. The context offers no reset: a script that reaches past it into the node's own modules is outside the promise, and acceptance is where a reader sees it.

A throttle guards request/2 and nothing else: a script calling Req itself goes around it. A node that restarts forgets every throttle, so its bucket is full and its breaker closed again.

Files

The files directory, files_dir/1, is where a file crosses between the person and the script: what a person or their client drops there a script reads, and what a script writes there a person opens — a spreadsheet a colleague dropped in, an export another person opens. Nothing confines a script to it, because acceptance is the boundary rather than what a running script can touch (YmerNode's Scripts section): it is where files are expected, not a wall. The node creates it at boot and never backs it up; the files in it are the user's, not the node's store.

Typst

render_to_pdf/4, render_to_png/4 and render_to_svg/4 render Typst markup with the node's policy merged underneath the script's own options: the files directory is the document's root, and the font store is built afresh for every render, so a font installed since the node started is seen by the next one. A script passes nothing and may override anything — a loop of many renders asks for cache_fonts: true itself, in the options after its bindings, and shares the node's one font store with every render asking for the same fonts: it is built by whichever asked first and rebuilt whenever a render asks for a different set.

A render sees every font installed on the machine the node runs on, put there the way any application's font is, beside the four families inside the package's native library; a node running in a container sees what the container has, not what the host machine has.

A script calling Typst itself goes around the policy, as one calling Req goes around the throttles: its root is the release's own directory and its font store is built at the first render and kept until the node restarts.

The notebook

The four functions mirror YmerNode.Notebook's own surface — execute/1, query/1, tables/0, table_schema/1 — and cross no boundary the LLM has not already crossed: it writes the notebook directly through the notebook tool. There is nothing here to declare, which is why YmerNode.Script.declarations/0 has no notebook key.

Summary

Types

t()

What the node hands every run — Req, JSON, the notebook, secrets, the throttles it declares, the node's time zone, the files directory and the Typst renders — provided by the node rather than declared by the script. The struct itself carries the script's own name, the action being run, the secret names its declarations allow it to resolve, the throttles they declare, and the run's deadline as the System.monotonic_time(:millisecond) instant it falls at.

Functions

Runs one DDL/DML statement against the notebook — YmerNode.Notebook.execute/1.

The files directory — the one directory on this machine a script reads files from and writes files to, <install>/data/files on the host and the directory FILES_PATH names in the container — created by the node at boot, never backed up by it, and the user's own: what a person or their client drops there a script reads with File, and what a script writes there a person opens. No path sits in a script's text, so a script synced between a user's nodes is the same bytes on every node, and each node answers its own.

Runs one read-only statement against the notebook — YmerNode.Notebook.query/1.

Typst options the node merges under every script render (config :ymer_node, YmerNode.Script.Context, :render_options), beneath the root, which is the files directory at every render.

Renders Typst markup to a PDF — Typst.render_to_pdf/3 with the node's policy merged underneath the script's own options, § Typst above.

Renders Typst markup to one PNG per page — Typst.render_to_png/3 with the node's policy merged underneath the script's own options, § Typst above.

Renders Typst markup to one SVG per page — Typst.render_to_svg/3 with the node's policy merged underneath the script's own options, § Typst above.

Makes one HTTP request through Req, with the node's request_options/0 merged underneath the script's own — so a script can override any of them and gets a sane policy when it overrides none.

Req options the node merges under every script request (config :ymer_node, YmerNode.Script.Context, :request_options).

Resolves a declared secret by name.

Full schema for one notebook table — YmerNode.Notebook.table_schema/1.

Lists the notebook's user tables — YmerNode.Notebook.tables/0.

The node's time zone, as the IANA name of the zone this node runs in — read from TZ when the container boots, Etc/UTC when it is unset — for a script to name the zone it reports in without carrying one: a script synced between a user's nodes is the same bytes on every node, and each node answers its own. A script may still name any zone literally.

Types

t()

@type t() :: %YmerNode.Script.Context{
  action: atom(),
  deadline: integer(),
  script: String.t(),
  secrets: [String.t()],
  throttles: %{required(String.t()) => YmerNode.Script.throttle()}
}

What the node hands every run — Req, JSON, the notebook, secrets, the throttles it declares, the node's time zone, the files directory and the Typst renders — provided by the node rather than declared by the script. The struct itself carries the script's own name, the action being run, the secret names its declarations allow it to resolve, the throttles they declare, and the run's deadline as the System.monotonic_time(:millisecond) instant it falls at.

It carries no connection, no client and no credential — those are reached through the functions on this module, which is what lets the node change any of them without touching a single script.

Functions

execute(context, sql)

Runs one DDL/DML statement against the notebook — YmerNode.Notebook.execute/1.

files_dir(context)

The files directory — the one directory on this machine a script reads files from and writes files to, <install>/data/files on the host and the directory FILES_PATH names in the container — created by the node at boot, never backed up by it, and the user's own: what a person or their client drops there a script reads with File, and what a script writes there a person opens. No path sits in a script's text, so a script synced between a user's nodes is the same bytes on every node, and each node answers its own.

Always an absolute path, as a string, so a script reading a file there writes File.read!(Path.join(Context.files_dir(context), "issues.xlsx")). File and Path are the whole of what a script needs: sub-directories are the script's to make (File.mkdir_p!/1) and the user's to arrange. The value is config :ymer_node, YmerNode.Script.Context, :files_dirconfig/runtime.exs writes it from FILES_PATH in the prod environment, config/dev.exs and config/test.exs name a directory of their own — so every environment sets it, and an absent key is a configuration defect this refuses by name rather than a case to default.

query(context, sql)

Runs one read-only statement against the notebook — YmerNode.Notebook.query/1.

render_options()

Typst options the node merges under every script render (config :ymer_node, YmerNode.Script.Context, :render_options), beneath the root, which is the files directory at every render.

Defaults to cache_fonts: false: the font store is built afresh for every render, so a font installed since the node started is seen by the next one. A node on a machine whose font scan is slow sets cache_fonts: true here, once, rather than in scripts synced between its user's nodes; a script that wants the cache for a loop still asks for it itself, § Typst above.

render_to_pdf(context, markup, bindings \\ [], options \\ [])

Renders Typst markup to a PDF — Typst.render_to_pdf/3 with the node's policy merged underneath the script's own options, § Typst above.

Answers the package's own {:ok, binary} | {:error, message}, so a script rendering a report writes {:ok, pdf} = Context.render_to_pdf(context, markup, summary: Typst.Format.escape(summary)) and puts the bytes where the person opens them — a binding interpolated as markup is escaped first, or a # in the text fails the render.

render_to_png(context, markup, bindings \\ [], options \\ [])

Renders Typst markup to one PNG per page — Typst.render_to_png/3 with the node's policy merged underneath the script's own options, § Typst above.

Answers the package's own {:ok, [binary]} | {:error, message}, one binary per page in order, so a script counting pages writes {:ok, pages} = Context.render_to_png(context, markup).

render_to_svg(context, markup, bindings \\ [], options \\ [])

Renders Typst markup to one SVG per page — Typst.render_to_svg/3 with the node's policy merged underneath the script's own options, § Typst above.

Answers the package's own {:ok, [String.t()]} | {:error, message}, one string per page in order, so a script embedding the first page writes {:ok, [first | _rest]} = Context.render_to_svg(context, markup).

request(context, options)

Makes one HTTP request through Req, with the node's request_options/0 merged underneath the script's own — so a script can override any of them and gets a sane policy when it overrides none.

throttle: name sends the request through the throttle the script declared under that name, § Throttles above: it waits for a token first, and its status reaches the throttle's breaker, every retried attempt's included.

Answers Req's own {:ok, %Req.Response{}} | {:error, exception} — the exception a YmerNode.Scripts.Throttle.Error when a throttle refused the request before it was sent. A script that wants only the body matches on %{status: 200, body: body}.

request_options()

Req options the node merges under every script request (config :ymer_node, YmerNode.Script.Context, :request_options).

Defaults to retry: false and a 15 s receive timeout. config/test.exs adds a :plug pointing at Req.Test, which is what lets a test stub a script's outbound calls without the script knowing.

secret(context, name)

Resolves a declared secret by name.

{:error, {:secret_undeclared, name}} when declarations/0 does not list the name — the declaration is enforced, not documentation — and {:error, {:secret_not_found, name}} when it is declared but not set on this machine. Both carry the name, so the two branches are symmetric and a run that hands either back can say which secret it wanted.

table_schema(context, table)

Full schema for one notebook table — YmerNode.Notebook.table_schema/1.

tables(context)

Lists the notebook's user tables — YmerNode.Notebook.tables/0.

time_zone(context)

The node's time zone, as the IANA name of the zone this node runs in — read from TZ when the container boots, Etc/UTC when it is unset — for a script to name the zone it reports in without carrying one: a script synced between a user's nodes is the same bytes on every node, and each node answers its own. A script may still name any zone literally.

Always a string, and always one every zone-aware DateTime function accepts, so a script reporting in it writes DateTime.shift_zone(instant, Context.time_zone(context)). The value is config :ymer_node, YmerNode.Script.Context, :time_zone: config/runtime.exs writes it from TZ in the prod environment alone, refusing boot on a name the release's zone database does not know, so a dev or test node answers Etc/UTC.