Skip to main content

Module crossing

Module crossing 

Source
Expand description

Crossing a workspace boundary — opening a peer a PeerResolver located, and descending the spanning tree into it. The layer above prov_graph::peer, which stops at an address on purpose. Crossing a workspace boundary — opening a peer, and descending into it.

prov_graph::peer declares where the other workspaces are and stops there, in so many words: “nothing here opens a workspace, and nothing here can: reading the peer would need a second ReadStorage and a second IdIndex, which only the host has.” That is true of prov-graph, and it stays true — it is why Graph keeps two type parameters and why no method on it takes a resolver.

It is not true one layer up. discovery::build already opens a second Workspace at an arbitrary directory out of the same storage handle, because the storage seam is device-wide rather than root-scoped: a crate that can walk up the filesystem to find a root it was not given can also open a root a resolver hands it. So the descent lives here, beside discovery, and prov-graph learns nothing.

This keeps the promise peer makes rather than breaking it. Following a foreign reference is still “a second step after resolution, taken by a caller that wants it”; all that changes is that prov ships that second step once instead of each host writing it.

§What stays refused

  • A PeerLocation::Url is never opened. prov does no network I/O, and this is the one absolute in the module: a URL peer is an address to render, not a root to read. It is Refusal::Url under every trust level, including one a resolver has confirmed by its own means.
  • No writes across a boundary. Everything here reads. Registration stays a publish-time contract — prov never reaches into another workspace to register on its behalf — so a reference to an unpublished foreign document can still dangle, and a mv still rewrites no peer’s inbound references.
  • No peer table in the config. A name is a fact about an archive, a location is a fact about a disk; being able to follow a map does not make the map the archive’s business. Every location here comes from a PeerResolver the host supplied.
  • No resolver on Graph, and no cost to a traversal that never crosses.
  • No following by default. NoPeers is what a consumer that was given no resolver uses, and under it descend is exactly tree with every foreign leaf carrying Refusal::Unknown.

§Every refusal is an answer

peer has no error case, on purpose: “a peer that cannot be found, cannot be read, or turns out to be someone else is never a failure, because a foreign reference is carried whether or not it resolves.” That does not change because someone asked to follow it, so Refusal is never an Err — it is the Boundary a leaf carries, and it is what lets a followed tree say why (“the peer notes is on record at a directory that calls itself journal”) rather than silently showing what an unfollowed tree shows.

§Termination

Two workspaces may list each other, and an org root plus N repositories that each link back is the expected shape rather than an exotic one. So descend keeps a trail of the workspaces on the current path from the origin, and refuses a peer already on it with Refusal::Cycle — a leaf, not an error.

A trail rather than a global visited set, for the reason tree gives for the same choice one level down: revisiting a workspace from another branch is fine, because each branch materializes its own subtree, and that is what makes this a tree rather than a DAG rendered flat. An org root that names two documents of one repository — its README and its docs/tasks index, say — renders both, exactly as Graph::tree renders a document reached from two branches. Only a back-edge to a workspace on the current path is a cycle. NodeKind::Cycle stays a statement about one workspace’s own spanning tree; this is the same shape one level up.

The trail is keyed on both halves, because both failures are reachable: two names for one directory (a symlinked checkout) and two directories claiming one name (the failure PeerLookup::confirm exists to catch, arriving one hop later). The directory half is the lexically normalized root directory — neither storage port offers a canonicalize, so a symlink is not resolved away and it is the declared workspace_id that catches that case. The origin is seeded onto the trail, so a peer that lists it back stops there.

Cost stays one open per peer whatever the trail does: opened peers are memoized by name for the walk’s duration, so an org root with many links into one repository opens it once and walks it as many times as it is named. Federation::workspaces therefore lists every peer opened, once, in first-reached order.

Structs§

Descent
How far a descend goes, and on whose say-so.
Federation
A spanning tree that crossed one or more workspace boundaries, and the workspaces it crossed into.
Node
One node of a federated spanning tree.
Peer
A peer workspace the reader opened for reading — read-only, always.
Reached
One workspace a descend entered.

Enums§

Boundary
What a node’s position says about the boundary it sits on.
Crossing
The outcome of open_peer: a workspace, or the reason there is not one.
Refusal
Why a boundary was not crossed. Every variant is an answer, never an error.
Trust
How much doubt a reader will accept about which workspace it is opening.

Constants§

DEFAULT_DEPTH
How many boundaries a descend crosses before it stops, when the caller names no bound of its own. Crossings, not tree levels: a workspace’s own depth is bounded by its spanning tree and needs no help.

Functions§

descend
Materialize the spanning tree from start, following every confirmed foreign spanning leaf into the peer it names and continuing there.
open_discovered
Open the read-only Workspace a Discovered describes.
open_peer
Open the workspace named workspace, if the host knows where it is and trust allows following what it says.