use std::pin::Pin;
use std::task::{Context, Poll};
use futures_util::Stream;
use tokio::sync::mpsc;
use crate::client::events::SubscriptionId;
use crate::client::router::RouterCommand;
use crate::error::ServerError;
use crate::session::{
CommandFields as WireCommandFields, FilteringMode, MaxFrequency as WireFrequency,
SubscriptionEvent as Wire,
};
use crate::subscription::item_update::ItemUpdate;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[non_exhaustive]
pub struct CommandFields {
pub key: usize,
pub command: usize,
}
#[cfg(feature = "test-util")]
impl CommandFields {
#[must_use]
pub(crate) const fn from_parts(key: usize, command: usize) -> Self {
Self { key, command }
}
}
impl From<WireCommandFields> for CommandFields {
fn from(fields: WireCommandFields) -> Self {
Self {
key: fields.key,
command: fields.command,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[non_exhaustive]
pub enum UpdateFrequency {
Unlimited,
Limited {
updates_per_second: String,
},
Unrecognized {
literal: String,
},
}
impl From<WireFrequency> for UpdateFrequency {
fn from(frequency: WireFrequency) -> Self {
match frequency {
WireFrequency::Unlimited => Self::Unlimited,
WireFrequency::Limited { updates_per_second } => Self::Limited { updates_per_second },
WireFrequency::Unrecognized { literal } => Self::Unrecognized { literal },
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[non_exhaustive]
pub enum Filtering {
Filtered,
Unfiltered,
Unrecognized {
literal: String,
},
}
impl From<FilteringMode> for Filtering {
fn from(mode: FilteringMode) -> Self {
match mode {
FilteringMode::Filtered => Self::Filtered,
FilteringMode::Unfiltered => Self::Unfiltered,
FilteringMode::Unrecognized { literal } => Self::Unrecognized { literal },
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum SubscriptionEvent {
Activated {
item_count: usize,
field_count: usize,
command_fields: Option<CommandFields>,
},
Update(Box<ItemUpdate>),
EndOfSnapshot {
item_index: usize,
},
SnapshotCleared {
item_index: usize,
},
Overflow {
item_index: usize,
dropped_count: u64,
},
Reconfigured {
max_frequency: UpdateFrequency,
filtering: Filtering,
},
Deferred {
reason: String,
},
Rejected(ServerError),
Unsubscribed,
Undecodable {
detail: String,
},
}
impl SubscriptionEvent {
#[must_use]
#[inline]
pub const fn is_terminal(&self) -> bool {
matches!(self, Self::Unsubscribed | Self::Rejected(_))
}
pub(crate) fn from_wire(event: Wire) -> Self {
match event {
Wire::Activated {
item_count,
field_count,
command_fields,
} => Self::Activated {
item_count,
field_count,
command_fields: command_fields.map(Into::into),
},
Wire::Update(update) => Self::Update(Box::new(update)),
Wire::EndOfSnapshot { item_index } => Self::EndOfSnapshot { item_index },
Wire::SnapshotCleared { item_index } => Self::SnapshotCleared { item_index },
Wire::Overflow {
item_index,
dropped_count,
} => Self::Overflow {
item_index,
dropped_count,
},
Wire::Reconfigured {
max_frequency,
filtering,
} => Self::Reconfigured {
max_frequency: max_frequency.into(),
filtering: filtering.into(),
},
Wire::Unsubscribed => Self::Unsubscribed,
}
}
}
#[derive(Debug)]
pub struct Updates {
id: SubscriptionId,
events: mpsc::Receiver<SubscriptionEvent>,
control: mpsc::UnboundedSender<RouterCommand>,
}
impl Updates {
pub(crate) const fn new(
id: SubscriptionId,
events: mpsc::Receiver<SubscriptionEvent>,
control: mpsc::UnboundedSender<RouterCommand>,
) -> Self {
Self {
id,
events,
control,
}
}
#[must_use]
#[inline]
pub const fn id(&self) -> SubscriptionId {
self.id
}
}
impl Stream for Updates {
type Item = SubscriptionEvent;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
self.events.poll_recv(cx)
}
}
impl Drop for Updates {
fn drop(&mut self) {
if self
.control
.send(RouterCommand::StreamDropped { id: self.id })
.is_err()
{
tracing::debug!(
id = self.id.get(),
"subscription stream dropped after the client"
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_events_that_end_a_subscription_are_marked_terminal() {
assert!(SubscriptionEvent::Unsubscribed.is_terminal());
assert!(SubscriptionEvent::Rejected(ServerError::new(19, "not found")).is_terminal());
assert!(
!SubscriptionEvent::EndOfSnapshot { item_index: 1 }.is_terminal(),
"end of snapshot is a boundary, not an ending"
);
assert!(
!SubscriptionEvent::Overflow {
item_index: 1,
dropped_count: 4
}
.is_terminal()
);
}
#[test]
fn test_wire_events_translate_without_losing_anything() {
let activated = SubscriptionEvent::from_wire(Wire::Activated {
item_count: 3,
field_count: 4,
command_fields: Some(WireCommandFields { key: 1, command: 2 }),
});
assert!(matches!(
activated,
SubscriptionEvent::Activated {
item_count: 3,
field_count: 4,
command_fields: Some(CommandFields { key: 1, command: 2 })
}
));
let overflow = SubscriptionEvent::from_wire(Wire::Overflow {
item_index: 2,
dropped_count: 17,
});
assert!(matches!(
overflow,
SubscriptionEvent::Overflow {
item_index: 2,
dropped_count: 17
}
));
}
#[test]
fn test_reconfiguration_keeps_the_servers_own_text() {
let event = SubscriptionEvent::from_wire(Wire::Reconfigured {
max_frequency: WireFrequency::Limited {
updates_per_second: "3.0".to_owned(),
},
filtering: FilteringMode::Unrecognized {
literal: "novel".to_owned(),
},
});
assert!(matches!(
event,
SubscriptionEvent::Reconfigured {
max_frequency: UpdateFrequency::Limited { updates_per_second },
filtering: Filtering::Unrecognized { literal },
} if updates_per_second == "3.0" && literal == "novel"
));
}
#[test]
fn test_an_unreadable_frequency_is_not_delivered_as_a_limit() {
let event = SubscriptionEvent::from_wire(Wire::Reconfigured {
max_frequency: WireFrequency::Unrecognized {
literal: "banana".to_owned(),
},
filtering: FilteringMode::Filtered,
});
assert!(matches!(
event,
SubscriptionEvent::Reconfigured {
max_frequency: UpdateFrequency::Unrecognized { literal },
filtering: Filtering::Filtered,
} if literal == "banana"
));
}
}