feat(folio-webApp): OPFS-backed sqlite worker + webpack config

sqlite.worker.js implements the SQLDelight web-worker protocol
(exec/begin_transaction/end_transaction/rollback_transaction) on top
of @sqlite.org/sqlite-wasm. Prefers the OPFS SAH pool VFS for
persistent storage and falls back to in-memory when OPFS is
unavailable.

webpack.config.d/coopcoep.js sends COOP/COEP headers on the dev
server so cross-origin isolation is available, even though the SAH
pool itself does not require it. webpack.config.d/sqlite-wasm.js
enables asyncWebAssembly so webpack can bundle sqlite3.wasm via the
'new URL("sqlite3.wasm", import.meta.url)' reference inside the
sqlite-wasm package.
This commit is contained in:
pj committed 2026-04-26 16:37:45 +07:00
1 parent 80b02fd19f
commit db2801d16c
3 files changed
+89

No files matched your search

@@ -0,0 +1,71 @@
import sqlite3InitModule from '@sqlite.org/sqlite-wasm';
let db = null;
let isPersistent = false;
async function init() {
const sqlite3 = await sqlite3InitModule({
print: (msg) => console.log('[sqlite]', msg),
printErr: (msg) => console.error('[sqlite]', msg),
});
try {
const pool = await sqlite3.installOpfsSAHPoolVfs({
directory: '/folio-sqlite-pool',
clearOnInit: false,
initialCapacity: 6,
});
db = new pool.OpfsSAHPoolDb('/folio.sqlite3');
isPersistent = true;
console.log('[sqlite] using OPFS SAH pool');
} catch (sahErr) {
console.warn('[sqlite] OPFS SAH pool unavailable, falling back to in-memory:', sahErr?.message ?? sahErr);
db = new sqlite3.oo1.DB(':memory:', 'ct');
isPersistent = false;
}
}
function execQuery(sql, params) {
const rows = db.exec({
sql,
bind: params,
returnValue: 'resultRows',
});
return { values: rows };
}
function dispatch(data) {
switch (data && data.action) {
case 'exec':
if (!data.sql) throw new Error('exec: missing sql');
return execQuery(data.sql, data.params);
case 'begin_transaction':
db.exec('BEGIN TRANSACTION;');
return { values: [] };
case 'end_transaction':
db.exec('COMMIT;');
return { values: [] };
case 'rollback_transaction':
db.exec('ROLLBACK;');
return { values: [] };
default:
throw new Error('Unsupported action: ' + (data && data.action));
}
}
const ready = init();
self.onmessage = (event) => {
const data = event.data;
ready
.then(() => {
const results = dispatch(data);
self.postMessage({ id: data.id, results });
})
.catch((err) => {
const message = err && err.message ? String(err.message) : String(err);
self.postMessage({ id: data && data.id, error: { message, name: err && err.name } });
});
};
self.postMessage({ ready: true, persistent: isPersistent });
@@ -0,0 +1,9 @@
config.devServer = {
...(config.devServer || {}),
headers: {
...((config.devServer && config.devServer.headers) || {}),
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp',
'Cross-Origin-Resource-Policy': 'cross-origin',
},
};
@@ -0,0 +1,9 @@
// Webpack picks up `new Worker(new URL("./sqlite.worker.js", import.meta.url),
// { type: "module" })` from the compiled Kotlin/wasm output and bundles the
// worker. The bundled worker imports `@sqlite.org/sqlite-wasm`, which in turn
// loads sqlite3.wasm via `new URL("sqlite3.wasm", import.meta.url)`. Enabling
// asyncWebAssembly lets webpack process that .wasm reference correctly.
config.experiments = {
...(config.experiments || {}),
asyncWebAssembly: true,
};