Add readme info

Add basic limbo-opfs-test.html
Remove unused browser-its structure - we may bring it back
This commit is contained in:
Elijah Morgan
2025-01-01 16:14:48 -05:00
parent 058ca89561
commit 5765ccbfbb
5 changed files with 100 additions and 2276 deletions

View File

@@ -7,3 +7,30 @@ This source tree contains Limbo Wasm bindings.
```
./scripts/build
```
# Browser Support
Adding experimental support for limbo in the browser. This is done by adding support for OPFS as a VFS.
To see a basic example of this `npm run dev` and navigate to `http://localhost:5173/limbo-opfs-test.html` and open the console.
## Design
This design mirrors sqlite's approach for OPFS support. It has a sync api in `opfs.js` which communicates with `opfs-sync-proxy.js` via `SharedArrayBuffer` and `Atomics.wait`. This allows us to live the VFS api in `lib.rs` unchanged.
You can see `limbo-opfs-test.html` for basic usage.
## UTs
There are OPFS specific unit tests and then some basic limbo unit tests. These are run via `npm test` or `npx vitest`.
For more info and log output you can run `npx vitest:ui` but you can get some parallel execution of test cases which cause issues.
## TODO
-[] Add a wrapper js that provides a clean interface to the `limbo-worker.js`
-[] Add more tests for opfs.js operations
-[] Add error return handling
-[] Make sure posix flags for open are handled instead of just being ignored (this requires creating a mapping of behaviors from posix to opfs as far as makes sense)

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +0,0 @@
{
"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

@@ -1,84 +0,0 @@
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);
};

View File

@@ -1,62 +1,83 @@
<!DOCTYPE html>
<html>
<head>
<title>Limbo Test</title>
</head>
<body>
<button onclick="runTests()">Run Tests</button>
<script type="module">
import init, { Database } from './pkg/limbo_wasm.js';
<script type="module">
function waitForMessage(worker, type, op) {
return 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);
});
}
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;
}
};
async function runTests() {
const worker = new Worker('./src/limbo-worker.js', { type: 'module' });
// Wait for ready then send createDb
await waitForMessage(worker, 'ready');
worker.postMessage({
op: 'createDb',
path: 'test.db'
});
window.runTests = async () => {
console.log('Starting tests...');
try {
console.log('Before connect call');
const [db] = await connect();
console.log('After connect call');
// Wait for createDb success then send exec
await waitForMessage(worker, 'success', 'createDb');
worker.postMessage({
op: 'exec',
sql: `
CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT);
`
});
console.log("made it here");
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');
// Wait for exec success then send prepare
await waitForMessage(worker, 'success', 'exec');
worker.postMessage({
op: 'exec',
sql: `
INSERT INTO users VALUES (1, 'Alice', 'alice@example.org');
`
});
const stmt = db.prepare("SELECT * FROM users");
const result = stmt.raw().all();
console.log('Query result:', result);
console.log('Tests completed successfully');
await waitForMessage(worker, 'success', 'exec');
worker.postMessage({
op: 'exec',
sql: `
INSERT INTO users VALUES (2, 'Bob', 'bob@example.org');
`
});
} catch (e) {
console.error('Test error:', e);
}
};
</script>
await waitForMessage(worker, 'success', 'exec');
worker.postMessage({
op: 'exec',
sql: `
INSERT INTO users VALUES (3, 'bill', 'bill@example.com');
`
});
// Wait for exec success then send prepare
await waitForMessage(worker, 'success', 'exec');
worker.postMessage({
op: 'prepare',
sql: 'SELECT * FROM users;'
});
const results = await waitForMessage(worker, 'result');
console.log('Query results:', results);
}
runTests().catch(console.error);
</script>
</body>
</html>