@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.
- package/README.md +51 -0
- package/docs/api-ref/go.md +399 -0
- package/docs/api-ref/python.md +399 -0
- package/docs/api-ref/rust.md +399 -0
- package/docs/api-ref/typescript.md +399 -0
- package/docs/invalidate-spec.md +303 -0
- package/docs/sdk.md +722 -0
- package/package.json +120 -0
- package/src/accepts.ts +78 -0
- package/src/body-limit.ts +100 -0
- package/src/build/index.ts +174 -0
- package/src/cli-go/auth.go +15 -0
- package/src/cli-go/config.go +165 -0
- package/src/cli-go/errors.go +57 -0
- package/src/cli-go/multipart.go +55 -0
- package/src/cli-go/output.go +109 -0
- package/src/cli-go/root.go +30 -0
- package/src/cli-go/stream.go +88 -0
- package/src/cli-go/version.go +5 -0
- package/src/cli.ts +204 -0
- package/src/client/error.ts +27 -0
- package/src/client/http.ts +502 -0
- package/src/client/index.ts +127 -0
- package/src/client/sdk.ts +265 -0
- package/src/client/sse.ts +102 -0
- package/src/client/types.ts +211 -0
- package/src/client/ws.ts +115 -0
- package/src/client-go/errors.go +207 -0
- package/src/client-go/invalidation.go +373 -0
- package/src/client-go/realtime.go +317 -0
- package/src/client-go/result.go +20 -0
- package/src/client-go/runtime.go +413 -0
- package/src/client-go/sse.go +149 -0
- package/src/client-go/transport.go +452 -0
- package/src/client-go/ws.go +104 -0
- package/src/client-mcp/README.md +10 -0
- package/src/client-mcp/server.ts +94 -0
- package/src/client-mcp/types.ts +19 -0
- package/src/client-python/_errors.py +115 -0
- package/src/client-python/_invalidation.py +578 -0
- package/src/client-python/_realtime.py +156 -0
- package/src/client-python/_runtime.py +469 -0
- package/src/client-python/_sse.py +99 -0
- package/src/client-python/_transport.py +335 -0
- package/src/client-python/_ws.py +131 -0
- package/src/client-python/py.typed +0 -0
- package/src/client-rust/errors.rs +200 -0
- package/src/client-rust/invalidation.rs +441 -0
- package/src/client-rust/invalidation_sync.rs +306 -0
- package/src/client-rust/realtime.rs +695 -0
- package/src/client-rust/result.rs +22 -0
- package/src/client-rust/runtime.rs +547 -0
- package/src/client-rust/runtime_sync.rs +452 -0
- package/src/client-rust/sse.rs +167 -0
- package/src/client-rust/ws.rs +143 -0
- package/src/cloudflare/realtime.ts +236 -0
- package/src/codegen-go-cli.ts +1180 -0
- package/src/codegen-go.ts +1010 -0
- package/src/codegen-hash.ts +8 -0
- package/src/codegen-ir.ts +517 -0
- package/src/codegen-loaders.ts +53 -0
- package/src/codegen-mcp.ts +364 -0
- package/src/codegen-python.ts +1028 -0
- package/src/codegen-rust.ts +1652 -0
- package/src/codegen-sanitize.ts +67 -0
- package/src/codegen-schema-naming.ts +146 -0
- package/src/codegen.ts +5015 -0
- package/src/context.ts +210 -0
- package/src/cookie-sign.ts +58 -0
- package/src/cookie.ts +89 -0
- package/src/cors.ts +96 -0
- package/src/crypto.ts +19 -0
- package/src/csrf.ts +54 -0
- package/src/curl-logger.ts +235 -0
- package/src/detect-runtime.ts +19 -0
- package/src/error.ts +52 -0
- package/src/errors.ts +87 -0
- package/src/etag.ts +74 -0
- package/src/feature-load.ts +23 -0
- package/src/go-type-emitter.ts +404 -0
- package/src/honey-response.ts +354 -0
- package/src/i18n-slot.ts +20 -0
- package/src/i18n.ts +234 -0
- package/src/index.ts +3843 -0
- package/src/init.ts +149 -0
- package/src/input.ts +8 -0
- package/src/ip-restrict.ts +192 -0
- package/src/logger.ts +149 -0
- package/src/middleware.ts +199 -0
- package/src/node-request.ts +240 -0
- package/src/node.ts +212 -0
- package/src/openapi/register.ts +19 -0
- package/src/openapi/scalar.ts +33 -0
- package/src/openapi/spec-factory.ts +46 -0
- package/src/openapi/spec.ts +31 -0
- package/src/openapi/swagger.ts +34 -0
- package/src/plugin.ts +771 -0
- package/src/powered-by.ts +17 -0
- package/src/pretty-json.ts +39 -0
- package/src/proxy.ts +155 -0
- package/src/python-type-emitter.ts +144 -0
- package/src/realtime/buffer.ts +101 -0
- package/src/realtime/bus.ts +93 -0
- package/src/realtime/longpoll.ts +148 -0
- package/src/realtime/protocol.ts +128 -0
- package/src/realtime/route.ts +104 -0
- package/src/realtime/runtime.ts +430 -0
- package/src/request-id.ts +23 -0
- package/src/request-to-curl.ts +34 -0
- package/src/response.ts +414 -0
- package/src/rust-type-emitter.ts +412 -0
- package/src/secure-headers.ts +69 -0
- package/src/serve-register.ts +8 -0
- package/src/serve-slot.ts +20 -0
- package/src/serve.ts +141 -0
- package/src/server-timing.ts +66 -0
- package/src/static.ts +60 -0
- package/src/telemetry/otel.ts +139 -0
- package/src/testing.ts +114 -0
- package/src/timeout.ts +36 -0
- package/src/tree.ts +464 -0
- package/src/ts-type-emitter.ts +106 -0
- package/src/type-emitter.ts +524 -0
- package/src/type-extractor.ts +749 -0
- package/src/types.ts +638 -0
- package/src/validation.ts +300 -0
- package/src/ws/bun.ts +89 -0
- package/src/ws/cloudflare.ts +133 -0
- package/src/ws/deno.ts +109 -0
- package/src/ws/node.ts +161 -0
- 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
|
+
}
|