feat add basic opfs support and tests

This commit is contained in:
Elijah Morgan
2025-01-01 10:30:55 -05:00
parent 1bc1f38737
commit 058ca89561
26 changed files with 5925 additions and 4 deletions

15
Cargo.lock generated
View File

@@ -1120,6 +1120,8 @@ dependencies = [
"js-sys",
"limbo_core",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
@@ -2337,6 +2339,19 @@ dependencies = [
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.49"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2"
dependencies = [
"cfg-if",
"js-sys",
"once_cell",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.99"

2
bindings/wasm/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
node_modules/
*.wasm

View File

@@ -15,3 +15,5 @@ console_error_panic_hook = "0.1.7"
js-sys = "0.3.72"
limbo_core = { path = "../../core", default-features = false }
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
web-sys = "0.3"

2125
bindings/wasm/browser-its/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,15 @@
{
"name": "limbo-wasm-integration-tests",
"type": "module",
"private": true,
"scripts": {
"test": "PROVIDER=better-sqlite3 ava tests/test.js && PROVIDER=limbo-wasm ava tests/test.js"
},
"devDependencies": {
"ava": "^5.3.0"
},
"dependencies": {
"better-sqlite3": "^8.4.0",
"limbo-wasm": "../pkg"
}
}

View File

@@ -0,0 +1,84 @@
import test from "ava";
test.beforeEach(async (t) => {
const [db, errorType, provider] = await connect();
db.exec(`
DROP TABLE IF EXISTS users;
CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)
`);
db.exec(
"INSERT INTO users (id, name, email) VALUES (1, 'Alice', 'alice@example.org')"
);
db.exec(
"INSERT INTO users (id, name, email) VALUES (2, 'Bob', 'bob@example.com')"
);
t.context = {
db,
errorType,
provider
};
});
test.serial("Statement.raw().all()", async (t) => {
const db = t.context.db;
const stmt = db.prepare("SELECT * FROM users");
const expected = [
[1, "Alice", "alice@example.org"],
[2, "Bob", "bob@example.com"],
];
t.deepEqual(stmt.raw().all(), expected);
});
test.serial("Statement.raw().get()", async (t) => {
const db = t.context.db;
const stmt = db.prepare("SELECT * FROM users");
const expected = [
1, "Alice", "alice@example.org"
];
t.deepEqual(stmt.raw().get(), expected);
const emptyStmt = db.prepare("SELECT * FROM users WHERE id = -1");
t.is(emptyStmt.raw().get(), undefined);
});
test.serial("Statement.raw().iterate()", async (t) => {
const db = t.context.db;
const stmt = db.prepare("SELECT * FROM users");
const expected = [
{ done: false, value: [1, "Alice", "alice@example.org"] },
{ done: false, value: [2, "Bob", "bob@example.com"] },
{ done: true, value: undefined },
];
let iter = stmt.raw().iterate();
t.is(typeof iter[Symbol.iterator], 'function');
t.deepEqual(iter.next(), expected[0])
t.deepEqual(iter.next(), expected[1])
t.deepEqual(iter.next(), expected[2])
const emptyStmt = db.prepare("SELECT * FROM users WHERE id = -1");
t.is(typeof emptyStmt[Symbol.iterator], 'undefined');
t.throws(() => emptyStmt.next(), { instanceOf: TypeError });
});
const connect = async (path_opt) => {
const path = path_opt ?? "hello.db";
const provider = process.env.PROVIDER;
if (provider === "limbo-wasm") {
const database = process.env.LIBSQL_DATABASE ?? path;
const x = await import("limbo-wasm");
const options = {};
const db = new x.Database(database, options);
return [db, x.SqliteError, provider];
}
if (provider == "better-sqlite3") {
const x = await import("better-sqlite3");
const options = {};
const db = x.default(path, options);
return [db, x.SqliteError, provider];
}
throw new Error("Unknown provider: " + provider);
};

82
bindings/wasm/index.html Normal file
View File

@@ -0,0 +1,82 @@
<!DOCTYPE html>
<html>
<body>
<script type="module">
import { VFSInterface } from './src/opfs-interface.js';
window.VFSInterface = VFSInterface;
</script>
</body>
</html>
<!-- <!DOCTYPE html> -->
<!-- <html> -->
<!-- <head> -->
<!-- <title>OPFS Tests</title> -->
<!-- <style> -->
<!-- .status-box { -->
<!-- width: 100px; -->
<!-- height: 100px; -->
<!-- margin: 20px; -->
<!-- border: 2px solid #333; -->
<!-- } -->
<!-- .success { background-color: #4CAF50; } -->
<!-- .error { background-color: #f44336; } -->
<!-- .running { background-color: #FFA500; } -->
<!-- </style> -->
<!-- </head> -->
<!-- <body> -->
<!-- <h1>OPFS Tests</h1> -->
<!-- <button id="startTests">Run Tests</button> -->
<!-- <div id="status" class="status-box"></div> -->
<!-- <div id="results"></div> -->
<!---->
<!-- <script type="module"> -->
<!-- import { VFSInterface } from './src/opfs-interface.js'; -->
<!-- -->
<!-- const status = document.getElementById('status'); -->
<!-- const results = document.getElementById('results'); -->
<!-- -->
<!-- async function runTests() { -->
<!-- status.className = 'status-box running'; -->
<!-- results.innerHTML = ''; -->
<!-- -->
<!-- const log = (msg) => { -->
<!-- console.log(msg); -->
<!-- results.innerHTML += `<p>${msg}</p>`; -->
<!-- }; -->
<!---->
<!-- try { -->
<!-- const vfs = new VFSInterface("./src/opfs-worker.js"); -->
<!-- -->
<!-- log('Test 1: Basic Write/Read'); -->
<!-- const testFd = await vfs.open('test.txt'); -->
<!-- const writeData = new Uint8Array([1, 2, 3, 4]); -->
<!-- const bytesWritten = await vfs.pwrite(testFd, writeData, 0); -->
<!-- log(`Wrote ${bytesWritten} bytes`); -->
<!---->
<!-- const readBuffer = new Uint8Array(4); -->
<!-- const bytesRead = await vfs.pread(testFd, readBuffer, 0); -->
<!-- log(`Read ${bytesRead} bytes: ${Array.from(readBuffer)}`); -->
<!-- -->
<!-- log('Test 2: File Size'); -->
<!-- const size = await vfs.size(testFd); -->
<!-- log(`File size: ${size} bytes`); -->
<!-- -->
<!-- log('Test 3: Close File'); -->
<!-- await vfs.close(testFd); -->
<!-- log('File closed successfully'); -->
<!---->
<!-- status.className = 'status-box success'; -->
<!---->
<!-- } catch (error) { -->
<!-- log(`Error: ${error.message}`); -->
<!-- console.error('Full error:', error); -->
<!-- status.className = 'status-box error'; -->
<!-- } -->
<!-- console.log("done and exiting"); -->
<!-- } -->
<!---->
<!-- document.getElementById('startTests').onclick = () => runTests().catch(console.error); -->
<!-- </script> -->
<!-- </body> -->
<!-- </html> -->

View File

@@ -46,7 +46,10 @@ impl Database {
}
#[wasm_bindgen]
pub fn exec(&self, _sql: &str) {}
pub fn exec(&self, _sql: &str) {
let _res = self.conn.execute(_sql).unwrap();
// Statement::new(RefCell::new(stmt), false)
}
#[wasm_bindgen]
pub fn prepare(&self, _sql: &str) -> Statement {
@@ -300,7 +303,7 @@ impl limbo_core::DatabaseStorage for DatabaseStorage {
}
}
#[wasm_bindgen(module = "/vfs.js")]
#[wasm_bindgen(module = "/src/web-vfs.js")]
extern "C" {
type VFS;

View File

@@ -0,0 +1,62 @@
<!DOCTYPE html>
<html>
<body>
<button onclick="runTests()">Run Tests</button>
<script type="module">
import init, { Database } from './pkg/limbo_wasm.js';
console.log('Page loading, initializing WASM...');
try {
await navigator.storage.getDirectory();
console.log('OPFS access granted');
await init();
console.log('WASM initialized successfully');
} catch (e) {
console.error('Initialization failed:', e);
}
window.connect = async () => {
console.log('Connect started...');
try {
console.log('Creating Database instance...');
const db = await new Database("hello.db"); // Added await here
console.log('Database instance created:', db);
return [db, Error, "limbo-wasm"];
} catch (e) {
console.error('Connection error type:', e.constructor.name);
console.error('Connection error:', e);
throw e;
}
};
window.runTests = async () => {
console.log('Starting tests...');
try {
console.log('Before connect call');
const [db] = await connect();
console.log('After connect call');
await db.exec(`
DROP TABLE IF EXISTS users;
CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)
`);
await db.exec("INSERT INTO users VALUES (1, 'Alice', 'alice@example.org')");
await db.exec("INSERT INTO users VALUES (2, 'Bob', 'bob@example.com')");
console.log('Test data inserted');
const stmt = db.prepare("SELECT * FROM users");
const result = stmt.raw().all();
console.log('Query result:', result);
console.log('Tests completed successfully');
} catch (e) {
console.error('Test error:', e);
}
};
</script>
</body>
</html>

View File

@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<title>Limbo Test</title>
</head>
<body>
<script type="module">
window.Worker = Worker;
</script>
</body>
</html>

2674
bindings/wasm/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -16,5 +16,20 @@
"limbo_wasm.d.ts"
],
"main": "limbo_wasm.js",
"types": "limbo_wasm.d.ts"
"types": "limbo_wasm.d.ts",
"type": "module",
"scripts": {
"dev": "vite",
"test": "vitest --sequence.shuffle=false",
"test:ui": "vitest --ui"
},
"devDependencies": {
"@playwright/test": "^1.40.0",
"@vitest/ui": "^1.0.0",
"happy-dom": "^12.0.0",
"playwright": "^1.40.0",
"vite": "^5.0.0",
"vite-plugin-wasm": "^3.4.1",
"vitest": "^1.0.0"
}
}

View File

@@ -0,0 +1,13 @@
// Using Playwright (recommended)
import { expect, test } from "@playwright/test";
// playwright.config.js
export default {
use: {
headless: true,
// Required for SharedArrayBuffer
launchOptions: {
args: ["--cross-origin-isolated"],
},
},
};

View File

@@ -1,4 +1,4 @@
#!/bin/bash
wasm-pack build --no-pack --target nodejs
wasm-pack build --no-pack --target web
cp package.json pkg/package.json

View File

@@ -0,0 +1,55 @@
import { VFS } from "./opfs.js";
import init, { Database } from "./../pkg/limbo_wasm.js";
let db = null;
let currentStmt = null;
async function initVFS() {
const vfs = new VFS();
await vfs.ready;
self.vfs = vfs;
return vfs;
}
async function initAll() {
await initVFS();
await init();
}
initAll().then(() => {
self.postMessage({ type: "ready" });
self.onmessage = (e) => {
try {
switch (e.data.op) {
case "createDb": {
db = new Database(e.data.path);
self.postMessage({ type: "success", op: "createDb" });
break;
}
case "exec": {
console.log(e.data.sql);
db.exec(e.data.sql);
self.postMessage({ type: "success", op: "exec" });
break;
}
case "prepare": {
currentStmt = db.prepare(e.data.sql);
const results = currentStmt.raw().all();
self.postMessage({ type: "result", result: results });
break;
}
case "get": {
const row = currentStmt?.raw().get();
self.postMessage({ type: "result", result: row });
break;
}
}
} catch (err) {
self.postMessage({ type: "error", error: err.toString() });
}
};
}).catch((error) => {
self.postMessage({ type: "error", error: error.toString() });
});

View File

@@ -0,0 +1,67 @@
export class VFSInterface {
constructor(workerUrl) {
this.worker = new Worker(workerUrl, { type: "module" });
this.nextMessageId = 1;
this.pendingRequests = new Map();
this.worker.onmessage = (event) => {
console.log("interface onmessage: ", event.data);
let { id, result, error } = event.data;
const resolver = this.pendingRequests.get(id);
if (event.data?.buffer && event.data?.size) {
result = { size: event.data.size, buffer: event.data.buffer };
}
if (resolver) {
this.pendingRequests.delete(id);
if (error) {
resolver.reject(new Error(error));
} else {
resolver.resolve(result);
}
}
};
}
_sendMessage(method, args) {
const id = this.nextMessageId++;
return new Promise((resolve, reject) => {
this.pendingRequests.set(id, { resolve, reject });
this.worker.postMessage({ id, method, args });
});
}
async open(path, flags) {
return await this._sendMessage("open", { path, flags });
}
async close(fd) {
return await this._sendMessage("close", { fd });
}
async pwrite(fd, buffer, offset) {
return await this._sendMessage("pwrite", { fd, buffer, offset }, [
buffer.buffer,
]);
}
async pread(fd, buffer, offset) {
console.log("interface in buffer: ", [...buffer]);
const result = await this._sendMessage("pread", {
fd,
buffer: buffer,
offset,
}, []);
console.log("interface out buffer: ", [...buffer]);
buffer.set(new Uint8Array(result.buffer));
return buffer.length;
}
async size(fd) {
return await this._sendMessage("size", { fd });
}
async sync(fd) {
return await this._sendMessage("sync", { fd });
}
}

View File

@@ -0,0 +1,116 @@
// opfs-sync-proxy.js
let transferBuffer, statusBuffer, statusArray, statusView;
let transferArray;
let rootDir = null;
const handles = new Map();
let nextFd = 1;
self.postMessage("ready");
onmessage = async (e) => {
console.log("handle message: ", e.data);
if (e.data.cmd === "init") {
console.log("init");
transferBuffer = e.data.transferBuffer;
statusBuffer = e.data.statusBuffer;
transferArray = new Uint8Array(transferBuffer);
statusArray = new Int32Array(statusBuffer);
statusView = new DataView(statusBuffer);
self.postMessage("done");
return;
}
const result = await handleCommand(e.data);
sendResult(result);
};
self.onerror = (error) => {
console.error("opfssync error: ", error);
// Don't close, keep running
return true; // Prevents default error handling
};
function handleCommand(msg) {
console.log(`handle message: ${msg.cmd}`);
switch (msg.cmd) {
case "open":
return handleOpen(msg.path);
case "close":
return handleClose(msg.fd);
case "read":
return handleRead(msg.fd, msg.offset, msg.size);
case "write":
return handleWrite(msg.fd, msg.buffer, msg.offset);
case "size":
return handleSize(msg.fd);
case "sync":
return handleSync(msg.fd);
}
}
async function handleOpen(path) {
if (!rootDir) {
rootDir = await navigator.storage.getDirectory();
}
const fd = nextFd++;
const handle = await rootDir.getFileHandle(path, { create: true });
const syncHandle = await handle.createSyncAccessHandle();
handles.set(fd, syncHandle);
return { fd };
}
function handleClose(fd) {
const handle = handles.get(fd);
handle.close();
handles.delete(fd);
return { success: true };
}
function handleRead(fd, offset, size) {
const handle = handles.get(fd);
const readBuffer = new ArrayBuffer(size);
const readSize = handle.read(readBuffer, { at: offset });
console.log("opfssync read: size: ", readBuffer.byteLength);
const tmp = new Uint8Array(readBuffer);
console.log("opfssync read buffer: ", [...tmp]);
transferArray.set(tmp);
return { success: true, length: readSize };
}
function handleWrite(fd, buffer, offset) {
console.log("opfssync buffer size:", buffer.byteLength);
console.log("opfssync write buffer: ", [...buffer]);
const handle = handles.get(fd);
const size = handle.write(buffer, { at: offset });
return { success: true, length: size };
}
function handleSize(fd) {
const handle = handles.get(fd);
return { success: true, length: handle.getSize() };
}
function handleSync(fd) {
const handle = handles.get(fd);
handle.flush();
return { success: true };
}
function sendResult(result) {
if (result?.fd) {
statusView.setInt32(4, result.fd, true);
} else {
console.log("opfs-sync-proxy: result.length: ", result.length);
statusView.setInt32(4, result?.length || 0, true);
}
Atomics.store(statusArray, 0, 1);
Atomics.notify(statusArray, 0);
}

View File

@@ -0,0 +1,101 @@
import { VFS } from "./opfs.js";
const vfs = new VFS();
onmessage = async function (e) {
if (!vfs.isReady) {
console.log("opfs ready: ", vfs.isReady);
await vfs.ready;
console.log("opfs ready: ", vfs.isReady);
}
const { id, method, args } = e.data;
console.log(`interface onmessage method: ${method}`);
try {
let result;
switch (method) {
case "open":
result = vfs.open(args.path, args.flags);
break;
case "close":
result = vfs.close(args.fd);
break;
case "pread": {
const buffer = new Uint8Array(args.buffer);
result = vfs.pread(args.fd, buffer, args.offset);
self.postMessage(
{ id, size: result, error: null, buffer },
);
console.log("read size: ", result);
console.log("read buffer: ", [...buffer]);
return;
}
case "pwrite": {
result = vfs.pwrite(args.fd, args.buffer, args.offset);
console.log("write size: ", result);
break;
}
case "size":
result = vfs.size(args.fd);
break;
case "sync":
result = vfs.sync(args.fd);
break;
default:
throw new Error(`Unknown method: ${method}`);
}
self.postMessage(
{ id, result, error: null },
);
} catch (error) {
self.postMessage({ id, result: null, error: error.message });
}
};
console.log("opfs-worker.js");
// checkCompatibility();
// // In VFS class
// this.worker.onerror = (error) => {
// console.error("Worker stack:", error.error?.stack || error.message);
// };
// checkCompatibility();
//
// async function checkCompatibility() {
// console.log("begin check compatibility");
// // OPFS API check
// if (!("storage" in navigator && "getDirectory" in navigator.storage)) {
// throw new Error("OPFS API not supported");
// }
//
// // SharedArrayBuffer support check
// if (typeof SharedArrayBuffer === "undefined") {
// throw new Error("SharedArrayBuffer not supported");
// }
//
// // Atomics API check
// if (typeof Atomics === "undefined") {
// throw new Error("Atomics API not supported");
// }
//
// // Permission check for OPFS
// try {
// const root = await navigator.storage.getDirectory();
// await root.getFileHandle("test.txt", { create: true });
// } catch (e) {
// console.log(e);
// console.log("throwing OPFS permission Denied");
// throw new Error("OPFS permission denied");
// }
//
// // Cross-Origin-Isolation check for SharedArrayBuffer
// if (!crossOriginIsolated) {
// throw new Error("Cross-Origin-Isolation required for SharedArrayBuffer");
// }
//
// console.log("done check compatibility");
// return true;
// }

134
bindings/wasm/src/opfs.js Normal file
View File

@@ -0,0 +1,134 @@
// First file: VFS class
class VFS {
constructor() {
this.transferBuffer = new SharedArrayBuffer(1024 * 1024); // 1mb
this.statusBuffer = new SharedArrayBuffer(8); // Room for status + size
this.statusArray = new Int32Array(this.statusBuffer);
this.statusView = new DataView(this.statusBuffer);
this.worker = new Worker(
new URL("./opfs-sync-proxy.js", import.meta.url),
{ type: "module" },
);
this.isReady = false;
this.ready = new Promise((resolve, reject) => {
this.worker.addEventListener("message", async (e) => {
if (e.data === "ready") {
await this.initWorker();
this.isReady = true;
resolve();
}
}, { once: true });
this.worker.addEventListener("error", reject, { once: true });
});
this.worker.onerror = (e) => {
console.error("Sync proxy worker error:", e.message);
};
}
initWorker() {
return new Promise((resolve) => {
this.worker.addEventListener("message", (e) => {
console.log("eventListener: ", e.data);
resolve();
}, { once: true });
this.worker.postMessage({
cmd: "init",
transferBuffer: this.transferBuffer,
statusBuffer: this.statusBuffer,
});
});
}
open(path) {
Atomics.store(this.statusArray, 0, 0);
this.worker.postMessage({ cmd: "open", path });
Atomics.wait(this.statusArray, 0, 0);
const result = this.statusView.getInt32(4, true);
console.log("opfs.js open result: ", result);
console.log("opfs.js open result type: ", typeof result);
return result;
}
close(fd) {
Atomics.store(this.statusArray, 0, 0);
this.worker.postMessage({ cmd: "close", fd });
Atomics.wait(this.statusArray, 0, 0);
return true;
}
pread(fd, buffer, offset) {
let bytesRead = 0;
while (bytesRead < buffer.byteLength) {
const chunkSize = Math.min(
this.transferBuffer.byteLength,
buffer.byteLength - bytesRead,
);
Atomics.store(this.statusArray, 0, 0);
this.worker.postMessage({
cmd: "read",
fd,
offset: offset + bytesRead,
size: chunkSize,
});
Atomics.wait(this.statusArray, 0, 0);
const readSize = this.statusView.getInt32(4, true);
buffer.set(
new Uint8Array(this.transferBuffer, 0, readSize),
bytesRead,
);
console.log("opfs pread buffer: ", [...buffer]);
bytesRead += readSize;
if (readSize < chunkSize) break;
}
return bytesRead;
}
pwrite(fd, buffer, offset) {
console.log("write buffer size: ", buffer.byteLength);
Atomics.store(this.statusArray, 0, 0);
this.worker.postMessage({
cmd: "write",
fd,
buffer: buffer,
offset: offset,
});
Atomics.wait(this.statusArray, 0, 0);
console.log(
"opfs pwrite length statusview: ",
this.statusView.getInt32(4, true),
);
return this.statusView.getInt32(4, true);
}
size(fd) {
Atomics.store(this.statusArray, 0, 0);
this.worker.postMessage({ cmd: "size", fd });
Atomics.wait(this.statusArray, 0, 0);
const result = this.statusView.getInt32(4, true);
console.log("opfs.js size result: ", result);
console.log("opfs.js size result type: ", typeof result);
return BigInt(result);
}
sync(fd) {
Atomics.store(this.statusArray, 0, 0);
this.worker.postMessage({ cmd: "sync", fd });
Atomics.wait(this.statusArray, 0, 0);
}
}
export { VFS };

View File

@@ -0,0 +1,32 @@
export class VFS {
constructor() {
return self.vfs;
}
open(path, flags) {
const result = self.vfs.open(path);
consol.log("webvfs open result: ", result);
consol.log("webvfs open result type: ", typeof result);
return result;
}
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);
}
}

View File

@@ -0,0 +1,62 @@
import { expect, test } from "vitest";
test("basic database operations", async () => {
const page = globalThis.__page__;
await page.goto("http://localhost:5173/limbo-test.html");
page.on("console", (msg) => console.log(msg.text()));
const result = await page.evaluate(async () => {
const worker = new Worker("./src/limbo-worker.js", { type: "module" });
const waitForMessage = (type, op) =>
new Promise((resolve, reject) => {
const handler = (e) => {
if (e.data.type === type && (!op || e.data.op === op)) {
worker.removeEventListener("message", handler);
resolve(e.data);
} else if (e.data.type === "error") {
worker.removeEventListener("message", handler);
reject(e.data.error);
}
};
worker.addEventListener("message", handler);
});
try {
await waitForMessage("ready");
worker.postMessage({ op: "createDb", path: "test.db" });
await waitForMessage("success", "createDb");
worker.postMessage({
op: "exec",
sql:
"CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT);",
});
await waitForMessage("success", "exec");
worker.postMessage({
op: "exec",
sql: "INSERT INTO users VALUES (1, 'Alice', 'alice@example.org');",
});
await waitForMessage("success", "exec");
worker.postMessage({
op: "prepare",
sql: "SELECT * FROM users;",
});
const results = await waitForMessage("result");
return results;
} catch (error) {
return { error: error.message };
}
});
if (result.error) throw new Error(`Test failed: ${result.error}`);
console.log("test results: ", result);
console.log("test results: ", result.result[0]);
expect(result.result).toHaveLength(1);
expect(result.result[0]).toEqual([1, "Alice", "alice@example.org"]);
// expect(1).toEqual(1);
});

View File

@@ -0,0 +1,144 @@
import { expect, test } from "vitest";
test("basic read/write functionality", async () => {
const page = globalThis.__page__;
await page.goto("http://localhost:5173/index.html");
page.on("console", (msg) => console.log(msg.text()));
const result = await page.evaluate(async () => {
const vfs = new window.VFSInterface("/src/opfs-worker.js");
let fd;
try {
fd = await vfs.open("test.txt", {});
const writeData = new Uint8Array([1, 2, 3, 4]);
const bytesWritten = await vfs.pwrite(fd, writeData, 0);
const readData = new Uint8Array(4);
const bytesRead = await vfs.pread(fd, readData, 0);
await vfs.close(fd);
return { fd, bytesWritten, bytesRead, readData: Array.from(readData) };
} catch (error) {
if (fd !== undefined) await vfs.close(fd);
return { error: error.message };
}
});
if (result.error) throw new Error(`Test failed: ${result.error}`);
expect(result.fd).toBe(1);
expect(result.bytesWritten).toBe(4);
expect(result.bytesRead).toBe(4);
expect(result.readData).toEqual([1, 2, 3, 4]);
});
test("larger data read/write", async () => {
const page = globalThis.__page__;
await page.goto("http://localhost:5173/index.html");
const result = await page.evaluate(async () => {
const vfs = new window.VFSInterface("/src/opfs-worker.js");
let fd;
try {
fd = await vfs.open("large.txt", {});
const writeData = new Uint8Array(1024).map((_, i) => i % 256);
const bytesWritten = await vfs.pwrite(fd, writeData, 0);
const readData = new Uint8Array(1024);
const bytesRead = await vfs.pread(fd, readData, 0);
await vfs.close(fd);
return { bytesWritten, bytesRead, readData: Array.from(readData) };
} catch (error) {
if (fd !== undefined) await vfs.close(fd);
return { error: error.message };
}
});
if (result.error) throw new Error(`Test failed: ${result.error}`);
expect(result.bytesWritten).toBe(1024);
expect(result.bytesRead).toBe(1024);
expect(result.readData).toEqual(
Array.from({ length: 1024 }, (_, i) => i % 256),
);
});
test("partial reads and writes", async () => {
const page = globalThis.__page__;
await page.goto("http://localhost:5173/index.html");
const result = await page.evaluate(async () => {
const vfs = new window.VFSInterface("/src/opfs-worker.js");
let fd;
try {
fd = await vfs.open("partial.txt", {});
// Write data in chunks
const writeData1 = new Uint8Array([1, 2, 3, 4]);
const writeData2 = new Uint8Array([5, 6, 7, 8]);
await vfs.pwrite(fd, writeData1, 0);
await vfs.pwrite(fd, writeData2, 4);
// Read partial chunks
const readData1 = new Uint8Array(2);
const readData2 = new Uint8Array(4);
const readData3 = new Uint8Array(2);
await vfs.pread(fd, readData1, 0); // Should read [1,2]
await vfs.pread(fd, readData2, 2); // Should read [3,4,5,6]
await vfs.pread(fd, readData3, 6); // Should read [7,8]
await vfs.close(fd);
return {
readData1: Array.from(readData1),
readData2: Array.from(readData2),
readData3: Array.from(readData3),
};
} catch (error) {
if (fd !== undefined) await vfs.close(fd);
return { error: error.message };
}
});
if (result.error) throw new Error(`Test failed: ${result.error}`);
expect(result.readData1).toEqual([1, 2]);
expect(result.readData2).toEqual([3, 4, 5, 6]);
expect(result.readData3).toEqual([7, 8]);
});
test("file size operations", async () => {
const page = globalThis.__page__;
await page.goto("http://localhost:5173/index.html");
const result = await page.evaluate(async () => {
const vfs = new window.VFSInterface("/src/opfs-worker.js");
let fd;
try {
fd = await vfs.open("size.txt", {});
// First write
const writeData1 = new Uint8Array([1, 2, 3, 4]);
await vfs.pwrite(fd, writeData1, 0);
const size1 = await vfs.size(fd);
// Second write with new array
const writeData2 = new Uint8Array([5, 6, 7, 8]);
await vfs.pwrite(fd, writeData2, 4);
const size2 = await vfs.size(fd);
await vfs.close(fd);
return { size1, size2 };
} catch (error) {
if (fd !== undefined) await vfs.close(fd);
return { error: error.message };
}
});
if (result.error) throw new Error(`Test failed: ${result.error}`);
expect(Number(result.size1)).toBe(4);
expect(Number(result.size2)).toBe(8);
});

View File

@@ -0,0 +1,33 @@
import { afterEach, beforeEach } from "vitest";
import { chromium } from "playwright";
import { createServer } from "vite";
let browser;
let context;
let page;
let server;
beforeEach(async () => {
// Start Vite dev server
server = await createServer({
configFile: "./vite.config.js",
root: ".",
server: {
port: 5173,
},
});
await server.listen();
browser = await chromium.launch();
context = await browser.newContext();
page = await context.newPage();
globalThis.__page__ = page;
});
afterEach(async () => {
await context.close();
await browser.close();
await server.close();
});

View File

@@ -0,0 +1,37 @@
<!DOCTYPE html>
<html>
<body>
<script type="module">
import { VFSInterface } from './src/opfs-interface.js';
console.log('made it here')
window.runTest = async () => {
console.log('made it here')
const vfs = new VFSInterface(new URL('./src/opfs-worker.js', import.meta.url).href);
console.log('made it here')
// Test file operations
const fd = await vfs.open('test.txt', {});
// Write data
const writeData = new Uint8Array([1, 2, 3, 4]);
const bytesWritten = await vfs.pwrite(fd, writeData, 0);
// Read data
const readData = new Uint8Array(4);
const bytesRead = await vfs.pread(fd, readData, 0);
// Close file
await vfs.close(fd);
return {
bytesWritten,
bytesRead,
readData: Array.from(readData)
};
};
</script>
</body>
</html>

View File

@@ -0,0 +1,34 @@
import { defineConfig } from "vite";
import wasm from "vite-plugin-wasm";
export default defineConfig({
plugins: [wasm()],
test: {
globals: true,
environment: "happy-dom",
setupFiles: ["./test/setup.js"],
include: ["test/*.test.js"],
sequence: {
shuffle: false,
concurrent: false,
},
},
server: {
headers: {
"Cross-Origin-Embedder-Policy": "require-corp",
"Cross-Origin-Opener-Policy": "same-origin",
"Cross-Origin-Resource-Policy": "cross-origin",
},
},
worker: {
format: "es",
rollupOptions: {
output: {
format: "es",
},
},
},
// worker: {
// format: "es",
// },
});

View File

@@ -35,6 +35,7 @@ use storage::sqlite3_ondisk::{DatabaseHeader, DATABASE_HEADER_SIZE};
pub use storage::wal::WalFile;
pub use storage::wal::WalFileShared;
use util::parse_schema_rows;
// use web_sys::console; // Add to dependencies in Cargo.toml
use translate::select::prepare_select_plan;
use types::OwnedValue;
@@ -80,6 +81,8 @@ impl Database {
pub fn open_file(io: Arc<dyn IO>, path: &str) -> Result<Arc<Database>> {
use storage::wal::WalFileShared;
// console::log_1(&"Hello from Rust!".into());
let file = io.open_file(path, io::OpenFlags::Create, true)?;
maybe_init_database_file(&file, &io)?;
let page_io = Rc::new(FileStorage::new(file));