use std::cell::RefCell;
use std::collections::HashMap;
use wasm_bindgen::{JsCast, JsValue};
pub(crate) const RECEIPTS_FILE: &str = ".lh_receipts.jsonl";
const RECEIPTS_CAP: usize = 256;
thread_local! {
static MODULE_HASHES: RefCell<HashMap<i32, (String, [u8; 32])>> =
RefCell::new(HashMap::new());
}
pub(crate) fn note_module(uid: i32, name: &str, bytes: &[u8]) {
let hash = crate::registry::keccak32(bytes);
MODULE_HASHES.with(|m| {
m.borrow_mut().insert(uid, (name.to_string(), hash));
});
}
pub(crate) fn handle_frame_calls(data: &JsValue) {
use js_sys::Reflect;
let Ok(calls) = Reflect::get(data, &JsValue::from_str("calls")) else { return };
if calls.is_undefined() || calls.is_null() {
return;
}
let Ok(arr) = calls.dyn_into::<js_sys::Array>() else { return };
let mut lines: Vec<String> = Vec::new();
for entry in arr.iter() {
if let Some(line) = record_to_line(&entry) {
lines.push(line);
}
}
if let Ok(dropped) = Reflect::get(data, &JsValue::from_str("callsDropped")) {
if let Some(n) = dropped.as_f64() {
if n > 0.0 {
web_sys::console::warn_1(&JsValue::from_str(&format!(
"receipts: {n} call record(s) dropped this frame (per-frame cap)"
)));
}
}
}
if lines.is_empty() {
return;
}
wasm_bindgen_futures::spawn_local(async move {
let fs = crate::app::shared_opfs();
let existing = fs
.read(RECEIPTS_FILE)
.await
.ok()
.and_then(|b| String::from_utf8(b).ok())
.unwrap_or_default();
let mut blob = existing;
for line in &lines {
blob = crate::receipt::ring_append(&blob, line, RECEIPTS_CAP);
}
if let Err(e) = fs.write_atomic(RECEIPTS_FILE, blob.as_bytes()).await {
web_sys::console::warn_1(&JsValue::from_str(&format!("receipts: write: {e}")));
}
});
}
fn record_to_line(entry: &JsValue) -> Option<String> {
use js_sys::Reflect;
let get = |k: &str| Reflect::get(entry, &JsValue::from_str(k)).ok();
let uid = get("uid")?.as_f64()? as i32;
let export = get("fn")?.as_string()?;
let status_code = get("status")?.as_f64()? as i32;
let status = crate::receipt::CallStatus::from_compose_status(status_code)?;
let (name, module_keccak) = MODULE_HASHES.with(|m| m.borrow().get(&uid).cloned())?;
let args: Vec<i32> = get("args")
.and_then(|v| v.dyn_into::<js_sys::Array>().ok())
.map(|a| a.iter().filter_map(|x| x.as_f64().map(|f| f as i32)).collect())
.unwrap_or_default();
let result = get("result").and_then(|v| v.as_f64()).map(|f| f as i32);
let receipt = crate::receipt::Receipt::call_on_module(
module_keccak,
crate::receipt::CallRecord { export, args, result, status, fuel: 0 },
);
let mut json = receipt.to_json();
json["module"] = serde_json::Value::String(name);
Some(json.to_string())
}