I was able to get SQLite WASM to work on Expo Web with the following setup:
Add sqlite wasm files to public/
:
public
├── sqlite_wasm
│ ├── sqlite3-opfs-async-proxy.js
│ ├── sqlite3-worker1.js
│ ├── sqlite3.js
│ └── sqlite3.wasm
└── ...
Add sqlite3-worker1-promiser.js
to lib/
and import it:
import "./sqlite3-worker1-promiser";
Type definitions for promiser:
interface PromiserConfig {
onready: () => void;
worker: Worker | (() => Worker);
generateMessageId?: (messageObject: any) => string;
debug?: unknown;
}
type PromiserMessageType = "open" | "close" | "config-get" | "exec";
type PromiserMessageArgs<T extends PromiserMessageType> = {
open: {
filename: string;
};
close: unknown;
"config-get": unknown;
exec: {
sql: string;
bind?: Bind;
returnValue: "resultRows";
rowMode: "array" | "object";
resultRows?: Array<any>;
};
}[T];
type PromiserMessageResult<T extends PromiserMessageType> = {
open: unknown;
close: unknown;
"config-get": unknown;
exec: PromiserMessageArgs<"exec">;
}[T];
type PromiserMessageError = {
type: "error";
dbId: string;
messageId: string;
result: {
operation: PromiserMessageType;
message: string;
errorClass: string;
input: unknown;
stack: Array<string>;
};
};
type PromiserMessageResponse<T extends PromiserMessageType> = {
type: T;
dbId: string;
messageId: string;
result: PromiserMessageResult<T>;
};
type Promiser = <T extends PromiserMessageType>(
type: T,
args: PromiserMessageArgs<T>
) => Promise<PromiserMessageResponse<T>>;
type InitPromiserFn = (config: PromiserConfig) => Promiser;
Spawn the worker:
const initPromiserAsync = () => {
let worker = new Worker("/sqlite_wasm/sqlite3-worker1.js");
return new Promise<Promiser>((resolve, reject) => {
let initPromiser =
globalThis.sqlite3Worker1Promiser as unknown as InitPromiserFn;
let promiser = initPromiser({
onready: () => {
resolve(promiser);
},
worker,
});
});
};
Then use promiser to interact with the worker:
let promiser = await initPromiserAsync();
let filename = `file:app.db?vfs=opfs`;
await promiser("open", { filename });
const exec = async (args: PromiserMessageArgs<"exec">): Promise<PromiserMessageResponse<"exec">> => {
return await this.promiser("exec", args);
}
The OPFS VFS needs SharedArrayBuffer which needs a secure context (COEP/COOP headers). Metro can be configured to send the correct headers during development.
// metro.config.js
const { getDefaultConfig } = require("expo/metro-config");
const config = getDefaultConfig(__dirname);
config.server.enhanceMiddleware = (middleware) => {
return (req, res, next) => {
middleware(req, res, next);
res.setHeader("Cross-Origin-Embedder-Policy", "credentialless");
res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
};
};
module.exports = config;