Skip to main content

liminal_protocol/lifecycle/
binding.rs

1use crate::outcome::CandidatePhase;
2use crate::wire::{
3    BindingEpoch, CloseCause, ConversationId, DeliverySeq, DetachedCause, DiedCause, ParticipantId,
4    ParticipantIndex, TransactionOrder,
5};
6
7/// Stable admission ordering key for one lifecycle candidate.
8///
9/// The phase is the canonical typed protocol phase rather than a free integer.
10/// Binding-terminal transitions below additionally fix it to
11/// [`CandidatePhase::BindingTerminal`] and derive the participant index from
12/// the participant identifier, which is exactly that permanent index in v1.
13#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
14pub struct AdmissionOrder {
15    transaction_order: TransactionOrder,
16    candidate_phase: CandidatePhase,
17    participant_index: ParticipantIndex,
18}
19
20impl AdmissionOrder {
21    /// Creates an order for a typed lifecycle candidate.
22    #[must_use]
23    pub const fn new(
24        transaction_order: TransactionOrder,
25        candidate_phase: CandidatePhase,
26        participant_index: ParticipantIndex,
27    ) -> Self {
28        Self {
29            transaction_order,
30            candidate_phase,
31            participant_index,
32        }
33    }
34
35    /// Creates the only order shape valid for a binding terminal.
36    #[must_use]
37    pub const fn binding_terminal(
38        transaction_order: TransactionOrder,
39        participant_id: ParticipantId,
40    ) -> Self {
41        Self::new(
42            transaction_order,
43            CandidatePhase::BindingTerminal,
44            participant_id,
45        )
46    }
47
48    /// Returns the conversation transaction-order major.
49    #[must_use]
50    pub const fn transaction_order(self) -> TransactionOrder {
51        self.transaction_order
52    }
53
54    /// Returns the canonical candidate phase.
55    #[must_use]
56    pub const fn candidate_phase(self) -> CandidatePhase {
57        self.candidate_phase
58    }
59
60    /// Returns the permanent participant-index tie-breaker.
61    #[must_use]
62    pub const fn participant_index(self) -> ParticipantIndex {
63        self.participant_index
64    }
65}
66
67/// Active binding authority.
68#[derive(Clone, Copy, Debug, PartialEq, Eq)]
69pub struct ActiveBinding {
70    /// Bound participant; in v1 this value is also its permanent participant index.
71    pub participant_id: ParticipantId,
72    /// Bound conversation.
73    pub conversation_id: ConversationId,
74    /// Immutable current binding epoch.
75    pub binding_epoch: BindingEpoch,
76}
77
78/// A durable binding-terminal record was appended in the fate transaction.
79#[derive(Clone, Copy, Debug, PartialEq, Eq)]
80pub struct CommittedBindingTerminalPosition {
81    transaction_order: TransactionOrder,
82    delivery_seq: DeliverySeq,
83}
84
85impl CommittedBindingTerminalPosition {
86    /// Creates the assigned major and delivery key for an appended terminal.
87    #[must_use]
88    pub const fn new(transaction_order: TransactionOrder, delivery_seq: DeliverySeq) -> Self {
89        Self {
90            transaction_order,
91            delivery_seq,
92        }
93    }
94
95    /// Returns the assigned conversation transaction order.
96    #[must_use]
97    pub const fn transaction_order(self) -> TransactionOrder {
98        self.transaction_order
99    }
100
101    /// Returns the committed terminal record's delivery sequence.
102    #[must_use]
103    pub const fn delivery_seq(self) -> DeliverySeq {
104        self.delivery_seq
105    }
106}
107
108/// A binding fate was durably accepted but its terminal append remains pending.
109#[derive(Clone, Copy, Debug, PartialEq, Eq)]
110pub struct PendingBindingTerminalPosition {
111    transaction_order: TransactionOrder,
112}
113
114impl PendingBindingTerminalPosition {
115    /// Creates the immutable major reserved for a pending binding terminal.
116    #[must_use]
117    pub const fn new(transaction_order: TransactionOrder) -> Self {
118        Self { transaction_order }
119    }
120
121    /// Returns the assigned conversation transaction order.
122    #[must_use]
123    pub const fn transaction_order(self) -> TransactionOrder {
124        self.transaction_order
125    }
126}
127
128/// Durable placement selected by an unrefusable binding-fate transaction.
129#[derive(Clone, Copy, Debug, PartialEq, Eq)]
130pub enum BindingTerminalDisposition {
131    /// The terminal record committed in the source transaction.
132    Committed(CommittedBindingTerminalPosition),
133    /// The exact terminal remains in the bounded pending binding slot.
134    Pending(PendingBindingTerminalPosition),
135}
136
137#[derive(Clone, Copy, Debug, PartialEq, Eq)]
138struct BindingTerminalIdentity {
139    participant_id: ParticipantId,
140    conversation_id: ConversationId,
141    binding_epoch: BindingEpoch,
142    admission_order: AdmissionOrder,
143}
144
145impl BindingTerminalIdentity {
146    const fn from_active(binding: ActiveBinding, transaction_order: TransactionOrder) -> Self {
147        Self {
148            participant_id: binding.participant_id,
149            conversation_id: binding.conversation_id,
150            binding_epoch: binding.binding_epoch,
151            admission_order: AdmissionOrder::binding_terminal(
152                transaction_order,
153                binding.participant_id,
154            ),
155        }
156    }
157}
158
159/// Appended `Detached` terminal with a cause valid for that record class.
160#[derive(Clone, Copy, Debug, PartialEq, Eq)]
161pub struct CommittedDetachedTerminal {
162    identity: BindingTerminalIdentity,
163    cause: DetachedCause,
164    delivery_seq: DeliverySeq,
165}
166
167impl CommittedDetachedTerminal {
168    /// Returns the permanent participant identifier/index.
169    #[must_use]
170    pub const fn participant_id(self) -> ParticipantId {
171        self.identity.participant_id
172    }
173
174    /// Returns the owning conversation.
175    #[must_use]
176    pub const fn conversation_id(self) -> ConversationId {
177        self.identity.conversation_id
178    }
179
180    /// Returns the exact ended binding epoch.
181    #[must_use]
182    pub const fn binding_epoch(self) -> BindingEpoch {
183        self.identity.binding_epoch
184    }
185
186    /// Returns the type-restricted `Detached` cause.
187    #[must_use]
188    pub const fn cause(self) -> DetachedCause {
189        self.cause
190    }
191
192    /// Returns the typed binding-terminal admission position.
193    #[must_use]
194    pub const fn admission_order(self) -> AdmissionOrder {
195        self.identity.admission_order
196    }
197
198    /// Returns the committed lifecycle delivery sequence.
199    #[must_use]
200    pub const fn delivery_seq(self) -> DeliverySeq {
201        self.delivery_seq
202    }
203}
204
205/// Appended `Died` terminal with a cause valid for that record class.
206#[derive(Clone, Copy, Debug, PartialEq, Eq)]
207pub struct CommittedDiedTerminal {
208    identity: BindingTerminalIdentity,
209    cause: DiedCause,
210    delivery_seq: DeliverySeq,
211}
212
213impl CommittedDiedTerminal {
214    /// Returns the permanent participant identifier/index.
215    #[must_use]
216    pub const fn participant_id(self) -> ParticipantId {
217        self.identity.participant_id
218    }
219
220    /// Returns the owning conversation.
221    #[must_use]
222    pub const fn conversation_id(self) -> ConversationId {
223        self.identity.conversation_id
224    }
225
226    /// Returns the exact ended binding epoch.
227    #[must_use]
228    pub const fn binding_epoch(self) -> BindingEpoch {
229        self.identity.binding_epoch
230    }
231
232    /// Returns the type-restricted `Died` cause.
233    #[must_use]
234    pub const fn cause(self) -> DiedCause {
235        self.cause
236    }
237
238    /// Returns the typed binding-terminal admission position.
239    #[must_use]
240    pub const fn admission_order(self) -> AdmissionOrder {
241        self.identity.admission_order
242    }
243
244    /// Returns the committed lifecycle delivery sequence.
245    #[must_use]
246    pub const fn delivery_seq(self) -> DeliverySeq {
247        self.delivery_seq
248    }
249}
250
251/// Cause-partitioned durable binding-terminal record.
252///
253/// The variant determines the allowed cause domain, so no independent record
254/// kind can disagree with the stored cause.
255#[derive(Clone, Copy, Debug, PartialEq, Eq)]
256pub enum CommittedBindingTerminal {
257    /// An appended `Detached` terminal.
258    Detached(CommittedDetachedTerminal),
259    /// An appended `Died` terminal.
260    Died(CommittedDiedTerminal),
261}
262
263impl CommittedBindingTerminal {
264    /// Returns the permanent participant identifier/index.
265    #[must_use]
266    pub const fn participant_id(self) -> ParticipantId {
267        match self {
268            Self::Detached(value) => value.participant_id(),
269            Self::Died(value) => value.participant_id(),
270        }
271    }
272
273    /// Returns the owning conversation.
274    #[must_use]
275    pub const fn conversation_id(self) -> ConversationId {
276        match self {
277            Self::Detached(value) => value.conversation_id(),
278            Self::Died(value) => value.conversation_id(),
279        }
280    }
281
282    /// Returns the exact ended binding epoch.
283    #[must_use]
284    pub const fn binding_epoch(self) -> BindingEpoch {
285        match self {
286            Self::Detached(value) => value.binding_epoch(),
287            Self::Died(value) => value.binding_epoch(),
288        }
289    }
290
291    /// Returns the typed binding-terminal admission position.
292    #[must_use]
293    pub const fn admission_order(self) -> AdmissionOrder {
294        match self {
295            Self::Detached(value) => value.admission_order(),
296            Self::Died(value) => value.admission_order(),
297        }
298    }
299
300    /// Returns the committed lifecycle delivery sequence.
301    #[must_use]
302    pub const fn delivery_seq(self) -> DeliverySeq {
303        match self {
304            Self::Detached(value) => value.delivery_seq(),
305            Self::Died(value) => value.delivery_seq(),
306        }
307    }
308
309    /// Returns the record class derived from the cause-partitioned variant.
310    #[must_use]
311    pub const fn kind(self) -> BindingTerminalKind {
312        match self {
313            Self::Detached(_) => BindingTerminalKind::Detached,
314            Self::Died(_) => BindingTerminalKind::Died,
315        }
316    }
317
318    /// Returns the shared seven-class close-cause view.
319    #[must_use]
320    pub const fn close_cause(self) -> CloseCause {
321        match self {
322            Self::Detached(value) => value.cause().close_cause(),
323            Self::Died(value) => value.cause().close_cause(),
324        }
325    }
326
327    /// Returns the restricted `Detached` cause, or `None` for a `Died` record.
328    #[must_use]
329    pub const fn detached_cause(self) -> Option<DetachedCause> {
330        match self {
331            Self::Detached(value) => Some(value.cause()),
332            Self::Died(_) => None,
333        }
334    }
335
336    /// Returns the restricted `Died` cause, or `None` for a `Detached` record.
337    #[must_use]
338    pub const fn died_cause(self) -> Option<DiedCause> {
339        match self {
340            Self::Detached(_) => None,
341            Self::Died(value) => Some(value.cause()),
342        }
343    }
344}
345
346impl From<CommittedDetachedTerminal> for CommittedBindingTerminal {
347    fn from(value: CommittedDetachedTerminal) -> Self {
348        Self::Detached(value)
349    }
350}
351
352impl From<CommittedDiedTerminal> for CommittedBindingTerminal {
353    fn from(value: CommittedDiedTerminal) -> Self {
354        Self::Died(value)
355    }
356}
357
358/// Pending `Detached` terminal with no possible `Died`-only cause.
359#[derive(Clone, Copy, Debug, PartialEq, Eq)]
360pub struct PendingDetachedFinalization {
361    identity: BindingTerminalIdentity,
362    cause: DetachedCause,
363}
364
365impl PendingDetachedFinalization {
366    /// Returns the permanent participant identifier/index.
367    #[must_use]
368    pub const fn participant_id(self) -> ParticipantId {
369        self.identity.participant_id
370    }
371
372    /// Returns the owning conversation.
373    #[must_use]
374    pub const fn conversation_id(self) -> ConversationId {
375        self.identity.conversation_id
376    }
377
378    /// Returns the exact ended binding epoch.
379    #[must_use]
380    pub const fn binding_epoch(self) -> BindingEpoch {
381        self.identity.binding_epoch
382    }
383
384    /// Returns the type-restricted `Detached` cause.
385    #[must_use]
386    pub const fn cause(self) -> DetachedCause {
387        self.cause
388    }
389
390    /// Returns the immutable binding-terminal admission position.
391    #[must_use]
392    pub const fn admission_order(self) -> AdmissionOrder {
393        self.identity.admission_order
394    }
395
396    /// Commits the exact pending terminal at its assigned delivery sequence.
397    #[must_use]
398    pub const fn commit(self, delivery_seq: DeliverySeq) -> CommittedDetachedTerminal {
399        CommittedDetachedTerminal {
400            identity: self.identity,
401            cause: self.cause,
402            delivery_seq,
403        }
404    }
405}
406
407/// Pending `Died` terminal with no possible `Detached`-only cause.
408#[derive(Clone, Copy, Debug, PartialEq, Eq)]
409pub struct PendingDiedFinalization {
410    identity: BindingTerminalIdentity,
411    cause: DiedCause,
412}
413
414impl PendingDiedFinalization {
415    /// Returns the permanent participant identifier/index.
416    #[must_use]
417    pub const fn participant_id(self) -> ParticipantId {
418        self.identity.participant_id
419    }
420
421    /// Returns the owning conversation.
422    #[must_use]
423    pub const fn conversation_id(self) -> ConversationId {
424        self.identity.conversation_id
425    }
426
427    /// Returns the exact ended binding epoch.
428    #[must_use]
429    pub const fn binding_epoch(self) -> BindingEpoch {
430        self.identity.binding_epoch
431    }
432
433    /// Returns the type-restricted `Died` cause.
434    #[must_use]
435    pub const fn cause(self) -> DiedCause {
436        self.cause
437    }
438
439    /// Returns the immutable binding-terminal admission position.
440    #[must_use]
441    pub const fn admission_order(self) -> AdmissionOrder {
442        self.identity.admission_order
443    }
444
445    /// Commits the exact pending terminal at its assigned delivery sequence.
446    #[must_use]
447    pub const fn commit(self, delivery_seq: DeliverySeq) -> CommittedDiedTerminal {
448        CommittedDiedTerminal {
449            identity: self.identity,
450            cause: self.cause,
451            delivery_seq,
452        }
453    }
454}
455
456/// Binding-terminal lifecycle record kind, derived from cause-partitioned state.
457#[derive(Clone, Copy, Debug, PartialEq, Eq)]
458pub enum BindingTerminalKind {
459    /// Clean/supersession/shutdown `Detached` record.
460    Detached,
461    /// Unexpected `Died` record.
462    Died,
463}
464
465/// Binding authority ended, but its terminal record awaits durable append.
466///
467/// Both variants have private, transition-derived fields. Consequently a
468/// `Detached` finalization cannot carry `ConnectionLost`, `ProcessKilled`,
469/// `ProtocolError`, or `UncleanServerRestart`, and a `Died` finalization cannot
470/// carry `CleanDeregister`, `Superseded`, or `ServerShutdown`.
471#[derive(Clone, Copy, Debug, PartialEq, Eq)]
472pub enum PendingFinalization {
473    /// A pending `Detached` record and its restricted cause.
474    Detached(PendingDetachedFinalization),
475    /// A pending `Died` record and its restricted cause.
476    Died(PendingDiedFinalization),
477}
478
479impl PendingFinalization {
480    /// Returns the permanent participant identifier/index.
481    #[must_use]
482    pub const fn participant_id(self) -> ParticipantId {
483        match self {
484            Self::Detached(value) => value.participant_id(),
485            Self::Died(value) => value.participant_id(),
486        }
487    }
488
489    /// Returns the owning conversation.
490    #[must_use]
491    pub const fn conversation_id(self) -> ConversationId {
492        match self {
493            Self::Detached(value) => value.conversation_id(),
494            Self::Died(value) => value.conversation_id(),
495        }
496    }
497
498    /// Returns the exact ended binding epoch.
499    #[must_use]
500    pub const fn binding_epoch(self) -> BindingEpoch {
501        match self {
502            Self::Detached(value) => value.binding_epoch(),
503            Self::Died(value) => value.binding_epoch(),
504        }
505    }
506
507    /// Returns the immutable binding-terminal admission position.
508    #[must_use]
509    pub const fn admission_order(self) -> AdmissionOrder {
510        match self {
511            Self::Detached(value) => value.admission_order(),
512            Self::Died(value) => value.admission_order(),
513        }
514    }
515
516    /// Returns the record class derived from the cause-partitioned variant.
517    #[must_use]
518    pub const fn kind(self) -> BindingTerminalKind {
519        match self {
520            Self::Detached(_) => BindingTerminalKind::Detached,
521            Self::Died(_) => BindingTerminalKind::Died,
522        }
523    }
524
525    /// Returns the shared seven-class close-cause view.
526    #[must_use]
527    pub const fn close_cause(self) -> CloseCause {
528        match self {
529            Self::Detached(value) => value.cause().close_cause(),
530            Self::Died(value) => value.cause().close_cause(),
531        }
532    }
533
534    /// Returns the restricted `Detached` cause, or `None` for a `Died` state.
535    #[must_use]
536    pub const fn detached_cause(self) -> Option<DetachedCause> {
537        match self {
538            Self::Detached(value) => Some(value.cause()),
539            Self::Died(_) => None,
540        }
541    }
542
543    /// Returns the restricted `Died` cause, or `None` for a `Detached` state.
544    #[must_use]
545    pub const fn died_cause(self) -> Option<DiedCause> {
546        match self {
547            Self::Detached(_) => None,
548            Self::Died(value) => Some(value.cause()),
549        }
550    }
551
552    /// Commits the exact pending terminal without changing cause, epoch, or order.
553    #[must_use]
554    pub const fn commit(self, delivery_seq: DeliverySeq) -> CommittedBindingTerminal {
555        match self {
556            Self::Detached(value) => CommittedBindingTerminal::Detached(value.commit(delivery_seq)),
557            Self::Died(value) => CommittedBindingTerminal::Died(value.commit(delivery_seq)),
558        }
559    }
560}
561
562impl From<PendingDetachedFinalization> for PendingFinalization {
563    fn from(value: PendingDetachedFinalization) -> Self {
564        Self::Detached(value)
565    }
566}
567
568impl From<PendingDiedFinalization> for PendingFinalization {
569    fn from(value: PendingDiedFinalization) -> Self {
570        Self::Died(value)
571    }
572}
573
574/// Result of a fate that records a `Detached` lifecycle terminal.
575#[derive(Clone, Copy, Debug, PartialEq, Eq)]
576pub enum DetachedBindingTransition {
577    /// Terminal record committed with its durable delivery key.
578    Committed(CommittedDetachedTerminal),
579    /// Authority ended and the exact terminal remains bounded-pending.
580    Pending(PendingDetachedFinalization),
581}
582
583impl DetachedBindingTransition {
584    /// Returns the post-transition binding slot.
585    #[must_use]
586    pub const fn binding_state(self) -> BindingState {
587        match self {
588            Self::Committed(_) => BindingState::Detached,
589            Self::Pending(value) => {
590                BindingState::PendingFinalization(PendingFinalization::Detached(value))
591            }
592        }
593    }
594}
595
596/// Result of a fate that records a `Died` lifecycle terminal.
597#[derive(Clone, Copy, Debug, PartialEq, Eq)]
598pub enum DiedBindingTransition {
599    /// Terminal record committed with its durable delivery key.
600    Committed(CommittedDiedTerminal),
601    /// Authority ended and the exact terminal remains bounded-pending.
602    Pending(PendingDiedFinalization),
603}
604
605impl DiedBindingTransition {
606    /// Projects a committed Died terminal; pending finalization has no progress yet.
607    #[must_use]
608    pub const fn observer_progress_projection(&self) -> Option<super::ObserverProgressProjection> {
609        let Self::Committed(terminal) = self else {
610            return None;
611        };
612        Some(super::ObserverProgressProjection::new(
613            terminal.conversation_id(),
614            terminal.delivery_seq(),
615        ))
616    }
617
618    /// Returns the post-transition binding slot.
619    #[must_use]
620    pub const fn binding_state(self) -> BindingState {
621        match self {
622            Self::Committed(_) => BindingState::Detached,
623            Self::Pending(value) => {
624                BindingState::PendingFinalization(PendingFinalization::Died(value))
625            }
626        }
627    }
628}
629
630impl ActiveBinding {
631    const fn finish_detached(
632        self,
633        cause: DetachedCause,
634        disposition: BindingTerminalDisposition,
635    ) -> DetachedBindingTransition {
636        match disposition {
637            BindingTerminalDisposition::Committed(position) => {
638                DetachedBindingTransition::Committed(CommittedDetachedTerminal {
639                    identity: BindingTerminalIdentity::from_active(
640                        self,
641                        position.transaction_order,
642                    ),
643                    cause,
644                    delivery_seq: position.delivery_seq,
645                })
646            }
647            BindingTerminalDisposition::Pending(position) => {
648                DetachedBindingTransition::Pending(PendingDetachedFinalization {
649                    identity: BindingTerminalIdentity::from_active(
650                        self,
651                        position.transaction_order,
652                    ),
653                    cause,
654                })
655            }
656        }
657    }
658
659    const fn finish_died(
660        self,
661        cause: DiedCause,
662        disposition: BindingTerminalDisposition,
663    ) -> DiedBindingTransition {
664        match disposition {
665            BindingTerminalDisposition::Committed(position) => {
666                DiedBindingTransition::Committed(CommittedDiedTerminal {
667                    identity: BindingTerminalIdentity::from_active(
668                        self,
669                        position.transaction_order,
670                    ),
671                    cause,
672                    delivery_seq: position.delivery_seq,
673                })
674            }
675            BindingTerminalDisposition::Pending(position) => {
676                DiedBindingTransition::Pending(PendingDiedFinalization {
677                    identity: BindingTerminalIdentity::from_active(
678                        self,
679                        position.transaction_order,
680                    ),
681                    cause,
682                })
683            }
684        }
685    }
686
687    /// Ends authority for explicit detach or clean protocol Disconnect.
688    #[must_use]
689    pub const fn clean_deregister(
690        self,
691        disposition: BindingTerminalDisposition,
692    ) -> DetachedBindingTransition {
693        self.finish_detached(DetachedCause::CleanDeregister, disposition)
694    }
695
696    /// Commits an explicit detach terminal at its assigned durable position.
697    #[must_use]
698    pub const fn commit_clean_deregister(
699        self,
700        position: CommittedBindingTerminalPosition,
701    ) -> CommittedDetachedTerminal {
702        CommittedDetachedTerminal {
703            identity: BindingTerminalIdentity::from_active(self, position.transaction_order),
704            cause: DetachedCause::CleanDeregister,
705            delivery_seq: position.delivery_seq,
706        }
707    }
708
709    /// Ends explicit-detach authority while retaining its exact pending terminal.
710    #[must_use]
711    pub const fn pending_clean_deregister(
712        self,
713        position: PendingBindingTerminalPosition,
714    ) -> PendingDetachedFinalization {
715        PendingDetachedFinalization {
716            identity: BindingTerminalIdentity::from_active(self, position.transaction_order),
717            cause: DetachedCause::CleanDeregister,
718        }
719    }
720
721    /// Ends every binding still active when a clean protocol Disconnect arrives.
722    #[must_use]
723    pub const fn clean_disconnect(
724        self,
725        disposition: BindingTerminalDisposition,
726    ) -> DetachedBindingTransition {
727        self.clean_deregister(disposition)
728    }
729
730    /// Commits the old terminal in an authorized superseding attach handoff.
731    ///
732    /// Supersession is an optional new-major producer and cannot leave only the
733    /// old terminal pending; the enclosing attach transaction either appends
734    /// the ordered `Detached(Superseded)`/`Attached` pair or commits nothing.
735    #[must_use]
736    pub const fn superseded(
737        self,
738        position: CommittedBindingTerminalPosition,
739    ) -> CommittedDetachedTerminal {
740        CommittedDetachedTerminal {
741            identity: BindingTerminalIdentity::from_active(self, position.transaction_order),
742            cause: DetachedCause::Superseded,
743            delivery_seq: position.delivery_seq,
744        }
745    }
746
747    /// Ends authority during an orderly server shutdown.
748    #[must_use]
749    pub const fn server_shutdown(
750        self,
751        disposition: BindingTerminalDisposition,
752    ) -> DetachedBindingTransition {
753        self.finish_detached(DetachedCause::ServerShutdown, disposition)
754    }
755
756    /// Ends authority after TCP/keepalive/read/write connection loss.
757    #[must_use]
758    pub const fn connection_lost(
759        self,
760        disposition: BindingTerminalDisposition,
761    ) -> DiedBindingTransition {
762        self.finish_died(DiedCause::ConnectionLost, disposition)
763    }
764
765    /// Ends authority after trapped linked-EXIT or known forced termination.
766    #[must_use]
767    pub const fn process_killed(
768        self,
769        disposition: BindingTerminalDisposition,
770    ) -> DiedBindingTransition {
771        self.finish_died(DiedCause::ProcessKilled, disposition)
772    }
773
774    /// Ends authority after a terminating decode or protocol-state refusal.
775    #[must_use]
776    pub const fn protocol_error(
777        self,
778        disposition: BindingTerminalDisposition,
779    ) -> DiedBindingTransition {
780        self.finish_died(DiedCause::ProtocolError, disposition)
781    }
782
783    /// Recovers a durably active epoch owned by a prior server incarnation.
784    ///
785    /// The prior incarnation in the `Died` cause is derived from the exact old
786    /// binding epoch, so callers cannot pair the state with a different value.
787    #[must_use]
788    pub const fn unclean_server_restart(
789        self,
790        disposition: BindingTerminalDisposition,
791    ) -> DiedBindingTransition {
792        self.finish_died(
793            DiedCause::UncleanServerRestart {
794                prior_server_incarnation: self
795                    .binding_epoch
796                    .connection_incarnation
797                    .server_incarnation,
798            },
799            disposition,
800        )
801    }
802}
803
804/// Binding-slot state; pending finalization carries no live authority.
805#[derive(Clone, Copy, Debug, PartialEq, Eq)]
806pub enum BindingState {
807    /// No binding or pending terminal exists.
808    Detached,
809    /// Current live binding authority.
810    Bound(ActiveBinding),
811    /// Authority ended and a cause-partitioned terminal record is pending.
812    PendingFinalization(PendingFinalization),
813}
814
815pub(super) const fn restore_pending_finalization(
816    binding: ActiveBinding,
817    cause: CloseCause,
818    transaction_order: TransactionOrder,
819) -> Option<PendingFinalization> {
820    let position =
821        BindingTerminalDisposition::Pending(PendingBindingTerminalPosition::new(transaction_order));
822    match cause {
823        CloseCause::CleanDeregister => match binding.clean_deregister(position) {
824            DetachedBindingTransition::Pending(value) => Some(PendingFinalization::Detached(value)),
825            DetachedBindingTransition::Committed(_) => None,
826        },
827        CloseCause::ServerShutdown => match binding.server_shutdown(position) {
828            DetachedBindingTransition::Pending(value) => Some(PendingFinalization::Detached(value)),
829            DetachedBindingTransition::Committed(_) => None,
830        },
831        // Supersession is an indivisible committed handoff and can never be pending.
832        CloseCause::Superseded => None,
833        CloseCause::ConnectionLost => match binding.connection_lost(position) {
834            DiedBindingTransition::Pending(value) => Some(PendingFinalization::Died(value)),
835            DiedBindingTransition::Committed(_) => None,
836        },
837        CloseCause::ProcessKilled => match binding.process_killed(position) {
838            DiedBindingTransition::Pending(value) => Some(PendingFinalization::Died(value)),
839            DiedBindingTransition::Committed(_) => None,
840        },
841        CloseCause::ProtocolError => match binding.protocol_error(position) {
842            DiedBindingTransition::Pending(value) => Some(PendingFinalization::Died(value)),
843            DiedBindingTransition::Committed(_) => None,
844        },
845        CloseCause::UncleanServerRestart {
846            prior_server_incarnation,
847        } => {
848            if binding
849                .binding_epoch
850                .connection_incarnation
851                .server_incarnation
852                != prior_server_incarnation
853            {
854                return None;
855            }
856            match binding.unclean_server_restart(position) {
857                DiedBindingTransition::Pending(value) => Some(PendingFinalization::Died(value)),
858                DiedBindingTransition::Committed(_) => None,
859            }
860        }
861    }
862}
863
864pub(super) const fn restore_committed_terminal(
865    binding: ActiveBinding,
866    cause: CloseCause,
867    transaction_order: TransactionOrder,
868    delivery_seq: DeliverySeq,
869) -> Option<CommittedBindingTerminal> {
870    let position = BindingTerminalDisposition::Committed(CommittedBindingTerminalPosition::new(
871        transaction_order,
872        delivery_seq,
873    ));
874    match cause {
875        CloseCause::CleanDeregister => match binding.clean_deregister(position) {
876            DetachedBindingTransition::Committed(value) => {
877                Some(CommittedBindingTerminal::Detached(value))
878            }
879            DetachedBindingTransition::Pending(_) => None,
880        },
881        CloseCause::Superseded => Some(CommittedBindingTerminal::Detached(binding.superseded(
882            CommittedBindingTerminalPosition::new(transaction_order, delivery_seq),
883        ))),
884        CloseCause::ServerShutdown => match binding.server_shutdown(position) {
885            DetachedBindingTransition::Committed(value) => {
886                Some(CommittedBindingTerminal::Detached(value))
887            }
888            DetachedBindingTransition::Pending(_) => None,
889        },
890        CloseCause::ConnectionLost => match binding.connection_lost(position) {
891            DiedBindingTransition::Committed(value) => Some(CommittedBindingTerminal::Died(value)),
892            DiedBindingTransition::Pending(_) => None,
893        },
894        CloseCause::ProcessKilled => match binding.process_killed(position) {
895            DiedBindingTransition::Committed(value) => Some(CommittedBindingTerminal::Died(value)),
896            DiedBindingTransition::Pending(_) => None,
897        },
898        CloseCause::ProtocolError => match binding.protocol_error(position) {
899            DiedBindingTransition::Committed(value) => Some(CommittedBindingTerminal::Died(value)),
900            DiedBindingTransition::Pending(_) => None,
901        },
902        CloseCause::UncleanServerRestart {
903            prior_server_incarnation,
904        } => {
905            if binding
906                .binding_epoch
907                .connection_incarnation
908                .server_incarnation
909                != prior_server_incarnation
910            {
911                return None;
912            }
913            match binding.unclean_server_restart(position) {
914                DiedBindingTransition::Committed(value) => {
915                    Some(CommittedBindingTerminal::Died(value))
916                }
917                DiedBindingTransition::Pending(_) => None,
918            }
919        }
920    }
921}