use super::*;
use crate::storage::warp_table::{
IndexKey,
observed::{ViewApplyOutcome, ViewConnection},
};
use crate::{StateMachine, StepInputOf};
#[cfg(kani)]
mod proofs;
#[cfg(test)]
mod tests;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct SubscriptionSession(u64);
#[derive(Debug)]
pub struct SubscriptionStart<Query, Command, View> {
pub session: SubscriptionSession,
pub request: ContinuousRequest<Query, Command, View>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum Phase {
Disconnected,
Subscribing(RequestId),
Baseline(SubscriptionId),
Live(SubscriptionId),
Refresh(SubscriptionId),
Resynchronizing {
id: RequestId,
subscription: SubscriptionId,
baseline: Baseline,
},
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum Baseline {
Replace,
Refresh,
}
pub struct SubscribedView<Id: IndexKey, Value: Clone + Eq> {
copy: ContinuousView<Id, Value>,
generation: u64,
phase: Phase,
}
#[derive(Debug)]
pub enum SubscriptionReceiveError {
WrongSession,
Unexpected,
Apply(ContinuousViewError),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SubscriptionProgress {
Subscribed,
Updated(ViewApplyOutcome),
Resynchronizing,
Refused(SubscriptionRefusal),
}
pub struct ReceiveSubscription<'a, Value, Outcome, Refusal, Id, Projection> {
pub session: SubscriptionSession,
pub reply: &'a ContinuousReply<Value, Outcome, Refusal, Id, Projection>,
}
impl<Id: IndexKey, Value: Clone + Eq> StateMachine for SubscribedView<Id, Value> {}
impl<Id: IndexKey, Value: Clone + Eq> SubscribedView<Id, Value> {
pub fn new(mut copy: ContinuousView<Id, Value>) -> Self {
copy.set_connection(ViewConnection::Disconnected);
Self {
copy,
generation: 0,
phase: Phase::Disconnected,
}
}
pub const fn view(&self) -> &ContinuousView<Id, Value> {
&self.copy
}
pub fn subscribe<Query, Command, View>(
&mut self,
id: RequestId,
view: View,
guarantee: Guarantee,
) -> Result<SubscriptionStart<Query, Command, View>, (View, Error)> {
if self.phase != Phase::Disconnected {
return Err((view, Error::Busy));
}
let Some(generation) = self.generation.checked_add(1) else {
return Err((view, Error::Exhausted));
};
self.generation = generation;
self.phase = Phase::Subscribing(id);
Ok(SubscriptionStart {
session: SubscriptionSession(generation),
request: ContinuousRequest::Subscribe {
id,
view,
guarantee,
},
})
}
pub fn disconnect(
&mut self,
session: SubscriptionSession,
) -> Result<(), SubscriptionReceiveError> {
if session.0 != self.generation {
return Err(SubscriptionReceiveError::WrongSession);
}
self.phase = Phase::Disconnected;
self.copy.set_connection(ViewConnection::Disconnected);
Ok(())
}
pub fn resynchronize<Query, Command, View>(
&mut self,
id: RequestId,
) -> Result<ContinuousRequest<Query, Command, View>, Error> {
let (subscription, baseline) = match self.phase {
Phase::Baseline(subscription) => (subscription, Baseline::Replace),
Phase::Live(subscription) | Phase::Refresh(subscription) => {
(subscription, Baseline::Refresh)
}
Phase::Disconnected | Phase::Subscribing(_) | Phase::Resynchronizing { .. } => {
return Err(Error::Busy);
}
};
self.phase = Phase::Resynchronizing {
id,
subscription,
baseline,
};
self.copy.set_connection(ViewConnection::Disconnected);
Ok(ContinuousRequest::Resynchronize { id, subscription })
}
pub fn into_view(mut self) -> ContinuousView<Id, Value> {
self.copy.set_connection(ViewConnection::Disconnected);
self.copy
}
}
impl<Value, Outcome, Refusal, Id: IndexKey, Projection: Clone + Eq>
StepInputOf<SubscribedView<Id, Projection>>
for ReceiveSubscription<'_, Value, Outcome, Refusal, Id, Projection>
{
type Outcome = Result<SubscriptionProgress, SubscriptionReceiveError>;
fn step(self, subscriber: &mut SubscribedView<Id, Projection>) -> Self::Outcome {
if self.session.0 != subscriber.generation {
return Err(SubscriptionReceiveError::WrongSession);
}
match self.reply {
ContinuousReply::Subscribed { id, subscription }
if subscriber.phase == Phase::Subscribing(*id) =>
{
subscriber.phase = Phase::Baseline(*subscription);
Ok(SubscriptionProgress::Subscribed)
}
ContinuousReply::Resynchronizing { id, subscription } => match subscriber.phase {
Phase::Resynchronizing {
id: expected,
subscription: current,
baseline,
} if expected == *id && current == *subscription => {
subscriber.phase = match baseline {
Baseline::Replace => Phase::Baseline(*subscription),
Baseline::Refresh => Phase::Refresh(*subscription),
};
Ok(SubscriptionProgress::Resynchronizing)
}
Phase::Disconnected
| Phase::Subscribing(_)
| Phase::Baseline(_)
| Phase::Live(_)
| Phase::Refresh(_)
| Phase::Resynchronizing { .. } => Err(SubscriptionReceiveError::Unexpected),
},
ContinuousReply::SubscriptionRefused { id, reason } if matches!(subscriber.phase, Phase::Subscribing(request) | Phase::Resynchronizing { id: request, .. } if request == *id) =>
{
subscriber.phase = Phase::Disconnected;
subscriber.copy.set_connection(ViewConnection::Disconnected);
Ok(SubscriptionProgress::Refused(*reason))
}
ContinuousReply::View {
subscription,
update,
} if matches!(subscriber.phase, Phase::Baseline(current) | Phase::Live(current) | Phase::Refresh(current) if current == *subscription) =>
{
let baseline = matches!(subscriber.phase, Phase::Baseline(_));
if matches!(subscriber.phase, Phase::Refresh(_))
&& !matches!(update, ViewUpdate::Baseline { .. })
{
return Ok(SubscriptionProgress::Updated(
ViewApplyOutcome::BaselineRequired,
));
}
let outcome = if baseline {
subscriber.copy.reconnect(update)
} else {
subscriber.copy.receive(update)
}
.map_err(SubscriptionReceiveError::Apply)?;
match outcome {
ViewApplyOutcome::Applied => {
subscriber.phase = Phase::Live(*subscription);
subscriber.copy.set_connection(ViewConnection::Connected);
}
ViewApplyOutcome::Stale => {}
ViewApplyOutcome::BaselineRequired => {
subscriber.copy.set_connection(ViewConnection::Disconnected);
}
}
Ok(SubscriptionProgress::Updated(outcome))
}
ContinuousReply::Command { .. }
| ContinuousReply::ReplicationInterrupted { .. }
| ContinuousReply::Subscribed { .. }
| ContinuousReply::Unsubscribed { .. }
| ContinuousReply::SubscriptionRefused { .. }
| ContinuousReply::View { .. } => Err(SubscriptionReceiveError::Unexpected),
}
}
}