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    /// Describes this terminal as the capsule that restores it.
346    ///
347    /// The cause-partitioned variant is not stated: restore derives it from the
348    /// cause, which is the whole point of the partition, so writing it would
349    /// give one fact two homes and a way to disagree with itself.
350    #[must_use]
351    pub const fn storage_description(self) -> super::storage::CommittedBindingTerminalRestore {
352        super::storage::CommittedBindingTerminalRestore {
353            binding: ActiveBinding {
354                participant_id: self.participant_id(),
355                conversation_id: self.conversation_id(),
356                binding_epoch: self.binding_epoch(),
357            },
358            cause: self.close_cause(),
359            transaction_order: self.admission_order().transaction_order(),
360            delivery_seq: self.delivery_seq(),
361        }
362    }
363}
364
365impl From<CommittedDetachedTerminal> for CommittedBindingTerminal {
366    fn from(value: CommittedDetachedTerminal) -> Self {
367        Self::Detached(value)
368    }
369}
370
371impl From<CommittedDiedTerminal> for CommittedBindingTerminal {
372    fn from(value: CommittedDiedTerminal) -> Self {
373        Self::Died(value)
374    }
375}
376
377/// Pending `Detached` terminal with no possible `Died`-only cause.
378#[derive(Clone, Copy, Debug, PartialEq, Eq)]
379pub struct PendingDetachedFinalization {
380    identity: BindingTerminalIdentity,
381    cause: DetachedCause,
382}
383
384impl PendingDetachedFinalization {
385    /// Returns the permanent participant identifier/index.
386    #[must_use]
387    pub const fn participant_id(self) -> ParticipantId {
388        self.identity.participant_id
389    }
390
391    /// Returns the owning conversation.
392    #[must_use]
393    pub const fn conversation_id(self) -> ConversationId {
394        self.identity.conversation_id
395    }
396
397    /// Returns the exact ended binding epoch.
398    #[must_use]
399    pub const fn binding_epoch(self) -> BindingEpoch {
400        self.identity.binding_epoch
401    }
402
403    /// Returns the type-restricted `Detached` cause.
404    #[must_use]
405    pub const fn cause(self) -> DetachedCause {
406        self.cause
407    }
408
409    /// Returns the immutable binding-terminal admission position.
410    #[must_use]
411    pub const fn admission_order(self) -> AdmissionOrder {
412        self.identity.admission_order
413    }
414
415    /// Commits the exact pending terminal at its assigned delivery sequence.
416    #[must_use]
417    pub const fn commit(self, delivery_seq: DeliverySeq) -> CommittedDetachedTerminal {
418        CommittedDetachedTerminal {
419            identity: self.identity,
420            cause: self.cause,
421            delivery_seq,
422        }
423    }
424}
425
426/// Pending `Died` terminal with no possible `Detached`-only cause.
427#[derive(Clone, Copy, Debug, PartialEq, Eq)]
428pub struct PendingDiedFinalization {
429    identity: BindingTerminalIdentity,
430    cause: DiedCause,
431}
432
433impl PendingDiedFinalization {
434    /// Returns the permanent participant identifier/index.
435    #[must_use]
436    pub const fn participant_id(self) -> ParticipantId {
437        self.identity.participant_id
438    }
439
440    /// Returns the owning conversation.
441    #[must_use]
442    pub const fn conversation_id(self) -> ConversationId {
443        self.identity.conversation_id
444    }
445
446    /// Returns the exact ended binding epoch.
447    #[must_use]
448    pub const fn binding_epoch(self) -> BindingEpoch {
449        self.identity.binding_epoch
450    }
451
452    /// Returns the type-restricted `Died` cause.
453    #[must_use]
454    pub const fn cause(self) -> DiedCause {
455        self.cause
456    }
457
458    /// Returns the immutable binding-terminal admission position.
459    #[must_use]
460    pub const fn admission_order(self) -> AdmissionOrder {
461        self.identity.admission_order
462    }
463
464    /// Commits the exact pending terminal at its assigned delivery sequence.
465    #[must_use]
466    pub const fn commit(self, delivery_seq: DeliverySeq) -> CommittedDiedTerminal {
467        CommittedDiedTerminal {
468            identity: self.identity,
469            cause: self.cause,
470            delivery_seq,
471        }
472    }
473}
474
475/// Binding-terminal lifecycle record kind, derived from cause-partitioned state.
476#[derive(Clone, Copy, Debug, PartialEq, Eq)]
477pub enum BindingTerminalKind {
478    /// Clean/supersession/shutdown `Detached` record.
479    Detached,
480    /// Unexpected `Died` record.
481    Died,
482}
483
484/// Binding authority ended, but its terminal record awaits durable append.
485///
486/// Both variants have private, transition-derived fields. Consequently a
487/// `Detached` finalization cannot carry `ConnectionLost`, `ProcessKilled`,
488/// `ProtocolError`, or `UncleanServerRestart`, and a `Died` finalization cannot
489/// carry `CleanDeregister`, `Superseded`, or `ServerShutdown`.
490#[derive(Clone, Copy, Debug, PartialEq, Eq)]
491pub enum PendingFinalization {
492    /// A pending `Detached` record and its restricted cause.
493    Detached(PendingDetachedFinalization),
494    /// A pending `Died` record and its restricted cause.
495    Died(PendingDiedFinalization),
496}
497
498impl PendingFinalization {
499    /// Returns the permanent participant identifier/index.
500    #[must_use]
501    pub const fn participant_id(self) -> ParticipantId {
502        match self {
503            Self::Detached(value) => value.participant_id(),
504            Self::Died(value) => value.participant_id(),
505        }
506    }
507
508    /// Returns the owning conversation.
509    #[must_use]
510    pub const fn conversation_id(self) -> ConversationId {
511        match self {
512            Self::Detached(value) => value.conversation_id(),
513            Self::Died(value) => value.conversation_id(),
514        }
515    }
516
517    /// Returns the exact ended binding epoch.
518    #[must_use]
519    pub const fn binding_epoch(self) -> BindingEpoch {
520        match self {
521            Self::Detached(value) => value.binding_epoch(),
522            Self::Died(value) => value.binding_epoch(),
523        }
524    }
525
526    /// Returns the immutable binding-terminal admission position.
527    #[must_use]
528    pub const fn admission_order(self) -> AdmissionOrder {
529        match self {
530            Self::Detached(value) => value.admission_order(),
531            Self::Died(value) => value.admission_order(),
532        }
533    }
534
535    /// Returns the record class derived from the cause-partitioned variant.
536    #[must_use]
537    pub const fn kind(self) -> BindingTerminalKind {
538        match self {
539            Self::Detached(_) => BindingTerminalKind::Detached,
540            Self::Died(_) => BindingTerminalKind::Died,
541        }
542    }
543
544    /// Returns the shared seven-class close-cause view.
545    #[must_use]
546    pub const fn close_cause(self) -> CloseCause {
547        match self {
548            Self::Detached(value) => value.cause().close_cause(),
549            Self::Died(value) => value.cause().close_cause(),
550        }
551    }
552
553    /// Returns the restricted `Detached` cause, or `None` for a `Died` state.
554    #[must_use]
555    pub const fn detached_cause(self) -> Option<DetachedCause> {
556        match self {
557            Self::Detached(value) => Some(value.cause()),
558            Self::Died(_) => None,
559        }
560    }
561
562    /// Returns the restricted `Died` cause, or `None` for a `Detached` state.
563    #[must_use]
564    pub const fn died_cause(self) -> Option<DiedCause> {
565        match self {
566            Self::Detached(_) => None,
567            Self::Died(value) => Some(value.cause()),
568        }
569    }
570
571    /// Commits the exact pending terminal without changing cause, epoch, or order.
572    #[must_use]
573    pub const fn commit(self, delivery_seq: DeliverySeq) -> CommittedBindingTerminal {
574        match self {
575            Self::Detached(value) => CommittedBindingTerminal::Detached(value.commit(delivery_seq)),
576            Self::Died(value) => CommittedBindingTerminal::Died(value.commit(delivery_seq)),
577        }
578    }
579
580    /// Describes this pending terminal as the capsule that restores it.
581    ///
582    /// A pending terminal has no delivery sequence yet, which is the whole
583    /// difference between it and a committed one, so its capsule has no such
584    /// field to supply.
585    #[must_use]
586    pub const fn storage_description(self) -> super::storage::PendingFinalizationRestore {
587        super::storage::PendingFinalizationRestore {
588            binding: ActiveBinding {
589                participant_id: self.participant_id(),
590                conversation_id: self.conversation_id(),
591                binding_epoch: self.binding_epoch(),
592            },
593            cause: self.close_cause(),
594            transaction_order: self.admission_order().transaction_order(),
595        }
596    }
597}
598
599impl From<PendingDetachedFinalization> for PendingFinalization {
600    fn from(value: PendingDetachedFinalization) -> Self {
601        Self::Detached(value)
602    }
603}
604
605impl From<PendingDiedFinalization> for PendingFinalization {
606    fn from(value: PendingDiedFinalization) -> Self {
607        Self::Died(value)
608    }
609}
610
611/// Result of a fate that records a `Detached` lifecycle terminal.
612#[derive(Clone, Copy, Debug, PartialEq, Eq)]
613pub enum DetachedBindingTransition {
614    /// Terminal record committed with its durable delivery key.
615    Committed(CommittedDetachedTerminal),
616    /// Authority ended and the exact terminal remains bounded-pending.
617    Pending(PendingDetachedFinalization),
618}
619
620impl DetachedBindingTransition {
621    /// Projects a committed Detached terminal; pending finalization has no progress yet.
622    #[must_use]
623    pub const fn observer_progress_projection(&self) -> Option<super::ObserverProgressProjection> {
624        let Self::Committed(terminal) = self else {
625            return None;
626        };
627        Some(super::ObserverProgressProjection::sourced(
628            terminal.conversation_id(),
629            terminal.delivery_seq(),
630            super::checkpoint_codec::ObserverProjectionSource::Terminal(
631                CommittedBindingTerminal::Detached(*terminal).storage_description(),
632            ),
633        ))
634    }
635
636    /// Returns the post-transition binding slot.
637    #[must_use]
638    pub const fn binding_state(self) -> BindingState {
639        match self {
640            Self::Committed(_) => BindingState::Detached,
641            Self::Pending(value) => {
642                BindingState::PendingFinalization(PendingFinalization::Detached(value))
643            }
644        }
645    }
646}
647
648/// Result of a fate that records a `Died` lifecycle terminal.
649#[derive(Clone, Copy, Debug, PartialEq, Eq)]
650pub enum DiedBindingTransition {
651    /// Terminal record committed with its durable delivery key.
652    Committed(CommittedDiedTerminal),
653    /// Authority ended and the exact terminal remains bounded-pending.
654    Pending(PendingDiedFinalization),
655}
656
657impl DiedBindingTransition {
658    /// Projects a committed Died terminal; pending finalization has no progress yet.
659    #[must_use]
660    pub const fn observer_progress_projection(&self) -> Option<super::ObserverProgressProjection> {
661        let Self::Committed(terminal) = self else {
662            return None;
663        };
664        Some(super::ObserverProgressProjection::sourced(
665            terminal.conversation_id(),
666            terminal.delivery_seq(),
667            super::checkpoint_codec::ObserverProjectionSource::Terminal(
668                CommittedBindingTerminal::Died(*terminal).storage_description(),
669            ),
670        ))
671    }
672
673    /// Returns the post-transition binding slot.
674    #[must_use]
675    pub const fn binding_state(self) -> BindingState {
676        match self {
677            Self::Committed(_) => BindingState::Detached,
678            Self::Pending(value) => {
679                BindingState::PendingFinalization(PendingFinalization::Died(value))
680            }
681        }
682    }
683}
684
685impl ActiveBinding {
686    const fn finish_detached(
687        self,
688        cause: DetachedCause,
689        disposition: BindingTerminalDisposition,
690    ) -> DetachedBindingTransition {
691        match disposition {
692            BindingTerminalDisposition::Committed(position) => {
693                DetachedBindingTransition::Committed(CommittedDetachedTerminal {
694                    identity: BindingTerminalIdentity::from_active(
695                        self,
696                        position.transaction_order,
697                    ),
698                    cause,
699                    delivery_seq: position.delivery_seq,
700                })
701            }
702            BindingTerminalDisposition::Pending(position) => {
703                DetachedBindingTransition::Pending(PendingDetachedFinalization {
704                    identity: BindingTerminalIdentity::from_active(
705                        self,
706                        position.transaction_order,
707                    ),
708                    cause,
709                })
710            }
711        }
712    }
713
714    const fn finish_died(
715        self,
716        cause: DiedCause,
717        disposition: BindingTerminalDisposition,
718    ) -> DiedBindingTransition {
719        match disposition {
720            BindingTerminalDisposition::Committed(position) => {
721                DiedBindingTransition::Committed(CommittedDiedTerminal {
722                    identity: BindingTerminalIdentity::from_active(
723                        self,
724                        position.transaction_order,
725                    ),
726                    cause,
727                    delivery_seq: position.delivery_seq,
728                })
729            }
730            BindingTerminalDisposition::Pending(position) => {
731                DiedBindingTransition::Pending(PendingDiedFinalization {
732                    identity: BindingTerminalIdentity::from_active(
733                        self,
734                        position.transaction_order,
735                    ),
736                    cause,
737                })
738            }
739        }
740    }
741
742    /// Ends authority for explicit detach or clean protocol Disconnect.
743    #[must_use]
744    pub const fn clean_deregister(
745        self,
746        disposition: BindingTerminalDisposition,
747    ) -> DetachedBindingTransition {
748        self.finish_detached(DetachedCause::CleanDeregister, disposition)
749    }
750
751    /// Commits an explicit detach terminal at its assigned durable position.
752    #[must_use]
753    pub const fn commit_clean_deregister(
754        self,
755        position: CommittedBindingTerminalPosition,
756    ) -> CommittedDetachedTerminal {
757        CommittedDetachedTerminal {
758            identity: BindingTerminalIdentity::from_active(self, position.transaction_order),
759            cause: DetachedCause::CleanDeregister,
760            delivery_seq: position.delivery_seq,
761        }
762    }
763
764    /// Ends explicit-detach authority while retaining its exact pending terminal.
765    #[must_use]
766    pub const fn pending_clean_deregister(
767        self,
768        position: PendingBindingTerminalPosition,
769    ) -> PendingDetachedFinalization {
770        PendingDetachedFinalization {
771            identity: BindingTerminalIdentity::from_active(self, position.transaction_order),
772            cause: DetachedCause::CleanDeregister,
773        }
774    }
775
776    /// Ends every binding still active when a clean protocol Disconnect arrives.
777    #[must_use]
778    pub const fn clean_disconnect(
779        self,
780        disposition: BindingTerminalDisposition,
781    ) -> DetachedBindingTransition {
782        self.clean_deregister(disposition)
783    }
784
785    /// Commits the old terminal in an authorized superseding attach handoff.
786    ///
787    /// Supersession is an optional new-major producer and cannot leave only the
788    /// old terminal pending; the enclosing attach transaction either appends
789    /// the ordered `Detached(Superseded)`/`Attached` pair or commits nothing.
790    #[must_use]
791    pub const fn superseded(
792        self,
793        position: CommittedBindingTerminalPosition,
794    ) -> CommittedDetachedTerminal {
795        CommittedDetachedTerminal {
796            identity: BindingTerminalIdentity::from_active(self, position.transaction_order),
797            cause: DetachedCause::Superseded,
798            delivery_seq: position.delivery_seq,
799        }
800    }
801
802    /// Ends authority during an orderly server shutdown.
803    #[must_use]
804    pub const fn server_shutdown(
805        self,
806        disposition: BindingTerminalDisposition,
807    ) -> DetachedBindingTransition {
808        self.finish_detached(DetachedCause::ServerShutdown, disposition)
809    }
810
811    /// Ends authority after TCP/keepalive/read/write connection loss.
812    #[must_use]
813    pub const fn connection_lost(
814        self,
815        disposition: BindingTerminalDisposition,
816    ) -> DiedBindingTransition {
817        self.finish_died(DiedCause::ConnectionLost, disposition)
818    }
819
820    /// Ends authority after trapped linked-EXIT or known forced termination.
821    #[must_use]
822    pub const fn process_killed(
823        self,
824        disposition: BindingTerminalDisposition,
825    ) -> DiedBindingTransition {
826        self.finish_died(DiedCause::ProcessKilled, disposition)
827    }
828
829    /// Ends authority after a terminating decode or protocol-state refusal.
830    #[must_use]
831    pub const fn protocol_error(
832        self,
833        disposition: BindingTerminalDisposition,
834    ) -> DiedBindingTransition {
835        self.finish_died(DiedCause::ProtocolError, disposition)
836    }
837
838    /// Recovers a durably active epoch owned by a prior server incarnation.
839    ///
840    /// The prior incarnation in the `Died` cause is derived from the exact old
841    /// binding epoch, so callers cannot pair the state with a different value.
842    #[must_use]
843    pub const fn unclean_server_restart(
844        self,
845        disposition: BindingTerminalDisposition,
846    ) -> DiedBindingTransition {
847        self.finish_died(
848            DiedCause::UncleanServerRestart {
849                prior_server_incarnation: self
850                    .binding_epoch
851                    .connection_incarnation
852                    .server_incarnation,
853            },
854            disposition,
855        )
856    }
857}
858
859/// Binding-slot state; pending finalization carries no live authority.
860#[derive(Clone, Copy, Debug, PartialEq, Eq)]
861pub enum BindingState {
862    /// No binding or pending terminal exists.
863    Detached,
864    /// Current live binding authority.
865    Bound(ActiveBinding),
866    /// Authority ended and a cause-partitioned terminal record is pending.
867    PendingFinalization(PendingFinalization),
868}
869
870pub(super) const fn restore_pending_finalization(
871    binding: ActiveBinding,
872    cause: CloseCause,
873    transaction_order: TransactionOrder,
874) -> Option<PendingFinalization> {
875    let position =
876        BindingTerminalDisposition::Pending(PendingBindingTerminalPosition::new(transaction_order));
877    match cause {
878        CloseCause::CleanDeregister => match binding.clean_deregister(position) {
879            DetachedBindingTransition::Pending(value) => Some(PendingFinalization::Detached(value)),
880            DetachedBindingTransition::Committed(_) => None,
881        },
882        CloseCause::ServerShutdown => match binding.server_shutdown(position) {
883            DetachedBindingTransition::Pending(value) => Some(PendingFinalization::Detached(value)),
884            DetachedBindingTransition::Committed(_) => None,
885        },
886        // Supersession is an indivisible committed handoff and can never be pending.
887        CloseCause::Superseded => None,
888        CloseCause::ConnectionLost => match binding.connection_lost(position) {
889            DiedBindingTransition::Pending(value) => Some(PendingFinalization::Died(value)),
890            DiedBindingTransition::Committed(_) => None,
891        },
892        CloseCause::ProcessKilled => match binding.process_killed(position) {
893            DiedBindingTransition::Pending(value) => Some(PendingFinalization::Died(value)),
894            DiedBindingTransition::Committed(_) => None,
895        },
896        CloseCause::ProtocolError => match binding.protocol_error(position) {
897            DiedBindingTransition::Pending(value) => Some(PendingFinalization::Died(value)),
898            DiedBindingTransition::Committed(_) => None,
899        },
900        CloseCause::UncleanServerRestart {
901            prior_server_incarnation,
902        } => {
903            if binding
904                .binding_epoch
905                .connection_incarnation
906                .server_incarnation
907                != prior_server_incarnation
908            {
909                return None;
910            }
911            match binding.unclean_server_restart(position) {
912                DiedBindingTransition::Pending(value) => Some(PendingFinalization::Died(value)),
913                DiedBindingTransition::Committed(_) => None,
914            }
915        }
916    }
917}
918
919pub(super) const fn restore_committed_terminal(
920    binding: ActiveBinding,
921    cause: CloseCause,
922    transaction_order: TransactionOrder,
923    delivery_seq: DeliverySeq,
924) -> Option<CommittedBindingTerminal> {
925    let position = BindingTerminalDisposition::Committed(CommittedBindingTerminalPosition::new(
926        transaction_order,
927        delivery_seq,
928    ));
929    match cause {
930        CloseCause::CleanDeregister => match binding.clean_deregister(position) {
931            DetachedBindingTransition::Committed(value) => {
932                Some(CommittedBindingTerminal::Detached(value))
933            }
934            DetachedBindingTransition::Pending(_) => None,
935        },
936        CloseCause::Superseded => Some(CommittedBindingTerminal::Detached(binding.superseded(
937            CommittedBindingTerminalPosition::new(transaction_order, delivery_seq),
938        ))),
939        CloseCause::ServerShutdown => match binding.server_shutdown(position) {
940            DetachedBindingTransition::Committed(value) => {
941                Some(CommittedBindingTerminal::Detached(value))
942            }
943            DetachedBindingTransition::Pending(_) => None,
944        },
945        CloseCause::ConnectionLost => match binding.connection_lost(position) {
946            DiedBindingTransition::Committed(value) => Some(CommittedBindingTerminal::Died(value)),
947            DiedBindingTransition::Pending(_) => None,
948        },
949        CloseCause::ProcessKilled => match binding.process_killed(position) {
950            DiedBindingTransition::Committed(value) => Some(CommittedBindingTerminal::Died(value)),
951            DiedBindingTransition::Pending(_) => None,
952        },
953        CloseCause::ProtocolError => match binding.protocol_error(position) {
954            DiedBindingTransition::Committed(value) => Some(CommittedBindingTerminal::Died(value)),
955            DiedBindingTransition::Pending(_) => None,
956        },
957        CloseCause::UncleanServerRestart {
958            prior_server_incarnation,
959        } => {
960            if binding
961                .binding_epoch
962                .connection_incarnation
963                .server_incarnation
964                != prior_server_incarnation
965            {
966                return None;
967            }
968            match binding.unclean_server_restart(position) {
969                DiedBindingTransition::Committed(value) => {
970                    Some(CommittedBindingTerminal::Died(value))
971                }
972                DiedBindingTransition::Pending(_) => None,
973            }
974        }
975    }
976}