use core::time::Duration;
use anyhow::Context as _;
use async_nats::jetstream::{
self,
consumer::{AckPolicy, PullConsumer, pull},
stream::{Config, RetentionPolicy, StorageType},
};
use super::{create_or_update_stream, duplicate_window};
pub const OUTPUT_PREFIX: &str = "events.matched.v1.p";
pub const OUTPUT_STREAM: &str = "EVENTS-MATCHED";
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct OutputConfig {
pub max_age: Duration,
}
impl Default for OutputConfig {
fn default() -> Self {
Self {
max_age: Duration::from_secs(15 * 60),
}
}
}
pub fn output_subject(partition: u64) -> String {
format!("{OUTPUT_PREFIX}.{partition}")
}
pub async fn ensure_output_stream(
context: &jetstream::Context,
config: &OutputConfig,
) -> anyhow::Result<jetstream::stream::Stream> {
create_or_update_stream(
context,
Config {
name: OUTPUT_STREAM.into(),
subjects: vec![format!("{OUTPUT_PREFIX}.>")],
retention: RetentionPolicy::Limits,
storage: StorageType::File,
max_age: config.max_age,
duplicate_window: duplicate_window(config.max_age),
..Default::default()
},
)
.await
.context("could not reconcile committed-output stream")
}
pub async fn output_consumer(
stream: &jetstream::stream::Stream,
name: &str,
) -> anyhow::Result<PullConsumer> {
stream
.get_or_create_consumer(
name,
pull::Config {
durable_name: Some(name.to_owned()),
filter_subject: format!("{OUTPUT_PREFIX}.>"),
ack_policy: AckPolicy::Explicit,
..Default::default()
},
)
.await
.with_context(|| format!("could not create output consumer {name}"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn subjects_are_distinct_per_partition() {
assert_eq!(output_subject(0), "events.matched.v1.p.0");
assert_eq!(output_subject(485), "events.matched.v1.p.485");
assert_eq!(output_subject(1023), "events.matched.v1.p.1023");
}
#[test]
fn stream_name_and_prefix_are_stable() {
assert_eq!(OUTPUT_STREAM, "EVENTS-MATCHED");
assert_eq!(OUTPUT_PREFIX, "events.matched.v1.p");
}
}