From 08bcf98e679d5713fdf15dbe41e0769e19db5352 Mon Sep 17 00:00:00 2001 From: Sebastian Jeltsch Date: Mon, 6 Oct 2025 17:12:28 +0200 Subject: [PATCH] Disable V8 runtime by default (custom builds can still enable it) and add a truly static binary release for Linux using MUSL. Building with MUSL required vendoring sqlite-vec and OpenSSL. Also clean up no-longer-supported TypeScript guest scripts. --- .github/workflows/release.yml | 19 +- .gitmodules | 3 + Cargo.lock | 16 +- Cargo.toml | 2 + client/testfixture/scripts.delme/index.ts | 198 ------------------ crates/cli/Cargo.toml | 4 +- crates/client/src/lib.rs | 2 +- crates/core/Cargo.toml | 2 +- crates/core/src/server/mod.rs | 3 +- crates/extension/Cargo.toml | 2 +- .../src/content/docs/reference/benchmarks.mdx | 11 +- .../traildepot/scripts.delme/main.ts | 25 --- .../traildepot/scripts.delme/main.ts | 49 ----- vendor/sqlite-vec | 1 + 14 files changed, 40 insertions(+), 297 deletions(-) delete mode 100644 client/testfixture/scripts.delme/index.ts delete mode 100644 examples/coffee-vector-search/traildepot/scripts.delme/main.ts delete mode 100644 examples/collab-clicker-ssr/traildepot/scripts.delme/main.ts create mode 160000 vendor/sqlite-vec diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 249d9970..a4b685b1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,7 +24,7 @@ jobs: generate_release_notes: true body_path: ./CHANGELOG-release.md - release-linux: + release-linux-glibc: needs: changelog runs-on: ubuntu-latest steps: @@ -53,13 +53,13 @@ jobs: CARGO_PROFILE_RELEASE_LTO=fat CARGO_PROFILE_RELEASE_CODEGEN_UNITS=1 PNPM_OFFLINE=TRUE \ cargo build --target x86_64-unknown-linux-gnu --release --bin trail && \ cd ../.. && \ - zip -r -j trailbase_${{ github.ref_name }}_x86_64_linux.zip target/x86_64-unknown-linux-gnu/release/trail CHANGELOG.md LICENSE + zip -r -j trailbase_${{ github.ref_name }}_x86_64_linux_glibc.zip target/x86_64-unknown-linux-gnu/release/trail CHANGELOG.md LICENSE - name: Release binaries uses: softprops/action-gh-release@v2 with: fail_on_unmatched_files: true - files: trailbase_${{ github.ref_name }}_x86_64_linux.zip + files: trailbase_${{ github.ref_name }}_x86_64_linux_glibc.zip release-linux-static: needs: changelog @@ -71,7 +71,7 @@ jobs: - name: Install Dependencies run: | sudo apt-get update && \ - sudo apt-get install -y --no-install-recommends curl libssl-dev pkg-config libclang-dev protobuf-compiler libprotobuf-dev zip + sudo apt-get install -y --no-install-recommends curl libssl-dev pkg-config libclang-dev protobuf-compiler libprotobuf-dev zip musl-tools - uses: pnpm/action-setup@v4 with: version: 9 @@ -81,22 +81,21 @@ jobs: - uses: actions-rs/toolchain@v1 with: toolchain: 1.90.0 - target: x86_64-unknown-linux-gnu + target: x86_64-unknown-linux-musl default: true - name: Rust Build - # NOTE: static linux builds fail with toolchains >1.86 if run from workspace root?! run: | cd crates/cli && \ - RUSTFLAGS="-C target-feature=+crt-static" CARGO_PROFILE_RELEASE_LTO=fat CARGO_PROFILE_RELEASE_CODEGEN_UNITS=1 PNPM_OFFLINE=TRUE \ - cargo build --target x86_64-unknown-linux-gnu --release --bin trail && \ + CARGO_PROFILE_RELEASE_LTO=fat CARGO_PROFILE_RELEASE_CODEGEN_UNITS=1 PNPM_OFFLINE=TRUE \ + cargo build --target x86_64-unknown-linux-musl --features=vendor-ssl --release --bin trail && \ cd ../.. && \ - zip -r -j trailbase_${{ github.ref_name }}_x86_64_linux_static_glibc.zip target/x86_64-unknown-linux-gnu/release/trail CHANGELOG.md LICENSE + zip -r -j trailbase_${{ github.ref_name }}_x86_64_linux.zip target/x86_64-unknown-linux-musl/release/trail CHANGELOG.md LICENSE - name: Release binaries uses: softprops/action-gh-release@v2 with: fail_on_unmatched_files: true - files: trailbase_${{ github.ref_name }}_x86_64_linux_static_glibc.zip + files: trailbase_${{ github.ref_name }}_x86_64_linux.zip release-linux-auth-ui: needs: changelog diff --git a/.gitmodules b/.gitmodules index 461e75d0..60999020 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "vendor/sqlean/bundled/sqlean"] path = crates/sqlean/bundled/sqlean url = https://github.com/trailbaseio/sqlean +[submodule "vendor/sqlite-vec"] + path = vendor/sqlite-vec + url = https://github.com/ignatz/sqlite-vec.git diff --git a/Cargo.lock b/Cargo.lock index 7fb8e488..7f3dd4ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4699,6 +4699,15 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +[[package]] +name = "openssl-src" +version = "300.5.2+3.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d270b79e2926f5150189d475bc7e9d2c69f9c4697b185fa917d5a32b792d21b4" +dependencies = [ + "cc", +] + [[package]] name = "openssl-sys" version = "0.9.109" @@ -4707,6 +4716,7 @@ checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" dependencies = [ "cc", "libc", + "openssl-src", "pkg-config", "vcpkg", ] @@ -6715,11 +6725,10 @@ dependencies = [ [[package]] name = "sqlite-vec" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec77b84fb8dd5f0f8def127226db83b5d1152c5bf367f09af03998b76ba554a" +version = "0.1.7-alpha.2" dependencies = [ "cc", + "rusqlite", ] [[package]] @@ -7936,6 +7945,7 @@ dependencies = [ "log", "mimalloc", "minijinja", + "openssl", "reqwest", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index fd58e667..44ed4f6c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ members = [ "examples/wasm-guest-rust", "examples/coffee-vector-search/guests/rust", "examples/collab-clicker-ssr/guests/rust", + "vendor/sqlite-vec/bindings/rust", ] default-members = [ "crates/assets", @@ -81,6 +82,7 @@ rand = { version = "^0.9.0" } reqwest = { version = "0.12.8", default-features = false, features = ["rustls-tls", "json"] } rusqlite = { version = "0.37.0", default-features = false, features = ["bundled", "column_decltype", "functions", "backup", "preupdate_hook"] } rust-embed = { version = "8.4.0", default-features = false, features = ["mime-guess"] } +sqlite-vec = { path = "vendor/sqlite-vec/bindings/rust", default-features = false } tokio = { version = "^1.38.0", default-features = false, features = ["macros", "net", "rt-multi-thread", "fs", "signal", "time", "sync"] } tracing = { version = "0.1.40", default-features = false } tracing-subscriber = { version = "0.3.18", default-features = false, features = ["smallvec", "std", "fmt", "json"] } diff --git a/client/testfixture/scripts.delme/index.ts b/client/testfixture/scripts.delme/index.ts deleted file mode 100644 index b72e2c30..00000000 --- a/client/testfixture/scripts.delme/index.ts +++ /dev/null @@ -1,198 +0,0 @@ -import { - addCronCallback, - addPeriodicCallback, - addRoute, - execute, - htmlHandler, - jsonHandler, - parsePath, - query, - stringHandler, - transaction, - HttpError, - StatusCodes, - Transaction, -} from "../trailbase.js"; -import type { - Blob, - JsonRequestType, - ParsedPath, - StringRequestType, -} from "../trailbase.d.ts"; - -addRoute( - "GET", - "/test", - stringHandler(async (req: StringRequestType) => { - const uri: ParsedPath = parsePath(req.uri); - - const table = uri.query.get("table"); - if (table) { - const rows = await query(`SELECT COUNT(*) FROM "${table}"`, []); - return `entries: ${rows[0][0]}`; - } - - return `test: ${req.uri}`; - }), -); - -addRoute( - "GET", - "/test/{table}", - stringHandler(async (req: StringRequestType) => { - const table = req.params["table"]; - if (table) { - const rows = await query(`SELECT COUNT(*) FROM "${table}"`, []); - return `entries: ${rows[0][0]}`; - } - - return `test: ${req.uri}`; - }), -); - -addRoute( - "GET", - "/tx/{table}", - stringHandler(async (req: StringRequestType) => { - const table = req.params["table"]; - if (table) { - const count = await transaction((tx: Transaction) => { - const rows = tx.query(`SELECT COUNT(*) FROM "${table}"`, []); - return rows[0][0] as number; - }); - - return `entries: ${count}`; - } - - return `test: ${req.uri}`; - }), -); - -addRoute( - "GET", - "/html", - htmlHandler((_req: StringRequestType) => { - return ` - - -

Html Handler

- - - `; - }), -); - -addRoute( - "GET", - "/json", - jsonHandler((_req: JsonRequestType) => { - return { - int: 5, - real: 4.2, - msg: "foo", - obj: { - nested: true, - }, - }; - }), -); - -addRoute( - "GET", - "/error", - jsonHandler((_req: JsonRequestType) => { - throw new HttpError(StatusCodes.IM_A_TEAPOT, "I'm a teapot"); - }), -); - -addRoute( - "GET", - "/fetch", - stringHandler(async (req: StringRequestType) => { - const query = parsePath(req.uri).query; - const url = query.get("url"); - - if (url) { - const response = await fetch(url); - return await response.text(); - } - - throw new HttpError(StatusCodes.BAD_REQUEST, `Missing ?url param: ${req.params}`); - }), -); - -addRoute( - "GET", - "/fibonacci", - stringHandler((req: StringRequestType) => { - const uri: ParsedPath = parsePath(req.uri); - const n = uri.query.get("n"); - - return fibonacci(n ? parseInt(n) : 40).toString(); - }), -); - -addRoute( - "GET", - "/addDeletePost", - stringHandler(async (_req: StringRequestType) => { - const userId: Blob = ( - await query("SELECT id FROM _user WHERE email = 'admin@localhost'", []) - )[0][0] as Blob; - - console.info("user id:", userId.blob); - - const now = Date.now().toString(); - const numInsertions = await execute( - `INSERT INTO post (author, title, body) VALUES (?1, 'title' , ?2)`, - [{ blob: userId.blob }, now], - ); - - const numDeletions = await execute(`DELETE FROM post WHERE body = ?1`, [ - now, - ]); - - console.assert(numInsertions == numDeletions); - - return "Ok"; - }), -); - -class Completer { - public readonly promise: Promise; - public complete: (value: PromiseLike | T) => void; - - public constructor() { - this.promise = new Promise((resolve, _reject) => { - this.complete = resolve; - }); - } -} - -const completer = new Completer(); - -addPeriodicCallback(100, (cancel) => { - completer.complete("resolved"); - cancel(); -}); - -addCronCallback("JS-registered Job", "@hourly", async () => { - console.info("JS-registered cron job reporting for duty 🚀"); -}); - -addRoute( - "GET", - "/await", - stringHandler(async (_req) => await completer.promise), -); - -function fibonacci(num: number): number { - switch (num) { - case 0: - return 0; - case 1: - return 1; - default: - return fibonacci(num - 1) + fibonacci(num - 2); - } -} diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 1af2926b..be83be0b 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -12,10 +12,11 @@ doctest = false name = "trail" [features] -default = ["v8"] +default = [] # Conditionally enable "v8" feature of dep:trailbase. v8 = ["trailbase/v8"] swagger = ["dep:utoipa-swagger-ui"] +vendor-ssl = ["dep:openssl"] [dependencies] axum = { version = "^0.8.1", features=["multipart"] } @@ -26,6 +27,7 @@ itertools = "0.14.0" log = "^0.4.21" mimalloc = { version = "^0.1.41", default-features = false } minijinja = { workspace = true } +openssl = { version = "0.10.73", features = ["vendored"], optional = true } reqwest = { workspace = true } serde = { version = "^1.0.203", features = ["derive"] } serde_json = "^1.0.117" diff --git a/crates/client/src/lib.rs b/crates/client/src/lib.rs index 78c4d486..a1a9db09 100644 --- a/crates/client/src/lib.rs +++ b/crates/client/src/lib.rs @@ -1,7 +1,7 @@ //! A client library to connect to a TrailBase server via HTTP. //! //! TrailBase is a sub-millisecond, open-source application server with type-safe APIs, built-in -//! JS/ES6/TS runtime, realtime, auth, and admin UI built on Rust, SQLite & V8. +//! WASM runtime, realtime, auth, and admin UI built on Rust, SQLite & Wasmtime. #![forbid(unsafe_code, clippy::unwrap_used)] #![allow(clippy::needless_return)] diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index d5cc1ece..ef0aaedc 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -24,7 +24,7 @@ name = "benchmark" harness = false [features] -default = ["v8", "wasm"] +default = ["wasm"] v8 = ["dep:trailbase-js"] wasm = ["dep:trailbase-wasm-runtime-host"] diff --git a/crates/core/src/server/mod.rs b/crates/core/src/server/mod.rs index 796f7423..84ba4e7c 100644 --- a/crates/core/src/server/mod.rs +++ b/crates/core/src/server/mod.rs @@ -74,7 +74,8 @@ pub struct ServerOptions { /// Limit the set of allowed origins the HTTP server will answer to. pub cors_allowed_origins: Vec, - /// Number of V8 worker threads. If set to None, default of num available cores will be used. + /// Number of dedicated runtime threads. If set to None, default of num available cores will be + /// used. pub runtime_threads: Option, /// TLS certificate path. diff --git a/crates/extension/Cargo.toml b/crates/extension/Cargo.toml index e0e99f45..671d35e1 100644 --- a/crates/extension/Cargo.toml +++ b/crates/extension/Cargo.toml @@ -24,7 +24,7 @@ regex = "1.11.0" rusqlite = { workspace = true } serde = { version = "^1.0.203", features = ["derive"] } serde_json = "1.0.121" -sqlite-vec = { version = "0.1.6", default-features = false } +sqlite-vec = { workspace = true } thiserror = "2.0.12" trailbase-sqlean = { workspace = true } uuid = { workspace = true } diff --git a/docs/src/content/docs/reference/benchmarks.mdx b/docs/src/content/docs/reference/benchmarks.mdx index 6b7f18cb..08e4abed 100644 --- a/docs/src/content/docs/reference/benchmarks.mdx +++ b/docs/src/content/docs/reference/benchmarks.mdx @@ -189,17 +189,14 @@ scheduling, or more generally the same CPU variability we observed earlier. ## Runtimes -[TrailBase is currently going through a transition from a V8-based runtime to a -WASM one](/blog/switching_to_a_wasm_runtime). +[TrailBase went through a transition from a V8-based runtime to a WASM one, which can support many guest languages such as JS, TS and Rust](/blog/switching_to_a_wasm_runtime). -V8's execution with just-in-time (JIT) compilation is quite speedy and is about -40x faster than Goja, PocketBase's interpreter and similar other interpreters -(the graph's y-axis is logarithmic). -With the new WASM-based runtime, execution of JS relies on bundling an +With the new WebAssembly-based runtime, execution of JS relies on bundling an interpreter and thus performance has regressed to be roughly on-par with Goja and other JIT-less engines. However, guests languages that can compile natively to WASM, e.g. Rust, can shine -with roughly a 135x speed-up with strong state isolation between requests. +with roughly a 135x speed-up with strong state isolation between requests (the +graph's y-axis is logarithmic). import { RuntimeFib40Times } from "./_benchmarks/wasm_runtime.tsx"; diff --git a/examples/coffee-vector-search/traildepot/scripts.delme/main.ts b/examples/coffee-vector-search/traildepot/scripts.delme/main.ts deleted file mode 100644 index 000e7db9..00000000 --- a/examples/coffee-vector-search/traildepot/scripts.delme/main.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { addRoute, jsonHandler, parsePath, query } from "../trailbase.js"; - -/// Register a handler for the `/search` API route. -addRoute( - "GET", - "/search", - jsonHandler(async (req) => { - // Get the query params from the url, e.g. '/search?aroma=4&acidity=7'. - const searchParams = parsePath(req.uri).query; - const aroma = searchParams.get("aroma") ?? 8; - const flavor = searchParams.get("flavor") ?? 8; - const acid = searchParams.get("acidity") ?? 8; - const sweet = searchParams.get("sweetness") ?? 8; - - // Query the database for the closest match. - return await query( - `SELECT Owner, Aroma, Flavor, Acidity, Sweetness - FROM coffee - ORDER BY vec_distance_L2( - embedding, FORMAT("[%f, %f, %f, %f]", $1, $2, $3, $4)) - LIMIT 100`, - [+aroma, +flavor, +acid, +sweet], - ); - }), -); diff --git a/examples/collab-clicker-ssr/traildepot/scripts.delme/main.ts b/examples/collab-clicker-ssr/traildepot/scripts.delme/main.ts deleted file mode 100644 index 7bd42ca1..00000000 --- a/examples/collab-clicker-ssr/traildepot/scripts.delme/main.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { addRoute, htmlHandler, jsonHandler, query, fs } from "../trailbase.js"; -import { render } from "../../dist/server/entry-server.js"; - -let _template: Promise | null = null; - -async function getTemplate(): Promise { - if (_template == null) { - const template = _template = fs.readTextFile('dist/client/index.html'); - return await template; - } - return await _template; -} - -addRoute( - "GET", - "/clicked", - jsonHandler(async (_req) => { - const rows = await query( - "UPDATE counter SET value = value + 1 WHERE id = 1 RETURNING value", - [], - ) - - const count = rows.length > 0 ? rows[0][0] as number : -1; - return { count }; - }), -); - -/// Register a root handler. -addRoute( - "GET", - "/", - htmlHandler(async (req) => { - // NOTE: this is replicating vite SSR template's server.js; - const rows = await query( - "SELECT value FROM counter WHERE id = 1", - [], - ) - - const count = rows.length > 0 ? rows[0][0] as number : 0; - const rendered = render(req.uri, count); - - const html = (await getTemplate()) - .replace(``, rendered.head ?? '') - .replace(``, rendered.html ?? '') - .replace(``, rendered.data ?? ''); - - return html; - }), -); diff --git a/vendor/sqlite-vec b/vendor/sqlite-vec new file mode 160000 index 00000000..736f40e6 --- /dev/null +++ b/vendor/sqlite-vec @@ -0,0 +1 @@ +Subproject commit 736f40e68be15440a6df9e320f8d1dd801282126