Skip to main content

max / makenotwork

4.5 KB · 132 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 /// Every message in the room removed at once, by the room's owner.
32 ///
33 /// Its own frame rather than a fan of `Purge`s or `Delete`s because a wipe
34 /// is not addressed at anyone: the client empties the log outright and does
35 /// not have to know who wrote what. A per-author fan would also be unbounded
36 /// in the number of people who have ever spoken, which is the same burst
37 /// `Purge` exists to avoid one author at a time.
38 Wipe,
39
40 /// The connection fell behind and messages were dropped from the broadcast
41 /// buffer. The client re-fetches from its cursor.
42 ///
43 /// Never silently swallowed. SyncKit's SSE can ignore lag because its clients
44 /// re-pull on their own schedule; a chat client that ignored it would show a
45 /// room with a hole in it and no indication anything was missing.
46 Gap,
47 }
48
49 impl ChatEvent {
50 /// The SSE event name for this frame.
51 pub fn name(&self) -> &'static str {
52 match self {
53 Self::Message(_) => "message",
54 Self::Delete { .. } => "delete",
55 Self::Purge { .. } => "purge",
56 Self::Wipe => "wipe",
57 Self::Gap => "gap",
58 }
59 }
60
61 /// The cursor a client should record after handling this frame, if any.
62 ///
63 /// Only `Message` advances the cursor. A deletion is not a position in the
64 /// room, and reconnecting past a `Gap` is exactly what the cursor is for.
65 pub fn cursor(&self) -> Option<MessageId> {
66 match self {
67 Self::Message(m) => Some(m.id),
68 _ => None,
69 }
70 }
71 }
72
73 #[cfg(test)]
74 mod tests {
75 use super::*;
76 use crate::ids::RoomId;
77 use uuid::Uuid;
78
79 fn message(id: i64) -> Message {
80 Message {
81 id: MessageId(id),
82 room_id: RoomId(Uuid::nil()),
83 author_id: UserId(Uuid::nil()),
84 body_html: "hi".into(),
85 created_at: 0,
86 nonce: None,
87 author: None,
88 }
89 }
90
91 #[test]
92 fn only_messages_advance_the_cursor() {
93 assert_eq!(ChatEvent::Message(message(7)).cursor(), Some(MessageId(7)));
94 assert_eq!(ChatEvent::Delete { id: MessageId(7) }.cursor(), None);
95 assert_eq!(
96 ChatEvent::Purge {
97 author_id: UserId(Uuid::nil())
98 }
99 .cursor(),
100 None
101 );
102 assert_eq!(ChatEvent::Gap.cursor(), None);
103 // A wipe empties the room but does not rewind it. Resetting the cursor
104 // here would send the client back to ask for messages that were just
105 // deleted, and the reply would be empty every time.
106 assert_eq!(ChatEvent::Wipe.cursor(), None);
107 }
108
109 #[test]
110 fn a_wipe_serializes_as_a_bare_tag() {
111 // No payload: the client empties the log and needs nothing else to do
112 // it. Adding a field later is additive; removing one would not be.
113 assert_eq!(
114 serde_json::to_string(&ChatEvent::Wipe).unwrap(),
115 r#"{"type":"wipe"}"#
116 );
117 assert_eq!(ChatEvent::Wipe.name(), "wipe");
118 }
119
120 #[test]
121 fn serializes_tagged_for_the_client() {
122 let json = serde_json::to_string(&ChatEvent::Delete { id: MessageId(3) }).unwrap();
123 assert!(json.contains(r#""type":"delete""#), "{json}");
124 }
125
126 #[test]
127 fn nonce_is_omitted_when_absent() {
128 let json = serde_json::to_string(&message(1)).unwrap();
129 assert!(!json.contains("nonce"), "{json}");
130 }
131 }
132