feat: produce message acked events - #204
Conversation
87a87f3 to
4388ede
Compare
4388ede to
52b3939
Compare
| cx: &mut ServiceContext<S>, | ||
| content: &[u8], | ||
| ) -> Result<(), super::ChatError> { | ||
| ) -> Result<super::MessageId, super::ChatError> { |
There was a problem hiding this comment.
[Dust] Add use statement to simplify types without conflicts
| let content = match message { | ||
| Some(cm) => { | ||
| let reliable = | ||
| ReliablePayload::decode(cm.message.as_slice()).map_err(ChatError::generic)?; | ||
| service_ctx.causal.on_receive(&self.convo_id, &reliable); | ||
| Some(Content { | ||
| bytes: reliable.content.to_vec(), | ||
| encoded_credential: cm.sender.clone(), | ||
| }) | ||
| } | ||
| None => None, | ||
| }; |
There was a problem hiding this comment.
The previous match statement is returning a Option, so that this match statement can process it.
[Dust] Consider separating this code into smaller functions, or sequential processing steps so its easier to read.
| /// Evidence of *delivery to a peer's client*, not of a human reading it. | ||
| #[derive(Debug, Clone, PartialEq, Eq)] | ||
| pub struct MessageAck { |
There was a problem hiding this comment.
[Sand] Disambiguate by renaming to DeliveryAck. MessageAck could be anything.
| /// IDs of messages we authored: a reference to one is an acknowledgement. | ||
| own: HashSet<String>, | ||
| /// Which peers have acknowledged each of our messages, so each is | ||
| /// surfaced exactly once. | ||
| acked_by: HashMap<String, HashSet<String>>, |
There was a problem hiding this comment.
[Sand] This data will need to be culled at somepoint, or the memory footprint will grow with the number of messages sent.
There was a problem hiding this comment.
Good point, better to clean the state after persistence added.
| if state.own.contains(&entry.message_id) | ||
| && payload.sender_id != entry.sender_id | ||
| && state | ||
| .acked_by | ||
| .entry(entry.message_id.clone()) | ||
| .or_default() | ||
| .insert(payload.sender_id.clone()) | ||
| { | ||
| acked.push(MessageAck { | ||
| conversation_id: conversation_id.to_owned(), | ||
| message_id: entry.message_id.clone(), | ||
| acker_id: payload.sender_id.clone(), | ||
| }); | ||
| } |
There was a problem hiding this comment.
[!] This is clean, easy to read and thoughful about the duplicate messages. Nice work.
| let missing: Vec<MissingMessage> = harness.raya().take_missing_messages(); | ||
| assert_eq!(missing.len(), 1, "exactly one message should be missing"); |
There was a problem hiding this comment.
[?] Out of curiosity; What is the motivation behind the "Store and Forward" architecture decision?
It seems like this could be prone to state errors, compared to extending ConvoOutcome, and providing lookup functions for historical state?
There was a problem hiding this comment.
The "store and forward" architecture is set in the early iteration within groupv1 when the events system is not ready.
Re-architecture adds a lot of changes that makes this PR bloat, will be implemented later.
| /// Drain all acknowledgements of our own messages detected so far. | ||
| pub fn take_acks(&self) -> Vec<MessageAck> { | ||
| std::mem::take(&mut self.inner.borrow_mut().acked) | ||
| } |
There was a problem hiding this comment.
[Dust] Adding functions for each memory state could get cumbersome. This seems to mimics the Event data. Perhaps just use the existing event system?
There was a problem hiding this comment.
Will be fixed later with replaced causal state design.
| /// Map the acknowledgements the core observed while processing one payload onto | ||
| /// [`Event::MessageAcked`], one per peer per message. | ||
| /// | ||
| /// Drained from the same place as [`missing_events`]: the causal history of the | ||
| /// message just processed is what carried the acknowledgement. | ||
| fn ack_events(acks: Vec<MessageAck>, directory: &impl AccountDirectory) -> Vec<Event> { |
There was a problem hiding this comment.
[Dust] Ack is used as both a noun and a verb which makes this more difficult to understand than it needs to be.
| fn missing_events(missing: Vec<MissingMessage>, directory: &impl AccountDirectory) -> Vec<Event> { | ||
| missing | ||
| .into_iter() | ||
| .map(|m| Event::MessageMissing { | ||
| convo_id: Arc::from(m.conversation_id), | ||
| message_id: m.frontier.message_id().to_owned(), | ||
| sender_hint: sender_hint(directory, m.frontier.sender_id()), | ||
| }) | ||
| .collect() | ||
| } |
There was a problem hiding this comment.
[Dust] Consider using From traits to handle conversions rather than using these custom conversion functions. These conversions are canonical - defining it that way makes the code easier to follow
There was a problem hiding this comment.
These aren't T -> Event, they're (T, &Directory) -> Event: sender_hint resolves the self-asserted sender_id against the account directory, which isn't in DeliveryAck/MissingMessage. From takes one argument, so the impl can't be written.
| MessageAcked { | ||
| convo_id: Arc<str>, | ||
| message_id: String, | ||
| acker: Option<MessageSender>, |
There was a problem hiding this comment.
[Dust] In all other cases this field is called "Sender". For symmetry I'd consider using it here too.
There was a problem hiding this comment.
message_id here is our message, so its sender is this client — sender would name the wrong participant. The field is the peer that acknowledged it, which is why it's acked_by (renamed from acker per your other note).
52b3939 to
cdbdb9d
Compare
Fix #121.
GroupV2 conversations now carry the same causal-history envelope GroupV1 has had
since #105, and both group kinds report which peers hold a message we sent.
How
A peer can only name our message in its causal history if it held that message
when it sent, so the reference is the receipt.
on_receivecompares eachreferenced id against the ids we authored and reports one ack per peer per
message. Nothing is sent back on purpose — that keeps this a delivery signal
rather than the read receipt rejected earlier on the issue.
API
Event::MessageAcked { convo_id, message_id, acker }— a peer holds a messagewe sent.
Event::MessageMissing { convo_id, message_id, sender_hint }— a message wenever received, revealed by the causal history of one that did arrive.
send_message/send_contentnow return theMessageIdinstead of(), soan application can match later acks to what it sent.
chat-cli renders both: "delivered to …" under its own messages, and a status
line for a gap / missing message.
Acceptance criteria
ReliablePayloadframe for group chatsLimits
Acknowledgement is passive: it rides on whatever a peer sends next. A peer that
never sends never acknowledges, and neither does one that speaks after our
message has dropped out of its 10-entry frontier. Absence of an ack means "not
confirmed", never "not delivered."
That is also why "seen by all members" is not included — it cannot be answered
honestly with this mechanism alone, since a member who reads without ever
replying leaves it permanently unsatisfied. It needs content-free sync messages
or the bloom filter.
ackeris the payload's self-assertedsender_id; binding it to theMLS-authenticated sender is deliberately out of scope.
Notes
The event stream now interleaves acks with messages, so a consumer that assumed
one event per message sees an extra one. That is why
saro_and_raya.rsgainedan ack-skipping helper for its back-and-forth assertions.