use std::sync::Arc;
use std::time::Duration;
use batpak::store::{Open, Store};
use flume::Receiver;
use super::config::SubscriptionRuntimeConfig;
use super::error::{stream_code, SubscriptionRuntimeError};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RuntimeCursor(Vec<u8>);
impl RuntimeCursor {
#[must_use]
pub fn from_bytes(bytes: Vec<u8>) -> Self {
Self(bytes)
}
#[must_use]
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum SessionDelivery {
Event(SessionEventDelivery),
Watermark(SessionWatermarkDelivery),
Error(SessionError),
End(SessionEnd),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SessionEventDelivery {
pub subscription_id: String,
pub delivery_index: u64,
pub cursor_before: RuntimeCursor,
pub cursor_after: RuntimeCursor,
pub wire_payload_schema_ref: String,
pub envelope_bytes: Vec<u8>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SessionWatermarkDelivery {
pub subscription_id: String,
pub delivery_index: u64,
pub cursor_after: RuntimeCursor,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SessionError {
pub subscription_id: Option<String>,
pub code: &'static str,
pub last_delivered_cursor: Option<RuntimeCursor>,
pub last_acked_cursor: Option<RuntimeCursor>,
pub message: Vec<u8>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SessionEnd {
pub subscription_id: String,
pub reason_code: &'static str,
pub cursor_after: Option<RuntimeCursor>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum SessionPoll {
Delivery(SessionDelivery),
Blocked,
Ended,
}
pub trait SubscriptionSession: Send {
fn poll(&mut self, timeout: Duration) -> Result<SessionPoll, SubscriptionRuntimeError>;
}
pub trait SubscriptionSessionFactory {
fn open_session(
&self,
subscription_id: &str,
resume_cursor: Option<&[u8]>,
client_window: u32,
control_rx: Receiver<SessionControl>,
) -> Result<Box<dyn SubscriptionSession>, SubscriptionRuntimeError>;
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum SessionControl {
Ack {
delivery_index: u64,
cursor: RuntimeCursor,
},
Cancel,
Disconnected,
Malformed,
}
#[derive(Clone)]
pub struct SubscriptionStore {
pub(crate) inner: Arc<Store<Open>>,
}
impl SubscriptionStore {
#[must_use]
pub fn new(store: Arc<Store<Open>>) -> Self {
Self { inner: store }
}
}
#[must_use]
pub fn unknown_subscription_error(subscription_id: &str) -> SessionDelivery {
SessionDelivery::Error(SessionError {
subscription_id: Some(subscription_id.to_owned()),
code: stream_code::UNKNOWN_SUBSCRIPTION,
last_delivered_cursor: None,
last_acked_cursor: None,
message: subscription_id.as_bytes().to_vec(),
})
}
#[must_use]
pub fn cursor_invalid_error(reason: &'static str) -> SessionDelivery {
SessionDelivery::Error(SessionError {
subscription_id: None,
code: stream_code::CURSOR_INVALID,
last_delivered_cursor: None,
last_acked_cursor: None,
message: reason.as_bytes().to_vec(),
})
}
#[must_use]
pub fn cursor_mismatch_error(reason: &'static str) -> SessionDelivery {
SessionDelivery::Error(SessionError {
subscription_id: None,
code: stream_code::CURSOR_MISMATCH,
last_delivered_cursor: None,
last_acked_cursor: None,
message: reason.as_bytes().to_vec(),
})
}
#[must_use]
pub fn slow_consumer_error(
subscription_id: &str,
last_delivered_cursor: Option<RuntimeCursor>,
last_acked_cursor: Option<RuntimeCursor>,
) -> SessionError {
SessionError {
subscription_id: Some(subscription_id.to_owned()),
code: stream_code::SLOW_CONSUMER,
last_delivered_cursor,
last_acked_cursor,
message: b"delivery window full".to_vec(),
}
}
#[must_use]
pub fn receipt_decode_failed_error(
subscription_id: &str,
last_delivered_cursor: Option<RuntimeCursor>,
last_acked_cursor: Option<RuntimeCursor>,
) -> SessionError {
SessionError {
subscription_id: Some(subscription_id.to_owned()),
code: stream_code::RECEIPT_DECODE_FAILED,
last_delivered_cursor,
last_acked_cursor,
message: b"syncbat receipt event payload decode failed".to_vec(),
}
}
#[must_use]
pub fn client_cancel_end(
subscription_id: &str,
cursor_after: Option<RuntimeCursor>,
) -> SessionDelivery {
SessionDelivery::End(SessionEnd {
subscription_id: subscription_id.to_owned(),
reason_code: stream_code::CLIENT_CANCELLED,
cursor_after,
})
}
#[must_use]
pub fn malformed_control_error(
subscription_id: &str,
last_delivered_cursor: Option<RuntimeCursor>,
last_acked_cursor: Option<RuntimeCursor>,
) -> SessionDelivery {
SessionDelivery::Error(SessionError {
subscription_id: Some(subscription_id.to_owned()),
code: stream_code::MALFORMED_STREAM_FRAME,
last_delivered_cursor,
last_acked_cursor,
message: b"malformed stream control frame".to_vec(),
})
}
#[must_use]
pub fn ack_invalid_error(
subscription_id: &str,
reason: &'static str,
last_delivered_cursor: Option<RuntimeCursor>,
last_acked_cursor: Option<RuntimeCursor>,
) -> SessionDelivery {
SessionDelivery::Error(SessionError {
subscription_id: Some(subscription_id.to_owned()),
code: match reason {
"ack cursor does not match sent cursor" | "ack delivery index out of range" => {
stream_code::MALFORMED_STREAM_FRAME
}
_ => stream_code::CURSOR_INVALID,
},
last_delivered_cursor,
last_acked_cursor,
message: reason.as_bytes().to_vec(),
})
}
#[must_use]
pub fn cursor_mismatch_terminal(
subscription_id: &str,
reason: &'static str,
last_delivered_cursor: Option<RuntimeCursor>,
last_acked_cursor: Option<RuntimeCursor>,
) -> SessionDelivery {
SessionDelivery::Error(SessionError {
subscription_id: Some(subscription_id.to_owned()),
code: stream_code::CURSOR_MISMATCH,
last_delivered_cursor,
last_acked_cursor,
message: reason.as_bytes().to_vec(),
})
}
#[must_use]
pub fn queue_capacity(
client_window: u32,
server_max_window: usize,
route_cap: Option<usize>,
) -> u64 {
let client = u64::from(client_window);
let server = u64::try_from(server_max_window).unwrap_or(u64::MAX);
let route = route_cap
.and_then(|cap| u64::try_from(cap).ok())
.unwrap_or(server);
client.min(server).min(route)
}
pub fn validate_open_limits(
config: SubscriptionRuntimeConfig,
client_window: u32,
queue_cap: u64,
) -> Result<(), SubscriptionRuntimeError> {
config.validate()?;
if client_window == 0 {
return Err(SubscriptionRuntimeError::InvalidConfig {
reason: "client window is zero",
});
}
if queue_cap == 0 {
return Err(SubscriptionRuntimeError::InvalidConfig {
reason: "delivery queue capacity is zero",
});
}
Ok(())
}