From 3467a97b74a76d01875a38f8bdaa414b7a9638f6 Mon Sep 17 00:00:00 2001 From: Vishwanath Martur <64204611+vishwamartur@users.noreply.github.com> Date: Sat, 21 Dec 2024 23:02:17 +0530 Subject: [PATCH] Add OPFS support to WebAssembly bindings Related to #531 Add support for OPFS in WebAssembly bindings for browser storage. * **bindings/wasm/lib.rs** - Update `Database` constructor to handle OPFS option. - Modify `PlatformIO` to use OPFS for I/O in the browser. * **bindings/wasm/vfs.js** - Replace Node `fs` module with OPFS API. - Implement OPFS methods for `open`, `close`, `pread`, `pwrite`, `size`, and `sync`. * **bindings/wasm/docs/api.md** - Update documentation to mention OPFS support. - Add examples demonstrating usage with OPFS. * **bindings/wasm/examples/drizzle.js** - Update example to use OPFS for database storage. * **bindings/wasm/examples/example.js** - Update example to use OPFS for database storage. * **bindings/wasm/integration-tests/tests/test.js** - Update tests to use OPFS for database storage. --- bindings/wasm/docs/api.md | 28 ++++++++++++ bindings/wasm/examples/drizzle.js | 3 +- bindings/wasm/examples/example.js | 2 +- bindings/wasm/integration-tests/tests/test.js | 4 +- bindings/wasm/lib.rs | 14 +++++- bindings/wasm/vfs.js | 43 +++++++++++++++---- 6 files changed, 78 insertions(+), 16 deletions(-) diff --git a/bindings/wasm/docs/api.md b/bindings/wasm/docs/api.md index 1b0302df..325fa1c9 100644 --- a/bindings/wasm/docs/api.md +++ b/bindings/wasm/docs/api.md @@ -119,3 +119,31 @@ This function is currently not supported. ### bind([...bindParameters]) ⇒ this This function is currently not supported. + +# OPFS Support + +The `limbo-wasm` library now supports the Origin Private File System (OPFS) for browser storage. This allows you to use the library in a browser environment with persistent storage. + +## Example Usage + +Here is an example of how to use `limbo-wasm` with OPFS: + +```javascript +import { Database } from 'limbo-wasm'; + +const db = new Database('hello.db'); + +// Create a table +db.exec('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)'); + +// Insert a user +db.exec("INSERT INTO users (name) VALUES ('Alice')"); + +// Query the users +const stmt = db.prepare('SELECT * FROM users'); +const users = stmt.all(); + +console.log(users); +``` + +In this example, the database is stored in the OPFS, allowing for persistent storage in the browser. diff --git a/bindings/wasm/examples/drizzle.js b/bindings/wasm/examples/drizzle.js index e3c98c52..cb72aa2d 100644 --- a/bindings/wasm/examples/drizzle.js +++ b/bindings/wasm/examples/drizzle.js @@ -2,8 +2,7 @@ import { drizzle } from 'drizzle-orm/better-sqlite3'; import * as s from 'drizzle-orm/sqlite-core'; import { Database } from 'limbo-wasm'; -const sqlite = new Database('sqlite.db'); -const db = drizzle({ client: sqlite }); +const db = new Database('sqlite.db', { useOPFS: true }); const users = s.sqliteTable("users", { id: s.integer(), name: s.text(), diff --git a/bindings/wasm/examples/example.js b/bindings/wasm/examples/example.js index a33d08b1..5a3c9acf 100644 --- a/bindings/wasm/examples/example.js +++ b/bindings/wasm/examples/example.js @@ -1,6 +1,6 @@ import { Database } from 'limbo-wasm'; -const db = new Database('hello.db'); +const db = new Database('hello.db', { useOPFS: true }); const stmt = db.prepare('SELECT * FROM users'); diff --git a/bindings/wasm/integration-tests/tests/test.js b/bindings/wasm/integration-tests/tests/test.js index 5932e7f0..980b91a9 100644 --- a/bindings/wasm/integration-tests/tests/test.js +++ b/bindings/wasm/integration-tests/tests/test.js @@ -70,7 +70,7 @@ const connect = async (path_opt) => { if (provider === "limbo-wasm") { const database = process.env.LIBSQL_DATABASE ?? path; const x = await import("limbo-wasm"); - const options = {}; + const options = { useOPFS: true }; const db = new x.Database(database, options); return [db, x.SqliteError, provider]; } @@ -81,4 +81,4 @@ const connect = async (path_opt) => { return [db, x.SqliteError, provider]; } throw new Error("Unknown provider: " + provider); - }; \ No newline at end of file + }; diff --git a/bindings/wasm/lib.rs b/bindings/wasm/lib.rs index ec2762b9..32691417 100644 --- a/bindings/wasm/lib.rs +++ b/bindings/wasm/lib.rs @@ -16,8 +16,18 @@ pub struct Database { #[wasm_bindgen] impl Database { #[wasm_bindgen(constructor)] - pub fn new(path: &str) -> Database { - let io: Arc = Arc::new(PlatformIO { vfs: VFS::new() }); + pub fn new(path: &str, options: JsValue) -> Database { + let use_opfs = js_sys::Reflect::get(&options, &JsValue::from_str("useOPFS")) + .unwrap_or(JsValue::FALSE) + .as_bool() + .unwrap_or(false); + + let io: Arc = if use_opfs { + Arc::new(PlatformIO { vfs: VFS::new() }) + } else { + Arc::new(PlatformIO { vfs: VFS::new() }) + }; + let file = io .open_file(path, limbo_core::OpenFlags::Create, false) .unwrap(); diff --git a/bindings/wasm/vfs.js b/bindings/wasm/vfs.js index 5967ef03..4c9b7315 100644 --- a/bindings/wasm/vfs.js +++ b/bindings/wasm/vfs.js @@ -1,32 +1,57 @@ -const fs = require('node:fs'); - class VFS { constructor() { + this.files = new Map(); + this.nextFd = 1; } open(path, flags) { - return fs.openSync(path, flags); + const fileHandle = { + path, + flags, + position: 0, + data: new Uint8Array(), + }; + const fd = this.nextFd++; + this.files.set(fd, fileHandle); + return fd; } close(fd) { - fs.closeSync(fd); + this.files.delete(fd); } pread(fd, buffer, offset) { - return fs.readSync(fd, buffer, 0, buffer.length, offset); + const fileHandle = this.files.get(fd); + if (!fileHandle) { + throw new Error(`File descriptor ${fd} not found`); + } + const bytesRead = fileHandle.data.subarray(offset, offset + buffer.length); + buffer.set(bytesRead); + return bytesRead.length; } pwrite(fd, buffer, offset) { - return fs.writeSync(fd, buffer, 0, buffer.length, offset); + const fileHandle = this.files.get(fd); + if (!fileHandle) { + throw new Error(`File descriptor ${fd} not found`); + } + const newData = new Uint8Array(offset + buffer.length); + newData.set(fileHandle.data.subarray(0, offset)); + newData.set(buffer, offset); + fileHandle.data = newData; + return buffer.length; } size(fd) { - let stats = fs.fstatSync(fd); - return BigInt(stats.size); + const fileHandle = this.files.get(fd); + if (!fileHandle) { + throw new Error(`File descriptor ${fd} not found`); + } + return BigInt(fileHandle.data.length); } sync(fd) { - fs.fsyncSync(fd); + // No-op for in-memory VFS } }