use std::sync::Arc;
use super::{
bindings::fraiseql::host::{context, io, logging},
host_bridge::DynHostContext,
};
use crate::types::{EventPayload, LogEntry, LogLevel, ResourceLimits};
pub struct StoreData {
pub event_payload: EventPayload,
pub host_context: Option<Arc<dyn DynHostContext>>,
pub logs: Vec<LogEntry>,
pub limits: ResourceLimits,
pub memory_peak_bytes: u64,
pub memory_current_bytes: u64,
wasi_ctx: wasmtime_wasi::WasiCtx,
wasi_table: wasmtime::component::ResourceTable,
}
impl StoreData {
#[must_use]
pub fn new(event_payload: EventPayload, limits: ResourceLimits) -> Self {
Self {
event_payload,
host_context: None,
logs: Vec::new(),
limits,
memory_peak_bytes: 0,
memory_current_bytes: 0,
wasi_ctx: wasmtime_wasi::WasiCtxBuilder::new().build(),
wasi_table: wasmtime::component::ResourceTable::new(),
}
}
pub fn set_host_context(&mut self, context: Arc<dyn DynHostContext>) {
self.host_context = Some(context);
}
fn require_host_context(&self) -> Result<&dyn DynHostContext, String> {
self.host_context
.as_deref()
.ok_or_else(|| "host context not available".to_string())
}
pub fn log_message(&mut self, level: LogLevel, message: &str) {
if self.logs.len() < self.limits.max_log_entries {
let entry = LogEntry {
level,
message: message.to_string(),
timestamp: chrono::Utc::now(),
};
self.logs.push(entry);
match level {
LogLevel::Debug => tracing::debug!("{}", message),
LogLevel::Info => tracing::info!("{}", message),
LogLevel::Warn => tracing::warn!("{}", message),
LogLevel::Error => tracing::error!("{}", message),
}
}
}
#[must_use]
pub const fn event_payload_ref(&self) -> &EventPayload {
&self.event_payload
}
pub fn get_event_payload_json(&self) -> wasmtime::Result<String> {
serde_json::to_string(&self.event_payload).map_err(|e| wasmtime::Error::msg(e.to_string()))
}
}
impl wasmtime_wasi::WasiView for StoreData {
fn ctx(&mut self) -> wasmtime_wasi::WasiCtxView<'_> {
wasmtime_wasi::WasiCtxView {
ctx: &mut self.wasi_ctx,
table: &mut self.wasi_table,
}
}
}
const fn map_log_level(level: logging::LogLevel) -> LogLevel {
match level {
logging::LogLevel::Debug => LogLevel::Debug,
logging::LogLevel::Info => LogLevel::Info,
logging::LogLevel::Warn => LogLevel::Warn,
logging::LogLevel::Error => LogLevel::Error,
}
}
impl logging::Host for StoreData {
async fn log(&mut self, level: logging::LogLevel, message: String) {
self.log_message(map_log_level(level), &message);
}
}
impl context::Host for StoreData {
async fn get_event_payload(&mut self) -> String {
self.get_event_payload_json()
.unwrap_or_else(|e| format!("{{\"error\": \"{e}\"}}",))
}
async fn get_auth_context(&mut self) -> Result<String, String> {
let host = self.require_host_context()?;
let value = host.auth_context().map_err(|e| e.to_string())?;
serde_json::to_string(&value).map_err(|e| e.to_string())
}
async fn get_env_var(&mut self, name: String) -> Option<String> {
let Ok(host) = self.require_host_context() else {
return None;
};
host.env_var(&name).ok().flatten()
}
async fn get_idempotency_token(&mut self) -> Option<String> {
let Ok(host) = self.require_host_context() else {
return None;
};
host.idempotency_token()
}
}
#[allow(clippy::needless_pass_by_value)] fn host_err_string(error: fraiseql_error::FraiseQLError) -> String {
if error.is_client_error() {
format!("{} {error}", crate::types::PERMANENT_ERROR_MARKER)
} else {
error.to_string()
}
}
impl io::Host for StoreData {
async fn query(&mut self, graphql: String, variables: String) -> Result<String, String> {
let host = self.require_host_context()?;
let vars: serde_json::Value =
serde_json::from_str(&variables).map_err(|e| e.to_string())?;
let result = host.query(&graphql, vars).await.map_err(host_err_string)?;
serde_json::to_string(&result).map_err(|e| e.to_string())
}
async fn sql_query(&mut self, sql: String, params: String) -> Result<String, String> {
let host = self.require_host_context()?;
let params_vec: Vec<serde_json::Value> =
serde_json::from_str(¶ms).map_err(|e| e.to_string())?;
let result = host.sql_query(&sql, ¶ms_vec).await.map_err(host_err_string)?;
serde_json::to_string(&result).map_err(|e| e.to_string())
}
async fn http_request(
&mut self,
method: String,
url: String,
headers: Vec<(String, String)>,
body: Option<Vec<u8>>,
) -> Result<io::HttpResponse, String> {
let host = self.require_host_context()?;
let response = host
.http_request(&method, &url, &headers, body.as_deref())
.await
.map_err(host_err_string)?;
Ok(io::HttpResponse {
status: response.status,
headers: response.headers,
body: response.body,
})
}
async fn storage_get(&mut self, bucket: String, key: String) -> Result<Vec<u8>, String> {
let host = self.require_host_context()?;
host.storage_get(&bucket, &key).await.map_err(host_err_string)
}
async fn storage_put(
&mut self,
bucket: String,
key: String,
body: Vec<u8>,
content_type: String,
) -> Result<(), String> {
let host = self.require_host_context()?;
host.storage_put(&bucket, &key, &body, &content_type)
.await
.map_err(host_err_string)
}
async fn send_email(&mut self, request: String) -> Result<String, String> {
let host = self.require_host_context()?;
let req: crate::outbound::SendEmailRequest =
serde_json::from_str(&request).map_err(|e| e.to_string())?;
let response = host.send_email(&req).await.map_err(host_err_string)?;
serde_json::to_string(&response).map_err(|e| e.to_string())
}
}
impl wasmtime::ResourceLimiter for StoreData {
fn memory_growing(
&mut self,
_current: usize,
desired: usize,
_maximum: Option<usize>,
) -> wasmtime::Result<bool> {
self.memory_current_bytes = desired as u64;
if self.memory_current_bytes > self.memory_peak_bytes {
self.memory_peak_bytes = self.memory_current_bytes;
}
let max = self.limits.max_memory_bytes;
if self.memory_current_bytes > max {
return Err(wasmtime::Error::msg(format!(
"Memory limit exceeded: {} > {max}",
self.memory_current_bytes
)));
}
Ok(true)
}
fn table_growing(
&mut self,
_current: usize,
_desired: usize,
_maximum: Option<usize>,
) -> wasmtime::Result<bool> {
Ok(true)
}
}
#[cfg(test)]
mod tests;