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

@ -4,4 +4,4 @@ A self contained static website for browsing the collection of salvaged mp3.com
## Runtime Notes ## Runtime Notes
- Uses the official `sqlite3` WASM build (vendored under `vendor/sqlite3/`) so that FTS5 is available client-side. - Uses the `sql.js` WASM build (bundled as `sql-wasm.js`/`sql-wasm.wasm`) so that FTS5 is available client-side.

View file

@ -365,8 +365,8 @@
<!-- Third-party libs loaded from CDNs (no trackers). Pinned versions. --> <!-- Third-party libs loaded from CDNs (no trackers). Pinned versions. -->
<!-- fflate: tiny ZIP library used to unzip the downloaded DB archive client-side. --> <!-- fflate: tiny ZIP library used to unzip the downloaded DB archive client-side. -->
<script src="https://cdn.jsdelivr.net/npm/fflate@0.8.2/umd/index.js" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/fflate@0.8.2/umd/index.js" crossorigin="anonymous"></script>
<!-- SQLite WASM build (oo1 API) shipped locally to ensure FTS5 support. --> <!-- sql.js build (FTS5-enabled) shipped locally to ensure search support. -->
<script src="./vendor/sqlite3/sqlite3.js"></script> <script src="./sql-wasm.js"></script>
<!-- App logic --> <!-- App logic -->
<script src="./script.js"></script> <script src="./script.js"></script>

View file

@ -2,13 +2,13 @@
Application bootstrap for the static, client-side metadata browser. Application bootstrap for the static, client-side metadata browser.
- Downloads a zipped SQLite DB (db.zip) with progress and unzips it in-memory - 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 - 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 - Swaps interchangeable UX elements (views) in a viewport, with fade transitions
Assumptions: Assumptions:
- A ZIP archive is hosted at `/db.zip` containing a single `.sqlite` file. - 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 `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). - We store raw DB bytes in IndexedDB (localStorage is too small for large DBs).
*/ */
@ -299,62 +299,30 @@
// --- SQLite WASM init --- // --- SQLite WASM init ---
async function initSql() { 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.setStep('Initializing SQLite…');
loader.setDetail('Loading WASM'); loader.setDetail('Loading WASM');
const sqlite3 = await sqlite3InitModule({ const SQL = await initSqlJs({
locateFile: (file) => `./vendor/sqlite3/${file}`, locateFile: (file) => `./${file}`,
}); });
return createSqlCompat(sqlite3); return createSqlCompat(SQL);
} }
function createSqlCompat(sqlite3) { function createSqlCompat(SQL) {
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 { class Statement {
constructor(stmt) { constructor(stmt) {
this._stmt = stmt; this._stmt = stmt;
this._columnNames = null; this._columnNames = null;
} }
bind(values) { bind(values) {
this._stmt.bind(values); if (values !== undefined) this._stmt.bind(values);
return true; return true;
} }
step() { step() {
return !!this._stmt.step(); return !!this._stmt.step();
} }
getAsObject() { getAsObject() {
if (!this._columnNames) this._columnNames = this._stmt.getColumnNames([]); if (!this._columnNames) this._columnNames = this._stmt.getColumnNames();
const row = Object.create(null); const row = Object.create(null);
const count = this._columnNames.length; const count = this._columnNames.length;
for (let i = 0; i < count; i += 1) { for (let i = 0; i < count; i += 1) {
@ -363,14 +331,14 @@
return row; return row;
} }
free() { free() {
this._stmt.finalize(); this._stmt.free();
} }
} }
class Database { class Database {
constructor(bytes) { constructor(bytes) {
this._db = new sqlite3.oo1.DB(); const source = bytes instanceof Uint8Array ? bytes : bytes ? new Uint8Array(bytes) : undefined;
if (bytes) loadDatabaseBytes(this._db, bytes); this._db = source ? new SQL.Database(source) : new SQL.Database();
} }
prepare(sql) { prepare(sql) {
return new Statement(this._db.prepare(sql)); return new Statement(this._db.prepare(sql));
@ -381,9 +349,6 @@
close() { close() {
this._db.close(); this._db.close();
} }
get pointer() {
return this._db.pointer;
}
} }
function ensureFts5Available(dbHandle) { function ensureFts5Available(dbHandle) {