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が計画されているので、そちらも追っておく。