PGliteとOPFSでブラウザ内DBを試すメモ

PGliteはPostgreSQLをWASMにコンパイルしてブラウザやNode.jsで動かせるライブラリ。永続化先にOPFSを使うとIndexedDBより速いんだけど、Safariでまともに動かない問題がある。このあたりのメモとして残す。

OPFS

WHATWGのFile System Living Standardで標準化された、オリジンごとに隔離されたプライベートファイルシステム。ユーザーからは見えない。通常のFile System Access APIと違い、パーミッション確認やSafe Browsingチェックのオーバーヘッドがない。

https://web.dev/articles/origin-private-file-system

FileSystemSyncAccessHandleを使うことでWeb Worker内限定だけど、バイト単位の同期的なランダムアクセスができる。

// Web Worker内
const root = await navigator.storage.getDirectory();
const fileHandle = await root.getFileHandle('mydb', { create: true });
const accessHandle = await fileHandle.createSyncAccessHandle();

const buffer = new ArrayBuffer(1024);
accessHandle.read(buffer, { at: 0 });
accessHandle.write(new Uint8Array([1, 2, 3]), { at: 512 });
accessHandle.flush();
accessHandle.close();

データベースはページ単位でランダムアクセスするので、オブジェクト単位でしか読み書きできないIndexedDBより相性が良い。

PGliteと

PGliteはElectricSQLが開発しているPostgreSQLのWASMビルド。過去のブラウザPostgres系プロジェクトがLinux VMエミュレーションで動かしていたのに対して、PostgreSQLのシングルユーザーモードをWASMに直接コンパイルしている。

import { PGlite } from '@electric-sql/pglite';

const db = new PGlite();
await db.query("SELECT 'Hello world' as message");

普通のPostgreSQLのSQLがそのまま動く。pgvectorも拡張としてロードできるので、ブラウザ内でベクトル検索まで完結する。

本記事のコード例は @electric-sql/pglite@0.3.16 で動作確認。0.4.x では API が変わっている部分があるので注意。

OPFSバックエンドで永続化する

PGliteのOPFSバックエンドは opfs-ahp(Access Handle Pool)という実装。

https://github.com/electric-sql/pglite/issues/9

PGliteは完全に同期的なWASMビルドで、クエリ処理中に非同期APIを呼べない。createSyncAccessHandle() はPromiseを返すので、あらかじめランダム名のアクセスハンドルをプールしておき、PostgreSQLのファイル操作を同期的にマッピングする。

https://pglite.dev/docs/filesystems

When you first start PGlite we open a pool of OPFS access handles with randomised file names; these are then allocated to files as needed.

import { PGlite } from '@electric-sql/pglite';
import { OpfsAhpFS } from '@electric-sql/pglite/opfs-ahp';

// Web Worker内で実行
const db = new PGlite({ fs: new OpfsAhpFS('my-pglite-db') });

メインスレッドからは PGliteWorker 経由で使う。

// worker.ts
import { PGlite } from '@electric-sql/pglite';
import { OpfsAhpFS } from '@electric-sql/pglite/opfs-ahp';
import { worker } from '@electric-sql/pglite/worker';

worker({
  async init() {
    return new PGlite({ fs: new OpfsAhpFS('my-pglite-db') });
  },
});
// main.ts
import { PGliteWorker } from '@electric-sql/pglite/worker';

const db = new PGliteWorker(
  new Worker(new URL('./worker.ts', import.meta.url), { type: 'module' })
);
await db.waitReady;
await db.query('SELECT NOW()');

Safariで動かない問題

PGlite公式より。

Safari appears to have a limit of 252 open sync access handles, this prevents this VFS from working due to a standard Postgres install consisting of over 300 files.

PostgreSQLはシステムカタログ、WAL、一時ファイル等で300ファイル以上使う。Access Handle Pool方式では各ファイルに SyncAccessHandle が要るので、Safariの252個制限に引っかかる。SQLiteは単一ファイルなのでこの問題は起きない。

他にもSafari固有の制約がある(PowerSync調査)。 - プライベートブラウジングではOPFS自体が使えない - readwrite-unsafe モード未対応(Chrome 121+のみ)

252ハンドル上限の引き上げについては具体的な動きが見当たらない(誰か知ってたら教えてください)。

なのでSafari向けにはIndexedDBへフォールバックする。FileSystemSyncAccessHandle の存在チェックだけでは不十分で、SafariはAPI自体をサポートしているため検出できない。初期化途中の300ファイル超の時点でハンドル枯渇エラーになるので、UA判定で事前に回避する。

// worker.ts
import { PGlite } from '@electric-sql/pglite';
import { worker } from '@electric-sql/pglite/worker';
import { OpfsAhpFS } from '@electric-sql/pglite/opfs-ahp';

function canUseOpfsAhp(): boolean {
  if (typeof navigator?.storage?.getDirectory !== 'function') {
    return false;
  }
  // Safari は OPFS API 自体はサポートしているが、252ハンドル制限で PGlite は動かない。
  // UA で WebKit かつ非 Chromium(= Safari)を判定する。
  const ua = navigator.userAgent;
  const isWebKit = /AppleWebKit/i.test(ua);
  const isChromium = /Chrome|Chromium/i.test(ua);
  return !(isWebKit && !isChromium);
}

worker({
  async init() {
    if (canUseOpfsAhp()) {
      return new PGlite({ fs: new OpfsAhpFS('my-db') });
    }
    return new PGlite('idb://my-db', { relaxedDurability: true });
  },
});

注意点

  • Safari非対応(OPFS AHP)。252ハンドル制限でIndexedDBフォールバック必須
  • Web Worker必須。SyncAccessHandle はメインスレッドで使えない
  • シングルタブ制約。複数タブで同じDBを開くと壊れる。SharedWorkerかBroadcastChannelでロックが要る
  • ストレージquota。navigator.storage.persist() しておくのが安全

新しいOPFS VFSが計画されているので、そちらも追っておく。