photon_backend/storage/port.rs
1//! Object-safe storage port for publish, subscribe, checkpoints, and optional retention.
2//!
3//! **Application authors** usually select a built-in adapter (`mem`, `sqlite`, `nats`, `kafka`, `fluvio`)
4//! via [`photon::PhotonBuilder::storage_port`] — you rarely implement this trait directly.
5//! **Adapter authors** implement [`StoragePort`] in a storage crate and wire it at boot.
6//!
7//! Reference in-process implementation: [`InProcStoragePort`]. Runnable host example:
8//! `cargo run -p uf-photon --example embedded_mem --features runtime,mem`.
9//!
10//! See also: [`crate::checkpoint`], [`crate::retention`], [`crate::backend`].
11
12use std::pin::Pin;
13use std::time::Duration;
14
15use async_trait::async_trait;
16use futures::stream::Stream;
17use serde_json::Value;
18
19use crate::error::Result;
20use crate::models::Event;
21
22/// Capabilities advertised by a storage port (surfaced via [`crate::backend::GenericPhotonBackend`]).
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub struct StorageCapabilities {
25 /// Whether [`StoragePort::get_event`] is supported.
26 pub supports_get_event: bool,
27 /// Maximum replay window for bounded retention adapters.
28 pub max_replay_window: Option<Duration>,
29 /// Stable telemetry / bench label (`mem`, `nats`, …).
30 pub telemetry_label: &'static str,
31}
32
33impl StorageCapabilities {
34 /// In-process embedded tier defaults.
35 #[must_use]
36 pub const fn mem() -> Self {
37 Self {
38 supports_get_event: true,
39 max_replay_window: None,
40 telemetry_label: "mem",
41 }
42 }
43
44 /// Embedded `SQLite` tier defaults.
45 #[must_use]
46 pub const fn sqlite() -> Self {
47 Self {
48 supports_get_event: true,
49 max_replay_window: None,
50 telemetry_label: "sqlite",
51 }
52 }
53
54 /// Broker tier defaults (~15 min replay window).
55 #[must_use]
56 pub const fn broker(label: &'static str) -> Self {
57 Self {
58 supports_get_event: false,
59 max_replay_window: Some(Duration::from_mins(15)),
60 telemetry_label: label,
61 }
62 }
63}
64
65/// Storage adapter contract — append, subscribe, checkpoints, and optional retention.
66///
67/// Each method documents its behavior under **Contract**. Built-in implementations:
68///
69/// | Adapter | Type | Crate |
70/// |---------|------|-------|
71/// | In-process | [`InProcStoragePort`](super::InProcStoragePort) | `photon-backend` (`mem`) |
72/// | `SQLite` | `SqliteStoragePort` | `photon-backend-sqlite` |
73/// | NATS `JetStream` | `NatsStoragePort` | `photon-backend-nats` |
74/// | Kafka | `KafkaStoragePort` | `photon-backend-kafka` |
75/// | Fluvio | `FluvioStoragePort` | `photon-backend-fluvio` |
76///
77/// # Example (use a built-in port)
78///
79/// ```rust,no_run
80/// use std::sync::Arc;
81///
82/// use photon_backend::{InProcStoragePort, StoragePort, TransportCrypto};
83///
84/// # fn main() -> photon_backend::Result<()> {
85/// let port: Arc<dyn StoragePort> = Arc::new(InProcStoragePort::new(
86/// TransportCrypto::from_env()?,
87/// ));
88/// let _caps = port.capabilities();
89/// # Ok(())
90/// # }
91/// ```
92///
93/// Install at boot via the facade [`PhotonBuilder`](https://docs.rs/uf-photon/latest/photon/struct.PhotonBuilder.html).
94/// Host walkthrough: [Integrating the host](https://docs.rs/uf-photon/latest/photon/#integrating-the-host).
95#[async_trait]
96pub trait StoragePort: Send + Sync {
97 /// Adapter capabilities for contract tests and telemetry.
98 fn capabilities(&self) -> StorageCapabilities;
99
100 /// Append one event to a topic partition.
101 ///
102 /// # Contract
103 ///
104 /// - Assigns a monotonically increasing `seq` per `(topic_name, topic_key)` partition.
105 /// - Returns the persisted [`Event`] including stable `event_id`.
106 /// - Payload and actor JSON are opaque to the adapter (encryption happens above the port).
107 async fn append(
108 &self,
109 topic_name: &str,
110 topic_key: Option<&str>,
111 actor_json: Value,
112 payload_json: Value,
113 ) -> Result<Event>;
114
115 /// Stream events for a topic partition, optionally replaying after `after_seq`.
116 ///
117 /// # Contract
118 ///
119 /// - When `after_seq` is set, only events with `seq > after_seq` are yielded.
120 /// - When `topic_key_filter` is set, only matching partition keys are delivered.
121 /// - The stream runs until dropped or an error; live adapters may block for new events.
122 fn subscribe(
123 &self,
124 topic_name: String,
125 topic_key_filter: Option<String>,
126 after_seq: Option<i64>,
127 ) -> Pin<Box<dyn Stream<Item = Result<Event>> + Send>>;
128
129 /// Point lookup by event id (optional — see [`StorageCapabilities::supports_get_event`]).
130 ///
131 /// # Contract
132 ///
133 /// - Returns `None` when the id is unknown or the event was truncated by retention.
134 async fn get_event(&self, event_id: &str) -> Result<Option<Event>>;
135
136 /// Load durable subscription high-water seq.
137 ///
138 /// # Contract
139 ///
140 /// - Returns `None` when no checkpoint exists (caller chooses replay start policy).
141 async fn load_checkpoint(
142 &self,
143 subscription_name: &str,
144 topic_name: &str,
145 topic_key: Option<&str>,
146 ) -> Result<Option<i64>>;
147
148 /// Persist durable subscription high-water seq.
149 ///
150 /// # Contract
151 ///
152 /// - `last_seq` is monotonic per subscription partition; adapters may reject regressions.
153 async fn commit_checkpoint(
154 &self,
155 subscription_name: &str,
156 topic_name: &str,
157 topic_key: Option<&str>,
158 last_seq: i64,
159 ) -> Result<()>;
160
161 /// Trim events before `truncate_bound` for a partition (no-op when unsupported).
162 ///
163 /// # Contract
164 ///
165 /// - Default implementation returns `Ok(0)` without removing events.
166 /// - Supporting adapters remove events with `seq < truncate_bound`.
167 async fn truncate_before(
168 &self,
169 topic_name: &str,
170 topic_key: Option<&str>,
171 truncate_bound: i64,
172 ) -> Result<u64> {
173 let _ = (topic_name, topic_key, truncate_bound);
174 Ok(0)
175 }
176
177 /// Last delivered seq pin for retention watermarks (optional).
178 ///
179 /// # Contract
180 ///
181 /// - Default returns `None` (no delivery-layer retention pin).
182 async fn delivery_seq_pin(
183 &self,
184 topic_name: &str,
185 topic_key: Option<&str>,
186 ) -> Option<i64> {
187 let _ = (topic_name, topic_key);
188 None
189 }
190}