Skip to main content

liminal_protocol/
client.rs

1//! Transport-agnostic client participant state and sealed effects.
2//!
3//! The client aggregate owns correlation, detach replay, and the durability
4//! barrier for one outstanding operation. The authorized round-3 order is
5//! commit-seal, persist the committed resume record, then release executable
6//! authority; pending has no durable form.
7//!
8//! # `LP-CLIENT-GOAL` API-shape rationale
9//!
10//! The Phase 1 brief requires authority-safe shapes rather than caller-owned
11//! state parts. The resulting public shape is deliberate:
12//!
13//! 1. Replay and reconnect transitions consume and return the root aggregate so
14//!    their facts are persisted atomically; standalone state-part recomposition
15//!    is not representable.
16//! 2. [`ReconnectAggregate`] has no public constructor because fresh detached
17//!    reconnect state would separate permit identity from participant facts.
18//! 3. [`recover_reconnect_permit`] exists because an unissued committed cold
19//!    record must release its permit once without making permits cloneable.
20//! 4. Only the commit seal exposes a durable record; pending has no encoder. The
21//!    authorized round-3 order is commit-seal, persist committed `LPCR`, release.
22//! 5. The retained name [`crate::outcome::ReconnectDelayResult`] carries an
23//!    event, never a delay: the brief explicitly supersedes timer scheduling.
24//! 6. A different retained detach is refused while live, but terminal or
25//!    attach-superseded replay yields only to an exact newer-generation detach.
26//! 7. Terminalized-detach fixture construction remains `cfg(test)` only; wire
27//!    authority construction is not weakened in production.
28//! 8. Detach recording is atomic with [`record_operation`], so no public raw
29//!    envelope-to-replay mint or second caller-owned persistence step exists.
30//! 9. [`DetachTransportFate`] is closed to response unavailability because this
31//!    protocol crate owns no socket, runtime, or transport handle.
32//! 10. [`DetachReplayOutcome`] is exhaustive so a generic server value cannot be
33//!     relabeled by a caller as one of the three terminal detach outcomes.
34//! 11. Reconnect fresh-event producers are separate typed functions rather than
35//!     a generic event-injection seam, limiting minting to the brief's classes.
36//! 12. [`recover_expected_operation`] is the one-use post-restore counterpart to
37//!     the pending-to-commit barrier; detach recovery also marks replay in flight.
38//! 13. Replay inspection returns `None` only for its named Empty state; terminal
39//!     payloads remain lossless and distinct.
40//! 14. There is no speculative persistence format. A pending-window crash means
41//!     the operation did not happen and restart may record it again.
42//! 15. `RecordAdmission` responses are always ambiguous because wire identity is
43//!     insufficient; `ObserverRecovery` compares its echoed list. As the M7
44//!     companion rule, both tokenless classes resolve as typed abandoned on every
45//!     restore and are never re-released. A later sealed-transport-context SDK leg
46//!     may add outbound attempt tokens and lift this restriction.
47//! 16. A crash or restore that destroys live authority mints a serialized,
48//!     take-once [`LostAuthorityTestimony`] with no public constructor
49//!     (`LP-CLIENT-GOAL` piece 4, r2, 2026-07-18). Every public fate or
50//!     recovery entry point consumes either a live one-use correlation, a
51//!     one-use sealed attempt authority, or the pending serialized testimony;
52//!     no publicly constructible process-fate value exists, so a fate that
53//!     consumes neither is unrepresentable at the API surface.
54//! 17. Detached bindings retain the attach secret because the complete client
55//!     record must remain capable of a later credential attach after restart.
56//! 18. Expected recovery and replay-start atomically share one detach issuance
57//!     bit, guaranteeing one first-send authority in either call order.
58//! 19. Both pending atoms use the retain-in-bytes encode shape (`LP-CLIENT-GOAL`
59//!     piece 2, r2, 2026-07-18): encoding an aggregate that holds a pending
60//!     testimony or abandonment emits bytes that carry the atom, so
61//!     encode-without-take loses nothing and no checkpoint window is refused.
62//!     The refuse-while-pending shape was rejected because piece 4 requires the
63//!     testimony to round-trip losslessly through the aggregate encoding.
64//! 20. Re-recording the retained detach envelope requires replay-status
65//!     compatibility with a fresh first send: only `Parked` admits; superseded,
66//!     Leave-superseded, terminal, and in-flight statuses refuse with typed
67//!     [`ClientOperationRecordRefusalReason::DetachReplayIncompatible`]
68//!     (r2, 2026-07-18), closing the round-4 door that revived expected-detach
69//!     authority over an inactive replay.
70//!
71//! # Exhaustive constructible-state audit
72//!
73//! Every state accepted by restore or reachable through a public apply path is
74//! listed here. The exhaustive conservation property test mechanically covers
75//! the **live authority** and **typed consumption** columns across 610 applied
76//! paths from a 4-operation × 11-action alphabet through depth 7, attempting
77//! every action at every step: 1041 attempts end in the crate's typed refusal
78//! and 1748 are unrepresentable because the consumed one-use value does not
79//! exist at the type level. Every row below is either reachable-and-tested or
80//! refused-by-construction with the refusing type named.
81//!
82//! | Owned state | Restore/apply acceptance | Live authority | Typed consumption / exit |
83//! |---|---|---:|---|
84//! | Binding `Unbound` | new, restore | 0 | enrollment/attach → `Bound`; identity-bound requests refuse |
85//! | Binding `Bound` | enrollment/attach, restore | 0 | detach result → `Detached`; Leave/Retired → `Left` |
86//! | Binding `Detached` | detach result, restore | 0 | exact-secret attach → `Bound`; Leave/Retired → `Left` |
87//! | Binding `Left` | correlated Leave/Retired, restore | 0 | permanent; inbound/outbound return `AlreadyDead` |
88//! | Expected `None` | new, abort, consumed outcome/fate, restore | 0 | one [`record_operation`] admission |
89//! | Tokenless expected, live process only | record/commit/release | 0 or 1 | exact response remains conservative; fate consumes; every restore converts it into the durable `TokenlessAfterCrash` abandonment atom below |
90//! | Token-bearing non-detach, unissued | committed `LPCR`, restore | 0 | [`recover_expected_operation`] issues once |
91//! | Token-bearing non-detach, issued | release/recovery, restore | 1 live, or serialized testimony after restore | correlated outcome/fate consumes the live correlation; restore mints the take-once testimony consumed only by [`resolve_lost_operation_authority`] |
92//! | Expected detach + replay `Parked` | commit, live fate, testimony resolution, restore only when exact and unissued | 0 | recovery or replay start issues exactly one effect |
93//! | Expected detach + replay `InFlight` | release/start, restore only when exact and issued | 1 live, or serialized testimony after restore | correlated outcome/fate consumes the live correlation; restore mints the take-once testimony whose [`resolve_lost_operation_authority`] resolution parks exact-token replay |
94//! | Expected detach + replay `Empty`, `Superseded`, `LeaveSuperseded`, or terminal | never accepted, unreachable by construction | 0 | refused at restore by `ClientResumeRestoreError::ExpectedDetachActiveReplayMismatch` and in live code by [`ClientOperationRecordRefusalReason::DetachReplayIncompatible`], which closes the only door that could re-couple an expected detach to an inactive replay |
95//! | Active replay without exact expected detach | never accepted | 0 | typed `ActiveReplayExpectedDetachMismatch` restore refusal |
96//! | Replay `Empty` | new, abort, restore without expected detach | 0 | admitted detach records `Parked` atomically |
97//! | Replay `Superseded` | authority-consuming matching attach, restore | 0 | old generation terminal; exact newer generation may replace; same-envelope re-record refused by construction (`DetachReplayIncompatible`) |
98//! | Replay `LeaveSuperseded` | authority-consuming matching durable Leave, restore | 0 | proves only that matching Leave/retirement superseded replay; public [`apply_leave_durable`] does **not** change binding to `Left`; same-envelope re-record refused by construction (`DetachReplayIncompatible`) |
99//! | Replay terminal (three exact payload arms) | authority-consuming exact outcome, restore | 0 | lossless terminal; exact newer-generation detach may replace; same-envelope re-record refused by construction (`DetachReplayIncompatible`) |
100//! | Restored-loss testimony, operation slot | minted only by validated restore of an issued token-bearing operation; carried losslessly by encode/decode | counts as the destroyed authority | consumed exactly once by [`resolve_lost_operation_authority`]; coupling refusals in both directions: a serialized atom without its issued state (or with the wrong kind) is refused by construction by `ClientResumeRestoreError::LostAuthorityTestimonyMismatch`, and every live consumption of the testified state is refused reachable-and-tested by the typed `LostAuthorityPending` reasons on [`decide_correlated_inbound`], [`record_expected_operation_fate`], [`transport_fate`], [`apply_attach`], [`apply_leave_durable`], and [`apply_detach_outcome`] |
101//! | Restored-loss testimony, reconnect slot | minted only by validated restore of an issued permit or in-progress attempt; carried losslessly by encode/decode | counts as the destroyed authority | consumed exactly once by [`resolve_lost_reconnect_authority`]; coupling refusals in both directions: a serialized atom without its matching machine state is refused by construction by `ClientResumeRestoreError::LostAuthorityTestimonyMismatch`, and prior-process handles are refused reachable-and-tested by `ReconnectAttemptRefusalReason::LostAuthorityPending` and `ReconnectAttemptFateRefusalReason::LostAuthorityPending` |
102//! | Pending `TokenlessAfterCrash` abandonment | minted by restore of a tokenless expected operation; carried losslessly by encode/decode with its issued flag | `was_issued` marks a destroyed authority | taken exactly once by [`ClientParticipantAggregate::take_restored_operation_abandonment`]; a second take observes nothing; coupling refusals in both directions: a serialized abandonment beside a tokenless expected operation is refused by construction by `ClientResumeRestoreError::PendingAbandonmentConflict`, and tokenless admission while pending is refused reachable-and-tested by [`ClientOperationRecordRefusalReason::AbandonmentPending`] |
103//! | Reconnect `Parked` | new/failure/loss resolution, restore | 0 | typed fresh event → permit |
104//! | Reconnect permit unissued | committed restore testimony | 0 | one [`recover_reconnect_permit`] issue |
105//! | Reconnect permit issued | event/recovery, restore | 1 live, or serialized testimony after restore | held permit → attempt; restore mints the take-once testimony consumed only by [`resolve_lost_reconnect_authority`] |
106//! | Reconnect attempt | permit redemption, restore | 1 live, or serialized testimony after restore | held fate → `Online`/`Parked`; restore mints the take-once testimony consumed only by [`resolve_lost_reconnect_authority`] |
107//! | Reconnect `Online` | successful fate, restore | 0 | later typed fresh event → permit |
108
109use crate::wire::{ClientRequest, Generation, ParticipantAckEnvelope};
110
111/// Coarse client binding state without exposing credential-bearing state parts.
112#[derive(Clone, Copy, Debug, PartialEq, Eq)]
113pub enum ClientBindingStatus {
114    /// No participant binding has been established.
115    Unbound,
116    /// A live binding and attach credential are retained.
117    Bound,
118    /// The most recently correlated detach completed.
119    Detached,
120    /// A correlated durable Leave permanently retired the participant.
121    Left,
122}
123
124#[derive(Clone, Debug, PartialEq, Eq)]
125pub(super) enum ClientBindingState {
126    Unbound,
127    Bound {
128        conversation_id: u64,
129        participant_id: u64,
130        generation: Generation,
131        attach_secret: crate::wire::AttachSecret,
132        binding_epoch: crate::wire::BindingEpoch,
133    },
134    Detached {
135        conversation_id: u64,
136        participant_id: u64,
137        generation: Generation,
138        attach_secret: crate::wire::AttachSecret,
139    },
140    Left {
141        conversation_id: u64,
142        participant_id: u64,
143        generation: Generation,
144    },
145}
146
147impl ClientBindingState {
148    const fn status(&self) -> ClientBindingStatus {
149        match self {
150            Self::Unbound => ClientBindingStatus::Unbound,
151            Self::Bound { .. } => ClientBindingStatus::Bound,
152            Self::Detached { .. } => ClientBindingStatus::Detached,
153            Self::Left { .. } => ClientBindingStatus::Left,
154        }
155    }
156
157    const fn is_left(&self) -> bool {
158        matches!(self, Self::Left { .. })
159    }
160
161    fn matches_ack(&self, request: &ParticipantAckEnvelope) -> bool {
162        match self {
163            Self::Bound {
164                conversation_id,
165                participant_id,
166                generation,
167                ..
168            } => {
169                *conversation_id == request.conversation_id
170                    && *participant_id == request.participant_id
171                    && *generation == request.capability_generation
172            }
173            Self::Unbound | Self::Detached { .. } | Self::Left { .. } => false,
174        }
175    }
176
177    fn accepts_request(&self, request: &ClientRequest) -> bool {
178        if self.is_left() {
179            return false;
180        }
181        match request {
182            ClientRequest::Enrollment(_) => matches!(self, Self::Unbound),
183            ClientRequest::CredentialAttach(value) => {
184                matches!(self, Self::Unbound)
185                    || self.matches_credential(
186                        value.conversation_id,
187                        value.participant_id,
188                        value.capability_generation,
189                        value.attach_secret,
190                        true,
191                    )
192            }
193            ClientRequest::Detach(value) => self.matches_identity(
194                value.conversation_id,
195                value.participant_id,
196                value.capability_generation,
197                false,
198            ),
199            ClientRequest::ParticipantAck(value) => self.matches_identity(
200                value.conversation_id,
201                value.participant_id,
202                value.capability_generation,
203                false,
204            ),
205            ClientRequest::Leave(value) => self.matches_credential(
206                value.conversation_id,
207                value.participant_id,
208                value.capability_generation,
209                value.attach_secret,
210                true,
211            ),
212            ClientRequest::MarkerAck(value) => self.matches_identity(
213                value.conversation_id,
214                value.participant_id,
215                value.capability_generation,
216                false,
217            ),
218            ClientRequest::RecordAdmission(value) => self.matches_identity(
219                value.conversation_id,
220                value.participant_id,
221                value.capability_generation,
222                false,
223            ),
224            ClientRequest::ObserverRecovery(_) => true,
225        }
226    }
227
228    fn matches_identity(
229        &self,
230        conversation: u64,
231        participant: u64,
232        generation_value: Generation,
233        allow_detached: bool,
234    ) -> bool {
235        match self {
236            Self::Bound {
237                conversation_id,
238                participant_id,
239                generation,
240                ..
241            } => {
242                (*conversation_id, *participant_id, *generation)
243                    == (conversation, participant, generation_value)
244            }
245            Self::Detached {
246                conversation_id,
247                participant_id,
248                generation,
249                ..
250            } if allow_detached => {
251                (*conversation_id, *participant_id, *generation)
252                    == (conversation, participant, generation_value)
253            }
254            Self::Unbound | Self::Detached { .. } | Self::Left { .. } => false,
255        }
256    }
257
258    fn matches_credential(
259        &self,
260        conversation: u64,
261        participant: u64,
262        generation_value: Generation,
263        presented_secret: crate::wire::AttachSecret,
264        allow_detached: bool,
265    ) -> bool {
266        match self {
267            Self::Bound { attach_secret, .. } => {
268                *attach_secret == presented_secret
269                    && self.matches_identity(
270                        conversation,
271                        participant,
272                        generation_value,
273                        allow_detached,
274                    )
275            }
276            Self::Detached { attach_secret, .. } if allow_detached => {
277                *attach_secret == presented_secret
278                    && self.matches_identity(conversation, participant, generation_value, true)
279            }
280            Self::Unbound | Self::Detached { .. } | Self::Left { .. } => false,
281        }
282    }
283}
284
285/// Closed description of which live process-local authority was destroyed.
286///
287/// The kind is descriptive only: no caller-suppliable kind value gates any
288/// authority transition (`LP-CLIENT-GOAL` piece 4, r2, 2026-07-18).
289#[derive(Clone, Copy, Debug, PartialEq, Eq)]
290pub enum LostAuthorityKind {
291    /// An issued operation's one-use response correlation did not survive the
292    /// process.
293    IssuedOperationCorrelation,
294    /// An in-flight detach transport attempt did not survive the process.
295    DetachTransportAttempt,
296    /// An issued reconnect attempt permit did not survive the process.
297    ReconnectPermit,
298    /// An in-progress reconnect attempt did not survive the process.
299    ReconnectAttempt,
300}
301
302/// Serialized, take-once testimony that live process-local authority was
303/// destroyed by crash or restore (`LP-CLIENT-GOAL` piece 4, r2, 2026-07-18).
304///
305/// The crate mints this atom exactly when a validated cold restore accepts a
306/// state whose live authority did not survive the process. It is persisted in
307/// the aggregate encoding, so encode/decode round-trips it losslessly, and it
308/// is consumed exactly once by the one recovery path that resolves the loss:
309/// [`resolve_lost_operation_authority`] for the operation domain and
310/// [`resolve_lost_reconnect_authority`] for the reconnect domain. It has no
311/// public constructor, so a fate value that consumes neither a live one-use
312/// correlation nor this testimony is unrepresentable at the API surface.
313///
314/// ```compile_fail
315/// use liminal_protocol::client::{LostAuthorityKind, LostAuthorityTestimony};
316/// let _forged = LostAuthorityTestimony {
317///     kind: LostAuthorityKind::ReconnectPermit,
318/// };
319/// ```
320#[derive(Debug, PartialEq, Eq)]
321pub struct LostAuthorityTestimony {
322    kind: LostAuthorityKind,
323}
324
325impl LostAuthorityTestimony {
326    pub(super) const fn mint(kind: LostAuthorityKind) -> Self {
327        Self { kind }
328    }
329
330    /// Reports which destroyed authority this testimony records.
331    #[must_use]
332    pub const fn kind(&self) -> LostAuthorityKind {
333        self.kind
334    }
335}
336
337#[derive(Debug, PartialEq, Eq)]
338pub(super) struct ExpectedOperationState {
339    pub(super) request: ClientRequest,
340    pub(super) issued: bool,
341    pub(super) authorization: u64,
342    pub(super) lost: Option<LostAuthorityTestimony>,
343}
344
345/// Why a persisted expected operation was deliberately not re-released.
346#[derive(Clone, Copy, Debug, PartialEq, Eq)]
347pub enum RestoredExpectedOperationAbandonmentReason {
348    /// The operation class has no outbound attempt token and cannot be proven
349    /// unsent after a crash.
350    TokenlessAfterCrash,
351}
352
353/// Typed restore resolution for an operation that cannot safely be re-issued.
354///
355/// The abandonment is durable (`LP-CLIENT-GOAL` piece 4, r2, 2026-07-18): it
356/// is serialized in the aggregate encoding, survives encode-without-take, and
357/// is consumed exactly once by
358/// [`ClientParticipantAggregate::take_restored_operation_abandonment`].
359#[derive(Debug, PartialEq, Eq)]
360pub struct RestoredExpectedOperationAbandonment {
361    pub(super) request: ClientRequest,
362    pub(super) reason: RestoredExpectedOperationAbandonmentReason,
363    pub(super) was_issued: bool,
364}
365
366impl RestoredExpectedOperationAbandonment {
367    /// Borrows the exact operation the restore boundary abandoned.
368    #[must_use]
369    pub const fn request(&self) -> &ClientRequest {
370        &self.request
371    }
372
373    /// Reports the closed abandonment reason.
374    #[must_use]
375    pub const fn reason(&self) -> RestoredExpectedOperationAbandonmentReason {
376        self.reason
377    }
378
379    /// Reports whether the abandoned operation had been issued before the
380    /// crash, meaning a live send authority was destroyed with the process.
381    #[must_use]
382    pub const fn was_issued(&self) -> bool {
383        self.was_issued
384    }
385
386    /// Consumes the resolution into the request callers may explicitly re-record.
387    #[must_use]
388    pub fn into_request(self) -> ClientRequest {
389        self.request
390    }
391}
392
393/// Non-cloneable client participant state shell.
394///
395/// Its expected operation, credential-bearing binding, replay request, and
396/// reconnect state are private so callers must delegate every decision. This
397/// brief-required root ownership prevents callers from recombining independently
398/// persisted authorities into a state the crate never validated.
399#[derive(Debug, PartialEq, Eq)]
400pub struct ClientParticipantAggregate {
401    pub(super) binding: ClientBindingState,
402    pub(super) expected: Option<ExpectedOperationState>,
403    pub(super) next_operation_authorization: u64,
404    pub(super) detach_replay: SdkDetachReplayAggregate,
405    pub(super) reconnect: ReconnectAggregate,
406    pub(super) restored_abandonment: Option<RestoredExpectedOperationAbandonment>,
407}
408
409impl ClientParticipantAggregate {
410    /// Creates a fresh unbound client aggregate.
411    #[must_use]
412    pub const fn new() -> Self {
413        Self {
414            binding: ClientBindingState::Unbound,
415            expected: None,
416            next_operation_authorization: 0,
417            detach_replay: SdkDetachReplayAggregate::new(),
418            reconnect: ReconnectAggregate::new(),
419            restored_abandonment: None,
420        }
421    }
422
423    /// Reports binding status without exposing credential-bearing state.
424    #[must_use]
425    pub const fn binding_status(&self) -> ClientBindingStatus {
426        self.binding.status()
427    }
428
429    /// Reports whether one write-ahead operation is outstanding.
430    #[must_use]
431    pub const fn has_expected_operation(&self) -> bool {
432        self.expected.is_some()
433    }
434
435    /// Borrows the detach replay aggregate for status inspection.
436    #[must_use]
437    pub const fn detach_replay(&self) -> &SdkDetachReplayAggregate {
438        &self.detach_replay
439    }
440
441    /// Borrows the reconnect aggregate for status inspection.
442    #[must_use]
443    pub const fn reconnect(&self) -> &ReconnectAggregate {
444        &self.reconnect
445    }
446
447    /// Takes the typed tokenless-operation resolution produced by cold restore.
448    ///
449    /// The expected slot is already empty when this value exists; taking the
450    /// event cannot mint or release executable authority. The abandonment is
451    /// durable until taken: encode-without-take retains it, and a second take
452    /// observes nothing (`LP-CLIENT-GOAL` piece 4, r2, 2026-07-18).
453    #[must_use]
454    pub const fn take_restored_operation_abandonment(
455        &mut self,
456    ) -> Option<RestoredExpectedOperationAbandonment> {
457        self.restored_abandonment.take()
458    }
459
460    /// Borrows the pending tokenless abandonment without consuming it.
461    #[must_use]
462    pub const fn restored_operation_abandonment(
463        &self,
464    ) -> Option<&RestoredExpectedOperationAbandonment> {
465        self.restored_abandonment.as_ref()
466    }
467
468    /// Borrows the pending operation-domain lost-authority testimony, if any.
469    ///
470    /// While this testimony is pending, every correlation-consuming operation
471    /// path refuses with a typed lost-authority reason; only
472    /// [`resolve_lost_operation_authority`] consumes it.
473    #[must_use]
474    pub const fn lost_operation_testimony(&self) -> Option<&LostAuthorityTestimony> {
475        match &self.expected {
476            Some(expected) => expected.lost.as_ref(),
477            None => None,
478        }
479    }
480
481    /// Borrows the pending reconnect-domain lost-authority testimony, if any.
482    ///
483    /// While this testimony is pending, permit redemption and attempt fates
484    /// refuse with a typed lost-authority reason; only
485    /// [`resolve_lost_reconnect_authority`] consumes it.
486    #[must_use]
487    pub const fn lost_reconnect_testimony(&self) -> Option<&LostAuthorityTestimony> {
488        self.reconnect.lost.as_ref()
489    }
490
491    pub(super) const fn operation_loss_pending(&self) -> bool {
492        self.lost_operation_testimony().is_some()
493    }
494}
495
496impl Default for ClientParticipantAggregate {
497    fn default() -> Self {
498        Self::new()
499    }
500}
501
502mod barrier;
503mod correlation;
504mod inbound;
505mod reconnect;
506mod replay;
507mod resume;
508mod resume_decode;
509mod resume_encode;
510
511pub use barrier::*;
512pub use inbound::{
513    ClientCorrelatedInboundDecision, ClientCorrelatedInboundRefusal, ClientInboundApplied,
514    ClientInboundDecision, ClientInboundRefusal, ClientInboundRefusalReason,
515    decide_correlated_inbound, decide_inbound,
516};
517pub use reconnect::{
518    EstablishedConnectionTransportFate, ExplicitReconnectAction, LostReconnectAuthorityDecision,
519    ProvedOnlineTransition, ReconnectAggregate, ReconnectAttemptDecision, ReconnectAttemptFate,
520    ReconnectAttemptFateDecision, ReconnectAttemptFateRefusalReason, ReconnectAttemptPermit,
521    ReconnectAttemptRefusalReason, ReconnectFreshEvent, ReconnectInProgressAttempt,
522    ReconnectPermitDecision, ReconnectPermitRefusal, ReconnectPermitRefusalReason,
523    RecoveredReconnectPermitDecision, record_attempt_fate, record_explicit_reconnect,
524    record_online_transition, record_transport_fate, recover_reconnect_permit, redeem_attempt,
525    resolve_lost_reconnect_authority,
526};
527pub use replay::{
528    ApplyAttachDecision, ApplyDetachOutcomeDecision, ApplyLeaveDecision, DetachReplayApplied,
529    DetachReplayOutcome, DetachReplayRefusal, DetachReplayRefusalReason, DetachReplayStatus,
530    DetachReplayTerminal, DetachTransportAttempt, DetachTransportAttemptDecision,
531    DetachTransportFate, DetachTransportFateDecision, SdkDetachReplayAggregate, apply_attach,
532    apply_detach_outcome, apply_leave_durable, transport_attempt_started, transport_fate,
533};
534pub use resume::{
535    ClientResumeRecord, ClientResumeRecordDecodeError, ClientResumeRecordEncodeError,
536    ClientResumeRecordSection, ClientResumeRestoreError,
537};
538
539#[cfg(test)]
540mod authority_property_tests;
541#[cfg(test)]
542mod d1_flip_tests;
543#[cfg(test)]
544mod r2_tests;
545#[cfg(test)]
546mod resume_tests;
547#[cfg(test)]
548mod review_tests;
549#[cfg(test)]
550mod rider_tests;
551#[cfg(test)]
552mod round3_tests;
553#[cfg(test)]
554mod round4_tests;
555#[cfg(test)]
556mod tests;