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