Remove wasm binding

With napi v3 we can compile our javascript binding to wasm, which can
reduce a lot of maintenance overhead and complexity
This commit is contained in:
Diego Reis
2025-07-28 11:41:00 -03:00
parent 016c84ed7d
commit 98bec9868b
54 changed files with 2826 additions and 7292 deletions

64
.github/labeler.yml vendored
View File

@@ -1,108 +1,106 @@
simulator:
- changed-files:
- any-glob-to-any-file: 'simulator/**/*'
- any-glob-to-any-file: "simulator/**/*"
docs:
- changed-files:
- any-glob-to-any-file: 'docs/**/*'
- any-glob-to-any-file: "docs/**/*"
extensionlib:
- changed-files:
- any-glob-to-any-file: ['extensions/core/**/*', 'macros/src/ext/*', 'core/ext/*']
- any-glob-to-any-file:
["extensions/core/**/*", "macros/src/ext/*", "core/ext/*"]
macros:
- changed-files:
- any-glob-to-any-file: ['macros/**/*']
- any-glob-to-any-file: ["macros/**/*"]
extensions-other:
- all:
- changed-files:
- any-glob-to-any-file: 'extensions/**/*'
- all-globs-to-all-files: '!extensions/core/**/*'
- all:
- changed-files:
- any-glob-to-any-file: "extensions/**/*"
- all-globs-to-all-files: "!extensions/core/**/*"
fuzzing:
- changed-files:
- any-glob-to-any-file: 'fuzz/**/*'
- any-glob-to-any-file: "fuzz/**/*"
perf/benchmarks:
- changed-files:
- any-glob-to-any-file: ['perf/**/*', 'core/benches/*']
- any-glob-to-any-file: ["perf/**/*", "core/benches/*"]
go-bindings:
- changed-files:
- any-glob-to-any-file: 'bindings/go/**/*'
wasm-bindings:
- changed-files:
- any-glob-to-any-file: 'bindings/wasm/**/*'
- any-glob-to-any-file: "bindings/go/**/*"
python-bindings:
- changed-files:
- any-glob-to-any-file: 'bindings/python/**/*'
- any-glob-to-any-file: "bindings/python/**/*"
js-bindings:
- changed-files:
- any-glob-to-any-file: 'bindings/javascript/**/*'
- any-glob-to-any-file: "bindings/javascript/**/*"
rust-bindings:
- changed-files:
- any-glob-to-any-file: 'bindings/rust/**/*'
- any-glob-to-any-file: "bindings/rust/**/*"
java-bindings:
- changed-files:
- any-glob-to-any-file: 'bindings/java/**/*'
- any-glob-to-any-file: "bindings/java/**/*"
parser:
- changed-files:
- any-glob-to-any-file: "vendored/sqlite3-parser/*"
- any-glob-to-any-file: "vendored/sqlite3-parser/*"
cli:
- changed-files:
- any-glob-to-any-file: "cli/**/*"
- any-glob-to-any-file: "cli/**/*"
sqlite3:
- changed-files:
- any-glob-to-any-file: "sqlite3/**/*"
- any-glob-to-any-file: "sqlite3/**/*"
core:
- changed-files:
- any-glob-to-any-file: 'core/**/*'
- any-glob-to-any-file: "core/**/*"
optimizer:
- changed-files:
- any-glob-to-any-file: 'core/translate/optimizer/*'
- any-glob-to-any-file: "core/translate/optimizer/*"
translation/planning:
- changed-files:
- any-glob-to-any-file: 'core/translate/*.rs'
- any-glob-to-any-file: "core/translate/*.rs"
io:
- changed-files:
- any-glob-to-any-file: 'core/io/*'
- any-glob-to-any-file: "core/io/*"
mvcc:
- changed-files:
- any-glob-to-any-file: 'core/mvcc/**/*'
- any-glob-to-any-file: "core/mvcc/**/*"
vdbe:
- changed-files:
- any-glob-to-any-file: 'core/vdbe/*'
- any-glob-to-any-file: "core/vdbe/*"
json:
- changed-files:
- any-glob-to-any-file: 'core/json/*'
- any-glob-to-any-file: "core/json/*"
storage:
- changed-files:
- any-glob-to-any-file: 'core/storage/*'
- any-glob-to-any-file: "core/storage/*"
vector:
- changed-files:
- any-glob-to-any-file: 'core/vector/*'
- any-glob-to-any-file: "core/vector/*"
antithesis:
- changed-files:
- any-glob-to-any-file: ['antithesis-tests/**/*', 'scripts/antithesis/*', 'stress/**/*']
- any-glob-to-any-file:
["antithesis-tests/**/*", "scripts/antithesis/*", "stress/**/*"]
ci-actions:
- changed-files:
- any-glob-to-any-file: '.github/workflows/*.yml'
- any-glob-to-any-file: ".github/workflows/*.yml"

View File

@@ -2,9 +2,9 @@ name: Rust
on:
push:
branches: [ "main" ]
branches: ["main"]
pull_request:
branches: [ "main" ]
branches: ["main"]
env:
CARGO_TERM_COLOR: always
@@ -13,12 +13,12 @@ jobs:
cargo-fmt-check:
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@stable
- name: Check formatting
run: cargo fmt --check
- name: Check formatting (fuzz)
run: cd fuzz && cargo fmt --check
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@stable
- name: Check formatting
run: cargo fmt --check
- name: Check formatting (fuzz)
run: cd fuzz && cargo fmt --check
build-native:
strategy:
@@ -28,21 +28,21 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- uses: useblacksmith/rust-cache@v3
with:
prefix-key: "v1-rust" # can be updated if we need to reset caches due to non-trivial change in the dependencies (for example, custom env var were set for single workspace project)
- name: Set up Python 3.10
uses: useblacksmith/setup-python@v6
with:
python-version: "3.10"
- name: Build
run: cargo build --verbose
- name: Test
env:
RUST_LOG: ${{ runner.debug && 'turso_core::storage=trace' || '' }}
run: cargo test --verbose
timeout-minutes: 20
- uses: actions/checkout@v3
- uses: useblacksmith/rust-cache@v3
with:
prefix-key: "v1-rust" # can be updated if we need to reset caches due to non-trivial change in the dependencies (for example, custom env var were set for single workspace project)
- name: Set up Python 3.10
uses: useblacksmith/setup-python@v6
with:
python-version: "3.10"
- name: Build
run: cargo build --verbose
- name: Test
env:
RUST_LOG: ${{ runner.debug && 'turso_core::storage=trace' || '' }}
run: cargo test --verbose
timeout-minutes: 20
clippy:
runs-on: blacksmith-4vcpu-ubuntu-2404
@@ -50,21 +50,7 @@ jobs:
- uses: actions/checkout@v3
- name: Clippy
run: |
cargo clippy --workspace --all-features --all-targets --exclude limbo-wasm -- --deny=warnings
- name: Clippy `limbo-wasm` crate `nodejs` feature
run: |
cargo clippy --package limbo-wasm --features nodejs --all-targets --no-deps -- -A clippy::all -W clippy::correctness -W clippy::perf -W clippy::suspicious --deny=warnings
- name: Clippy `limbo-wasm` crate `web` feature
run: |
cargo clippy --package limbo-wasm --no-default-features --features web --all-targets --no-deps -- -A clippy::all -W clippy::correctness -W clippy::perf -W clippy::suspicious --deny=warnings
build-wasm:
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v3
- name: Install
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
- run: wasm-pack build --target nodejs bindings/wasm
cargo clippy --workspace --all-features --all-targets -- --deny=warnings
simulator:
runs-on: blacksmith-4vcpu-ubuntu-2404
@@ -95,10 +81,10 @@ jobs:
- name: Test
run: make test
timeout-minutes: 20
# - uses: "./.github/shared/install_sqlite"
# - name: Test with index enabled
# run: SQLITE_EXEC="scripts/limbo-sqlite3-index-experimental" make test
# timeout-minutes: 20
# - uses: "./.github/shared/install_sqlite"
# - name: Test with index enabled
# run: SQLITE_EXEC="scripts/limbo-sqlite3-index-experimental" make test
# timeout-minutes: 20
test-sqlite:
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
@@ -106,4 +92,3 @@ jobs:
- uses: "./.github/shared/install_sqlite"
- name: Test
run: SQLITE_EXEC="sqlite3" make test-compat

1057
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,30 +3,29 @@
[workspace]
resolver = "2"
members = [
"bindings/dart/rust",
"bindings/go",
"bindings/java",
"bindings/javascript",
"bindings/python",
"bindings/rust",
"bindings/wasm",
"cli",
"core",
"extensions/completion",
"extensions/core",
"extensions/crypto",
"extensions/csv",
"extensions/ipaddr",
"extensions/percentile",
"extensions/regexp",
"extensions/tests",
"macros",
"simulator",
"sqlite3",
"stress",
"testing/sqlite_test_ext",
"tests",
"vendored/sqlite3-parser/sqlparser_bench",
"bindings/dart/rust",
"bindings/go",
"bindings/java",
"bindings/javascript",
"bindings/python",
"bindings/rust",
"cli",
"core",
"extensions/completion",
"extensions/core",
"extensions/crypto",
"extensions/csv",
"extensions/ipaddr",
"extensions/percentile",
"extensions/regexp",
"extensions/tests",
"macros",
"simulator",
"sqlite3",
"stress",
"testing/sqlite_test_ext",
"tests",
"vendored/sqlite3-parser/sqlparser_bench",
]
exclude = ["perf/latency/limbo"]

View File

@@ -18,7 +18,6 @@ COPY ./bindings/java ./bindings/java/
COPY ./bindings/javascript ./bindings/javascript/
COPY ./bindings/python ./bindings/python/
COPY ./bindings/rust ./bindings/rust/
COPY ./bindings/wasm ./bindings/wasm/
COPY ./cli ./cli/
COPY ./core ./core/
COPY ./extensions ./extensions/
@@ -35,7 +34,7 @@ RUN cargo chef prepare --bin turso_stress --recipe-path recipe.json
# Build the project.
#
FROM chef AS builder
FROM chef AS builder
ARG antithesis=true
@@ -62,7 +61,6 @@ COPY --from=planner /app/bindings/go ./bindings/go/
COPY --from=planner /app/bindings/javascript ./bindings/javascript/
COPY --from=planner /app/bindings/java ./bindings/java/
COPY --from=planner /app/bindings/python ./bindings/python/
COPY --from=planner /app/bindings/wasm ./bindings/wasm/
COPY --from=planner /app/core ./core/
COPY --from=planner /app/extensions ./extensions/
COPY --from=planner /app/macros ./macros/
@@ -97,7 +95,7 @@ COPY --from=builder /app/target/antithesis/turso_stress /symbols
COPY stress/docker-entrypoint.sh /bin
RUN chmod +x /bin/docker-entrypoint.sh
COPY --from=builder /app/target/wheels/* /tmp
COPY --from=builder /app/target/wheels/* /tmp
RUN pip install /tmp/*.whl
WORKDIR /app

View File

@@ -9,7 +9,7 @@ MINIMUM_TCL_VERSION := 8.6
SQLITE_EXEC ?= scripts/limbo-sqlite3
RUST_LOG := off
all: check-rust-version check-wasm-target limbo limbo-wasm
all: check-rust-version limbo
.PHONY: all
check-rust-version:
@@ -39,14 +39,6 @@ check-tcl-version:
| tclsh
.PHONY: check-tcl-version
check-wasm-target:
@echo "Checking wasm32-wasip1 target..."
@if ! rustup target list | grep -q "wasm32-wasip1 (installed)"; then \
echo "Installing wasm32-wasip1 target..."; \
rustup target add wasm32-wasip1; \
fi
.PHONY: check-wasm-target
limbo:
cargo build
.PHONY: limbo
@@ -55,11 +47,6 @@ limbo-c:
cargo cbuild
.PHONY: limbo-c
limbo-wasm:
rustup target add wasm32-wasip1
cargo build --package limbo-wasm --target wasm32-wasip1
.PHONY: limbo-wasm
uv-sync:
uv sync --all-packages
.PHONE: uv-sync

View File

@@ -40,7 +40,7 @@ Turso Database is a _work-in-progress_, in-process OLTP database engine library
* [Java](bindings/java)
* [Python](bindings/python)
* [Rust](bindings/rust)
* [WebAssembly](bindings/wasm)
* [WebAssembly](bindings/javascript)
* **Asynchronous I/O** support on Linux with `io_uring`
* **OS support** for Linux, macOS, and Windows

View File

@@ -2,3 +2,7 @@ nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.9.2.cjs
enableHardenedMode: false
supportedArchitectures:
cpu:
- current
- wasm32

View File

@@ -0,0 +1 @@
export * from '@tursodatabase/turso-wasm32-wasi'

File diff suppressed because it is too large Load Diff

View File

@@ -6,19 +6,24 @@
"url": "https://github.com/tursodatabase/turso"
},
"description": "The Turso database library",
"main": "wrapper.js",
"files": [
"wrapper.js",
"browser.js"
],
"types": "index.d.ts",
"napi": {
"binaryName": "turso",
"targets": [
"x86_64-unknown-linux-gnu",
"x86_64-pc-windows-msvc",
"universal-apple-darwin"
"universal-apple-darwin",
"wasm32-wasip1-threads"
]
},
"license": "MIT",
"devDependencies": {
"@napi-rs/cli": "^3.0.4",
"@napi-rs/wasm-runtime": "^1.0.1",
"ava": "^6.0.1",
"better-sqlite3": "^11.9.1"
},

View File

@@ -0,0 +1,60 @@
import {
createOnMessage as __wasmCreateOnMessageForFsProxy,
getDefaultContext as __emnapiGetDefaultContext,
instantiateNapiModuleSync as __emnapiInstantiateNapiModuleSync,
WASI as __WASI,
} from '@napi-rs/wasm-runtime'
const __wasi = new __WASI({
version: 'preview1',
})
const __wasmUrl = new URL('./turso.wasm32-wasi.wasm', import.meta.url).href
const __emnapiContext = __emnapiGetDefaultContext()
const __sharedMemory = new WebAssembly.Memory({
initial: 4000,
maximum: 65536,
shared: true,
})
const __wasmFile = await fetch(__wasmUrl).then((res) => res.arrayBuffer())
const {
instance: __napiInstance,
module: __wasiModule,
napiModule: __napiModule,
} = __emnapiInstantiateNapiModuleSync(__wasmFile, {
context: __emnapiContext,
asyncWorkPoolSize: 4,
wasi: __wasi,
onCreateWorker() {
const worker = new Worker(new URL('./wasi-worker-browser.mjs', import.meta.url), {
type: 'module',
})
return worker
},
overwriteImports(importObject) {
importObject.env = {
...importObject.env,
...importObject.napi,
...importObject.emnapi,
memory: __sharedMemory,
}
return importObject
},
beforeInit({ instance }) {
for (const name of Object.keys(instance.exports)) {
if (name.startsWith('__napi_register__')) {
instance.exports[name]()
}
}
},
})
export default __napiModule.exports
export const Database = __napiModule.exports.Database
export const Statement = __napiModule.exports.Statement

View File

@@ -0,0 +1,112 @@
/* eslint-disable */
/* prettier-ignore */
/* auto-generated by NAPI-RS */
const __nodeFs = require('node:fs')
const __nodePath = require('node:path')
const { WASI: __nodeWASI } = require('node:wasi')
const { Worker } = require('node:worker_threads')
const {
createOnMessage: __wasmCreateOnMessageForFsProxy,
getDefaultContext: __emnapiGetDefaultContext,
instantiateNapiModuleSync: __emnapiInstantiateNapiModuleSync,
} = require('@napi-rs/wasm-runtime')
const __rootDir = __nodePath.parse(process.cwd()).root
const __wasi = new __nodeWASI({
version: 'preview1',
env: process.env,
preopens: {
[__rootDir]: __rootDir,
}
})
const __emnapiContext = __emnapiGetDefaultContext()
const __sharedMemory = new WebAssembly.Memory({
initial: 4000,
maximum: 65536,
shared: true,
})
let __wasmFilePath = __nodePath.join(__dirname, 'turso.wasm32-wasi.wasm')
const __wasmDebugFilePath = __nodePath.join(__dirname, 'turso.wasm32-wasi.debug.wasm')
if (__nodeFs.existsSync(__wasmDebugFilePath)) {
__wasmFilePath = __wasmDebugFilePath
} else if (!__nodeFs.existsSync(__wasmFilePath)) {
try {
__wasmFilePath = __nodePath.resolve('@tursodatabase/turso-wasm32-wasi')
} catch {
throw new Error('Cannot find turso.wasm32-wasi.wasm file, and @tursodatabase/turso-wasm32-wasi package is not installed.')
}
}
const { instance: __napiInstance, module: __wasiModule, napiModule: __napiModule } = __emnapiInstantiateNapiModuleSync(__nodeFs.readFileSync(__wasmFilePath), {
context: __emnapiContext,
asyncWorkPoolSize: (function() {
const threadsSizeFromEnv = Number(process.env.NAPI_RS_ASYNC_WORK_POOL_SIZE ?? process.env.UV_THREADPOOL_SIZE)
// NaN > 0 is false
if (threadsSizeFromEnv > 0) {
return threadsSizeFromEnv
} else {
return 4
}
})(),
reuseWorker: true,
wasi: __wasi,
onCreateWorker() {
const worker = new Worker(__nodePath.join(__dirname, 'wasi-worker.mjs'), {
env: process.env,
})
worker.onmessage = ({ data }) => {
__wasmCreateOnMessageForFsProxy(__nodeFs)(data)
}
// The main thread of Node.js waits for all the active handles before exiting.
// But Rust threads are never waited without `thread::join`.
// So here we hack the code of Node.js to prevent the workers from being referenced (active).
// According to https://github.com/nodejs/node/blob/19e0d472728c79d418b74bddff588bea70a403d0/lib/internal/worker.js#L415,
// a worker is consist of two handles: kPublicPort and kHandle.
{
const kPublicPort = Object.getOwnPropertySymbols(worker).find(s =>
s.toString().includes("kPublicPort")
);
if (kPublicPort) {
worker[kPublicPort].ref = () => {};
}
const kHandle = Object.getOwnPropertySymbols(worker).find(s =>
s.toString().includes("kHandle")
);
if (kHandle) {
worker[kHandle].ref = () => {};
}
worker.unref();
}
return worker
},
overwriteImports(importObject) {
importObject.env = {
...importObject.env,
...importObject.napi,
...importObject.emnapi,
memory: __sharedMemory,
}
return importObject
},
beforeInit({ instance }) {
for (const name of Object.keys(instance.exports)) {
if (name.startsWith('__napi_register__')) {
instance.exports[name]()
}
}
},
})
module.exports = __napiModule.exports
module.exports.Database = __napiModule.exports.Database
module.exports.Statement = __napiModule.exports.Statement

View File

@@ -0,0 +1,32 @@
import { instantiateNapiModuleSync, MessageHandler, WASI } from '@napi-rs/wasm-runtime'
const handler = new MessageHandler({
onLoad({ wasmModule, wasmMemory }) {
const wasi = new WASI({
print: function () {
// eslint-disable-next-line no-console
console.log.apply(console, arguments)
},
printErr: function() {
// eslint-disable-next-line no-console
console.error.apply(console, arguments)
},
})
return instantiateNapiModuleSync(wasmModule, {
childThread: true,
wasi,
overwriteImports(importObject) {
importObject.env = {
...importObject.env,
...importObject.napi,
...importObject.emnapi,
memory: wasmMemory,
}
},
})
},
})
globalThis.onmessage = function (e) {
handler.handle(e)
}

View File

@@ -0,0 +1,63 @@
import fs from "node:fs";
import { createRequire } from "node:module";
import { parse } from "node:path";
import { WASI } from "node:wasi";
import { parentPort, Worker } from "node:worker_threads";
const require = createRequire(import.meta.url);
const { instantiateNapiModuleSync, MessageHandler, getDefaultContext } = require("@napi-rs/wasm-runtime");
if (parentPort) {
parentPort.on("message", (data) => {
globalThis.onmessage({ data });
});
}
Object.assign(globalThis, {
self: globalThis,
require,
Worker,
importScripts: function (f) {
;(0, eval)(fs.readFileSync(f, "utf8") + "//# sourceURL=" + f);
},
postMessage: function (msg) {
if (parentPort) {
parentPort.postMessage(msg);
}
},
});
const emnapiContext = getDefaultContext();
const __rootDir = parse(process.cwd()).root;
const handler = new MessageHandler({
onLoad({ wasmModule, wasmMemory }) {
const wasi = new WASI({
version: 'preview1',
env: process.env,
preopens: {
[__rootDir]: __rootDir,
},
});
return instantiateNapiModuleSync(wasmModule, {
childThread: true,
wasi,
context: emnapiContext,
overwriteImports(importObject) {
importObject.env = {
...importObject.env,
...importObject.napi,
...importObject.emnapi,
memory: wasmMemory
};
},
});
},
});
globalThis.onmessage = function (e) {
handler.handle(e);
};

View File

@@ -1073,6 +1073,7 @@ __metadata:
resolution: "@tursodatabase/turso@workspace:."
dependencies:
"@napi-rs/cli": "npm:^3.0.4"
"@napi-rs/wasm-runtime": "npm:^1.0.1"
ava: "npm:^6.0.1"
better-sqlite3: "npm:^11.9.1"
languageName: unknown

View File

@@ -1,5 +0,0 @@
node_modules/
*.wasm
**/dist/
limbo-wasm*tgz
claude.md

View File

@@ -1,25 +0,0 @@
[package]
name = "limbo-wasm"
version.workspace = true
authors.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[lib]
crate-type = ["cdylib"]
path = "lib.rs"
[dependencies]
console_error_panic_hook = "0.1.7"
js-sys = "0.3.72"
turso_core = { path = "../../core", default-features = false }
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
web-sys = "0.3"
getrandom = { version = "0.2.15", features = ["js"] }
[features]
web = []
nodejs = []
default = ["nodejs"]

View File

@@ -1,42 +0,0 @@
# Limbo Wasm bindings
This source tree contains Limbo Wasm bindings.
## Building
For nodejs
```
./scripts/build
```
For web
```
./scripts/build web
```
# 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)

View File

@@ -1,121 +0,0 @@
# class Database
The `Database` class represents a connection that can prepare and execute SQL statements.
## Methods
### new Database(path) ⇒ Database
Creates a new database connection.
| Param | Type | Description |
| ------- | ------------------- | ------------------------- |
| path | <code>string</code> | Path to the database file |
### prepare(sql) ⇒ Statement
Prepares a SQL statement for execution.
| Param | Type | Description |
| ------ | ------------------- | ------------------------------------ |
| sql | <code>string</code> | The SQL statement string to prepare. |
The function returns a `Statement` object.
### transaction(function) ⇒ function
This function is currently not supported.
### pragma(string, [options]) ⇒ results
This function is currently not supported.
### backup(destination, [options]) ⇒ promise
This function is currently not supported.
### serialize([options]) ⇒ Buffer
This function is currently not supported.
### function(name, [options], function) ⇒ this
This function is currently not supported.
### aggregate(name, options) ⇒ this
This function is currently not supported.
### table(name, definition) ⇒ this
This function is currently not supported.
### loadExtension(path, [entryPoint]) ⇒ this
This function is currently not supported.
### exec(sql) ⇒ this
This function is currently not supported.
### close() ⇒ this
This function is currently not supported.
# class Statement
## Methods
### run([...bindParameters]) ⇒ object
This function is currently not supported.
### get([...bindParameters]) ⇒ row
Executes the SQL statement and returns the first row.
| Param | Type | Description |
| -------------- | ----------------------------- | ------------------------------------------------ |
| bindParameters | <code>array of objects</code> | The bind parameters for executing the statement. |
### all([...bindParameters]) ⇒ array of rows
Executes the SQL statement and returns an array of the resulting rows.
| Param | Type | Description |
| -------------- | ----------------------------- | ------------------------------------------------ |
| bindParameters | <code>array of objects</code> | The bind parameters for executing the statement. |
### iterate([...bindParameters]) ⇒ iterator
Executes the SQL statement and returns an iterator to the resulting rows.
| Param | Type | Description |
| -------------- | ----------------------------- | ------------------------------------------------ |
| bindParameters | <code>array of objects</code> | The bind parameters for executing the statement. |
### pluck([toggleState]) ⇒ this
This function is currently not supported.
### expand([toggleState]) ⇒ this
This function is currently not supported.
### raw([rawMode]) ⇒ this
Toggle raw mode.
| Param | Type | Description |
| ------- | -------------------- | --------------------------------------------------------------------------------- |
| rawMode | <code>boolean</code> | Enable or disable raw mode. If you don't pass the parameter, raw mode is enabled. |
This function enables or disables raw mode. Prepared statements return objects by default, but if raw mode is enabled, the functions return arrays instead.
### columns() ⇒ array of objects
This function is currently not supported.
### bind([...bindParameters]) ⇒ this
This function is currently not supported.

View File

@@ -1,12 +0,0 @@
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 users = s.sqliteTable("users", {
id: s.integer(),
name: s.text(),
})
const result = db.select().from(users).all();
console.log(result);

View File

@@ -1,13 +0,0 @@
import { Database } from 'limbo-wasm';
const db = new Database('hello.db');
db.exec('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)');
db.exec('INSERT INTO users (name) VALUES (\'Alice\')');
const stmt = db.prepare('SELECT * FROM users');
const users = stmt.all();
console.log(users);

View File

@@ -1,18 +0,0 @@
{
"name": "limbo-example",
"version": "1.0.0",
"description": "",
"type": "module",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "MIT",
"dependencies": {
"better-sqlite3": "^11.5.0",
"drizzle-orm": "^0.36.3",
"limbo-wasm": ".."
}
}

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 && rm *.db *.db-wal"
},
"devDependencies": {
"ava": "^6.2.0"
},
"dependencies": {
"better-sqlite3": "^11.7.0",
"limbo-wasm": ".."
}
}

View File

@@ -1,90 +0,0 @@
import test from "ava";
import { unlinkSync, existsSync } from "node:fs"
test.beforeEach(async (t) => {
const [db, errorType, provider] = await connect();
// DROP TABLE IF EXISTS users; <- is not supported in lib.rs yet
db.exec(`
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) => {
// delete hello.db if it exists
if (existsSync("hello.db")) {
unlinkSync("hello.db");
}
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,439 +0,0 @@
#[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<turso_core::Database>,
conn: Arc<turso_core::Connection>,
}
#[allow(clippy::arc_with_non_send_sync)]
#[wasm_bindgen]
impl Database {
#[wasm_bindgen(constructor)]
pub fn new(path: &str) -> Database {
let io: Arc<dyn turso_core::IO> = 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<turso_core::Statement>,
}
#[wasm_bindgen]
impl RowIterator {
fn new(inner: RefCell<turso_core::Statement>) -> 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<turso_core::Statement>,
raw: bool,
}
#[wasm_bindgen]
impl Statement {
fn new(inner: RefCell<turso_core::Statement>, raw: bool) -> Self {
Self { inner, raw }
}
#[wasm_bindgen]
pub fn raw(mut self, toggle: Option<bool>) -> 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<turso_core::Completion>,
) -> Result<Arc<turso_core::Completion>> {
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<std::cell::RefCell<turso_core::Buffer>>,
c: Arc<turso_core::Completion>,
) -> Result<Arc<turso_core::Completion>> {
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<turso_core::Completion>) -> Result<Arc<turso_core::Completion>> {
self.vfs.sync(self.fd);
c.complete(0);
#[allow(clippy::arc_with_non_send_sync)]
Ok(c)
}
fn size(&self) -> Result<u64> {
Ok(self.vfs.size(self.fd))
}
}
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<Arc<dyn turso_core::File>> {
let fd = self.vfs.open(path, "a+");
Ok(Arc::new(File {
vfs: VFS::new(),
fd,
}))
}
fn wait_for_completion(&self, c: Arc<turso_core::Completion>) -> 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<turso_core::MemoryIO> {
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<dyn turso_core::File>,
}
unsafe impl Send for DatabaseFile {}
unsafe impl Sync for DatabaseFile {}
impl DatabaseFile {
pub fn new(file: Arc<dyn turso_core::File>) -> 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<std::cell::RefCell<turso_core::Buffer>>,
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<u64> {
self.file.size()
}
}
#[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 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 sync(this: &VFS, fd: i32);
}
#[wasm_bindgen(start)]
pub fn init() {
console_error_panic_hook::set_once();
}

View File

@@ -1,33 +0,0 @@
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);
}
}
module.exports = { VFS };

View File

@@ -1,11 +0,0 @@
{
"name": "limbo-perf",
"type": "module",
"private": true,
"dependencies": {
"limbo-wasm": "../pkg",
"better-sqlite3": "^9.5.0",
"libsql": "..",
"mitata": "^0.1.11"
}
}

View File

@@ -1,44 +0,0 @@
import { run, bench, group, baseline } from 'mitata';
import Database from 'better-sqlite3';
const db = new Database('better-sqlite3.db');
db.exec('CREATE TABLE t (x)');
db.exec('INSERT INTO t VALUES (1)');
db.exec('INSERT INTO t VALUES (2)');
db.exec('INSERT INTO t VALUES (3)');
group('SQL queries [all()]', () => {
const stmt1 = db.prepare("SELECT 1");
bench('SELECT 1', () => {
stmt1.all();
});
const stmt2 = db.prepare("SELECT * FROM t");
bench('SELECT * FROM t', () => {
stmt2.all();
});
});
group('SQL queries [iterate()]', () => {
const stmt1 = db.prepare("SELECT 1");
const it1 = stmt1.iterate();
bench('SELECT 1', () => {
it1.next();
});
const stmt2 = db.prepare("SELECT * FROM t");
const it2 = stmt2.iterate();
bench('SELECT * FROM t', () => {
it2.next();
});
});
await run({
units: false,
silent: false,
avg: true,
json: false,
colors: true,
min_max: true,
percentiles: true,
});

View File

@@ -1,44 +0,0 @@
import { run, bench, group, baseline } from 'mitata';
import { Database } from 'limbo-wasm';
const db = new Database('limbo.db');
db.exec('CREATE TABLE t (x)');
db.exec('INSERT INTO t VALUES (1)');
db.exec('INSERT INTO t VALUES (2)');
db.exec('INSERT INTO t VALUES (3)');
group('SQL queries [all()]', () => {
const stmt1 = db.prepare("SELECT 1");
bench('SELECT 1', () => {
stmt1.all();
});
const stmt2 = db.prepare("SELECT * FROM t");
bench('SELECT * FROM t', () => {
stmt2.all();
});
});
group('SQL queries [iterate()]', () => {
const stmt1 = db.prepare("SELECT 1");
const it1 = stmt1.iterate();
bench('SELECT 1', () => {
it1.next();
});
const stmt2 = db.prepare("SELECT * FROM t");
const it2 = stmt2.iterate();
bench('SELECT * FROM t', () => {
it2.next();
});
});
await run({
units: false,
silent: false,
avg: true,
json: false,
colors: true,
min_max: true,
percentiles: true,
});

View File

@@ -1,42 +0,0 @@
#!/bin/bash
set -e
# Define final output directories
NODE_DIR="node"
WEB_DIR="web"
rm -rf $NODE_DIR/dist $WEB_DIR/dist pkg
mkdir -p $NODE_DIR/dist $WEB_DIR/dist pkg
# Build Node.js target
npx wasm-pack build \
--target nodejs \
--out-name index \
--no-default-features \
--features nodejs
rm -rf pkg/package.json # don't want generated package.json
mv pkg/* $NODE_DIR/dist/
cp -r $NODE_DIR/src/* $NODE_DIR/dist/
cd node/dist/
for f in *.js; do
cp "$f" "${f%.js}.cjs"
done
rm *.js
cd ../../
rm -r pkg
# Build web target
npx wasm-pack build \
--target web \
--out-name index \
--no-default-features \
--features web
rm -rf pkg/package.json # don't want generated package.json
mv pkg/* $WEB_DIR/dist/
cp -r $WEB_DIR/src/* $WEB_DIR/dist/
# mv $WEB_DIR/index.js $WEB_DIR/index.mjs
rm -rf pkg

View File

@@ -1 +0,0 @@
test.db

View File

@@ -1,11 +0,0 @@
// import { Database } from "limbo-wasm/node";
const { Database } = require("limbo-wasm");
// Rest of your code...
const db = new Database("test.db");
db.exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)");
db.exec("INSERT INTO users (name) VALUES ('test')");
const stmt = db.prepare("SELECT * FROM users");
console.log(stmt.all());

View File

@@ -1,86 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Limbo Test</title>
</head>
<body>
<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);
});
}
async function runTests() {
// const worker = new Worker('./src/limbo-worker.js', { type: 'module' });
// const worker = new Worker('limbo-wasm/src/limbo-worker.js', { type: 'module' });
// const worker = new Worker('./node_modules/limbo-wasm/src/limbo-worker.js', { type: 'module' });
const worker = new Worker(new URL('limbo-wasm/limbo-worker.js', import.meta.url), { type: 'module' });
// Wait for ready then send createDb
await waitForMessage(worker, 'ready');
worker.postMessage({
op: 'createDb',
path: 'test.db'
});
// 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");
// 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');
`
});
await waitForMessage(worker, 'success', 'exec');
worker.postMessage({
op: 'exec',
sql: `
INSERT INTO users VALUES (2, 'Bob', 'bob@example.org');
`
});
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>

View File

@@ -1,962 +0,0 @@
{
"name": "test-limbo",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "test-limbo",
"dependencies": {
"limbo-wasm": ".."
},
"devDependencies": {
"vite": "^6.2.6",
"vite-plugin-wasm": "^3.4.1"
}
},
"..": {},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz",
"integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.2.tgz",
"integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.2.tgz",
"integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.2.tgz",
"integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz",
"integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.2.tgz",
"integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.2.tgz",
"integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.2.tgz",
"integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.2.tgz",
"integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.2.tgz",
"integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.2.tgz",
"integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.2.tgz",
"integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.2.tgz",
"integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.2.tgz",
"integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.2.tgz",
"integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.2.tgz",
"integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz",
"integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.2.tgz",
"integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.2.tgz",
"integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.2.tgz",
"integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.2.tgz",
"integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz",
"integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.2.tgz",
"integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.2.tgz",
"integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz",
"integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.30.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.30.1.tgz",
"integrity": "sha512-pSWY+EVt3rJ9fQ3IqlrEUtXh3cGqGtPDH1FQlNZehO2yYxCHEX1SPsz1M//NXwYfbTlcKr9WObLnJX9FsS9K1Q==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.30.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.30.1.tgz",
"integrity": "sha512-/NA2qXxE3D/BRjOJM8wQblmArQq1YoBVJjrjoTSBS09jgUisq7bqxNHJ8kjCHeV21W/9WDGwJEWSN0KQ2mtD/w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.30.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.30.1.tgz",
"integrity": "sha512-r7FQIXD7gB0WJ5mokTUgUWPl0eYIH0wnxqeSAhuIwvnnpjdVB8cRRClyKLQr7lgzjctkbp5KmswWszlwYln03Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.30.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.30.1.tgz",
"integrity": "sha512-x78BavIwSH6sqfP2xeI1hd1GpHL8J4W2BXcVM/5KYKoAD3nNsfitQhvWSw+TFtQTLZ9OmlF+FEInEHyubut2OA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.30.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.30.1.tgz",
"integrity": "sha512-HYTlUAjbO1z8ywxsDFWADfTRfTIIy/oUlfIDmlHYmjUP2QRDTzBuWXc9O4CXM+bo9qfiCclmHk1x4ogBjOUpUQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.30.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.30.1.tgz",
"integrity": "sha512-1MEdGqogQLccphhX5myCJqeGNYTNcmTyaic9S7CG3JhwuIByJ7J05vGbZxsizQthP1xpVx7kd3o31eOogfEirw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.30.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.30.1.tgz",
"integrity": "sha512-PaMRNBSqCx7K3Wc9QZkFx5+CX27WFpAMxJNiYGAXfmMIKC7jstlr32UhTgK6T07OtqR+wYlWm9IxzennjnvdJg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.30.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.30.1.tgz",
"integrity": "sha512-B8Rcyj9AV7ZlEFqvB5BubG5iO6ANDsRKlhIxySXcF1axXYUyqwBok+XZPgIYGBgs7LDXfWfifxhw0Ik57T0Yug==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.30.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.30.1.tgz",
"integrity": "sha512-hqVyueGxAj3cBKrAI4aFHLV+h0Lv5VgWZs9CUGqr1z0fZtlADVV1YPOij6AhcK5An33EXaxnDLmJdQikcn5NEw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.30.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.30.1.tgz",
"integrity": "sha512-i4Ab2vnvS1AE1PyOIGp2kXni69gU2DAUVt6FSXeIqUCPIR3ZlheMW3oP2JkukDfu3PsexYRbOiJrY+yVNSk9oA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
"version": "4.30.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.30.1.tgz",
"integrity": "sha512-fARcF5g296snX0oLGkVxPmysetwUk2zmHcca+e9ObOovBR++9ZPOhqFUM61UUZ2EYpXVPN1redgqVoBB34nTpQ==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.30.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.30.1.tgz",
"integrity": "sha512-GLrZraoO3wVT4uFXh67ElpwQY0DIygxdv0BNW9Hkm3X34wu+BkqrDrkcsIapAY+N2ATEbvak0XQ9gxZtCIA5Rw==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.30.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.30.1.tgz",
"integrity": "sha512-0WKLaAUUHKBtll0wvOmh6yh3S0wSU9+yas923JIChfxOaaBarmb/lBKPF0w/+jTVozFnOXJeRGZ8NvOxvk/jcw==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.30.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.30.1.tgz",
"integrity": "sha512-GWFs97Ruxo5Bt+cvVTQkOJ6TIx0xJDD/bMAOXWJg8TCSTEK8RnFeOeiFTxKniTc4vMIaWvCplMAFBt9miGxgkA==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.30.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.30.1.tgz",
"integrity": "sha512-UtgGb7QGgXDIO+tqqJ5oZRGHsDLO8SlpE4MhqpY9Llpzi5rJMvrK6ZGhsRCST2abZdBqIBeXW6WPD5fGK5SDwg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.30.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.30.1.tgz",
"integrity": "sha512-V9U8Ey2UqmQsBT+xTOeMzPzwDzyXmnAoO4edZhL7INkwQcaW1Ckv3WJX3qrrp/VHaDkEWIBWhRwP47r8cdrOow==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.30.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.30.1.tgz",
"integrity": "sha512-WabtHWiPaFF47W3PkHnjbmWawnX/aE57K47ZDT1BXTS5GgrBUEpvOzq0FI0V/UYzQJgdb8XlhVNH8/fwV8xDjw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.30.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.30.1.tgz",
"integrity": "sha512-pxHAU+Zv39hLUTdQQHUVHf4P+0C47y/ZloorHpzs2SXMRqeAWmGghzAhfOlzFHHwjvgokdFAhC4V+6kC1lRRfw==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.30.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.30.1.tgz",
"integrity": "sha512-D6qjsXGcvhTjv0kI4fU8tUuBDF/Ueee4SVX79VfNDXZa64TfCW1Slkb6Z7O1p7vflqZjcmOVdZlqf8gvJxc6og==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@types/estree": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
"dev": true,
"license": "MIT"
},
"node_modules/esbuild": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz",
"integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.25.2",
"@esbuild/android-arm": "0.25.2",
"@esbuild/android-arm64": "0.25.2",
"@esbuild/android-x64": "0.25.2",
"@esbuild/darwin-arm64": "0.25.2",
"@esbuild/darwin-x64": "0.25.2",
"@esbuild/freebsd-arm64": "0.25.2",
"@esbuild/freebsd-x64": "0.25.2",
"@esbuild/linux-arm": "0.25.2",
"@esbuild/linux-arm64": "0.25.2",
"@esbuild/linux-ia32": "0.25.2",
"@esbuild/linux-loong64": "0.25.2",
"@esbuild/linux-mips64el": "0.25.2",
"@esbuild/linux-ppc64": "0.25.2",
"@esbuild/linux-riscv64": "0.25.2",
"@esbuild/linux-s390x": "0.25.2",
"@esbuild/linux-x64": "0.25.2",
"@esbuild/netbsd-arm64": "0.25.2",
"@esbuild/netbsd-x64": "0.25.2",
"@esbuild/openbsd-arm64": "0.25.2",
"@esbuild/openbsd-x64": "0.25.2",
"@esbuild/sunos-x64": "0.25.2",
"@esbuild/win32-arm64": "0.25.2",
"@esbuild/win32-ia32": "0.25.2",
"@esbuild/win32-x64": "0.25.2"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/limbo-wasm": {
"resolved": "..",
"link": true
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true,
"license": "ISC"
},
"node_modules/postcss": {
"version": "8.5.3",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
"integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.8",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/rollup": {
"version": "4.30.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.30.1.tgz",
"integrity": "sha512-mlJ4glW020fPuLi7DkM/lN97mYEZGWeqBnrljzN0gs7GLctqX3lNWxKQ7Gl712UAX+6fog/L3jh4gb7R6aVi3w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "1.0.6"
},
"bin": {
"rollup": "dist/bin/rollup"
},
"engines": {
"node": ">=18.0.0",
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.30.1",
"@rollup/rollup-android-arm64": "4.30.1",
"@rollup/rollup-darwin-arm64": "4.30.1",
"@rollup/rollup-darwin-x64": "4.30.1",
"@rollup/rollup-freebsd-arm64": "4.30.1",
"@rollup/rollup-freebsd-x64": "4.30.1",
"@rollup/rollup-linux-arm-gnueabihf": "4.30.1",
"@rollup/rollup-linux-arm-musleabihf": "4.30.1",
"@rollup/rollup-linux-arm64-gnu": "4.30.1",
"@rollup/rollup-linux-arm64-musl": "4.30.1",
"@rollup/rollup-linux-loongarch64-gnu": "4.30.1",
"@rollup/rollup-linux-powerpc64le-gnu": "4.30.1",
"@rollup/rollup-linux-riscv64-gnu": "4.30.1",
"@rollup/rollup-linux-s390x-gnu": "4.30.1",
"@rollup/rollup-linux-x64-gnu": "4.30.1",
"@rollup/rollup-linux-x64-musl": "4.30.1",
"@rollup/rollup-win32-arm64-msvc": "4.30.1",
"@rollup/rollup-win32-ia32-msvc": "4.30.1",
"@rollup/rollup-win32-x64-msvc": "4.30.1",
"fsevents": "~2.3.2"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/vite": {
"version": "6.2.6",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.6.tgz",
"integrity": "sha512-9xpjNl3kR4rVDZgPNdTL0/c6ao4km69a/2ihNQbcANz8RuCOK3hQBmLSJf3bRKVQjVMda+YvizNE8AwvogcPbw==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.25.0",
"postcss": "^8.5.3",
"rollup": "^4.30.1"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
},
"funding": {
"url": "https://github.com/vitejs/vite?sponsor=1"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
},
"peerDependencies": {
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
"jiti": ">=1.21.0",
"less": "*",
"lightningcss": "^1.21.0",
"sass": "*",
"sass-embedded": "*",
"stylus": "*",
"sugarss": "*",
"terser": "^5.16.0",
"tsx": "^4.8.1",
"yaml": "^2.4.2"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
},
"jiti": {
"optional": true
},
"less": {
"optional": true
},
"lightningcss": {
"optional": true
},
"sass": {
"optional": true
},
"sass-embedded": {
"optional": true
},
"stylus": {
"optional": true
},
"sugarss": {
"optional": true
},
"terser": {
"optional": true
},
"tsx": {
"optional": true
},
"yaml": {
"optional": true
}
}
},
"node_modules/vite-plugin-wasm": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/vite-plugin-wasm/-/vite-plugin-wasm-3.4.1.tgz",
"integrity": "sha512-ja3nSo2UCkVeitltJGkS3pfQHAanHv/DqGatdI39ja6McgABlpsZ5hVgl6wuR8Qx5etY3T5qgDQhOWzc5RReZA==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"vite": "^2 || ^3 || ^4 || ^5 || ^6"
}
}
}
}

View File

@@ -1,15 +0,0 @@
{
"name": "test-limbo",
"private": true,
"type": "module",
"dependencies": {
"limbo-wasm": ".."
},
"scripts": {
"dev": "vite"
},
"devDependencies": {
"vite": "^6.2.6",
"vite-plugin-wasm": "^3.4.1"
}
}

View File

@@ -1,24 +0,0 @@
import { defineConfig } from "vite";
import wasm from "vite-plugin-wasm";
export default defineConfig({
plugins: [wasm()],
server: {
headers: {
"Cross-Origin-Embedder-Policy": "require-corp",
"Cross-Origin-Opener-Policy": "same-origin",
"Cross-Origin-Resource-Policy": "cross-origin",
},
fs: {
allow: ["../web/dist"],
},
},
worker: {
format: "es",
rollupOptions: {
output: {
format: "es",
},
},
},
});

View File

@@ -1,9 +0,0 @@
<!DOCTYPE html>
<html>
<body>
<script type="module">
import { VFSInterface } from './src/opfs-interface.js';
window.VFSInterface = VFSInterface;
</script>
</body>
</html>

View File

@@ -1,83 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Limbo Test</title>
</head>
<body>
<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);
});
}
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'
});
// 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");
// 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');
`
});
await waitForMessage(worker, 'success', 'exec');
worker.postMessage({
op: 'exec',
sql: `
INSERT INTO users VALUES (2, 'Bob', 'bob@example.org');
`
});
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>

View File

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

View File

@@ -1,13 +0,0 @@
// 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,74 +0,0 @@
import { VFS } from "./opfs.js";
import init, { Database } from "../dist/index.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": {
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() });
});
// logLevel:
//
// 0 = no logging output
// 1 = only errors
// 2 = warnings and errors
// 3 = debug, warnings, and errors
const logLevel = 1;
const loggers = {
0: console.error.bind(console),
1: console.warn.bind(console),
2: console.log.bind(console),
};
const logImpl = (level, ...args) => {
if (logLevel > level) loggers[level]("OPFS asyncer:", ...args);
};
const log = (...args) => logImpl(2, ...args);
const warn = (...args) => logImpl(1, ...args);
const error = (...args) => logImpl(0, ...args);

View File

@@ -1,67 +0,0 @@
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

@@ -1,136 +0,0 @@
// 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) => {
log("handle message: ", e.data);
if (e.data.cmd === "init") {
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) {
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 });
log("opfssync read: size: ", readBuffer.byteLength);
const tmp = new Uint8Array(readBuffer);
log("opfssync read buffer: ", [...tmp]);
transferArray.set(tmp);
return { success: true, length: readSize };
}
function handleWrite(fd, buffer, offset) {
log("opfssync buffer size:", buffer.byteLength);
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 {
log("opfs-sync-proxy: result.length: ", result.length);
statusView.setInt32(4, result?.length || 0, true);
}
Atomics.store(statusArray, 0, 1);
Atomics.notify(statusArray, 0);
}
// logLevel:
//
// 0 = no logging output
// 1 = only errors
// 2 = warnings and errors
// 3 = debug, warnings, and errors
const logLevel = 1;
const loggers = {
0: console.error.bind(console),
1: console.warn.bind(console),
2: console.log.bind(console),
};
const logImpl = (level, ...args) => {
if (logLevel > level) loggers[level]("OPFS asyncer:", ...args);
};
const log = (...args) => logImpl(2, ...args);
const warn = (...args) => logImpl(1, ...args);
const error = (...args) => logImpl(0, ...args);

View File

@@ -1,57 +0,0 @@
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");

View File

@@ -1,154 +0,0 @@
// 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) => {
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);
log("opfs.js open result: ", result);
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,
);
log("opfs pread buffer: ", [...buffer]);
bytesRead += readSize;
if (readSize < chunkSize) break;
}
return bytesRead;
}
pwrite(fd, buffer, offset) {
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);
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);
log("opfs.js size result: ", result);
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);
}
}
// logLevel:
//
// 0 = no logging output
// 1 = only errors
// 2 = warnings and errors
// 3 = debug, warnings, and errors
const logLevel = 1;
const loggers = {
0: console.error.bind(console),
1: console.warn.bind(console),
2: console.log.bind(console),
};
const logImpl = (level, ...args) => {
if (logLevel > level) loggers[level]("OPFS asyncer:", ...args);
};
const log = (...args) => logImpl(2, ...args);
const warn = (...args) => logImpl(1, ...args);
const error = (...args) => logImpl(0, ...args);
export { VFS };

View File

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

View File

@@ -1,23 +0,0 @@
import { createServer } from "vite";
import { chromium } from "playwright";
export async function setupTestEnvironment(port) {
const server = await createServer({
configFile: "./vite.config.js",
root: ".",
server: { port },
});
await server.listen();
const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();
globalThis.__page__ = page;
return { server, browser, context, page };
}
export async function teardownTestEnvironment({ server, browser, context }) {
await context.close();
await browser.close();
await server.close();
}

View File

@@ -1,72 +0,0 @@
import { afterAll, beforeAll, beforeEach, expect, test } from "vitest";
import { setupTestEnvironment, teardownTestEnvironment } from "./helpers.js";
let testEnv;
beforeAll(async () => {
testEnv = await setupTestEnvironment(5174);
});
beforeEach(async () => {
const { page } = testEnv;
await page.goto("http://localhost:5174/limbo-test.html");
});
afterAll(async () => {
await teardownTestEnvironment(testEnv);
});
test("basic database operations", async () => {
const { page } = testEnv;
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}`);
expect(result.result).toHaveLength(1);
expect(result.result[0]).toEqual([1, "Alice", "alice@example.org"]);
});

View File

@@ -1,141 +0,0 @@
// test/opfs.test.js
import { afterAll, beforeAll, beforeEach, expect, test } from "vitest";
import { setupTestEnvironment, teardownTestEnvironment } from "./helpers.js";
let testEnv;
beforeAll(async () => {
testEnv = await setupTestEnvironment(5173);
});
beforeEach(async () => {
const { page } = testEnv;
await page.goto("http://localhost:5173/index.html");
await page.waitForFunction(() => window.VFSInterface !== undefined);
});
afterAll(async () => {
await teardownTestEnvironment(testEnv);
});
const opfsImport = "./src/opfs-worker.js";
test("basic read/write functionality", async () => {
const { page } = testEnv;
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 } = testEnv;
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 } = testEnv;
const result = await page.evaluate(async () => {
const vfs = new window.VFSInterface("./src/opfs-worker.js");
let fd;
try {
fd = await vfs.open("partial.txt", {});
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);
const readData1 = new Uint8Array(2);
const readData2 = new Uint8Array(4);
const readData3 = new Uint8Array(2);
await vfs.pread(fd, readData1, 0);
await vfs.pread(fd, readData2, 2);
await vfs.pread(fd, readData3, 6);
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 } = testEnv;
const result = await page.evaluate(async () => {
const vfs = new window.VFSInterface("./src/opfs-worker.js");
let fd;
try {
fd = await vfs.open("size.txt", {});
const writeData1 = new Uint8Array([1, 2, 3, 4]);
await vfs.pwrite(fd, writeData1, 0);
const size1 = await vfs.size(fd);
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

@@ -1,28 +0,0 @@
import { defineConfig } from "vite";
import wasm from "vite-plugin-wasm";
export default defineConfig({
publicDir: "./html",
root: "./",
plugins: [wasm()],
test: {
globals: true,
setupFiles: ["./test/setup.js"],
include: ["test/*.test.js"],
},
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",
},
},
},
});

View File

@@ -20,7 +20,7 @@ NPM_PACKAGES = [
"bindings/javascript/npm/darwin-universal",
"bindings/javascript/npm/linux-x64-gnu",
"bindings/javascript/npm/win32-x64-msvc",
"bindings/wasm",
"bindings/javascript/npm/wasm32-wasip1-threads",
]