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    /// Projects a committed Detached terminal; pending finalization has no progress yet.
585    #[must_use]
586    pub const fn observer_progress_projection(&self) -> Option<super::ObserverProgressProjection> {
587        let Self::Committed(terminal) = self else {
588            return None;
589        };
590        Some(super::ObserverProgressProjection::new(
591            terminal.conversation_id(),
592            terminal.delivery_seq(),
593        ))
594    }
595
596    /// Returns the post-transition binding slot.
597    #[must_use]
598    pub const fn binding_state(self) -> BindingState {
599        match self {
600            Self::Committed(_) => BindingState::Detached,
601            Self::Pending(value) => {
602                BindingState::PendingFinalization(PendingFinalization::Detached(value))
603            }
604        }
605    }
606}
607
608/// Result of a fate that records a `Died` lifecycle terminal.
609#[derive(Clone, Copy, Debug, PartialEq, Eq)]
610pub enum DiedBindingTransition {
611    /// Terminal record committed with its durable delivery key.
612    Committed(CommittedDiedTerminal),
613    /// Authority ended and the exact terminal remains bounded-pending.
614    Pending(PendingDiedFinalization),
615}
616
617impl DiedBindingTransition {
618    /// Projects a committed Died terminal; pending finalization has no progress yet.
619    #[must_use]
620    pub const fn observer_progress_projection(&self) -> Option<super::ObserverProgressProjection> {
621        let Self::Committed(terminal) = self else {
622            return None;
623        };
624        Some(super::ObserverProgressProjection::new(
625            terminal.conversation_id(),
626            terminal.delivery_seq(),
627        ))
628    }
629
630    /// Returns the post-transition binding slot.
631    #[must_use]
632    pub const fn binding_state(self) -> BindingState {
633        match self {
634            Self::Committed(_) => BindingState::Detached,
635            Self::Pending(value) => {
636                BindingState::PendingFinalization(PendingFinalization::Died(value))
637            }
638        }
639    }
640}
641
642impl ActiveBinding {
643    const fn finish_detached(
644        self,
645        cause: DetachedCause,
646        disposition: BindingTerminalDisposition,
647    ) -> DetachedBindingTransition {
648        match disposition {
649            BindingTerminalDisposition::Committed(position) => {
650                DetachedBindingTransition::Committed(CommittedDetachedTerminal {
651                    identity: BindingTerminalIdentity::from_active(
652                        self,
653                        position.transaction_order,
654                    ),
655                    cause,
656                    delivery_seq: position.delivery_seq,
657                })
658            }
659            BindingTerminalDisposition::Pending(position) => {
660                DetachedBindingTransition::Pending(PendingDetachedFinalization {
661                    identity: BindingTerminalIdentity::from_active(
662                        self,
663                        position.transaction_order,
664                    ),
665                    cause,
666                })
667            }
668        }
669    }
670
671    const fn finish_died(
672        self,
673        cause: DiedCause,
674        disposition: BindingTerminalDisposition,
675    ) -> DiedBindingTransition {
676        match disposition {
677            BindingTerminalDisposition::Committed(position) => {
678                DiedBindingTransition::Committed(CommittedDiedTerminal {
679                    identity: BindingTerminalIdentity::from_active(
680                        self,
681                        position.transaction_order,
682                    ),
683                    cause,
684                    delivery_seq: position.delivery_seq,
685                })
686            }
687            BindingTerminalDisposition::Pending(position) => {
688                DiedBindingTransition::Pending(PendingDiedFinalization {
689                    identity: BindingTerminalIdentity::from_active(
690                        self,
691                        position.transaction_order,
692                    ),
693                    cause,
694                })
695            }
696        }
697    }
698
699    /// Ends authority for explicit detach or clean protocol Disconnect.
700    #[must_use]
701    pub const fn clean_deregister(
702        self,
703        disposition: BindingTerminalDisposition,
704    ) -> DetachedBindingTransition {
705        self.finish_detached(DetachedCause::CleanDeregister, disposition)
706    }
707
708    /// Commits an explicit detach terminal at its assigned durable position.
709    #[must_use]
710    pub const fn commit_clean_deregister(
711        self,
712        position: CommittedBindingTerminalPosition,
713    ) -> CommittedDetachedTerminal {
714        CommittedDetachedTerminal {
715            identity: BindingTerminalIdentity::from_active(self, position.transaction_order),
716            cause: DetachedCause::CleanDeregister,
717            delivery_seq: position.delivery_seq,
718        }
719    }
720
721    /// Ends explicit-detach authority while retaining its exact pending terminal.
722    #[must_use]
723    pub const fn pending_clean_deregister(
724        self,
725        position: PendingBindingTerminalPosition,
726    ) -> PendingDetachedFinalization {
727        PendingDetachedFinalization {
728            identity: BindingTerminalIdentity::from_active(self, position.transaction_order),
729            cause: DetachedCause::CleanDeregister,
730        }
731    }
732
733    /// Ends every binding still active when a clean protocol Disconnect arrives.
734    #[must_use]
735    pub const fn clean_disconnect(
736        self,
737        disposition: BindingTerminalDisposition,
738    ) -> DetachedBindingTransition {
739        self.clean_deregister(disposition)
740    }
741
742    /// Commits the old terminal in an authorized superseding attach handoff.
743    ///
744    /// Supersession is an optional new-major producer and cannot leave only the
745    /// old terminal pending; the enclosing attach transaction either appends
746    /// the ordered `Detached(Superseded)`/`Attached` pair or commits nothing.
747    #[must_use]
748    pub const fn superseded(
749        self,
750        position: CommittedBindingTerminalPosition,
751    ) -> CommittedDetachedTerminal {
752        CommittedDetachedTerminal {
753            identity: BindingTerminalIdentity::from_active(self, position.transaction_order),
754            cause: DetachedCause::Superseded,
755            delivery_seq: position.delivery_seq,
756        }
757    }
758
759    /// Ends authority during an orderly server shutdown.
760    #[must_use]
761    pub const fn server_shutdown(
762        self,
763        disposition: BindingTerminalDisposition,
764    ) -> DetachedBindingTransition {
765        self.finish_detached(DetachedCause::ServerShutdown, disposition)
766    }
767
768    /// Ends authority after TCP/keepalive/read/write connection loss.
769    #[must_use]
770    pub const fn connection_lost(
771        self,
772        disposition: BindingTerminalDisposition,
773    ) -> DiedBindingTransition {
774        self.finish_died(DiedCause::ConnectionLost, disposition)
775    }
776
777    /// Ends authority after trapped linked-EXIT or known forced termination.
778    #[must_use]
779    pub const fn process_killed(
780        self,
781        disposition: BindingTerminalDisposition,
782    ) -> DiedBindingTransition {
783        self.finish_died(DiedCause::ProcessKilled, disposition)
784    }
785
786    /// Ends authority after a terminating decode or protocol-state refusal.
787    #[must_use]
788    pub const fn protocol_error(
789        self,
790        disposition: BindingTerminalDisposition,
791    ) -> DiedBindingTransition {
792        self.finish_died(DiedCause::ProtocolError, disposition)
793    }
794
795    /// Recovers a durably active epoch owned by a prior server incarnation.
796    ///
797    /// The prior incarnation in the `Died` cause is derived from the exact old
798    /// binding epoch, so callers cannot pair the state with a different value.
799    #[must_use]
800    pub const fn unclean_server_restart(
801        self,
802        disposition: BindingTerminalDisposition,
803    ) -> DiedBindingTransition {
804        self.finish_died(
805            DiedCause::UncleanServerRestart {
806                prior_server_incarnation: self
807                    .binding_epoch
808                    .connection_incarnation
809                    .server_incarnation,
810            },
811            disposition,
812        )
813    }
814}
815
816/// Binding-slot state; pending finalization carries no live authority.
817#[derive(Clone, Copy, Debug, PartialEq, Eq)]
818pub enum BindingState {
819    /// No binding or pending terminal exists.
820    Detached,
821    /// Current live binding authority.
822    Bound(ActiveBinding),
823    /// Authority ended and a cause-partitioned terminal record is pending.
824    PendingFinalization(PendingFinalization),
825}
826
827pub(super) const fn restore_pending_finalization(
828    binding: ActiveBinding,
829    cause: CloseCause,
830    transaction_order: TransactionOrder,
831) -> Option<PendingFinalization> {
832    let position =
833        BindingTerminalDisposition::Pending(PendingBindingTerminalPosition::new(transaction_order));
834    match cause {
835        CloseCause::CleanDeregister => match binding.clean_deregister(position) {
836            DetachedBindingTransition::Pending(value) => Some(PendingFinalization::Detached(value)),
837            DetachedBindingTransition::Committed(_) => None,
838        },
839        CloseCause::ServerShutdown => match binding.server_shutdown(position) {
840            DetachedBindingTransition::Pending(value) => Some(PendingFinalization::Detached(value)),
841            DetachedBindingTransition::Committed(_) => None,
842        },
843        // Supersession is an indivisible committed handoff and can never be pending.
844        CloseCause::Superseded => None,
845        CloseCause::ConnectionLost => match binding.connection_lost(position) {
846            DiedBindingTransition::Pending(value) => Some(PendingFinalization::Died(value)),
847            DiedBindingTransition::Committed(_) => None,
848        },
849        CloseCause::ProcessKilled => match binding.process_killed(position) {
850            DiedBindingTransition::Pending(value) => Some(PendingFinalization::Died(value)),
851            DiedBindingTransition::Committed(_) => None,
852        },
853        CloseCause::ProtocolError => match binding.protocol_error(position) {
854            DiedBindingTransition::Pending(value) => Some(PendingFinalization::Died(value)),
855            DiedBindingTransition::Committed(_) => None,
856        },
857        CloseCause::UncleanServerRestart {
858            prior_server_incarnation,
859        } => {
860            if binding
861                .binding_epoch
862                .connection_incarnation
863                .server_incarnation
864                != prior_server_incarnation
865            {
866                return None;
867            }
868            match binding.unclean_server_restart(position) {
869                DiedBindingTransition::Pending(value) => Some(PendingFinalization::Died(value)),
870                DiedBindingTransition::Committed(_) => None,
871            }
872        }
873    }
874}
875
876pub(super) const fn restore_committed_terminal(
877    binding: ActiveBinding,
878    cause: CloseCause,
879    transaction_order: TransactionOrder,
880    delivery_seq: DeliverySeq,
881) -> Option<CommittedBindingTerminal> {
882    let position = BindingTerminalDisposition::Committed(CommittedBindingTerminalPosition::new(
883        transaction_order,
884        delivery_seq,
885    ));
886    match cause {
887        CloseCause::CleanDeregister => match binding.clean_deregister(position) {
888            DetachedBindingTransition::Committed(value) => {
889                Some(CommittedBindingTerminal::Detached(value))
890            }
891            DetachedBindingTransition::Pending(_) => None,
892        },
893        CloseCause::Superseded => Some(CommittedBindingTerminal::Detached(binding.superseded(
894            CommittedBindingTerminalPosition::new(transaction_order, delivery_seq),
895        ))),
896        CloseCause::ServerShutdown => match binding.server_shutdown(position) {
897            DetachedBindingTransition::Committed(value) => {
898                Some(CommittedBindingTerminal::Detached(value))
899            }
900            DetachedBindingTransition::Pending(_) => None,
901        },
902        CloseCause::ConnectionLost => match binding.connection_lost(position) {
903            DiedBindingTransition::Committed(value) => Some(CommittedBindingTerminal::Died(value)),
904            DiedBindingTransition::Pending(_) => None,
905        },
906        CloseCause::ProcessKilled => match binding.process_killed(position) {
907            DiedBindingTransition::Committed(value) => Some(CommittedBindingTerminal::Died(value)),
908            DiedBindingTransition::Pending(_) => None,
909        },
910        CloseCause::ProtocolError => match binding.protocol_error(position) {
911            DiedBindingTransition::Committed(value) => Some(CommittedBindingTerminal::Died(value)),
912            DiedBindingTransition::Pending(_) => None,
913        },
914        CloseCause::UncleanServerRestart {
915            prior_server_incarnation,
916        } => {
917            if binding
918                .binding_epoch
919                .connection_incarnation
920                .server_incarnation
921                != prior_server_incarnation
922            {
923                return None;
924            }
925            match binding.unclean_server_restart(position) {
926                DiedBindingTransition::Committed(value) => {
927                    Some(CommittedBindingTerminal::Died(value))
928                }
929                DiedBindingTransition::Pending(_) => None,
930            }
931        }
932    }
933}