photon/lib.rs
1//! Pub/sub event pipeline facade.
2//!
3//! Typed topics, durable subscriptions with checkpoints, and the same API in single-process and
4//! multi-node deployments. Enable the `runtime` feature for the full stack (`Photon`, backends,
5//! executor). Photon uses pluggable **storage adapters**
6//! (`mem`, `sqlite`, `nats`, `fluvio`, `kafka`) behind [`StoragePort`] and Quark inventory for
7//! topic/handler discovery. Payloads are encrypted before append; storage stays opaque. Canonical
8//! business data remains in your datastore — the transport log is for event delivery, not system
9//! of record.
10//!
11//! ## Features
12//!
13//! - **Typed topics and handlers** — [`topic`] / [`subscribe`] register via Quark inventory
14//! - **Same API everywhere** — `publish_on` / `start_executor` whether you run one process or many
15//! - **Pluggable storage** — `mem`, `sqlite`, or broker adapters (`nats`, `kafka`, `fluvio`)
16//! - **Durable subscriptions** — checkpointed replay after restart on durable adapters
17//! - **Host-owned crypto** — `PHOTON_TRANSPORT_KEY` encrypts envelopes before they hit storage
18//!
19//! *Typed pub/sub over a persistent event log — pick embedded or brokered topology.*
20//!
21//! # Getting started
22//!
23//! You always publish and subscribe the same way (`OrderCreated { … }.publish_on(&photon)`,
24//! `#[subscribe]`, [`Photon::start_executor`]). What changes is **whether events stay in one
25//! process or cross a broker to another binary**.
26//!
27//! ## Choose a topology
28//!
29//! - **[Mode 1 — Embedded](#mode-1--embedded-one-binary)** — one process owns publish and
30//! handlers. Start here (`mem` or `sqlite`).
31//! - **[Mode 2 — Brokered](#mode-2--brokered-publisher--worker-binaries)** — publisher binary(ies)
32//! and worker binary(ies) share a broker. Photon does **not** ship a separate server binary;
33//! each process embeds Photon against the same adapter.
34//!
35//! | Topology | Adapter | Features | When to use |
36//! |----------|---------|----------|-------------|
37//! | Embedded | [`InProcStoragePort`] (default) | `runtime`,`mem` | Local / tests |
38//! | Embedded durable | `SqliteStoragePort` | `runtime`,`sqlite` | Single host, restart-safe |
39//! | Brokered | NATS / Kafka / Fluvio | `runtime` + `nats`/`kafka`/`fluvio` | Multi-process / fleet |
40//!
41//! Adapter builders and env vars: [`config`].
42//!
43//! After you pick a mode, continue with [declare topics](#3-declare-topics-and-handlers).
44//!
45//! ## Mode 1 — Embedded (one binary)
46//!
47//! This process publishes **and** runs handlers. There is no second binary and no external broker.
48//!
49//! ```text
50//! Your app ──publish / #[subscribe]──► Photon ──StoragePort──► mem | sqlite
51//! ```
52//!
53//! **Prerequisites:** Cargo features `runtime` + `mem` (or `sqlite`), and `PHOTON_TRANSPORT_KEY`
54//! (base64 of 32 bytes). See [`config`].
55//!
56//! **In-memory** (default — [`PhotonBuilder`] installs [`InProcStoragePort`] when you omit
57//! [`storage_port`](PhotonBuilder::storage_port)):
58//!
59//! ```rust,no_run
60//! use std::sync::Arc;
61//!
62//! use photon::{JsonIdentityFactory, Photon};
63//!
64//! # fn main() -> photon::Result<()> {
65//! let photon = Photon::builder().auto_registry().build()?;
66//! photon.start_executor(Arc::new(JsonIdentityFactory))?;
67//! // Prefer EventType { … }.publish_on(&photon).await
68//! # let _ = photon;
69//! # Ok(())
70//! # }
71//! ```
72//!
73//! **`SQLite`** — durable single-process (write-through + in-memory live fanout):
74//!
75//! ```rust,ignore
76//! use std::sync::Arc;
77//!
78//! use photon::{JsonIdentityFactory, Photon, SqliteStoragePort};
79//!
80//! # async fn boot() -> photon::Result<()> {
81//! let port = Arc::new(SqliteStoragePort::open("/var/lib/photon/events.db").await?);
82//! let photon = Photon::builder()
83//! .storage_port(port)
84//! .auto_registry()
85//! .build()?;
86//! photon.start_executor(Arc::new(JsonIdentityFactory))?;
87//! # let _ = photon;
88//! # Ok(())
89//! # }
90//! ```
91//!
92//! Runnable: `cargo run -p uf-photon --example embedded_mem --features runtime,mem`.
93//! Then jump to [declare topics](#3-declare-topics-and-handlers).
94//!
95//! ## Mode 2 — Brokered (publisher + worker binaries)
96//!
97//! Use this when multiple processes (or hosts) must share the same topic log. `mem` and `sqlite`
98//! cannot fan out across processes — wire a broker adapter instead.
99//!
100//! ```text
101//! Publisher binary ──publish_on──► Photon ──StoragePort──► broker (NATS / Kafka / Fluvio)
102//! Worker binary ──start_executor / #[subscribe]──► Photon ──same broker──►
103//! ```
104//!
105//! ### What you create
106//!
107//! | Piece | Purpose |
108//! |-------|---------|
109//! | Shared topics | Same `#[topic]` types (shared crate or both binaries compile them) |
110//! | Publisher binary | `[[bin]]` that publishes; typically **no** `start_executor` |
111//! | Worker binary | `[[bin]]` with `#[subscribe]` handlers; **must** call `start_executor` |
112//! | Broker | NATS / Kafka / Fluvio cluster your ops team runs |
113//! | Shared env | Same `PHOTON_TRANSPORT_KEY` + broker URL (e.g. `PHOTON_NATS_URL`) on every process |
114//!
115//! ### Shared setup (both binaries)
116//!
117//! 1. Enable `runtime` plus one broker feature (`nats` is the usual first choice).
118//! 2. Build the same storage port against the **same** cluster — pick a builder below.
119//! 3. Call [`.auto_registry()`](PhotonBuilder::auto_registry) when using macros.
120//! 4. Keep the [`Photon`] handle for `publish_on` / `subscribe_on`.
121//!
122//! | Adapter | Feature | Builder (publisher + worker examples) |
123//! |---------|---------|----------------------------------------|
124//! | NATS | `nats` | [`NatsStoragePortBuilder`](../photon_backend_nats/struct.NatsStoragePortBuilder.html) |
125//! | Kafka | `kafka` | [`KafkaStoragePortBuilder`](../photon_backend_kafka/struct.KafkaStoragePortBuilder.html) |
126//! | Fluvio | `fluvio` | [`FluvioStoragePortBuilder`](../photon_backend_fluvio/struct.FluvioStoragePortBuilder.html) |
127//!
128//! Env index: [`config`]. NATS sketches follow; swap the builder for Kafka/Fluvio.
129//!
130//! ### Publisher binary
131//!
132//! Wire the broker port, declare topics, publish. Skip [`Photon::start_executor`] unless this
133//! process also handles events.
134//!
135//! ```rust,ignore
136//! use std::sync::Arc;
137//!
138//! use photon::{Photon, NatsStoragePort, ReplayCursor};
139//!
140//! # async fn boot_publisher() -> photon::Result<()> {
141//! let port = Arc::new(
142//! NatsStoragePort::builder()
143//! .from_env_defaults()
144//! .replay_cursor(ReplayCursor::StreamSeq)
145//! .sync_ack(true)
146//! .build()
147//! .await?,
148//! );
149//! let photon = Photon::builder()
150//! .storage_port(port)
151//! .auto_registry()
152//! .build()?;
153//! // OrderCreated { … }.publish_on(&photon).await?;
154//! # let _ = photon;
155//! # Ok(())
156//! # }
157//! ```
158//!
159//! ### Worker binary
160//!
161//! Same storage port wiring as the publisher, plus `#[subscribe]` handlers and
162//! [`Photon::start_executor`]:
163//!
164//! ```rust,ignore
165//! use std::sync::Arc;
166//!
167//! use photon::{JsonIdentityFactory, Photon, NatsStoragePort, ReplayCursor};
168//!
169//! # async fn boot_worker() -> photon::Result<()> {
170//! let port = Arc::new(
171//! NatsStoragePort::builder()
172//! .from_env_defaults()
173//! .replay_cursor(ReplayCursor::StreamSeq)
174//! .sync_ack(true)
175//! .build()
176//! .await?,
177//! );
178//! let photon = Photon::builder()
179//! .storage_port(port)
180//! .auto_registry()
181//! .build()?;
182//! photon.start_executor(Arc::new(JsonIdentityFactory))?;
183//! # let _ = photon;
184//! # Ok(())
185//! # }
186//! ```
187//!
188//! ### Run both
189//!
190//! 1. Start the broker.
191//! 2. Start **worker(s)** first so subscriptions are ready.
192//! 3. Start one or more **publishers**.
193//!
194//! Then continue with [declare topics](#3-declare-topics-and-handlers).
195//!
196//! ## 3. Declare topics and handlers
197//!
198//! - [`topic`] — typed event struct + inventory registration; generates `publish_on` / `subscribe_on`
199//! - [`subscribe`] — inventory-registered handler; requires [`Photon::start_executor`] on workers
200//! - [`prelude`] — common imports (`Event`, `SubscribeOpts`, `Photon`, macros)
201//!
202//! Attribute tables: [`config`]. Runnable: `keyed_topic`, `consumer_group`, `subscribe_v2`.
203//!
204//! ## 4. Publish and subscribe
205//!
206//! After [`topic`], the macro generates typed methods on your event struct. Prefer those over
207//! raw [`Photon::publish`] / [`Photon::subscribe`].
208//!
209//! **Publish** — `publish_on` with an explicit [`Photon`] handle:
210//!
211//! ```rust,ignore
212//! OrderCreated {
213//! order_id: "ord-1".into(),
214//! amount_cents: 9900,
215//! }
216//! .publish_on(&photon)
217//! .await?;
218//! ```
219//!
220//! **Typed stream** — `subscribe_on` (no `#[subscribe]` / executor required):
221//!
222//! ```rust,ignore
223//! use futures::StreamExt;
224//! use photon::SubscribeOpts;
225//!
226//! let opts = SubscribeOpts::default_ephemeral();
227//! let mut stream = OrderCreated::subscribe_on(&photon, opts).await?;
228//! if let Some(Ok(envelope)) = stream.next().await {
229//! let _payload = envelope.payload; // OrderCreated
230//! }
231//! ```
232//!
233//! **Inventory handlers** — `#[subscribe]` + [`Photon::start_executor`] (Mode 1 / Mode 2 workers).
234//!
235//! Optional sugar after [`configure`]: `.publish()` / `.subscribe()` without a handle.
236//! Raw topic-name API (advanced): [`Photon::subscribe`].
237//!
238//! Runnable: `embedded_mem` (`publish_on` + `#[subscribe]`), `keyed_topic` (`subscribe_on`),
239//! `manual_subscribe` (raw [`Photon::subscribe`]).
240//!
241//! ## 5. Run the executor and reclaim
242//!
243//! - [`Photon::start_executor`] — dispatch inventory-registered `#[subscribe]` handlers (Mode 1
244//! and Mode 2 workers)
245//! - [`Photon::reclaim_transport`] — retention sweep past the safe watermark
246//! - Optional ops telemetry: [`OpsLog`] via [`PhotonBuilder::ops_log`] (example: `telemetry_ops_log`)
247//!
248//! ## Notes
249//!
250//! - **Transport key:** boot fails closed without a valid `PHOTON_TRANSPORT_KEY` (see [`config`]).
251//! - **Durable multi-process:** use a broker adapter; `mem` does not cross process boundaries.
252//! - **Lab topologies:** testkit / bench `PHOTON_TOPOLOGY` values are harness labels — not the
253//! Mode 1 / Mode 2 product choices above.
254//! - **Custom adapters:** implement [`StoragePort`] and pass it to
255//! [`PhotonBuilder::storage_port`]. Advanced delivery traits live behind that port.
256//!
257//! ## Architecture
258//!
259//! ```text
260//! Application → Photon (macros + Photon runtime) → storage port → delivery backend
261//! ```
262//!
263//! ```text
264//! Typed API (#[topic], publish_on / publish, #[subscribe])
265//! │
266//! v
267//! Photon runtime
268//! │
269//! v
270//! StoragePort ──► mem (InProcStoragePort)
271//! ──► sqlite (SqliteStoragePort)
272//! ──► nats / fluvio / kafka (broker crates)
273//! ──► custom implementation
274//! ```
275//!
276//! Full option reference: [`config`]. Macro expansion: repository `docs/macro-expansion.md`.
277
278/// Register a typed topic struct (`#[photon::topic]`).
279pub use photon_macros::topic;
280/// Register an async handler (`#[photon::subscribe]`).
281pub use photon_macros::subscribe;
282/// Re-export identity port traits and JSON stubs from [`photon_core`].
283pub use photon_core::{
284 actor_downcast_methods, Actor, IdentityError, IdentityFactory, JsonActor, JsonIdentityFactory,
285};
286/// Quark inventory for compile-time topic/handler registration.
287pub use quark::inventory;
288
289#[cfg(feature = "runtime")]
290mod runtime;
291
292#[cfg(feature = "runtime")]
293pub use runtime::*;
294
295#[cfg(feature = "runtime")]
296pub mod config;