Skip to main content

ferro_projection/
lib.rs

1//! # ferro-projection
2//!
3//! Live read-model runtime: subscribe to domain events, persist per-key
4//! snapshots, broadcast deltas.
5//!
6//! **Not to be confused with [`ferro-projections`] (plural).** That crate
7//! is the Service Projection abstraction (`ServiceDef → IntentGraph →
8//! JsonUiRenderer`). This crate (`ferro-projection`, singular) is the
9//! live read-model runtime that subscribes to domain events, maintains a
10//! materialized state, and broadcasts deltas. The two abstractions are
11//! orthogonal — most apps will use both for different reasons.
12//!
13//! ferro-projection is the *live-read-model* primitive. [`ferro-events`]
14//! says *something happened*. [`ferro-broadcast`] says *something is
15//! visible to clients*. ferro-projection composes the two: events fold
16//! into per-key state, deltas land on `projection.{name}.{key}` channels.
17//!
18//! ## Per-key serialization
19//!
20//! ```text
21//!  Event::dispatch() ─┐
22//!                     │
23//!       ProjectionListener<P> ──┐
24//!                               │
25//!                               ▼
26//!  ┌── per-key Mutex (DashMap<String, Arc<Mutex<()>>>) ──┐
27//!  │   1. load snapshot from projection_snapshots        │
28//!  │   2. apply(&mut state, &event) → Delta              │
29//!  │   3. upsert snapshot (state, version+1)             │
30//!  │   4. broadcast on projection.{name}.{key}            │
31//!  └─────────────────────────────────────────────────────┘
32//!                                │
33//!                                ▼
34//!                  WebSocket clients receive the delta
35//! ```
36//!
37//! ## Schema and migration
38//!
39//! ferro-projection ships a SeaORM migration as
40//! [`CreateProjectionSnapshotsTable`]. Register it in your consumer-side
41//! `Migrator`:
42//!
43//! ```rust,ignore
44//! impl MigratorTrait for Migrator {
45//!     fn migrations() -> Vec<Box<dyn MigrationTrait>> {
46//!         vec![
47//!             Box::new(ferro_projection::CreateProjectionSnapshotsTable),
48//!             // ... your app migrations
49//!         ]
50//!     }
51//! }
52//! ```
53//!
54//! ## Operational footguns
55//!
56//! 1. **Broadcast failure does NOT roll back state.** If
57//!    `Broadcast::send` returns `Err`, the snapshot row is already
58//!    persisted; the runtime logs at `tracing::warn!` and surfaces
59//!    `ProjectionError::Broadcast`. Subscribers reconcile by re-reading
60//!    the snapshot.
61//! 2. **Single-instance assumption.** v0 assumes a single application
62//!    instance owns each projection's listener. Multi-instance
63//!    deployments must elect a single projection-runner node or accept
64//!    last-writer-wins behavior on concurrent applies to the same key
65//!    from different nodes.
66//! 3. **`register` is not idempotent on `Arc` identity.** Calling
67//!    `Arc<ProjectionRuntime<P>>::register()` twice registers two
68//!    listeners — both fire on each dispatch (same semantic as
69//!    Laravel's `Event::listen`). Register once at app startup.
70//!
71//! [`ferro-projections`]: https://docs.rs/ferro-projections
72//! [`ferro-events`]: https://docs.rs/ferro-events
73//! [`ferro-broadcast`]: https://docs.rs/ferro-broadcast
74
75mod entity;
76mod error;
77mod key;
78mod listener;
79mod migration;
80mod projection;
81mod runtime;
82
83pub use error::ProjectionError;
84pub use key::ProjectionKey;
85pub use migration::Migration as CreateProjectionSnapshotsTable;
86pub use projection::Projection;
87pub use runtime::ProjectionRuntime;
88
89// SeaORM entity re-exports for consumers needing native SeaORM query access.
90pub use entity::{
91    ActiveModel as ProjectionSnapshotActiveModel, Entity as ProjectionSnapshotEntity,
92    Model as ProjectionSnapshotModel,
93};