1use alloc::boxed::Box;
24use alloc::collections::{BTreeMap, BTreeSet};
25use alloc::string::String;
26use alloc::vec::Vec;
27
28use umsh_core::{ChannelKey, ChannelTag, NodeHint, PublicKey};
29
30use crate::engine::sequence::MessageHandle;
31use crate::engine::{
32 ArchiveKey, ArchiveResult, BodyRef, CompletionStatus, ComposeError, ComposeIntent,
33 DeliveryState, Diagnostic, Direction, Engine, EngineConfig, Event, MessageMutation,
34 MutationKind, Output, Presence, ReceiveError, ResolvedRef, StreamCheckpoint, Transmission,
35};
36use crate::model::{ConversationKey, MessageType, SenderScope};
37use crate::validate::{DeliveryPath, DirectChannelProfile, Envelope, TextProfile};
38
39#[derive(Clone, Default)]
47pub struct ChannelRegistry {
48 entries: BTreeMap<ChannelTag, ChannelEntry>,
49}
50
51#[derive(Clone, Copy)]
54struct ChannelEntry {
55 key: ChannelKey,
56 max_flood_hops: Option<u8>,
59}
60
61impl ChannelRegistry {
62 pub fn register(&mut self, tag: ChannelTag, key: ChannelKey, max_flood_hops: Option<u8>) {
65 self.entries.insert(
66 tag,
67 ChannelEntry {
68 key,
69 max_flood_hops,
70 },
71 );
72 }
73
74 pub fn remove(&mut self, tag: &ChannelTag) {
75 self.entries.remove(tag);
76 }
77
78 pub fn key(&self, tag: &ChannelTag) -> Option<ChannelKey> {
79 self.entries.get(tag).map(|entry| entry.key)
80 }
81
82 pub fn tag_for(&self, key: &ChannelKey) -> Option<ChannelTag> {
88 self.entries
89 .iter()
90 .find(|(_, entry)| entry.key.0 == key.0)
91 .map(|(tag, _)| *tag)
92 }
93
94 pub fn max_flood_hops(&self, tag: &ChannelTag) -> Option<u8> {
98 self.entries.get(tag).and_then(|entry| entry.max_flood_hops)
99 }
100
101 pub fn contains(&self, tag: &ChannelTag) -> bool {
102 self.entries.contains_key(tag)
103 }
104
105 pub fn tags(&self) -> impl Iterator<Item = ChannelTag> + '_ {
106 self.entries.keys().copied()
107 }
108}
109
110#[derive(Clone, Copy, Debug, PartialEq, Eq)]
114pub struct Seeds {
115 pub session_id: u64,
120 pub jitter_seed: u64,
123}
124
125#[derive(Clone, Copy, Debug, PartialEq, Eq)]
127pub enum TextSource {
128 Direct { peer: PublicKey },
130 ChannelGroup {
133 channel: ChannelTag,
134 hint: NodeHint,
135 full_key: Option<PublicKey>,
136 },
137 ChannelDirect {
139 channel: ChannelTag,
140 peer: PublicKey,
141 },
142}
143
144impl TextSource {
145 pub fn channel(&self) -> Option<ChannelTag> {
146 match self {
147 Self::Direct { .. } => None,
148 Self::ChannelGroup { channel, .. } | Self::ChannelDirect { channel, .. } => {
149 Some(*channel)
150 }
151 }
152 }
153
154 pub fn envelope(&self) -> Envelope {
160 match *self {
161 Self::Direct { peer } => Envelope {
162 path: DeliveryPath::Unicast,
163 conversation: ConversationKey::Direct { peer },
164 sender: SenderScope::Peer(peer),
165 },
166 Self::ChannelGroup { channel, hint, .. } => Envelope {
167 path: DeliveryPath::Multicast,
168 conversation: ConversationKey::ChannelGroup { channel },
169 sender: SenderScope::ClaimedMember(hint),
170 },
171 Self::ChannelDirect { channel, peer } => Envelope {
172 path: DeliveryPath::BlindUnicast,
173 conversation: ConversationKey::ChannelDirect { channel, peer },
174 sender: SenderScope::Peer(peer),
175 },
176 }
177 }
178
179 pub fn full_key(&self) -> Option<PublicKey> {
181 match *self {
182 Self::Direct { peer } | Self::ChannelDirect { peer, .. } => Some(peer),
183 Self::ChannelGroup { full_key, .. } => full_key,
184 }
185 }
186}
187
188#[derive(Clone, Debug, PartialEq, Eq)]
190pub struct Mutation {
191 pub handle: MessageHandle,
192 pub revision: u32,
194 pub change: Change,
195}
196
197impl Mutation {
198 pub fn from_engine<'e>(
203 mutation: MessageMutation,
204 body: impl Fn(&BodyRef) -> &'e str,
205 sender_key: impl FnOnce(ConversationKey, SenderScope) -> Option<PublicKey>,
206 ) -> Self {
207 let change = match mutation.kind {
208 MutationKind::Insert {
209 conversation,
210 sender,
211 direction,
212 message_type,
213 wire_id,
214 epoch,
215 client_token,
216 sender_handle,
217 regarding,
218 bg_color,
219 text_color,
220 body: text,
221 status,
222 presence,
223 late,
224 notify,
225 } => Change::Insert(Insert {
226 conversation,
227 sender,
228 sender_key: sender_key(conversation, sender),
229 direction,
230 message_type,
231 wire_id,
232 epoch,
233 client_token,
234 sender_handle: sender_handle.map(|value| String::from(body(&value))),
235 regarding,
236 bg_color,
237 text_color,
238 body: String::from(body(&text)),
239 status,
240 presence,
241 late,
242 notify,
243 }),
244 MutationKind::UpdateBody {
245 body: text,
246 status,
247 late,
248 notify,
249 } => Change::UpdateBody {
250 body: String::from(body(&text)),
251 status,
252 late,
253 notify,
254 },
255 MutationKind::Edit {
256 conversation,
257 original,
258 body: text,
259 } => Change::Edit {
260 conversation,
261 original,
262 body: String::from(body(&text)),
263 },
264 MutationKind::Delete {
265 conversation,
266 original,
267 } => Change::Delete {
268 conversation,
269 original,
270 },
271 };
272 Self {
273 handle: mutation.handle,
274 revision: mutation.revision,
275 change,
276 }
277 }
278
279 pub fn describes_receipt(&self) -> bool {
283 match &self.change {
284 Change::Insert(insert) => {
285 insert.direction == Direction::Inbound && insert.presence == Presence::Present
286 }
287 Change::UpdateBody { .. } => true,
288 Change::Edit { .. } | Change::Delete { .. } => false,
289 }
290 }
291}
292
293#[derive(Clone, Debug, PartialEq, Eq)]
294pub enum Change {
295 Insert(Insert),
296 UpdateBody {
298 body: String,
299 status: CompletionStatus,
300 late: bool,
301 notify: bool,
302 },
303 Edit {
305 conversation: ConversationKey,
306 original: ResolvedRef,
307 body: String,
308 },
309 Delete {
311 conversation: ConversationKey,
312 original: ResolvedRef,
313 },
314}
315
316#[derive(Clone, Debug, PartialEq, Eq)]
318pub struct Insert {
319 pub conversation: ConversationKey,
320 pub sender: SenderScope,
321 pub sender_key: Option<PublicKey>,
325 pub direction: Direction,
326 pub message_type: MessageType,
327 pub wire_id: Option<u8>,
328 pub epoch: u16,
329 pub client_token: Option<u32>,
330 pub sender_handle: Option<String>,
331 pub regarding: Option<ResolvedRef>,
332 pub bg_color: Option<[u8; 3]>,
333 pub text_color: Option<[u8; 3]>,
334 pub body: String,
335 pub status: CompletionStatus,
336 pub presence: Presence,
337 pub late: bool,
338 pub notify: bool,
339}
340
341#[derive(Clone, Copy, Debug, PartialEq, Eq)]
343pub struct Delivery {
344 pub handle: MessageHandle,
345 pub fragment: Option<u8>,
346 pub state: DeliveryState,
347 pub via_flood: bool,
350}
351
352#[derive(Clone, Copy, Debug, PartialEq, Eq)]
354pub struct ArchiveLookup {
355 pub request_id: u32,
356 pub key: ArchiveKey,
357}
358
359#[derive(Clone, Debug, PartialEq, Eq)]
361pub struct ArchiveWrite {
362 pub key: ArchiveKey,
363 pub payload: Vec<u8>,
364}
365
366#[derive(Clone, Copy, Debug, PartialEq, Eq)]
372pub struct ArchiveDelete {
373 pub conversation: ConversationKey,
374 pub message_id: u8,
375}
376
377#[derive(Clone, Copy, Debug, PartialEq, Eq)]
384pub struct SenderResolution {
385 pub channel: ChannelTag,
386 pub hint: NodeHint,
387 pub key: PublicKey,
388}
389
390#[derive(Clone, Debug, PartialEq, Eq)]
392pub enum ChatOutput {
393 Mutation(Mutation),
394 Delivery(Delivery),
395 Lookup(ArchiveLookup),
396 SenderResolved(SenderResolution),
397 StoreArchive(ArchiveWrite),
398 DeleteArchive(ArchiveDelete),
399 Event(Event),
402 Diagnostic(Diagnostic),
403}
404
405#[derive(Debug)]
410pub struct Outbound {
411 session_id: u64,
412 transmissions: Vec<Transmission>,
413}
414
415impl Outbound {
416 pub fn session_id(&self) -> u64 {
420 self.session_id
421 }
422
423 pub fn is_empty(&self) -> bool {
424 self.transmissions.is_empty()
425 }
426
427 pub fn len(&self) -> usize {
428 self.transmissions.len()
429 }
430
431 pub fn transmissions(&self) -> &[Transmission] {
432 &self.transmissions
433 }
434
435 pub fn into_transmissions(self) -> Vec<Transmission> {
436 self.transmissions
437 }
438}
439
440#[derive(Debug)]
442pub struct Drain {
443 pub session_id: u64,
445 pub outputs: Vec<ChatOutput>,
446 pub outbound: Outbound,
447}
448
449impl Drain {
450 pub fn is_empty(&self) -> bool {
451 self.outputs.is_empty() && self.outbound.is_empty()
452 }
453}
454
455#[must_use = "a composed batch holds its frames until it is committed or rejected"]
462#[derive(Debug)]
463pub struct PendingBatch {
464 id: u64,
465 session_id: u64,
466 pub checkpoint: StreamCheckpoint,
467 pub archive_deletes: Vec<ArchiveDelete>,
468 pub archives: Vec<ArchiveWrite>,
469 pub mutations: Vec<Mutation>,
470 pub outputs: Vec<ChatOutput>,
472 transmissions: Vec<Transmission>,
473}
474
475impl PendingBatch {
476 pub fn id(&self) -> u64 {
478 self.id
479 }
480
481 pub fn session_id(&self) -> u64 {
483 self.session_id
484 }
485
486 pub fn transmission_count(&self) -> usize {
491 self.transmissions.len()
492 }
493}
494
495#[derive(Clone, Copy, Debug, PartialEq, Eq)]
496pub enum ChatComposeError {
497 BatchPending,
501 Engine(ComposeError),
502 Incomplete,
505}
506
507#[derive(Clone, Copy, Debug, PartialEq, Eq)]
508pub enum ChatReceiveError {
509 ChannelNotHeld,
511 Engine(ReceiveError),
512}
513
514#[derive(Clone, Copy, Debug, PartialEq, Eq)]
516pub struct BatchMismatch;
517
518#[derive(Clone, Copy, Debug, PartialEq, Eq)]
520pub enum TransmitUpdate {
521 Sent,
522 Acked {
523 via_flood: bool,
525 },
526 Failed,
527}
528
529pub struct ChatFacade<
531 P: TextProfile + Clone = DirectChannelProfile,
532 const SLOTS: usize = 4,
533 const PAGES: usize = 24,
534> {
535 engine: Box<Engine<P, SLOTS, PAGES>>,
537 profile: P,
538 config: EngineConfig,
539 local_key: PublicKey,
540 local_handle: String,
542 session_id: u64,
543 channels: ChannelRegistry,
544 members: BTreeMap<(ChannelTag, [u8; 3]), PublicKey>,
547 flooded: BTreeSet<(u32, Option<u8>)>,
550 staged: Vec<ChatOutput>,
551 staged_transmissions: Vec<Transmission>,
552 next_batch_id: u64,
553 pending_batch: Option<u64>,
554}
555
556struct Collected {
559 checkpoint: Option<StreamCheckpoint>,
560 outputs: Vec<ChatOutput>,
561 transmissions: Vec<Transmission>,
562}
563
564impl<P: TextProfile + Clone, const SLOTS: usize, const PAGES: usize> ChatFacade<P, SLOTS, PAGES> {
565 pub fn new(profile: P, local_key: PublicKey, config: EngineConfig, seeds: Seeds) -> Self {
566 Self {
567 engine: Box::new(Engine::new(
568 profile.clone(),
569 local_key,
570 config.clone(),
571 seeds.jitter_seed,
572 )),
573 profile,
574 config,
575 local_key,
576 local_handle: String::new(),
577 session_id: seeds.session_id.max(1),
578 channels: ChannelRegistry::default(),
579 members: BTreeMap::new(),
580 flooded: BTreeSet::new(),
581 staged: Vec::new(),
582 staged_transmissions: Vec::new(),
583 next_batch_id: 1,
584 pending_batch: None,
585 }
586 }
587
588 pub fn local_key(&self) -> PublicKey {
589 self.local_key
590 }
591
592 pub fn session_id(&self) -> u64 {
594 self.session_id
595 }
596
597 pub fn channels(&self) -> &ChannelRegistry {
598 &self.channels
599 }
600
601 pub fn channels_mut(&mut self) -> &mut ChannelRegistry {
602 &mut self.channels
603 }
604
605 pub fn has_pending_batch(&self) -> bool {
607 self.pending_batch.is_some()
608 }
609
610 pub fn set_local_handle(&mut self, handle: &str) {
613 self.local_handle.clear();
614 self.local_handle.push_str(handle);
615 self.engine.set_local_handle(handle);
616 }
617
618 pub fn local_handle(&self) -> &str {
619 &self.local_handle
620 }
621
622 pub fn member_key(&self, channel: &ChannelTag, hint: &NodeHint) -> Option<PublicKey> {
624 self.members.get(&(*channel, hint.0)).copied()
625 }
626
627 pub fn restore(&mut self, checkpoints: &[StreamCheckpoint], now_ms: u64) {
630 self.engine.restore(checkpoints, now_ms);
631 self.stage();
632 }
633
634 pub fn compose(
638 &mut self,
639 conversation: ConversationKey,
640 client_token: u32,
641 intent: ComposeIntent<'_>,
642 stored: Option<&StreamCheckpoint>,
643 now_ms: u64,
644 ) -> Result<PendingBatch, ChatComposeError> {
645 if self.pending_batch.is_some() {
646 return Err(ChatComposeError::BatchPending);
647 }
648 if let Some(stored) = stored.filter(|stored| stored.conversation == conversation) {
649 self.engine.recall(stored);
650 }
651 if let Err(error) = self
652 .engine
653 .compose(conversation, client_token, intent, now_ms)
654 {
655 self.discard_outputs(now_ms);
657 return Err(ChatComposeError::Engine(error));
658 }
659 let collected = self.collect();
660 let Some(checkpoint) = collected.checkpoint else {
661 self.abandon(collected.transmissions, now_ms);
662 return Err(ChatComposeError::Incomplete);
663 };
664 let mut batch = PendingBatch {
665 id: self.next_batch_id,
666 session_id: self.session_id,
667 checkpoint,
668 archive_deletes: Vec::new(),
669 archives: Vec::new(),
670 mutations: Vec::new(),
671 outputs: Vec::new(),
672 transmissions: collected.transmissions,
673 };
674 for output in collected.outputs {
675 match output {
676 ChatOutput::Mutation(mutation) => batch.mutations.push(mutation),
677 ChatOutput::StoreArchive(archive) => batch.archives.push(archive),
678 ChatOutput::DeleteArchive(delete) => batch.archive_deletes.push(delete),
679 other => batch.outputs.push(other),
680 }
681 }
682 self.next_batch_id = self.next_batch_id.wrapping_add(1).max(1);
683 self.pending_batch = Some(batch.id);
684 Ok(batch)
685 }
686
687 pub fn commit(&mut self, batch: PendingBatch) -> Result<Outbound, BatchMismatch> {
689 if self.pending_batch != Some(batch.id) {
690 return Err(BatchMismatch);
691 }
692 self.pending_batch = None;
693 Ok(Outbound {
694 session_id: batch.session_id,
695 transmissions: batch.transmissions,
696 })
697 }
698
699 pub fn reject(
711 &mut self,
712 batch: PendingBatch,
713 checkpoints: &[StreamCheckpoint],
714 seeds: Seeds,
715 now_ms: u64,
716 ) -> Result<Drain, BatchMismatch> {
717 if self.pending_batch != Some(batch.id) {
718 return Err(BatchMismatch);
719 }
720 self.pending_batch = None;
721 for transmission in &batch.transmissions {
722 self.engine.transmit_update(
723 transmission.transmission_id,
724 DeliveryState::Failed,
725 now_ms,
726 );
727 }
728 self.stage();
729 let drain = self.drain();
730
731 *self.engine = Engine::new(
732 self.profile.clone(),
733 self.local_key,
734 self.config.clone(),
735 seeds.jitter_seed,
736 );
737 self.engine.set_local_handle(&self.local_handle);
738 self.session_id = seeds.session_id.max(1);
739 self.flooded.clear();
740 self.engine.restore(checkpoints, now_ms);
741 self.discard_outputs(now_ms);
742 Ok(drain)
743 }
744
745 pub fn receive(
747 &mut self,
748 source: TextSource,
749 payload: &[u8],
750 now_ms: u64,
751 ) -> Result<(), ChatReceiveError> {
752 if let Some(channel) = source.channel()
753 && !self.channels.contains(&channel)
754 {
755 return Err(ChatReceiveError::ChannelNotHeld);
756 }
757 if let TextSource::ChannelGroup {
758 channel,
759 hint,
760 full_key: Some(key),
761 } = source
762 {
763 self.resolve_member(channel, hint, key);
764 }
765 let result = self
766 .engine
767 .receive(&source.envelope(), source.full_key(), payload, now_ms)
768 .map_err(ChatReceiveError::Engine);
769 self.stage();
770 result
771 }
772
773 pub fn archive_result(&mut self, request_id: u32, result: ArchiveResult<'_>, now_ms: u64) {
775 self.engine.archive_result(request_id, result, now_ms);
776 self.stage();
777 }
778
779 pub fn retransmit(&mut self, key: &ArchiveKey, handle: MessageHandle, payload: &[u8]) -> bool {
783 let queued = self.engine.retransmit(key, handle, payload);
784 self.stage();
785 queued
786 }
787
788 pub fn adopt(
793 &mut self,
794 conversation: ConversationKey,
795 message_id: u8,
796 epoch: u16,
797 stored: Option<&StreamCheckpoint>,
798 now_ms: u64,
799 ) -> Option<MessageHandle> {
800 if let Some(stored) = stored.filter(|stored| stored.conversation == conversation) {
801 self.engine.recall(stored);
802 }
803 let handle = self.engine.adopt(conversation, message_id, epoch, now_ms);
804 self.stage();
805 handle
806 }
807
808 pub fn tick(&mut self, now_ms: u64) {
810 self.engine.tick(now_ms);
811 self.stage();
812 }
813
814 pub fn transmit_update(
817 &mut self,
818 session_id: u64,
819 transmission_id: u32,
820 update: TransmitUpdate,
821 now_ms: u64,
822 ) {
823 if session_id != self.session_id {
824 return;
825 }
826 let state = match update {
827 TransmitUpdate::Sent => DeliveryState::Sent,
828 TransmitUpdate::Acked { via_flood } => {
829 if via_flood
830 && let Some((handle, fragment)) = self.engine.in_flight_frame(transmission_id)
831 {
832 self.flooded.insert((handle.0, fragment));
833 }
834 DeliveryState::Acked
835 }
836 TransmitUpdate::Failed => DeliveryState::Failed,
837 };
838 self.engine.transmit_update(transmission_id, state, now_ms);
839 self.stage();
840 }
841
842 pub fn drain(&mut self) -> Drain {
844 Drain {
845 session_id: self.session_id,
846 outputs: core::mem::take(&mut self.staged),
847 outbound: Outbound {
848 session_id: self.session_id,
849 transmissions: core::mem::take(&mut self.staged_transmissions),
850 },
851 }
852 }
853
854 fn resolve_member(&mut self, channel: ChannelTag, hint: NodeHint, key: PublicKey) {
855 if self.members.contains_key(&(channel, hint.0)) {
856 return;
857 }
858 self.members.insert((channel, hint.0), key);
859 self.staged
860 .push(ChatOutput::SenderResolved(SenderResolution {
861 channel,
862 hint,
863 key,
864 }));
865 }
866
867 fn stage(&mut self) {
869 let collected = self.collect();
870 debug_assert!(collected.checkpoint.is_none());
872 self.staged.extend(collected.outputs);
873 self.staged_transmissions.extend(collected.transmissions);
874 }
875
876 fn discard_outputs(&mut self, now_ms: u64) {
879 let collected = self.collect();
880 self.abandon(collected.transmissions, now_ms);
881 }
882
883 fn abandon(&mut self, transmissions: Vec<Transmission>, now_ms: u64) {
884 for transmission in transmissions {
885 self.engine.transmit_update(
886 transmission.transmission_id,
887 DeliveryState::Failed,
888 now_ms,
889 );
890 }
891 while self.engine.poll_output().is_some() {}
892 }
893
894 fn collect(&mut self) -> Collected {
895 let mut collected = Collected {
896 checkpoint: None,
897 outputs: Vec::new(),
898 transmissions: Vec::new(),
899 };
900 while let Some(output) = self.engine.poll_output() {
903 match output {
904 Output::Transmit(transmission) => {
905 if let Some(key) = transmission.archive {
906 collected
907 .outputs
908 .push(ChatOutput::StoreArchive(ArchiveWrite {
909 key,
910 payload: transmission.payload.to_vec(),
911 }));
912 }
913 collected.transmissions.push(transmission);
914 }
915 Output::StoreCheckpoint {
916 conversation,
917 next_id,
918 epoch,
919 } => {
920 collected.checkpoint = Some(StreamCheckpoint {
921 conversation,
922 next_id,
923 epoch,
924 });
925 }
926 Output::LookupOutbound {
927 request_id,
928 conversation,
929 sequence,
930 } => collected.outputs.push(ChatOutput::Lookup(ArchiveLookup {
931 request_id,
932 key: ArchiveKey {
933 conversation,
934 message_id: sequence.message_id,
935 fragment: sequence.fragment.map(|fragment| fragment.index),
936 },
937 })),
938 Output::StoreArchive { key, payload } => {
939 collected
940 .outputs
941 .push(ChatOutput::StoreArchive(ArchiveWrite {
942 key,
943 payload: payload.to_vec(),
944 }));
945 }
946 Output::DeleteArchive {
947 conversation,
948 message_id,
949 } => collected
950 .outputs
951 .push(ChatOutput::DeleteArchive(ArchiveDelete {
952 conversation,
953 message_id,
954 })),
955 Output::StoreMessage(mutation) => {
956 let mutation = self.owned_mutation(mutation);
957 collected.outputs.push(ChatOutput::Mutation(mutation));
958 }
959 Output::Event(Event::DeliveryStateChanged {
960 handle,
961 fragment,
962 state,
963 }) => collected.outputs.push(ChatOutput::Delivery(Delivery {
964 handle,
965 fragment,
966 state,
967 via_flood: state == DeliveryState::Acked
968 && self.flooded.remove(&(handle.0, fragment)),
969 })),
970 Output::Event(event) => collected.outputs.push(ChatOutput::Event(event)),
971 Output::Diagnostic(diagnostic) => {
972 collected.outputs.push(ChatOutput::Diagnostic(diagnostic));
973 }
974 }
975 }
976 collected
977 }
978
979 fn owned_mutation(&self, mutation: MessageMutation) -> Mutation {
980 Mutation::from_engine(
981 mutation,
982 |body| self.engine.body(body),
983 |conversation, sender| self.sender_key(conversation, sender),
984 )
985 }
986
987 fn sender_key(&self, conversation: ConversationKey, sender: SenderScope) -> Option<PublicKey> {
990 match sender {
991 SenderScope::Peer(peer) => Some(peer),
992 SenderScope::Local => None,
993 SenderScope::ClaimedMember(hint) => match conversation {
994 ConversationKey::ChannelGroup { channel } => self.member_key(&channel, &hint),
995 _ => None,
996 },
997 }
998 }
999}
1000
1001#[cfg(test)]
1002mod tests;