Add Wasmtime's component-model-async back.

This commit is contained in:
Sebastian Jeltsch 2026-01-23 12:16:33 +01:00
parent 369785a7f1
commit 2c8cfdf9bd
9 changed files with 78 additions and 96 deletions

View file

@ -108,7 +108,7 @@ trailbase-wasm-common = { path = "crates/wasm-runtime-common", version = "0.2.0"
trailbase-wasm-runtime-host = { path = "crates/wasm-runtime-host", version = "0.1.0" }
ts-rs = { version = "11", features = ["uuid-impl", "serde-json-impl", "indexmap-impl"] }
uuid = { version = "1", default-features = false, features = ["std", "v4", "v7", "serde"] }
wasmtime = { version = "41.0.0", features = ["winch"] }
wasmtime = { version = "41.0.0", features = ["winch", "component-model-async"] }
wasmtime-wasi = { version = "41.0.0", default-features = false, features = [] }
wasmtime-wasi-http = { version = "41.0.0", features = [] }
wasmtime-wasi-http = { version = "41.0.0", features = ["component-model-async"] }
wasmtime-wasi-io = { version = "41.0.0", features = [] }

View file

@ -3,6 +3,7 @@ name = "trailbase-wasm"
version = "0.5.0"
edition = "2024"
license = "OSL-3.0"
rust-version = "1.93"
description = "WASM runtime for the TrailBase framework"
homepage = "https://trailbase.io"
repository = "https://github.com/trailbaseio/trailbase"

View file

@ -1,9 +1,8 @@
use trailbase_sqlvalue::{Blob, DecodeError, SqlValue};
use wstd::http::body::IntoBody;
use wstd::http::{Client, Request};
use crate::wit::trailbase::database::sqlite::{
tx_begin, tx_commit, tx_execute, tx_query, tx_rollback,
execute as call_execute, query as call_query, tx_begin, tx_commit, tx_execute, tx_query,
tx_rollback,
};
pub use crate::wit::trailbase::database::sqlite::{TxError, Value};
@ -62,84 +61,18 @@ pub async fn query(
query: impl std::string::ToString,
params: impl Into<Vec<Value>>,
) -> Result<Vec<Vec<Value>>, Error> {
let r = SqliteRequest {
query: query.to_string(),
params: params.into().into_iter().map(to_sql_value).collect(),
};
let request = Request::builder()
.uri("http://__sqlite/query")
.method("POST")
.body(
serde_json::to_vec(&r)
.map_err(|_| Error::UnexpectedType)?
.into_body(),
)
.map_err(|err| Error::Other(err.to_string()))?;
let client = Client::new();
let (_parts, mut body) = client
.send(request)
return call_query(query.to_string(), params.into())
.await
.map_err(|err| Error::Other(err.to_string()))?
.into_parts();
let bytes = body
.bytes()
.await
.map_err(|err| Error::Other(err.to_string()))?;
return match serde_json::from_slice(&bytes) {
Ok(SqliteResponse::Query { rows }) => Ok(
rows
.into_iter()
.map(|row| {
row
.into_iter()
.map(from_sql_value)
.collect::<Result<Vec<_>, _>>()
})
.collect::<Result<Vec<_>, _>>()?,
),
Ok(_) => Err(Error::UnexpectedType),
Err(err) => Err(Error::Other(err.to_string())),
};
.map_err(|err| Error::Other(err.to_string()));
}
pub async fn execute(
query: impl std::string::ToString,
params: impl Into<Vec<Value>>,
) -> Result<usize, Error> {
let r = SqliteRequest {
query: query.to_string(),
params: params.into().into_iter().map(to_sql_value).collect(),
};
let request = Request::builder()
.uri("http://__sqlite/execute")
.method("POST")
.body(
serde_json::to_vec(&r)
.map_err(|_| Error::UnexpectedType)?
.into_body(),
)
.map_err(|err| Error::Other(err.to_string()))?;
let client = Client::new();
let (_parts, mut body) = client
.send(request)
) -> Result<u64, Error> {
return call_execute(query.to_string(), params.into())
.await
.map_err(|err| Error::Other(err.to_string()))?
.into_parts();
let bytes = body
.bytes()
.await
.map_err(|err| Error::Other(err.to_string()))?;
return match serde_json::from_slice(&bytes) {
Ok(SqliteResponse::Execute { rows_affected }) => Ok(rows_affected),
Ok(_) => Err(Error::UnexpectedType),
Err(err) => Err(Error::Other(err.to_string())),
};
.map_err(|err| Error::Other(err.to_string()));
}
// fn from_json_value(value: serde_json::Value) -> Result<Value, Error> {
@ -165,15 +98,15 @@ pub async fn execute(
// };
// }
fn from_sql_value(value: SqlValue) -> Result<Value, DecodeError> {
return match value {
SqlValue::Null => Ok(Value::Null),
SqlValue::Integer(v) => Ok(Value::Integer(v)),
SqlValue::Real(v) => Ok(Value::Real(v)),
SqlValue::Text(v) => Ok(Value::Text(v)),
SqlValue::Blob(v) => Ok(Value::Blob(v.into_bytes()?)),
};
}
// fn from_sql_value(value: SqlValue) -> Result<Value, DecodeError> {
// return match value {
// SqlValue::Null => Ok(Value::Null),
// SqlValue::Integer(v) => Ok(Value::Integer(v)),
// SqlValue::Real(v) => Ok(Value::Real(v)),
// SqlValue::Text(v) => Ok(Value::Text(v)),
// SqlValue::Blob(v) => Ok(Value::Blob(v.into_bytes()?)),
// };
// }
// #[derive(Serialize)]
// struct Blob {

View file

@ -25,7 +25,7 @@ world interfaces {
// TrailBase's interfaces:
@since(version = 0.1.0)
include trailbase:database/interfaces@0.1.0;
include trailbase:database/interfaces@0.1.1;
@since(version = 0.1.0)
export init-endpoint;
@ -38,7 +38,7 @@ world interfaces {
world init {
// TrailBase's interfaces:
@since(version = 0.1.0)
include trailbase:database/interfaces@0.1.0;
include trailbase:database/interfaces@0.1.1;
@since(version = 0.1.0)
export init-endpoint;

View file

@ -1,4 +1,4 @@
package trailbase:database@0.1.0;
package trailbase:database@0.1.1;
interface sqlite {
// WARNING: Evolving a variant currently breaks the ABI:
@ -15,12 +15,10 @@ interface sqlite {
real(f64),
}
// NOTE: Ideally, we'd use these but they can currently block guests, w/o a
// better non-blocking event loop.
// @since(version = 0.1.0)
// execute: func(query: string, params: list<value>) -> result<u64, tx-error>;
// @since(version = 0.1.0)
// query: func(query: string, params: list<value>) -> result<list<list<value>>, tx-error>;
@since(version = 0.1.1)
execute: async func(query: string, params: list<value>) -> result<u64, tx-error>;
@since(version = 0.1.1)
query: async func(query: string, params: list<value>) -> result<list<list<value>>, tx-error>;
// However, transactions have to be sync.
@since(version = 0.1.0)

View file

@ -3,6 +3,7 @@ name = "trailbase-wasm-runtime-host"
version = "0.1.0"
edition = "2024"
license = "OSL-3.0"
rust-version = "1.93"
description = "WASM runtime for the TrailBase framework"
homepage = "https://trailbase.io"
exclude = [

View file

@ -5,7 +5,7 @@ use std::sync::Arc;
use trailbase_sqlite::{Params, Rows};
use trailbase_wasi_keyvalue::WasiKeyValueCtx;
use wasmtime::Result;
use wasmtime::component::{HasData, ResourceTable};
use wasmtime::component::{Accessor, HasData, ResourceTable};
use wasmtime_wasi::{WasiCtx, WasiCtxView, WasiView};
use wasmtime_wasi_http::{WasiHttpCtx, WasiHttpView};
use wasmtime_wasi_io::IoView;
@ -40,6 +40,7 @@ wasmtime::component::bindgen!({
// to return traps from generated functions.
imports: {
"trailbase:database/sqlite.tx-begin": async,
"trailbase:database/sqlite.execute": async,
},
exports: {
default: async | store | task_exit,
@ -145,6 +146,52 @@ impl HasData for State {
type Data<'a> = &'a mut State;
}
impl self::trailbase::database::sqlite::HostWithStore for State {
async fn execute<T>(
accessor: &Accessor<T, Self>,
query: String,
params: Vec<Value>,
) -> Result<u64, TxError> {
let Some(conn) = accessor.with(|mut a| a.get().shared.conn.clone()) else {
return Err(TxError::Other("missing conn".to_string()));
};
let params: Vec<_> = params.into_iter().map(to_sqlite_value).collect();
return conn
.execute(query, params)
.await
.map_err(|err| TxError::Other(err.to_string()))
.map(|v| v as u64);
}
async fn query<T>(
accessor: &Accessor<T, Self>,
query: String,
params: Vec<Value>,
) -> Result<Vec<Vec<Value>>, TxError> {
let Some(conn) = accessor.with(|mut a| a.get().shared.conn.clone()) else {
return Err(TxError::Other("missing conn".to_string()));
};
let params: Vec<_> = params.into_iter().map(to_sqlite_value).collect();
let rows = conn
.write_query_rows(query, params)
.await
.map_err(|err| TxError::Other(err.to_string()))?;
let values: Vec<_> = rows
.into_iter()
.map(|trailbase_sqlite::Row(row, _col)| {
return row.into_iter().map(from_sqlite_value).collect::<Vec<_>>();
})
.collect();
return Ok(values);
}
}
impl self::trailbase::database::sqlite::Host for State {
fn tx_begin(&mut self) -> impl Future<Output = Result<(), TxError>> + Send {
async fn begin(

View file

@ -426,12 +426,14 @@ fn build_config(cache: Option<wasmtime::Cache>, use_winch: bool) -> Config {
// calling synchronous bindings will panic.
config.async_support(true);
config.wasm_component_model(true);
config.wasm_component_model_async(true);
// config.wasm_backtrace_details(wasmtime::WasmBacktraceDetails::Enable);
// Compilation settings.
config.cache(cache);
if use_winch {
// FIXME: ASYNC component model doesn't currently support winch;
if false && use_winch {
config.strategy(wasmtime::Strategy::Winch);
} else {
config.strategy(wasmtime::Strategy::Cranelift);