doc-events
Source: crates/lvcs-doc/src/actor/events.rs.
/// The Automerge actor id a change is written under (design §3), bound to
/// its signer (decisions 125 and 146): [`AuthorId::granted`] under a grant
/// whose terms name a quota, [`AuthorId::participant`] for every other
/// change, the owner's included, each with one implementation, here. Import and
/// caps-seed changes keep their `IMPORT_DOMAIN` actor ids. The verifier
/// refuses a change whose actor id isn't the derivation its record names,
/// so each actor id has exactly one possible signer.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct AuthorId(#[serde(with = "crate::cbor::fixed")] pub [u8; 16]);
/// A participant's Ed25519 public key (`lvcs_vocab::ids::SIGNATURE`),
/// written in `ID_ENCODING` on the API. Workspace ids, override ids, bundle
/// ids, and thread messages name participants by this key, never by
/// [`AuthorId`] (design §8, §9).
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct ParticipantKey(#[serde(with = "crate::cbor::fixed")] pub [u8; 32]);
/// Who caused a change and when: event data, never read inside an action
/// (design §3). `time_ms` becomes the Automerge commit time, which grant
/// windows are checked against (§15). The shell builds it at request time
/// with the live grant it checked the write against (decision 125):
/// `signer` and `grant` are what the change's record names and its
/// `ChangeSig` must sign, `device` the raw Ed25519 key of this daemon, and
/// `author` the actor id their derivation gives ([`AuthorId`]). The
/// document writes the record ([`ChangeMessage`]) from it in one place.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Origin {
pub author: AuthorId,
pub signer: ParticipantKey,
pub grant: GrantId,
pub device: DeviceKey,
/// Unix time in milliseconds, read by the shell when it accepted the
/// request. The commit time is `time_ms / 1000`, floored, because
/// Automerge commit times are in seconds (design §3).
pub time_ms: i64,
}
/// Correlates a request with its output; chosen by the daemon shell, unique
/// per connection.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct RequestId(pub u64);
/// Replace `delete` code points at `pos` with `insert`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Splice {
pub pos: usize,
pub delete: usize,
pub insert: String,
}
/// How a save treats the file's `/meta/derived` flag (row 180).
///
/// The saving daemon sets it from its classification of the path. The text
/// kind treats a [`DiskEdit`] or [`Edit`] as derived when it is `Mark`, or
/// `Keep` while `/meta/derived` is true at the document's live heads, and a
/// [`BundleEdit`] as derived when it is `Mark`. A derived text save commits
/// one whole-text replacement, based where its author last looked (never
/// at the live heads), and puts `/meta/derived` true beside `/meta/format`
/// and `/meta/deleted`. `Unmark` puts it false and saves as a text save
/// always does; `Keep` on a document that isn't derived saves the same
/// way, so a peer with a stale view never re-enables text merging. A
/// binary save is whole already: it commits as it always does and puts
/// the flag as a text save would. An executable-bit change or a removal
/// puts no flag.
///
/// [`BundleEdit`]: crate::actor::BundleEdit
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum DerivedFlag {
/// No rule applies: the document's own flag decides.
Keep,
/// A positive rule marks the path derived.
Mark,
/// An explicit negation marks it not derived.
Unmark,
}
/// Splice `/content`. Every splice's positions are Unicode code points in
/// the text at `base` (the heads the client last saw); splices are sorted by
/// `pos` and don't overlap. The document actor applies them on the author's
/// own branch (design §3, decisions 75, 78, and 125): it forks at `base`
/// plus the author's latest change of any kind (the heads of the last
/// change under every actor whose record names `origin.signer`, since the
/// actor id under a grant with a quota changes at each renewal and a
/// participant's differs per device), with the fork's actor id set to `origin.author`, translates the
/// splices from the text at `base`
/// onto the fork through element-id anchors (decision 76), commits there at
/// `origin.time_ms / 1000` seconds, and merges the fork into the live
/// document. When `base` plus the author's latest change already is the live
/// heads, it commits on the live document directly. The translation crosses
/// only the changes since `base` on the author's branch: each deleted base
/// character is deleted only if still visible, and each insert is anchored
/// on the base character before it (`MoveCursor::Before`: the nearest
/// earlier base character still visible) or on the start. The same change
/// deletes and re-puts `/meta/format` (`text`) and `/meta/deleted` (false),
/// so both are always recorded (decision 76). An unknown or empty `base`
/// answers [`DocOutput::Rejected`] with [`RejectReason::UnknownBase`];
/// splices that are unsorted, overlapping, or out of bounds at a known
/// `base` answer it with [`RejectReason::OutOfBounds`] (decision 81).
///
/// A derived edit ([`DerivedFlag`]) applies its splices to the text the
/// document renders at `base` and commits the result whole there, after
/// the same bounds check against that text; one that changes nothing
/// there commits nothing and answers `Changed` with no splices.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Edit {
pub req: RequestId,
pub origin: Origin,
#[serde(with = "crate::cbor::heads")]
pub base: Vec<ChangeHash>,
pub splices: Vec<Splice>,
/// How the edit treats `/meta/derived`.
pub derived: DerivedFlag,
}
/// Read the rendered body, or the value at `pointer` (RFC 6901) into the
/// document's Automerge structure. A thread renders no file: with no
/// pointer it reads its whole Automerge structure (design §4). Reads go
/// through the mailbox and are journaled like any event. A pointer that
/// names nothing answers [`DocOutput::Rejected`] with
/// [`RejectReason::Failed`] (decision 81).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Read {
pub req: RequestId,
pub pointer: Option<String>,
}
/// Release the document; the actor stops.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Close;
/// A change from the mirrored directory (decisions 33, 47, 54, 63, 75, and
/// 78), authored by the daemon's default participant (decision 64). Every
/// kind of change commits on the author's own branch, at its base plus the
/// author's latest change of any kind (decision 78).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DiskEdit {
pub origin: Origin,
/// Assigned by the mirror, increasing per mirror. Handling this edit
/// emits exactly one [`DocOutput::Rendered`], with this stamp as
/// `after`, whether it committed, changed nothing, or faulted before
/// rendering, so the mirror can tell which save a render follows and
/// its pending-save rule always completes (decisions 75 and 78).
pub stamp: u64,
pub change: DiskChange,
/// How the save treats `/meta/derived`: a derived text save commits
/// the whole `new_text` at the base the rule on [`DiskChange::Text`]
/// chooses ([`DerivedFlag`]).
pub derived: DerivedFlag,
}
/// What changed on disk. A change that leaves the file as it was where it
/// would commit (see each variant) makes no commit, so a save reported
/// twice is absorbed (decision 75), unless it is a text or binary save and
/// the file is deleted at the live heads: then it commits, and its re-put
/// `/meta/deleted` (false) restores the file, so a recreate after another
/// participant's removal is never lost (decision 78). An executable or
/// removal change puts no content and restores nothing, so it has no such
/// exception.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum DiskChange {
/// A text save (design §8, decisions 47, 66, 75, 78, 79, and 131). When
/// the file renders as exactly `new_text` at the live heads (not deleted,
/// `/meta/format` not `binary`, `/content` equal to `new_text`), the
/// save makes no commit and is not reported stale, whatever base it
/// would choose, and only renders (decision 79): a rescan of a file the
/// mirror has no disk base for, such as a freshly imported one, never
/// inserts its content a second time. A file deleted or rendering as
/// binary at the live heads goes through the base choice below even
/// when its `/content` is `new_text`. Otherwise the document actor
/// puts the heads of the author's latest change in the document, of
/// any kind (any actor whose record names `origin.signer`, decision
/// 125), ahead of `candidates` (dropping a later
/// candidate with the same heads, compared as sets): every change the
/// author makes forks at its base plus the author's latest change, so
/// the text there is what the author last saw plus what it wrote. It
/// skips candidates it doesn't have, and picks the one whose text
/// differs least from `new_text` (inserted plus deleted code points),
/// ties to the earliest unless a later one's version is older (the
/// earlier one's history holds it), which then wins: a stale save is
/// never taken as made from the newer version (decision 296); an empty
/// `new_text` takes the first, the
/// newest, since against it each one's distance is its own length and
/// the oldest, shortest one would win (decision 131). With none usable
/// (the author has no change in the document, and the mirror no disk
/// base the document has, as for a new file) the base is the genesis
/// heads, since the author then saw nothing, so content another
/// participant created concurrently is kept and merges as concurrent
/// text (decision 78). When the base's text is
/// `new_text` and the file renders as text (not deleted, `/meta/format`
/// not `binary`) where the save would commit, at the base plus the
/// author's latest change, nothing changes relative to the base on the
/// author's branch, and no commit is made unless the file is deleted at
/// the live heads: another participant's later binary save stays in
/// place. Otherwise it diffs the base's text against `new_text` and applies the
/// splices like an [`Edit`] with that base, on the author's own branch,
/// so the save lands as a concurrent change; that change deletes and
/// re-puts `/meta/format` (`text`) and `/meta/deleted` (false)
/// (decision 76), which restores a file the author removed or saved as
/// binary since the base, or anyone removed (decision 78).
/// A save that reaches the base choice is reported with
/// [`DocOutput::StaleSave`], changed or not, when the base is a
/// candidate other than the first, the first candidate while its
/// history lacks the author's latest change, the author's latest
/// change while `candidates` is non-empty and the history of its first
/// entry holds a change that is not part of the latest change's
/// history, or the genesis heads while the document held changes
/// beyond genesis when the save arrived. Before it commits, a save
/// whose base is not the genesis heads, made from an older version
/// than the text the document shows, is refused when the lines it
/// changes overlap lines changed since that version, measured against
/// the base plus the author's latest change: no commit,
/// [`DocOutput::SaveRefused`], and a render of the document as it is
/// (decision 296).
Text {
new_text: String,
/// Recent disk bases for the file, newest first: the heads of the
/// mirror's writes still in flight, then those of the last
/// `DISK_BASE_RING` confirmed writes (decision 75).
#[serde(with = "crate::cbor::head_lists")]
candidates: Vec<Vec<ChangeHash>>,
/// The heads of the version a hook read the file at (`track_read`,
/// decision 117), which the mirror recorded and this save takes.
/// When the document holds them, they equal or descend from the
/// author's latest change, and the save is not strictly nearer the
/// first candidate's text than theirs (decision 241: a non-empty
/// save only), they are the save's base, chosen before any other
/// candidate and never stale: the save is exactly what the author
/// read plus what it wrote. Otherwise the rule above applies as if
/// there were none: a tool that read the disk again before it
/// wrote saved the renders since the read, and a diff against the
/// read would apply them a second time.
#[serde(with = "crate::cbor::opt_heads")]
loaded: Option<Vec<ChangeHash>>,
},
/// A save of bytes that are not valid UTF-8: replaces `/bytes`
/// (last writer wins), deleting and re-putting `/meta/format`
/// (`binary`) and `/meta/deleted` (false) (decision 76), committed on
/// `origin.author`'s own branch at `base` (decision 78). No commit when
/// the file already renders exactly these bytes there and is not
/// deleted at the live heads.
Binary {
#[serde(with = "crate::cbor::bytes")]
bytes: Vec<u8>,
/// The mirror's newest known disk state of the file (decision 78);
/// empty when it has none. An empty base, or one the document
/// doesn't have, is the genesis heads: the author then saw nothing.
#[serde(with = "crate::cbor::heads")]
base: Vec<ChangeHash>,
},
/// The executable bit changed: sets `/meta/executable`, committed on
/// `origin.author`'s own branch at `base` (decision 78). No commit when
/// the bit already has this value there.
Executable {
executable: bool,
/// As for [`DiskChange::Binary`].
#[serde(with = "crate::cbor::heads")]
base: Vec<ChangeHash>,
},
/// The file was removed: sets `/meta/deleted`, committed on
/// `origin.author`'s own branch at `base` (decision 78), so another
/// participant's edit the author never saw keeps the file. No commit
/// when the file is already deleted there.
Removed {
/// As for [`DiskChange::Binary`].
#[serde(with = "crate::cbor::heads")]
base: Vec<ChangeHash>,
},
}
/// A file as a document renders it. `/meta/format` selects `/content` or
/// `/bytes` (last writer wins between them).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum FileBody {
Text(String),
Binary(#[serde(with = "crate::cbor::bytes")] Vec<u8>),
/// Every concurrent `/meta/deleted` value is true; a genesis-only
/// document is deleted.
Deleted,
}
/// Names a daemon-wide fact in the local journal (decision 233): ids rise
/// within a daemon run, and each daemon run starts them again at 0.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct FactId(pub u64);
/// A daemon-wide fact the shell hands to many documents (decision 233):
/// the journal holds it once, as a record no actor's, and each
/// recipient's entry names `id` in place of the event. The actor reads
/// `value` alone, so the id never reaches a context or a fingerprint.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Fact<T> {
/// The journal's name for it.
pub id: FactId,
/// The fact.
pub value: T,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum DocEvent {
Edit(Edit),
Read(Read),
DiskEdit(DiskEdit),
/// Emit [`DocOutput::Rendered`] for the current heads, for example to a
/// mirror that restarted (design §2).
Render,
/// The compaction tick, every `COMPACT_SECS`: emit a compacted
/// [`DocOutput::Persist`] when incremental saves were emitted since the
/// last one (design §3).
Compact,
Close,
WatchAnchors(WatchAnchors),
Claim(Claim),
ReadAt(ReadAt),
BundleEdit(BundleEdit),
/// Boxed: the claim it carries is much larger than any other event.
OverrideInit(Box<OverrideInit>),
IndexInit(IndexInit),
IndexChange(IndexChange),
RegisterWorkspace(RegisterWorkspace),
RecordBundle(RecordBundle),
/// Boxed, like [`DocEvent::OverrideInit`]: its anchor, addressees, and
/// first message make it much larger than most events.
ThreadOpen(Box<ThreadOpen>),
ThreadAppend(ThreadAppend),
/// A write to an issue (decision 304).
IssueWrite(IssueWrite),
/// Start syncing with a peer (decision 30; `M3.doc`, `M3.sync`).
PeerJoined(PeerJoined),
/// Stop syncing with a peer; a daemon-wide fact (decision 233).
PeerLeft(Fact<PeerLeft>),
/// A sync message from a peer, verified whole before Automerge sees it
/// (decision 68).
SyncIn(SyncIn),
/// The daemon's signature for a committed change (design §11).
Signed(Signed),
/// The grants every change is verified against (from `.lvcs/caps`);
/// a daemon-wide fact (decision 233).
Grants(Fact<GrantsKnown>),
/// The caps document settled with a peer: that peer's deferred sync
/// messages get their last check (decision 108); a daemon-wide fact
/// (decision 233).
CapsSettled(Fact<CapsSettled>),
/// Add a signed grant to the `.lvcs/caps` document. Boxed, like
/// [`DocEvent::OverrideInit`].
IssueGrant(Box<IssueGrant>),
/// A bundle's target heads in this document (decision 48).
BundleTargets(BundleTargets),
/// A bundle is complete everywhere: render past it (decision 48).
BundleVisible(BundleVisible),
/// Show a derived file at a counted generation's heads (row 180).
Pin(Pin),
/// Check two concurrent bundles for overlap here (decision 65). Boxed:
/// its two sides are much larger than most events.
CheckOverlap(Box<CheckOverlap>),
/// Resolve cursor pairs at the current heads (design §5).
Resolve(ResolveAnchors),
/// Anchor a notification's range at past heads (design §5; `M3.doc`).
Anchor(AnchorRange),
/// Does one set of heads descend from another (decision 120)?
Descends(DescendsQuery),
/// Where a protected file's approved heads start (decision 120).
Baseline(BaselineQuery),
/// Put an entry into a channel document (decision 119). Boxed, like
/// [`DocEvent::OverrideInit`]: an attestation carries a frontier.
ChannelPut(Box<ChannelPut>),
/// Add a hash to the `.lvcs/caps` document's signed allowlist
/// (decision 128).
AllowSecret(AllowSecret),
/// Revoke grants in the `.lvcs/caps` document (decision 148). Boxed,
/// like [`DocEvent::IssueGrant`].
RevokeGrant(Box<RevokeGrant>),
}
impl DocEvent {
/// The daemon-wide fact this event delivers, when it is one (decision
/// 233): the journal names it in place of the event.
#[must_use]
pub const fn fact(&self) -> Option<FactId> {
if let Self::Grants(Fact { id, .. })
| Self::CapsSettled(Fact { id, .. })
| Self::PeerLeft(Fact { id, .. }) = self
{
Some(*id)
} else {
None
}
}
}
/// Outputs a document actor hands to its effect sink. An applied change
/// emits, in order: `Persist`, one `SignRequest` per change it committed
/// (design §11), `Changed`, `StaleSave` (stale text saves only),
/// `Rendered`. A [`DiskEdit`] that changes nothing emits only
/// `StaleSave` (stale text saves only) and `Rendered`. A [`DiskEdit`] whose
/// handling faults emits what it emitted before the fault (at most
/// `Persist`, `Changed`, and `StaleSave`, in that order), then `Rendered`;
/// only a render that faults itself has no body to send (decision 78).
/// An override document emits `OverrideRendered` wherever a file document
/// emits `Rendered`. Every render is preceded by one `AnchorsResolved` per
/// watcher, in watcher order, at the render's heads (decision 84).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum DocOutput {
/// A change was applied. `splices` turn the live text at the previous
/// heads into the text at `heads`, computed from the two (a
/// deterministic diff, decision 75) rather than echoed from the request,
/// so subscribers can patch their buffers; `req` is set when the change
/// answers a request.
Changed {
req: Option<RequestId>,
#[serde(with = "crate::cbor::heads")]
heads: Vec<ChangeHash>,
splices: Vec<Splice>,
},
/// A request was refused; `reason` says why, so the daemon can answer
/// `UNKNOWN_BASE` and `OUT_OF_BOUNDS` correctly (decision 81).
Rejected {
req: RequestId,
reason: RejectReason,
},
/// Bytes to append to this document's store under `.lvcs/docs/`
/// (decision 30): an incremental save, or a full compacted save that
/// replaces earlier ones.
Persist {
#[serde(with = "crate::cbor::bytes")]
bytes: Vec<u8>,
compacted: bool,
},
/// The whole file at `heads`, emitted after every applied change, once
/// for every [`DiskEdit`] (also one that changes nothing or faults,
/// decision 78), and on [`DocEvent::Render`], for the mirror actor
/// (decisions 33, 63, and 75).
/// `after` is the `stamp` of the [`DiskEdit`] whose handling produced
/// this render, and `None` for any other render (decision 75).
/// `touched` is what that edit changed when it committed a text save,
/// a binary save, or a removal, at `heads`, and `None` for every other
/// render, an executable-bit change included (decision 165).
Rendered {
#[serde(with = "crate::cbor::heads")]
heads: Vec<ChangeHash>,
body: FileBody,
executable: bool,
after: Option<u64>,
touched: Option<Touched>,
/// Set when the render shows one head of a derived document alone
/// (row 180's winner rule): the head shown, and each other head
/// whose text differs, with its signer. `None` for every other
/// render.
superseded: Option<Superseded>,
},
/// The answer to a [`Read`].
ReadDone {
req: RequestId,
#[serde(with = "crate::cbor::heads")]
heads: Vec<ChangeHash>,
value: ReadValue,
},
/// A text save was applied, or found unchanged, on a possibly stale base
/// (design §8, decisions 66 and 75; the rule is on [`DiskChange::Text`]),
/// for the daemon's `stale_save` push. `chosen_base` is the base the
/// save was diffed against; `newest_base` is the heads of the author's
/// latest change when `chosen_base` is the save's first candidate,
/// otherwise the save's first candidate (the mirror's newest disk base),
/// or empty when the save had none; `heads` is the document's heads
/// after the save.
StaleSave {
#[serde(with = "crate::cbor::heads")]
chosen_base: Vec<ChangeHash>,
#[serde(with = "crate::cbor::heads")]
newest_base: Vec<ChangeHash>,
#[serde(with = "crate::cbor::heads")]
heads: Vec<ChangeHash>,
},
/// A text save was refused, uncommitted (the stale-overlap row
/// revising decision 49): it was made from an older version of the
/// file than the one the document shows, and the lines it changes
/// overlap lines changed since. Emitted just before the save's
/// `Rendered`, which renders the document as it is, for the daemon's
/// `refused_save` notification.
SaveRefused(StaleOverlap),
/// An [`Edit`] to a `file` document was refused, uncommitted (decision
/// 310): its base is older than the text the document shows, and the
/// lines it changes overlap lines another participant changed since
/// that base, measured as a stale save is (decision 296). It answers
/// the edit's request in place of `Changed` or `Rejected`.
EditRefused {
req: RequestId,
overlap: StaleOverlap,
},
AnchorsResolved(AnchorsResolved),
Claimed(Claimed),
ReadAtDone(ReadAtDone),
/// An override document's render, where a file document emits
/// [`DocOutput::Rendered`] (decision 84).
OverrideRendered(OverrideRendered),
/// A workspace index document's render, after every applied change and
/// on [`DocEvent::Render`] (it emits no `Changed`: it has no text).
IndexRendered(IndexRendered),
/// The registry document's render, after every applied change and on
/// [`DocEvent::Render`] (it emits no `Changed`: it has no text).
RegistryRendered(RegistryRendered),
/// A sync message for a peer (decision 30; `M3.doc`, `M3.sync`).
SyncOut(SyncOut),
/// Nothing more to send a peer until it answers (decision 68).
SyncSettled(SyncSettled),
/// A peer's whole sync message was rejected (decision 68).
SyncRejected(SyncRejected),
/// A committed change awaits its signature (design §11).
SignRequest(SignRequest),
/// The answer to [`DocEvent::BundleTargets`] (decision 48).
BundleHeld(BundleHeld),
/// The answer to [`DocEvent::CheckOverlap`] (decision 65).
OverlapChecked(OverlapChecked),
/// The answer to [`DocEvent::Resolve`].
Resolved(Resolved),
/// The answer to [`DocEvent::Anchor`].
Anchored(Anchored),
/// The `.lvcs/caps` document's render (design §11).
CapsRendered(CapsRendered),
/// Signatures to append to the document's signature sidecar (design
/// §11; `M3.doc`).
SaveSigs(SigSave),
/// The signer behind each Automerge actor of the document's verified
/// changes, for attribution (`M3.doc`, `M3.daemon`).
Signers(Signers),
/// The answer to [`DocEvent::Descends`] (decision 120).
DescendsDone(DescendsDone),
/// The answer to [`DocEvent::Baseline`] (decision 120).
BaselineDone(BaselineDone),
/// A channel document's render (decision 119).
ChannelRendered(ChannelRendered),
/// The revocations a peer of the `.lvcs/caps` document holds
/// (decision 326).
PeerRevocations(PeerRevocations),
}
/// Why a request answered [`DocOutput::Rejected`] (decision 81). Only an
/// [`Edit`] is rejected for its base or splices; a [`DiskEdit`] is never
/// rejected (one with an unknown base commits at the genesis heads,
/// decision 78).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum RejectReason {
/// The edit's `base` is empty or names heads the document doesn't have.
UnknownBase,
/// The edit's splices are unsorted, overlapping, or out of bounds at its
/// known `base`.
OutOfBounds,
/// Anything else: Automerge or another callback faulted while answering
/// the request, a read pointer names nothing, or the actor was closed.
Failed,
}
/// What a [`Read`] returns.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ReadValue {
/// No pointer, in a file or override document: the rendered file.
Body(FileBody),
/// The value at the pointer, as JSON text; with no pointer, in a
/// thread, the whole document.
Json(String),
}
/// The journaled fingerprint of a document actor, the one content check the
/// journal keeps beside each entry's trace (decision 286).
///
/// It holds the document's heads, which replay loads an incarnation at,
/// how much of the signature sidecar the incarnation loaded, which replay
/// loads the same prefix of, and a digest of the durable context
/// (decisions 32, 84, 222, and 286).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DocFingerprint {
#[serde(with = "crate::cbor::heads")]
pub heads: Vec<ChangeHash>,
/// The bytes of the signature sidecar the incarnation loaded (after
/// its format header). The sidecar is only ever appended to, so replay
/// loads this prefix of it: the signatures the incarnation started
/// with, not ones a later `SaveSigs` appended, which its trace would
/// otherwise show as signatures no longer awaited (decision 286).
pub sigs: u64,
/// The digest covers exactly the heads, the watch map, the grants
/// known, the deferred sync messages, the bundle gate, and the pin
/// (decisions 32, 84, and 111, row 180): the `HASH` of the canonical
/// CBOR array of five items. First the heads, each a byte string, in Automerge's sorted
/// order. Then the watch map: every [`WatchAnchors`] set in force, a
/// map from watcher id to a map from override id to its anchor. Then
/// the sync, a map of two keys: `known`, the `HASH` of the last
/// `GrantsKnown`'s canonical CBOR (null before the first), and
/// `deferred`, an array holding, for each `SyncIn` held deferred
/// (decision 108) in arrival order, a map whose one key `digest` is
/// the `HASH` of that event's canonical CBOR, its tag included; each
/// digest is taken when its event arrives, so no state re-encodes
/// them. Last the bundle gate (decision 48), a map of the bundles
/// `hidden`, made `visible`, registered (`targets`, each with its
/// target `heads` and whether it is `held`), and `merged` here in this
/// incarnation. Then the [`Pin`]'s heads, an array of byte strings, or
/// null with none. A control document has no watches, an open gate,
/// and no pin, so those items are always empty there. So the journal covers anchor
/// watches, what sync waits on, and what the gate shows as well as
/// content. Nothing else of the context is covered: the signature
/// sidecar (`sigs`, the changes still `unsigned` and those `asked`
/// for, and the `signers` pairs), which a load reads from the sidecar
/// and replay loads the same prefix of (`sigs` above); each peer's Automerge sync
/// state with its tag (decision 110), which has no canonical full
/// encoding and is rebuilt from the incarnation's own events; and the
/// save bookkeeping (heads saved, increments since a compaction) and
/// the outputs, which the heads and the events already decide.
#[serde(with = "crate::cbor::fixed")]
pub digest: [u8; 32],
}
/// Show a derived `file` document at a counted generation's heads (row 180).
///
/// `heads` are the heads of the counted generation main's visible
/// frontier shows, or `None` for no generation. While set, every render, read, and read at heads of a
/// version that includes `heads` shows the file at them (the greatest of
/// them alone when several), hiding every other head as the winner rule
/// does and naming each in `Rendered.superseded`; a version that doesn't
/// include them, or a document that doesn't hold them, is shown by the
/// winner rule. A pin that changes what the live document shows renders.
/// Every other kind ignores it.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Pin {
#[serde(with = "crate::cbor::opt_heads")]
pub heads: Option<Vec<ChangeHash>>,
}
/// The state label of the machine a document actor runs, which its kind's
/// layout selects (decision 82): `file` and `override` documents run the
/// text machine, a workspace index, the registry, a thread, the caps
/// document, and a channel their own.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum DocLabel {
Text(DocLifecycleStates),
Index(IndexLifecycleStates),
Registry(RegistryLifecycleStates),
Thread(ThreadLifecycleStates),
Issue(IssueLifecycleStates),
Caps(CapsLifecycleStates),
Channel(ChannelLifecycleStates),
}
impl sml::StateName for DocLabel {
fn name(&self) -> &'static str {
match self {
Self::Text(label) => label.name(),
Self::Index(label) => label.name(),
Self::Registry(label) => label.name(),
Self::Thread(label) => label.name(),
Self::Issue(label) => label.name(),
Self::Caps(label) => label.name(),
Self::Channel(label) => label.name(),
}
}
}
/// Does `heads`, a set of this document's heads, equal or descend from
/// `from` (decision 120)? The `channels` actor asks it to decide
/// supersession and approved heads, which only a document can tell. File,
/// override, workspace index, and registry documents answer with exactly
/// one [`DescendsDone`]: [`Descent::Yes`] when every change in `from` is in
/// the history of `heads`, `No` when one is not, and `Unknown` when either
/// set is empty or names a change the document doesn't hold. Every other
/// kind, and a closed actor, answers `Unknown`. `req` is the shell's.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DescendsQuery {
pub req: RequestId,
#[serde(with = "crate::cbor::heads")]
pub heads: Vec<ChangeHash>,
#[serde(with = "crate::cbor::heads")]
pub from: Vec<ChangeHash>,
}
/// The answer to a [`DescendsQuery`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct DescendsDone {
pub req: RequestId,
pub outcome: Descent,
}
/// Where a protected file's approved heads start (decisions 120 and 122):
/// a `file` document answers with exactly one [`BaselineDone`] holding the
/// heads after its genesis and its owner-signed import change (the record
/// `[owner, GrantId::OWNER, null, null]` at time 0 on top of genesis,
/// decision 125), or its genesis heads when it has no import. Every other
/// kind, and a closed actor, answers `heads: None`. `req` is the shell's.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct BaselineQuery {
pub req: RequestId,
}
/// The answer to a [`BaselineQuery`].
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BaselineDone {
pub req: RequestId,
#[serde(with = "crate::cbor::opt_heads")]
pub heads: Option<Vec<ChangeHash>>,
}