Skip to main content

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}