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}