From 5d474150da15b845c7bdcc145033968f5c06b06a Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 5 Jan 2026 19:44:23 +0200 Subject: [PATCH] feat(client/lightweight): integrate SQLite --- apps/client/package.json | 1 + apps/client/src/local-server-worker.ts | 111 ++++++++++++++++++++++++- apps/client/vite.config.mts | 21 ++++- pnpm-lock.yaml | 13 ++- 4 files changed, 140 insertions(+), 6 deletions(-) diff --git a/apps/client/package.json b/apps/client/package.json index 57628fd37..36cae84b8 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -28,6 +28,7 @@ "@mind-elixir/node-menu": "5.0.1", "@popperjs/core": "2.11.8", "@preact/signals": "2.5.1", + "@sqlite.org/sqlite-wasm": "3.51.1-build2", "@triliumnext/ckeditor5": "workspace:*", "@triliumnext/codemirror": "workspace:*", "@triliumnext/commons": "workspace:*", diff --git a/apps/client/src/local-server-worker.ts b/apps/client/src/local-server-worker.ts index 42d7ee5e2..a968bbae6 100644 --- a/apps/client/src/local-server-worker.ts +++ b/apps/client/src/local-server-worker.ts @@ -2,8 +2,16 @@ // This will eventually import your core server and DB provider. // import { createCoreServer } from "@trilium/core"; (bundled) +import sqlite3InitModule from '@sqlite.org/sqlite-wasm'; + const encoder = new TextEncoder(); +// SQLite WASM instance +let sqlite3: any = null; +let db: any = null; +let sqliteInitPromise: Promise | null = null; +let sqliteInitError: string | null = null; + function jsonResponse(obj, status = 200, extraHeaders = {}) { const body = encoder.encode(JSON.stringify(obj)).buffer; return { @@ -22,8 +30,94 @@ function textResponse(text, status = 200, extraHeaders = {}) { }; } +// Initialize SQLite WASM +async function initSQLite() { + if (sqlite3) return; // Already initialized + if (sqliteInitError) return; // Failed before, don't retry + if (sqliteInitPromise) return sqliteInitPromise; // Already initializing + + sqliteInitPromise = (async () => { + try { + console.log("[Worker] Initializing SQLite WASM..."); + const startTime = performance.now(); + + // Just call the init module without custom locateFile + // The module will use import.meta.url to find sqlite3.wasm + sqlite3 = await sqlite3InitModule({ + print: console.log, + printErr: console.error, + }); + + const initTime = performance.now() - startTime; + console.log(`[Worker] SQLite WASM initialized in ${initTime.toFixed(2)}ms:`, sqlite3.version); + + // Open a database in memory for now + db = new sqlite3.oo1.DB(':memory:', 'c'); + console.log("[Worker] Database opened"); + + // Create a simple test table + db.exec(` + CREATE TABLE IF NOT EXISTS options ( + name TEXT PRIMARY KEY, + value TEXT + ); + INSERT INTO options (name, value) VALUES + ('theme', 'dark'), + ('layoutOrientation', 'vertical'), + ('headingStyle', 'default'); + `); + console.log("[Worker] Test table created and populated"); + } catch (error) { + sqliteInitError = String(error); + console.error("[Worker] SQLite initialization failed:", error); + throw error; + } + })(); + + return sqliteInitPromise; +} + // Example: your /bootstrap handler placeholder -function handleBootstrap() { +async function handleBootstrap() { + console.log("[Worker] Bootstrap request received"); + + // Try to initialize SQLite with timeout + let dbInfo: any = { dbStatus: 'not initialized' }; + + if (sqliteInitError) { + dbInfo = { dbStatus: 'failed', error: sqliteInitError }; + } else { + try { + // Don't wait too long for SQLite initialization + await Promise.race([ + initSQLite(), + new Promise((_, reject) => setTimeout(() => reject(new Error("SQLite init timeout")), 5000)) + ]); + + // Query the database if initialized + if (db) { + const stmt = db.prepare('SELECT * FROM options'); + const options: Record = {}; + while (stmt.step()) { + const row = stmt.get({}); + options[row.name] = row.value; + } + stmt.finalize(); + + dbInfo = { + sqliteVersion: sqlite3.version.libVersion, + optionsFromDB: options, + dbStatus: 'connected' + }; + } + } catch (e) { + console.error("[Worker] Error during bootstrap:", e); + dbInfo = { dbStatus: 'error', error: String(e) }; + } + } + + console.log("[Worker] Sending bootstrap response"); + // Later: return real globals from your core state/config. return jsonResponse({ assetPath: "assets", @@ -37,7 +131,9 @@ function handleBootstrap() { isElectron: false, hasNativeTitleBar: false, hasBackgroundEffects: true, - currentLocale: { id: "en", rtl: false } + currentLocale: { id: "en", rtl: false }, + // Add SQLite info for testing + sqlite: dbInfo }); } @@ -45,7 +141,7 @@ function handleBootstrap() { async function dispatch(request) { const url = new URL(request.url); - console.log("Dispatch ", url); + console.log("[Worker] Dispatch:", url.pathname); // NOTE: your core router will do this later. if (request.method === "GET" && url.pathname === "/bootstrap") { return handleBootstrap(); @@ -58,14 +154,22 @@ async function dispatch(request) { return textResponse("Not found", 404); } +// Start SQLite initialization as soon as the worker loads (in background) +console.log("[Worker] Starting background SQLite initialization..."); +initSQLite().catch(err => { + console.error("[Worker] Background SQLite init failed:", err); +}); + self.onmessage = async (event) => { const msg = event.data; if (!msg || msg.type !== "LOCAL_REQUEST") return; const { id, request } = msg; + console.log("[Worker] Received LOCAL_REQUEST:", id, request.method, request.url); try { const response = await dispatch(request); + console.log("[Worker] Dispatch completed, sending response:", id); // Transfer body back (if any) self.postMessage({ @@ -74,6 +178,7 @@ self.onmessage = async (event) => { response }, response.body ? [response.body] : []); } catch (e) { + console.error("[Worker] Dispatch error:", e); self.postMessage({ type: "LOCAL_RESPONSE", id, diff --git a/apps/client/vite.config.mts b/apps/client/vite.config.mts index 04fa9c785..f5d37fee3 100644 --- a/apps/client/vite.config.mts +++ b/apps/client/vite.config.mts @@ -8,12 +8,25 @@ import { viteStaticCopy } from 'vite-plugin-static-copy' const assets = [ "assets", "stylesheets", "fonts", "translations" ]; const isDev = process.env.NODE_ENV === "development"; + +// Always copy SQLite WASM files so they're available to the module +const sqliteWasmPlugin = viteStaticCopy({ + targets: [ + { + // Copy the entire jswasm directory to maintain the module's expected structure + src: "../../node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/*", + dest: "node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm" + } + ] +}); + let plugins: any = [ preact({ babel: { compact: !isDev } - }) + }), + sqliteWasmPlugin // Always include SQLite WASM files ]; if (!isDev) { @@ -43,6 +56,12 @@ export default defineConfig(() => ({ cacheDir: '../../node_modules/.vite/apps/client', base: "", plugins, + optimizeDeps: { + exclude: ['@sqlite.org/sqlite-wasm'] + }, + worker: { + format: 'es' + }, resolve: { alias: [ { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fc5d57faf..3e4783a2c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -193,6 +193,9 @@ importers: '@preact/signals': specifier: 2.5.1 version: 2.5.1(preact@10.28.1) + '@sqlite.org/sqlite-wasm': + specifier: 3.51.1-build2 + version: 3.51.1-build2 '@triliumnext/ckeditor5': specifier: workspace:* version: link:../../packages/ckeditor5 @@ -5195,6 +5198,10 @@ packages: '@socket.io/component-emitter@3.1.2': resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + '@sqlite.org/sqlite-wasm@3.51.1-build2': + resolution: {integrity: sha512-lVPTBlFsEijJ3wuoIbMfC9QMZKfL8huHN8D/lijNKoVxPqUDNvDtXse0wafe7USSmyfKAMb1JZ3ISSr/Vgbn5w==} + hasBin: true + '@ssddanbrown/codemirror-lang-smarty@1.0.0': resolution: {integrity: sha512-F0ut1kmdbT3eORk3xVIKfQsGCZiQdh+6sLayBa0+FTex2gyIQlVQZRRA7bPSlchI3uZtWwNnqGNz5O/QLWRlFg==} @@ -15433,6 +15440,8 @@ snapshots: '@ckeditor/ckeditor5-utils': 47.3.0 '@ckeditor/ckeditor5-watchdog': 47.3.0 es-toolkit: 1.39.5 + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-dev-build-tools@54.2.3(@swc/helpers@0.5.17)(tslib@2.8.1)(typescript@5.9.3)': dependencies: @@ -16096,8 +16105,6 @@ snapshots: '@ckeditor/ckeditor5-ui': 47.3.0 '@ckeditor/ckeditor5-utils': 47.3.0 ckeditor5: 47.3.0 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-restricted-editing@47.3.0': dependencies: @@ -19927,6 +19934,8 @@ snapshots: '@socket.io/component-emitter@3.1.2': {} + '@sqlite.org/sqlite-wasm@3.51.1-build2': {} + '@ssddanbrown/codemirror-lang-smarty@1.0.0': {} '@ssddanbrown/codemirror-lang-twig@1.0.0':