liminal_protocol/wire/push.rs
1use alloc::vec::Vec;
2
3use super::{
4 BindingEpoch, CloseCause, ConversationId, DeliverySeq, ObserverEpoch, ParticipantId,
5 PushDiscriminant, RecordKind, SettlementEpoch,
6};
7
8/// Causes valid only for a `Detached` lifecycle record.
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub enum DetachedCause {
11 /// Clean deregistration.
12 CleanDeregister,
13 /// Binding supersession.
14 Superseded,
15 /// Orderly server shutdown.
16 ServerShutdown,
17}
18
19impl DetachedCause {
20 /// Converts to the shared close-cause registry without permitting Died-only causes.
21 #[must_use]
22 pub const fn close_cause(self) -> CloseCause {
23 match self {
24 Self::CleanDeregister => CloseCause::CleanDeregister,
25 Self::Superseded => CloseCause::Superseded,
26 Self::ServerShutdown => CloseCause::ServerShutdown,
27 }
28 }
29}
30
31/// Causes valid only for a `Died` lifecycle record.
32#[derive(Clone, Copy, Debug, PartialEq, Eq)]
33pub enum DiedCause {
34 /// Transport connection was lost.
35 ConnectionLost,
36 /// Participant process was killed.
37 ProcessKilled,
38 /// Participant protocol error.
39 ProtocolError,
40 /// Binding was recovered after an unclean restart.
41 UncleanServerRestart {
42 /// Server incarnation that previously owned the binding.
43 prior_server_incarnation: u64,
44 },
45}
46
47impl DiedCause {
48 /// Converts to the shared close-cause registry without permitting Detached-only causes.
49 #[must_use]
50 pub const fn close_cause(self) -> CloseCause {
51 match self {
52 Self::ConnectionLost => CloseCause::ConnectionLost,
53 Self::ProcessKilled => CloseCause::ProcessKilled,
54 Self::ProtocolError => CloseCause::ProtocolError,
55 Self::UncleanServerRestart {
56 prior_server_incarnation,
57 } => CloseCause::UncleanServerRestart {
58 prior_server_incarnation,
59 },
60 }
61 }
62}
63
64/// Exact record-kind body carried by `ParticipantDelivery`.
65#[derive(Clone, Debug, PartialEq, Eq)]
66pub enum ParticipantRecord {
67 /// Ordinary application record.
68 OrdinaryRecord {
69 /// Verified sender participant.
70 sender_participant_id: ParticipantId,
71 /// Opaque application payload.
72 payload: Vec<u8>,
73 },
74 /// Participant binding was attached.
75 Attached {
76 /// Affected participant.
77 affected_participant_id: ParticipantId,
78 /// New binding epoch.
79 binding_epoch: BindingEpoch,
80 },
81 /// Binding ended with a Detached-class cause.
82 Detached {
83 /// Affected participant.
84 affected_participant_id: ParticipantId,
85 /// Ended binding epoch.
86 binding_epoch: BindingEpoch,
87 /// Type-restricted Detached cause.
88 cause: DetachedCause,
89 },
90 /// Binding ended with a Died-class cause.
91 Died {
92 /// Affected participant.
93 affected_participant_id: ParticipantId,
94 /// Ended binding epoch.
95 binding_epoch: BindingEpoch,
96 /// Type-restricted Died cause.
97 cause: DiedCause,
98 },
99 /// Participant permanently left.
100 Left {
101 /// Affected participant.
102 affected_participant_id: ParticipantId,
103 /// Binding ended by the same Leave commit, if any.
104 ended_binding_epoch: Option<BindingEpoch>,
105 },
106 /// Retained history was explicitly abandoned and compacted.
107 HistoryCompacted {
108 /// Affected participant.
109 affected_participant_id: ParticipantId,
110 /// Last sequence known delivered before abandonment.
111 abandoned_after: DeliverySeq,
112 /// Last abandoned sequence.
113 abandoned_through: DeliverySeq,
114 /// Physical floor selected by the compaction decision.
115 physical_floor_at_decision: DeliverySeq,
116 },
117}
118
119impl ParticipantRecord {
120 /// Returns the explicit record-kind selector.
121 #[must_use]
122 pub const fn record_kind(&self) -> RecordKind {
123 match self {
124 Self::OrdinaryRecord { .. } => RecordKind::OrdinaryRecord,
125 Self::Attached { .. } => RecordKind::Attached,
126 Self::Detached { .. } => RecordKind::Detached,
127 Self::Died { .. } => RecordKind::Died,
128 Self::Left { .. } => RecordKind::Left,
129 Self::HistoryCompacted { .. } => RecordKind::HistoryCompacted,
130 }
131 }
132}
133
134/// Complete participant delivery push body (`0x0201`).
135///
136/// Delivery is at-least-once: the server may push the same
137/// `(conversation_id, delivery_seq)` more than once on one healthy connection
138/// with no transport loss — the publication scan decides offer count and
139/// interleave position, and neither carries application meaning. Every repeat
140/// of one pair
141/// is byte-identical to its first offer, so consumers deduplicate on the pair
142/// (participant contract R-C3, amendment A3, `«PUSH-REOFFER-SCHEDULE»`).
143#[derive(Clone, Debug, PartialEq, Eq)]
144pub struct ParticipantDelivery {
145 /// Conversation multiplexing key.
146 pub conversation_id: ConversationId,
147 /// Delivered record sequence.
148 pub delivery_seq: DeliverySeq,
149 /// Exact tagged record body.
150 pub record: ParticipantRecord,
151}
152
153/// Exhaustive pushed participant control/value.
154#[derive(Clone, Debug, PartialEq, Eq)]
155pub enum ServerPush {
156 /// Observer progress wake (`0x0200`).
157 ObserverProgressed {
158 /// Conversation whose observer advanced.
159 conversation_id: ConversationId,
160 /// Refusal epoch the progress may wake.
161 refused_epoch: ObserverEpoch,
162 /// Current observer progress.
163 observer_progress: DeliverySeq,
164 },
165 /// Participant record delivery (`0x0201`).
166 ParticipantDelivery(ParticipantDelivery),
167 /// Marker-settlement clearing wake (`0x0202`).
168 ///
169 /// Paired with the `MarkerSettlementBackpressure` refusal and delivered
170 /// CONNECTION-SCOPED: exactly to the connections that received that refusal
171 /// in this process lifetime, mirroring `ObserverProgressed`'s connection-level
172 /// delivery. A refused attach client holds no binding and therefore receives
173 /// no `ParticipantDelivery`, so without this wake the only honest client
174 /// behavior would be polling.
175 ///
176 /// It is NEVER sent to a connection refused at the enrollment wrapper: that
177 /// wrapper carries no membership predicate, so its refusal
178 /// (`EnrollmentSettlementBackpressure`) carries neither epoch nor wake
179 /// (participant contract §0.16 condition 2, enrollment wrapper).
180 MarkerSettled {
181 /// Conversation whose marker settlement cleared.
182 conversation_id: ConversationId,
183 /// Refusal epoch this clearing wakes; matched by the stage-11 retry
184 /// against the refusal's own `refused_epoch`.
185 refused_epoch: SettlementEpoch,
186 },
187}
188
189impl ServerPush {
190 /// Returns the stable push discriminant.
191 #[must_use]
192 pub const fn discriminant(&self) -> PushDiscriminant {
193 match self {
194 Self::ObserverProgressed { .. } => PushDiscriminant::ObserverProgressed,
195 Self::ParticipantDelivery(_) => PushDiscriminant::ParticipantDelivery,
196 Self::MarkerSettled { .. } => PushDiscriminant::MarkerSettled,
197 }
198 }
199}