tephra/lib.rs
1//! A DCB-compliant, immutable event store with global ordering.
2//!
3//! Tephra is a Dynamic Consistency Boundary (DCB) event store. Instead of a static consistency
4//! boundary baked into an aggregate, the boundary is derived per decision from a [`Query`].
5//! Events carry an [`EventType`] plus a set of [`Tags`], so one event can belong to several
6//! entities at once, and a decision reads exactly the events it depends on and guards exactly
7//! those on append (an [`AppendCondition`]).
8//!
9//! This crate is the embedded engine: the durable log, the single writer, the index, and the
10//! read paths. Use it directly in-process, or reach it over the network with the
11//! [`tephra-server`](https://crates.io/crates/tephra-server) TCP server and the
12//! [`tephra-client`](https://crates.io/crates/tephra-client) client.
13//!
14//! # Design
15//!
16//! The log is the source of truth and everything else is derived. Data is written once, never
17//! updated and never deleted, keyed by a dense monotonic [`Position`] assigned by the single
18//! writer. Indexes need no write-ahead log and no fsync on the write path, because they can be
19//! rebuilt by replaying the log. The `ARCHITECTURE.md` document in the repository records the
20//! full rationale and the alternatives that were rejected.
21//!
22//! # Example
23//!
24//! ```no_run
25//! use tephra::{
26//! AppendCondition, Event, EventType, Position, Query, QueryItem, SegmentConfig,
27//! SegmentSet, Tag, Tags, WriteCoordinator, WriterConfig,
28//! };
29//!
30//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
31//! // Open (or create) a log directory and start the single-writer coordinator.
32//! let set = SegmentSet::open("tephra-data", SegmentConfig::new(256 * 1024 * 1024))?;
33//! let (coordinator, handle) = WriteCoordinator::start(set, WriterConfig::default())?;
34//!
35//! // Build a packed event, then append it guarded so it fails if course:c1 already exists.
36//! let ty = EventType::new("CourseOpened")?;
37//! let tags = Tags::new([Tag::new("course:c1")?])?;
38//! let event = Event::new(&ty, &tags, br#"{"course":"c1","seats":30}"#)?;
39//! let guard = AppendCondition::new(Query::item(QueryItem::with_tags(
40//! Tags::new([Tag::new("course:c1")?])?,
41//! )));
42//! handle.append(vec![event], Some(guard))?;
43//!
44//! // Reads run on the caller's thread over a snapshot published at each commit. `read` returns
45//! // a lending iterator, so it is consumed with `while let`, not a `for` loop.
46//! let query = Query::item(QueryItem::with_tags(Tags::new([Tag::new("course:c1")?])?));
47//! let mut reads = handle.read(&query, Position::ZERO, None);
48//! while let Some(item) = reads.next() {
49//! let seq = item?;
50//! println!("{} {}", seq.position, seq.event.event_type());
51//! }
52//!
53//! // Shutdown joins the writer thread and flushes cleanly.
54//! coordinator.shutdown();
55//! # Ok(())
56//! # }
57//! ```
58
59pub mod event;
60pub mod index;
61pub mod log;
62pub mod query;
63pub mod read;
64pub mod writer;
65
66pub use event::{Event, EventRef};
67pub use log::set::{PositionRange, SegmentConfig, SegmentSet};
68pub use query::Matches;
69#[cfg(feature = "async")]
70pub use read::pool::{ReadPool, ReadPoolConfig, ReadStream};
71pub use read::{
72 DEFAULT_MAX_BATCH_EVENTS, ReadConfig, ReadError, ReadHandle, Subscription, WaitOutcome,
73};
74pub use tephra_types::{
75 AppendCondition, EventType, MAX_NAME_LEN, NameError, Position, Query, QueryItem, Tag, Tags,
76 TagsError,
77};
78pub use writer::{AppendError, ConflictSite, WriteCoordinator, WriteHandle, WriterConfig};
79
80/// The crate README and the workspace README, compiled as doctests so their code samples
81/// cannot drift from the API. These items exist only during doctest builds (`cfg(doctest)`),
82/// so they never appear in the published documentation.
83#[cfg(doctest)]
84#[doc = include_str!("../README.md")]
85struct CrateReadme;
86
87#[cfg(doctest)]
88#[doc = include_str!("../../../README.md")]
89struct WorkspaceReadme;