Skip to content

Instantly share code, notes, and snippets.

@hazelmeow
Last active September 16, 2024 23:14
Show Gist options
  • Save hazelmeow/0d7083e7d18d7ccace2f4c46d6ac4932 to your computer and use it in GitHub Desktop.
Save hazelmeow/0d7083e7d18d7ccace2f4c46d6ac4932 to your computer and use it in GitHub Desktop.
SQLite WASM in Expo Web

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;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment