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,75 @@
|
|
|
1
|
+
//! Time zones and locale formatting through the host's `Intl` (`intl/*` sync ops, see
|
|
2
|
+
//! contracts/ABI.md "Optional sync ops: intl/*").
|
|
3
|
+
//!
|
|
4
|
+
//! A wasm32 guest has no time zone database: `chrono::Local` and `std` know only UTC. Asking for a
|
|
5
|
+
//! zone here never falls back to UTC; an unknown zone, or a host without these ops, is an error.
|
|
6
|
+
//! (To stay self-contained instead, `jiff` with its bundled tzdb or `chrono-tz` work on wasm32 too, at
|
|
7
|
+
//! roughly 0.4-0.5 MB of wasm per call, which the cloud pays at every cold call.)
|
|
8
|
+
|
|
9
|
+
use serde_json::{json, Value};
|
|
10
|
+
|
|
11
|
+
use crate::{Error, Result};
|
|
12
|
+
|
|
13
|
+
fn call(op: &str, args: Value) -> Result<Value> {
|
|
14
|
+
let out = crate::sys::sync(op, args.to_string().as_bytes());
|
|
15
|
+
let v: Value = serde_json::from_slice(&out)?;
|
|
16
|
+
match v.get("error") {
|
|
17
|
+
Some(err) => {
|
|
18
|
+
let e = Error::from_json(err);
|
|
19
|
+
let msg = e.message().to_string();
|
|
20
|
+
if msg.contains("unknown sync op") {
|
|
21
|
+
Err(Error::msg(format!(
|
|
22
|
+
"host lacks {op}: update convex-rust to >= {MIN_CONVEX_RUST} (V8 host); the wasmtime host has no intl ops, use jiff or chrono-tz with a bundled tzdb"
|
|
23
|
+
)))
|
|
24
|
+
} else {
|
|
25
|
+
Err(e)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
None => Ok(v.get("ok").cloned().unwrap_or(Value::Null)),
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/// First `convex-rust` release that serves the `intl/*` ops on V8 hosts.
|
|
33
|
+
pub const MIN_CONVEX_RUST: &str = "0.2.0";
|
|
34
|
+
|
|
35
|
+
/// Offset of the IANA zone `tz` from UTC at `epoch_ms`, in seconds (positive east of UTC), DST included.
|
|
36
|
+
/// Errors on an unknown zone; never returns 0 as a stand-in.
|
|
37
|
+
pub fn tz_offset_secs(tz: &str, epoch_ms: f64) -> Result<i32> {
|
|
38
|
+
let v = call("intl/tzOffset", json!({ "tz": tz, "epochMs": epoch_ms }))?;
|
|
39
|
+
v.as_i64()
|
|
40
|
+
.map(|n| n as i32)
|
|
41
|
+
.ok_or_else(|| Error::msg(format!("intl/tzOffset: unexpected reply {v}")))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/// Converts a wall-clock time in `tz` (milliseconds since the epoch as if the wall clock were UTC) to the real
|
|
45
|
+
/// epoch milliseconds. In a DST gap or overlap (a wall-clock time that does not exist, or exists twice) the
|
|
46
|
+
/// result is one of the two candidate instants.
|
|
47
|
+
pub fn local_to_epoch_ms(tz: &str, local_ms: f64) -> Result<f64> {
|
|
48
|
+
let first = tz_offset_secs(tz, local_ms)? as f64 * 1000.0;
|
|
49
|
+
let guess = local_ms - first;
|
|
50
|
+
let second = tz_offset_secs(tz, guess)? as f64 * 1000.0;
|
|
51
|
+
Ok(if second == first { guess } else { local_ms - second })
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// `new Intl.DateTimeFormat(locale, options).format(epoch_ms)`. `options` is the Intl options object, for example
|
|
55
|
+
/// `{"timeZone": "Europe/Lisbon", "dateStyle": "full", "timeStyle": "short"}`. An unknown `timeZone` or locale
|
|
56
|
+
/// is an error.
|
|
57
|
+
pub fn format_date(epoch_ms: f64, locale: &str, options: Value) -> Result<String> {
|
|
58
|
+
format("date", json!(epoch_ms), locale, options)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// `new Intl.NumberFormat(locale, options).format(value)`, for example options
|
|
62
|
+
/// `{"style": "currency", "currency": "EUR"}`.
|
|
63
|
+
pub fn format_number(value: f64, locale: &str, options: Value) -> Result<String> {
|
|
64
|
+
format("number", json!(value), locale, options)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
fn format(kind: &str, value: Value, locale: &str, options: Value) -> Result<String> {
|
|
68
|
+
let v = call(
|
|
69
|
+
"intl/format",
|
|
70
|
+
json!({ "kind": kind, "value": value, "locale": locale, "options": options }),
|
|
71
|
+
)?;
|
|
72
|
+
v.as_str()
|
|
73
|
+
.map(str::to_string)
|
|
74
|
+
.ok_or_else(|| Error::msg(format!("intl/format: unexpected reply {v}")))
|
|
75
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
//! Write Convex actions in Rust (`"use rust"`).
|
|
2
|
+
//!
|
|
3
|
+
//! ```ignore
|
|
4
|
+
//! use convex_fn::{action, Ctx, Result};
|
|
5
|
+
//!
|
|
6
|
+
//! #[action]
|
|
7
|
+
//! async fn hello(ctx: &Ctx, args: serde_json::Value) -> Result<String> {
|
|
8
|
+
//! let msgs = ctx.run_query("messages:list", serde_json::json!({})).await?;
|
|
9
|
+
//! Ok(format!("{} messages", msgs.as_array().map_or(0, Vec::len)))
|
|
10
|
+
//! }
|
|
11
|
+
//! ```
|
|
12
|
+
//!
|
|
13
|
+
//! # Logging, clock, random, env
|
|
14
|
+
//!
|
|
15
|
+
//! - `println!` goes to WASI stdout and `eprintln!` to stderr; the host turns
|
|
16
|
+
//! each line into a function log line (stdout at log/info level, stderr at
|
|
17
|
+
//! error level). No setup needed.
|
|
18
|
+
//! - Feature `log`: a `log::Log` implementation is installed by the `#[action]`
|
|
19
|
+
//! entry unless a logger is already set (`error`->3, `warn`->2, `info`->1,
|
|
20
|
+
//! `debug`/`trace`->0). Messages are written as-is (no target prefix).
|
|
21
|
+
//! - Feature `tracing`: a minimal subscriber is installed the same way; events
|
|
22
|
+
//! are written as `span{fields}: message key=value`, at the mapped level.
|
|
23
|
+
//! - `std::env::var`, `SystemTime::now`, `Instant` and `getrandom` (0.2 and 0.3,
|
|
24
|
+
//! hence `rand`) work through WASI.
|
|
25
|
+
//! - With `tokio-rt`, blocking calls (`block_on`, `reqwest::blocking`) made
|
|
26
|
+
//! inside an action drive the host task directly; tokio tasks spawned on the
|
|
27
|
+
//! `LocalSet` do not progress until the blocking call returns.
|
|
28
|
+
//!
|
|
29
|
+
//! Host interface: `contracts/ABI.md`. Function metadata: `contracts/MANIFEST.md`.
|
|
30
|
+
|
|
31
|
+
mod args;
|
|
32
|
+
pub mod crypto;
|
|
33
|
+
mod ctx;
|
|
34
|
+
pub mod dns;
|
|
35
|
+
mod error;
|
|
36
|
+
mod executor;
|
|
37
|
+
mod fanout;
|
|
38
|
+
pub mod fs;
|
|
39
|
+
pub mod intl;
|
|
40
|
+
mod function_ref;
|
|
41
|
+
mod http;
|
|
42
|
+
mod logging;
|
|
43
|
+
#[cfg(convex_native)]
|
|
44
|
+
pub mod native;
|
|
45
|
+
mod net;
|
|
46
|
+
mod sys;
|
|
47
|
+
pub mod values;
|
|
48
|
+
#[doc(hidden)]
|
|
49
|
+
pub mod wire;
|
|
50
|
+
|
|
51
|
+
pub use args::{ConvexType, Field, Validator};
|
|
52
|
+
pub use convex_fn_macros::{action, http_action, ConvexArgs, ConvexValue};
|
|
53
|
+
pub use ctx::{
|
|
54
|
+
ChunkSource, Chunks, Ctx, FetchRequest, FetchResponse, FetchStream, Redirect, Scheduler,
|
|
55
|
+
Storage, StorageFile, StorageMetadata, VectorFilter, VectorHit, VectorSearchQuery,
|
|
56
|
+
};
|
|
57
|
+
pub use error::{ConvexError, Error, Result};
|
|
58
|
+
pub use executor::{block_on, join_all, race, select, Either};
|
|
59
|
+
pub use fanout::par_map;
|
|
60
|
+
pub use function_ref::{
|
|
61
|
+
ComponentFunctionRef, FunctionAddress, FunctionHandle, FunctionRef, FunctionTarget,
|
|
62
|
+
};
|
|
63
|
+
pub use http::{BodyWriter, HttpRequest, HttpResponse};
|
|
64
|
+
pub use serde;
|
|
65
|
+
pub use serde_json::{json, Value};
|
|
66
|
+
pub use values::{Bytes, Float64, Int64};
|
|
67
|
+
|
|
68
|
+
/// Wire format shared with `convex_fn_test`; not public API.
|
|
69
|
+
#[doc(hidden)]
|
|
70
|
+
pub use wire as __wire;
|
|
71
|
+
|
|
72
|
+
/// Write one line to the function log at the given level.
|
|
73
|
+
pub mod log {
|
|
74
|
+
use crate::wire::LogLevel;
|
|
75
|
+
|
|
76
|
+
pub fn debug(msg: &str) {
|
|
77
|
+
crate::sys::log_line(LogLevel::Debug, msg)
|
|
78
|
+
}
|
|
79
|
+
pub fn info(msg: &str) {
|
|
80
|
+
crate::sys::log_line(LogLevel::Info, msg)
|
|
81
|
+
}
|
|
82
|
+
pub fn warn(msg: &str) {
|
|
83
|
+
crate::sys::log_line(LogLevel::Warn, msg)
|
|
84
|
+
}
|
|
85
|
+
pub fn error(msg: &str) {
|
|
86
|
+
crate::sys::log_line(LogLevel::Error, msg)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/// Hooks for running actions natively (used by `convex_fn_test`).
|
|
91
|
+
#[cfg(all(not(target_arch = "wasm32"), not(convex_native)))]
|
|
92
|
+
pub mod testing {
|
|
93
|
+
/// A host implementing contracts/ABI.md in-process. Byte payloads are the
|
|
94
|
+
/// same JSON the wasm host exchanges.
|
|
95
|
+
pub trait Host {
|
|
96
|
+
fn task_submit(&mut self, request: &[u8]) -> i64;
|
|
97
|
+
/// `None` means "nothing ready" (`block = false`) or "no outstanding
|
|
98
|
+
/// tasks" (`block = true`).
|
|
99
|
+
fn task_poll(&mut self, block: bool) -> Option<Vec<u8>>;
|
|
100
|
+
fn sync_syscall(&mut self, op: &str, args: &[u8]) -> Vec<u8>;
|
|
101
|
+
fn log(&mut self, level: i32, msg: &str);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/// Install `host` for the current thread, returning the previous one.
|
|
105
|
+
pub fn install_host(host: Box<dyn Host>) -> Option<Box<dyn Host>> {
|
|
106
|
+
crate::executor::reset();
|
|
107
|
+
crate::sys::install(host)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/// Remove and return the current thread's host.
|
|
111
|
+
pub fn uninstall_host() -> Option<Box<dyn Host>> {
|
|
112
|
+
crate::sys::uninstall()
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/// Optional wasm export: frees the result buffer of a `convex_run:*` call once the host has read it
|
|
117
|
+
/// (contracts/V8HOST.md). `ptr`/`len` are the two halves of the packed value the call returned; that
|
|
118
|
+
/// buffer is an exactly sized boxed slice (see `run_export`). Hosts that do not know it just leak the buffer.
|
|
119
|
+
#[cfg(target_arch = "wasm32")]
|
|
120
|
+
#[no_mangle]
|
|
121
|
+
pub extern "C" fn convex_free(ptr: *mut u8, len: usize) {
|
|
122
|
+
if len == 0 || ptr.is_null() {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
// SAFETY: `run_export` returned `Box<[u8]>::into_raw` parts (ptr, len); the host frees them once.
|
|
126
|
+
unsafe { drop(Box::from_raw(std::ptr::slice_from_raw_parts_mut(ptr, len))) };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
#[doc(hidden)]
|
|
130
|
+
pub mod __rt {
|
|
131
|
+
use std::future::Future;
|
|
132
|
+
|
|
133
|
+
use serde::{de::DeserializeOwned, Serialize};
|
|
134
|
+
|
|
135
|
+
use crate::{error::Error, Ctx};
|
|
136
|
+
|
|
137
|
+
/// A fresh `Ctx` for native test hosts (`convex_fn_test`).
|
|
138
|
+
#[cfg(all(not(target_arch = "wasm32"), not(convex_native)))]
|
|
139
|
+
pub fn test_ctx() -> Ctx {
|
|
140
|
+
Ctx::new()
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
pub use crate::http::adapter as http_adapter;
|
|
144
|
+
|
|
145
|
+
pub use crate::args::{manifest_bytes, manifest_len, manifest_string, NoValidator, Probe};
|
|
146
|
+
|
|
147
|
+
/// Stand-in return type with no validator.
|
|
148
|
+
pub struct NoReturnType;
|
|
149
|
+
|
|
150
|
+
pub const fn const_bytes<const N: usize>(s: &str) -> [u8; N] {
|
|
151
|
+
let b = s.as_bytes();
|
|
152
|
+
let mut out = [0u8; N];
|
|
153
|
+
let mut i = 0;
|
|
154
|
+
while i < N {
|
|
155
|
+
out[i] = b[i];
|
|
156
|
+
i += 1;
|
|
157
|
+
}
|
|
158
|
+
out
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/// Installs the `log` / `tracing` bridges (features `log`, `tracing`) unless
|
|
162
|
+
/// the user already installed their own. No-op without those features.
|
|
163
|
+
pub fn install_logging() {
|
|
164
|
+
crate::logging::install();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/// Body of every `convex_run:*` export.
|
|
168
|
+
#[cfg(target_arch = "wasm32")]
|
|
169
|
+
pub fn run_export<A, R, F, Fut>(args_ptr: *mut u8, args_len: usize, f: F) -> i64
|
|
170
|
+
where
|
|
171
|
+
A: DeserializeOwned,
|
|
172
|
+
R: Serialize,
|
|
173
|
+
F: FnOnce(Ctx, A) -> Fut,
|
|
174
|
+
Fut: Future<Output = crate::Result<R>>,
|
|
175
|
+
{
|
|
176
|
+
crate::error::install_panic_hook();
|
|
177
|
+
// A reused instance must not see the previous call's tasks/streams (the host restarts ids at 0).
|
|
178
|
+
crate::executor::reset_state();
|
|
179
|
+
// SAFETY: the host wrote `args_len` bytes into a buffer from `convex_alloc`.
|
|
180
|
+
let args = unsafe { Vec::from_raw_parts(args_ptr, args_len, args_len.max(1)) };
|
|
181
|
+
let envelope = run_to_envelope(&args, f);
|
|
182
|
+
let out = std::mem::ManuallyDrop::new(envelope.into_bytes().into_boxed_slice());
|
|
183
|
+
((out.as_ptr() as u64) << 32 | out.len() as u64) as i64
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/// Body of every `convex_run:*` export in native (`--cfg convex_native`) builds.
|
|
187
|
+
#[cfg(convex_native)]
|
|
188
|
+
pub fn run_export<A, R, F, Fut>(args_ptr: *mut u8, args_len: usize, f: F) -> i64
|
|
189
|
+
where
|
|
190
|
+
A: DeserializeOwned,
|
|
191
|
+
R: Serialize,
|
|
192
|
+
F: FnOnce(Ctx, A) -> Fut,
|
|
193
|
+
Fut: Future<Output = crate::Result<R>>,
|
|
194
|
+
{
|
|
195
|
+
crate::native::run_export_native(args_ptr, args_len, f)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/// Plain native builds (`cargo test`) never call the exports.
|
|
199
|
+
#[cfg(all(not(target_arch = "wasm32"), not(convex_native)))]
|
|
200
|
+
pub fn run_export<A, R, F, Fut>(_args_ptr: *mut u8, _args_len: usize, _f: F) -> i64
|
|
201
|
+
where
|
|
202
|
+
A: DeserializeOwned,
|
|
203
|
+
R: Serialize,
|
|
204
|
+
F: FnOnce(Ctx, A) -> Fut,
|
|
205
|
+
Fut: Future<Output = crate::Result<R>>,
|
|
206
|
+
{
|
|
207
|
+
unreachable!("convex_run exports are only callable on wasm32 or with --cfg convex_native")
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/// Body of every `#[http_action]` `convex_run:*` export: the call's args are the request head.
|
|
211
|
+
pub fn run_http_export<F, Fut>(args_ptr: *mut u8, args_len: usize, f: F) -> i64
|
|
212
|
+
where
|
|
213
|
+
F: FnOnce(Ctx, crate::HttpRequest) -> Fut + 'static,
|
|
214
|
+
Fut: Future<Output = crate::Result<crate::HttpResponse>> + 'static,
|
|
215
|
+
{
|
|
216
|
+
run_export(args_ptr, args_len, crate::http::adapter(f))
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/// Runs an action on raw Convex-JSON args and returns the result envelope
|
|
220
|
+
/// (contracts/ABI.md "Result envelope").
|
|
221
|
+
pub fn run_to_envelope<A, R, F, Fut>(args: &[u8], f: F) -> String
|
|
222
|
+
where
|
|
223
|
+
A: DeserializeOwned,
|
|
224
|
+
R: Serialize,
|
|
225
|
+
F: FnOnce(Ctx, A) -> Fut,
|
|
226
|
+
Fut: Future<Output = crate::Result<R>>,
|
|
227
|
+
{
|
|
228
|
+
install_logging();
|
|
229
|
+
let result = serde_json::from_slice::<A>(args)
|
|
230
|
+
.map_err(|e| Error::msg(format!("invalid arguments: {e}")))
|
|
231
|
+
.and_then(|a| crate::executor::block_on(f(Ctx::new(), a)))
|
|
232
|
+
.and_then(|r| {
|
|
233
|
+
serde_json::to_value(r)
|
|
234
|
+
.map_err(|e| Error::msg(format!("invalid return value: {e}")))
|
|
235
|
+
});
|
|
236
|
+
let envelope = match result {
|
|
237
|
+
Ok(v) => serde_json::json!({ "ok": v }),
|
|
238
|
+
Err(e) => serde_json::json!({ "error": e.to_json() }),
|
|
239
|
+
};
|
|
240
|
+
envelope.to_string()
|
|
241
|
+
}
|
|
242
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
//! Optional bridges from the `log` and `tracing` ecosystems to the host `log`
|
|
2
|
+
//! import (contracts/ABI.md levels: 0 debug, 1 info, 2 warn, 3 error).
|
|
3
|
+
//!
|
|
4
|
+
//! Both are installed automatically by the `#[action]` entry (and by
|
|
5
|
+
//! `convex_fn_test`) when the corresponding cargo feature is on and the user has
|
|
6
|
+
//! not installed a logger / default subscriber of their own.
|
|
7
|
+
//!
|
|
8
|
+
//! Plain `println!` / `eprintln!` need no feature: the WASI stdout / stderr
|
|
9
|
+
//! file descriptors are turned into log lines by the host (stdout at info,
|
|
10
|
+
//! stderr at error).
|
|
11
|
+
|
|
12
|
+
#[cfg(feature = "log")]
|
|
13
|
+
mod log_bridge {
|
|
14
|
+
use log_crate::{Level, LevelFilter, Log, Metadata, Record};
|
|
15
|
+
|
|
16
|
+
use crate::{sys, wire::LogLevel};
|
|
17
|
+
|
|
18
|
+
pub struct HostLogger;
|
|
19
|
+
|
|
20
|
+
pub fn level(l: Level) -> LogLevel {
|
|
21
|
+
match l {
|
|
22
|
+
Level::Trace | Level::Debug => LogLevel::Debug,
|
|
23
|
+
Level::Info => LogLevel::Info,
|
|
24
|
+
Level::Warn => LogLevel::Warn,
|
|
25
|
+
Level::Error => LogLevel::Error,
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
impl Log for HostLogger {
|
|
30
|
+
fn enabled(&self, _: &Metadata<'_>) -> bool {
|
|
31
|
+
true
|
|
32
|
+
}
|
|
33
|
+
fn log(&self, record: &Record<'_>) {
|
|
34
|
+
sys::log_line(level(record.level()), &record.args().to_string());
|
|
35
|
+
}
|
|
36
|
+
fn flush(&self) {}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
static LOGGER: HostLogger = HostLogger;
|
|
40
|
+
|
|
41
|
+
pub fn install() {
|
|
42
|
+
if log_crate::set_logger(&LOGGER).is_ok() {
|
|
43
|
+
log_crate::set_max_level(LevelFilter::Trace);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
#[cfg(feature = "tracing")]
|
|
49
|
+
mod tracing_bridge {
|
|
50
|
+
use std::{
|
|
51
|
+
collections::HashMap,
|
|
52
|
+
fmt::Write as _,
|
|
53
|
+
sync::{
|
|
54
|
+
atomic::{AtomicU64, Ordering},
|
|
55
|
+
Mutex,
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
use tracing_core::{
|
|
60
|
+
field::{Field, Visit},
|
|
61
|
+
span, Event, Level, Metadata, Subscriber,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
use crate::{sys, wire::LogLevel};
|
|
65
|
+
|
|
66
|
+
#[derive(Default)]
|
|
67
|
+
struct Fields {
|
|
68
|
+
message: String,
|
|
69
|
+
rest: String,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
impl Fields {
|
|
73
|
+
fn kv(&mut self, name: &str, value: std::fmt::Arguments<'_>) {
|
|
74
|
+
if !self.rest.is_empty() {
|
|
75
|
+
self.rest.push(' ');
|
|
76
|
+
}
|
|
77
|
+
let _ = write!(self.rest, "{name}={value}");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
impl Visit for Fields {
|
|
82
|
+
fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
|
|
83
|
+
if field.name() == "message" {
|
|
84
|
+
let _ = write!(self.message, "{value:?}");
|
|
85
|
+
} else {
|
|
86
|
+
self.kv(field.name(), format_args!("{value:?}"));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
fn record_str(&mut self, field: &Field, value: &str) {
|
|
90
|
+
if field.name() == "message" {
|
|
91
|
+
self.message.push_str(value);
|
|
92
|
+
} else {
|
|
93
|
+
self.kv(field.name(), format_args!("{value:?}"));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
struct SpanData {
|
|
99
|
+
name: &'static str,
|
|
100
|
+
fields: String,
|
|
101
|
+
refs: usize,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/// Formats events as `span{fields}: message key=value ...` and writes them
|
|
105
|
+
/// to the host log.
|
|
106
|
+
#[derive(Default)]
|
|
107
|
+
pub struct HostSubscriber {
|
|
108
|
+
next_id: AtomicU64,
|
|
109
|
+
spans: Mutex<HashMap<u64, SpanData>>,
|
|
110
|
+
stack: Mutex<Vec<u64>>,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
pub fn level(l: &Level) -> LogLevel {
|
|
114
|
+
match *l {
|
|
115
|
+
Level::TRACE | Level::DEBUG => LogLevel::Debug,
|
|
116
|
+
Level::INFO => LogLevel::Info,
|
|
117
|
+
Level::WARN => LogLevel::Warn,
|
|
118
|
+
Level::ERROR => LogLevel::Error,
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
impl Subscriber for HostSubscriber {
|
|
123
|
+
fn enabled(&self, _: &Metadata<'_>) -> bool {
|
|
124
|
+
true
|
|
125
|
+
}
|
|
126
|
+
fn new_span(&self, attrs: &span::Attributes<'_>) -> span::Id {
|
|
127
|
+
let mut f = Fields::default();
|
|
128
|
+
attrs.record(&mut f);
|
|
129
|
+
let id = self.next_id.fetch_add(1, Ordering::Relaxed) + 1;
|
|
130
|
+
self.spans.lock().unwrap().insert(
|
|
131
|
+
id,
|
|
132
|
+
SpanData {
|
|
133
|
+
name: attrs.metadata().name(),
|
|
134
|
+
fields: f.rest,
|
|
135
|
+
refs: 1,
|
|
136
|
+
},
|
|
137
|
+
);
|
|
138
|
+
span::Id::from_u64(id)
|
|
139
|
+
}
|
|
140
|
+
fn record(&self, id: &span::Id, values: &span::Record<'_>) {
|
|
141
|
+
let mut f = Fields::default();
|
|
142
|
+
values.record(&mut f);
|
|
143
|
+
if let Some(s) = self.spans.lock().unwrap().get_mut(&id.into_u64()) {
|
|
144
|
+
if !s.fields.is_empty() && !f.rest.is_empty() {
|
|
145
|
+
s.fields.push(' ');
|
|
146
|
+
}
|
|
147
|
+
s.fields.push_str(&f.rest);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
fn record_follows_from(&self, _: &span::Id, _: &span::Id) {}
|
|
151
|
+
fn clone_span(&self, id: &span::Id) -> span::Id {
|
|
152
|
+
if let Some(s) = self.spans.lock().unwrap().get_mut(&id.into_u64()) {
|
|
153
|
+
s.refs += 1;
|
|
154
|
+
}
|
|
155
|
+
id.clone()
|
|
156
|
+
}
|
|
157
|
+
fn try_close(&self, id: span::Id) -> bool {
|
|
158
|
+
let mut spans = self.spans.lock().unwrap();
|
|
159
|
+
let k = id.into_u64();
|
|
160
|
+
if let Some(s) = spans.get_mut(&k) {
|
|
161
|
+
s.refs -= 1;
|
|
162
|
+
if s.refs == 0 {
|
|
163
|
+
spans.remove(&k);
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
false
|
|
168
|
+
}
|
|
169
|
+
fn enter(&self, id: &span::Id) {
|
|
170
|
+
self.stack.lock().unwrap().push(id.into_u64());
|
|
171
|
+
}
|
|
172
|
+
fn exit(&self, id: &span::Id) {
|
|
173
|
+
let mut st = self.stack.lock().unwrap();
|
|
174
|
+
if let Some(i) = st.iter().rposition(|x| *x == id.into_u64()) {
|
|
175
|
+
st.remove(i);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
fn event(&self, event: &Event<'_>) {
|
|
179
|
+
let mut f = Fields::default();
|
|
180
|
+
event.record(&mut f);
|
|
181
|
+
let mut line = String::new();
|
|
182
|
+
{
|
|
183
|
+
let spans = self.spans.lock().unwrap();
|
|
184
|
+
for id in self.stack.lock().unwrap().iter() {
|
|
185
|
+
if let Some(s) = spans.get(id) {
|
|
186
|
+
line.push_str(s.name);
|
|
187
|
+
if !s.fields.is_empty() {
|
|
188
|
+
let _ = write!(line, "{{{}}}", s.fields);
|
|
189
|
+
}
|
|
190
|
+
line.push(':');
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if !line.is_empty() {
|
|
195
|
+
line.push(' ');
|
|
196
|
+
}
|
|
197
|
+
line.push_str(&f.message);
|
|
198
|
+
if !f.rest.is_empty() {
|
|
199
|
+
if !f.message.is_empty() {
|
|
200
|
+
line.push(' ');
|
|
201
|
+
}
|
|
202
|
+
line.push_str(&f.rest);
|
|
203
|
+
}
|
|
204
|
+
sys::log_line(level(event.metadata().level()), &line);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
pub fn install() {
|
|
209
|
+
// Fails (harmlessly) when a global default subscriber already exists.
|
|
210
|
+
let _ = tracing_core::dispatcher::set_global_default(tracing_core::Dispatch::new(
|
|
211
|
+
HostSubscriber::default(),
|
|
212
|
+
));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/// Installs the enabled bridges unless the user already installed their own.
|
|
217
|
+
pub(crate) fn install() {
|
|
218
|
+
#[cfg(feature = "log")]
|
|
219
|
+
log_bridge::install();
|
|
220
|
+
#[cfg(feature = "tracing")]
|
|
221
|
+
tracing_bridge::install();
|
|
222
|
+
}
|