liminal_sdk/remote/websocket/
binding.rs1use liminal_protocol::client::{
17 ClientParticipantAggregate, ClientResponseCorrelation, DetachReplayRefusalReason,
18 DetachTransportFate, DetachTransportFateDecision, EstablishedConnectionTransportFate,
19 ExplicitReconnectAction, ReconnectAttemptDecision, ReconnectAttemptFate,
20 ReconnectAttemptFateDecision, ReconnectAttemptFateRefusalReason, ReconnectAttemptPermit,
21 ReconnectAttemptRefusalReason, ReconnectFreshEvent, ReconnectInProgressAttempt,
22 ReconnectPermitDecision, ReconnectPermitRefusalReason, record_attempt_fate,
23 record_explicit_reconnect, record_transport_fate, redeem_attempt, transport_fate,
24};
25use liminal_protocol::outcome::ReconnectState;
26
27use super::core::TransportTerminal;
28
29#[derive(Debug, PartialEq, Eq)]
31pub enum OpenRequestDecision {
32 Authorized {
34 event: ReconnectFreshEvent,
36 },
37 Refused(OpenRequestRefusal),
39}
40
41#[derive(Clone, Copy, Debug, PartialEq, Eq)]
43pub enum OpenRequestRefusal {
44 OpenAlreadyInProgress,
46 Permit(ReconnectPermitRefusalReason),
48 Redemption(ReconnectAttemptRefusalReason),
50}
51
52#[derive(Debug, PartialEq, Eq)]
54pub enum AttemptFateOutcome {
55 Recorded {
57 state: ReconnectState,
59 },
60 Refused(AttemptFateRefusal),
62}
63
64#[derive(Clone, Copy, Debug, PartialEq, Eq)]
66pub enum AttemptFateRefusal {
67 NoOpenInProgress,
69 Fate(ReconnectAttemptFateRefusalReason),
71}
72
73#[derive(Debug, PartialEq, Eq)]
75pub enum LossRecordOutcome {
76 PermitRetained,
79 Refused(LossRecordRefusal),
81}
82
83#[derive(Clone, Copy, Debug, PartialEq, Eq)]
85pub enum LossRecordRefusal {
86 NotEstablished,
88 Permit(ReconnectPermitRefusalReason),
90}
91
92#[derive(Debug, PartialEq, Eq)]
94pub enum DetachLossOutcome {
95 Parked,
97 Refused(DetachReplayRefusalReason),
99}
100
101#[derive(Debug)]
108pub struct WebSocketAuthorityBinding {
109 aggregate: ClientParticipantAggregate,
110 permit: Option<ReconnectAttemptPermit>,
111 attempt: Option<ReconnectInProgressAttempt>,
112 last_loss_diagnostic: Option<TransportTerminal>,
113}
114
115impl WebSocketAuthorityBinding {
116 #[must_use]
118 pub const fn new() -> Self {
119 Self::with_aggregate(ClientParticipantAggregate::new())
120 }
121
122 #[must_use]
125 pub const fn with_aggregate(aggregate: ClientParticipantAggregate) -> Self {
126 Self {
127 aggregate,
128 permit: None,
129 attempt: None,
130 last_loss_diagnostic: None,
131 }
132 }
133
134 #[must_use]
136 pub const fn aggregate(&self) -> &ClientParticipantAggregate {
137 &self.aggregate
138 }
139
140 #[must_use]
142 pub const fn reconnect_state(&self) -> ReconnectState {
143 self.aggregate.reconnect().state()
144 }
145
146 #[must_use]
150 pub const fn last_loss_diagnostic(&self) -> Option<&TransportTerminal> {
151 self.last_loss_diagnostic.as_ref()
152 }
153
154 pub fn request_open(&mut self) -> OpenRequestDecision {
165 if self.attempt.is_some() {
166 return OpenRequestDecision::Refused(OpenRequestRefusal::OpenAlreadyInProgress);
167 }
168 let aggregate = self.take_aggregate();
169 let (aggregate, permit) = match self.permit.take() {
170 Some(retained) => (aggregate, retained),
171 None => {
172 match record_explicit_reconnect(aggregate, ExplicitReconnectAction::ReconnectNow) {
173 ReconnectPermitDecision::Permitted {
174 aggregate, permit, ..
175 } => (aggregate, permit),
176 ReconnectPermitDecision::Refused(refusal) => {
177 let reason = refusal.reason();
178 let (aggregate, _event) = refusal.into_parts();
179 self.aggregate = aggregate;
180 return OpenRequestDecision::Refused(OpenRequestRefusal::Permit(reason));
181 }
182 }
183 }
184 };
185 match redeem_attempt(aggregate, permit) {
186 ReconnectAttemptDecision::Started { aggregate, attempt } => {
187 let event = attempt.event();
188 self.aggregate = aggregate;
189 self.attempt = Some(attempt);
190 OpenRequestDecision::Authorized { event }
191 }
192 ReconnectAttemptDecision::Refused {
193 aggregate,
194 permit,
195 reason,
196 } => {
197 self.aggregate = aggregate;
198 self.permit = Some(permit);
199 OpenRequestDecision::Refused(OpenRequestRefusal::Redemption(reason))
200 }
201 }
202 }
203
204 pub fn connection_established(&mut self) -> AttemptFateOutcome {
206 self.record_open_fate(ReconnectAttemptFate::Connected)
207 }
208
209 pub fn open_failed(&mut self) -> AttemptFateOutcome {
212 self.record_open_fate(ReconnectAttemptFate::Failed)
213 }
214
215 fn record_open_fate(&mut self, fate: ReconnectAttemptFate) -> AttemptFateOutcome {
216 let Some(attempt) = self.attempt.take() else {
217 return AttemptFateOutcome::Refused(AttemptFateRefusal::NoOpenInProgress);
218 };
219 let aggregate = self.take_aggregate();
220 match record_attempt_fate(aggregate, attempt, fate) {
221 ReconnectAttemptFateDecision::Recorded(aggregate) => {
222 let state = aggregate.reconnect().state();
223 self.aggregate = aggregate;
224 AttemptFateOutcome::Recorded { state }
225 }
226 ReconnectAttemptFateDecision::Refused {
227 aggregate,
228 attempt,
229 reason,
230 ..
231 } => {
232 self.aggregate = aggregate;
233 self.attempt = Some(attempt);
234 AttemptFateOutcome::Refused(AttemptFateRefusal::Fate(reason))
235 }
236 }
237 }
238
239 pub fn established_terminal(&mut self, terminal: &TransportTerminal) -> LossRecordOutcome {
248 if self.reconnect_state() != ReconnectState::Online {
249 return LossRecordOutcome::Refused(LossRecordRefusal::NotEstablished);
250 }
251 let aggregate = self.take_aggregate();
252 match record_transport_fate(aggregate, EstablishedConnectionTransportFate::Lost) {
253 ReconnectPermitDecision::Permitted {
254 aggregate, permit, ..
255 } => {
256 self.aggregate = aggregate;
257 self.permit = Some(permit);
258 self.last_loss_diagnostic = Some(*terminal);
259 LossRecordOutcome::PermitRetained
260 }
261 ReconnectPermitDecision::Refused(refusal) => {
262 let reason = refusal.reason();
263 let (aggregate, _event) = refusal.into_parts();
264 self.aggregate = aggregate;
265 LossRecordOutcome::Refused(LossRecordRefusal::Permit(reason))
266 }
267 }
268 }
269
270 pub fn detach_send_lost(
273 &mut self,
274 correlation: ClientResponseCorrelation,
275 ) -> DetachLossOutcome {
276 let aggregate = self.take_aggregate();
277 match transport_fate(
278 aggregate,
279 correlation,
280 DetachTransportFate::ResponseUnavailable,
281 ) {
282 DetachTransportFateDecision::Parked(applied) => {
283 self.aggregate = applied.into_aggregate();
284 DetachLossOutcome::Parked
285 }
286 DetachTransportFateDecision::Refused(refusal) => {
287 let reason = refusal.reason();
288 let (aggregate, _input) = refusal.into_parts();
289 self.aggregate = aggregate;
290 DetachLossOutcome::Refused(reason)
291 }
292 }
293 }
294
295 const fn take_aggregate(&mut self) -> ClientParticipantAggregate {
298 core::mem::replace(&mut self.aggregate, ClientParticipantAggregate::new())
299 }
300}
301
302impl Default for WebSocketAuthorityBinding {
303 fn default() -> Self {
304 Self::new()
305 }
306}