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