Switch search runtime to vendored sqlite3 WASM
This commit is contained in:
parent
fc85729374
commit
04c8ce7005
6 changed files with 13059 additions and 10 deletions
109
script.js
109
script.js
|
@ -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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue