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.
- package/LICENSE +201 -0
- package/README.md +137 -2
- package/dist/cli/bin.d.ts +2 -0
- package/dist/cli/bin.js +200 -0
- package/dist/cli/build.d.ts +146 -0
- package/dist/cli/build.js +902 -0
- package/dist/cli/demangle.d.ts +7 -0
- package/dist/cli/demangle.js +328 -0
- package/dist/cli/diagnostics.d.ts +34 -0
- package/dist/cli/diagnostics.js +296 -0
- package/dist/cli/generate.d.ts +29 -0
- package/dist/cli/generate.js +82 -0
- package/dist/cli/manifest.d.ts +24 -0
- package/dist/cli/manifest.js +75 -0
- package/dist/cli/rustApi.d.ts +78 -0
- package/dist/cli/rustApi.js +609 -0
- package/dist/cli/scaffold.d.ts +22 -0
- package/dist/cli/scaffold.js +114 -0
- package/dist/cli/shim.d.ts +30 -0
- package/dist/cli/shim.js +140 -0
- package/dist/cli/size.d.ts +17 -0
- package/dist/cli/size.js +73 -0
- package/dist/cli/split.d.ts +20 -0
- package/dist/cli/split.js +140 -0
- package/dist/cli/symbols.d.ts +30 -0
- package/dist/cli/symbols.js +133 -0
- package/dist/cli/toolchain.d.ts +53 -0
- package/dist/cli/toolchain.js +167 -0
- package/dist/cli/validators.d.ts +6 -0
- package/dist/cli/validators.js +66 -0
- package/dist/cli/wasisdk.d.ts +82 -0
- package/dist/cli/wasisdk.js +220 -0
- package/dist/cli/wasmHints.d.ts +29 -0
- package/dist/cli/wasmHints.js +172 -0
- package/dist/runtime/base64.d.ts +4 -0
- package/dist/runtime/base64.js +77 -0
- package/dist/runtime/crypto.d.ts +7 -0
- package/dist/runtime/crypto.js +421 -0
- package/dist/runtime/fastjson.d.ts +3 -0
- package/dist/runtime/fastjson.js +170 -0
- package/dist/runtime/host.d.ts +73 -0
- package/dist/runtime/host.js +1230 -0
- package/dist/runtime/http.d.ts +40 -0
- package/dist/runtime/http.js +157 -0
- package/dist/runtime/index.d.ts +2 -0
- package/dist/runtime/index.js +1 -0
- package/dist/runtime/intl.d.ts +5 -0
- package/dist/runtime/intl.js +69 -0
- package/dist/runtime/tmpfs.d.ts +129 -0
- package/dist/runtime/tmpfs.js +624 -0
- package/dist/runtime/wasi.d.ts +33 -0
- package/dist/runtime/wasi.js +216 -0
- package/package.json +57 -4
- package/rust/convex_fn/Cargo.toml +32 -0
- package/rust/convex_fn/LICENSE +201 -0
- package/rust/convex_fn/build.rs +3 -0
- package/rust/convex_fn/src/args.rs +416 -0
- package/rust/convex_fn/src/crypto.rs +237 -0
- package/rust/convex_fn/src/ctx.rs +1044 -0
- package/rust/convex_fn/src/dns.rs +356 -0
- package/rust/convex_fn/src/error.rs +179 -0
- package/rust/convex_fn/src/executor.rs +577 -0
- package/rust/convex_fn/src/fanout.rs +106 -0
- package/rust/convex_fn/src/fs.rs +19 -0
- package/rust/convex_fn/src/function_ref.rs +219 -0
- package/rust/convex_fn/src/http.rs +441 -0
- package/rust/convex_fn/src/intl.rs +75 -0
- package/rust/convex_fn/src/lib.rs +242 -0
- package/rust/convex_fn/src/logging.rs +222 -0
- package/rust/convex_fn/src/native.rs +360 -0
- package/rust/convex_fn/src/net.rs +122 -0
- package/rust/convex_fn/src/sys.rs +178 -0
- package/rust/convex_fn/src/values.rs +121 -0
- package/rust/convex_fn/src/wire.rs +579 -0
- package/rust/convex_fn_macros/Cargo.toml +13 -0
- package/rust/convex_fn_macros/LICENSE +201 -0
- package/rust/convex_fn_macros/src/derive.rs +632 -0
- package/rust/convex_fn_macros/src/lib.rs +338 -0
- package/rust/convex_reqwest/Cargo.toml +69 -0
- package/rust/convex_reqwest/LICENSE +201 -0
- package/rust/convex_reqwest/src/blocking.rs +287 -0
- package/rust/convex_reqwest/src/body.rs +152 -0
- package/rust/convex_reqwest/src/client.rs +359 -0
- package/rust/convex_reqwest/src/config_types.rs +150 -0
- package/rust/convex_reqwest/src/error.rs +241 -0
- package/rust/convex_reqwest/src/into_url.rs +57 -0
- package/rust/convex_reqwest/src/lib.rs +42 -0
- package/rust/convex_reqwest/src/multipart.rs +140 -0
- package/rust/convex_reqwest/src/request.rs +358 -0
- package/rust/convex_reqwest/src/response.rs +310 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
//! Convex values that plain JSON can't carry, in the Convex JSON export
|
|
2
|
+
//! format: `{"$integer": …}`, `{"$bytes": …}`, `{"$float": …}`.
|
|
3
|
+
//! Everything else (strings, `f64`, bools, null, arrays, objects) is plain serde.
|
|
4
|
+
|
|
5
|
+
use base64::Engine;
|
|
6
|
+
use serde::{de::Error as _, Deserialize, Deserializer, Serialize, Serializer};
|
|
7
|
+
use serde_json::Value;
|
|
8
|
+
|
|
9
|
+
const B64: base64::engine::GeneralPurpose = base64::engine::general_purpose::STANDARD;
|
|
10
|
+
|
|
11
|
+
fn tagged<S: Serializer>(s: S, tag: &str, bytes: &[u8]) -> Result<S::Ok, S::Error> {
|
|
12
|
+
use serde::ser::SerializeMap;
|
|
13
|
+
let mut m = s.serialize_map(Some(1))?;
|
|
14
|
+
m.serialize_entry(tag, &B64.encode(bytes))?;
|
|
15
|
+
m.end()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
fn untag<'de, D: Deserializer<'de>>(d: D, tag: &str) -> Result<Vec<u8>, D::Error> {
|
|
19
|
+
let v = Value::deserialize(d)?;
|
|
20
|
+
let s = v
|
|
21
|
+
.get(tag)
|
|
22
|
+
.and_then(Value::as_str)
|
|
23
|
+
.ok_or_else(|| D::Error::custom(format!("expected {{\"{tag}\": <base64>}}")))?;
|
|
24
|
+
B64.decode(s).map_err(D::Error::custom)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/// Convex `int64` (`bigint` in JS).
|
|
28
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
|
|
29
|
+
pub struct Int64(pub i64);
|
|
30
|
+
|
|
31
|
+
impl Serialize for Int64 {
|
|
32
|
+
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
|
|
33
|
+
tagged(s, "$integer", &self.0.to_le_bytes())
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
impl<'de> Deserialize<'de> for Int64 {
|
|
38
|
+
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
|
|
39
|
+
let b: [u8; 8] = untag(d, "$integer")?
|
|
40
|
+
.try_into()
|
|
41
|
+
.map_err(|_| D::Error::custom("$integer must be 8 bytes"))?;
|
|
42
|
+
Ok(Int64(i64::from_le_bytes(b)))
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// Convex `bytes` (`ArrayBuffer` in JS).
|
|
47
|
+
#[derive(Clone, Debug, PartialEq, Eq, Default)]
|
|
48
|
+
pub struct Bytes(pub Vec<u8>);
|
|
49
|
+
|
|
50
|
+
impl Serialize for Bytes {
|
|
51
|
+
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
|
|
52
|
+
tagged(s, "$bytes", &self.0)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
impl<'de> Deserialize<'de> for Bytes {
|
|
57
|
+
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
|
|
58
|
+
Ok(Bytes(untag(d, "$bytes")?))
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// Convex `float64` that may be NaN, ±Infinity or -0 (plain `f64` covers the rest).
|
|
63
|
+
#[derive(Clone, Copy, Debug, Default)]
|
|
64
|
+
pub struct Float64(pub f64);
|
|
65
|
+
|
|
66
|
+
impl PartialEq for Float64 {
|
|
67
|
+
fn eq(&self, other: &Self) -> bool {
|
|
68
|
+
self.0.to_bits() == other.0.to_bits()
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
impl Serialize for Float64 {
|
|
73
|
+
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
|
|
74
|
+
let f = self.0;
|
|
75
|
+
if f.is_finite() && !(f == 0.0 && f.is_sign_negative()) {
|
|
76
|
+
s.serialize_f64(f)
|
|
77
|
+
} else {
|
|
78
|
+
tagged(s, "$float", &f.to_le_bytes())
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
impl<'de> Deserialize<'de> for Float64 {
|
|
84
|
+
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
|
|
85
|
+
let v = Value::deserialize(d)?;
|
|
86
|
+
if let Some(f) = v.as_f64() {
|
|
87
|
+
return Ok(Float64(f));
|
|
88
|
+
}
|
|
89
|
+
let s = v
|
|
90
|
+
.get("$float")
|
|
91
|
+
.and_then(Value::as_str)
|
|
92
|
+
.ok_or_else(|| D::Error::custom("expected a number or {\"$float\": <base64>}"))?;
|
|
93
|
+
let b: [u8; 8] = B64
|
|
94
|
+
.decode(s)
|
|
95
|
+
.map_err(D::Error::custom)?
|
|
96
|
+
.try_into()
|
|
97
|
+
.map_err(|_| D::Error::custom("$float must be 8 bytes"))?;
|
|
98
|
+
Ok(Float64(f64::from_le_bytes(b)))
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
#[cfg(test)]
|
|
103
|
+
mod tests {
|
|
104
|
+
use super::*;
|
|
105
|
+
|
|
106
|
+
#[test]
|
|
107
|
+
fn roundtrips() {
|
|
108
|
+
for f in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY, -0.0, 1.5] {
|
|
109
|
+
let j = serde_json::to_string(&Float64(f)).unwrap();
|
|
110
|
+
assert_eq!(serde_json::from_str::<Float64>(&j).unwrap(), Float64(f));
|
|
111
|
+
}
|
|
112
|
+
let j = serde_json::to_string(&Int64(-2)).unwrap();
|
|
113
|
+
assert_eq!(j, r#"{"$integer":"/v////////8="}"#);
|
|
114
|
+
assert_eq!(serde_json::from_str::<Int64>(&j).unwrap(), Int64(-2));
|
|
115
|
+
let j = serde_json::to_string(&Bytes(vec![1, 2, 3])).unwrap();
|
|
116
|
+
assert_eq!(
|
|
117
|
+
serde_json::from_str::<Bytes>(&j).unwrap(),
|
|
118
|
+
Bytes(vec![1, 2, 3])
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
//! The JSON wire format of contracts/ABI.md in one place: task requests the
|
|
2
|
+
//! guest submits, events the host delivers, and log levels. Used by `Ctx` and
|
|
3
|
+
//! the executor, and (through the hidden `__wire` export) by `convex_fn_test`
|
|
4
|
+
//! so the mock host does not re-spell the keys.
|
|
5
|
+
|
|
6
|
+
use base64::Engine;
|
|
7
|
+
use serde_json::{json, Value};
|
|
8
|
+
|
|
9
|
+
pub(crate) const B64: base64::engine::GeneralPurpose = base64::engine::general_purpose::STANDARD;
|
|
10
|
+
|
|
11
|
+
/// Host `log` import levels.
|
|
12
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
13
|
+
#[repr(i32)]
|
|
14
|
+
pub enum LogLevel {
|
|
15
|
+
Debug = 0,
|
|
16
|
+
Info = 1,
|
|
17
|
+
Warn = 2,
|
|
18
|
+
Error = 3,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
impl LogLevel {
|
|
22
|
+
pub const fn as_i32(self) -> i32 {
|
|
23
|
+
self as i32
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
pub fn from_i32(level: i32) -> Option<Self> {
|
|
27
|
+
match level {
|
|
28
|
+
0 => Some(Self::Debug),
|
|
29
|
+
1 => Some(Self::Info),
|
|
30
|
+
2 => Some(Self::Warn),
|
|
31
|
+
3 => Some(Self::Error),
|
|
32
|
+
_ => None,
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ---- task requests (guest -> host) ----
|
|
38
|
+
|
|
39
|
+
pub fn syscall_request(name: &str, args: Value) -> Value {
|
|
40
|
+
json!({ "type": "syscall", "name": name, "args": args })
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// How a request body travels (contracts/ABI.md "Task requests").
|
|
44
|
+
#[derive(Clone, Copy, Debug)]
|
|
45
|
+
pub enum BodyRef<'a> {
|
|
46
|
+
None,
|
|
47
|
+
/// Base64 string inside the request JSON.
|
|
48
|
+
Inline(&'a [u8]),
|
|
49
|
+
/// `{"ptr","len"}` into guest memory (needs `net/caps` `bodyRef`).
|
|
50
|
+
Ref(&'a [u8]),
|
|
51
|
+
/// Fed by the guest through `net/body_write` (needs `requestStream`).
|
|
52
|
+
Stream,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
impl BodyRef<'_> {
|
|
56
|
+
fn to_json(self) -> Value {
|
|
57
|
+
match self {
|
|
58
|
+
BodyRef::None => Value::Null,
|
|
59
|
+
BodyRef::Inline(b) => Value::String(B64.encode(b)),
|
|
60
|
+
BodyRef::Ref(b) => crate::net::bytes_ref(b),
|
|
61
|
+
BodyRef::Stream => json!({ "stream": true }),
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
pub fn fetch_request(
|
|
67
|
+
url: &str,
|
|
68
|
+
method: &str,
|
|
69
|
+
headers: &[(String, String)],
|
|
70
|
+
body: Option<&[u8]>,
|
|
71
|
+
) -> Value {
|
|
72
|
+
fetch_request_with(
|
|
73
|
+
url,
|
|
74
|
+
method,
|
|
75
|
+
headers,
|
|
76
|
+
body.map_or(BodyRef::None, BodyRef::Inline),
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
pub fn fetch_request_with(
|
|
81
|
+
url: &str,
|
|
82
|
+
method: &str,
|
|
83
|
+
headers: &[(String, String)],
|
|
84
|
+
body: BodyRef<'_>,
|
|
85
|
+
) -> Value {
|
|
86
|
+
json!({
|
|
87
|
+
"type": "fetch",
|
|
88
|
+
"url": url,
|
|
89
|
+
"method": method,
|
|
90
|
+
"headers": headers,
|
|
91
|
+
"body": body.to_json(),
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
pub fn sleep_request(ms: u64) -> Value {
|
|
96
|
+
json!({ "type": "sleep", "ms": ms })
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
pub fn storage_store_request(data: &[u8], content_type: Option<&str>) -> Value {
|
|
100
|
+
storage_store_request_with(BodyRef::Inline(data), content_type)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
pub fn storage_store_request_with(body: BodyRef<'_>, content_type: Option<&str>) -> Value {
|
|
104
|
+
json!({
|
|
105
|
+
"type": "storageStore",
|
|
106
|
+
"contentType": content_type,
|
|
107
|
+
"body": body.to_json(),
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
pub fn storage_get_request(storage_id: &str) -> Value {
|
|
112
|
+
json!({ "type": "storageGet", "storageId": storage_id })
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/// Starts the response of an HTTP action whose body is streamed (V8 host only).
|
|
116
|
+
pub fn http_start_request(status: u16, headers: &[(String, String)]) -> Value {
|
|
117
|
+
json!({ "type": "httpStart", "status": status, "headers": headers })
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/// One chunk of a streamed HTTP action response body.
|
|
121
|
+
pub fn http_write_request(data: &[u8]) -> Value {
|
|
122
|
+
json!({ "type": "httpWrite", "body": B64.encode(data) })
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// Ends a streamed HTTP action response body.
|
|
126
|
+
pub fn http_end_request() -> Value {
|
|
127
|
+
json!({ "type": "httpEnd" })
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/// A task request as the host sees it (used by the mock host).
|
|
131
|
+
#[derive(Clone, Debug, PartialEq)]
|
|
132
|
+
pub enum Request {
|
|
133
|
+
Syscall {
|
|
134
|
+
name: String,
|
|
135
|
+
args: Value,
|
|
136
|
+
},
|
|
137
|
+
Sleep {
|
|
138
|
+
ms: u64,
|
|
139
|
+
},
|
|
140
|
+
StorageStore {
|
|
141
|
+
content_type: Option<String>,
|
|
142
|
+
body: WireBody,
|
|
143
|
+
},
|
|
144
|
+
StorageGet {
|
|
145
|
+
storage_id: String,
|
|
146
|
+
},
|
|
147
|
+
Fetch {
|
|
148
|
+
url: String,
|
|
149
|
+
method: String,
|
|
150
|
+
headers: Vec<(String, String)>,
|
|
151
|
+
body: WireBody,
|
|
152
|
+
},
|
|
153
|
+
HttpStart {
|
|
154
|
+
status: u16,
|
|
155
|
+
headers: Vec<(String, String)>,
|
|
156
|
+
},
|
|
157
|
+
HttpWrite {
|
|
158
|
+
/// Base64 body, as sent.
|
|
159
|
+
body: String,
|
|
160
|
+
},
|
|
161
|
+
HttpEnd,
|
|
162
|
+
/// Unknown `type` (`None` when absent).
|
|
163
|
+
Other(Option<String>),
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/// A request body as received by a host.
|
|
167
|
+
#[derive(Clone, Debug, PartialEq)]
|
|
168
|
+
pub enum WireBody {
|
|
169
|
+
None,
|
|
170
|
+
/// Base64 body, as sent.
|
|
171
|
+
Base64(String),
|
|
172
|
+
/// A reference into guest memory.
|
|
173
|
+
Ref {
|
|
174
|
+
ptr: usize,
|
|
175
|
+
len: usize,
|
|
176
|
+
},
|
|
177
|
+
/// Guest-fed (`net/body_write`).
|
|
178
|
+
Stream,
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
impl WireBody {
|
|
182
|
+
pub fn parse(v: &Value) -> Self {
|
|
183
|
+
if let Some(s) = v.as_str() {
|
|
184
|
+
return WireBody::Base64(s.to_string());
|
|
185
|
+
}
|
|
186
|
+
if v.get("stream").and_then(Value::as_bool) == Some(true) {
|
|
187
|
+
return WireBody::Stream;
|
|
188
|
+
}
|
|
189
|
+
match (
|
|
190
|
+
v.get("ptr").and_then(Value::as_u64),
|
|
191
|
+
v.get("len").and_then(Value::as_u64),
|
|
192
|
+
) {
|
|
193
|
+
(Some(p), Some(l)) => WireBody::Ref {
|
|
194
|
+
ptr: p as usize,
|
|
195
|
+
len: l as usize,
|
|
196
|
+
},
|
|
197
|
+
_ => WireBody::None,
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
impl Request {
|
|
203
|
+
pub fn parse(req: &Value) -> Self {
|
|
204
|
+
let s = |k: &str| req[k].as_str().unwrap_or_default().to_string();
|
|
205
|
+
match req["type"].as_str() {
|
|
206
|
+
Some("syscall") => Request::Syscall {
|
|
207
|
+
name: s("name"),
|
|
208
|
+
args: req["args"].clone(),
|
|
209
|
+
},
|
|
210
|
+
Some("sleep") => Request::Sleep {
|
|
211
|
+
ms: req["ms"].as_u64().unwrap_or_default(),
|
|
212
|
+
},
|
|
213
|
+
Some("storageStore") => Request::StorageStore {
|
|
214
|
+
content_type: req["contentType"].as_str().map(str::to_string),
|
|
215
|
+
body: WireBody::parse(&req["body"]),
|
|
216
|
+
},
|
|
217
|
+
Some("storageGet") => Request::StorageGet {
|
|
218
|
+
storage_id: s("storageId"),
|
|
219
|
+
},
|
|
220
|
+
Some("fetch") => Request::Fetch {
|
|
221
|
+
url: s("url"),
|
|
222
|
+
method: req["method"].as_str().unwrap_or("GET").to_string(),
|
|
223
|
+
headers: serde_json::from_value(req["headers"].clone()).unwrap_or_default(),
|
|
224
|
+
body: WireBody::parse(&req["body"]),
|
|
225
|
+
},
|
|
226
|
+
Some("httpStart") => Request::HttpStart {
|
|
227
|
+
status: req["status"].as_u64().unwrap_or_default() as u16,
|
|
228
|
+
headers: serde_json::from_value(req["headers"].clone()).unwrap_or_default(),
|
|
229
|
+
},
|
|
230
|
+
Some("httpWrite") => Request::HttpWrite { body: s("body") },
|
|
231
|
+
Some("httpEnd") => Request::HttpEnd,
|
|
232
|
+
other => Request::Other(other.map(str::to_string)),
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ---- task results (host -> guest) ----
|
|
238
|
+
|
|
239
|
+
/// The `ok` value of a `fetch` task: the response head.
|
|
240
|
+
pub fn fetch_head(
|
|
241
|
+
status: u16,
|
|
242
|
+
status_text: &str,
|
|
243
|
+
headers: &[(String, String)],
|
|
244
|
+
url: &str,
|
|
245
|
+
body_stream_id: Option<&str>,
|
|
246
|
+
) -> Value {
|
|
247
|
+
json!({
|
|
248
|
+
"status": status,
|
|
249
|
+
"statusText": status_text,
|
|
250
|
+
"headers": headers,
|
|
251
|
+
"url": url,
|
|
252
|
+
"bodyStreamId": body_stream_id,
|
|
253
|
+
})
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/// The `ok` value of a `storageGet` task for an existing file.
|
|
257
|
+
pub fn storage_file_head(
|
|
258
|
+
body_stream_id: &str,
|
|
259
|
+
content_length: usize,
|
|
260
|
+
content_type: Option<&str>,
|
|
261
|
+
) -> Value {
|
|
262
|
+
json!({
|
|
263
|
+
"bodyStreamId": body_stream_id,
|
|
264
|
+
"contentLength": content_length,
|
|
265
|
+
"contentType": content_type,
|
|
266
|
+
})
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/// `bodyStreamId` of a fetch / storageGet head, if any.
|
|
270
|
+
pub fn body_stream_id(head: &Value) -> Option<&str> {
|
|
271
|
+
head.get("bodyStreamId").and_then(Value::as_str)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ---- events (host -> guest) ----
|
|
275
|
+
|
|
276
|
+
/// A host event (contracts/ABI.md "Events").
|
|
277
|
+
#[derive(Clone, Debug, PartialEq)]
|
|
278
|
+
pub enum Event {
|
|
279
|
+
/// A task finished; `result` is `Err(error json)` when it failed.
|
|
280
|
+
Done {
|
|
281
|
+
task_id: i64,
|
|
282
|
+
result: Result<Value, Value>,
|
|
283
|
+
},
|
|
284
|
+
/// A body stream chunk. `terminal` is true for `end` or any `error` key;
|
|
285
|
+
/// `error` is set when the error is a string.
|
|
286
|
+
Chunk {
|
|
287
|
+
stream_id: String,
|
|
288
|
+
data: Option<String>,
|
|
289
|
+
error: Option<String>,
|
|
290
|
+
terminal: bool,
|
|
291
|
+
},
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/// First byte of a binary chunk event (contracts/ABI.md "Binary chunk
|
|
295
|
+
/// events"): `0x01 | u8 id_len | stream id | raw bytes`. JSON events start
|
|
296
|
+
/// with `{`, so the two never collide. A host sends binary events only to a
|
|
297
|
+
/// module that exports `convex_binary_events`.
|
|
298
|
+
pub const BIN_CHUNK: u8 = 1;
|
|
299
|
+
|
|
300
|
+
/// Capability marker: its presence as an export tells the host this module
|
|
301
|
+
/// parses binary chunk events. Old modules lack it and keep getting JSON.
|
|
302
|
+
#[no_mangle]
|
|
303
|
+
pub extern "C" fn convex_binary_events() -> i32 {
|
|
304
|
+
1
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/// Typed view of a JSON event, borrowing from the input; `ok` / `error`
|
|
308
|
+
/// payloads are the only parts that become `Value`s.
|
|
309
|
+
#[derive(serde::Deserialize)]
|
|
310
|
+
struct RawEvent<'a> {
|
|
311
|
+
#[serde(rename = "type", borrow)]
|
|
312
|
+
ty: std::borrow::Cow<'a, str>,
|
|
313
|
+
#[serde(rename = "taskId", default)]
|
|
314
|
+
task_id: Option<i64>,
|
|
315
|
+
#[serde(rename = "streamId", borrow, default)]
|
|
316
|
+
stream_id: Option<std::borrow::Cow<'a, str>>,
|
|
317
|
+
#[serde(borrow, default)]
|
|
318
|
+
data: Option<std::borrow::Cow<'a, str>>,
|
|
319
|
+
#[serde(default)]
|
|
320
|
+
end: Option<bool>,
|
|
321
|
+
#[serde(default)]
|
|
322
|
+
ok: Option<Value>,
|
|
323
|
+
#[serde(default)]
|
|
324
|
+
error: Option<Value>,
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/// A host event as the executor consumes it: chunk bytes are already decoded.
|
|
328
|
+
#[derive(Debug, PartialEq)]
|
|
329
|
+
pub enum Incoming {
|
|
330
|
+
Done {
|
|
331
|
+
task_id: i64,
|
|
332
|
+
result: Result<Value, Value>,
|
|
333
|
+
},
|
|
334
|
+
Chunk {
|
|
335
|
+
stream_id: String,
|
|
336
|
+
data: Option<Vec<u8>>,
|
|
337
|
+
error: Option<String>,
|
|
338
|
+
terminal: bool,
|
|
339
|
+
},
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
impl Incoming {
|
|
343
|
+
/// Parses a JSON or binary event without building an intermediate `Value`
|
|
344
|
+
/// for chunks; `None` for malformed or unknown events.
|
|
345
|
+
/// Like [`Incoming::parse`], but takes the event buffer: a binary chunk reuses it (the
|
|
346
|
+
/// header is shifted out in place) instead of allocating and copying the payload.
|
|
347
|
+
pub fn parse_owned(mut bytes: Vec<u8>) -> Option<Self> {
|
|
348
|
+
if bytes.first() == Some(&BIN_CHUNK) {
|
|
349
|
+
let id_len = *bytes.get(1)? as usize;
|
|
350
|
+
let id = std::str::from_utf8(bytes.get(2..2 + id_len)?).ok()?.to_string();
|
|
351
|
+
bytes.drain(..2 + id_len);
|
|
352
|
+
return Some(Incoming::Chunk {
|
|
353
|
+
stream_id: id,
|
|
354
|
+
data: Some(bytes),
|
|
355
|
+
error: None,
|
|
356
|
+
terminal: false,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
Self::parse(&bytes)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
pub fn parse(bytes: &[u8]) -> Option<Self> {
|
|
363
|
+
if bytes.first() == Some(&BIN_CHUNK) {
|
|
364
|
+
let id_len = *bytes.get(1)? as usize;
|
|
365
|
+
let id = std::str::from_utf8(bytes.get(2..2 + id_len)?).ok()?;
|
|
366
|
+
return Some(Incoming::Chunk {
|
|
367
|
+
stream_id: id.to_string(),
|
|
368
|
+
data: Some(bytes[2 + id_len..].to_vec()),
|
|
369
|
+
error: None,
|
|
370
|
+
terminal: false,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
let ev: RawEvent = serde_json::from_slice(bytes).ok()?;
|
|
374
|
+
match &*ev.ty {
|
|
375
|
+
"done" => Some(Incoming::Done {
|
|
376
|
+
task_id: ev.task_id.unwrap_or(-1),
|
|
377
|
+
result: match ev.error {
|
|
378
|
+
Some(err) => Err(err),
|
|
379
|
+
None => Ok(ev.ok.unwrap_or(Value::Null)),
|
|
380
|
+
},
|
|
381
|
+
}),
|
|
382
|
+
"chunk" => {
|
|
383
|
+
let data = match ev.data {
|
|
384
|
+
Some(d) => {
|
|
385
|
+
let mut out = Vec::with_capacity(d.len() / 4 * 3 + 3);
|
|
386
|
+
// An undecodable chunk is dropped, as before.
|
|
387
|
+
B64.decode_vec(d.as_bytes(), &mut out).ok().map(|_| out)
|
|
388
|
+
}
|
|
389
|
+
None => None,
|
|
390
|
+
};
|
|
391
|
+
Some(Incoming::Chunk {
|
|
392
|
+
stream_id: ev.stream_id.map(|s| s.into_owned()).unwrap_or_default(),
|
|
393
|
+
data,
|
|
394
|
+
terminal: ev.error.is_some() || ev.end == Some(true),
|
|
395
|
+
error: ev.error.and_then(|e| e.as_str().map(str::to_string)),
|
|
396
|
+
})
|
|
397
|
+
}
|
|
398
|
+
_ => None,
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
impl Event {
|
|
404
|
+
/// Parses one JSON event; `None` if it is not JSON or of an unknown type.
|
|
405
|
+
pub fn parse(bytes: &[u8]) -> Option<Self> {
|
|
406
|
+
Some(match Incoming::parse(bytes)? {
|
|
407
|
+
Incoming::Done { task_id, result } => Event::Done { task_id, result },
|
|
408
|
+
Incoming::Chunk {
|
|
409
|
+
stream_id,
|
|
410
|
+
data,
|
|
411
|
+
error,
|
|
412
|
+
terminal,
|
|
413
|
+
} => Event::Chunk {
|
|
414
|
+
stream_id,
|
|
415
|
+
data: data.map(|d| B64.encode(d)),
|
|
416
|
+
error,
|
|
417
|
+
terminal,
|
|
418
|
+
},
|
|
419
|
+
})
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/// The wire JSON of this event (used by the mock host).
|
|
423
|
+
pub fn to_json(&self) -> Value {
|
|
424
|
+
match self {
|
|
425
|
+
Event::Done {
|
|
426
|
+
task_id,
|
|
427
|
+
result: Ok(v),
|
|
428
|
+
} => json!({ "type": "done", "taskId": task_id, "ok": v }),
|
|
429
|
+
Event::Done {
|
|
430
|
+
task_id,
|
|
431
|
+
result: Err(e),
|
|
432
|
+
} => json!({ "type": "done", "taskId": task_id, "error": e }),
|
|
433
|
+
Event::Chunk {
|
|
434
|
+
stream_id,
|
|
435
|
+
data,
|
|
436
|
+
error,
|
|
437
|
+
terminal,
|
|
438
|
+
} => {
|
|
439
|
+
let mut v = json!({ "type": "chunk", "streamId": stream_id });
|
|
440
|
+
if let Some(d) = data {
|
|
441
|
+
v["data"] = Value::String(d.clone());
|
|
442
|
+
}
|
|
443
|
+
if let Some(e) = error {
|
|
444
|
+
v["error"] = Value::String(e.clone());
|
|
445
|
+
} else if *terminal {
|
|
446
|
+
v["end"] = Value::Bool(true);
|
|
447
|
+
}
|
|
448
|
+
v
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
pub fn done_ok(task_id: i64, ok: Value) -> Self {
|
|
454
|
+
Event::Done {
|
|
455
|
+
task_id,
|
|
456
|
+
result: Ok(ok),
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
pub fn done_err(task_id: i64, error: Value) -> Self {
|
|
461
|
+
Event::Done {
|
|
462
|
+
task_id,
|
|
463
|
+
result: Err(error),
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
pub fn chunk_data(stream_id: &str, bytes: &[u8]) -> Self {
|
|
468
|
+
Event::Chunk {
|
|
469
|
+
stream_id: stream_id.to_string(),
|
|
470
|
+
data: Some(B64.encode(bytes)),
|
|
471
|
+
error: None,
|
|
472
|
+
terminal: false,
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
pub fn chunk_end(stream_id: &str) -> Self {
|
|
477
|
+
Event::Chunk {
|
|
478
|
+
stream_id: stream_id.to_string(),
|
|
479
|
+
data: None,
|
|
480
|
+
error: None,
|
|
481
|
+
terminal: true,
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
pub fn chunk_error(stream_id: &str, message: &str) -> Self {
|
|
486
|
+
Event::Chunk {
|
|
487
|
+
stream_id: stream_id.to_string(),
|
|
488
|
+
data: None,
|
|
489
|
+
error: Some(message.to_string()),
|
|
490
|
+
terminal: true,
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
#[cfg(test)]
|
|
496
|
+
mod tests {
|
|
497
|
+
use super::*;
|
|
498
|
+
|
|
499
|
+
#[test]
|
|
500
|
+
fn event_round_trip() {
|
|
501
|
+
for ev in [
|
|
502
|
+
Event::done_ok(3, json!({"a": 1})),
|
|
503
|
+
Event::done_err(4, json!({"message": "x"})),
|
|
504
|
+
Event::chunk_data("s", b"hi"),
|
|
505
|
+
Event::chunk_end("s"),
|
|
506
|
+
Event::chunk_error("s", "boom"),
|
|
507
|
+
] {
|
|
508
|
+
let bytes = ev.to_json().to_string().into_bytes();
|
|
509
|
+
assert_eq!(Event::parse(&bytes), Some(ev));
|
|
510
|
+
}
|
|
511
|
+
assert_eq!(Event::parse(b"{\"type\":\"nope\"}"), None);
|
|
512
|
+
assert_eq!(Event::parse(b"not json"), None);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
#[test]
|
|
516
|
+
fn binary_chunk_events_parse() {
|
|
517
|
+
let mut ev = vec![BIN_CHUNK, 3];
|
|
518
|
+
ev.extend_from_slice(b"abc");
|
|
519
|
+
ev.extend_from_slice(&[0, 255, 7]);
|
|
520
|
+
assert_eq!(
|
|
521
|
+
Incoming::parse(&ev),
|
|
522
|
+
Some(Incoming::Chunk {
|
|
523
|
+
stream_id: "abc".into(),
|
|
524
|
+
data: Some(vec![0, 255, 7]),
|
|
525
|
+
error: None,
|
|
526
|
+
terminal: false
|
|
527
|
+
})
|
|
528
|
+
);
|
|
529
|
+
assert_eq!(Incoming::parse_owned(ev.clone()), Incoming::parse(&ev));
|
|
530
|
+
let json = br#"{"type":"chunk","streamId":"s","end":true}"#.to_vec();
|
|
531
|
+
assert_eq!(Incoming::parse_owned(json.clone()), Incoming::parse(&json));
|
|
532
|
+
// Truncated header.
|
|
533
|
+
assert_eq!(Incoming::parse(&[BIN_CHUNK, 9, b'a']), None);
|
|
534
|
+
assert_eq!(Incoming::parse_owned(vec![BIN_CHUNK, 9, b'a']), None);
|
|
535
|
+
// JSON chunk decodes straight to bytes.
|
|
536
|
+
assert_eq!(
|
|
537
|
+
Incoming::parse(br#"{"type":"chunk","streamId":"s","data":"aGk="}"#),
|
|
538
|
+
Some(Incoming::Chunk {
|
|
539
|
+
stream_id: "s".into(),
|
|
540
|
+
data: Some(b"hi".to_vec()),
|
|
541
|
+
error: None,
|
|
542
|
+
terminal: false
|
|
543
|
+
})
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
#[test]
|
|
548
|
+
fn request_keys_are_stable() {
|
|
549
|
+
let r = fetch_request(
|
|
550
|
+
"http://x/",
|
|
551
|
+
"POST",
|
|
552
|
+
&[("a".into(), "b".into())],
|
|
553
|
+
Some(b"hi"),
|
|
554
|
+
);
|
|
555
|
+
assert_eq!(
|
|
556
|
+
r.to_string(),
|
|
557
|
+
r#"{"body":"aGk=","headers":[["a","b"]],"method":"POST","type":"fetch","url":"http://x/"}"#
|
|
558
|
+
);
|
|
559
|
+
assert_eq!(sleep_request(5).to_string(), r#"{"ms":5,"type":"sleep"}"#);
|
|
560
|
+
assert_eq!(
|
|
561
|
+
storage_get_request("id").to_string(),
|
|
562
|
+
r#"{"storageId":"id","type":"storageGet"}"#
|
|
563
|
+
);
|
|
564
|
+
assert_eq!(
|
|
565
|
+
Request::parse(&storage_store_request(b"hi", Some("t/x"))),
|
|
566
|
+
Request::StorageStore {
|
|
567
|
+
content_type: Some("t/x".into()),
|
|
568
|
+
body: WireBody::Base64("aGk=".into())
|
|
569
|
+
}
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
#[test]
|
|
574
|
+
fn log_levels() {
|
|
575
|
+
assert_eq!(LogLevel::Error.as_i32(), 3);
|
|
576
|
+
assert_eq!(LogLevel::from_i32(0), Some(LogLevel::Debug));
|
|
577
|
+
assert_eq!(LogLevel::from_i32(9), None);
|
|
578
|
+
}
|
|
579
|
+
}
|