mirror of
https://github.com/aljazceru/turso.git
synced 2026-01-29 21:04:23 +01:00
feat add basic opfs support and tests
This commit is contained in:
15
Cargo.lock
generated
15
Cargo.lock
generated
@@ -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
2
bindings/wasm/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
node_modules/
|
||||
*.wasm
|
||||
@@ -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
2125
bindings/wasm/browser-its/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
15
bindings/wasm/browser-its/package.json
Normal file
15
bindings/wasm/browser-its/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
84
bindings/wasm/browser-its/tests/test.js
Normal file
84
bindings/wasm/browser-its/tests/test.js
Normal 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
82
bindings/wasm/index.html
Normal 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> -->
|
||||
@@ -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;
|
||||
|
||||
|
||||
62
bindings/wasm/limbo-opfs-test.html
Normal file
62
bindings/wasm/limbo-opfs-test.html
Normal 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>
|
||||
11
bindings/wasm/limbo-test.html
Normal file
11
bindings/wasm/limbo-test.html
Normal 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
2674
bindings/wasm/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
13
bindings/wasm/playwright.config.js
Normal file
13
bindings/wasm/playwright.config.js
Normal 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"],
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -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
|
||||
|
||||
55
bindings/wasm/src/limbo-worker.js
Normal file
55
bindings/wasm/src/limbo-worker.js
Normal 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() });
|
||||
});
|
||||
|
||||
67
bindings/wasm/src/opfs-interface.js
Normal file
67
bindings/wasm/src/opfs-interface.js
Normal 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 });
|
||||
}
|
||||
}
|
||||
116
bindings/wasm/src/opfs-sync-proxy.js
Normal file
116
bindings/wasm/src/opfs-sync-proxy.js
Normal 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);
|
||||
}
|
||||
101
bindings/wasm/src/opfs-worker.js
Normal file
101
bindings/wasm/src/opfs-worker.js
Normal 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
134
bindings/wasm/src/opfs.js
Normal 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 };
|
||||
32
bindings/wasm/src/web-vfs.js
Normal file
32
bindings/wasm/src/web-vfs.js
Normal 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);
|
||||
}
|
||||
}
|
||||
62
bindings/wasm/test/limbo.test.js
Normal file
62
bindings/wasm/test/limbo.test.js
Normal 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);
|
||||
});
|
||||
144
bindings/wasm/test/opfs.test.js
Normal file
144
bindings/wasm/test/opfs.test.js
Normal 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);
|
||||
});
|
||||
33
bindings/wasm/test/setup.js
Normal file
33
bindings/wasm/test/setup.js
Normal 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();
|
||||
});
|
||||
|
||||
37
bindings/wasm/test/test.html
Normal file
37
bindings/wasm/test/test.html
Normal 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>
|
||||
|
||||
34
bindings/wasm/vite.config.js
Normal file
34
bindings/wasm/vite.config.js
Normal 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",
|
||||
// },
|
||||
});
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user