workspace
Source: crates/lvcs-ws/src/workspace/mod.rs.
/// The document a span of a view comes from. Ordered (decision 89): the
/// base, then overrides by workspace id and then override id.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum Layer {
/// The `file` document at the path: the bottom of the stack.
Base,
/// An override document of this workspace or of one it stacks on,
/// with the id of the workspace it belongs to, so a stacked workspace
/// can address any layer exactly (`OVERRIDE_ADDRESS` with both ids).
Override {
/// The workspace the override belongs to.
workspace: WorkspaceId,
/// The override's id.
id: OverrideId,
},
}
/// One run of a view's text taken from one layer.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Span {
/// Code points of the view text; never 0.
pub len: usize,
/// The layer the run is taken from.
pub layer: Layer,
/// Where the run starts in the layer's `/content`, in code points at
/// that layer's heads in the view's frontier, so a workspace stacked on
/// this one can place anchors made in any layer.
pub from: usize,
}
/// Which layer each part of a view's body came from (design §8), so a save
/// through the mirror reaches the right layer and a file with no overrides
/// can be materialized as a clone of the base file.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum SpanMap {
/// A text body: spans covering it in order, their lengths summing to
/// its code points (none for an empty text).
Text(Vec<Span>),
/// A binary body, taken whole from one layer (binary files have no
/// ranges, design §3).
Binary(Layer),
/// A deleted body: the base file is deleted and this workspace holds
/// no range override of it, or a whiteout in this workspace or one it
/// stacks on hides it.
Deleted,
}
/// A workspace's composed view of one file (design §8, decision 82).
///
/// It is the base view with each claimed range swapped for its override's
/// text, whiteouts deleted, and workspace-local files added. A claim whose
/// whole
/// range the base deleted (`AnchorSpan::Collapsed { at }`) keeps its
/// override's text at the gap where the range was, overrides collapsed at
/// one gap in claim-time start order and then id (decisions 92 and 99).
/// While the base file is deleted, its anchor spans are ignored and the
/// file stays present as this workspace's range overrides of it, their
/// texts in claim-time start order and then id (decision 92).
/// `DocRendered`-shaped:
/// the shell hashes `body` and hands it to this workspace's mirror as a
/// `DocRendered`, and to every workspace stacked on this one as its base
/// render. `heads` is the view's frontier: the sorted, deduplicated union of
/// the heads of every document composed (the base view's and each
/// override's), and `layers` maps each of those documents, as the layer it
/// is, to its own heads (decision 89): the base view's layers and each
/// override of this workspace the view composes, so a claim on any layer
/// is made at the heads of the document that holds its characters. `after`
/// keeps the mirror's save rule (decisions 75 and 78):
/// the workspace emits exactly one render whose `after` is the `stamp` of
/// each [`ViewSave`], once every layer edit that save produced is answered
/// (for a save a part of which could not be routed, with the next save of
/// its path that lands whole), and `None` for every other render.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ViewRendered {
/// The file's path.
pub path: WsPath,
/// The view's frontier.
#[serde(with = "cbor::heads")]
pub heads: Vec<ChangeHash>,
/// The composed body.
pub body: FileBody,
/// The view's executable bit.
pub executable: bool,
/// The `stamp` of the [`ViewSave`] this render answers; `None` for
/// every other render.
pub after: Option<u64>,
/// Which layer each part of `body` came from.
pub spans: SpanMap,
/// Each document composed, as the layer it is, with its own heads.
#[serde(with = "cbor::head_map")]
pub layers: BTreeMap<Layer, Vec<ChangeHash>>,
}
/// An `edit` of `path` through this workspace's view (design §8).
///
/// `edit.base` is a view frontier (a [`ViewRendered`]'s `heads`, so it
/// mixes the heads of several documents), and the splices are code points
/// of the view text there. Copy-on-write (design §8): a splice spanning
/// several layers is split at their boundaries, its inserted text going to
/// the layer where it starts (decision 100); the part of the edit inside an
/// existing claim goes to that override; the parts outside every claim that
/// fall in one maximal unclaimed run of a layer, a run that a collapsed
/// override's gap bounds, so no claim holds one strictly inside it
/// (decision 100), are claimed first, together, as one range (`Claim`, a
/// new override) and go there; nothing reaches a base document (decision
/// 99). An insertion at a gap where overrides of this workspace with empty
/// text sit (emptied, or collapsed, decision 92) goes to the first of them
/// in claim-time start order, then id; else one at a gap that touches this
/// workspace's claims goes to the override whose claim ends just before the
/// gap, else to the one that starts just after it (an override whose text
/// is empty keeps its claim); only a gap touching none of them is claimed
/// (decisions 95 and 99). An edit or save that creates the file where the
/// base has none, or a deleted one (at genesis or after history, whited out
/// or not), makes it a workspace-local file, which merge-back recreates
/// (decision 98). One that creates the file in a view showing none of this
/// workspace's claims of it (whited out, or a binary base) claims the whole
/// document. Either way the claims of the path that the view hides leave
/// the index with it (range claims, and whole-document ones under a
/// concurrent whiteout, M3), since claims never overlap (decision 84). The
/// edit applies whole or not at all (decision 90): its parts outside every
/// claim are claimed and their new overrides initialized first, its claims
/// to one document one at a time (each after the previous `Claimed` and the
/// watch that includes it), and its parts are routed together only once
/// every claim has succeeded; a refused claim answers the edit `Rejected`
/// with nothing routed (an override already created keeps its claim-time
/// text). The edits and saves of one path run one at a time, each starting
/// once the one before is answered, and an edit whose `base` is older than
/// the path's newest view is rebased onto that view (the changes in between
/// are kept, as for concurrent edits) and split through its span map. An
/// edit that arrives during a merge-back (after its `ws.merge` request)
/// waits for the merge's answer, one that arrived before the request runs
/// before the merge, and one whose `base` is a view from before the last
/// merge-back of its path is rejected `UnknownBase` (decision 94).
/// `participant` is the connection's participant: the claimant key of any
/// override the edit claims. `edit.req` is unique in the workspace
/// (decision 91): the shell assigns it, increasing per workspace, and maps
/// the answer back to the connection's own request id, since a workspace
/// answers out of order across claim round trips; the actor rejects
/// (`Failed`) a `ViewEdit` that starts while an edit with its `req` is in
/// flight (started and not yet answered). Answered with exactly one
/// [`WorkspaceOutput::Answer`] for `edit.req`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ViewEdit {
/// The file edited.
pub path: WsPath,
/// The connection's participant: the claimant of any override the edit
/// claims.
pub participant: ParticipantKey,
/// The edit, its splices in the view's code points at `edit.base`.
pub edit: Edit,
}
/// A save through this workspace's mirror: the mirror's `DiskEdit` for
/// `path`, which the shell routes here instead of to a document (decision
/// 82).
///
/// It waits for its path like a [`ViewEdit`], is diffed against the view it
/// was made against (of the views named by the path's latest landed save,
/// its `candidates`, and the newest view, the one whose text differs least
/// from its own, ties to the earliest: the mirror's rule, decision 78; when
/// that is a view from before the last merge-back of its path, the first
/// view after the merge instead, reported as stale with
/// [`WorkspaceOutput::StaleSave`], decision 94), rebased onto the newest
/// view, split through the span map, and
/// routed like a [`ViewEdit`]: to the overrides it touches, claiming new
/// ones outside every claim, never to a base document. Each part reaches
/// its override as a text save whose `loaded` heads are the override's
/// heads in the view the part was made from, so the override diffs it
/// against exactly that text, as a view edit's part is applied at its
/// base, and never against its author's latest change there. A removal of
/// a file the view shows whites the path out, recording the heads of the
/// base render that the view its `base` names was composed from (decision 86;
/// none when it names no view the workspace emitted), so merge-back never
/// deletes a base edit that never reached disk; this workspace's range
/// claims of the path leave the index with it, so a whiteout and range
/// claims never coexist (decision 97). Concurrent whiteouts of one path
/// (M3: participants on two peers removing it) write one index key, and
/// the entry the index resolves it to is the path's whiteout, whose heads
/// govern merge-back and whose participant its conflict names; the
/// removal is done once the index shows the path whited out by anyone.
/// Such a whiteout may hide a whole-document claim of the path (a
/// removal of a view showing one goes to that override instead), which
/// leaves the index when the path is created again, as for a
/// [`ViewEdit`].
/// A derived save (row 180: `edit.derived` is `Mark`) is never split: one
/// that changes the file goes to the view's whole-document or
/// workspace-local override when it has one, and otherwise claims the
/// whole file, as a non-text body's save does, retiring the path's range
/// claims; either way the override takes its whole text with the save's
/// flag, and an override document treats a `Keep` save as derived when its
/// own `/meta/derived` is true. `participant` is
/// the daemon's default participant, the mirror's author (decision 64).
/// Like a view edit it applies whole or not at all. Answered with exactly
/// one [`ViewRendered`] whose `after` is `edit.stamp`. A save that can't
/// be routed (a claim refused or lost) is answered with the render of the
/// next save of its path that lands whole, which holds its text too, since
/// a save is the whole file: so the mirror never writes a view without a
/// save over it.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ViewSave {
/// The file saved.
pub path: WsPath,
/// The daemon's default participant, the mirror's author.
pub participant: ParticipantKey,
/// The mirror's `DiskEdit`.
pub edit: DiskEdit,
}
/// How a document, or this workspace's view, answered an edit.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum EditOutcome {
/// Applied; the document's heads (for a view, the view frontier) after
/// it.
Applied {
/// The heads after the edit.
#[serde(with = "cbor::heads")]
heads: Vec<ChangeHash>,
},
/// Refused, for this reason.
Rejected(RejectReason),
}
/// A document answered an `Edit` or `BundleEdit` this workspace routed to
/// it.
///
/// It is the document's `DocOutput::Changed` for that `req` (`Applied`) or
/// `DocOutput::Rejected`, which the shell routes back here by `address` and
/// `req`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LayerAnswer {
/// The document that answered.
pub address: String,
/// The routed edit's request.
pub req: RequestId,
/// How the document answered.
pub outcome: EditOutcome,
}
/// A stacked workspace's merge-back, landed on the workspace it stacks on
/// (design §8, decision 101's M3 deferral; frozen at the M3 freeze for
/// `M3.workspace`).
///
/// The base workspace absorbs it copy-on-write, as view edits of its own
/// overrides (claiming where its view's text comes from a lower layer, as
/// for a [`ViewEdit`]), so the work reaches `main` only through the base
/// workspace's own merge-back, and nothing is written to any base document
/// or the registry. `from` is the merging workspace and `req` its merge's
/// request, which [`Absorbed`] echoes; `origin` and `merger` are the
/// merging participant's (the claimant of any override the absorb claims).
/// `paths` holds each path the merge changes: `base` is the base
/// workspace's view frontier of the path the merging workspace composed
/// against (its base render's `heads`), and `changes` are written over the
/// base workspace's view there, splices in view code points, as a
/// document's [`BundleEdit`]s are over it.
///
/// The base workspace absorbs each path's changes in order, as ops of the
/// path that run after the view edits and saves that arrived before the
/// bundle, by `merger`: `Text` and `Recreate` as a view edit at `base`
/// (rebased onto the path's newest view, like any view edit; a `Recreate`
/// inserts its text into the view there, which shows the file deleted),
/// and `Binary`, `Executable`, and `Removed` as the matching save (a
/// removal whites the path out, recording the base render the view at
/// `base` was composed from). The path's first change is refused
/// `UnknownBase` when `base` is no view the base workspace emitted since
/// the last merge-back of the path, and `Failed` when it replaces the
/// whole file (`Recreate`, `Binary`, `Removed`) and the path's newest view
/// is no longer `base`, so it never drops an edit the merging workspace
/// didn't see (a recreate onto a view that shows the file again is a
/// conflict that keeps the base, decision 98), and `Failed` when the path
/// sets a binary body or an executable bit that the base workspace's view
/// would never take: one that swaps in range claims of the base workspace,
/// or would once the path's text edit claims ranges in it, shows its
/// base's body and bit (claims never combine, decision 84). A later change
/// that can't land then (after a restart) is refused `Failed` too. A
/// refused change is the path's outcome, and its later changes
/// never run; otherwise the path is `Applied` at the view frontier after
/// its last change, answered (except at a restart) only once the base
/// workspace has rendered that view, so the merging workspace's base
/// render already shows it.
///
/// `paths` is never empty: a stacked merge whose plan changes nothing
/// finishes without a bundle, so no `Absorbed` with no outcome answers a
/// merge, and the merging workspace counts a path an `Absorbed` doesn't
/// name as `Rejected(Failed)`. (The base workspace answers a bundle with
/// no path, which no workspace sends, at once with no outcome, and a path
/// with no change `Rejected(Failed)`.)
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StackedBundle {
/// The merging workspace.
pub from: WorkspaceId,
/// Its merge's request, which [`Absorbed`] echoes.
pub req: RequestId,
/// The merging participant's origin.
pub origin: Origin,
/// The merging participant: the claimant of any override the absorb
/// claims.
pub merger: ParticipantKey,
/// Each path the merge changes, with its changes.
pub paths: BTreeMap<WsPath, StackedChange>,
}
/// One path's changes in a [`StackedBundle`]: in the order they apply,
/// as `merge::plan` gives one document's (a body change, then an
/// executable bit), never empty.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StackedChange {
/// The base workspace's view frontier of the path that the merging
/// workspace composed against.
#[serde(with = "cbor::heads")]
pub base: Vec<ChangeHash>,
/// The changes, in order, written over the view there.
pub changes: Vec<BundleChange>,
}
/// The base workspace's answer to a [`StackedBundle`] from `from` for its
/// merge `req`.
///
/// It holds each path's outcome: `Applied` with the base workspace's view
/// frontier after it, or `Rejected` (`UnknownBase` for a `base` the base
/// workspace emitted no view at since the last merge-back of the path,
/// `Failed` when it can't absorb now, and otherwise the refusal of the
/// change's claim or routed edit, as for a view edit). Each change applies
/// whole or not at all, like a view edit, and a path's changes after a
/// refused one never run (a restart can end a path between its body change
/// and its executable bit); the stacked workspace closes its changeset only
/// when every path applied, and otherwise answers its merge with the
/// refused paths as `rejected` (decision 88).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Absorbed {
/// The merging workspace.
pub from: WorkspaceId,
/// Its merge's request.
pub req: RequestId,
/// Each path's outcome.
pub outcomes: BTreeMap<WsPath, EditOutcome>,
}
/// Outputs of the workspace actor.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WorkspaceOutput {
/// The composed view of one file, for this workspace's mirror and every
/// workspace stacked on this one.
Rendered(ViewRendered),
/// Route `edit` to the override document at `address`, one of this
/// workspace's (copy-on-write, design §8: an edit inside an existing
/// claim goes to that override, and one outside every claim to the new
/// override its claim created; nothing a workspace writes reaches a base
/// document before merge-back). `edit.req` is chosen by the workspace
/// actor, increasing per workspace; the shell keeps it apart from
/// connection requests and answers with [`WorkspaceViewEvents::LayerAnswer`].
Edit {
/// The override document.
address: String,
/// The edit routed.
edit: Edit,
},
/// Route part of a [`ViewSave`] to the override document at `address`,
/// one of this workspace's, carrying the save's `stamp`: a text save of
/// the override's whole new text, or a binary, executable-bit, or
/// removal change of a whole-document or workspace-local override. The
/// override answers with exactly one `OverrideRendered` whose `after` is
/// that stamp. Never routed to a base document: nothing a workspace
/// writes reaches one before merge-back.
Save {
/// The override document.
address: String,
/// The save's change, with its stamp.
edit: DiskEdit,
},
/// Ask the document at `address`, which holds the claimed characters in
/// the base view, to anchor a claim at that document's heads in the
/// view (its layer's entry in [`ViewRendered::layers`], decision 89); it
/// answers with `Claimed`.
Claim {
/// The document holding the claimed characters.
address: String,
/// The claim.
claim: Claim,
},
/// Create the override document at `address` (`OVERRIDE_ADDRESS` with
/// this workspace's id and the new override's id) and deliver `init` as
/// its first event.
CreateOverride {
/// The new override document's address.
address: String,
/// Its first event.
init: OverrideInit,
},
/// Set this workspace's watched anchors in the document at `address`.
Watch {
/// The watched document.
address: String,
/// This workspace's anchors in it.
watch: WatchAnchors,
},
/// Change this workspace's index document (`WORKSPACE_INDEX_ADDRESS`).
Index(IndexChange),
/// Answer a [`ViewEdit`] with its `edit.req`.
Answer {
/// The answered edit's `edit.req`.
req: RequestId,
/// How the view answered it.
outcome: EditOutcome,
},
/// A text [`ViewSave`] made against a view from before the last
/// merge-back of its path is diffed against the first view after the
/// merge instead (decision 94), mirroring `DocOutput::StaleSave`, for
/// the daemon's `stale_save` push: emitted when the save starts, before
/// anything it routes, with `chosen_base` the frontier of the view the
/// save is diffed against and `newest_base` that of the newest view of
/// `path` then.
StaleSave {
/// The saved file.
path: WsPath,
/// The frontier of the view the save is diffed against.
chosen_base: Vec<ChangeHash>,
/// The frontier of the path's newest view.
newest_base: Vec<ChangeHash>,
},
/// Read the document at `address` at past heads, for a merge-back's
/// inputs; `read.req` is chosen like a routed edit's, and the answer
/// comes back as [`WorkspaceViewEvents::ReadAtDone`].
ReadAt {
/// The document read.
address: String,
/// What to read.
read: ReadAt,
},
/// Route one merge-back change of the plan to the base document at
/// `address`; answered with [`WorkspaceViewEvents::LayerAnswer`].
Bundle {
/// The base document.
address: String,
/// The change.
edit: BundleEdit,
},
/// Write the bundle's merge record to `.lvcs/registry`
/// (`REGISTRY_ADDRESS`), once every change of the bundle is answered
/// and applied. A bundle a change of which was rejected gets no record,
/// so none of it becomes visible (decision 48), and opens no thread.
Record(RecordBundle),
/// Create the thread document at `address` (`THREAD_ADDRESS`) for a
/// merge-back conflict and deliver `open` as its first event.
OpenThread {
/// The new thread document's address.
address: String,
/// Its first event.
open: ThreadOpen,
},
/// A stacked merge-back: the shell delivers `bundle` to the workspace
/// `into` (this workspace's base) as [`WorkspaceViewEvents::StackedBundle`], or,
/// when that workspace isn't running, answers this one with an
/// [`Absorbed`] refusing every path `Failed`. `bundle.paths` is never
/// empty.
MergeInto {
/// The workspace this one stacks on.
into: WorkspaceId,
/// The bundle it absorbs.
bundle: StackedBundle,
},
/// The answer to a [`WorkspaceViewEvents::StackedBundle`], which the shell delivers
/// to `from` as [`WorkspaceViewEvents::Absorbed`].
Absorbed(Absorbed),
/// Answer a [`MergeRequest`]. A merge that writes its record closes the
/// changeset first (decision 94): once every document it changed has
/// rendered the change, every override and whiteout it read leaves the
/// index (`IndexEdit::RemoveOverride`, `RemoveLocal` for a
/// workspace-local file, `ClearWhiteout`), and the merge is answered
/// when the index render shows that, after the views of its paths,
/// which fall through to the merged base. (A clear is shown once the
/// path no longer holds the whiteout the merge read: another
/// participant's concurrent whiteout of the path, which the clear never
/// saw, stays for the next merge-back.) A conflict's thread names the
/// override documents, which keep their text. A merge a read of which
/// was refused, or a bundle change of which was rejected, writes no
/// record, opens no thread, and retires nothing: it is answered with
/// [`MergeResult::rejected`] naming those documents (decision 88). A
/// stacked merge closes its changeset the same way once every path it
/// sent applied and its base renders show them, and is answered with
/// a record it never writes, its documents the paths it changed, each
/// with the workspace below's view frontiers before and after; a path
/// refused or missing from the `Absorbed` answer is `rejected`.
Merged(MergeResult),
}