use std::time::Duration;
use photon_backend::{PhotonError, Result, TransportCrypto};
use crate::retention::retention_from_env;
use crate::replicas::replicas_from_env;
pub const URL_ENV: &str = "PHOTON_NATS_URL";
pub const STREAM_ENV: &str = "PHOTON_NATS_STREAM";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ReplayCursor {
#[default]
StreamSeq,
TailOnly,
}
#[derive(Clone)]
pub struct NatsConfig {
pub url: String,
pub stream_name: String,
pub retention: Duration,
pub replicas: usize,
pub crypto: TransportCrypto,
pub replay_cursor: ReplayCursor,
pub sync_ack: bool,
pub max_inflight: u32,
pub stream_shards: u32,
}
impl NatsConfig {
#[must_use]
pub const fn effective_replicas(&self) -> usize {
if self.stream_shards > 1 {
1
} else {
self.replicas
}
}
#[must_use]
pub const fn is_sharded(&self) -> bool {
self.stream_shards > 1
}
}
pub const REPLAY_CURSOR_ENV: &str = "PHOTON_NATS_REPLAY_CURSOR";
pub const SYNC_ACK_ENV: &str = "PHOTON_NATS_SYNC_ACK";
pub const MAX_INFLIGHT_ENV: &str = "PHOTON_NATS_MAX_INFLIGHT";
#[derive(Default)]
pub struct NatsStoragePortBuilder {
url: Option<String>,
stream_name: Option<String>,
retention: Option<Duration>,
replicas: Option<usize>,
crypto: Option<TransportCrypto>,
replay_cursor: Option<ReplayCursor>,
sync_ack: Option<bool>,
max_inflight: Option<u32>,
stream_shards: Option<u32>,
}
impl NatsStoragePortBuilder {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn from_env_defaults(mut self) -> Self {
if self.url.is_none() {
self.url = std::env::var(URL_ENV).ok();
}
if self.stream_name.is_none() {
self.stream_name = Some(
std::env::var(STREAM_ENV).unwrap_or_else(|_| "photon".into()),
);
}
if self.retention.is_none() {
self.retention = Some(retention_from_env());
}
if self.replicas.is_none() {
self.replicas = Some(replicas_from_env());
}
if self.replay_cursor.is_none() {
self.replay_cursor = Some(replay_cursor_from_env());
}
if self.sync_ack.is_none() {
self.sync_ack = Some(sync_ack_from_env());
}
if self.max_inflight.is_none() {
self.max_inflight = Some(max_inflight_from_env(self.sync_ack.unwrap_or(true)));
}
if self.stream_shards.is_none() {
self.stream_shards = Some(crate::stream_shard::stream_shards_from_env());
}
self
}
#[must_use]
pub fn url(mut self, url: impl Into<String>) -> Self {
self.url = Some(url.into());
self
}
#[must_use]
pub fn stream_name(mut self, name: impl Into<String>) -> Self {
self.stream_name = Some(name.into());
self
}
#[must_use]
pub const fn retention(mut self, retention: Duration) -> Self {
self.retention = Some(retention);
self
}
#[must_use]
pub const fn replicas(mut self, replicas: usize) -> Self {
self.replicas = Some(replicas);
self
}
#[must_use]
pub fn crypto(mut self, crypto: TransportCrypto) -> Self {
self.crypto = Some(crypto);
self
}
#[must_use]
pub const fn replay_cursor(mut self, cursor: ReplayCursor) -> Self {
self.replay_cursor = Some(cursor);
self
}
#[must_use]
pub const fn sync_ack(mut self, wait: bool) -> Self {
self.sync_ack = Some(wait);
self
}
#[must_use]
pub fn max_inflight(mut self, n: u32) -> Self {
self.max_inflight = Some(n.max(1));
self
}
#[must_use]
pub fn stream_shards(mut self, n: u32) -> Self {
self.stream_shards = Some(n.clamp(1, crate::stream_shard::MAX_STREAM_SHARDS));
self
}
pub fn resolve(self) -> Result<NatsConfig> {
let builder = self.from_env_defaults();
let url = builder.url.ok_or_else(|| {
PhotonError::Internal(format!("{URL_ENV} not set for nats storage adapter"))
})?;
Ok(NatsConfig {
url,
stream_name: builder
.stream_name
.unwrap_or_else(|| "photon".into()),
retention: builder.retention.unwrap_or_else(retention_from_env),
replicas: builder.replicas.unwrap_or_else(replicas_from_env),
crypto: match builder.crypto {
Some(c) => c,
None => TransportCrypto::from_env()?,
},
replay_cursor: builder
.replay_cursor
.unwrap_or(ReplayCursor::StreamSeq),
sync_ack: builder.sync_ack.unwrap_or(true),
max_inflight: builder
.max_inflight
.unwrap_or_else(|| max_inflight_from_env(builder.sync_ack.unwrap_or(true))),
stream_shards: builder
.stream_shards
.unwrap_or_else(crate::stream_shard::stream_shards_from_env),
})
}
}
fn replay_cursor_from_env() -> ReplayCursor {
match std::env::var(REPLAY_CURSOR_ENV)
.unwrap_or_else(|_| "stream_seq".into())
.to_ascii_lowercase()
.as_str()
{
"tail_only" | "tail" | "none" => ReplayCursor::TailOnly,
_ => ReplayCursor::StreamSeq,
}
}
fn sync_ack_from_env() -> bool {
!matches!(
std::env::var(SYNC_ACK_ENV)
.unwrap_or_else(|_| "1".into())
.as_str(),
"0" | "false" | "off" | "no"
)
}
fn max_inflight_from_env(sync_ack: bool) -> u32 {
if let Ok(raw) = std::env::var(MAX_INFLIGHT_ENV) {
if let Ok(n) = raw.parse::<u32>() {
return n.max(1);
}
}
if sync_ack { 1 } else { 256 }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn replay_cursor_parses_tail_only() {
std::env::set_var(REPLAY_CURSOR_ENV, "tail_only");
assert_eq!(replay_cursor_from_env(), ReplayCursor::TailOnly);
std::env::remove_var(REPLAY_CURSOR_ENV);
}
}