Expand description
A DCB-compliant, immutable event store with global ordering.
Tephra is a Dynamic Consistency Boundary (DCB) event store. Instead of a static consistency
boundary baked into an aggregate, the boundary is derived per decision from a Query.
Events carry an EventType plus a set of Tags, so one event can belong to several
entities at once, and a decision reads exactly the events it depends on and guards exactly
those on append (an AppendCondition).
This crate is the embedded engine: the durable log, the single writer, the index, and the
read paths. Use it directly in-process, or reach it over the network with the
tephra-server TCP server and the
tephra-client client.
§Design
The log is the source of truth and everything else is derived. Data is written once, never
updated and never deleted, keyed by a dense monotonic Position assigned by the single
writer. Indexes need no write-ahead log and no fsync on the write path, because they can be
rebuilt by replaying the log. The ARCHITECTURE.md document in the repository records the
full rationale and the alternatives that were rejected.
§Example
use tephra::{
AppendCondition, Event, EventType, Position, Query, QueryItem, SegmentConfig,
SegmentSet, Tag, Tags, WriteCoordinator, WriterConfig,
};
// Open (or create) a log directory and start the single-writer coordinator.
let set = SegmentSet::open("tephra-data", SegmentConfig::new(256 * 1024 * 1024))?;
let (coordinator, handle) = WriteCoordinator::start(set, WriterConfig::default())?;
// Build a packed event, then append it guarded so it fails if course:c1 already exists.
let ty = EventType::new("CourseOpened")?;
let tags = Tags::new([Tag::new("course:c1")?])?;
let event = Event::new(&ty, &tags, br#"{"course":"c1","seats":30}"#)?;
let guard = AppendCondition::new(Query::item(QueryItem::with_tags(
Tags::new([Tag::new("course:c1")?])?,
)));
handle.append(vec![event], Some(guard))?;
// Reads run on the caller's thread over a snapshot published at each commit. `read` returns
// a lending iterator, so it is consumed with `while let`, not a `for` loop.
let query = Query::item(QueryItem::with_tags(Tags::new([Tag::new("course:c1")?])?));
let mut reads = handle.read(&query, Position::ZERO, None);
while let Some(item) = reads.next() {
let seq = item?;
println!("{} {}", seq.position, seq.event.event_type());
}
// Shutdown joins the writer thread and flushes cleanly.
coordinator.shutdown();Re-exports§
pub use event::Event;pub use event::EventRef;pub use log::set::PositionRange;pub use log::set::SegmentConfig;pub use log::set::SegmentSet;pub use query::Matches;pub use read::DEFAULT_MAX_BATCH_EVENTS;pub use read::ReadConfig;pub use read::ReadError;pub use read::ReadHandle;pub use read::Subscription;pub use read::WaitOutcome;pub use writer::AppendError;pub use writer::ConflictSite;pub use writer::WriteCoordinator;pub use writer::WriteHandle;pub use writer::WriterConfig;
Modules§
- event
- Event codec.
- index
- Layer 3: the derived index.
- log
- query
- Query match predicate over an encoded event.
- read
- Layer 5: off-thread read paths.
- writer
- Layer 2: the write coordinator.
Structs§
- Append
Condition - The guard on an
appendcall. - Event
Type - An event type. An arbitrary opaque, non-empty string (the spec never parses it),
stored as an exact-sized
Box<str>. - Position
- Query
Item - One alternative in a
Query: a type constraint AND a tag constraint. - Tag
- A tag, e.g.
course:c1. An arbitrary opaque, non-empty string (the spec does not split it into key/value), stored as an exact-sizedBox<str>. LikeEventType, it has noDeref; useas_str,AsRef<str>, orDisplay. - Tags
- A sorted, duplicate-free set of tags.
Enums§
- Name
Error - Error constructing an
EventTypeorTag. - Query
- A query: a set of
QueryItems OR’d together, or the catch-allQuery::All. - Tags
Error - Error constructing
Tags.
Constants§
- MAX_
NAME_ LEN - Maximum length, in bytes, of an
EventTypeorTag. Each is stored with a fixed-widthu16length in the engine’s encoded header, so the field capacity is the limit.