use crate::vfs::{File, OpenFlags, Vfs};
use crate::{Connection, Error, Value};
use alloc::boxed::Box;
use alloc::collections::BTreeMap;
use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use core::cell::RefCell;
use js_sys::{Array, Object, Reflect, Uint8Array};
use wasm_bindgen::prelude::*;
use web_sys::{FileSystemReadWriteOptions, FileSystemSyncAccessHandle};
fn value_to_js(v: &Value) -> JsValue {
match v {
Value::Null => JsValue::NULL,
Value::Integer(i) => {
if *i >= -(1i64 << 53) && *i <= (1i64 << 53) {
JsValue::from_f64(*i as f64)
} else {
JsValue::from(*i) }
}
Value::Real(f) => JsValue::from_f64(*f),
Value::Text(s) => JsValue::from_str(s),
Value::Blob(b) => Uint8Array::from(b.as_slice()).into(),
}
}
fn query_result_to_js(qr: &crate::QueryResult) -> Result<JsValue, JsValue> {
let obj = Object::new();
let cols = Array::new();
for c in &qr.columns {
cols.push(&JsValue::from_str(c));
}
Reflect::set(&obj, &JsValue::from_str("columns"), &cols)?;
let rows = Array::new();
for row in &qr.rows {
let jr = Array::new();
for v in row {
jr.push(&value_to_js(v));
}
rows.push(&jr);
}
Reflect::set(&obj, &JsValue::from_str("rows"), &rows)?;
Ok(obj.into())
}
fn to_js_err(e: Error) -> JsValue {
JsValue::from(js_sys::Error::new(&e.to_string()))
}
#[wasm_bindgen]
pub struct Database {
conn: Connection,
}
#[wasm_bindgen]
impl Database {
#[wasm_bindgen(constructor)]
pub fn new() -> Result<Database, JsValue> {
Connection::open_memory()
.map(|conn| Database { conn })
.map_err(to_js_err)
}
pub fn deserialize(bytes: &[u8]) -> Result<Database, JsValue> {
Connection::deserialize(bytes)
.map(|conn| Database { conn })
.map_err(to_js_err)
}
#[wasm_bindgen(js_name = openOpfs)]
pub fn open_opfs(files: &Object, path: &str, create: bool) -> Result<Database, JsValue> {
let vfs = OpfsVfs::from_js(files)?;
let conn = if create {
Connection::create_vfs(&vfs, path, 4096)
} else {
Connection::open_vfs(&vfs, path)
}
.map_err(to_js_err)?;
Ok(Database { conn })
}
pub fn exec(&mut self, sql: &str) -> Result<usize, JsValue> {
self.conn.execute(sql).map_err(to_js_err)
}
pub fn query(&self, sql: &str) -> Result<JsValue, JsValue> {
let qr = self.conn.query(sql).map_err(to_js_err)?;
query_result_to_js(&qr)
}
pub fn serialize(&self) -> Result<Vec<u8>, JsValue> {
self.conn.serialize().map_err(to_js_err)
}
}
struct OpfsFile {
handle: FileSystemSyncAccessHandle,
}
impl OpfsFile {
fn opts_at(offset: u64) -> FileSystemReadWriteOptions {
let o = FileSystemReadWriteOptions::new();
o.set_at(offset as f64);
o
}
}
impl File for OpfsFile {
fn read_exact_at(&self, buf: &mut [u8], offset: u64) -> crate::Result<()> {
let opts = Self::opts_at(offset);
let n = self
.handle
.read_with_u8_array_and_options(buf, &opts)
.map_err(|_| Error::Io("OPFS read failed".into()))? as usize;
if n < buf.len() {
for b in &mut buf[n..] {
*b = 0;
}
}
Ok(())
}
fn write_all_at(&mut self, buf: &[u8], offset: u64) -> crate::Result<()> {
let opts = Self::opts_at(offset);
let n = self
.handle
.write_with_u8_array_and_options(buf, &opts)
.map_err(|_| Error::Io("OPFS write failed".into()))? as usize;
if n != buf.len() {
return Err(Error::Io("OPFS short write".into()));
}
Ok(())
}
fn truncate(&mut self, size: u64) -> crate::Result<()> {
self.handle
.truncate_with_f64(size as f64)
.map_err(|_| Error::Io("OPFS truncate failed".into()))
}
fn sync(&mut self) -> crate::Result<()> {
self.handle
.flush()
.map_err(|_| Error::Io("OPFS flush failed".into()))
}
fn size(&self) -> crate::Result<u64> {
self.handle
.get_size()
.map(|s| s as u64)
.map_err(|_| Error::Io("OPFS getSize failed".into()))
}
}
struct OpfsEntry {
handle: FileSystemSyncAccessHandle,
deleted: bool,
}
struct OpfsVfs {
files: RefCell<BTreeMap<String, OpfsEntry>>,
}
impl OpfsVfs {
fn from_js(obj: &Object) -> Result<OpfsVfs, JsValue> {
let files = RefCell::new(BTreeMap::new());
let entries = Object::entries(obj);
for entry in entries.iter() {
let pair: Array = entry.into();
let key = pair.get(0).as_string().ok_or_else(|| {
JsValue::from(js_sys::Error::new("OPFS file map key must be a string"))
})?;
let handle: FileSystemSyncAccessHandle = pair.get(1).dyn_into().map_err(|_| {
JsValue::from(js_sys::Error::new(
"OPFS file map value must be a FileSystemSyncAccessHandle",
))
})?;
files.borrow_mut().insert(
key,
OpfsEntry {
handle,
deleted: false,
},
);
}
Ok(OpfsVfs { files })
}
}
impl Vfs for OpfsVfs {
fn open(&self, path: &str, flags: OpenFlags) -> crate::Result<Box<dyn File>> {
let mut files = self.files.borrow_mut();
match files.get_mut(path) {
Some(entry) => {
if entry.deleted {
if !flags.create {
return Err(Error::CantOpen(format!("no such file: {path}")));
}
entry
.handle
.truncate_with_f64(0.0)
.map_err(|_| Error::Io("OPFS truncate-on-create failed".into()))?;
entry.deleted = false;
}
Ok(Box::new(OpfsFile {
handle: entry.handle.clone(),
}))
}
None => Err(Error::CantOpen(format!(
"OPFS handle not registered for {path}"
))),
}
}
fn delete(&self, path: &str) -> crate::Result<()> {
if let Some(entry) = self.files.borrow_mut().get_mut(path) {
entry
.handle
.truncate_with_f64(0.0)
.map_err(|_| Error::Io("OPFS delete (truncate) failed".into()))?;
entry.deleted = true;
}
Ok(())
}
fn exists(&self, path: &str) -> crate::Result<bool> {
Ok(self
.files
.borrow()
.get(path)
.map(|e| !e.deleted)
.unwrap_or(false))
}
}