Skip to main content

Module record

Module record 

Source
Expand description

The records of the log of riff-server.

§Design

Each change that must not be lost is a Record in one log (01M3T410XDD9W4EC0Y68FAA7XN). A record has a position (1, 2, 3, and so on), the time of its write, its cause, and one Change. The change names say what happened, in the past tense. These Rust types are the schema of the log.

The fields next to the change are the Envelope: one type that Record and the tolerant reader Line::parse share. So a new field of the envelope gets to the writer, the reader and each start.

The cause is in the envelope: the caller (By) and the kind of the command (01M3X4Z60G1FXQTDC5XDJ05BAX). So the log alone shows who made each change. A record from before this rule has no cause: it reads, and its cause is not known.

The envelope also has the call ID of the command: the field call (01M48VFFY5CK9MRXJESV2NHY5F). It came after release 1.0.0. A record of a command with no call ID, and each record of 1.0.0, has none. A build of 1.0.0 skips the field. riff-server uses it to give a repeated call the reply of the first try, also after a start.

§One layout for each type

Each type of the log, the wire and the checkpoint has one struct (01M49W17GGV1K5FZEKJRPV0GNA). A tolerant reader holds the struct itself, as Line::parse holds the Envelope with #[serde(flatten)]: it never keeps a second list of the fields. Each conversion between two such types names each field of its source in a destructure with no .. (01M49W17M2JVNYSHDQJWHZ8A7X): a field that the target does not need is named with _. So a new field fails the build at each place that must carry it, and no review must find it:

ⓘ
use riff_core::record::Envelope;

// A conversion that does not name the field `call` does not build.
fn written(envelope: Envelope) -> (u64, u64) {
    let Envelope { position, written_at_ms, by: _, command: _ } = envelope;
    (position, written_at_ms)
}

Each type has a round-trip test: each variant with each optional field set, written and read back (01M49W18ETF4KZJN848M91VY35). The records go through the real path of the store in riff-server/tests/calls.rs.

A record names a session by its URI with no lead mark and no claims: the who and the place at the time of the change (01M3T411QW1SQV12RJVATEJ8YD). So a replay knows where each session was.

§The rules for a change of a record (01M3T4111PFM0C6KPREWFS9EQQ)

  • A new field has a default, and the default means “as before”. An old build skips a field that it does not know.
  • Do not change the type or the meaning of a field. Do not use the name of a removed field again.
  • A new kind of change gets a new name. A build that does not know a kind skips the record (Line::Unknown).
  • A kind is never renamed, and the name of a removed kind is never used again (01M3XM2C3MND6YB24SGZ565353). The file kinds.json in the directory of each release under crates/riff-server/tests/fixtures/ lists the new kinds of the release (01M43GSRSDJMGAH8SR1GD4Z3XF). The file of 1.0.0 also lists each field of the envelope. A test fails when a name of a list is gone from the code.
  • The enum Change and the list Change::KINDS come from one macro. So a variant cannot be missing from the list. The order of the list is not a part of the format.
  • A field with a set of named values that can grow has the value other: the class in By, the Scope of a pause, and the reason of a start (StartReason). A build reads a value that it does not know as other: a text, and each other form of JSON. apply stores nothing for such a value. Such a record counts as a skipped record (Record::other): the build writes no checkpoint past it (01M3XM2C18TT8VSKGD77YPZG53). The state of a pause_set has two values and no other.
  • A posted record has two such parts too (01M3XSF90E9JYYTC13D9THY4WE): the kind of the message (Kind), and each selector of its to (Selector). A selector that the build does not know is a selector other: an object with a field or a value that the build does not know, and each other form of JSON. It keeps its JSON as it came, and it matches no session. The message stays in its thread, and a reader shows it as a message.
  • A session URI keeps each query part that the build does not know, as it came (SessionUri::other, 01M3XYYSY536AEJVERBPTQFQYX). The session is the same session, and the part gives no mark: no lead and no claim. Such a record counts as a skipped record too.
  • command is text. A reader takes a kind of command that it does not know as text.
  • Only the read of JSON takes a value of a later build. A call with such a kind, such a selector or such a URI of its caller is refused.

§Each type with named values in a record

The list has each enum that a record holds, and each struct that refused a field that it does not know. A struct that is not in the list skips such a field. A new type of these two sorts in a record needs a line here.

TypeWhereA value that the build does not know
ChangechangeThe record is skipped (Line::Unknown).
Bybyother
Scopepause_setother
StartReasonsession_startedother
Kindthe message of postedother
Selectorthe to of the message of postedother: a new field, a new value of a field, and each other form of JSON
RiffStatepause_setThe line does not read. The set never grows: a pause is set or ended. A new sort of pause is a new Scope.

§Each text with a grammar in a record

The list has each text that the read of a record checks or takes apart, and each number. A text that is not in the list is free text: the read takes each value. A new text with a grammar in a record needs a line here.

TextWhereA value that the build does not know
The query of a session URI (SessionUri)session of a change, from of a messageThe URI keeps the part, and the record counts as a skipped record: a part with a new name, lead with a value other than true, a second session, and a claim with a character that is not allowed.
The other parts of a session URIthe sameThe line does not read. They never grow: riff://USER@HOST, then /OWNER/REPO or /-, then #WORKTREE, and each part holds only ASCII letters, digits, -, _, . and ~. A new fact of a session is a new query part.
The session ID of a session URIthe session part of the queryThe line does not read when the ID is empty or has a character that is not allowed. The grammar never grows: the ID is a name with only ASCII letters, digits, -, _, . and ~, and a new fact of a session is a new query part.
A thread name (ThreadName)threadThe line does not read. The grammar never grows: a text that is not empty and has no white space. Each new sort of thread name fits it, as dm: did. A name that starts with dm: reads with each character, also white space. The read does not take a name apart.
The caller in by{"session":"USER/ID"}other (By)
A session in woken (Who)postedThe read checks no character. A field that the build does not know is skipped.
The signature and the payload of a messagesig, payloadThe read keeps the text, and checks nothing. A reader checks them: a text that does not check gives a message that is not verified.
An item, an email, a user, the ID of a riff, a body, a command, the reason of a hold, the title of a waveitem, email, user, riff_id, body, command, reason, titleFree text.
An item of a plan, a need, a done needitems, needs, done of plan_setFree text: handle of plan checks the form issue-N, and the read does not.
A time, a position, a seq, a count, the number of a wavewritten_at_ms, at_ms, due_ms, position, seq, after_secs, per_host, numberThe line does not read. A number is a whole number that is not negative, and its type never changes (the rule of a field). per_host is at most 65535.
A markworker, must_clear, adminThe line does not read. A mark is true or false, and its type never changes.

The header of a chunk has the number of the format. A header of a later format stops the load (01M3T411F3K6FD28R3Q3ZE4VCN): this is the one sign that a build must not read the log.

§Example

use riff_core::name::Who;
use riff_core::record::{By, Change, Claimed, Envelope, Line, Record};

let record = Record {
    envelope: Envelope {
        position: 1234,
        written_at_ms: 1_790_000_000_000,
        by: Some(By::Session(Who::new("ann", Some("s1"))?)),
        command: Some("claim".into()),
        call: None,
    },
    change: Change::Claimed(Claimed {
        session: "riff://ann@heron/acme/app?session=s1".parse()?,
        thread: "acme/app".parse()?,
        item: "issue-7".into(),
    }),
};
let line = serde_json::to_string(&record).unwrap();
assert_eq!(
    line,
    r#"{"position":1234,"written_at_ms":1790000000000,"by":{"session":"ann/s1"},"command":"claim","change":{"claimed":{"session":"riff://ann@heron/acme/app?session=s1","thread":"acme/app","item":"issue-7"}}}"#
);
assert_eq!(Line::parse(&line)?, Line::Record(Box::new(record)));

// A record with no cause reads. Its cause is not known.
let old = r#"{"position":7,"written_at_ms":1,"change":{"member_invited":{"email":"ann@acme.io"}}}"#;
let Line::Record(old) = Line::parse(old)? else { panic!("a known kind") };
assert_eq!((old.envelope.by, old.envelope.command), (None, None));

Structs§

AdminSet
Claimed
A claim of one work item in one thread.
Email
A person, by the verified email in lower case. A record of the people names a person by the email: the state finds the USER.
Envelope
The fields of a record next to its change. See the design of the module.
Forgotten
A session that the state forgets.
ItemFreed
The end of the hold of one item in one repository thread.
ItemHeld
The hold of one item in one repository thread, with its reason (01M43GSGB9ZFHSG0Q83Y50FEGW). The envelope of the record names who held it, and when. A hold is not a claim.
Member
A session and a thread.
NotesSet
The notes of the lead of one user in one repository thread (01M59258K3TYR09HC4AWHWZPEX). The text is the whole text: it replaces the old text. The envelope names who wrote it, and when.
OwnerAsked
A request for the owner role.
OwnerSet
The owner of the riff.
PauseSet
A pause that a command set or ended. The envelope of the record names who did it, and when.
PersonJoined
The first sign-in of a person: the verified email holds the USER (R209).
Plan
A plan with no thread: the current wave, its items with their needs, and the done needs. A need that is not in done is open.
PlanEnded
The end of the plan of one repository thread.
PlanItem
One item of a plan, with the items that it needs.
PlanSet
The plan of one repository thread (01M4A4YTNSJR0R1T9JNXPBSKHC): the full plan, so the record replaces the plan of the thread. The design is in the book: “Design: the plan on the server”.
Posted
A message in a thread.
Record
One line of the log: its envelope and its change.
Released
A claim that is free now.
RiffMade
The ID of a riff (01M3JNVBPMZ1K9WX7Q7DP6Y0DH).
SessionStarted
A start of a session.
SettingChanged
SigninsEnded
Wave
A wave of a plan: its number, and its title on the forge.

Enums§

By
Who caused a record: the caller of its command, with its class (01M3X4Z60G1FXQTDC5XDJ05BAX). It holds only the user and the session ID. The session in a change is a full URI, with the place.
Change
What happened.
Line
One line of the log, as this build reads it.
Scope
What a pause stops: the whole riff, or one repository (01M3XAHZG26ECNARX35JD73YXJ).

Constants§

MARKS
The marks of a post for an audit, longest first: the start of a body that names the step of the flow (01M3ZWRC3XBFN8FJDGE8XWZ5EA).