spikard 0.3.2 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/README.md +659 -659
  4. data/ext/spikard_rb/Cargo.toml +17 -17
  5. data/ext/spikard_rb/extconf.rb +10 -10
  6. data/ext/spikard_rb/src/lib.rs +6 -6
  7. data/lib/spikard/app.rb +386 -386
  8. data/lib/spikard/background.rb +27 -27
  9. data/lib/spikard/config.rb +396 -396
  10. data/lib/spikard/converters.rb +13 -13
  11. data/lib/spikard/handler_wrapper.rb +113 -113
  12. data/lib/spikard/provide.rb +214 -214
  13. data/lib/spikard/response.rb +173 -173
  14. data/lib/spikard/schema.rb +243 -243
  15. data/lib/spikard/sse.rb +111 -111
  16. data/lib/spikard/streaming_response.rb +44 -44
  17. data/lib/spikard/testing.rb +221 -221
  18. data/lib/spikard/upload_file.rb +131 -131
  19. data/lib/spikard/version.rb +5 -5
  20. data/lib/spikard/websocket.rb +59 -59
  21. data/lib/spikard.rb +43 -43
  22. data/sig/spikard.rbs +360 -360
  23. data/vendor/crates/spikard-core/Cargo.toml +40 -40
  24. data/vendor/crates/spikard-core/src/bindings/mod.rs +3 -3
  25. data/vendor/crates/spikard-core/src/bindings/response.rs +133 -133
  26. data/vendor/crates/spikard-core/src/debug.rs +63 -63
  27. data/vendor/crates/spikard-core/src/di/container.rs +726 -726
  28. data/vendor/crates/spikard-core/src/di/dependency.rs +273 -273
  29. data/vendor/crates/spikard-core/src/di/error.rs +118 -118
  30. data/vendor/crates/spikard-core/src/di/factory.rs +538 -538
  31. data/vendor/crates/spikard-core/src/di/graph.rs +545 -545
  32. data/vendor/crates/spikard-core/src/di/mod.rs +192 -192
  33. data/vendor/crates/spikard-core/src/di/resolved.rs +411 -411
  34. data/vendor/crates/spikard-core/src/di/value.rs +283 -283
  35. data/vendor/crates/spikard-core/src/errors.rs +39 -39
  36. data/vendor/crates/spikard-core/src/http.rs +153 -153
  37. data/vendor/crates/spikard-core/src/lib.rs +29 -29
  38. data/vendor/crates/spikard-core/src/lifecycle.rs +422 -422
  39. data/vendor/crates/spikard-core/src/parameters.rs +722 -722
  40. data/vendor/crates/spikard-core/src/problem.rs +310 -310
  41. data/vendor/crates/spikard-core/src/request_data.rs +189 -189
  42. data/vendor/crates/spikard-core/src/router.rs +249 -249
  43. data/vendor/crates/spikard-core/src/schema_registry.rs +183 -183
  44. data/vendor/crates/spikard-core/src/type_hints.rs +304 -304
  45. data/vendor/crates/spikard-core/src/validation.rs +699 -699
  46. data/vendor/crates/spikard-http/Cargo.toml +58 -58
  47. data/vendor/crates/spikard-http/src/auth.rs +247 -247
  48. data/vendor/crates/spikard-http/src/background.rs +249 -249
  49. data/vendor/crates/spikard-http/src/bindings/mod.rs +3 -3
  50. data/vendor/crates/spikard-http/src/bindings/response.rs +1 -1
  51. data/vendor/crates/spikard-http/src/body_metadata.rs +8 -8
  52. data/vendor/crates/spikard-http/src/cors.rs +490 -490
  53. data/vendor/crates/spikard-http/src/debug.rs +63 -63
  54. data/vendor/crates/spikard-http/src/di_handler.rs +423 -423
  55. data/vendor/crates/spikard-http/src/handler_response.rs +190 -190
  56. data/vendor/crates/spikard-http/src/handler_trait.rs +228 -228
  57. data/vendor/crates/spikard-http/src/handler_trait_tests.rs +284 -284
  58. data/vendor/crates/spikard-http/src/lib.rs +529 -529
  59. data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +149 -149
  60. data/vendor/crates/spikard-http/src/lifecycle.rs +428 -428
  61. data/vendor/crates/spikard-http/src/middleware/mod.rs +285 -285
  62. data/vendor/crates/spikard-http/src/middleware/multipart.rs +86 -86
  63. data/vendor/crates/spikard-http/src/middleware/urlencoded.rs +147 -147
  64. data/vendor/crates/spikard-http/src/middleware/validation.rs +287 -287
  65. data/vendor/crates/spikard-http/src/openapi/mod.rs +309 -309
  66. data/vendor/crates/spikard-http/src/openapi/parameter_extraction.rs +190 -190
  67. data/vendor/crates/spikard-http/src/openapi/schema_conversion.rs +308 -308
  68. data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +195 -195
  69. data/vendor/crates/spikard-http/src/parameters.rs +1 -1
  70. data/vendor/crates/spikard-http/src/problem.rs +1 -1
  71. data/vendor/crates/spikard-http/src/query_parser.rs +369 -369
  72. data/vendor/crates/spikard-http/src/response.rs +399 -399
  73. data/vendor/crates/spikard-http/src/router.rs +1 -1
  74. data/vendor/crates/spikard-http/src/schema_registry.rs +1 -1
  75. data/vendor/crates/spikard-http/src/server/handler.rs +87 -87
  76. data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +98 -98
  77. data/vendor/crates/spikard-http/src/server/mod.rs +805 -805
  78. data/vendor/crates/spikard-http/src/server/request_extraction.rs +119 -119
  79. data/vendor/crates/spikard-http/src/sse.rs +447 -447
  80. data/vendor/crates/spikard-http/src/testing/form.rs +14 -14
  81. data/vendor/crates/spikard-http/src/testing/multipart.rs +60 -60
  82. data/vendor/crates/spikard-http/src/testing/test_client.rs +285 -285
  83. data/vendor/crates/spikard-http/src/testing.rs +377 -377
  84. data/vendor/crates/spikard-http/src/type_hints.rs +1 -1
  85. data/vendor/crates/spikard-http/src/validation.rs +1 -1
  86. data/vendor/crates/spikard-http/src/websocket.rs +324 -324
  87. data/vendor/crates/spikard-rb/Cargo.toml +42 -42
  88. data/vendor/crates/spikard-rb/build.rs +8 -8
  89. data/vendor/crates/spikard-rb/src/background.rs +63 -63
  90. data/vendor/crates/spikard-rb/src/config.rs +294 -294
  91. data/vendor/crates/spikard-rb/src/conversion.rs +453 -453
  92. data/vendor/crates/spikard-rb/src/di.rs +409 -409
  93. data/vendor/crates/spikard-rb/src/handler.rs +625 -625
  94. data/vendor/crates/spikard-rb/src/lib.rs +2771 -2771
  95. data/vendor/crates/spikard-rb/src/lifecycle.rs +274 -274
  96. data/vendor/crates/spikard-rb/src/server.rs +283 -283
  97. data/vendor/crates/spikard-rb/src/sse.rs +231 -231
  98. data/vendor/crates/spikard-rb/src/test_client.rs +404 -404
  99. data/vendor/crates/spikard-rb/src/test_sse.rs +143 -143
  100. data/vendor/crates/spikard-rb/src/test_websocket.rs +221 -221
  101. data/vendor/crates/spikard-rb/src/websocket.rs +233 -233
  102. data/vendor/spikard-core/Cargo.toml +40 -40
  103. data/vendor/spikard-core/src/bindings/mod.rs +3 -3
  104. data/vendor/spikard-core/src/bindings/response.rs +133 -133
  105. data/vendor/spikard-core/src/debug.rs +63 -63
  106. data/vendor/spikard-core/src/di/container.rs +726 -726
  107. data/vendor/spikard-core/src/di/dependency.rs +273 -273
  108. data/vendor/spikard-core/src/di/error.rs +118 -118
  109. data/vendor/spikard-core/src/di/factory.rs +538 -538
  110. data/vendor/spikard-core/src/di/graph.rs +545 -545
  111. data/vendor/spikard-core/src/di/mod.rs +192 -192
  112. data/vendor/spikard-core/src/di/resolved.rs +411 -411
  113. data/vendor/spikard-core/src/di/value.rs +283 -283
  114. data/vendor/spikard-core/src/http.rs +153 -153
  115. data/vendor/spikard-core/src/lib.rs +28 -28
  116. data/vendor/spikard-core/src/lifecycle.rs +422 -422
  117. data/vendor/spikard-core/src/parameters.rs +719 -719
  118. data/vendor/spikard-core/src/problem.rs +310 -310
  119. data/vendor/spikard-core/src/request_data.rs +189 -189
  120. data/vendor/spikard-core/src/router.rs +249 -249
  121. data/vendor/spikard-core/src/schema_registry.rs +183 -183
  122. data/vendor/spikard-core/src/type_hints.rs +304 -304
  123. data/vendor/spikard-core/src/validation.rs +699 -699
  124. data/vendor/spikard-http/Cargo.toml +58 -58
  125. data/vendor/spikard-http/src/auth.rs +247 -247
  126. data/vendor/spikard-http/src/background.rs +249 -249
  127. data/vendor/spikard-http/src/bindings/mod.rs +3 -3
  128. data/vendor/spikard-http/src/bindings/response.rs +1 -1
  129. data/vendor/spikard-http/src/body_metadata.rs +8 -8
  130. data/vendor/spikard-http/src/cors.rs +490 -490
  131. data/vendor/spikard-http/src/debug.rs +63 -63
  132. data/vendor/spikard-http/src/di_handler.rs +423 -423
  133. data/vendor/spikard-http/src/handler_response.rs +190 -190
  134. data/vendor/spikard-http/src/handler_trait.rs +228 -228
  135. data/vendor/spikard-http/src/handler_trait_tests.rs +284 -284
  136. data/vendor/spikard-http/src/lib.rs +529 -529
  137. data/vendor/spikard-http/src/lifecycle/adapter.rs +149 -149
  138. data/vendor/spikard-http/src/lifecycle.rs +428 -428
  139. data/vendor/spikard-http/src/middleware/mod.rs +285 -285
  140. data/vendor/spikard-http/src/middleware/multipart.rs +86 -86
  141. data/vendor/spikard-http/src/middleware/urlencoded.rs +147 -147
  142. data/vendor/spikard-http/src/middleware/validation.rs +287 -287
  143. data/vendor/spikard-http/src/openapi/mod.rs +309 -309
  144. data/vendor/spikard-http/src/openapi/parameter_extraction.rs +190 -190
  145. data/vendor/spikard-http/src/openapi/schema_conversion.rs +308 -308
  146. data/vendor/spikard-http/src/openapi/spec_generation.rs +195 -195
  147. data/vendor/spikard-http/src/parameters.rs +1 -1
  148. data/vendor/spikard-http/src/problem.rs +1 -1
  149. data/vendor/spikard-http/src/query_parser.rs +369 -369
  150. data/vendor/spikard-http/src/response.rs +399 -399
  151. data/vendor/spikard-http/src/router.rs +1 -1
  152. data/vendor/spikard-http/src/schema_registry.rs +1 -1
  153. data/vendor/spikard-http/src/server/handler.rs +80 -80
  154. data/vendor/spikard-http/src/server/lifecycle_execution.rs +98 -98
  155. data/vendor/spikard-http/src/server/mod.rs +805 -805
  156. data/vendor/spikard-http/src/server/request_extraction.rs +119 -119
  157. data/vendor/spikard-http/src/sse.rs +447 -447
  158. data/vendor/spikard-http/src/testing/form.rs +14 -14
  159. data/vendor/spikard-http/src/testing/multipart.rs +60 -60
  160. data/vendor/spikard-http/src/testing/test_client.rs +285 -285
  161. data/vendor/spikard-http/src/testing.rs +377 -377
  162. data/vendor/spikard-http/src/type_hints.rs +1 -1
  163. data/vendor/spikard-http/src/validation.rs +1 -1
  164. data/vendor/spikard-http/src/websocket.rs +324 -324
  165. data/vendor/spikard-rb/Cargo.toml +42 -42
  166. data/vendor/spikard-rb/build.rs +8 -8
  167. data/vendor/spikard-rb/src/background.rs +63 -63
  168. data/vendor/spikard-rb/src/config.rs +294 -294
  169. data/vendor/spikard-rb/src/conversion.rs +392 -392
  170. data/vendor/spikard-rb/src/di.rs +409 -409
  171. data/vendor/spikard-rb/src/handler.rs +534 -534
  172. data/vendor/spikard-rb/src/lib.rs +2020 -2020
  173. data/vendor/spikard-rb/src/lifecycle.rs +267 -267
  174. data/vendor/spikard-rb/src/server.rs +283 -283
  175. data/vendor/spikard-rb/src/sse.rs +231 -231
  176. data/vendor/spikard-rb/src/test_client.rs +404 -404
  177. data/vendor/spikard-rb/src/test_sse.rs +143 -143
  178. data/vendor/spikard-rb/src/test_websocket.rs +221 -221
  179. data/vendor/spikard-rb/src/websocket.rs +233 -233
  180. metadata +1 -1
@@ -1,447 +1,447 @@
1
- //! Server-Sent Events (SSE) support for Spikard
2
- //!
3
- //! Provides SSE streaming with event generation and lifecycle management.
4
-
5
- use axum::{
6
- extract::State,
7
- response::{
8
- IntoResponse,
9
- sse::{Event, KeepAlive, Sse},
10
- },
11
- };
12
- use futures_util::stream;
13
- use serde_json::Value;
14
- use std::{convert::Infallible, sync::Arc, time::Duration};
15
- use tracing::{debug, error, info};
16
-
17
- /// SSE event producer trait
18
- ///
19
- /// Implement this trait to create custom Server-Sent Event (SSE) producers for your application.
20
- /// The producer generates events that are streamed to connected clients.
21
- ///
22
- /// # Understanding SSE
23
- ///
24
- /// Server-Sent Events (SSE) provide one-way communication from server to client over HTTP.
25
- /// Unlike WebSocket, SSE uses standard HTTP and automatically handles reconnection.
26
- /// Use SSE when you need to push data to clients without bidirectional communication.
27
- ///
28
- /// # Implementing the Trait
29
- ///
30
- /// You must implement the `next_event` method to generate events. The `on_connect` and
31
- /// `on_disconnect` methods are optional lifecycle hooks.
32
- ///
33
- /// # Example
34
- ///
35
- /// ```ignore
36
- /// use spikard_http::sse::{SseEventProducer, SseEvent};
37
- /// use serde_json::json;
38
- /// use std::time::Duration;
39
- /// use tokio::time::sleep;
40
- ///
41
- /// struct CounterProducer {
42
- /// limit: usize,
43
- /// }
44
- ///
45
- /// #[async_trait]
46
- /// impl SseEventProducer for CounterProducer {
47
- /// async fn next_event(&self) -> Option<SseEvent> {
48
- /// static COUNTER: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
49
- ///
50
- /// let count = COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
51
- /// if count < self.limit {
52
- /// Some(SseEvent::new(json!({"count": count})))
53
- /// } else {
54
- /// None
55
- /// }
56
- /// }
57
- ///
58
- /// async fn on_connect(&self) {
59
- /// println!("Client connected");
60
- /// }
61
- ///
62
- /// async fn on_disconnect(&self) {
63
- /// println!("Client disconnected");
64
- /// }
65
- /// }
66
- /// ```
67
- pub trait SseEventProducer: Send + Sync {
68
- /// Generate the next event
69
- ///
70
- /// Called repeatedly to produce the event stream. Should return `Some(event)` when
71
- /// an event is ready to send, or `None` when the stream should end.
72
- ///
73
- /// # Returns
74
- /// * `Some(event)` - Event to send to the client
75
- /// * `None` - Stream complete, connection will close
76
- fn next_event(&self) -> impl std::future::Future<Output = Option<SseEvent>> + Send;
77
-
78
- /// Called when a client connects to the SSE endpoint
79
- ///
80
- /// Optional lifecycle hook invoked when a new SSE connection is established.
81
- /// Default implementation does nothing.
82
- fn on_connect(&self) -> impl std::future::Future<Output = ()> + Send {
83
- async {}
84
- }
85
-
86
- /// Called when a client disconnects from the SSE endpoint
87
- ///
88
- /// Optional lifecycle hook invoked when an SSE connection is closed (either by the
89
- /// client or the stream ending). Default implementation does nothing.
90
- fn on_disconnect(&self) -> impl std::future::Future<Output = ()> + Send {
91
- async {}
92
- }
93
- }
94
-
95
- /// An individual SSE event
96
- ///
97
- /// Represents a single Server-Sent Event to be sent to a connected client.
98
- /// Events can have an optional type, ID, and retry timeout for advanced scenarios.
99
- ///
100
- /// # Fields
101
- ///
102
- /// * `event_type` - Optional event type string (used for client-side event filtering)
103
- /// * `data` - JSON data payload to send to the client
104
- /// * `id` - Optional event ID (clients can use this to resume after disconnect)
105
- /// * `retry` - Optional retry timeout in milliseconds (tells client when to reconnect)
106
- ///
107
- /// # SSE Format
108
- ///
109
- /// Events are serialized to the following text format:
110
- /// ```text
111
- /// event: event_type
112
- /// data: {"json":"value"}
113
- /// id: event-123
114
- /// retry: 3000
115
- /// ```
116
- #[derive(Debug, Clone)]
117
- pub struct SseEvent {
118
- /// Event type (optional)
119
- pub event_type: Option<String>,
120
- /// Event data (JSON value)
121
- pub data: Value,
122
- /// Event ID (optional, for client-side reconnection)
123
- pub id: Option<String>,
124
- /// Retry timeout in milliseconds (optional)
125
- pub retry: Option<u64>,
126
- }
127
-
128
- impl SseEvent {
129
- /// Create a new SSE event with data only
130
- ///
131
- /// Creates a minimal event with just the data payload. Use builder methods
132
- /// to add optional fields.
133
- ///
134
- /// # Arguments
135
- /// * `data` - JSON value to send to the client
136
- ///
137
- /// # Example
138
- ///
139
- /// ```ignore
140
- /// use serde_json::json;
141
- /// use spikard_http::sse::SseEvent;
142
- ///
143
- /// let event = SseEvent::new(json!({"status": "connected"}));
144
- /// ```
145
- pub fn new(data: Value) -> Self {
146
- Self {
147
- event_type: None,
148
- data,
149
- id: None,
150
- retry: None,
151
- }
152
- }
153
-
154
- /// Create a new SSE event with an event type and data
155
- ///
156
- /// Creates an event with a type field. Clients can filter events by type
157
- /// in their event listener.
158
- ///
159
- /// # Arguments
160
- /// * `event_type` - String identifying the event type (e.g., "update", "error")
161
- /// * `data` - JSON value to send to the client
162
- ///
163
- /// # Example
164
- ///
165
- /// ```ignore
166
- /// use serde_json::json;
167
- /// use spikard_http::sse::SseEvent;
168
- ///
169
- /// let event = SseEvent::with_type("update", json!({"count": 42}));
170
- /// // Client can listen with: eventSource.addEventListener("update", ...)
171
- /// ```
172
- pub fn with_type(event_type: impl Into<String>, data: Value) -> Self {
173
- Self {
174
- event_type: Some(event_type.into()),
175
- data,
176
- id: None,
177
- retry: None,
178
- }
179
- }
180
-
181
- /// Set the event ID for client-side reconnection support
182
- ///
183
- /// Sets an ID that clients can use to resume from this point if they disconnect.
184
- /// The client sends this ID back in the `Last-Event-ID` header when reconnecting.
185
- ///
186
- /// # Arguments
187
- /// * `id` - Unique identifier for this event
188
- ///
189
- /// # Example
190
- ///
191
- /// ```ignore
192
- /// use serde_json::json;
193
- /// use spikard_http::sse::SseEvent;
194
- ///
195
- /// let event = SseEvent::new(json!({"count": 1}))
196
- /// .with_id("event-1");
197
- /// ```
198
- pub fn with_id(mut self, id: impl Into<String>) -> Self {
199
- self.id = Some(id.into());
200
- self
201
- }
202
-
203
- /// Set the retry timeout for client reconnection
204
- ///
205
- /// Sets the time in milliseconds clients should wait before attempting to reconnect
206
- /// if the connection is lost. The client browser will automatically handle reconnection.
207
- ///
208
- /// # Arguments
209
- /// * `retry_ms` - Retry timeout in milliseconds
210
- ///
211
- /// # Example
212
- ///
213
- /// ```ignore
214
- /// use serde_json::json;
215
- /// use spikard_http::sse::SseEvent;
216
- ///
217
- /// let event = SseEvent::new(json!({"data": "value"}))
218
- /// .with_retry(5000); // Reconnect after 5 seconds
219
- /// ```
220
- pub fn with_retry(mut self, retry_ms: u64) -> Self {
221
- self.retry = Some(retry_ms);
222
- self
223
- }
224
-
225
- /// Convert to Axum's SSE Event
226
- fn into_axum_event(self) -> Result<Event, serde_json::Error> {
227
- let json_data = serde_json::to_string(&self.data)?;
228
-
229
- let mut event = Event::default().data(json_data);
230
-
231
- if let Some(event_type) = self.event_type {
232
- event = event.event(event_type);
233
- }
234
-
235
- if let Some(id) = self.id {
236
- event = event.id(id);
237
- }
238
-
239
- if let Some(retry) = self.retry {
240
- event = event.retry(Duration::from_millis(retry));
241
- }
242
-
243
- Ok(event)
244
- }
245
- }
246
-
247
- /// SSE state shared across connections
248
- ///
249
- /// Contains the event producer and optional JSON schema for validating
250
- /// events. This state is shared among all connections to the same SSE endpoint.
251
- pub struct SseState<P: SseEventProducer> {
252
- /// The event producer implementation
253
- producer: Arc<P>,
254
- /// Optional JSON Schema for validating outgoing events
255
- event_schema: Option<Arc<jsonschema::Validator>>,
256
- }
257
-
258
- impl<P: SseEventProducer> Clone for SseState<P> {
259
- fn clone(&self) -> Self {
260
- Self {
261
- producer: Arc::clone(&self.producer),
262
- event_schema: self.event_schema.clone(),
263
- }
264
- }
265
- }
266
-
267
- impl<P: SseEventProducer + 'static> SseState<P> {
268
- /// Create new SSE state with an event producer
269
- ///
270
- /// Creates a new state without event validation schema.
271
- /// Events are not validated.
272
- ///
273
- /// # Arguments
274
- /// * `producer` - The event producer implementation
275
- ///
276
- /// # Example
277
- ///
278
- /// ```ignore
279
- /// let state = SseState::new(MyProducer);
280
- /// ```
281
- pub fn new(producer: P) -> Self {
282
- Self {
283
- producer: Arc::new(producer),
284
- event_schema: None,
285
- }
286
- }
287
-
288
- /// Create new SSE state with an event producer and optional event schema
289
- ///
290
- /// Creates a new state with optional JSON schema for validating outgoing events.
291
- /// If a schema is provided and an event fails validation, it is silently dropped.
292
- ///
293
- /// # Arguments
294
- /// * `producer` - The event producer implementation
295
- /// * `event_schema` - Optional JSON schema for validating events
296
- ///
297
- /// # Returns
298
- /// * `Ok(state)` - Successfully created state
299
- /// * `Err(msg)` - Invalid schema provided
300
- ///
301
- /// # Example
302
- ///
303
- /// ```ignore
304
- /// use serde_json::json;
305
- ///
306
- /// let event_schema = json!({
307
- /// "type": "object",
308
- /// "properties": {
309
- /// "count": {"type": "integer"}
310
- /// }
311
- /// });
312
- ///
313
- /// let state = SseState::with_schema(MyProducer, Some(event_schema))?;
314
- /// ```
315
- pub fn with_schema(producer: P, event_schema: Option<serde_json::Value>) -> Result<Self, String> {
316
- let event_validator = if let Some(schema) = event_schema {
317
- Some(Arc::new(
318
- jsonschema::validator_for(&schema).map_err(|e| format!("Invalid event schema: {}", e))?,
319
- ))
320
- } else {
321
- None
322
- };
323
-
324
- Ok(Self {
325
- producer: Arc::new(producer),
326
- event_schema: event_validator,
327
- })
328
- }
329
- }
330
-
331
- /// SSE endpoint handler
332
- ///
333
- /// This is the main entry point for SSE connections. Use this as an Axum route
334
- /// handler by passing it to an Axum router's `.route()` method with `get()`.
335
- ///
336
- /// The handler establishes a connection and streams events from the producer to
337
- /// the client using the Server-Sent Events protocol (text/event-stream).
338
- ///
339
- /// # Arguments
340
- /// * `State(state)` - Application state containing the event producer and optional schema
341
- ///
342
- /// # Returns
343
- /// A streaming response with the `text/event-stream` content type
344
- ///
345
- /// # Example
346
- ///
347
- /// ```ignore
348
- /// use axum::{Router, routing::get, extract::State};
349
- ///
350
- /// let state = SseState::new(MyProducer);
351
- /// let router = Router::new()
352
- /// .route("/events", get(sse_handler::<MyProducer>))
353
- /// .with_state(state);
354
- ///
355
- /// // Client usage:
356
- /// // const eventSource = new EventSource('/events');
357
- /// // eventSource.onmessage = (e) => console.log(e.data);
358
- /// ```
359
- pub async fn sse_handler<P: SseEventProducer + 'static>(State(state): State<SseState<P>>) -> impl IntoResponse {
360
- info!("SSE client connected");
361
-
362
- state.producer.on_connect().await;
363
-
364
- let producer = Arc::clone(&state.producer);
365
- let event_schema = state.event_schema.clone();
366
- let stream = stream::unfold((producer, event_schema), |(producer, event_schema)| async move {
367
- match producer.next_event().await {
368
- Some(sse_event) => {
369
- debug!("Sending SSE event: {:?}", sse_event.event_type);
370
-
371
- if let Some(validator) = &event_schema
372
- && !validator.is_valid(&sse_event.data)
373
- {
374
- error!("SSE event validation failed");
375
- return Some((
376
- Ok::<_, Infallible>(Event::default().data("validation_error")),
377
- (producer, event_schema),
378
- ));
379
- }
380
-
381
- match sse_event.into_axum_event() {
382
- Ok(event) => Some((Ok::<_, Infallible>(event), (producer, event_schema))),
383
- Err(e) => {
384
- error!("Failed to serialize SSE event: {}", e);
385
- None
386
- }
387
- }
388
- }
389
- None => {
390
- info!("SSE stream ended");
391
- None
392
- }
393
- }
394
- });
395
-
396
- let sse_response =
397
- Sse::new(stream).keep_alive(KeepAlive::new().interval(Duration::from_secs(15)).text("keep-alive"));
398
-
399
- sse_response.into_response()
400
- }
401
-
402
- #[cfg(test)]
403
- mod tests {
404
- use super::*;
405
-
406
- struct TestProducer {
407
- count: std::sync::atomic::AtomicUsize,
408
- }
409
-
410
- impl SseEventProducer for TestProducer {
411
- async fn next_event(&self) -> Option<SseEvent> {
412
- let count = self.count.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
413
- if count < 3 {
414
- Some(SseEvent::new(serde_json::json!({
415
- "message": format!("Event {}", count)
416
- })))
417
- } else {
418
- None
419
- }
420
- }
421
- }
422
-
423
- #[test]
424
- fn test_sse_event_creation() {
425
- let event = SseEvent::new(serde_json::json!({"test": "data"}));
426
- assert!(event.event_type.is_none());
427
- assert!(event.id.is_none());
428
- assert!(event.retry.is_none());
429
-
430
- let event = SseEvent::with_type("notification", serde_json::json!({"test": "data"}))
431
- .with_id("123")
432
- .with_retry(5000);
433
- assert_eq!(event.event_type, Some("notification".to_string()));
434
- assert_eq!(event.id, Some("123".to_string()));
435
- assert_eq!(event.retry, Some(5000));
436
- }
437
-
438
- #[test]
439
- fn test_sse_state_creation() {
440
- let producer = TestProducer {
441
- count: std::sync::atomic::AtomicUsize::new(0),
442
- };
443
- let state = SseState::new(producer);
444
- let cloned = state.clone();
445
- assert!(Arc::ptr_eq(&state.producer, &cloned.producer));
446
- }
447
- }
1
+ //! Server-Sent Events (SSE) support for Spikard
2
+ //!
3
+ //! Provides SSE streaming with event generation and lifecycle management.
4
+
5
+ use axum::{
6
+ extract::State,
7
+ response::{
8
+ IntoResponse,
9
+ sse::{Event, KeepAlive, Sse},
10
+ },
11
+ };
12
+ use futures_util::stream;
13
+ use serde_json::Value;
14
+ use std::{convert::Infallible, sync::Arc, time::Duration};
15
+ use tracing::{debug, error, info};
16
+
17
+ /// SSE event producer trait
18
+ ///
19
+ /// Implement this trait to create custom Server-Sent Event (SSE) producers for your application.
20
+ /// The producer generates events that are streamed to connected clients.
21
+ ///
22
+ /// # Understanding SSE
23
+ ///
24
+ /// Server-Sent Events (SSE) provide one-way communication from server to client over HTTP.
25
+ /// Unlike WebSocket, SSE uses standard HTTP and automatically handles reconnection.
26
+ /// Use SSE when you need to push data to clients without bidirectional communication.
27
+ ///
28
+ /// # Implementing the Trait
29
+ ///
30
+ /// You must implement the `next_event` method to generate events. The `on_connect` and
31
+ /// `on_disconnect` methods are optional lifecycle hooks.
32
+ ///
33
+ /// # Example
34
+ ///
35
+ /// ```ignore
36
+ /// use spikard_http::sse::{SseEventProducer, SseEvent};
37
+ /// use serde_json::json;
38
+ /// use std::time::Duration;
39
+ /// use tokio::time::sleep;
40
+ ///
41
+ /// struct CounterProducer {
42
+ /// limit: usize,
43
+ /// }
44
+ ///
45
+ /// #[async_trait]
46
+ /// impl SseEventProducer for CounterProducer {
47
+ /// async fn next_event(&self) -> Option<SseEvent> {
48
+ /// static COUNTER: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
49
+ ///
50
+ /// let count = COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
51
+ /// if count < self.limit {
52
+ /// Some(SseEvent::new(json!({"count": count})))
53
+ /// } else {
54
+ /// None
55
+ /// }
56
+ /// }
57
+ ///
58
+ /// async fn on_connect(&self) {
59
+ /// println!("Client connected");
60
+ /// }
61
+ ///
62
+ /// async fn on_disconnect(&self) {
63
+ /// println!("Client disconnected");
64
+ /// }
65
+ /// }
66
+ /// ```
67
+ pub trait SseEventProducer: Send + Sync {
68
+ /// Generate the next event
69
+ ///
70
+ /// Called repeatedly to produce the event stream. Should return `Some(event)` when
71
+ /// an event is ready to send, or `None` when the stream should end.
72
+ ///
73
+ /// # Returns
74
+ /// * `Some(event)` - Event to send to the client
75
+ /// * `None` - Stream complete, connection will close
76
+ fn next_event(&self) -> impl std::future::Future<Output = Option<SseEvent>> + Send;
77
+
78
+ /// Called when a client connects to the SSE endpoint
79
+ ///
80
+ /// Optional lifecycle hook invoked when a new SSE connection is established.
81
+ /// Default implementation does nothing.
82
+ fn on_connect(&self) -> impl std::future::Future<Output = ()> + Send {
83
+ async {}
84
+ }
85
+
86
+ /// Called when a client disconnects from the SSE endpoint
87
+ ///
88
+ /// Optional lifecycle hook invoked when an SSE connection is closed (either by the
89
+ /// client or the stream ending). Default implementation does nothing.
90
+ fn on_disconnect(&self) -> impl std::future::Future<Output = ()> + Send {
91
+ async {}
92
+ }
93
+ }
94
+
95
+ /// An individual SSE event
96
+ ///
97
+ /// Represents a single Server-Sent Event to be sent to a connected client.
98
+ /// Events can have an optional type, ID, and retry timeout for advanced scenarios.
99
+ ///
100
+ /// # Fields
101
+ ///
102
+ /// * `event_type` - Optional event type string (used for client-side event filtering)
103
+ /// * `data` - JSON data payload to send to the client
104
+ /// * `id` - Optional event ID (clients can use this to resume after disconnect)
105
+ /// * `retry` - Optional retry timeout in milliseconds (tells client when to reconnect)
106
+ ///
107
+ /// # SSE Format
108
+ ///
109
+ /// Events are serialized to the following text format:
110
+ /// ```text
111
+ /// event: event_type
112
+ /// data: {"json":"value"}
113
+ /// id: event-123
114
+ /// retry: 3000
115
+ /// ```
116
+ #[derive(Debug, Clone)]
117
+ pub struct SseEvent {
118
+ /// Event type (optional)
119
+ pub event_type: Option<String>,
120
+ /// Event data (JSON value)
121
+ pub data: Value,
122
+ /// Event ID (optional, for client-side reconnection)
123
+ pub id: Option<String>,
124
+ /// Retry timeout in milliseconds (optional)
125
+ pub retry: Option<u64>,
126
+ }
127
+
128
+ impl SseEvent {
129
+ /// Create a new SSE event with data only
130
+ ///
131
+ /// Creates a minimal event with just the data payload. Use builder methods
132
+ /// to add optional fields.
133
+ ///
134
+ /// # Arguments
135
+ /// * `data` - JSON value to send to the client
136
+ ///
137
+ /// # Example
138
+ ///
139
+ /// ```ignore
140
+ /// use serde_json::json;
141
+ /// use spikard_http::sse::SseEvent;
142
+ ///
143
+ /// let event = SseEvent::new(json!({"status": "connected"}));
144
+ /// ```
145
+ pub fn new(data: Value) -> Self {
146
+ Self {
147
+ event_type: None,
148
+ data,
149
+ id: None,
150
+ retry: None,
151
+ }
152
+ }
153
+
154
+ /// Create a new SSE event with an event type and data
155
+ ///
156
+ /// Creates an event with a type field. Clients can filter events by type
157
+ /// in their event listener.
158
+ ///
159
+ /// # Arguments
160
+ /// * `event_type` - String identifying the event type (e.g., "update", "error")
161
+ /// * `data` - JSON value to send to the client
162
+ ///
163
+ /// # Example
164
+ ///
165
+ /// ```ignore
166
+ /// use serde_json::json;
167
+ /// use spikard_http::sse::SseEvent;
168
+ ///
169
+ /// let event = SseEvent::with_type("update", json!({"count": 42}));
170
+ /// // Client can listen with: eventSource.addEventListener("update", ...)
171
+ /// ```
172
+ pub fn with_type(event_type: impl Into<String>, data: Value) -> Self {
173
+ Self {
174
+ event_type: Some(event_type.into()),
175
+ data,
176
+ id: None,
177
+ retry: None,
178
+ }
179
+ }
180
+
181
+ /// Set the event ID for client-side reconnection support
182
+ ///
183
+ /// Sets an ID that clients can use to resume from this point if they disconnect.
184
+ /// The client sends this ID back in the `Last-Event-ID` header when reconnecting.
185
+ ///
186
+ /// # Arguments
187
+ /// * `id` - Unique identifier for this event
188
+ ///
189
+ /// # Example
190
+ ///
191
+ /// ```ignore
192
+ /// use serde_json::json;
193
+ /// use spikard_http::sse::SseEvent;
194
+ ///
195
+ /// let event = SseEvent::new(json!({"count": 1}))
196
+ /// .with_id("event-1");
197
+ /// ```
198
+ pub fn with_id(mut self, id: impl Into<String>) -> Self {
199
+ self.id = Some(id.into());
200
+ self
201
+ }
202
+
203
+ /// Set the retry timeout for client reconnection
204
+ ///
205
+ /// Sets the time in milliseconds clients should wait before attempting to reconnect
206
+ /// if the connection is lost. The client browser will automatically handle reconnection.
207
+ ///
208
+ /// # Arguments
209
+ /// * `retry_ms` - Retry timeout in milliseconds
210
+ ///
211
+ /// # Example
212
+ ///
213
+ /// ```ignore
214
+ /// use serde_json::json;
215
+ /// use spikard_http::sse::SseEvent;
216
+ ///
217
+ /// let event = SseEvent::new(json!({"data": "value"}))
218
+ /// .with_retry(5000); // Reconnect after 5 seconds
219
+ /// ```
220
+ pub fn with_retry(mut self, retry_ms: u64) -> Self {
221
+ self.retry = Some(retry_ms);
222
+ self
223
+ }
224
+
225
+ /// Convert to Axum's SSE Event
226
+ fn into_axum_event(self) -> Result<Event, serde_json::Error> {
227
+ let json_data = serde_json::to_string(&self.data)?;
228
+
229
+ let mut event = Event::default().data(json_data);
230
+
231
+ if let Some(event_type) = self.event_type {
232
+ event = event.event(event_type);
233
+ }
234
+
235
+ if let Some(id) = self.id {
236
+ event = event.id(id);
237
+ }
238
+
239
+ if let Some(retry) = self.retry {
240
+ event = event.retry(Duration::from_millis(retry));
241
+ }
242
+
243
+ Ok(event)
244
+ }
245
+ }
246
+
247
+ /// SSE state shared across connections
248
+ ///
249
+ /// Contains the event producer and optional JSON schema for validating
250
+ /// events. This state is shared among all connections to the same SSE endpoint.
251
+ pub struct SseState<P: SseEventProducer> {
252
+ /// The event producer implementation
253
+ producer: Arc<P>,
254
+ /// Optional JSON Schema for validating outgoing events
255
+ event_schema: Option<Arc<jsonschema::Validator>>,
256
+ }
257
+
258
+ impl<P: SseEventProducer> Clone for SseState<P> {
259
+ fn clone(&self) -> Self {
260
+ Self {
261
+ producer: Arc::clone(&self.producer),
262
+ event_schema: self.event_schema.clone(),
263
+ }
264
+ }
265
+ }
266
+
267
+ impl<P: SseEventProducer + 'static> SseState<P> {
268
+ /// Create new SSE state with an event producer
269
+ ///
270
+ /// Creates a new state without event validation schema.
271
+ /// Events are not validated.
272
+ ///
273
+ /// # Arguments
274
+ /// * `producer` - The event producer implementation
275
+ ///
276
+ /// # Example
277
+ ///
278
+ /// ```ignore
279
+ /// let state = SseState::new(MyProducer);
280
+ /// ```
281
+ pub fn new(producer: P) -> Self {
282
+ Self {
283
+ producer: Arc::new(producer),
284
+ event_schema: None,
285
+ }
286
+ }
287
+
288
+ /// Create new SSE state with an event producer and optional event schema
289
+ ///
290
+ /// Creates a new state with optional JSON schema for validating outgoing events.
291
+ /// If a schema is provided and an event fails validation, it is silently dropped.
292
+ ///
293
+ /// # Arguments
294
+ /// * `producer` - The event producer implementation
295
+ /// * `event_schema` - Optional JSON schema for validating events
296
+ ///
297
+ /// # Returns
298
+ /// * `Ok(state)` - Successfully created state
299
+ /// * `Err(msg)` - Invalid schema provided
300
+ ///
301
+ /// # Example
302
+ ///
303
+ /// ```ignore
304
+ /// use serde_json::json;
305
+ ///
306
+ /// let event_schema = json!({
307
+ /// "type": "object",
308
+ /// "properties": {
309
+ /// "count": {"type": "integer"}
310
+ /// }
311
+ /// });
312
+ ///
313
+ /// let state = SseState::with_schema(MyProducer, Some(event_schema))?;
314
+ /// ```
315
+ pub fn with_schema(producer: P, event_schema: Option<serde_json::Value>) -> Result<Self, String> {
316
+ let event_validator = if let Some(schema) = event_schema {
317
+ Some(Arc::new(
318
+ jsonschema::validator_for(&schema).map_err(|e| format!("Invalid event schema: {}", e))?,
319
+ ))
320
+ } else {
321
+ None
322
+ };
323
+
324
+ Ok(Self {
325
+ producer: Arc::new(producer),
326
+ event_schema: event_validator,
327
+ })
328
+ }
329
+ }
330
+
331
+ /// SSE endpoint handler
332
+ ///
333
+ /// This is the main entry point for SSE connections. Use this as an Axum route
334
+ /// handler by passing it to an Axum router's `.route()` method with `get()`.
335
+ ///
336
+ /// The handler establishes a connection and streams events from the producer to
337
+ /// the client using the Server-Sent Events protocol (text/event-stream).
338
+ ///
339
+ /// # Arguments
340
+ /// * `State(state)` - Application state containing the event producer and optional schema
341
+ ///
342
+ /// # Returns
343
+ /// A streaming response with the `text/event-stream` content type
344
+ ///
345
+ /// # Example
346
+ ///
347
+ /// ```ignore
348
+ /// use axum::{Router, routing::get, extract::State};
349
+ ///
350
+ /// let state = SseState::new(MyProducer);
351
+ /// let router = Router::new()
352
+ /// .route("/events", get(sse_handler::<MyProducer>))
353
+ /// .with_state(state);
354
+ ///
355
+ /// // Client usage:
356
+ /// // const eventSource = new EventSource('/events');
357
+ /// // eventSource.onmessage = (e) => console.log(e.data);
358
+ /// ```
359
+ pub async fn sse_handler<P: SseEventProducer + 'static>(State(state): State<SseState<P>>) -> impl IntoResponse {
360
+ info!("SSE client connected");
361
+
362
+ state.producer.on_connect().await;
363
+
364
+ let producer = Arc::clone(&state.producer);
365
+ let event_schema = state.event_schema.clone();
366
+ let stream = stream::unfold((producer, event_schema), |(producer, event_schema)| async move {
367
+ match producer.next_event().await {
368
+ Some(sse_event) => {
369
+ debug!("Sending SSE event: {:?}", sse_event.event_type);
370
+
371
+ if let Some(validator) = &event_schema
372
+ && !validator.is_valid(&sse_event.data)
373
+ {
374
+ error!("SSE event validation failed");
375
+ return Some((
376
+ Ok::<_, Infallible>(Event::default().data("validation_error")),
377
+ (producer, event_schema),
378
+ ));
379
+ }
380
+
381
+ match sse_event.into_axum_event() {
382
+ Ok(event) => Some((Ok::<_, Infallible>(event), (producer, event_schema))),
383
+ Err(e) => {
384
+ error!("Failed to serialize SSE event: {}", e);
385
+ None
386
+ }
387
+ }
388
+ }
389
+ None => {
390
+ info!("SSE stream ended");
391
+ None
392
+ }
393
+ }
394
+ });
395
+
396
+ let sse_response =
397
+ Sse::new(stream).keep_alive(KeepAlive::new().interval(Duration::from_secs(15)).text("keep-alive"));
398
+
399
+ sse_response.into_response()
400
+ }
401
+
402
+ #[cfg(test)]
403
+ mod tests {
404
+ use super::*;
405
+
406
+ struct TestProducer {
407
+ count: std::sync::atomic::AtomicUsize,
408
+ }
409
+
410
+ impl SseEventProducer for TestProducer {
411
+ async fn next_event(&self) -> Option<SseEvent> {
412
+ let count = self.count.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
413
+ if count < 3 {
414
+ Some(SseEvent::new(serde_json::json!({
415
+ "message": format!("Event {}", count)
416
+ })))
417
+ } else {
418
+ None
419
+ }
420
+ }
421
+ }
422
+
423
+ #[test]
424
+ fn test_sse_event_creation() {
425
+ let event = SseEvent::new(serde_json::json!({"test": "data"}));
426
+ assert!(event.event_type.is_none());
427
+ assert!(event.id.is_none());
428
+ assert!(event.retry.is_none());
429
+
430
+ let event = SseEvent::with_type("notification", serde_json::json!({"test": "data"}))
431
+ .with_id("123")
432
+ .with_retry(5000);
433
+ assert_eq!(event.event_type, Some("notification".to_string()));
434
+ assert_eq!(event.id, Some("123".to_string()));
435
+ assert_eq!(event.retry, Some(5000));
436
+ }
437
+
438
+ #[test]
439
+ fn test_sse_state_creation() {
440
+ let producer = TestProducer {
441
+ count: std::sync::atomic::AtomicUsize::new(0),
442
+ };
443
+ let state = SseState::new(producer);
444
+ let cloned = state.clone();
445
+ assert!(Arc::ptr_eq(&state.producer, &cloned.producer));
446
+ }
447
+ }