umsh_text/
chat.rs

1//! Allocated chat facade around the text engine (feature `alloc`).
2//!
3//! [`ChatFacade`] owns an [`Engine`] and the state every platform keeps
4//! beside it: the channels a session can hold conversations in, the full
5//! keys learned for channel members known only by hint, and the bookkeeping
6//! that lets a rejected compose put the engine back the way storage last saw
7//! it. Conversations are typed [`ConversationKey`]s; platform address formats
8//! are codecs over them, outside this module.
9//!
10//! Engine outputs leave the facade owned—bodies copied out of the engine's
11//! render arena—and in the order the engine produced them. Frames for the
12//! radio leave only inside an [`Outbound`], which only the facade makes:
13//!
14//! - [`ChatFacade::compose`] returns a [`PendingBatch`] that holds its frames
15//!   until [`ChatFacade::commit`] releases them, after the platform has stored
16//!   the batch's checkpoint, archive changes, and transcript mutations. A
17//!   failed store calls [`ChatFacade::reject`] instead, which fails the frames
18//!   and rebuilds the engine from the checkpoints storage still holds.
19//! - [`ChatFacade::drain`] returns everything else the engine produced, with
20//!   the frames that answer resend requests and ask for repairs. None of them
21//!   allocates a wire ID, so none waits on storage.
22
23use 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/// The channels a session can hold a conversation in, and the keys to reach
40/// them.
41///
42/// Channel membership already lives in the MAC, which resolves an inbound
43/// frame to a key by authenticating it. This is the chat layer's own view: it
44/// maps between the tag a conversation is keyed by and the key a send needs,
45/// in both directions.
46#[derive(Clone, Default)]
47pub struct ChannelRegistry {
48    entries: BTreeMap<ChannelTag, ChannelEntry>,
49}
50
51/// One registered channel: its key, and how far the user lets its traffic
52/// travel.
53#[derive(Clone, Copy)]
54struct ChannelEntry {
55    key: ChannelKey,
56    /// The user's own flood-hop ceiling for this channel, or `None` to use
57    /// the session-wide default.
58    max_flood_hops: Option<u8>,
59}
60
61impl ChannelRegistry {
62    /// Hold `tag`'s channel, reached with `key`. Registering a held channel
63    /// again replaces its key and ceiling.
64    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    /// The tag of the held channel `key` reaches.
83    ///
84    /// A received frame names its channel by the key that authenticated it,
85    /// which is the truth about which channel it belongs to: distinct keys
86    /// may share the two-byte identifier the frame carried.
87    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    /// The flood-hop ceiling registered for a channel, if the user set one.
95    /// `None` both for a channel left at the default and for one not
96    /// registered at all.
97    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/// Values a facade session is started from.
111///
112/// Both come from the platform, so the facade needs no randomness of its own.
113#[derive(Clone, Copy, Debug, PartialEq, Eq)]
114pub struct Seeds {
115    /// Names the engine instance whose [`MessageHandle`]s and transmission
116    /// IDs a platform holds, so they cannot be mistaken for another
117    /// instance's after a restart or a rejected batch. Never zero: zero is
118    /// taken as one.
119    pub session_id: u64,
120    /// Drives group-repair desynchronization only; scheduling randomness,
121    /// not security material.
122    pub jitter_seed: u64,
123}
124
125/// Who a received text payload came from, and over what.
126#[derive(Clone, Copy, Debug, PartialEq, Eq)]
127pub enum TextSource {
128    /// Authenticated unicast from a peer.
129    Direct { peer: PublicKey },
130    /// Multicast to a channel. The sender claims a hint; the full key is
131    /// present only when they addressed the frame with it.
132    ChannelGroup {
133        channel: ChannelTag,
134        hint: NodeHint,
135        full_key: Option<PublicKey>,
136    },
137    /// Blind unicast to us over a channel key, from an addressable peer.
138    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    /// The engine envelope for a payload from this source.
155    ///
156    /// A multicast member is always the claimed hint, with any full key
157    /// passed beside it: naming the key here instead would split one member
158    /// into two streams the moment a frame omitted it.
159    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    /// The sender's full key, when the frame carried or authenticated one.
180    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/// An owned transcript mutation (see [`MessageMutation`]).
189#[derive(Clone, Debug, PartialEq, Eq)]
190pub struct Mutation {
191    pub handle: MessageHandle,
192    /// Apply only if newer than the last revision applied for `handle`.
193    pub revision: u32,
194    pub change: Change,
195}
196
197impl Mutation {
198    /// The owned form of one of the engine's mutations, read while it is
199    /// fresh: `body` is [`Engine::body`] until the engine's queue empties.
200    /// `sender_key` gives the full key behind a sender scope, where one is
201    /// known.
202    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    /// Whether this record describes content that arrived in the frame that
280    /// produced it, as opposed to an outbound echo, a gap placeholder, or a
281    /// change to an earlier message. Radio metadata belongs only on these.
282    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    /// Replace the rendered body (reassembly progress or finalization).
297    UpdateBody {
298        body: String,
299        status: CompletionStatus,
300        late: bool,
301        notify: bool,
302    },
303    /// Apply an edit to the referenced original message.
304    Edit {
305        conversation: ConversationKey,
306        original: ResolvedRef,
307        body: String,
308    },
309    /// Mark the referenced original message deleted.
310    Delete {
311        conversation: ConversationKey,
312        original: ResolvedRef,
313    },
314}
315
316/// A new transcript record (see [`MutationKind::Insert`]).
317#[derive(Clone, Debug, PartialEq, Eq)]
318pub struct Insert {
319    pub conversation: ConversationKey,
320    pub sender: SenderScope,
321    /// The sender's full key: an authenticated peer's, or the key a claimed
322    /// member's hint has resolved to. `None` for our own messages and for
323    /// members not yet resolved.
324    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/// A change in a frame's delivery.
342#[derive(Clone, Copy, Debug, PartialEq, Eq)]
343pub struct Delivery {
344    pub handle: MessageHandle,
345    pub fragment: Option<u8>,
346    pub state: DeliveryState,
347    /// The acknowledgment answered the flood that followed a miss on the
348    /// peer's route, not the routed send itself.
349    pub via_flood: bool,
350}
351
352/// Look up resendable material; answer with [`ChatFacade::archive_result`].
353#[derive(Clone, Copy, Debug, PartialEq, Eq)]
354pub struct ArchiveLookup {
355    pub request_id: u32,
356    pub key: ArchiveKey,
357}
358
359/// Store this exact payload as resendable material under `key`.
360#[derive(Clone, Debug, PartialEq, Eq)]
361pub struct ArchiveWrite {
362    pub key: ArchiveKey,
363    pub payload: Vec<u8>,
364}
365
366/// Retire every archived fragment stored under one message ID.
367///
368/// Emitted when an edit or delete supersedes that ID's content, ahead of any
369/// replacement [`ArchiveWrite`], so superseded content can never be served
370/// to a resend request again.
371#[derive(Clone, Copy, Debug, PartialEq, Eq)]
372pub struct ArchiveDelete {
373    pub conversation: ConversationKey,
374    pub message_id: u8,
375}
376
377/// A channel member previously known only by their claimed hint has been
378/// resolved to a full public key.
379///
380/// Announced once per channel and hint: the first full key seen for a hint
381/// is the one kept, since a hint is only three bytes and two members could
382/// claim the same one.
383#[derive(Clone, Copy, Debug, PartialEq, Eq)]
384pub struct SenderResolution {
385    pub channel: ChannelTag,
386    pub hint: NodeHint,
387    pub key: PublicKey,
388}
389
390/// One owned facade output.
391#[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    /// Engine events other than delivery changes (repair progress, remote
400    /// unavailability).
401    Event(Event),
402    Diagnostic(Diagnostic),
403}
404
405/// Frames released to the radio, from one engine session.
406///
407/// Only the facade makes one: by committing a [`PendingBatch`], or in a
408/// [`Drain`].
409#[derive(Debug)]
410pub struct Outbound {
411    session_id: u64,
412    transmissions: Vec<Transmission>,
413}
414
415impl Outbound {
416    /// The session whose transmission IDs these are. Delivery reports name
417    /// it, so a report for a frame of an engine since rebuilt is ignored
418    /// rather than applied to an unrelated frame that reuses the ID.
419    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/// What the facade produced since the last drain.
441#[derive(Debug)]
442pub struct Drain {
443    /// The session whose handles the outputs name.
444    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/// A composed message waiting to be stored.
456///
457/// Store, in order, the checkpoint, the archive deletes, the archives, and
458/// the mutations; then [`ChatFacade::commit`] releases the frames. If storing
459/// fails, [`ChatFacade::reject`] fails them instead. The frames cannot reach
460/// the radio any other way.
461#[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    /// Anything else the compose produced: deliveries, events, diagnostics.
471    pub outputs: Vec<ChatOutput>,
472    transmissions: Vec<Transmission>,
473}
474
475impl PendingBatch {
476    /// Unique within a facade; never zero.
477    pub fn id(&self) -> u64 {
478        self.id
479    }
480
481    /// The session whose handles the mutations name.
482    pub fn session_id(&self) -> u64 {
483        self.session_id
484    }
485
486    /// How many frames the message goes out in. For an original message
487    /// this matches the archive count, but an edit's frames carry the
488    /// editing option while its replacement archive is re-encoded without
489    /// it, so the two can fragment differently.
490    pub fn transmission_count(&self) -> usize {
491        self.transmissions.len()
492    }
493}
494
495#[derive(Clone, Copy, Debug, PartialEq, Eq)]
496pub enum ChatComposeError {
497    /// An earlier batch is still waiting to be committed or rejected.
498    /// Rejecting rebuilds the engine, which would invalidate a second batch
499    /// composed in the meantime, so only one may be outstanding.
500    BatchPending,
501    Engine(ComposeError),
502    /// The engine's output queue overflowed and the batch is incomplete;
503    /// nothing was released.
504    Incomplete,
505}
506
507#[derive(Clone, Copy, Debug, PartialEq, Eq)]
508pub enum ChatReceiveError {
509    /// The source names a channel this session does not hold.
510    ChannelNotHeld,
511    Engine(ReceiveError),
512}
513
514/// The batch is not the one this facade is waiting on.
515#[derive(Clone, Copy, Debug, PartialEq, Eq)]
516pub struct BatchMismatch;
517
518/// Transport progress for a released frame.
519#[derive(Clone, Copy, Debug, PartialEq, Eq)]
520pub enum TransmitUpdate {
521    Sent,
522    Acked {
523        /// The acknowledgment answered a route-retry flood.
524        via_flood: bool,
525    },
526    Failed,
527}
528
529/// The chat facade.
530pub struct ChatFacade<
531    P: TextProfile + Clone = DirectChannelProfile,
532    const SLOTS: usize = 4,
533    const PAGES: usize = 24,
534> {
535    /// Kept off the caller's stack: the engine is a large bounded value.
536    engine: Box<Engine<P, SLOTS, PAGES>>,
537    profile: P,
538    config: EngineConfig,
539    local_key: PublicKey,
540    /// Reapplied whenever the engine is rebuilt.
541    local_handle: String,
542    session_id: u64,
543    channels: ChannelRegistry,
544    /// Full keys learned for claimed member hints, per channel; the first
545    /// key seen for a hint stays.
546    members: BTreeMap<(ChannelTag, [u8; 3]), PublicKey>,
547    /// Frames, as `(handle, fragment)`, whose acknowledgment came back
548    /// through a route-retry flood, held until the delivery is drained.
549    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
556/// Engine outputs collected and converted, before they are sorted into a
557/// batch or a drain.
558struct 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    /// The current engine session; see [`Seeds::session_id`].
593    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    /// Whether a composed batch is waiting to be committed or rejected.
606    pub fn has_pending_batch(&self) -> bool {
607        self.pending_batch.is_some()
608    }
609
610    /// Set the Sender Handle carried on our own group messages. It survives
611    /// a rebuild of the engine.
612    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    /// The full key a channel member's hint resolved to, if one has.
623    pub fn member_key(&self, channel: &ChannelTag, hint: &NodeHint) -> Option<PublicKey> {
624        self.members.get(&(*channel, hint.0)).copied()
625    }
626
627    /// Seed outbound stream continuity from stored checkpoints, oldest
628    /// first (see [`Engine::restore`]).
629    pub fn restore(&mut self, checkpoints: &[StreamCheckpoint], now_ms: u64) {
630        self.engine.restore(checkpoints, now_ms);
631        self.stage();
632    }
633
634    /// Compose a message into `conversation`. `stored` is the checkpoint
635    /// storage holds for it, if any, which the engine continues from when it
636    /// no longer remembers the conversation itself (see [`Engine::recall`]).
637    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            // Encoding can fail after some fragments were queued.
656            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    /// Release a stored batch's frames.
688    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    /// Give up on a batch that could not be stored.
700    ///
701    /// Its frames are failed, and the engine is rebuilt from `checkpoints`—
702    /// what storage holds—so nothing it composed since is remembered as
703    /// sent. The channels, the resolved members, and the Sender Handle are
704    /// the session's, not the engine's, and are kept.
705    ///
706    /// The returned drain holds everything produced up to the rebuild,
707    /// named by the old session. The rebuilt engine runs as
708    /// `seeds.session_id`, which must differ from the old one: the new
709    /// engine reissues handles and transmission IDs from the start.
710    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    /// Feed one received text payload (without the payload-type byte).
746    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    /// Answer an [`ArchiveLookup`].
774    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    /// Send one of the message `handle`'s frames again, as the archive
780    /// keeps it under `key` (see [`Engine::retransmit`]). Returns whether it
781    /// was queued.
782    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    /// Take back one of our messages sent before this session, as the
789    /// store keeps it (see [`Engine::adopt`]), with `stored` the
790    /// conversation's checkpoint as for [`ChatFacade::compose`]. Returns
791    /// the handle it goes by now.
792    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    /// Advance timers: reassembly expiry, notify deadlines, repair.
809    pub fn tick(&mut self, now_ms: u64) {
810        self.engine.tick(now_ms);
811        self.stage();
812    }
813
814    /// Report transport progress for a frame released in `session_id`.
815    /// Reports for an earlier session are ignored.
816    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    /// Everything produced since the last drain, in order.
843    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    /// Collect the engine's outputs into the staged drain.
868    fn stage(&mut self) {
869        let collected = self.collect();
870        // Only compose stores a checkpoint, and compose collects its own.
871        debug_assert!(collected.checkpoint.is_none());
872        self.staged.extend(collected.outputs);
873        self.staged_transmissions.extend(collected.transmissions);
874    }
875
876    /// Drop whatever the engine has queued, failing its frames so the engine
877    /// stops tracking them.
878    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        // Bodies are read from the render arena as each output is taken: it
901        // resets once the queue is empty.
902        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    /// The sender's full key when one is known. A multicast member claims
988    /// only a hint, so this is whatever key that hint has resolved to.
989    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;