Switch search runtime to vendored sqlite3 WASM

This commit is contained in:
Jordan Wages 2025-09-18 19:38:10 -05:00
commit 04c8ce7005
6 changed files with 13059 additions and 10 deletions

109
script.js
View file

@ -2,13 +2,13 @@
Application bootstrap for the static, client-side metadata browser.
- Downloads a zipped SQLite DB (db.zip) with progress and unzips it in-memory
- Caches the uncompressed DB bytes in IndexedDB for reuse on next load
- Loads sql.js (WASM) and opens the database from the cached bytes
- Loads the official sqlite3 WASM build (oo1 API) and opens the database from the cached bytes
- Swaps interchangeable UX elements (views) in a viewport, with fade transitions
Assumptions:
- A ZIP archive is hosted at `/db.zip` containing a single `.sqlite` file.
- We use `fflate` (UMD) from a CDN to unzip after download (keeps implementation minimal).
- We use `sql.js` from a CDN; the WASM is located via `locateFile` (see initSql()).
- We ship the sqlite3 WASM runtime locally to guarantee FTS5 support.
- We store raw DB bytes in IndexedDB (localStorage is too small for large DBs).
*/
@ -297,15 +297,109 @@
return dbBytes;
}
// --- SQL.js init ---
// --- SQLite WASM init ---
async function initSql() {
if (typeof initSqlJs !== 'function') throw new Error('sql.js not loaded');
if (typeof sqlite3InitModule !== 'function') throw new Error('sqlite3.js not loaded');
loader.setStep('Initializing SQLite…');
loader.setDetail('Loading WASM');
const SQL = await initSqlJs({
locateFile: (file) => `https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.10.2/${file}`,
const sqlite3 = await sqlite3InitModule({
locateFile: (file) => `./vendor/sqlite3/${file}`,
});
return SQL;
return createSqlCompat(sqlite3);
}
function createSqlCompat(sqlite3) {
const { capi, wasm } = sqlite3;
const SQLITE_DESERIALIZE_FREEONCLOSE = 0x01;
const SQLITE_DESERIALIZE_READONLY = 0x04;
function loadDatabaseBytes(dbHandle, bytes) {
if (!bytes || !bytes.byteLength) return;
const byteLength = bytes.byteLength;
const pData = wasm.alloc(byteLength);
try {
wasm.heap8u().set(bytes, pData);
const [pSchema] = wasm.allocCString('main', true);
try {
const rc = capi.sqlite3_deserialize(
dbHandle.pointer,
pSchema,
pData,
byteLength,
byteLength,
SQLITE_DESERIALIZE_FREEONCLOSE | SQLITE_DESERIALIZE_READONLY
);
if (rc !== capi.SQLITE_OK) {
throw new sqlite3.SQLite3Error(`sqlite3_deserialize failed with rc=${rc}`);
}
} finally {
wasm.dealloc(pSchema);
}
} catch (err) {
wasm.dealloc(pData);
throw err;
}
}
class Statement {
constructor(stmt) {
this._stmt = stmt;
this._columnNames = null;
}
bind(values) {
this._stmt.bind(values);
return true;
}
step() {
return !!this._stmt.step();
}
getAsObject() {
if (!this._columnNames) this._columnNames = this._stmt.getColumnNames([]);
const row = Object.create(null);
const count = this._columnNames.length;
for (let i = 0; i < count; i += 1) {
row[this._columnNames[i]] = this._stmt.get(i);
}
return row;
}
free() {
this._stmt.finalize();
}
}
class Database {
constructor(bytes) {
this._db = new sqlite3.oo1.DB();
if (bytes) loadDatabaseBytes(this._db, bytes);
}
prepare(sql) {
return new Statement(this._db.prepare(sql));
}
exec(sql) {
return this._db.exec(sql);
}
close() {
this._db.close();
}
get pointer() {
return this._db.pointer;
}
}
function ensureFts5Available(dbHandle) {
let stmt;
try {
stmt = dbHandle.prepare('CREATE VIRTUAL TABLE temp.__fts5_check USING fts5(content TEXT)');
stmt.step();
} catch (err) {
throw new Error('SQLite build missing FTS5 support; search requires an fts5-enabled wasm');
} finally {
try { if (stmt) stmt.free(); } catch (_) {}
try { dbHandle.exec('DROP TABLE IF EXISTS temp.__fts5_check'); } catch (_) {}
}
}
return { Database, ensureFts5Available };
}
// --- Templates & Views ---
@ -2380,6 +2474,7 @@
const SQL = await initSql();
loader.setDetail('Opening database');
const db = new SQL.Database(dbBytes);
SQL.ensureFts5Available(db);
activeDb = db;
window.__db = db;