Skip to content

Getting started

This page runs start to finish against a real server. Everything after “Run the server” uses the tephra-client crate.

The server is a synchronous, thread-per-connection TCP server. Start it from a checkout of the workspace:

Terminal window
cargo run -p tephra-server

It binds 127.0.0.1:9000 and writes its log and index segments under tephra-data in the working directory. Both are configurable by flag, environment variable, or a TOML file: see Operations for the full surface. The defaults, including the 256 MiB segment size, are the server’s, not the library’s.

In the project that will talk to the server:

Terminal window
cargo add tephra-client

Client::connect takes anything that resolves to a socket address and opens one TCP connection. A Client is not shared between threads: give each thread its own.

let mut client = Client::connect(addr)?;

An event is a type, a set of tags, and an opaque payload. append takes an iterator of events and an optional condition; here the condition is None, so the write is unconditional. It returns the position range the batch landed at.

let event = Event::new(
"CourseOpened",
&["course:c1"],
br#"{"course":"c1","seats":30}"#.to_vec(),
)?;
let result = client.append([event], None)?;
println!("recorded positions {} to {}", result.first, result.last);

The client’s Event is a friendly owned type (built from &str, &[&str], and bytes). The engine has a second, packed Event used only when you embed the engine directly; the two do not interconvert, and the client type is the one you want here. See Embedded for the other one.

read and read_all take a query and an after position. Position::ZERO means “from the start of the log”. Results come back in ascending position order.

let query = Query::item(QueryItem::with_tags(Tags::new([Tag::new("course:c1")?])?));
let (events, _watermark) = client.read_all(query, Position::ZERO)?;
for seq in &events {
println!("{} {}", seq.position(), seq.event().event_type());
}

Because the writer publishes each commit before the append reply returns, a client sees its own writes immediately: the read above returns the event the append above just recorded.

A subscription catches up on history and then tails the live edge, as one loop. It yields events in order, then a CaughtUp marker each time it reaches the live edge. The marker is where a read model can report that it is current.

// Subscribe from the start: catch up on history, then a CaughtUp marker at the live edge.
let query = Query::item(QueryItem::with_tags(Tags::new([Tag::new("course:c1")?])?));
let (mut stream, cancel) = client.subscribe(query, Position::ZERO)?;
for item in &mut stream {
match item? {
SubEvent::Event(seq) => {
println!("live {} {}", seq.position(), seq.event().event_type());
}
// Reached the live edge. A long-running consumer keeps going; this example stops.
SubEvent::CaughtUp(_) => break,
}
}
cancel.cancel();

A subscription does not end on its own: drop the stream, or call cancel on the paired SubscribeCancel, to close it.

Core concepts is the precise definition of events, tags, positions, and query semantics. Guides builds a real decision model and a read model on top of what is here.