Open4

SQLite WasmとOPFSを実際に使う際に調べたこと(備忘録)

Yuki ShindoYuki Shindo

はじめに

SQLite WasmOPFS を用いてWebフロントエンドでのデータ永続化を簡単に実現させるための NeverChange というライブラリを作成している

https://github.com/shinshin86/neverchange

こちらに関する詳細は以下のZennの記事に以前書かせていただいている。

https://zenn.dev/shinshin86/articles/fdf4cbe40b2bad

今回この NeverChange の開発をするにあたり、SQLite Wasm + OPFS での動作環境について調べた内容をこちらのスクラップにまとめていく。

なお、 SQLite Wasm 自体は以下のリポジトリを対象として書いていく。
https://github.com/sqlite/sqlite-wasm

ただ調べたところ他にも SQLite Wasm の実装はあり別の実装では内部の実装内容が異なっていそうだったので、これから書いていくような内容を行わずに SQLite Wasm を動かしたいという場合、それらのライブラリを使うことで解決できるかもしれない。

Yuki ShindoYuki Shindo

SQLite WasmとOPFSを有効化させるためには

まず SQLite Wasm では内部処理で SharedArrayBuffer を用いているが、これを動かすためには特定のヘッダー設定が必要となる。具体的には以下。

  • Cross-Origin-Opener-Policysame-origin
  • Cross-Origin-Embedder-Policyrequire-corp

まずこれを実現できないとSQLite Wasm + OPFS という設定で動かすことはできない。

そして実際にサイトにデプロイして動かすには、いかにしてこのヘッダー設定を達成するかが鍵となる。

workerで動かす必要がある

またOPFSを有効にして動かすにはworkerで動かす必要があるが、SQLite Wasm のREADMEにはわかりやすいサンプルが載っているので、そちらを踏襲すれば問題ない。

※以下はSQLite wasmのREADMEに記載されていたサンプルを引用したもの

import { sqlite3Worker1Promiser } from '@sqlite.org/sqlite-wasm';

const log = console.log;
const error = console.error;

const initializeSQLite = async () => {
  try {
    log('Loading and initializing SQLite3 module...');

    const promiser = await new Promise((resolve) => {
      const _promiser = sqlite3Worker1Promiser({
        onready: () => resolve(_promiser),
      });
    });

    log('Done initializing. Running demo...');

    const configResponse = await promiser('config-get', {});
    log('Running SQLite3 version', configResponse.result.version.libVersion);

    const openResponse = await promiser('open', {
      filename: 'file:mydb.sqlite3?vfs=opfs',
    });
    const { dbId } = openResponse;
    log(
      'OPFS is available, created persisted database at',
      openResponse.result.filename.replace(/^file:(.*?)\?vfs=opfs$/, '$1'),
    );
    // Your SQLite code here.
  } catch (err) {
    if (!(err instanceof Error)) {
      err = new Error(err.result.message);
    }
    error(err.name, err.message);
  }
};

initializeSQLite();
Yuki ShindoYuki Shindo

実際にホスティング環境で動かす方法

ここからはよく利用されているホスティング環境を含めたいくつかのユースケースにおける動かし方について触れていく。

なお、サーバー環境も含めてデプロイできるサービスであれば、独自にヘッダー周りは設定できると思うのでここでは割愛する。
(例:EC2にデプロイして利用するケースなど)

Viteを用いた開発環境のセットアップ

まずは開発環境周り。私は SQLite Wasm + OPFS での開発時は基本的に Vite を用いていた。
Vite を利用する場合設定ファイルに以下のような記述を書けば実行可能となる。

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  server: {
    headers: {
      'Cross-Origin-Opener-Policy': 'same-origin',
      'Cross-Origin-Embedder-Policy': 'require-corp',
    },
  },
  optimizeDeps: {
    exclude: ['@sqlite.org/sqlite-wasm'],
  },
});

optimizeDepsSQLite Wasm を入れる理由だが、SQLite Wasm にはWasmファイルが含まれているが、Viteの依存関係の最適化機能はJavaScriptモジュールの処理を想定したものなので、ここに含めてしまうとうまく動かない、というのが理由だった気がする。ここは認識がアバウトです。
(だが、optimizeDeps に入れないといずれにせよエラーにはなる)

Netlifyで動かす方法

NetlifySQLite Wasm + OPFS のアプリを動かすのはとても簡単なので、こういった無料から利用を開始できる静的ファイルを動かせる系のホスティング環境ならオススメの選択肢となる。

Netlify では _headers ファイルという独自の設定ファイルをルートディレクトリに配置することで、ヘッダーなどの設定を反映させることができる。

_headers ファイルには以下のような記述を書き、これをデプロイしたプロジェクトのルートに配置するだけでOK。

/*  
  Cross-Origin-Opener-Policy: same-origin
  Cross-Origin-Embedder-Policy: require-corp

GitHub Pagesで動かす方法

GitHub Pagesを使用する場合、Cross-Origin-Opener-Policy: same-originCross-Origin-Embedder-Policy: require-corp ヘッダーを設定できないため、そのままでは利用できない。

そこで以下の coi-serviceworker.js を利用する形となる。

https://github.com/gzuidhof/coi-serviceworker

これはservice workerを介してヘッダーを設定してくれるライブラリで、GitHub Pagesなどで利用する際はこれを使うのが定番?らしい。

インストール後、HTMLなどに以下のような記述を書けばヘッダーの適用をしてくれる。

<!DOCTYPE html>
<html>
<head>
  <title>NeverChange Example</title>
  <!-- coi-serviceworker.js is required for GitHub Pages -->
  <script src="/your-project-name/coi-serviceworker.js"></script>
</head>
<body>
  <div id="root"></div>
  <script type="module" src="/src/main.tsx"></script>
</body>
</html>

なお、以下のようにルート以下のディレクトリに配置した場合、何故か無限に読み込みループが発生する事態となったので注意。

<script src="/your-project-name/directory/coi-serviceworker.js"></script>

実際にGitHub Pagesにデプロイしているサンプルアプリのリポジトリを公開しているので、より実践的な構成はこちらが参考になると思われる。

https://github.com/shinshin86/mytodolist

Safariだと動かない

そしてここで残念なお知らせだが、このサービスワーカーを用いた仕組みは Safari では正しく機能しないため、Safari(iPhone、Macどちらも)環境では動作させることができない。

例えばiPhoneのSafariでも動作するWebアプリを作成したいとなった場合、Netlify を選ぶのがオススメかと思われる。

他のサイト事例について

今のところ、NetlifyGitHub Pages しか検証できていないが他にも似たようなサービスはたくさんあるので何か分かり次第追加していく予定。
逆にご存知のケースがあればコメントいただけるとありがたいです。

Yuki ShindoYuki Shindo

余談:Duck DB Wasm + OPFSの場合、もっと簡単に動く

SQLite Wasm とは別に DuckDB Wasm がある。
利用用途としては異なるが、こちらも利用することで SQLite Wasm 同様にWebフロントエンドに完結した形でDBの利用が可能となる。
(実際に私自身も DuckDB Wasm + OPFS の構成でログ分析用のアプリを書いて利用しているがとても便利)

そして Duck DB WasmOPFS を利用する場合、ここまで書いてきたようなヘッダーの設定を適用する必要はなく、普通に動く

これは Duck DB Wasm 側での実装が SharedArrayBuffer に依存していない実装になっているのかと想像しているが詳細までは追えていない。