@lovrozagar/honey 0.1.0

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 (131) hide show
  1. package/README.md +51 -0
  2. package/docs/api-ref/go.md +399 -0
  3. package/docs/api-ref/python.md +399 -0
  4. package/docs/api-ref/rust.md +399 -0
  5. package/docs/api-ref/typescript.md +399 -0
  6. package/docs/invalidate-spec.md +303 -0
  7. package/docs/sdk.md +722 -0
  8. package/package.json +120 -0
  9. package/src/accepts.ts +78 -0
  10. package/src/body-limit.ts +100 -0
  11. package/src/build/index.ts +174 -0
  12. package/src/cli-go/auth.go +15 -0
  13. package/src/cli-go/config.go +165 -0
  14. package/src/cli-go/errors.go +57 -0
  15. package/src/cli-go/multipart.go +55 -0
  16. package/src/cli-go/output.go +109 -0
  17. package/src/cli-go/root.go +30 -0
  18. package/src/cli-go/stream.go +88 -0
  19. package/src/cli-go/version.go +5 -0
  20. package/src/cli.ts +204 -0
  21. package/src/client/error.ts +27 -0
  22. package/src/client/http.ts +502 -0
  23. package/src/client/index.ts +127 -0
  24. package/src/client/sdk.ts +265 -0
  25. package/src/client/sse.ts +102 -0
  26. package/src/client/types.ts +211 -0
  27. package/src/client/ws.ts +115 -0
  28. package/src/client-go/errors.go +207 -0
  29. package/src/client-go/invalidation.go +373 -0
  30. package/src/client-go/realtime.go +317 -0
  31. package/src/client-go/result.go +20 -0
  32. package/src/client-go/runtime.go +413 -0
  33. package/src/client-go/sse.go +149 -0
  34. package/src/client-go/transport.go +452 -0
  35. package/src/client-go/ws.go +104 -0
  36. package/src/client-mcp/README.md +10 -0
  37. package/src/client-mcp/server.ts +94 -0
  38. package/src/client-mcp/types.ts +19 -0
  39. package/src/client-python/_errors.py +115 -0
  40. package/src/client-python/_invalidation.py +578 -0
  41. package/src/client-python/_realtime.py +156 -0
  42. package/src/client-python/_runtime.py +469 -0
  43. package/src/client-python/_sse.py +99 -0
  44. package/src/client-python/_transport.py +335 -0
  45. package/src/client-python/_ws.py +131 -0
  46. package/src/client-python/py.typed +0 -0
  47. package/src/client-rust/errors.rs +200 -0
  48. package/src/client-rust/invalidation.rs +441 -0
  49. package/src/client-rust/invalidation_sync.rs +306 -0
  50. package/src/client-rust/realtime.rs +695 -0
  51. package/src/client-rust/result.rs +22 -0
  52. package/src/client-rust/runtime.rs +547 -0
  53. package/src/client-rust/runtime_sync.rs +452 -0
  54. package/src/client-rust/sse.rs +167 -0
  55. package/src/client-rust/ws.rs +143 -0
  56. package/src/cloudflare/realtime.ts +236 -0
  57. package/src/codegen-go-cli.ts +1180 -0
  58. package/src/codegen-go.ts +1010 -0
  59. package/src/codegen-hash.ts +8 -0
  60. package/src/codegen-ir.ts +517 -0
  61. package/src/codegen-loaders.ts +53 -0
  62. package/src/codegen-mcp.ts +364 -0
  63. package/src/codegen-python.ts +1028 -0
  64. package/src/codegen-rust.ts +1652 -0
  65. package/src/codegen-sanitize.ts +67 -0
  66. package/src/codegen-schema-naming.ts +146 -0
  67. package/src/codegen.ts +5015 -0
  68. package/src/context.ts +210 -0
  69. package/src/cookie-sign.ts +58 -0
  70. package/src/cookie.ts +89 -0
  71. package/src/cors.ts +96 -0
  72. package/src/crypto.ts +19 -0
  73. package/src/csrf.ts +54 -0
  74. package/src/curl-logger.ts +235 -0
  75. package/src/detect-runtime.ts +19 -0
  76. package/src/error.ts +52 -0
  77. package/src/errors.ts +87 -0
  78. package/src/etag.ts +74 -0
  79. package/src/feature-load.ts +23 -0
  80. package/src/go-type-emitter.ts +404 -0
  81. package/src/honey-response.ts +354 -0
  82. package/src/i18n-slot.ts +20 -0
  83. package/src/i18n.ts +234 -0
  84. package/src/index.ts +3843 -0
  85. package/src/init.ts +149 -0
  86. package/src/input.ts +8 -0
  87. package/src/ip-restrict.ts +192 -0
  88. package/src/logger.ts +149 -0
  89. package/src/middleware.ts +199 -0
  90. package/src/node-request.ts +240 -0
  91. package/src/node.ts +212 -0
  92. package/src/openapi/register.ts +19 -0
  93. package/src/openapi/scalar.ts +33 -0
  94. package/src/openapi/spec-factory.ts +46 -0
  95. package/src/openapi/spec.ts +31 -0
  96. package/src/openapi/swagger.ts +34 -0
  97. package/src/plugin.ts +771 -0
  98. package/src/powered-by.ts +17 -0
  99. package/src/pretty-json.ts +39 -0
  100. package/src/proxy.ts +155 -0
  101. package/src/python-type-emitter.ts +144 -0
  102. package/src/realtime/buffer.ts +101 -0
  103. package/src/realtime/bus.ts +93 -0
  104. package/src/realtime/longpoll.ts +148 -0
  105. package/src/realtime/protocol.ts +128 -0
  106. package/src/realtime/route.ts +104 -0
  107. package/src/realtime/runtime.ts +430 -0
  108. package/src/request-id.ts +23 -0
  109. package/src/request-to-curl.ts +34 -0
  110. package/src/response.ts +414 -0
  111. package/src/rust-type-emitter.ts +412 -0
  112. package/src/secure-headers.ts +69 -0
  113. package/src/serve-register.ts +8 -0
  114. package/src/serve-slot.ts +20 -0
  115. package/src/serve.ts +141 -0
  116. package/src/server-timing.ts +66 -0
  117. package/src/static.ts +60 -0
  118. package/src/telemetry/otel.ts +139 -0
  119. package/src/testing.ts +114 -0
  120. package/src/timeout.ts +36 -0
  121. package/src/tree.ts +464 -0
  122. package/src/ts-type-emitter.ts +106 -0
  123. package/src/type-emitter.ts +524 -0
  124. package/src/type-extractor.ts +749 -0
  125. package/src/types.ts +638 -0
  126. package/src/validation.ts +300 -0
  127. package/src/ws/bun.ts +89 -0
  128. package/src/ws/cloudflare.ts +133 -0
  129. package/src/ws/deno.ts +109 -0
  130. package/src/ws/node.ts +161 -0
  131. package/src/yaml.ts +80 -0
@@ -0,0 +1,695 @@
1
+ /* Code generated by honey. DO NOT EDIT. */
2
+
3
+ use crate::errors::Error;
4
+ use bytes::Bytes;
5
+ use serde::{de::DeserializeOwned, Serialize};
6
+ use std::collections::HashMap;
7
+ use std::collections::VecDeque;
8
+ use std::marker::PhantomData;
9
+ use std::pin::Pin;
10
+
11
+ /// TransportKind tags which transport a connection was opened with.
12
+ /// The behaviour-bearing abstraction is the [`Transport`] trait.
13
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
14
+ pub enum TransportKind {
15
+ Ws,
16
+ Sse,
17
+ Longpoll,
18
+ }
19
+
20
+ /// ConnectionState mirrors the TS client's connection lifecycle.
21
+ #[derive(Debug, Clone, PartialEq, Eq)]
22
+ pub enum ConnectionState {
23
+ Connecting,
24
+ Connected,
25
+ Reconnecting,
26
+ Disconnected,
27
+ }
28
+
29
+ /// RealtimeError wraps realtime-specific error variants.
30
+ #[derive(Debug)]
31
+ pub enum RealtimeError {
32
+ Connect(Error),
33
+ Send(Error),
34
+ Recv(Error),
35
+ Closed,
36
+ NotImplemented(String),
37
+ }
38
+
39
+ impl std::fmt::Display for RealtimeError {
40
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41
+ match self {
42
+ RealtimeError::Connect(e) => write!(f, "connect error: {e}"),
43
+ RealtimeError::Send(e) => write!(f, "send error: {e}"),
44
+ RealtimeError::Recv(e) => write!(f, "recv error: {e}"),
45
+ RealtimeError::Closed => write!(f, "connection closed"),
46
+ RealtimeError::NotImplemented(s) => write!(f, "not implemented: {s}"),
47
+ }
48
+ }
49
+ }
50
+
51
+ impl std::error::Error for RealtimeError {}
52
+
53
+ /// ResumableConnectionOpts configures reconnect behaviour and auth.
54
+ #[derive(Debug, Clone, Default)]
55
+ pub struct ResumableConnectionOpts {
56
+ pub reconnect_token: Option<String>,
57
+ pub protocols: Option<Vec<String>>,
58
+ pub max_reconnect_attempts: Option<u32>,
59
+ pub reconnect_delay_ms: Option<u64>,
60
+ pub last_event_id: Option<String>,
61
+ pub headers: HashMap<String, String>,
62
+ }
63
+
64
+ /// TransportOpts is handed to each `Transport::connect`.
65
+ /// Mirrors TS `TransportOpts`; `http_client` lets SSE / longpoll adapters share a pool.
66
+ #[derive(Debug, Clone, Default)]
67
+ pub struct TransportOpts {
68
+ pub reconnect_token: Option<String>,
69
+ pub protocols: Option<Vec<String>>,
70
+ pub last_event_id: Option<String>,
71
+ pub headers: HashMap<String, String>,
72
+ pub http_client: Option<reqwest::Client>,
73
+ pub max_reconnect_attempts: Option<u32>,
74
+ pub reconnect_delay_ms: Option<u64>,
75
+ }
76
+
77
+ type WsStream = tokio_tungstenite::WebSocketStream<
78
+ tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>,
79
+ >;
80
+
81
+ type ByteStream = Pin<Box<dyn futures_util::Stream<Item = Result<Bytes, reqwest::Error>> + Send>>;
82
+
83
+ /// SseState holds mutable state for an active SSE resumable connection.
84
+ pub struct SseState {
85
+ pub byte_stream: ByteStream,
86
+ pub buf: Vec<u8>,
87
+ pub last_event_id: Option<String>,
88
+ pub retry_hint_ms: u64,
89
+ /* internal fields for reconnect and send */
90
+ http_client: reqwest::Client,
91
+ url: String,
92
+ extra_headers: HashMap<String, String>,
93
+ max_reconnect_attempts: u32,
94
+ reconnect_delay_ms: u64,
95
+ reconnect_attempts: u32,
96
+ }
97
+
98
+ /// LongpollState holds mutable state for a long-poll resumable connection.
99
+ pub struct LongpollState {
100
+ pub client: reqwest::Client,
101
+ pub url: String,
102
+ pub cursor: Option<String>,
103
+ pub pending_events: VecDeque<serde_json::Value>,
104
+ pub headers: HashMap<String, String>,
105
+ max_reconnect_attempts: u32,
106
+ reconnect_delay_ms: u64,
107
+ reconnect_attempts: u32,
108
+ }
109
+
110
+ /// TransportConn is the live per-transport connection.
111
+ /// Each built-in adapter returns its own impl (`WsConn`, `SseConn`, `LongpollConn`).
112
+ /// Send-only (not Sync): SSE's byte stream is a `Pin<Box<dyn Stream + Send>>`,
113
+ /// exclusive owned access via `&mut self` is sufficient — no cross-thread sharing needed.
114
+ #[async_trait::async_trait]
115
+ pub trait TransportConn: Send {
116
+ async fn send_json(&mut self, value: serde_json::Value) -> Result<(), RealtimeError>;
117
+ async fn recv_json(&mut self) -> Result<serde_json::Value, RealtimeError>;
118
+ async fn close(&mut self) -> Result<(), RealtimeError>;
119
+ fn kind(&self) -> TransportKind;
120
+ }
121
+
122
+ /// Transport is the pluggable adapter trait. Consumers can swap in their own
123
+ /// WS / SSE / longpoll impls by implementing this.
124
+ #[async_trait::async_trait]
125
+ pub trait Transport: Send + Sync + 'static {
126
+ fn name(&self) -> &'static str;
127
+ fn kind(&self) -> TransportKind;
128
+ async fn connect(
129
+ &self,
130
+ url: &str,
131
+ opts: &TransportOpts,
132
+ ) -> Result<Box<dyn TransportConn>, RealtimeError>;
133
+ }
134
+
135
+ /* ------------------------------------------------------------------ */
136
+ /* WS adapter */
137
+ /* ------------------------------------------------------------------ */
138
+
139
+ /// WsTransport dials a WebSocket via tokio-tungstenite.
140
+ #[derive(Debug, Default)]
141
+ pub struct WsTransport;
142
+
143
+ /// WsConn is the live WS connection returned by [`WsTransport::connect`].
144
+ pub struct WsConn {
145
+ /* WsStream is ~1384 bytes; box to flatten */
146
+ stream: Box<WsStream>,
147
+ }
148
+
149
+ #[async_trait::async_trait]
150
+ impl Transport for WsTransport {
151
+ fn name(&self) -> &'static str {
152
+ "ws"
153
+ }
154
+ fn kind(&self) -> TransportKind {
155
+ TransportKind::Ws
156
+ }
157
+ async fn connect(
158
+ &self,
159
+ url: &str,
160
+ opts: &TransportOpts,
161
+ ) -> Result<Box<dyn TransportConn>, RealtimeError> {
162
+ use tokio_tungstenite::tungstenite::client::IntoClientRequest;
163
+ let ws_url = url.replacen("https://", "wss://", 1).replacen("http://", "ws://", 1);
164
+ let mut req = ws_url
165
+ .as_str()
166
+ .into_client_request()
167
+ .map_err(|e| RealtimeError::Connect(Error::Other(e.to_string())))?;
168
+ if let Some(ref protos) = opts.protocols {
169
+ if !protos.is_empty() {
170
+ req.headers_mut().insert(
171
+ "Sec-WebSocket-Protocol",
172
+ protos
173
+ .join(", ")
174
+ .parse()
175
+ .map_err(|_| RealtimeError::Connect(Error::Other("invalid protocol header".into())))?,
176
+ );
177
+ }
178
+ }
179
+ for (k, v) in &opts.headers {
180
+ if let (Ok(name), Ok(val)) = (
181
+ tokio_tungstenite::tungstenite::http::HeaderName::from_bytes(k.as_bytes()),
182
+ v.parse::<tokio_tungstenite::tungstenite::http::HeaderValue>(),
183
+ ) {
184
+ req.headers_mut().insert(name, val);
185
+ }
186
+ }
187
+ let (stream, _) = tokio_tungstenite::connect_async(req)
188
+ .await
189
+ .map_err(|e| RealtimeError::Connect(Error::Other(e.to_string())))?;
190
+ Ok(Box::new(WsConn { stream: Box::new(stream) }))
191
+ }
192
+ }
193
+
194
+ #[async_trait::async_trait]
195
+ impl TransportConn for WsConn {
196
+ fn kind(&self) -> TransportKind {
197
+ TransportKind::Ws
198
+ }
199
+ async fn send_json(&mut self, value: serde_json::Value) -> Result<(), RealtimeError> {
200
+ use futures_util::SinkExt;
201
+ use tokio_tungstenite::tungstenite::Message;
202
+ let encoded = serde_json::to_vec(&value)
203
+ .map_err(|e| RealtimeError::Send(Error::Json(e)))?;
204
+ self.stream
205
+ .send(Message::Text(String::from_utf8_lossy(&encoded).into_owned()))
206
+ .await
207
+ .map_err(|e| RealtimeError::Send(Error::Other(e.to_string())))
208
+ }
209
+ async fn recv_json(&mut self) -> Result<serde_json::Value, RealtimeError> {
210
+ use futures_util::StreamExt;
211
+ use tokio_tungstenite::tungstenite::Message;
212
+ loop {
213
+ match self.stream.next().await {
214
+ None => return Err(RealtimeError::Closed),
215
+ Some(Err(e)) => return Err(RealtimeError::Recv(Error::Other(e.to_string()))),
216
+ Some(Ok(Message::Text(t))) => {
217
+ return serde_json::from_slice(t.as_bytes())
218
+ .map_err(|e| RealtimeError::Recv(Error::Json(e)));
219
+ }
220
+ Some(Ok(Message::Binary(b))) => {
221
+ return serde_json::from_slice(&b)
222
+ .map_err(|e| RealtimeError::Recv(Error::Json(e)));
223
+ }
224
+ Some(Ok(_)) => continue, /* ping/pong/close — skip */
225
+ }
226
+ }
227
+ }
228
+ async fn close(&mut self) -> Result<(), RealtimeError> {
229
+ use futures_util::SinkExt;
230
+ use tokio_tungstenite::tungstenite::protocol::CloseFrame;
231
+ use tokio_tungstenite::tungstenite::protocol::frame::coding::CloseCode;
232
+ let frame = CloseFrame { code: CloseCode::Normal, reason: "client close".into() };
233
+ self.stream
234
+ .send(tokio_tungstenite::tungstenite::Message::Close(Some(frame)))
235
+ .await
236
+ .ok();
237
+ Ok(())
238
+ }
239
+ }
240
+
241
+ /* ------------------------------------------------------------------ */
242
+ /* SSE adapter */
243
+ /* ------------------------------------------------------------------ */
244
+
245
+ /// SseTransport opens a GET / text-event-stream using reqwest.
246
+ #[derive(Debug, Default)]
247
+ pub struct SseTransport {
248
+ pub http_client: Option<reqwest::Client>,
249
+ }
250
+
251
+ /// SseConn is the live SSE connection returned by [`SseTransport::connect`].
252
+ pub struct SseConn {
253
+ state: SseState,
254
+ }
255
+
256
+ #[async_trait::async_trait]
257
+ impl Transport for SseTransport {
258
+ fn name(&self) -> &'static str {
259
+ "sse"
260
+ }
261
+ fn kind(&self) -> TransportKind {
262
+ TransportKind::Sse
263
+ }
264
+ async fn connect(
265
+ &self,
266
+ url: &str,
267
+ opts: &TransportOpts,
268
+ ) -> Result<Box<dyn TransportConn>, RealtimeError> {
269
+ let client = self
270
+ .http_client
271
+ .clone()
272
+ .or_else(|| opts.http_client.clone())
273
+ .unwrap_or_else(reqwest::Client::new);
274
+ let state = build_sse_state(url.to_string(), client, opts)
275
+ .await
276
+ .map_err(|e| RealtimeError::Connect(Error::Other(e.to_string())))?;
277
+ Ok(Box::new(SseConn { state }))
278
+ }
279
+ }
280
+
281
+ #[async_trait::async_trait]
282
+ impl TransportConn for SseConn {
283
+ fn kind(&self) -> TransportKind {
284
+ TransportKind::Sse
285
+ }
286
+ async fn send_json(&mut self, value: serde_json::Value) -> Result<(), RealtimeError> {
287
+ /* SSE is server → client; send POSTs back to the endpoint. */
288
+ self.state
289
+ .http_client
290
+ .post(self.state.url.as_str())
291
+ .json(&value)
292
+ .send()
293
+ .await
294
+ .map_err(|e| RealtimeError::Send(Error::Other(e.to_string())))?;
295
+ Ok(())
296
+ }
297
+ async fn recv_json(&mut self) -> Result<serde_json::Value, RealtimeError> {
298
+ use crate::sse::{find_double_newline, parse_sse_block};
299
+ use futures_util::StreamExt;
300
+ loop {
301
+ if let Some((start, end)) = find_double_newline(&self.state.buf) {
302
+ let block = self.state.buf[..start].to_vec();
303
+ self.state.buf.drain(..end);
304
+ let block_str = String::from_utf8_lossy(&block);
305
+ let trimmed = block_str.trim();
306
+ if !trimmed.is_empty() {
307
+ if let Some(ev) = parse_sse_block(trimmed) {
308
+ if !ev.id.is_empty() {
309
+ self.state.last_event_id = Some(ev.id.clone());
310
+ }
311
+ if ev.retry > 0 {
312
+ self.state.retry_hint_ms = ev.retry as u64;
313
+ }
314
+ return serde_json::from_str(&ev.data)
315
+ .map_err(|e| RealtimeError::Recv(Error::Json(e)));
316
+ }
317
+ }
318
+ continue;
319
+ }
320
+ match self.state.byte_stream.next().await {
321
+ Some(Ok(bytes)) => {
322
+ self.state.buf.extend_from_slice(&bytes);
323
+ }
324
+ Some(Err(e)) => {
325
+ return Err(RealtimeError::Recv(Error::Other(e.to_string())));
326
+ }
327
+ None => {
328
+ /* stream ended — attempt transport-level reconnect */
329
+ self.state.reconnect_attempts += 1;
330
+ if self.state.reconnect_attempts >= self.state.max_reconnect_attempts {
331
+ return Err(RealtimeError::Closed);
332
+ }
333
+ let delay_ms = if self.state.retry_hint_ms > 0 {
334
+ self.state.retry_hint_ms
335
+ } else {
336
+ self.state.reconnect_delay_ms
337
+ };
338
+ tokio::time::sleep(tokio::time::Duration::from_millis(delay_ms)).await;
339
+ let reopen_opts = TransportOpts {
340
+ last_event_id: self.state.last_event_id.clone(),
341
+ headers: self.state.extra_headers.clone(),
342
+ max_reconnect_attempts: Some(self.state.max_reconnect_attempts),
343
+ reconnect_delay_ms: Some(self.state.reconnect_delay_ms),
344
+ ..Default::default()
345
+ };
346
+ let new_state = build_sse_state(
347
+ self.state.url.clone(),
348
+ self.state.http_client.clone(),
349
+ &reopen_opts,
350
+ )
351
+ .await
352
+ .map_err(|e| RealtimeError::Connect(Error::Other(e.to_string())))?;
353
+ self.state.byte_stream = new_state.byte_stream;
354
+ self.state.buf.clear();
355
+ }
356
+ }
357
+ }
358
+ }
359
+ async fn close(&mut self) -> Result<(), RealtimeError> {
360
+ /* reqwest stream is dropped with self; nothing to do explicitly. */
361
+ Ok(())
362
+ }
363
+ }
364
+
365
+ /* ------------------------------------------------------------------ */
366
+ /* Longpoll adapter */
367
+ /* ------------------------------------------------------------------ */
368
+
369
+ /// LongpollTransport opens a long-poll GET loop using reqwest.
370
+ #[derive(Debug, Default)]
371
+ pub struct LongpollTransport {
372
+ pub http_client: Option<reqwest::Client>,
373
+ }
374
+
375
+ /// LongpollConn is the live longpoll connection returned by [`LongpollTransport::connect`].
376
+ pub struct LongpollConn {
377
+ state: LongpollState,
378
+ }
379
+
380
+ #[async_trait::async_trait]
381
+ impl Transport for LongpollTransport {
382
+ fn name(&self) -> &'static str {
383
+ "longpoll"
384
+ }
385
+ fn kind(&self) -> TransportKind {
386
+ TransportKind::Longpoll
387
+ }
388
+ async fn connect(
389
+ &self,
390
+ url: &str,
391
+ opts: &TransportOpts,
392
+ ) -> Result<Box<dyn TransportConn>, RealtimeError> {
393
+ let client = self
394
+ .http_client
395
+ .clone()
396
+ .or_else(|| opts.http_client.clone())
397
+ .unwrap_or_else(reqwest::Client::new);
398
+ let state = LongpollState {
399
+ client,
400
+ url: url.to_string(),
401
+ cursor: None,
402
+ pending_events: VecDeque::new(),
403
+ headers: opts.headers.clone(),
404
+ max_reconnect_attempts: opts.max_reconnect_attempts.unwrap_or(5),
405
+ reconnect_delay_ms: opts.reconnect_delay_ms.unwrap_or(1000),
406
+ reconnect_attempts: 0,
407
+ };
408
+ Ok(Box::new(LongpollConn { state }))
409
+ }
410
+ }
411
+
412
+ #[async_trait::async_trait]
413
+ impl TransportConn for LongpollConn {
414
+ fn kind(&self) -> TransportKind {
415
+ TransportKind::Longpoll
416
+ }
417
+ async fn send_json(&mut self, value: serde_json::Value) -> Result<(), RealtimeError> {
418
+ self.state
419
+ .client
420
+ .post(self.state.url.as_str())
421
+ .json(&value)
422
+ .send()
423
+ .await
424
+ .map_err(|e| RealtimeError::Send(Error::Other(e.to_string())))?;
425
+ Ok(())
426
+ }
427
+ async fn recv_json(&mut self) -> Result<serde_json::Value, RealtimeError> {
428
+ loop {
429
+ if let Some(ev) = self.state.pending_events.pop_front() {
430
+ return Ok(ev);
431
+ }
432
+ let request_url = match &self.state.cursor {
433
+ Some(c) => format!("{}?cursor={}", self.state.url, c),
434
+ None => self.state.url.clone(),
435
+ };
436
+ let mut req = self.state.client.get(request_url.as_str());
437
+ for (k, v) in &self.state.headers {
438
+ req = req.header(k.as_str(), v.as_str());
439
+ }
440
+ match req.send().await {
441
+ Err(e) => {
442
+ self.state.reconnect_attempts += 1;
443
+ if self.state.reconnect_attempts >= self.state.max_reconnect_attempts {
444
+ return Err(RealtimeError::Recv(Error::Other(e.to_string())));
445
+ }
446
+ tokio::time::sleep(tokio::time::Duration::from_millis(
447
+ self.state.reconnect_delay_ms,
448
+ ))
449
+ .await;
450
+ }
451
+ Ok(resp) => {
452
+ let bytes = resp
453
+ .bytes()
454
+ .await
455
+ .map_err(|e| RealtimeError::Recv(Error::Other(e.to_string())))?;
456
+
457
+ #[derive(serde::Deserialize)]
458
+ struct PollResponse {
459
+ #[serde(default)]
460
+ events: Vec<serde_json::Value>,
461
+ cursor: Option<String>,
462
+ }
463
+
464
+ let parsed: PollResponse = serde_json::from_slice(&bytes)
465
+ .map_err(|e| RealtimeError::Recv(Error::Json(e)))?;
466
+ self.state.cursor = parsed.cursor;
467
+ for ev in parsed.events {
468
+ self.state.pending_events.push_back(ev);
469
+ }
470
+ }
471
+ }
472
+ }
473
+ }
474
+ async fn close(&mut self) -> Result<(), RealtimeError> {
475
+ /* no persistent connection to tear down */
476
+ Ok(())
477
+ }
478
+ }
479
+
480
+ /* ------------------------------------------------------------------ */
481
+ /* ResumableConnection */
482
+ /* ------------------------------------------------------------------ */
483
+
484
+ /// ResumableConnection is a typed, self-reconnecting realtime channel.
485
+ /// Send/Recv type parameters encode the message types flowing in each direction.
486
+ pub struct ResumableConnection<Send, Recv> {
487
+ state: ConnectionState,
488
+ url: String,
489
+ opts: ResumableConnectionOpts,
490
+ transport_opts: TransportOpts,
491
+ transports: Vec<Box<dyn Transport>>,
492
+ conn: Option<Box<dyn TransportConn>>,
493
+ proven_index: Option<usize>,
494
+ reconnect_attempts: u32,
495
+ _send: PhantomData<Send>,
496
+ _recv: PhantomData<Recv>,
497
+ }
498
+
499
+ impl<SendMsg, RecvMsg> ResumableConnection<SendMsg, RecvMsg>
500
+ where
501
+ SendMsg: Serialize,
502
+ RecvMsg: DeserializeOwned,
503
+ {
504
+ /// connect tries each transport in order, memoizes the first that succeeds,
505
+ /// and returns the active ResumableConnection.
506
+ pub async fn connect(
507
+ url: String,
508
+ opts: ResumableConnectionOpts,
509
+ transports: Vec<Box<dyn Transport>>,
510
+ ) -> Result<Self, RealtimeError> {
511
+ if transports.is_empty() {
512
+ return Err(RealtimeError::Connect(Error::Other(
513
+ "no transports configured".into(),
514
+ )));
515
+ }
516
+ let transport_opts = TransportOpts {
517
+ reconnect_token: opts.reconnect_token.clone(),
518
+ protocols: opts.protocols.clone(),
519
+ last_event_id: opts.last_event_id.clone(),
520
+ headers: opts.headers.clone(),
521
+ http_client: None,
522
+ max_reconnect_attempts: opts.max_reconnect_attempts,
523
+ reconnect_delay_ms: opts.reconnect_delay_ms,
524
+ };
525
+ let mut rc = ResumableConnection {
526
+ state: ConnectionState::Connecting,
527
+ url,
528
+ opts,
529
+ transport_opts,
530
+ transports,
531
+ conn: None,
532
+ proven_index: None,
533
+ reconnect_attempts: 0,
534
+ _send: PhantomData,
535
+ _recv: PhantomData,
536
+ };
537
+ rc.open_chain().await?;
538
+ Ok(rc)
539
+ }
540
+
541
+ /// connect_with_defaults builds the canonical transport chain
542
+ /// `[WsTransport, SseTransport, LongpollTransport]` and calls [`Self::connect`].
543
+ /// Convenience entry point for consumers that don't need a custom chain.
544
+ pub async fn connect_with_defaults(
545
+ url: String,
546
+ opts: ResumableConnectionOpts,
547
+ ) -> Result<Self, RealtimeError> {
548
+ let transports: Vec<Box<dyn Transport>> = vec![
549
+ Box::new(WsTransport),
550
+ Box::new(SseTransport::default()),
551
+ Box::new(LongpollTransport::default()),
552
+ ];
553
+ Self::connect(url, opts, transports).await
554
+ }
555
+
556
+ /// state returns the current connection lifecycle state.
557
+ pub fn state(&self) -> &ConnectionState {
558
+ &self.state
559
+ }
560
+
561
+ /// proven_transport returns the kind of the last successfully-opened transport.
562
+ pub fn proven_transport(&self) -> Option<TransportKind> {
563
+ self.proven_index
564
+ .and_then(|i| self.transports.get(i))
565
+ .map(|t| t.kind())
566
+ }
567
+
568
+ /// current_transport returns the kind of the currently-active transport.
569
+ pub fn current_transport(&self) -> Option<TransportKind> {
570
+ self.conn.as_ref().map(|c| c.kind())
571
+ }
572
+
573
+ /// send serialises a message and writes it to the active transport.
574
+ pub async fn send(&mut self, data: &SendMsg) -> Result<(), RealtimeError> {
575
+ let conn = self.conn.as_mut().ok_or(RealtimeError::Closed)?;
576
+ let v = serde_json::to_value(data).map_err(|e| RealtimeError::Send(Error::Json(e)))?;
577
+ conn.send_json(v).await
578
+ }
579
+
580
+ /// recv reads the next message from the transport and deserialises it.
581
+ /// On transport failure: invalidates `proven_index`, sleeps `reconnect_delay_ms`,
582
+ /// re-runs `open_chain()` (full fallback sequence). Mirrors Python `_iter`
583
+ /// and TS `createResumableConnection`'s recv loop. Gives up after
584
+ /// `max_reconnect_attempts` attempts and returns the last error.
585
+ pub async fn recv(&mut self) -> Result<RecvMsg, RealtimeError> {
586
+ let max_attempts = self.opts.max_reconnect_attempts.unwrap_or(5);
587
+ let delay_ms = self.opts.reconnect_delay_ms.unwrap_or(1000);
588
+ loop {
589
+ let result = {
590
+ let conn = self.conn.as_mut().ok_or(RealtimeError::Closed)?;
591
+ conn.recv_json().await
592
+ };
593
+ match result {
594
+ Ok(v) => {
595
+ self.reconnect_attempts = 0;
596
+ return serde_json::from_value(v)
597
+ .map_err(|e| RealtimeError::Recv(Error::Json(e)));
598
+ }
599
+ Err(err) => {
600
+ self.reconnect_attempts += 1;
601
+ if max_attempts > 0 && self.reconnect_attempts > max_attempts {
602
+ self.state = ConnectionState::Disconnected;
603
+ return Err(err);
604
+ }
605
+ self.state = ConnectionState::Reconnecting;
606
+ /* drop the dead conn + invalidate proven so chain retries from the top */
607
+ if let Some(mut c) = self.conn.take() {
608
+ let _ = c.close().await;
609
+ }
610
+ self.proven_index = None;
611
+ if delay_ms > 0 {
612
+ tokio::time::sleep(tokio::time::Duration::from_millis(delay_ms)).await;
613
+ }
614
+ if let Err(open_err) = self.open_chain().await {
615
+ self.state = ConnectionState::Disconnected;
616
+ return Err(open_err);
617
+ }
618
+ }
619
+ }
620
+ }
621
+ }
622
+
623
+ /// close gracefully shuts down the connection.
624
+ pub async fn close(&mut self) -> Result<(), RealtimeError> {
625
+ if let Some(mut c) = self.conn.take() {
626
+ let _ = c.close().await;
627
+ }
628
+ self.state = ConnectionState::Disconnected;
629
+ Ok(())
630
+ }
631
+
632
+ async fn open_chain(&mut self) -> Result<(), RealtimeError> {
633
+ /* Try proven first if set */
634
+ if let Some(i) = self.proven_index {
635
+ if let Some(t) = self.transports.get(i) {
636
+ if let Ok(c) = t.connect(&self.url, &self.transport_opts).await {
637
+ self.conn = Some(c);
638
+ self.state = ConnectionState::Connected;
639
+ return Ok(());
640
+ }
641
+ }
642
+ }
643
+ /* Full chain */
644
+ let mut last_err: Option<RealtimeError> = None;
645
+ for (i, t) in self.transports.iter().enumerate() {
646
+ match t.connect(&self.url, &self.transport_opts).await {
647
+ Ok(c) => {
648
+ self.conn = Some(c);
649
+ self.proven_index = Some(i);
650
+ self.state = ConnectionState::Connected;
651
+ return Ok(());
652
+ }
653
+ Err(e) => last_err = Some(e),
654
+ }
655
+ }
656
+ Err(last_err.unwrap_or(RealtimeError::Connect(Error::Other(
657
+ "all transports failed".into(),
658
+ ))))
659
+ }
660
+
661
+ }
662
+
663
+ async fn build_sse_state(
664
+ url: String,
665
+ http_client: reqwest::Client,
666
+ opts: &TransportOpts,
667
+ ) -> Result<SseState, reqwest::Error> {
668
+ let mut req_builder = http_client
669
+ .get(url.as_str())
670
+ .header("Accept", "text/event-stream");
671
+ if let Some(ref last_event_id) = opts.last_event_id {
672
+ req_builder = req_builder.header("Last-Event-ID", last_event_id.as_str());
673
+ }
674
+ for (k, v) in &opts.headers {
675
+ req_builder = req_builder.header(k.as_str(), v.as_str());
676
+ }
677
+ let resp = req_builder.send().await?;
678
+ let reconnect_delay_ms = opts.reconnect_delay_ms.unwrap_or(1000);
679
+ let max_reconnect_attempts = opts.max_reconnect_attempts.unwrap_or(5);
680
+ let last_event_id = opts.last_event_id.clone();
681
+ let extra_headers = opts.headers.clone();
682
+ let byte_stream: ByteStream = Box::pin(resp.bytes_stream());
683
+ Ok(SseState {
684
+ byte_stream,
685
+ buf: Vec::new(),
686
+ last_event_id,
687
+ retry_hint_ms: reconnect_delay_ms,
688
+ http_client,
689
+ url,
690
+ extra_headers,
691
+ max_reconnect_attempts,
692
+ reconnect_delay_ms,
693
+ reconnect_attempts: 0,
694
+ })
695
+ }