photon_backend_nats/
port.rs1use std::pin::Pin;
4
5use async_trait::async_trait;
6use chrono::Utc;
7use futures::stream::Stream;
8use photon_backend::models::Event;
9use photon_backend::{
10 seal_event_for_storage, PhotonError, Result, StorageCapabilities, StoragePort,
11};
12use serde_json::Value;
13use uuid::Uuid;
14
15use crate::checkpoint::CheckpointStore;
16use crate::config::{NatsConfig, NatsStoragePortBuilder, ReplayCursor};
17use crate::connect::connect_nats;
18use crate::consumer::subscribe_push;
19use crate::message::encode_event;
20use crate::publish::PublishPipeline;
21use crate::stream::ensure_streams;
22use crate::stream_shard::{composite_seq, photon_subject_for, pick_shard, publish_routing_key};
23
24pub fn nats_url_from_env() -> Result<String> {
30 std::env::var(crate::config::URL_ENV).map_err(|_| {
31 PhotonError::Internal(format!(
32 "{} not set for nats storage adapter",
33 crate::config::URL_ENV
34 ))
35 })
36}
37
38pub struct NatsStoragePort {
40 jetstream: async_nats::jetstream::Context,
41 config: NatsConfig,
42 pipeline: PublishPipeline,
43 checkpoint_store: CheckpointStore,
44}
45
46impl NatsStoragePort {
47 #[must_use]
49 pub fn builder() -> NatsStoragePortBuilder {
50 NatsStoragePortBuilder::new()
51 }
52
53 pub async fn from_env() -> Result<Self> {
59 Self::builder().from_env_defaults().build().await
60 }
61
62 pub async fn connect(url: &str, stream_name: &str) -> Result<Self> {
68 Self::builder()
69 .url(url)
70 .stream_name(stream_name)
71 .from_env_defaults()
72 .build()
73 .await
74 }
75
76 #[must_use]
78 pub const fn config(&self) -> &NatsConfig {
79 &self.config
80 }
81
82 async fn connect_with_config(config: NatsConfig) -> Result<Self> {
83 let client = connect_nats(
84 &config.url,
85 config.transport_security,
86 config.credentials_file.as_deref(),
87 )
88 .await?;
89 let jetstream = async_nats::jetstream::new(client);
90 ensure_streams(&jetstream, &config).await?;
91 let checkpoint_store = CheckpointStore::connect(&jetstream, &config).await?;
92 let pipeline = PublishPipeline::new(&config);
93 Ok(Self {
94 jetstream,
95 config,
96 pipeline,
97 checkpoint_store,
98 })
99 }
100}
101
102impl NatsStoragePortBuilder {
103 pub async fn build(self) -> Result<NatsStoragePort> {
109 let config = self.resolve()?;
110 NatsStoragePort::connect_with_config(config).await
111 }
112}
113
114#[async_trait]
115impl StoragePort for NatsStoragePort {
116 fn capabilities(&self) -> StorageCapabilities {
117 StorageCapabilities::broker("nats")
118 }
119
120 async fn append(
121 &self,
122 topic_name: &str,
123 topic_key: Option<&str>,
124 actor_json: Value,
125 payload_json: Value,
126 ) -> Result<Event> {
127 let event = Event {
128 event_id: Uuid::new_v4().to_string(),
129 topic_name: topic_name.to_string(),
130 topic_key: topic_key.map(String::from),
131 seq: 0,
132 actor_json,
133 payload_json,
134 created_at: Utc::now(),
135 };
136 let (mut plain, sealed) = seal_event_for_storage(&self.config.crypto, event)?;
137
138 let routing = publish_routing_key(topic_key, &plain.event_id);
139 let shard = pick_shard(&routing, self.config.stream_shards);
140 let subject = photon_subject_for(shard, self.config.stream_shards, topic_name);
141 let (headers, body) = encode_event(&sealed)?;
142 let stream_seq = self
143 .pipeline
144 .publish(&self.jetstream, subject, Some(headers), body)
145 .await?;
146
147 if self.config.replay_cursor == ReplayCursor::StreamSeq {
148 if let Some(seq) = stream_seq {
149 let local = i64::try_from(seq).unwrap_or(i64::MAX);
150 plain.seq = if self.config.is_sharded() {
151 composite_seq(shard, seq)
152 } else {
153 local
154 };
155 }
156 }
157
158 Ok(plain)
159 }
160
161 fn subscribe(
162 &self,
163 topic_name: String,
164 topic_key_filter: Option<String>,
165 after_seq: Option<i64>,
166 ) -> Pin<Box<dyn Stream<Item = Result<Event>> + Send>> {
167 let effective_after = if self.config.replay_cursor == ReplayCursor::TailOnly {
168 None
169 } else {
170 after_seq
171 };
172 subscribe_push(
173 self.jetstream.clone(),
174 self.config.clone(),
175 self.checkpoint_store.clone(),
176 topic_name,
177 topic_key_filter,
178 effective_after,
179 )
180 }
181
182 async fn get_event(&self, _event_id: &str) -> Result<Option<Event>> {
183 Ok(None)
184 }
185
186 async fn list_by_topic(
187 &self,
188 _topic_name: &str,
189 _topic_key: Option<&str>,
190 _after_seq: Option<i64>,
191 _limit: usize,
192 ) -> Result<Vec<Event>> {
193 Ok(Vec::new())
194 }
195
196 async fn list_recent(&self, _limit: usize) -> Result<Vec<Event>> {
197 Ok(Vec::new())
198 }
199
200 async fn load_checkpoint(
201 &self,
202 subscription_name: &str,
203 topic_name: &str,
204 topic_key: Option<&str>,
205 ) -> Result<Option<i64>> {
206 if self.config.replay_cursor == ReplayCursor::TailOnly {
207 return Ok(None);
208 }
209 self.checkpoint_store
210 .load(subscription_name, topic_name, topic_key)
211 .await
212 }
213
214 async fn commit_checkpoint(
215 &self,
216 subscription_name: &str,
217 topic_name: &str,
218 topic_key: Option<&str>,
219 last_seq: i64,
220 ) -> Result<()> {
221 if self.config.replay_cursor == ReplayCursor::TailOnly {
222 return Ok(());
223 }
224 self.checkpoint_store
225 .commit(subscription_name, topic_name, topic_key, last_seq)
226 .await
227 }
228}