liminal_server/server/participant/
dispatch_impact.rs1use std::collections::{BTreeMap, BTreeSet};
9
10use liminal_protocol::wire::{BindingEpoch, ConversationId, ParticipantId};
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
15pub enum DispatchEffect {
16 Published,
18 Acknowledged,
20 BindingChanged,
22 EpisodeChanged,
24 Retired,
26}
27
28#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
30pub struct DispatchTarget {
31 participant_id: ParticipantId,
32 binding_epoch: BindingEpoch,
33}
34
35impl DispatchTarget {
36 #[must_use]
38 pub const fn new(participant_id: ParticipantId, binding_epoch: BindingEpoch) -> Self {
39 Self {
40 participant_id,
41 binding_epoch,
42 }
43 }
44
45 #[must_use]
47 pub const fn participant_id(self) -> ParticipantId {
48 self.participant_id
49 }
50
51 #[must_use]
53 pub const fn binding_epoch(self) -> BindingEpoch {
54 self.binding_epoch
55 }
56}
57
58#[derive(Clone, Debug, PartialEq, Eq)]
60pub enum DispatchImpact {
61 Unchanged,
63 Changed {
65 conversation_id: ConversationId,
67 effects: BTreeMap<DispatchEffect, BTreeSet<DispatchTarget>>,
70 },
71}
72
73impl DispatchImpact {
74 #[must_use]
76 pub const fn conversation_id(&self) -> Option<ConversationId> {
77 match self {
78 Self::Unchanged => None,
79 Self::Changed {
80 conversation_id, ..
81 } => Some(*conversation_id),
82 }
83 }
84
85 #[must_use]
87 pub const fn effects(&self) -> Option<&BTreeMap<DispatchEffect, BTreeSet<DispatchTarget>>> {
88 match self {
89 Self::Unchanged => None,
90 Self::Changed { effects, .. } => Some(effects),
91 }
92 }
93
94 #[must_use]
97 pub fn target_union(&self) -> BTreeSet<DispatchTarget> {
98 match self {
99 Self::Unchanged => BTreeSet::new(),
100 Self::Changed { effects, .. } => effects
101 .values()
102 .flat_map(|targets| targets.iter().copied())
103 .collect(),
104 }
105 }
106}
107
108#[derive(Debug, Default)]
110pub struct DispatchImpactAccumulator {
111 effects: BTreeMap<DispatchEffect, BTreeSet<DispatchTarget>>,
112 staged_effects: BTreeMap<DispatchEffect, BTreeSet<DispatchTarget>>,
113}
114
115impl DispatchImpactAccumulator {
116 #[must_use]
118 pub const fn new() -> Self {
119 Self {
120 effects: BTreeMap::new(),
121 staged_effects: BTreeMap::new(),
122 }
123 }
124
125 pub fn record(
131 &mut self,
132 effect: DispatchEffect,
133 targets: impl IntoIterator<Item = DispatchTarget>,
134 ) {
135 self.effects.entry(effect).or_default().extend(targets);
136 }
137
138 pub(crate) fn stage(
141 &mut self,
142 effect: DispatchEffect,
143 targets: impl IntoIterator<Item = DispatchTarget>,
144 ) {
145 self.staged_effects
146 .entry(effect)
147 .or_default()
148 .extend(targets);
149 }
150
151 pub(crate) fn install_staged(&mut self) {
153 let staged = core::mem::take(&mut self.staged_effects);
154 for (effect, targets) in staged {
155 self.record(effect, targets);
156 }
157 }
158
159 pub(crate) fn has_staged(&self) -> bool {
161 !self.staged_effects.is_empty()
162 }
163
164 pub fn merge(&mut self, prefix: Self) {
167 for (effect, targets) in prefix.effects {
168 self.record(effect, targets);
169 }
170 for (effect, targets) in prefix.staged_effects {
171 self.stage(effect, targets);
172 }
173 }
174
175 #[must_use]
177 pub fn is_empty(&self) -> bool {
178 self.effects.is_empty()
179 }
180
181 #[must_use]
183 pub fn finish(self, conversation_id: ConversationId) -> DispatchImpact {
184 if self.effects.is_empty() {
185 DispatchImpact::Unchanged
186 } else {
187 DispatchImpact::Changed {
188 conversation_id,
189 effects: self.effects,
190 }
191 }
192 }
193}