Switch SPA to bundled sql.js runtime

This commit is contained in:
Jordan Wages 2025-09-18 22:03:36 -05:00
commit e83de10d93
3 changed files with 15 additions and 50 deletions

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 the official sqlite3 WASM build (oo1 API) and opens the database from the cached bytes
- Loads the locally bundled sql.js WASM build (FTS5-enabled) 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 ship the sqlite3 WASM runtime locally to guarantee FTS5 support.
- We ship the sql.js WASM runtime locally to guarantee FTS5 support.
- We store raw DB bytes in IndexedDB (localStorage is too small for large DBs).
*/
@ -299,62 +299,30 @@
// --- SQLite WASM init ---
async function initSql() {
if (typeof sqlite3InitModule !== 'function') throw new Error('sqlite3.js not loaded');
if (typeof initSqlJs !== 'function') throw new Error('sql.js runtime not loaded');
loader.setStep('Initializing SQLite…');
loader.setDetail('Loading WASM');
const sqlite3 = await sqlite3InitModule({
locateFile: (file) => `./vendor/sqlite3/${file}`,
const SQL = await initSqlJs({
locateFile: (file) => `./${file}`,
});
return createSqlCompat(sqlite3);
return createSqlCompat(SQL);
}
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;
}
}
function createSqlCompat(SQL) {
class Statement {
constructor(stmt) {
this._stmt = stmt;
this._columnNames = null;
}
bind(values) {
this._stmt.bind(values);
if (values !== undefined) this._stmt.bind(values);
return true;
}
step() {
return !!this._stmt.step();
}
getAsObject() {
if (!this._columnNames) this._columnNames = this._stmt.getColumnNames([]);
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) {
@ -363,14 +331,14 @@
return row;
}
free() {
this._stmt.finalize();
this._stmt.free();
}
}
class Database {
constructor(bytes) {
this._db = new sqlite3.oo1.DB();
if (bytes) loadDatabaseBytes(this._db, bytes);
const source = bytes instanceof Uint8Array ? bytes : bytes ? new Uint8Array(bytes) : undefined;
this._db = source ? new SQL.Database(source) : new SQL.Database();
}
prepare(sql) {
return new Statement(this._db.prepare(sql));
@ -381,9 +349,6 @@
close() {
this._db.close();
}
get pointer() {
return this._db.pointer;
}
}
function ensureFts5Available(dbHandle) {