photon/lib.rs
1//! Pub/sub event pipeline (public crate).
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` seals envelopes, keeping ciphertext at rest
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//! - **[Embedded](#embedded-one-binary)** — one process owns publish and handlers. Start here
30//! (`mem` or `sqlite`).
31//! - **[Brokered](#brokered-publisher--worker-binaries)** — publisher binary(ies) and worker
32//! binary(ies) share a broker. Photon does **not** ship a separate server binary; each process
33//! 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 topology, continue with [declare topics](#3-declare-topics-and-handlers).
44//!
45//! ## 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//! Durable: `cargo run -p uf-photon --example embedded_sqlite --features runtime,sqlite`.
94//! Then jump to [declare topics](#3-declare-topics-and-handlers).
95//!
96//! ## Brokered (publisher + worker binaries)
97//!
98//! Use this when multiple processes (or hosts) must share the same topic log. `mem` and `sqlite`
99//! cannot fan out across processes — wire a broker adapter instead.
100//!
101//! ```text
102//! Publisher binary ──publish_on──► Photon ──StoragePort──► broker (NATS / Kafka / Fluvio)
103//! Worker binary ──start_executor / #[subscribe]──► Photon ──same broker──►
104//! ```
105//!
106//! ### What you create
107//!
108//! | Piece | Purpose |
109//! |-------|---------|
110//! | Shared topics | Same `#[topic]` types (shared crate or both binaries compile them) |
111//! | Publisher binary | `[[bin]]` that publishes; typically **no** `start_executor` |
112//! | Worker binary | `[[bin]]` with `#[subscribe]` handlers; **must** call `start_executor` |
113//! | Broker | NATS / Kafka / Fluvio cluster your ops team runs |
114//! | Shared env | Same `PHOTON_TRANSPORT_KEY` + broker URL (e.g. `PHOTON_NATS_URL`) on every process |
115//! | Broker TLS | Default [`BrokerTransportSecurity::RequireTls`]; plaintext needs explicit opt-in |
116//!
117//! ### Shared setup (both binaries)
118//!
119//! 1. Enable `runtime` plus one broker feature (`nats` is the usual first choice).
120//! 2. Build the same storage port against the **same** cluster — pick a builder below.
121//! 3. Call [`.auto_registry()`](PhotonBuilder::auto_registry) when using macros.
122//! 4. Keep the [`Photon`] handle for `publish_on` / `subscribe_on`.
123//!
124//! | Adapter | Feature | Builder (publisher + worker examples) |
125//! |---------|---------|----------------------------------------|
126//! | NATS | `nats` | [`NatsStoragePortBuilder`](../photon_backend_nats/struct.NatsStoragePortBuilder.html) |
127//! | Kafka | `kafka` | [`KafkaStoragePortBuilder`](../photon_backend_kafka/struct.KafkaStoragePortBuilder.html) |
128//! | Fluvio | `fluvio` | [`FluvioStoragePortBuilder`](../photon_backend_fluvio/struct.FluvioStoragePortBuilder.html) |
129//!
130//! | Concern | Production setting |
131//! |---------|-------------------|
132//! | Transport key | `PHOTON_TRANSPORT_KEY` from a secret manager (never `PHOTON_ALLOW_DEV_TRANSPORT_KEY`) |
133//! | Broker TLS | `.require_tls()` / TLS URL (`tls://…`); never set `PHOTON_ALLOW_INSECURE_BROKER` |
134//! | NATS auth | `.credentials_file("/run/secrets/nats.creds")` or `PHOTON_NATS_CREDS` (not URL userinfo) |
135//! | Kafka retention | Pre-create topics with `retention.ms` or broker defaults (`rskafka` cannot set configs) |
136//! | Fluvio retention | Applied at topic create from `PHOTON_FLUVIO_RETENTION` |
137//!
138//! Env index: [`config`]. NATS sketches follow; swap the builder for Kafka/Fluvio.
139//!
140//! ### Publisher binary
141//!
142//! Wire the broker port, declare topics, publish. Skip [`Photon::start_executor`] unless this
143//! process also handles events.
144//!
145//! ```rust,ignore
146//! use std::sync::Arc;
147//!
148//! use photon::{Photon, NatsStoragePort, ReplayCursor};
149//!
150//! # async fn boot_publisher() -> photon::Result<()> {
151//! let port = Arc::new(
152//! NatsStoragePort::builder()
153//! .url("tls://nats.example:4222")
154//! .credentials_file("/run/secrets/nats.creds")
155//! .require_tls()
156//! .replay_cursor(ReplayCursor::StreamSeq)
157//! .sync_ack(true)
158//! .build()
159//! .await?,
160//! );
161//! let photon = Photon::builder()
162//! .storage_port(port)
163//! .auto_registry()
164//! .build()?;
165//! // OrderCreated { … }.publish_on(&photon).await?;
166//! # let _ = photon;
167//! # Ok(())
168//! # }
169//! ```
170//!
171//! ### Worker binary
172//!
173//! Same storage port wiring as the publisher, plus `#[subscribe]` handlers and
174//! [`Photon::start_executor`]:
175//!
176//! ```rust,ignore
177//! use std::sync::Arc;
178//!
179//! use photon::{JsonIdentityFactory, Photon, NatsStoragePort, ReplayCursor};
180//!
181//! # async fn boot_worker() -> photon::Result<()> {
182//! let port = Arc::new(
183//! NatsStoragePort::builder()
184//! .from_env_defaults()
185//! .replay_cursor(ReplayCursor::StreamSeq)
186//! .sync_ack(true)
187//! .build()
188//! .await?,
189//! );
190//! let photon = Photon::builder()
191//! .storage_port(port)
192//! .auto_registry()
193//! .build()?;
194//! photon.start_executor(Arc::new(JsonIdentityFactory))?;
195//! # let _ = photon;
196//! # Ok(())
197//! # }
198//! ```
199//!
200//! ### Run both
201//!
202//! 1. Start the broker.
203//! 2. Start **worker(s)** first so subscriptions are ready.
204//! 3. Start one or more **publishers**.
205//!
206//! Runnable: `cargo run -p uf-photon --example nats_worker --features runtime,nats` then
207//! `nats_publisher` (same features). Same pair contract with other adapters: `kafka_worker` +
208//! `kafka_publisher` (`runtime,kafka`), `fluvio_worker` + `fluvio_publisher` (`runtime,fluvio`),
209//! and the production-TLS variant `nats_secure_worker` + `nats_secure_publisher`
210//! (`runtime,nats`). Multi-terminal runbook: repository `photon/README.md` § How to run examples.
211//!
212//! Then continue with [declare topics](#3-declare-topics-and-handlers).
213//!
214//! ## 3. Declare topics and handlers
215//!
216//! - [`topic`] — typed event struct + inventory registration; generates `publish_on` / `subscribe_on`
217//! - [`subscribe`] — inventory-registered handler; requires [`Photon::start_executor`] on workers
218//! - [`prelude`] — common imports (`Event`, `SubscribeOpts`, `Photon`, macros)
219//!
220//! Attribute tables: [`config`]. Runnable: `keyed_topic`, `consumer_group`, `subscribe_v2`.
221//!
222//! ## 4. Publish and subscribe
223//!
224//! After [`topic`], the macro generates typed methods on your event struct. Prefer those over
225//! raw [`Photon::publish`] / [`Photon::subscribe`].
226//!
227//! **Publish** — `publish_on` with an explicit [`Photon`] handle:
228//!
229//! ```rust,ignore
230//! OrderCreated {
231//! order_id: "ord-1".into(),
232//! amount_cents: 9900,
233//! }
234//! .publish_on(&photon)
235//! .await?;
236//! ```
237//!
238//! **Typed stream** — `subscribe_on` (no `#[subscribe]` / executor required):
239//!
240//! ```rust,ignore
241//! use futures::StreamExt;
242//! use photon::SubscribeOpts;
243//!
244//! let opts = SubscribeOpts::default_ephemeral();
245//! let mut stream = OrderCreated::subscribe_on(&photon, opts).await?;
246//! if let Some(Ok(envelope)) = stream.next().await {
247//! let _payload = envelope.payload; // OrderCreated
248//! }
249//! ```
250//!
251//! **Inventory handlers** — `#[subscribe]` + [`Photon::start_executor`] (Embedded hosts and
252//! Brokered workers).
253//!
254//! Optional sugar after [`configure`]: `.publish()` / `.subscribe()` without a handle.
255//! Raw topic-name API (advanced): [`Photon::subscribe`].
256//!
257//! Runnable: `embedded_mem` (`publish_on` + `#[subscribe]`), `keyed_topic` (`subscribe_on`),
258//! `manual_subscribe` (raw [`Photon::subscribe`]).
259//!
260//! ## 5. Run the executor and reclaim
261//!
262//! - [`Photon::start_executor`] — dispatch inventory-registered `#[subscribe]` handlers (Embedded
263//! hosts and Brokered workers)
264//! - [`Photon::reclaim_transport`] — retention sweep past the safe watermark
265//! - Optional ops telemetry: [`OpsLog`] via [`PhotonBuilder::ops_log`] (example: `telemetry_ops_log`)
266//! - Restart-safe checkpoints: `durable = "…"` resumes from the last committed seq after a crash
267//! (example: `durable_consumer_recovery`, `runtime,sqlite`)
268//!
269//! ## Notes
270//!
271//! - **Transport key:** boot fails closed without a valid `PHOTON_TRANSPORT_KEY` (see [`config`]).
272//! Actor/payload JSON is sealed before storage and broker write; handlers receive decrypted events.
273//! - **Host responsibilities:** Photon is a trusted-host library — authenticate and authorize at the
274//! process edge before publish/subscribe/admin/WS. See repository `SECURITY.md` (privileged handle,
275//! no in-core topic ACL, production identity factory, broker TLS via `BrokerTransportSecurity` /
276//! `PHOTON_ALLOW_INSECURE_BROKER`, never `PHOTON_ALLOW_DEV_TRANSPORT_KEY` in production).
277//! - **Durable multi-process:** use a broker adapter; `mem` does not cross process boundaries.
278//! - **Lab topologies:** testkit / bench `PHOTON_TOPOLOGY` values are harness labels for
279//! lab matrices. Product choices remain Embedded / Brokered above.
280//! - **Custom adapters:** implement [`StoragePort`] and pass it to
281//! [`PhotonBuilder::storage_port`]. Advanced delivery traits live behind that port.
282//!
283//! ## Architecture
284//!
285//! ```text
286//! Application → Photon (macros + Photon runtime) → storage port → delivery backend
287//! ```
288//!
289//! ```text
290//! Typed API (#[topic], publish_on / publish, #[subscribe])
291//! │
292//! v
293//! Photon runtime
294//! │
295//! v
296//! StoragePort ──► mem (InProcStoragePort)
297//! ──► sqlite (SqliteStoragePort)
298//! ──► nats / fluvio / kafka (broker crates)
299//! ──► custom implementation
300//! ```
301//!
302//! Full option reference: [`config`]. Macro expansion: repository `docs/macro-expansion.md`.
303
304#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used))]
305/// Re-export identity port traits and JSON stubs from [`photon_core`].
306pub use photon_core::{
307 actor_downcast_methods, Actor, IdentityError, IdentityFactory, JsonActor, JsonIdentityFactory,
308};
309/// Register an async handler (`#[photon::subscribe]`).
310pub use photon_macros::subscribe;
311/// Register a typed topic struct (`#[photon::topic]`).
312pub use photon_macros::topic;
313/// Quark inventory for compile-time topic/handler registration.
314pub use quark::inventory;
315
316#[cfg(feature = "runtime")]
317mod runtime;
318
319#[cfg(feature = "runtime")]
320pub use runtime::*;
321
322#[cfg(feature = "runtime")]
323pub mod config;