diff --git a/bindings/wasm/lib.rs b/bindings/wasm/lib.rs new file mode 100644 index 000000000..23ddebf68 --- /dev/null +++ b/bindings/wasm/lib.rs @@ -0,0 +1,457 @@ +#[cfg(all(feature = "web", feature = "nodejs"))] +compile_error!("Features 'web' and 'nodejs' cannot be enabled at the same time"); + +use js_sys::{Array, Object}; +use std::cell::RefCell; +use std::sync::Arc; +use turso_core::{Clock, Instant, OpenFlags, Result}; +use wasm_bindgen::prelude::*; + +#[allow(dead_code)] +#[wasm_bindgen] +pub struct Database { + db: Arc, + conn: Arc, +} + +#[allow(clippy::arc_with_non_send_sync)] +#[wasm_bindgen] +impl Database { + #[wasm_bindgen(constructor)] + pub fn new(path: &str) -> Database { + let io: Arc = Arc::new(PlatformIO { vfs: VFS::new() }); + let file = io.open_file(path, OpenFlags::Create, false).unwrap(); + let db_file = Arc::new(DatabaseFile::new(file)); + let db = turso_core::Database::open(io, path, db_file, false, false).unwrap(); + let conn = db.connect().unwrap(); + Database { db, conn } + } + + #[wasm_bindgen] + pub fn exec(&self, _sql: &str) { + self.conn.execute(_sql).unwrap(); + } + + #[wasm_bindgen] + pub fn prepare(&self, _sql: &str) -> Statement { + let stmt = self.conn.prepare(_sql).unwrap(); + Statement::new(RefCell::new(stmt), false) + } +} + +#[wasm_bindgen] +pub struct RowIterator { + inner: RefCell, +} + +#[wasm_bindgen] +impl RowIterator { + fn new(inner: RefCell) -> Self { + Self { inner } + } + + #[wasm_bindgen] + #[allow(clippy::should_implement_trait)] + pub fn next(&mut self) -> JsValue { + let mut stmt = self.inner.borrow_mut(); + match stmt.step() { + Ok(turso_core::StepResult::Row) => { + let row = stmt.row().unwrap(); + let row_array = Array::new(); + for value in row.get_values() { + let value = to_js_value(value); + row_array.push(&value); + } + JsValue::from(row_array) + } + Ok(turso_core::StepResult::IO) => JsValue::UNDEFINED, + Ok(turso_core::StepResult::Done) | Ok(turso_core::StepResult::Interrupt) => { + JsValue::UNDEFINED + } + + Ok(turso_core::StepResult::Busy) => JsValue::UNDEFINED, + Err(e) => panic!("Error: {e:?}"), + } + } +} + +#[wasm_bindgen] +pub struct Statement { + inner: RefCell, + raw: bool, +} + +#[wasm_bindgen] +impl Statement { + fn new(inner: RefCell, raw: bool) -> Self { + Self { inner, raw } + } + + #[wasm_bindgen] + pub fn raw(mut self, toggle: Option) -> Self { + self.raw = toggle.unwrap_or(true); + self + } + + pub fn get(&self) -> JsValue { + let mut stmt = self.inner.borrow_mut(); + match stmt.step() { + Ok(turso_core::StepResult::Row) => { + let row = stmt.row().unwrap(); + let row_array = js_sys::Array::new(); + for value in row.get_values() { + let value = to_js_value(value); + row_array.push(&value); + } + JsValue::from(row_array) + } + + Ok(turso_core::StepResult::IO) + | Ok(turso_core::StepResult::Done) + | Ok(turso_core::StepResult::Interrupt) + | Ok(turso_core::StepResult::Busy) => JsValue::UNDEFINED, + Err(e) => panic!("Error: {e:?}"), + } + } + + pub fn all(&self) -> js_sys::Array { + let array = js_sys::Array::new(); + loop { + let mut stmt = self.inner.borrow_mut(); + match stmt.step() { + Ok(turso_core::StepResult::Row) => { + let row = stmt.row().unwrap(); + let row_array = js_sys::Array::new(); + for value in row.get_values() { + let value = to_js_value(value); + row_array.push(&value); + } + array.push(&row_array); + } + Ok(turso_core::StepResult::IO) => {} + Ok(turso_core::StepResult::Interrupt) => break, + Ok(turso_core::StepResult::Done) => break, + Ok(turso_core::StepResult::Busy) => break, + Err(e) => panic!("Error: {e:?}"), + } + } + array + } + + #[wasm_bindgen] + pub fn iterate(self) -> JsValue { + let iterator = RowIterator::new(self.inner); + let iterator_obj = Object::new(); + + // Define the next method that will be called by JavaScript + let next_fn = js_sys::Function::new_with_args( + "", + "const value = this.iterator.next(); + const done = value === undefined; + return { + value, + done + };", + ); + + js_sys::Reflect::set(&iterator_obj, &JsValue::from_str("next"), &next_fn).unwrap(); + + js_sys::Reflect::set( + &iterator_obj, + &JsValue::from_str("iterator"), + &JsValue::from(iterator), + ) + .unwrap(); + + let symbol_iterator = js_sys::Function::new_no_args("return this;"); + js_sys::Reflect::set(&iterator_obj, &js_sys::Symbol::iterator(), &symbol_iterator).unwrap(); + + JsValue::from(iterator_obj) + } +} + +fn to_js_value(value: &turso_core::Value) -> JsValue { + match value { + turso_core::Value::Null => JsValue::null(), + turso_core::Value::Integer(i) => { + let i = *i; + if i >= i32::MIN as i64 && i <= i32::MAX as i64 { + JsValue::from(i as i32) + } else { + JsValue::from(i) + } + } + turso_core::Value::Float(f) => JsValue::from(*f), + turso_core::Value::Text(t) => JsValue::from_str(t.as_str()), + turso_core::Value::Blob(b) => js_sys::Uint8Array::from(b.as_slice()).into(), + } +} + +pub struct File { + vfs: VFS, + fd: i32, +} + +unsafe impl Send for File {} +unsafe impl Sync for File {} + +#[allow(dead_code)] +impl File { + fn new(vfs: VFS, fd: i32) -> Self { + Self { vfs, fd } + } +} + +impl turso_core::File for File { + fn lock_file(&self, _exclusive: bool) -> Result<()> { + // TODO + Ok(()) + } + + fn unlock_file(&self) -> Result<()> { + // TODO + Ok(()) + } + + fn pread( + &self, + pos: usize, + c: Arc, + ) -> Result> { + let r = match c.completion_type { + turso_core::CompletionType::Read(ref r) => r, + _ => unreachable!(), + }; + let nr = { + let mut buf = r.buf_mut(); + let buf: &mut [u8] = buf.as_mut_slice(); + self.vfs.pread(self.fd, buf, pos) + }; + r.complete(nr); + #[allow(clippy::arc_with_non_send_sync)] + Ok(c) + } + + fn pwrite( + &self, + pos: usize, + buffer: Arc>, + c: Arc, + ) -> Result> { + let w = match c.completion_type { + turso_core::CompletionType::Write(ref w) => w, + _ => unreachable!(), + }; + let buf = buffer.borrow(); + let buf: &[u8] = buf.as_slice(); + self.vfs.pwrite(self.fd, buf, pos); + w.complete(buf.len() as i32); + #[allow(clippy::arc_with_non_send_sync)] + Ok(c) + } + + fn sync(&self, c: Arc) -> Result> { + self.vfs.sync(self.fd); + c.complete(0); + #[allow(clippy::arc_with_non_send_sync)] + Ok(c) + } + + fn size(&self) -> Result { + Ok(self.vfs.size(self.fd)) + } + + fn truncate(&self, len: u64, c: turso_core::Completion) -> Result> { + self.vfs.truncate(self.fd, len as usize); + c.complete(0); + #[allow(clippy::arc_with_non_send_sync)] + Ok(Arc::new(c)) + } +} + +pub struct PlatformIO { + vfs: VFS, +} +unsafe impl Send for PlatformIO {} +unsafe impl Sync for PlatformIO {} + +impl Clock for PlatformIO { + fn now(&self) -> Instant { + let date = Date::new(); + let ms_since_epoch = date.getTime(); + + Instant { + secs: (ms_since_epoch / 1000.0) as i64, + micros: ((ms_since_epoch % 1000.0) * 1000.0) as u32, + } + } +} + +impl turso_core::IO for PlatformIO { + fn open_file( + &self, + path: &str, + _flags: OpenFlags, + _direct: bool, + ) -> Result> { + let fd = self.vfs.open(path, "a+"); + Ok(Arc::new(File { + vfs: VFS::new(), + fd, + })) + } + + fn wait_for_completion(&self, c: Arc) -> Result<()> { + while !c.is_completed() { + self.run_once()?; + } + Ok(()) + } + + fn run_once(&self) -> Result<()> { + Ok(()) + } + + fn generate_random_number(&self) -> i64 { + let mut buf = [0u8; 8]; + getrandom::getrandom(&mut buf).unwrap(); + i64::from_ne_bytes(buf) + } + + fn get_memory_io(&self) -> Arc { + Arc::new(turso_core::MemoryIO::new()) + } +} + +#[wasm_bindgen] +extern "C" { + type Date; + + #[wasm_bindgen(constructor)] + fn new() -> Date; + + #[wasm_bindgen(method, getter)] + fn toISOString(this: &Date) -> String; + + #[wasm_bindgen(method)] + fn getTime(this: &Date) -> f64; +} + +pub struct DatabaseFile { + file: Arc, +} + +unsafe impl Send for DatabaseFile {} +unsafe impl Sync for DatabaseFile {} + +impl DatabaseFile { + pub fn new(file: Arc) -> Self { + Self { file } + } +} + +impl turso_core::DatabaseStorage for DatabaseFile { + fn read_page(&self, page_idx: usize, c: turso_core::Completion) -> Result<()> { + let r = match c.completion_type { + turso_core::CompletionType::Read(ref r) => r, + _ => unreachable!(), + }; + let size = r.buf().len(); + assert!(page_idx > 0); + if !(512..=65536).contains(&size) || size & (size - 1) != 0 { + return Err(turso_core::LimboError::NotADB); + } + let pos = (page_idx - 1) * size; + self.file.pread(pos, c.into())?; + Ok(()) + } + + fn write_page( + &self, + page_idx: usize, + buffer: Arc>, + c: turso_core::Completion, + ) -> Result<()> { + let size = buffer.borrow().len(); + let pos = (page_idx - 1) * size; + self.file.pwrite(pos, buffer, c.into())?; + Ok(()) + } + + fn sync(&self, c: turso_core::Completion) -> Result<()> { + let _ = self.file.sync(c.into())?; + Ok(()) + } + + fn size(&self) -> Result { + self.file.size() + } + + fn truncate(&self, len: u64, c: turso_core::Completion) -> Result<()> { + self.file.truncate(len, c); + Ok(()) + } +} + +#[cfg(all(feature = "web", not(feature = "nodejs")))] +#[wasm_bindgen(module = "/web/src/web-vfs.js")] +extern "C" { + type VFS; + #[wasm_bindgen(constructor)] + fn new() -> VFS; + + #[wasm_bindgen(method)] + fn open(this: &VFS, path: &str, flags: &str) -> i32; + + #[wasm_bindgen(method)] + fn close(this: &VFS, fd: i32) -> bool; + + #[wasm_bindgen(method)] + fn pwrite(this: &VFS, fd: i32, buffer: &[u8], offset: usize) -> i32; + + #[wasm_bindgen(method)] + fn pread(this: &VFS, fd: i32, buffer: &mut [u8], offset: usize) -> i32; + + #[wasm_bindgen(method)] + fn size(this: &VFS, fd: i32) -> u64; + + #[wasm_bindgen(method)] + fn truncate(this: &VFS, fd: i32, len: usize); + + #[wasm_bindgen(method)] + fn sync(this: &VFS, fd: i32); +} + +#[cfg(all(feature = "nodejs", not(feature = "web")))] +#[wasm_bindgen(module = "/node/src/vfs.cjs")] +extern "C" { + type VFS; + #[wasm_bindgen(constructor)] + fn new() -> VFS; + + #[wasm_bindgen(method)] + fn open(this: &VFS, path: &str, flags: &str) -> i32; + + #[wasm_bindgen(method)] + fn close(this: &VFS, fd: i32) -> bool; + + #[wasm_bindgen(method)] + fn pwrite(this: &VFS, fd: i32, buffer: &[u8], offset: usize) -> i32; + + #[wasm_bindgen(method)] + fn pread(this: &VFS, fd: i32, buffer: &mut [u8], offset: usize) -> i32; + + #[wasm_bindgen(method)] + fn size(this: &VFS, fd: i32) -> u64; + + #[wasm_bindgen(method)] + fn truncate(this: &VFS, fd: i32, len: usize); + + #[wasm_bindgen(method)] + fn sync(this: &VFS, fd: i32); +} + +#[wasm_bindgen(start)] +pub fn init() { + console_error_panic_hook::set_once(); +} diff --git a/bindings/wasm/node/src/vfs.cjs b/bindings/wasm/node/src/vfs.cjs new file mode 100644 index 000000000..2501c512d --- /dev/null +++ b/bindings/wasm/node/src/vfs.cjs @@ -0,0 +1,37 @@ +const fs = require('node:fs'); + +class VFS { + constructor() { + } + + open(path, flags) { + return fs.openSync(path, flags); + } + + close(fd) { + fs.closeSync(fd); + } + + pread(fd, buffer, offset) { + return fs.readSync(fd, buffer, 0, buffer.length, offset); + } + + pwrite(fd, buffer, offset) { + return fs.writeSync(fd, buffer, 0, buffer.length, offset); + } + + size(fd) { + let stats = fs.fstatSync(fd); + return BigInt(stats.size); + } + + sync(fd) { + fs.fsyncSync(fd); + } + + truncate(fd, size) { + fs.ftruncateSync(fs, size) + } +} + +module.exports = { VFS }; diff --git a/bindings/wasm/web/src/web-vfs.js b/bindings/wasm/web/src/web-vfs.js new file mode 100644 index 000000000..21bdf9c0a --- /dev/null +++ b/bindings/wasm/web/src/web-vfs.js @@ -0,0 +1,33 @@ +export class VFS { + constructor() { + return self.vfs; + } + + open(path, flags) { + return self.vfs.open(path); + } + + close(fd) { + return self.vfs.close(fd); + } + + pread(fd, buffer, offset) { + return self.vfs.pread(fd, buffer, offset); + } + + pwrite(fd, buffer, offset) { + return self.vfs.pwrite(fd, buffer, offset); + } + + size(fd) { + return self.vfs.size(fd); + } + + sync(fd) { + return self.vfs.sync(fd); + } + + truncate(fd, size) { + return self.vfs.truncate(fd, size) + } +}