Active Record on Wasm
We can use Active Record to connect to a database running in the browser. For that, we implement a custom database adapter at the Rails side and an external interface object in the JavaScript runtime.
Wasmify Rails provides two adapters for in-Wasm database: sqlite3_wasm and pglite.
Using with sqlite3-wasm
Sqlite3 ships with an official Wasm port that allows you to work with a Sqlite3 database right in your browser. To connect it to your Rails on Wasm application, you must to the following:
Configure Active Record to use
sqlite3_wasmadapter in thedatabase.yml.Register an external interface object for your in-browser database as follows:
import sqlite3InitModule from "@sqlite.org/sqlite-wasm";
import {
registerSQLiteWasmInterface,
initRailsVM
} from "wasmify-rails";
// Create a database
const sqlite3 = await sqlite3InitModule();
const db = new sqlite3.oo1.DB("/railsdb.sqlite3", "ct");
// Make the database accessible from the Rails app
registerSQLiteWasmInterface(self, db);
// Initialize a Rails app
const vm = await initRailsVM("/app.wasm", {
database: { adapter: "sqlite3_wasm" }
});
// Prepare the database
vm.eval("ActiveRecord::Tasks::DatabaseTasks.prepare_all");
// Run some Active Record code
console.log("Post count:", vm.eval("Post.count").toString());
Persistence with sqlite3-wasm
See the corresponding documentation.
tl;dr sqlite3-wasm supports OPFS, but it's not available in the service worker context (the way we usually run Rails applications).
As an alternative to OPFS, you can use the File System API directly and create database snapshots periodically or after any mutating HTTP request (i.e., non-GET/HEAD/OPTIONS request):
export const backupDB = async (sqlite3, db) => {
const contents = sqlite3.capi.sqlite3_js_db_export(db);
const opfsRoot = await navigator.storage.getDirectory();
const fileHandle = await opfsRoot.getFileHandle("rails.sqlite3", {
create: true,
});
const writable = await fileHandle.createWritable();
await writable.write(contents);
await writable.close();
}
export const restoreDB = async (sqlite3, db) => {
const opfsRoot = await navigator.storage.getDirectory();
const fileHandle = await opfsRoot.getFileHandle("rails.sqlite3");
const file = await fileHandle.getFile();
if (!file) return false;
const arrayBuffer = await file.arrayBuffer();
const p = sqlite3.wasm.allocFromTypedArray(arrayBuffer);
const rc = sqlite3.capi.sqlite3_deserialize(
db.pointer,
"main",
p,
arrayBuffer.byteLength,
arrayBuffer.byteLength,
sqlite3.capi.SQLITE_DESERIALIZE_FREEONCLOSE |
sqlite3.capi.SQLITE_DESERIALIZE_RESIZEABLE,
);
db.checkRc(rc);
return true;
};
Using with PGlite
You can find a full working example here.
PGlite is a full-featured PostgreSQL running in Wasm. You can connect to it from your Rails on Wasm application using the corresponding Active Record adapter. The setup is as follows:
Configure Active Record to use
pgliteadapter in thedatabase.yml.Register an external interface object for your in-browser database as follows:
import { PGlite } from '@electric-sql/pglite';
import {
registerPGliteWasmInterface,
initRailsVM
} from "wasmify-rails";
const db = await PGlite.create();
// Make the database accessible from the Rails app
registerPGliteWasmInterface(self, db);
const vm = await initRailsVM("/app.wasm", {
database: { adapter: "pglite" },
async: true,
};
// Prepare the database
await vm.evalAsync("ActiveRecord::Tasks::DatabaseTasks.prepare_all");
// Run some Active Record code
const count = vm.evalAsync("Post.count")
console.log("Post count:", count.toString());
IMPORTANT: PGLite only provides async API, so we must execute Ruby code using evalAsync. The asyncify Ruby Wasm may not work correctly in older Ruby versions (see issue #555).
NullDB adapter
Wasmify Rails also comes with a NullDB adapter (a fork of the this project that could be useful in development to verify that the build works. You can use it outside of the browser (e.g., in wasmtime).