Skip to main content

max / makenotwork

3.3 KB · 106 lines History Blame Raw
1 //! What the server sends down the stream.
2 //!
3 //! Deliberately four variants. Presence, typing, join, and leave are all absent
4 //! by decision: typing in particular is the largest driver of event volume, and
5 //! both consumers run under a hard 512M cgroup cap where an OOM restarts the
6 //! whole site.
7
8 use serde::{Deserialize, Serialize};
9
10 use crate::ids::{MessageId, UserId};
11 use crate::message::Message;
12
13 /// A single frame on the stream.
14 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15 #[serde(tag = "type", rename_all = "snake_case")]
16 pub enum ChatEvent {
17 /// A new message.
18 Message(Message),
19
20 /// One message removed, by its author or by a moderator.
21 Delete { id: MessageId },
22
23 /// Every message from one author removed at once, following a ban.
24 ///
25 /// One event rather than N `Delete`s: a ban is issued on a hot path and the
26 /// deletion is a single statement keyed by (room, author), so fanning it out
27 /// per message would put an unbounded burst through every open connection
28 /// for no gain.
29 Purge { author_id: UserId },
30
31 /// The connection fell behind and messages were dropped from the broadcast
32 /// buffer. The client re-fetches from its cursor.
33 ///
34 /// Never silently swallowed. SyncKit's SSE can ignore lag because its clients
35 /// re-pull on their own schedule; a chat client that ignored it would show a
36 /// room with a hole in it and no indication anything was missing.
37 Gap,
38 }
39
40 impl ChatEvent {
41 /// The SSE event name for this frame.
42 pub fn name(&self) -> &'static str {
43 match self {
44 Self::Message(_) => "message",
45 Self::Delete { .. } => "delete",
46 Self::Purge { .. } => "purge",
47 Self::Gap => "gap",
48 }
49 }
50
51 /// The cursor a client should record after handling this frame, if any.
52 ///
53 /// Only `Message` advances the cursor. A deletion is not a position in the
54 /// room, and reconnecting past a `Gap` is exactly what the cursor is for.
55 pub fn cursor(&self) -> Option<MessageId> {
56 match self {
57 Self::Message(m) => Some(m.id),
58 _ => None,
59 }
60 }
61 }
62
63 #[cfg(test)]
64 mod tests {
65 use super::*;
66 use crate::ids::RoomId;
67 use uuid::Uuid;
68
69 fn message(id: i64) -> Message {
70 Message {
71 id: MessageId(id),
72 room_id: RoomId(Uuid::nil()),
73 author_id: UserId(Uuid::nil()),
74 body_html: "hi".into(),
75 created_at: 0,
76 nonce: None,
77 }
78 }
79
80 #[test]
81 fn only_messages_advance_the_cursor() {
82 assert_eq!(ChatEvent::Message(message(7)).cursor(), Some(MessageId(7)));
83 assert_eq!(ChatEvent::Delete { id: MessageId(7) }.cursor(), None);
84 assert_eq!(
85 ChatEvent::Purge {
86 author_id: UserId(Uuid::nil())
87 }
88 .cursor(),
89 None
90 );
91 assert_eq!(ChatEvent::Gap.cursor(), None);
92 }
93
94 #[test]
95 fn serializes_tagged_for_the_client() {
96 let json = serde_json::to_string(&ChatEvent::Delete { id: MessageId(3) }).unwrap();
97 assert!(json.contains(r#""type":"delete""#), "{json}");
98 }
99
100 #[test]
101 fn nonce_is_omitted_when_absent() {
102 let json = serde_json::to_string(&message(1)).unwrap();
103 assert!(!json.contains("nonce"), "{json}");
104 }
105 }
106