mirror of
https://github.com/trailbaseio/trailbase.git
synced 2025-12-16 15:15:51 -06:00
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.
This commit is contained in:
19
.github/workflows/release.yml
vendored
19
.github/workflows/release.yml
vendored
@@ -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
|
||||
|
||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -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
|
||||
|
||||
16
Cargo.lock
generated
16
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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>
|
||||
<body>
|
||||
<h1>Html Handler</h1>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}),
|
||||
);
|
||||
|
||||
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<T> {
|
||||
public readonly promise: Promise<T>;
|
||||
public complete: (value: PromiseLike<T> | T) => void;
|
||||
|
||||
public constructor() {
|
||||
this.promise = new Promise<T>((resolve, _reject) => {
|
||||
this.complete = resolve;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const completer = new Completer<string>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -24,7 +24,7 @@ name = "benchmark"
|
||||
harness = false
|
||||
|
||||
[features]
|
||||
default = ["v8", "wasm"]
|
||||
default = ["wasm"]
|
||||
v8 = ["dep:trailbase-js"]
|
||||
wasm = ["dep:trailbase-wasm-runtime-host"]
|
||||
|
||||
|
||||
@@ -74,7 +74,8 @@ pub struct ServerOptions {
|
||||
/// Limit the set of allowed origins the HTTP server will answer to.
|
||||
pub cors_allowed_origins: Vec<String>,
|
||||
|
||||
/// 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<usize>,
|
||||
|
||||
/// TLS certificate path.
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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],
|
||||
);
|
||||
}),
|
||||
);
|
||||
@@ -1,49 +0,0 @@
|
||||
import { addRoute, htmlHandler, jsonHandler, query, fs } from "../trailbase.js";
|
||||
import { render } from "../../dist/server/entry-server.js";
|
||||
|
||||
let _template: Promise<string> | null = null;
|
||||
|
||||
async function getTemplate(): Promise<string> {
|
||||
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(`<!--app-head-->`, rendered.head ?? '')
|
||||
.replace(`<!--app-html-->`, rendered.html ?? '')
|
||||
.replace(`<!--app-data-->`, rendered.data ?? '');
|
||||
|
||||
return html;
|
||||
}),
|
||||
);
|
||||
1
vendor/sqlite-vec
vendored
Submodule
1
vendor/sqlite-vec
vendored
Submodule
Submodule vendor/sqlite-vec added at 736f40e68b
Reference in New Issue
Block a user