eventfold 0.2.0

Lightweight, append-only event log with derived views — your application state is a fold over an event log
Documentation
# eventfold

Your application state is a fold over an event log.

**eventfold** is a lightweight, append-only event log with derived views for Rust. Your application state is always a function of the log — computed by folding events through pure reducer functions. Snapshots cache the result for incremental performance. Zero infrastructure: just files in a directory.

```
state = events.reduce(reducer, initial_state)
```

## Quick Example

```rust
use eventfold::{EventLog, Event};
use serde::{Serialize, Deserialize};
use serde_json::json;

#[derive(Default, Clone, Serialize, Deserialize, Debug)]
struct TodoState {
    items: Vec<TodoItem>,
    next_id: u64,
}

#[derive(Clone, Serialize, Deserialize, Debug)]
struct TodoItem {
    id: u64,
    text: String,
    done: bool,
}

fn todo_reducer(mut state: TodoState, event: &Event) -> TodoState {
    match event.event_type.as_str() {
        "todo_added" => {
            state.items.push(TodoItem {
                id: state.next_id,
                text: event.data["text"].as_str().unwrap().to_string(),
                done: false,
            });
            state.next_id += 1;
        }
        "todo_completed" => {
            let id = event.data["id"].as_u64().unwrap();
            if let Some(item) = state.items.iter_mut().find(|i| i.id == id) {
                item.done = true;
            }
        }
        "todo_deleted" => {
            let id = event.data["id"].as_u64().unwrap();
            state.items.retain(|i| i.id != id);
        }
        _ => {}
    }
    state
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut app = EventLog::builder("./data")
        .max_log_size(10_000_000)
        .view::<TodoState>("todos", todo_reducer)
        .open()?;

    app.append(&Event::new("todo_added", json!({"text": "buy milk"})))?;
    app.append(&Event::new("todo_added", json!({"text": "write docs"})))?;
    app.refresh_all()?;

    let todos: &TodoState = app.view("todos")?;
    println!("{:?}", todos);
    Ok(())
}
```

This is the entire data layer. No schema, no migrations, no ORM. The log file is the database. The reducer is the schema.

## Core Concepts

**Events** are append-only JSON lines in a log file. Each event has a type, arbitrary JSON data, and a timestamp — plus optional fields for event ID, actor, and metadata. The log never rewrites or deletes events.

**Reducers** are pure functions `fn(State, &Event) -> State` that give events meaning. They fold events into application state. Different reducers over the same log produce different views — same data, different lenses.

**Views** are derived state materialized by folding events through a reducer. Snapshots cache the result so subsequent refreshes only process new events. A view is always rebuildable from the full log.

## Installation

```
cargo add eventfold
```

## Features

- Append-only event log (JSONL)
- Derived views via pure reducer functions
- Incremental snapshots — only process new events on refresh
- Automatic log rotation with zstd compression
- Integrity checking via xxhash — auto-rebuild on corruption
- Crash-safe — atomic snapshot writes, graceful recovery from partial writes
- Structured events with optional ID, actor, and metadata fields
- Conditional append — optimistic concurrency via offset + hash checks
- File locking — exclusive writer prevents multi-process corruption
- Poll-based tailing — lightweight `has_new_events` check for change detection
- Blocking tail — OS-level file watching via `wait_for_events` for sub-millisecond notification
- Reader/writer separation — clone-friendly readers, exclusive writer
- Zero infrastructure — just files in a directory
- Single-crate, minimal dependencies, no async

## Data Layout

```
data/
  archive.jsonl.zst          # compressed event history (zstd frames)
  app.jsonl                  # active log, plain text, append-only
  views/
    todos.snapshot.json      # {"state": {...}, "offset": 12840, "hash": "a3f2..."}
    stats.snapshot.json
```

Only two data files: the compressed archive and the active log. Views are cached snapshots that are always rebuildable.

## When to Use

- Personal tools, CLIs, small web apps
- Prototypes and MVPs where you want persistence without a database
- Applications where the event history is valuable (audit logs, undo, time travel)
- Embedded state in single-process applications
- Any case where "just files in a directory" is the right level of infrastructure

## When Not to Use

- High-concurrency writers (file locking ensures single-writer safety, but throughput is limited)
- Distributed systems
- High write throughput (every append flushes to disk)
- Applications needing ad-hoc queries or indexes beyond what reducers build
- Anything requiring encryption or access control at the storage layer

## Examples

```
cargo run --example todo_cli        # minimal CLI todo app
cargo run --example multi_view      # same log, multiple views
cargo run --example rebuild         # changing a reducer and rebuilding
cargo run --example rotation        # manual and auto rotation
cargo run --example time_travel     # replaying to a specific point
cargo run --example notes_cli       # tagged notes with search
cargo run --example poll_tail       # poll-based tailing with has_new_events
cargo run --example blocking_tail   # blocking tail with wait_for_events
```

A full-stack Leptos web app example lives in `examples-leptos/todo-app/`.

## Documentation

- [Concepts & Guide]docs/guide.md — how it works, writing reducers, schema evolution, crash safety, debugging
- [API Reference]https://docs.rs/eventfold — rustdoc for all public types and methods

## License

MIT OR Apache-2.0