1use std::str::FromStr;
2
3use s2_common::http::ParseableHeader;
4use serde::Serialize;
5
6use super::{ReadBatch, StreamPosition};
7
8static LAST_EVENT_ID_HEADER: http::HeaderName = http::HeaderName::from_static("last-event-id");
9
10#[derive(Debug, Clone, Copy)]
11pub struct LastEventId {
12 pub seq_num: u64,
14 pub count: usize,
16 pub bytes: usize,
18}
19
20impl ParseableHeader for LastEventId {
21 fn name() -> &'static http::HeaderName {
22 &LAST_EVENT_ID_HEADER
23 }
24}
25
26impl Serialize for LastEventId {
27 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
28 where
29 S: serde::Serializer,
30 {
31 self.to_string().serialize(serializer)
32 }
33}
34
35impl std::fmt::Display for LastEventId {
36 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 let Self {
38 seq_num,
39 count,
40 bytes,
41 } = self;
42 write!(f, "{seq_num},{count},{bytes}")
43 }
44}
45
46impl FromStr for LastEventId {
47 type Err = s2_common::ValidationError;
48
49 fn from_str(s: &str) -> Result<Self, Self::Err> {
50 let mut iter = s.splitn(3, ",");
51
52 fn get_next<T>(
53 iter: &mut std::str::SplitN<&str>,
54 field: &str,
55 ) -> Result<T, s2_common::ValidationError>
56 where
57 T: FromStr,
58 <T as FromStr>::Err: std::fmt::Display,
59 {
60 let item = iter
61 .next()
62 .ok_or_else(|| format!("missing {field} in Last-Event-Id"))?;
63 item.parse()
64 .map_err(|e| format!("invalid {field} in Last-Event-ID: {e}").into())
65 }
66
67 let seq_num = get_next(&mut iter, "seq_num")?;
68 let count = get_next(&mut iter, "count")?;
69 let bytes = get_next(&mut iter, "bytes")?;
70
71 Ok(Self {
72 seq_num,
73 count,
74 bytes,
75 })
76 }
77}
78
79macro_rules! event {
80 ($name:ident, $val:expr) => {
81 #[derive(Serialize)]
82 #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
83 #[serde(rename_all = "snake_case")]
84 pub enum $name {
85 $name,
86 }
87
88 impl AsRef<str> for $name {
89 fn as_ref(&self) -> &str {
90 $val
91 }
92 }
93 };
94}
95
96event!(Batch, "batch");
97event!(Error, "error");
98event!(Ping, "ping");
99
100#[derive(Serialize)]
101#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
102#[serde(untagged)]
103pub enum ReadEvent {
104 #[cfg_attr(feature = "utoipa", schema(title = "batch"))]
105 Batch {
106 #[cfg_attr(feature = "utoipa", schema(inline))]
107 event: Batch,
108 data: ReadBatch,
109 #[cfg_attr(feature = "utoipa", schema(value_type = String, pattern = "^[0-9]+,[0-9]+,[0-9]+$"))]
110 id: LastEventId,
111 },
112 #[cfg_attr(feature = "utoipa", schema(title = "error"))]
113 Error {
114 #[cfg_attr(feature = "utoipa", schema(inline))]
115 event: Error,
116 data: String,
117 },
118 #[cfg_attr(feature = "utoipa", schema(title = "ping"))]
119 Ping {
120 #[cfg_attr(feature = "utoipa", schema(inline))]
121 event: Ping,
122 data: PingEventData,
123 },
124 #[cfg_attr(feature = "utoipa", schema(title = "done"))]
125 #[serde(skip)]
126 Done {
127 #[cfg_attr(feature = "utoipa", schema(value_type = String, pattern = r"^\[DONE\]$"))]
128 data: DoneEventData,
129 },
130}
131
132#[cfg(feature = "axum")]
133fn elapsed_since_epoch() -> std::time::Duration {
134 std::time::SystemTime::now()
135 .duration_since(std::time::SystemTime::UNIX_EPOCH)
136 .expect("healthy clock")
137}
138
139#[cfg(feature = "axum")]
140pub fn read_batch_event(
141 format: crate::data::Format,
142 batch: &s2_common::stream::ReadBatch,
143 id: LastEventId,
144) -> Result<axum::response::sse::Event, axum::Error> {
145 axum::response::sse::Event::default()
146 .event(Batch::Batch)
147 .id(id.to_string())
148 .json_data(super::json::serialize_read_batch(format, batch))
149}
150
151#[cfg(feature = "axum")]
152pub fn error_event(data: String) -> Result<axum::response::sse::Event, axum::Error> {
153 Ok(axum::response::sse::Event::default()
154 .event(Error::Error)
155 .data(data))
156}
157
158#[cfg(feature = "axum")]
159pub fn ping_event(
160 tail: s2_common::record::StreamPosition,
161) -> Result<axum::response::sse::Event, axum::Error> {
162 axum::response::sse::Event::default()
163 .event(Ping::Ping)
164 .json_data(PingEventData {
165 timestamp: elapsed_since_epoch().as_millis() as u64,
166 tail: tail.into(),
167 })
168}
169
170#[cfg(feature = "axum")]
171pub fn done_event() -> Result<axum::response::sse::Event, axum::Error> {
172 Ok(axum::response::sse::Event::default().data(DoneEventData))
173}
174
175#[derive(Debug, Clone, Serialize)]
176#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
177#[serde(rename = "[DONE]")]
178pub struct DoneEventData;
179
180impl AsRef<str> for DoneEventData {
181 fn as_ref(&self) -> &str {
182 "[DONE]"
183 }
184}
185
186#[rustfmt::skip]
187#[derive(Debug, Clone, Serialize)]
188#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
189pub struct PingEventData {
190 pub timestamp: u64,
192 pub tail: StreamPosition,
194}