#[cfg(feature = "wasm")]
use wasm_bindgen::prelude::*;
#[cfg(feature = "wasm")]
use wasm_bindgen_futures::JsFuture;
#[cfg(feature = "wasm")]
use js_sys::{Array, Reflect, Uint8Array};
#[cfg(feature = "wasm")]
use web_sys::{FileSystemDirectoryHandle, FileSystemGetFileOptions};
#[cfg(feature = "wasm")]
use crate::wasm::WasmAgentDB;
#[cfg(feature = "wasm")]
async fn opfs_root() -> Result<FileSystemDirectoryHandle, JsValue> {
let window = web_sys::window()
.ok_or_else(|| JsValue::from_str("OPFS: no global `window` object — are you in a Worker?"))?;
let navigator = window.navigator();
let storage = navigator.storage();
let promise = storage.get_directory();
let handle = JsFuture::from(promise).await?;
handle
.dyn_into::<FileSystemDirectoryHandle>()
.map_err(|_| JsValue::from_str("OPFS: getDirectory() did not return a FileSystemDirectoryHandle"))
}
#[cfg(feature = "wasm")]
fn db_filename(name: &str) -> String {
format!("{}.agentdb", name)
}
#[cfg(feature = "wasm")]
async fn opfs_read_bytes(dir: &FileSystemDirectoryHandle, filename: &str) -> Result<Vec<u8>, JsValue> {
let opts = FileSystemGetFileOptions::new();
let get_promise = dir.get_file_handle_with_options(filename, &opts);
let file_handle_val = match JsFuture::from(get_promise).await {
Ok(v) => v,
Err(err) => {
let name_prop = Reflect::get(&err, &JsValue::from_str("name"))
.unwrap_or_default();
if name_prop.as_string().as_deref() == Some("NotFoundError") {
return Ok(Vec::new());
}
return Err(err);
}
};
let file_handle = file_handle_val
.dyn_into::<web_sys::FileSystemFileHandle>()
.map_err(|_| JsValue::from_str("OPFS: getFileHandle() did not return a FileSystemFileHandle"))?;
let file_val = JsFuture::from(file_handle.get_file()).await?;
let file = file_val
.dyn_into::<web_sys::File>()
.map_err(|_| JsValue::from_str("OPFS: getFile() did not return a File"))?;
let ab_val = JsFuture::from(file.array_buffer()).await?;
let uint8 = Uint8Array::new(&ab_val);
Ok(uint8.to_vec())
}
#[cfg(feature = "wasm")]
async fn opfs_write_bytes(
dir: &FileSystemDirectoryHandle,
filename: &str,
data: &[u8],
) -> Result<(), JsValue> {
let opts = FileSystemGetFileOptions::new();
opts.set_create(true);
let file_handle_val =
JsFuture::from(dir.get_file_handle_with_options(filename, &opts)).await?;
let file_handle = file_handle_val
.dyn_into::<web_sys::FileSystemFileHandle>()
.map_err(|_| {
JsValue::from_str(
"OPFS: getFileHandle(create=true) did not return a FileSystemFileHandle",
)
})?;
let writable_val = JsFuture::from(file_handle.create_writable()).await?;
let writable = writable_val
.dyn_into::<web_sys::FileSystemWritableFileStream>()
.map_err(|_| {
JsValue::from_str(
"OPFS: createWritable() did not return a FileSystemWritableFileStream",
)
})?;
JsFuture::from(writable.write_with_u8_array(data)?).await?;
JsFuture::from(writable.close()).await?;
Ok(())
}
#[cfg(feature = "wasm")]
const SQLITE_DESERIALIZE_FREEONCLOSE: u32 = 0x0001;
#[cfg(feature = "wasm")]
const SQLITE_DESERIALIZE_RESIZEABLE: u32 = 0x0002;
#[cfg(feature = "wasm")]
pub(crate) fn sqlite_serialize(conn: &rusqlite::Connection) -> Result<Vec<u8>, JsValue> {
use libsqlite3_sys as ffi;
use std::ffi::CString;
unsafe {
let db_ptr = conn.handle();
let schema = CString::new("main").expect("CString: no NUL in \"main\"");
let mut size: ffi::sqlite3_int64 = 0;
let buf_ptr = ffi::sqlite3_serialize(db_ptr, schema.as_ptr(), &mut size, 0);
if buf_ptr.is_null() {
return Err(JsValue::from_str(
"sqlite3_serialize returned NULL (out of memory?)",
));
}
let len = size as usize;
let bytes = std::slice::from_raw_parts(buf_ptr, len).to_vec();
ffi::sqlite3_free(buf_ptr as *mut std::ffi::c_void);
Ok(bytes)
}
}
#[cfg(feature = "wasm")]
pub(crate) fn sqlite_deserialize(
conn: &rusqlite::Connection,
bytes: &[u8],
) -> Result<(), JsValue> {
use libsqlite3_sys as ffi;
use std::ffi::CString;
if bytes.is_empty() {
return Ok(());
}
unsafe {
let db_ptr = conn.handle();
let schema = CString::new("main").unwrap();
let len = bytes.len() as ffi::sqlite3_int64;
let buf = ffi::sqlite3_malloc64(bytes.len() as u64) as *mut u8;
if buf.is_null() {
return Err(JsValue::from_str("sqlite3_malloc64 failed (out of memory)"));
}
std::ptr::copy_nonoverlapping(bytes.as_ptr(), buf, bytes.len());
let rc = ffi::sqlite3_deserialize(
db_ptr,
schema.as_ptr(),
buf,
len,
len,
(SQLITE_DESERIALIZE_FREEONCLOSE | SQLITE_DESERIALIZE_RESIZEABLE) as u32,
);
if rc != ffi::SQLITE_OK {
return Err(JsValue::from_str(&format!(
"sqlite3_deserialize failed with code {}",
rc
)));
}
Ok(())
}
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub async fn open_persistent(name: &str) -> Result<WasmAgentDB, JsValue> {
let root = opfs_root().await?;
let filename = db_filename(name);
let bytes = opfs_read_bytes(&root, &filename).await?;
let db = WasmAgentDB::open_memory()?;
if !bytes.is_empty() {
db.deserialize_bytes(&bytes)?;
}
Ok(db)
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub async fn save(db: &WasmAgentDB, name: &str) -> Result<(), JsValue> {
let root = opfs_root().await?;
let filename = db_filename(name);
let bytes = db.serialize_bytes()?;
opfs_write_bytes(&root, &filename, &bytes).await
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub async fn delete_persistent(name: &str) -> Result<(), JsValue> {
let root = opfs_root().await?;
let filename = db_filename(name);
let promise = root.remove_entry(&filename);
match JsFuture::from(promise).await {
Ok(_) => Ok(()),
Err(err) => {
let name_prop = Reflect::get(&err, &JsValue::from_str("name"))
.unwrap_or_default();
if name_prop.as_string().as_deref() == Some("NotFoundError") {
Ok(())
} else {
Err(err)
}
}
}
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub async fn list_databases() -> Result<Vec<String>, JsValue> {
let root = opfs_root().await?;
let entries_iter = js_sys::Reflect::get(&root, &JsValue::from_str("entries"))
.map_err(|_| JsValue::from_str("OPFS: FileSystemDirectoryHandle.entries() not available"))?;
let entries_fn = entries_iter
.dyn_ref::<js_sys::Function>()
.ok_or_else(|| JsValue::from_str("OPFS: entries is not a function"))?;
let iterator = entries_fn.call0(&root)?;
let next_fn_val = Reflect::get(&iterator, &JsValue::from_str("next"))?;
let next_fn = next_fn_val
.dyn_ref::<js_sys::Function>()
.ok_or_else(|| JsValue::from_str("OPFS: iterator.next is not a function"))?;
let mut names = Vec::new();
loop {
let next_promise_val = next_fn.call0(&iterator)?;
let next_result = if let Some(promise) = next_promise_val.dyn_ref::<js_sys::Promise>() {
JsFuture::from(promise.clone()).await?
} else {
next_promise_val
};
let done = Reflect::get(&next_result, &JsValue::from_str("done"))?;
if done.as_bool().unwrap_or(false) {
break;
}
let value = Reflect::get(&next_result, &JsValue::from_str("value"))?;
let pair = value
.dyn_ref::<Array>()
.ok_or_else(|| JsValue::from_str("OPFS: iterator value is not an Array"))?;
if let Some(entry_name) = pair.get(0).as_string() {
if entry_name.ends_with(".agentdb") {
let logical_name = entry_name
.strip_suffix(".agentdb")
.unwrap_or(&entry_name)
.to_string();
names.push(logical_name);
}
}
}
Ok(names)
}
#[cfg(feature = "wasm")]
pub struct OpfsVfs {
pub vfs_name: String,
}
#[cfg(feature = "wasm")]
impl OpfsVfs {
pub fn new(vfs_name: &str) -> Self {
OpfsVfs {
vfs_name: vfs_name.to_string(),
}
}
}