convex-rust 0.0.0-stage → 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 (90) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +137 -2
  3. package/dist/cli/bin.d.ts +2 -0
  4. package/dist/cli/bin.js +200 -0
  5. package/dist/cli/build.d.ts +146 -0
  6. package/dist/cli/build.js +902 -0
  7. package/dist/cli/demangle.d.ts +7 -0
  8. package/dist/cli/demangle.js +328 -0
  9. package/dist/cli/diagnostics.d.ts +34 -0
  10. package/dist/cli/diagnostics.js +296 -0
  11. package/dist/cli/generate.d.ts +29 -0
  12. package/dist/cli/generate.js +82 -0
  13. package/dist/cli/manifest.d.ts +24 -0
  14. package/dist/cli/manifest.js +75 -0
  15. package/dist/cli/rustApi.d.ts +78 -0
  16. package/dist/cli/rustApi.js +609 -0
  17. package/dist/cli/scaffold.d.ts +22 -0
  18. package/dist/cli/scaffold.js +114 -0
  19. package/dist/cli/shim.d.ts +30 -0
  20. package/dist/cli/shim.js +140 -0
  21. package/dist/cli/size.d.ts +17 -0
  22. package/dist/cli/size.js +73 -0
  23. package/dist/cli/split.d.ts +20 -0
  24. package/dist/cli/split.js +140 -0
  25. package/dist/cli/symbols.d.ts +30 -0
  26. package/dist/cli/symbols.js +133 -0
  27. package/dist/cli/toolchain.d.ts +53 -0
  28. package/dist/cli/toolchain.js +167 -0
  29. package/dist/cli/validators.d.ts +6 -0
  30. package/dist/cli/validators.js +66 -0
  31. package/dist/cli/wasisdk.d.ts +82 -0
  32. package/dist/cli/wasisdk.js +220 -0
  33. package/dist/cli/wasmHints.d.ts +29 -0
  34. package/dist/cli/wasmHints.js +172 -0
  35. package/dist/runtime/base64.d.ts +4 -0
  36. package/dist/runtime/base64.js +77 -0
  37. package/dist/runtime/crypto.d.ts +7 -0
  38. package/dist/runtime/crypto.js +421 -0
  39. package/dist/runtime/fastjson.d.ts +3 -0
  40. package/dist/runtime/fastjson.js +170 -0
  41. package/dist/runtime/host.d.ts +73 -0
  42. package/dist/runtime/host.js +1230 -0
  43. package/dist/runtime/http.d.ts +40 -0
  44. package/dist/runtime/http.js +157 -0
  45. package/dist/runtime/index.d.ts +2 -0
  46. package/dist/runtime/index.js +1 -0
  47. package/dist/runtime/intl.d.ts +5 -0
  48. package/dist/runtime/intl.js +69 -0
  49. package/dist/runtime/tmpfs.d.ts +129 -0
  50. package/dist/runtime/tmpfs.js +624 -0
  51. package/dist/runtime/wasi.d.ts +33 -0
  52. package/dist/runtime/wasi.js +216 -0
  53. package/package.json +57 -4
  54. package/rust/convex_fn/Cargo.toml +32 -0
  55. package/rust/convex_fn/LICENSE +201 -0
  56. package/rust/convex_fn/build.rs +3 -0
  57. package/rust/convex_fn/src/args.rs +416 -0
  58. package/rust/convex_fn/src/crypto.rs +237 -0
  59. package/rust/convex_fn/src/ctx.rs +1044 -0
  60. package/rust/convex_fn/src/dns.rs +356 -0
  61. package/rust/convex_fn/src/error.rs +179 -0
  62. package/rust/convex_fn/src/executor.rs +577 -0
  63. package/rust/convex_fn/src/fanout.rs +106 -0
  64. package/rust/convex_fn/src/fs.rs +19 -0
  65. package/rust/convex_fn/src/function_ref.rs +219 -0
  66. package/rust/convex_fn/src/http.rs +441 -0
  67. package/rust/convex_fn/src/intl.rs +75 -0
  68. package/rust/convex_fn/src/lib.rs +242 -0
  69. package/rust/convex_fn/src/logging.rs +222 -0
  70. package/rust/convex_fn/src/native.rs +360 -0
  71. package/rust/convex_fn/src/net.rs +122 -0
  72. package/rust/convex_fn/src/sys.rs +178 -0
  73. package/rust/convex_fn/src/values.rs +121 -0
  74. package/rust/convex_fn/src/wire.rs +579 -0
  75. package/rust/convex_fn_macros/Cargo.toml +13 -0
  76. package/rust/convex_fn_macros/LICENSE +201 -0
  77. package/rust/convex_fn_macros/src/derive.rs +632 -0
  78. package/rust/convex_fn_macros/src/lib.rs +338 -0
  79. package/rust/convex_reqwest/Cargo.toml +69 -0
  80. package/rust/convex_reqwest/LICENSE +201 -0
  81. package/rust/convex_reqwest/src/blocking.rs +287 -0
  82. package/rust/convex_reqwest/src/body.rs +152 -0
  83. package/rust/convex_reqwest/src/client.rs +359 -0
  84. package/rust/convex_reqwest/src/config_types.rs +150 -0
  85. package/rust/convex_reqwest/src/error.rs +241 -0
  86. package/rust/convex_reqwest/src/into_url.rs +57 -0
  87. package/rust/convex_reqwest/src/lib.rs +42 -0
  88. package/rust/convex_reqwest/src/multipart.rs +140 -0
  89. package/rust/convex_reqwest/src/request.rs +358 -0
  90. package/rust/convex_reqwest/src/response.rs +310 -0
@@ -0,0 +1,219 @@
1
+ //! Typed function references, as generated into `_generated/api.rs` by
2
+ //! `npx convex dev` (the Rust counterpart of `FunctionReference` in JS).
3
+
4
+ use std::marker::PhantomData;
5
+
6
+ /// A reference to a Convex function taking `A` and returning `R`.
7
+ ///
8
+ /// ```ignore
9
+ /// ctx.run_query(api::messages::list, api::messages::ListArgs {}).await?;
10
+ /// ```
11
+ pub struct FunctionRef<A, R> {
12
+ path: &'static str,
13
+ _types: PhantomData<fn(A) -> R>,
14
+ }
15
+
16
+ impl<A, R> FunctionRef<A, R> {
17
+ /// `path` is a Convex function path such as `"messages:list"`.
18
+ pub const fn new(path: &'static str) -> Self {
19
+ FunctionRef {
20
+ path,
21
+ _types: PhantomData,
22
+ }
23
+ }
24
+
25
+ pub const fn path(&self) -> &'static str {
26
+ self.path
27
+ }
28
+ }
29
+
30
+ impl<A, R> Clone for FunctionRef<A, R> {
31
+ fn clone(&self) -> Self {
32
+ *self
33
+ }
34
+ }
35
+ impl<A, R> Copy for FunctionRef<A, R> {}
36
+
37
+ impl<A, R> std::fmt::Debug for FunctionRef<A, R> {
38
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39
+ write!(f, "FunctionRef({:?})", self.path)
40
+ }
41
+ }
42
+
43
+ /// How a function is addressed on the wire: the key of the syscall args
44
+ /// (`name`, `functionHandle` or `reference`) plus its string value.
45
+ pub trait FunctionAddress {
46
+ /// The path, `function://` handle or component reference path.
47
+ fn function_path(&self) -> &str;
48
+
49
+ /// The syscall args key the path goes under. Plain strings that start with
50
+ /// `function://` are handles, as in JS `getFunctionAddress`.
51
+ fn address_key(&self) -> &'static str {
52
+ "name"
53
+ }
54
+ }
55
+
56
+ fn str_key(s: &str) -> &'static str {
57
+ if s.starts_with(FUNCTION_HANDLE_PREFIX) {
58
+ "functionHandle"
59
+ } else {
60
+ "name"
61
+ }
62
+ }
63
+
64
+ const FUNCTION_HANDLE_PREFIX: &str = "function://";
65
+
66
+ /// A serialized function handle (`function://...`), as returned by
67
+ /// `createFunctionHandle` in JS. Works with `ctx.run_*` and the scheduler, and
68
+ /// is stable across deployments of the same function.
69
+ #[derive(Clone, Debug, PartialEq, Eq, Hash)]
70
+ pub struct FunctionHandle(String);
71
+
72
+ impl FunctionHandle {
73
+ pub fn new(handle: impl Into<String>) -> Self {
74
+ FunctionHandle(handle.into())
75
+ }
76
+
77
+ pub fn as_str(&self) -> &str {
78
+ &self.0
79
+ }
80
+ }
81
+
82
+ /// A reference to a function of a component, by the serialized reference path
83
+ /// that JS `getFunctionAddress` sends as `reference` (the value stored under
84
+ /// `Symbol.for("toReferencePath")` of `components.<name>.<fn>`).
85
+ #[derive(Clone, Debug, PartialEq, Eq, Hash)]
86
+ pub struct ComponentFunctionRef(String);
87
+
88
+ impl ComponentFunctionRef {
89
+ pub fn new(reference_path: impl Into<String>) -> Self {
90
+ ComponentFunctionRef(reference_path.into())
91
+ }
92
+
93
+ pub fn as_str(&self) -> &str {
94
+ &self.0
95
+ }
96
+ }
97
+
98
+ /// Something `ctx.run_query` / `run_mutation` / `run_action` can call: a
99
+ /// string path (untyped, as before), a typed [`FunctionRef`], a
100
+ /// [`FunctionHandle`] or a [`ComponentFunctionRef`].
101
+ pub trait FunctionTarget<A, R>: FunctionAddress {}
102
+
103
+ impl FunctionAddress for &str {
104
+ fn function_path(&self) -> &str {
105
+ self
106
+ }
107
+ fn address_key(&self) -> &'static str {
108
+ str_key(self)
109
+ }
110
+ }
111
+ impl<A, R> FunctionTarget<A, R> for &str {}
112
+
113
+ impl FunctionAddress for String {
114
+ fn function_path(&self) -> &str {
115
+ self
116
+ }
117
+ fn address_key(&self) -> &'static str {
118
+ str_key(self)
119
+ }
120
+ }
121
+ impl<A, R> FunctionTarget<A, R> for String {}
122
+
123
+ impl FunctionAddress for &String {
124
+ fn function_path(&self) -> &str {
125
+ self
126
+ }
127
+ fn address_key(&self) -> &'static str {
128
+ str_key(self)
129
+ }
130
+ }
131
+ impl<A, R> FunctionTarget<A, R> for &String {}
132
+
133
+ impl<A, R> FunctionAddress for FunctionRef<A, R> {
134
+ fn function_path(&self) -> &str {
135
+ self.path
136
+ }
137
+ }
138
+ impl<A, R> FunctionTarget<A, R> for FunctionRef<A, R> {}
139
+
140
+ impl<A, R> FunctionAddress for &FunctionRef<A, R> {
141
+ fn function_path(&self) -> &str {
142
+ self.path
143
+ }
144
+ }
145
+ impl<A, R> FunctionTarget<A, R> for &FunctionRef<A, R> {}
146
+
147
+ impl FunctionAddress for FunctionHandle {
148
+ fn function_path(&self) -> &str {
149
+ &self.0
150
+ }
151
+ fn address_key(&self) -> &'static str {
152
+ "functionHandle"
153
+ }
154
+ }
155
+ impl<A, R> FunctionTarget<A, R> for FunctionHandle {}
156
+
157
+ impl FunctionAddress for &FunctionHandle {
158
+ fn function_path(&self) -> &str {
159
+ &self.0
160
+ }
161
+ fn address_key(&self) -> &'static str {
162
+ "functionHandle"
163
+ }
164
+ }
165
+ impl<A, R> FunctionTarget<A, R> for &FunctionHandle {}
166
+
167
+ impl FunctionAddress for ComponentFunctionRef {
168
+ fn function_path(&self) -> &str {
169
+ &self.0
170
+ }
171
+ fn address_key(&self) -> &'static str {
172
+ "reference"
173
+ }
174
+ }
175
+ impl<A, R> FunctionTarget<A, R> for ComponentFunctionRef {}
176
+
177
+ impl FunctionAddress for &ComponentFunctionRef {
178
+ fn function_path(&self) -> &str {
179
+ &self.0
180
+ }
181
+ fn address_key(&self) -> &'static str {
182
+ "reference"
183
+ }
184
+ }
185
+ impl<A, R> FunctionTarget<A, R> for &ComponentFunctionRef {}
186
+
187
+ /// The `{name | functionHandle | reference: path}` part of a syscall's args.
188
+ pub(crate) fn address_json(target: &impl FunctionAddress) -> serde_json::Value {
189
+ let mut m = serde_json::Map::new();
190
+ m.insert(
191
+ target.address_key().to_string(),
192
+ serde_json::Value::String(target.function_path().to_string()),
193
+ );
194
+ serde_json::Value::Object(m)
195
+ }
196
+
197
+ #[cfg(test)]
198
+ mod tests {
199
+ use super::*;
200
+
201
+ #[test]
202
+ fn addresses() {
203
+ let r: FunctionRef<(), ()> = FunctionRef::new("a:b");
204
+ assert_eq!(address_json(&r), serde_json::json!({"name": "a:b"}));
205
+ assert_eq!(address_json(&"a:b"), serde_json::json!({"name": "a:b"}));
206
+ assert_eq!(
207
+ address_json(&"function://abc"),
208
+ serde_json::json!({"functionHandle": "function://abc"})
209
+ );
210
+ assert_eq!(
211
+ address_json(&FunctionHandle::new("function://x")),
212
+ serde_json::json!({"functionHandle": "function://x"})
213
+ );
214
+ assert_eq!(
215
+ address_json(&ComponentFunctionRef::new("_reference/childComponent/c/f")),
216
+ serde_json::json!({"reference": "_reference/childComponent/c/f"})
217
+ );
218
+ }
219
+ }
@@ -0,0 +1,441 @@
1
+ //! HTTP actions: `#[http_action]` handlers take an [`HttpRequest`] and return an
2
+ //! [`HttpResponse`] (contracts/V8HOST.md "HTTP actions").
3
+ //!
4
+ //! The request head arrives as the call's arguments; its body is a host stream read
5
+ //! chunk by chunk ([`HttpRequest::chunk`]) or whole ([`HttpRequest::bytes`]). A response
6
+ //! is either a buffered body or a streaming one: [`HttpResponse::streaming`] runs a body
7
+ //! producer after the head was sent, writing chunks with [`BodyWriter::write`] while the
8
+ //! client receives them (server-sent events, relayed LLM tokens, ...).
9
+
10
+ use std::{cell::RefCell, future::Future, pin::Pin, rc::Rc};
11
+
12
+ use base64::Engine;
13
+ use serde::{de::DeserializeOwned, Deserialize, Serialize};
14
+ use serde_json::{json, Value};
15
+
16
+ use crate::{
17
+ error::Error,
18
+ executor::{abandon_stream, submit, NextChunk, StreamFuture},
19
+ wire::{self, B64},
20
+ Ctx, Result,
21
+ };
22
+
23
+ /// Convex limits an HTTP action's response body to 20 MiB.
24
+ const MAX_BODY_BYTES: usize = 20 * 1024 * 1024;
25
+
26
+ /// Largest piece sent to the host per `BodyWriter::write` task.
27
+ const WRITE_PIECE_BYTES: usize = 1024 * 1024;
28
+
29
+ #[derive(Deserialize)]
30
+ struct RequestWire {
31
+ method: String,
32
+ url: String,
33
+ #[serde(default)]
34
+ headers: Vec<(String, String)>,
35
+ #[serde(default, rename = "bodyStreamId")]
36
+ body_stream_id: Option<String>,
37
+ /// Whole request body, base64 (hosts without request body streams: the `rustHttpAction` bridge
38
+ /// of the native / wasmtime hosts). Sent together with `buffered`.
39
+ #[serde(default)]
40
+ body: Option<String>,
41
+ /// The host cannot stream a response: a streaming body is collected and returned whole.
42
+ #[serde(default)]
43
+ buffered: bool,
44
+ }
45
+
46
+ /// An incoming HTTP request (the `Request` of a Convex `httpAction`).
47
+ #[derive(Debug, Deserialize)]
48
+ #[serde(from = "RequestWire")]
49
+ pub struct HttpRequest {
50
+ /// HTTP method, upper case (`"GET"`, `"POST"`, ...).
51
+ pub method: String,
52
+ /// The full request URL, e.g. `https://happy-animal-123.convex.site/hook?x=1`.
53
+ pub url: String,
54
+ /// Request headers; names are lower case, as `Headers` yields them.
55
+ pub headers: Vec<(String, String)>,
56
+ stream_id: Option<String>,
57
+ /// The not yet read inline body (`RequestWire::body`).
58
+ inline: Option<Vec<u8>>,
59
+ buffered: bool,
60
+ }
61
+
62
+ impl From<RequestWire> for HttpRequest {
63
+ fn from(w: RequestWire) -> Self {
64
+ // An undecodable inline body reads as an empty one; the bridge always sends valid base64.
65
+ let inline = w.body.map(|b| B64.decode(b).unwrap_or_default());
66
+ HttpRequest {
67
+ method: w.method,
68
+ url: w.url,
69
+ headers: w.headers,
70
+ stream_id: w.body_stream_id,
71
+ inline,
72
+ buffered: w.buffered,
73
+ }
74
+ }
75
+ }
76
+
77
+ impl HttpRequest {
78
+ /// Builds a request value (for tests); `body_stream_id` names a host stream.
79
+ #[doc(hidden)]
80
+ pub fn from_parts(
81
+ method: &str,
82
+ url: &str,
83
+ headers: Vec<(String, String)>,
84
+ body_stream_id: Option<String>,
85
+ ) -> Self {
86
+ RequestWire {
87
+ method: method.to_string(),
88
+ url: url.to_string(),
89
+ headers,
90
+ body_stream_id,
91
+ body: None,
92
+ buffered: false,
93
+ }
94
+ .into()
95
+ }
96
+
97
+ /// The first header called `name` (case-insensitive).
98
+ pub fn header(&self, name: &str) -> Option<&str> {
99
+ self.headers
100
+ .iter()
101
+ .find(|(k, _)| k.eq_ignore_ascii_case(name))
102
+ .map(|(_, v)| v.as_str())
103
+ }
104
+
105
+ /// The URL path (without query string and fragment).
106
+ pub fn path(&self) -> &str {
107
+ let rest = self.url.split_once("://").map_or(&*self.url, |(_, r)| r);
108
+ let rest = rest.find('/').map_or("/", |i| &rest[i..]);
109
+ rest.split(['?', '#']).next().unwrap_or("/")
110
+ }
111
+
112
+ /// The raw query string (without the `?`), if any.
113
+ pub fn query(&self) -> Option<&str> {
114
+ let (_, q) = self.url.split_once('?')?;
115
+ Some(q.split('#').next().unwrap_or(q))
116
+ }
117
+
118
+ /// Does the request have a body stream (false for `GET`/`HEAD`)?
119
+ pub fn has_body(&self) -> bool {
120
+ self.stream_id.is_some() || self.inline.is_some()
121
+ }
122
+
123
+ /// The next body chunk, `Ok(None)` once the body ended or if there is none.
124
+ /// After `None` or `Err`, further calls return `Ok(None)`.
125
+ pub async fn chunk(&mut self) -> Result<Option<Vec<u8>>> {
126
+ if let Some(body) = self.inline.take() {
127
+ return Ok((!body.is_empty()).then_some(body));
128
+ }
129
+ let Some(id) = self.stream_id.as_deref() else {
130
+ return Ok(None);
131
+ };
132
+ let r = NextChunk { stream_id: id }.await;
133
+ if !matches!(r, Ok(Some(_))) {
134
+ self.stream_id = None;
135
+ }
136
+ r
137
+ }
138
+
139
+ /// Reads the remaining body into one buffer.
140
+ pub async fn bytes(&mut self) -> Result<Vec<u8>> {
141
+ if let Some(body) = self.inline.take() {
142
+ return Ok(body);
143
+ }
144
+ match self.stream_id.take() {
145
+ Some(stream_id) => StreamFuture::new(stream_id).await,
146
+ None => Ok(vec![]),
147
+ }
148
+ }
149
+
150
+ /// Reads the remaining body as UTF-8 text.
151
+ pub async fn text(&mut self) -> Result<String> {
152
+ Ok(String::from_utf8(self.bytes().await?)?)
153
+ }
154
+
155
+ /// Reads the remaining body and parses it as JSON.
156
+ pub async fn json<T: DeserializeOwned>(&mut self) -> Result<T> {
157
+ Ok(serde_json::from_slice(&self.bytes().await?)?)
158
+ }
159
+ }
160
+
161
+ impl Drop for HttpRequest {
162
+ fn drop(&mut self) {
163
+ if let Some(id) = self.stream_id.take() {
164
+ abandon_stream(&id);
165
+ }
166
+ }
167
+ }
168
+
169
+ type BodyFuture = Pin<Box<dyn Future<Output = Result<()>>>>;
170
+ type BodyFn = Box<dyn FnOnce(BodyWriter) -> BodyFuture>;
171
+
172
+ enum Body {
173
+ Bytes(Vec<u8>),
174
+ Stream(BodyFn),
175
+ }
176
+
177
+ /// The response of an HTTP action: status, headers and a buffered or streaming body.
178
+ pub struct HttpResponse {
179
+ /// Status code, 200..=599.
180
+ pub status: u16,
181
+ /// Response headers.
182
+ pub headers: Vec<(String, String)>,
183
+ body: Body,
184
+ }
185
+
186
+ impl std::fmt::Debug for HttpResponse {
187
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
188
+ let mut d = f.debug_struct("HttpResponse");
189
+ d.field("status", &self.status).field("headers", &self.headers);
190
+ match &self.body {
191
+ Body::Bytes(b) => d.field("body", &format_args!("{} bytes", b.len())),
192
+ Body::Stream(_) => d.field("body", &format_args!("<stream>")),
193
+ };
194
+ d.finish()
195
+ }
196
+ }
197
+
198
+ impl HttpResponse {
199
+ /// An empty response with `status`.
200
+ pub fn new(status: u16) -> Self {
201
+ HttpResponse {
202
+ status,
203
+ headers: vec![],
204
+ body: Body::Bytes(vec![]),
205
+ }
206
+ }
207
+
208
+ /// An empty `200 OK` response.
209
+ pub fn ok() -> Self {
210
+ Self::new(200)
211
+ }
212
+
213
+ /// A `200` response with a `text/plain; charset=utf-8` body.
214
+ pub fn text(body: impl Into<String>) -> Self {
215
+ Self::ok()
216
+ .header("content-type", "text/plain; charset=utf-8")
217
+ .body(body.into().into_bytes())
218
+ }
219
+
220
+ /// A `200` response with `value` as an `application/json` body.
221
+ pub fn json<T: Serialize>(value: &T) -> Result<Self> {
222
+ Ok(Self::ok()
223
+ .header("content-type", "application/json")
224
+ .body(serde_json::to_vec(value)?))
225
+ }
226
+
227
+ /// Sets the status code.
228
+ pub fn status(mut self, status: u16) -> Self {
229
+ self.status = status;
230
+ self
231
+ }
232
+
233
+ /// Adds a header (repeated names are sent as repeated headers).
234
+ pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
235
+ self.headers.push((name.into(), value.into()));
236
+ self
237
+ }
238
+
239
+ /// Sets a buffered body.
240
+ pub fn body(mut self, body: impl Into<Vec<u8>>) -> Self {
241
+ self.body = Body::Bytes(body.into());
242
+ self
243
+ }
244
+
245
+ /// Makes the body a stream: after the head is sent, `produce` runs with a
246
+ /// [`BodyWriter`]; the body ends when the future finishes (an `Err` aborts the stream).
247
+ ///
248
+ /// ```ignore
249
+ /// HttpResponse::ok()
250
+ /// .header("content-type", "text/event-stream")
251
+ /// .streaming(|w| async move {
252
+ /// for i in 0..3 { w.write_str(&format!("data: {i}\n\n")).await?; }
253
+ /// Ok(())
254
+ /// })
255
+ /// ```
256
+ pub fn streaming<F, Fut>(mut self, produce: F) -> Self
257
+ where
258
+ F: FnOnce(BodyWriter) -> Fut + 'static,
259
+ Fut: Future<Output = Result<()>> + 'static,
260
+ {
261
+ self.body = Body::Stream(Box::new(move |w| Box::pin(produce(w))));
262
+ self
263
+ }
264
+
265
+ /// The buffered body, `None` for a streaming response.
266
+ pub fn body_bytes(&self) -> Option<&[u8]> {
267
+ match &self.body {
268
+ Body::Bytes(b) => Some(b),
269
+ Body::Stream(_) => None,
270
+ }
271
+ }
272
+ }
273
+
274
+ /// Writes the body of a streaming [`HttpResponse`]. `write` resolves once the host
275
+ /// accepted the bytes (it waits while the client is slower than the producer) and fails
276
+ /// once the client went away. On a host that cannot stream (`buffered`), the writes are
277
+ /// collected and sent as one body when the producer finishes.
278
+ #[derive(Debug)]
279
+ pub struct BodyWriter {
280
+ collected: Option<Rc<RefCell<Vec<u8>>>>,
281
+ }
282
+
283
+ impl BodyWriter {
284
+ /// Sends `data` to the client.
285
+ pub async fn write(&self, data: impl AsRef<[u8]>) -> Result<()> {
286
+ if let Some(buf) = &self.collected {
287
+ let mut buf = buf.borrow_mut();
288
+ if buf.len() + data.as_ref().len() > MAX_BODY_BYTES {
289
+ return Err(Error::msg(format!(
290
+ "HTTP response exceeds the {MAX_BODY_BYTES} byte limit"
291
+ )));
292
+ }
293
+ buf.extend_from_slice(data.as_ref());
294
+ return Ok(());
295
+ }
296
+ for piece in data.as_ref().chunks(WRITE_PIECE_BYTES) {
297
+ submit(&wire::http_write_request(piece)).await?;
298
+ }
299
+ Ok(())
300
+ }
301
+
302
+ /// Sends `text` to the client.
303
+ pub async fn write_str(&self, text: &str) -> Result<()> {
304
+ self.write(text.as_bytes()).await
305
+ }
306
+ }
307
+
308
+ /// Sends the response to the host and returns the `ok` value of the result envelope:
309
+ /// the buffered response (`{status, headers, body}`), or `{"streamed": true}` after a
310
+ /// streaming body was written.
311
+ pub(crate) async fn respond(resp: HttpResponse, buffered: bool) -> Result<Value> {
312
+ if !(200..=599).contains(&resp.status) {
313
+ return Err(Error::msg(format!(
314
+ "invalid HTTP status {} (expected 200..=599)",
315
+ resp.status
316
+ )));
317
+ }
318
+ let envelope = |status: u16, headers: &[(String, String)], b: &[u8]| {
319
+ json!({
320
+ "status": status,
321
+ "headers": headers,
322
+ "body": (!b.is_empty()).then(|| B64.encode(b)),
323
+ })
324
+ };
325
+ match resp.body {
326
+ Body::Bytes(b) => Ok(envelope(resp.status, &resp.headers, &b)),
327
+ Body::Stream(produce) if buffered => {
328
+ let buf = Rc::new(RefCell::new(Vec::new()));
329
+ produce(BodyWriter { collected: Some(buf.clone()) }).await?;
330
+ let body = buf.take();
331
+ Ok(envelope(resp.status, &resp.headers, &body))
332
+ }
333
+ Body::Stream(produce) => {
334
+ submit(&wire::http_start_request(resp.status, &resp.headers)).await?;
335
+ produce(BodyWriter { collected: None }).await?;
336
+ submit(&wire::http_end_request()).await?;
337
+ Ok(json!({ "streamed": true }))
338
+ }
339
+ }
340
+ }
341
+
342
+ /// Adapter used by `#[http_action]`: runs `f` and sends its response.
343
+ #[doc(hidden)]
344
+ pub fn adapter<F, Fut>(
345
+ f: F,
346
+ ) -> impl FnOnce(Ctx, HttpRequest) -> Pin<Box<dyn Future<Output = Result<Value>>>>
347
+ where
348
+ F: FnOnce(Ctx, HttpRequest) -> Fut + 'static,
349
+ Fut: Future<Output = Result<HttpResponse>> + 'static,
350
+ {
351
+ move |ctx, req| {
352
+ let buffered = req.buffered;
353
+ Box::pin(async move { respond(f(ctx, req).await?, buffered).await })
354
+ }
355
+ }
356
+
357
+ #[cfg(test)]
358
+ mod tests {
359
+ use super::*;
360
+
361
+ fn req(url: &str) -> HttpRequest {
362
+ HttpRequest::from_parts("GET", url, vec![("Content-Type".into(), "text/x".into())], None)
363
+ }
364
+
365
+ #[test]
366
+ fn path_and_query() {
367
+ assert_eq!(req("https://a.convex.site/hook?x=1#f").path(), "/hook");
368
+ assert_eq!(req("https://a.convex.site/hook?x=1#f").query(), Some("x=1"));
369
+ assert_eq!(req("https://a.convex.site").path(), "/");
370
+ assert_eq!(req("https://a.convex.site/a/b").query(), None);
371
+ }
372
+
373
+ #[test]
374
+ fn header_is_case_insensitive() {
375
+ assert_eq!(req("https://a/").header("content-TYPE"), Some("text/x"));
376
+ assert_eq!(req("https://a/").header("x"), None);
377
+ }
378
+
379
+ #[test]
380
+ fn request_deserializes_from_the_host_head() {
381
+ let r: HttpRequest = serde_json::from_value(json!({
382
+ "method": "POST", "url": "https://a/b", "headers": [["x", "y"]], "bodyStreamId": "s0"
383
+ }))
384
+ .unwrap();
385
+ assert_eq!((r.method.as_str(), r.header("x"), r.has_body()), ("POST", Some("y"), true));
386
+ let r: HttpRequest =
387
+ serde_json::from_value(json!({ "method": "GET", "url": "https://a/", "bodyStreamId": null }))
388
+ .unwrap();
389
+ assert!(!r.has_body() && r.headers.is_empty());
390
+ }
391
+
392
+ #[test]
393
+ fn inline_body_is_read_once_and_marks_a_buffered_host() {
394
+ let head = json!({
395
+ "method": "POST", "url": "https://a/b", "headers": [], "body": B64.encode(b"hello"), "buffered": true
396
+ });
397
+ let mut r: HttpRequest = serde_json::from_value(head.clone()).unwrap();
398
+ assert!(r.has_body() && r.buffered);
399
+ assert_eq!(crate::executor::block_on(r.bytes()).unwrap(), b"hello");
400
+ assert_eq!(crate::executor::block_on(r.bytes()).unwrap(), b"");
401
+ let mut r: HttpRequest = serde_json::from_value(head).unwrap();
402
+ assert_eq!(crate::executor::block_on(r.chunk()).unwrap(), Some(b"hello".to_vec()));
403
+ assert_eq!(crate::executor::block_on(r.chunk()).unwrap(), None);
404
+ let mut r: HttpRequest =
405
+ serde_json::from_value(json!({ "method": "POST", "url": "https://a/", "body": null, "buffered": true })).unwrap();
406
+ assert!(!r.has_body() && crate::executor::block_on(r.text()).unwrap().is_empty());
407
+ }
408
+
409
+ #[test]
410
+ fn buffered_host_collects_a_streaming_body() {
411
+ let resp = HttpResponse::ok().header("content-type", "text/plain").streaming(|w| async move {
412
+ w.write_str("ab").await?;
413
+ w.write(b"cd").await?;
414
+ Ok(())
415
+ });
416
+ let v = crate::executor::block_on(respond(resp, true)).unwrap();
417
+ assert_eq!(v["status"], 200);
418
+ assert_eq!(v["headers"], json!([["content-type", "text/plain"]]));
419
+ assert_eq!(v["body"], B64.encode(b"abcd"));
420
+ // A failing producer fails the call (the buffered host has no truncated bodies).
421
+ let failing = HttpResponse::ok().streaming(|w| async move {
422
+ w.write_str("x").await?;
423
+ Err(Error::msg("boom"))
424
+ });
425
+ assert!(crate::executor::block_on(respond(failing, true)).is_err());
426
+ // An empty collected body is `null`.
427
+ let empty = HttpResponse::ok().streaming(|_| async { Ok(()) });
428
+ assert!(crate::executor::block_on(respond(empty, true)).unwrap()["body"].is_null());
429
+ }
430
+
431
+ #[test]
432
+ fn builders() {
433
+ let r = HttpResponse::text("hi").status(201).header("x-a", "b");
434
+ assert_eq!(r.status, 201);
435
+ assert_eq!(r.body_bytes(), Some(&b"hi"[..]));
436
+ assert_eq!(r.headers[0].0, "content-type");
437
+ let j = HttpResponse::json(&json!({"a": 1})).unwrap();
438
+ assert_eq!(j.body_bytes(), Some(&br#"{"a":1}"#[..]));
439
+ assert!(HttpResponse::ok().streaming(|_| async { Ok(()) }).body_bytes().is_none());
440
+ }
441
+ }