Skip to main content

photon_runtime/
photon.rs

1//! Main Photon runtime handle — publish/subscribe (**Creating topics**) and builder wiring
2//! (**Integrating the host**).
3
4use std::sync::Arc;
5
6use futures::stream::Stream;
7use photon_core::IdentityFactory;
8
9use photon_backend::{Event, PhotonBackend, ReclaimReport, Result, StoragePort, TopicRegistry, ExecutorServices, BackendCapabilities};
10
11use crate::admin::collect_admin_snapshot;
12use crate::admin::AdminSnapshot;
13
14use crate::executor::ExecutorController;
15
16/// Shared storage port, executor services, and handler dispatch controller.
17#[derive(Clone)]
18pub struct PhotonRuntimeState {
19    /// Storage port used by executor checkpoint/retention services.
20    pub storage_port: Arc<dyn StoragePort>,
21    /// Services used by durable handler executors.
22    pub executor_services: Arc<ExecutorServices>,
23    /// Handler dispatch controller.
24    pub executor: Arc<ExecutorController>,
25}
26
27/// Main Photon runtime handle.
28#[derive(Clone)]
29pub struct Photon {
30    backend: Arc<dyn PhotonBackend>,
31    runtime: PhotonRuntimeState,
32}
33
34static DEFAULT_PHOTON: std::sync::RwLock<Option<Photon>> = std::sync::RwLock::new(None);
35
36/// Configure the default Photon instance used by macro-generated convenience helpers
37/// (`Type::publish()` / `Type::subscribe()`).
38///
39/// Prefer passing an explicit [`Photon`] handle via `publish_on` / `subscribe_on` or
40/// [`Photon::publish`]. This process-wide shim is optional sugar for simple hosts.
41///
42/// # Example
43///
44/// ```rust,no_run
45/// use std::sync::Arc;
46///
47/// use photon_core::JsonIdentityFactory;
48/// use photon_runtime::{configure, Photon};
49///
50/// # fn main() -> photon_backend::Result<()> {
51/// let photon = Photon::builder().auto_registry().build()?;
52/// photon.start_executor(Arc::new(JsonIdentityFactory))?;
53/// configure(photon);
54/// # Ok(())
55/// # }
56/// ```
57///
58/// # Panics
59///
60/// Panics if an internal lock is poisoned.
61pub fn configure(photon: Photon) {
62    let mut guard = DEFAULT_PHOTON.write().unwrap();
63    *guard = Some(photon);
64}
65
66/// Clone of the process-wide Photon set by [`configure`], if any.
67///
68/// Prefer an explicit [`Photon`] handle. This exists for macro convenience helpers.
69///
70/// # Panics
71///
72/// Panics if an internal lock is poisoned.
73pub fn default() -> Option<Photon> {
74    let guard = DEFAULT_PHOTON.read().unwrap();
75    guard.clone()
76}
77
78impl Photon {
79    pub(crate) fn new(backend: Arc<dyn PhotonBackend>, runtime: PhotonRuntimeState) -> Self {
80        Self { backend, runtime }
81    }
82
83    /// Start building a Photon runtime instance.
84    #[must_use]
85    pub fn builder() -> crate::builder::PhotonBuilder {
86        crate::builder::PhotonBuilder::default()
87    }
88
89    /// Telemetry label for the installed backend.
90    #[must_use]
91    pub fn backend_label(&self) -> &'static str {
92        self.backend.telemetry_label()
93    }
94
95    pub(crate) fn backend_capabilities(&self) -> BackendCapabilities {
96        PhotonBackend::capabilities(self.backend.as_ref())
97    }
98
99    /// Compose a read-only ops introspection snapshot for host admin UIs.
100    ///
101    /// Aggregates the topic catalog, handler inventory, backend capabilities, and checkpoint
102    /// cursors for inventory-registered handlers. Does not touch publish/subscribe hot paths.
103    ///
104    /// # Errors
105    ///
106    /// Returns an error if a checkpoint load fails.
107    pub async fn admin_snapshot(&self) -> Result<AdminSnapshot> {
108        collect_admin_snapshot(self).await
109    }
110
111    /// Publish a single event to a topic.
112    ///
113    /// # Errors
114    ///
115    /// Returns an error if the operation fails.
116    pub async fn publish(
117        &self,
118        topic_name: &str,
119        topic_key: Option<&str>,
120        actor_json: serde_json::Value,
121        payload_json: serde_json::Value,
122    ) -> Result<String> {
123        PhotonBackend::publish(
124            self.backend.as_ref(),
125            topic_name,
126            topic_key,
127            actor_json,
128            payload_json,
129        )
130        .await
131    }
132
133    /// Subscribe to topic events as a stream.
134    ///
135    /// For typed topics, prefer `TopicType::subscribe(opts)` or the `#[subscribe]` macro.
136    /// Runnable walkthrough: `cargo run -p uf-photon --example manual_subscribe --features runtime,mem`.
137    ///
138    /// # Example
139    ///
140    /// ```rust,no_run
141    /// use futures::StreamExt;
142    /// use photon_runtime::{configure, default, Photon};
143    ///
144    /// # async fn demo() -> photon_backend::Result<()> {
145    /// configure(Photon::builder().auto_registry().build()?);
146    /// let photon = default().expect("configure first");
147    /// let mut stream = photon.subscribe("my.topic", None, None);
148    /// if let Some(result) = stream.next().await {
149    ///     let _event = result?;
150    /// }
151    /// # Ok(())
152    /// # }
153    /// ```
154    #[must_use]
155    pub fn subscribe(
156        &self,
157        topic_name: &str,
158        topic_key_filter: Option<&str>,
159        after_seq: Option<i64>,
160    ) -> std::pin::Pin<Box<dyn Stream<Item = Result<Event>> + Send>> {
161        PhotonBackend::subscribe(
162            self.backend.as_ref(),
163            topic_name.to_string(),
164            topic_key_filter.map(std::string::ToString::to_string),
165            after_seq,
166        )
167    }
168
169    /// Subscribe to assigned virtual shards for a consumer group (multiplexed stream).
170    #[must_use]
171    pub fn subscribe_consumer_group(
172        &self,
173        topic_name: &str,
174        shard_ids: &[u32],
175        after_seq_by_shard: std::collections::HashMap<u32, Option<i64>>,
176    ) -> std::pin::Pin<Box<dyn Stream<Item = Result<Event>> + Send>> {
177        photon_backend::merge_shard_streams(
178            Arc::clone(&self.backend),
179            topic_name.to_string(),
180            shard_ids,
181            after_seq_by_shard,
182        )
183    }
184
185    /// Load a specific event by ID.
186    ///
187    /// # Errors
188    ///
189    /// Returns an error if the operation fails.
190    pub async fn get_event(&self, event_id: &str) -> Result<Option<Event>> {
191        PhotonBackend::get_event(self.backend.as_ref(), event_id).await
192    }
193
194    /// Return the registered topic catalog.
195    #[must_use]
196    pub fn registry(&self) -> &TopicRegistry {
197        PhotonBackend::registry(self.backend.as_ref())
198    }
199
200    /// Read the last checkpoint sequence for a subscription/topic pair.
201    ///
202    /// # Errors
203    ///
204    /// Returns an error if the operation fails.
205    pub async fn get_checkpoint_seq(
206        &self,
207        subscription_name: &str,
208        topic_name: &str,
209        topic_key: Option<&str>,
210    ) -> Result<Option<i64>> {
211        PhotonBackend::get_checkpoint_seq(
212            self.backend.as_ref(),
213            subscription_name,
214            topic_name,
215            topic_key,
216        )
217        .await
218    }
219
220    /// Persist an updated checkpoint sequence for a subscription/topic pair.
221    ///
222    /// # Errors
223    ///
224    /// Returns an error if the operation fails.
225    pub async fn set_checkpoint(
226        &self,
227        subscription_name: &str,
228        topic_name: &str,
229        topic_key: Option<&str>,
230        last_seq: i64,
231    ) -> Result<()> {
232        PhotonBackend::set_checkpoint(
233            self.backend.as_ref(),
234            subscription_name,
235            topic_name,
236            topic_key,
237            last_seq,
238        )
239        .await
240    }
241
242    /// Shared tailer / executor services.
243    #[must_use]
244    pub const fn runtime(&self) -> &PhotonRuntimeState {
245        &self.runtime
246    }
247
248    /// Reclaim transport log rows past the safe watermark (headless ops entry point).
249    ///
250    /// # Errors
251    ///
252    /// Returns an error if the operation fails.
253    pub async fn reclaim_transport(&self) -> Result<Vec<ReclaimReport>> {
254        self.runtime
255            .executor_services
256            .retention_reclaimer
257            .sweep_all()
258            .await
259    }
260
261    /// Start inventory-registered `#[photon::subscribe]` handlers.
262    ///
263    /// Requires an [`IdentityFactory`] (e.g. [`photon_core::JsonIdentityFactory`]) for actor
264    /// resolution. Optionally call [`configure`] so convenience `Type::publish()` helpers work.
265    ///
266    /// # Example
267    ///
268    /// ```rust,no_run
269    /// use std::sync::Arc;
270    ///
271    /// use photon_core::JsonIdentityFactory;
272    /// use photon_runtime::Photon;
273    ///
274    /// # async fn boot() -> photon_backend::Result<()> {
275    /// let photon = Photon::builder().auto_registry().build()?;
276    /// photon.start_executor(Arc::new(JsonIdentityFactory))?;
277    /// photon.shutdown_executor();
278    /// photon.join_executor().await;
279    /// # Ok(())
280    /// # }
281    /// ```
282    ///
283    /// # Errors
284    ///
285    /// Returns an error if the executor was already started on this runtime.
286    #[allow(clippy::needless_pass_by_value)] // Arc-by-value is the public ownership API
287    pub fn start_executor(&self, identity: Arc<dyn IdentityFactory>) -> Result<()> {
288        self.runtime.executor.start(self, &identity)
289    }
290
291    /// Signal handler loops to stop accepting new events.
292    ///
293    /// # Contract
294    ///
295    /// Idempotent. Pair with [`Self::join_executor`] to await in-flight work.
296    pub fn shutdown_executor(&self) {
297        self.runtime.executor.shutdown();
298    }
299
300    /// Await handler loops and in-flight dispatches after [`Self::shutdown_executor`].
301    ///
302    /// # Contract
303    ///
304    /// Safe when the executor was never started. Restart requires a new [`Photon`] build.
305    pub async fn join_executor(&self) {
306        self.runtime.executor.join().await;
307    }
308}