🤑

SQLite Wasm + OPFSで簡単アプリ化!ビジネスに活かすWasmとWeb技術

2024/10/01に公開

Wasmでお金を稼げるプロダクトは作れるのか?

こんにちは。私は技術者としてはかなりミーハーな部類に入ります。
そんなミーハーな私は盛り上がっている分野についてはとりあえず触ってみたい欲が先行します。

ここ数年で盛り上がっている分野といえば色々とありますが、例えばWasmはソフトウェアエンジニア界隈を中心に盛り上がっている分野の一つとして数えてもよいでしょう。

私自身Webフロントエンド開発なども行っているため、分野的にもWasm、そしてWasmを活用した事例などは気になるところです。

ところでこういった気になる分野の技術については、なるべくビジネス的にも機能する形で落とし込みたいと常日頃考えています。
つまり その技術を使うことでお金を得ることができる という状況で使いたいのです。

これは私の性格的な部分が大いに影響していると思いますが、趣味的な用途でしかその技術を利用しない場合、ちょっと作って満足してしまい『以降触る回数が0になる』ということがよくありますが、逆に仕事として利用せざるを得ない状況を作り出すことで、ある一定のプレッシャーを背負って向き合うことになるため、ある程度は学習効率が良くなる傾向にあります。

これは過去に実績があり、Go言語は趣味では何度書いても全く覚えられなかったのにもかかわらず、プロダクトでGoを採用したことで最低限は利用できるようになり、このGoで書かれたプロダクトは今も売上を生み出しています。

話は戻りますが、Wasmは興味がある分野なので、これを使ってお金を稼ぐことはできないか?と常日頃考えていました。しかし私はWebフロントエンド上でFigmaのようなリッチなアプリケーションを提供するアイデアは今のところないし、Wasmを使ったクールでお金を稼げるアイデアは全く浮かんできませんでした。

SQLite WasmとOPFSの組み合わせでWebフロントエンドにデータ永続化の光をもたらす

そんなある日、SQLite Wasm + OPFSを組み合わせればフロントエンド上でデータの永続化でできるという話を目にします。

SQLite Wasmはざっくりと書いてしまうとブラウザ上で使えるWasm化されたSQLiteです。
(ざっくりしすぎですね)

OPFSについては以下のMDNの記事が参考になりますが、概要等も以下に記載していきます。
https://developer.mozilla.org/ja/docs/Web/API/File_System_API/Origin_private_file_system

OPFSの特徴

  • ファイルシステムAPIの一部として提供されるストレージエンドポイントです。
  • パフォーマンスのために高度に最適化されています。
  • ユーザーからは見えない状態で、ブラウザからフォルダ、ファイルを扱うことができます
  • ウェブアプリケーションのオリジン(ドメイン)に紐付く形で管理できます
  • ウェブワーカー内で同期的に操作できるAPIも提供されています。
  • ブラウザのストレージ容量制限の対象となります。
    (ブラウザのストレージ容量制限については以下を参照ください)
    https://developer.mozilla.org/ja/docs/Web/API/Storage_API/Storage_quotas_and_eviction_criteria

OPFSとlocalStorageの違い

このOPFS、私は最初 localStorage とは何が違うのかと思いましたので、違いについても記載しておきます。
(上の内容を見れば違いは一目瞭然ですが)

a. データ構造:

  • OPFS: ファイルシステム構造(ファイルやフォルダ)を持ちます。
  • localStorage: キーと値のペアで構成される単純な構造です。

b. ストレージ容量:

  • OPFS: 比較的大きな容量を扱えます(ブラウザの制限内)。
  • localStorage: 通常5-10MB程度の小さな容量制限があります。

c. パフォーマンス:

  • OPFS: 大量のデータや頻繁な読み書きに適しています。
  • localStorage: 小規模なデータの保存に適していますが、大量のデータには向いていません。

d. API:

  • OPFS: ファイルシステム操作に似たAPIを提供し、非同期操作が可能です。
  • localStorage: シンプルなキー・バリューのセッター/ゲッターAPIのみで、同期的です。

e. データタイプ:

  • OPFS: バイナリデータを含む様々な種類のデータを扱えます。
  • localStorage: 文字列のみを保存でき、他のデータ型は文字列に変換する必要があります。

f. 用途:

  • OPFS: 大規模なデータセット、データベース、頻繁な更新が必要なアプリケーションに適しています。
  • localStorage: ユーザー設定、セッション情報など、小規模で簡単なデータの保存に適しています。

OPFSの制限:

そんなOPFSですが制限もあります。

  • 比較的新しい技術のため、ブラウザのサポートが限られています。
    • Safariでは厳しそうかも(iPhone / iPadでもこれを使いたかったので残念...😭)
  • Chromeの開発者ツールからlocalStorageのように簡単に確認することは現時点ではできません
  • 同じくChromeの開発者ツールから気軽にOPFS上のデータを削除できない
  • ちなみにここについては、Chromeの開発者ツールからデータを見るための拡張機能などもあるようです

結局OPFSの利点とは?

ドメインに紐づく形でファイルの読み書きをユーザーには意識させずに行うことができる仕組みです。Safariに完全対応していない点を除けば便利そうです!

SQLite WasmとOPFSを組み合わせる

思った以上にOPFSの説明が長くなってしまいましたが、SQLite Wasmで作成したDBファイルをこのOPFSで管理することによりブラウザ上でのデータ永続化が実現できます。

で、SQLite Wasm + OPFSはビジネス的にどう有効なの?

ここでSQLite Wasm + OPFSの組み合わせがどう有効なのか?と疑問に思った方も多いでしょう。

少しだけ私の話をしますと、普段は自身で作成したtoB系のプロダクトを提供するなどしています。

ビジネス規模的には超小規模ですが、企業が参入する価値を見出さない場所で事業を行うことで小規模だからこそ成立するビジネスを行っています。

そういったビジネスでは毎月の経費を切り詰めるのはとても重要です。
毎月のサーバー代に数万円払うよりは多少労力を払っても経費削減に手をいれるほうが、ビジネス的には長期的にメリットが大きくなります。
(こういった分野は複数の開発者を抱えた場合に毎月の収支が赤字となるケースが多いため、まずある一定規模以上の企業は参入してきません)

最近だとスモールビジネスという言葉をよく耳にしますが、たぶんこれに近いかと思います。

実際に現在行っている事業の1つではWebアプリケーションでの提供は行わず、デスクトップアプリという形で提供してサーバー代を浮かすなどしています。
これはデスクトップアプリ形式でないと提供できない機能があるので結果的にそうなっているものの、デスクトップアプリ形式にして認証など最低限の機能を提供したAPIのみを別途用意することで大幅な経費削減になっており、ビジネスとしては黒字化できています。
APIを提供するサーバーはかなり低スペックですが、規模の小ささも相まって余裕でさばけます。
(まあ当然作って終わりではないので、営業やらカスタマーサポートやら、様々な面で苦労はしていますが...)

というわけで、SQLite Wasm + OPFSですが、要はユーザーの画面内でのみデータ永続化できる仕組みが作れます。
デスクトップと構成的に似てはいるもののデスクトップアプリをインストールしてもらう必要もないし(※)、URLにアクセスすればそこで永続化されたデータを用いて価値を提供できるので、これは使えるのでは?と思っています。
(※PCを普段触らない方にとってはデスクトップアプリのインストールは大きな障壁にもなります)

もちろんブラウザを変えた場合にデータ同期は行えないなど色々とありますが、この仕組み自体にはビジネスチャンスがあるかもしれないと考えています。
(また、ここについては機能追加などである程度は対応もできます)

SQLite Wasmはどう使う?

色々とSQLite Wasm関連のプロジェクトはありそうですが、私は以下の sqlite-wasm を利用しています。

https://github.com/sqlite/sqlite-wasm

使い方は以下のような形です。

これは実際にTODOアプリをSQLite WasmとOPFSを組み合わせて作成したときのコードです。
https://github.com/shinshin86/todo-opfs-sqlite

https://github.com/shinshin86/todo-opfs-sqlite/blob/main/src/db.ts

OPFS については以下の部分で処理しており、 OPFS に対応していなければメモリを利用するようにしています。

// OPFS
let openResponse;
try {
  openResponse = await promiser('open', {
    filename: 'file:todo.sqlite3?vfs=opfs',
  });
  console.log('OPFS database opened:', openResponse.result.filename);
} catch (opfsError) {
  console.warn('OPFS is not available, falling back to in-memory database:', opfsError);
  openResponse = await promiser('open', {
    filename: ':memory:',
  });
  console.log('In-memory database opened');
}

このような実装をするだけでフロントエンドだけでデータ永続化が行えるのは非常に嬉しいです。

アプリケーションレイヤーでサクッと実装していくための開発フロー(neverchange + sqlc)

SQLite Wasm + OPFS 自体は上に書いた実装で問題なく動きますが、アプリケーション開発をしていく上ではもっと簡略化した形でコードを書きたいと思ったので SQLite Wasm + OPFSをラップしたライブラリを作りました。

WebフロントエンドでSQLite Wasm + OPFSを用いてデータ永続化を行えるライブラリ『neverchange』のご紹介

名称は neverchange と言います。

まだアルファ版ですが、日々ドッグフーディングをしつつ、安定するまで開発は続けていく予定です。

neverchange - logo

https://github.com/shinshin86/neverchange

余談ですが、ライブラリの名称については『アナと雪の女王2』に出てくる Some Thing Never Change という劇中歌から着想を得ています。
(個人的には名曲です。ぜひ聴いてみてください)

このライブラリを使うと以下のようにフロントエンド上で実装が可能です。
※より具体的な実装については後ほどリポジトリを紹介します

import { NeverChangeDB } from 'neverchange';

async function main() {
  // Initialize the database
  const db = new NeverChangeDB('myDatabase');
  await db.init();

  // Create a table
  await db.execute(`
    CREATE TABLE IF NOT EXISTS users (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      name TEXT NOT NULL,
      email TEXT UNIQUE NOT NULL
    )
  `);

  // Insert data
  await db.execute(
    'INSERT INTO users (name, email) VALUES (?, ?)',
    ['John Doe', 'john@example.com']
  );

  // Query data
  const users = await db.query('SELECT * FROM users');
  console.log('Users:', users);

  // Close the database connection
  await db.close();
}

main().catch(console.error);

またDBのmigration機能もつけました。

SQLite Wasmを用いる場合、ユーザー側(Webフロントエンド)でしかDB管理できませんが、migrations 内に詰めていくことで、途中でテーブル構成を変更した場合にも対応できます。
(こういうのって他にもっと良いアイデアあるのでしょうか?私自身に知見が足りないという思いもあり、もしあれば教えていただけると嬉しいです🙇‍♂️)

import { NeverChangeDB } from 'neverchange';

// Define migrations
const migrations = [
  {
    version: 1,
    up: async (db) => {
      await db.execute(`
        CREATE TABLE users (
          id INTEGER PRIMARY KEY AUTOINCREMENT,
          name TEXT NOT NULL
        )
      `);
    }
  },
  {
    version: 2,
    up: async (db) => {
      await db.execute(`
        ALTER TABLE users ADD COLUMN email TEXT
      `);
    }
  }
];

async function main() {
  // Initialize the database with migrations
  const db = new NeverChangeDB('myDatabase', { isMigrationActive: true });
  db.addMigrations(migrations);
  await db.init();

  // The database will now have the latest schema
  const tableInfo = await db.query('PRAGMA table_info(users)');
  console.log('Users table schema:', tableInfo);

  await db.close();
}

main().catch(console.error);

このような形で最低限の機能がついています。
(他に dumpなどもありますが割愛)

sqlcでSQLを用いてコード生成を行う

ただ、これだけでは地道にSQLを書いていくことは避けられません。

もっと楽にDB操作のためのコードを作成できないものだろうか?と思い、目をつけたのが sqlcでした。

sqlcについては既にZennに非常にわかりやすい記事がありますので、説明は割愛します。
https://zenn.dev/shiguredo/articles/sqlc-gen-typescript

上の記事で紹介されている sqlc-gen-typescript を使って neverchange 用のコードを生成すれば楽ができるのでは?と思い、えいやっ!の精神でforkして neverchange 対応したリポジトリが以下です
https://github.com/shinshin86/sqlc-gen-typescript/tree/neverchange

(追記:上記リポジトリですが、よりneverchangeで簡単に使えるようにフォークしたリポジトリ自体を別途作成しました。こちらについてはこのあと説明していきます)

使い方は

# 対象のブランチを指定してclone
git clone -b neverchange https://github.com/shinshin86/sqlc-gen-typescript.git
cd sqlc-gen-typescript
npm install

# build
make out.js

# Wasmファイルを生成するためにJavyを利用します
# このコマンドは、既にインストールしてパスが通っている前提です
make examples/plugin.wasm

上記コマンドを実行すると example ディレクトリに plugin.wasm が生まれます。

あとは sqlc.yaml に以下のように設定を記載し sqlc generate を実行すれば、neverchange用のコードが生成されます。

version: '2'
plugins:
- name: ts
  wasm:
    url: file://{生成したplugin.wasmのパス}
    sha256: {sha256_of_your_wasm_file} # この項目はなくても動くかも
sql:
- schema: "{schema.sqlのパス}"
  queries: "{query.sqlのパス}"
  engine: "sqlite"
  codegen:
  - out: "{出力先のパス}"
    plugin: ts
    options:
      runtime: node
      driver: neverchange

これらの過程を踏んで実際に作成したTODOリストアプリがあります。

https://github.com/shinshin86/mytodolist

実際に以下のようなコードが生成され、アプリ側から簡単に扱えるようになります。

https://github.com/shinshin86/mytodolist/blob/main/src/db/query_sql.ts

個人的にWebフロントエンドでそこまで複雑で大規模なDB操作を行うケースはないかと思うので、sqlcを組み合わせることで、ある程度の開発需要は満たせるのかと考えています。

追記:neverchange用のsqlcプラグイン『sqlc-gen-typescript-for-neverchange』を作成しました

https://github.com/shinshin86/sqlc-gen-typescript-for-neverchange

上に書いたフォークしたリポジトリをもとに、より簡単にsqlcで使えるようにするために sqlc-gen-typescript-for-neverchange という neverchange特化のプラグインを作成しました。

リリースバイナリも直接ダウンロードできる場所に置いているため、sqlc.yaml に以下のように記述することで簡単にコード生成が可能です。

version: '2'
plugins:
- name: ts
  wasm:
    url: https://github.com/shinshin86/sqlc-gen-typescript-for-neverchange/releases/download/v0.0.1/sqlc-gen-typescript-for-neverchange_0.0.1.wasm
    sha256: 224c6494cc2f8383ae79a2ca4f9d3c5ce0ebacbf6df161d98c414bdfaf1db82d
sql:
- schema: "schema.sql"
  queries: "query.sql"
  engine: "sqlite"
  codegen:
  - plugin: ts
    out: src/authors
    options:
      runtime: node
      driver: neverchange

コード生成自体は普段の sqlc と同様に

sqlc generate

でSQLをもとにコード生成が可能です。

フロントエンドでSQLite Wasm + OPFSでアプリを作成しようと考えている方は是非試してみていただけると嬉しいです。

OPFSをGitHub PagesやNetlifyでは使えない問題の対処法

上で紹介した TODOリストアプリ は実際にGitHub Pages上で動作しています。

Safari 以外のブラウザで以下にアクセスすることで実際に永続的なデータ管理の元、あなただけのTODOリストが使えます。

https://shinshin86.github.io/mytodolist/

ただ、実際は上に書いたコードだけではGitHub Pageなどでは動きませんでした。
最初動かそうとした際に、以下のようなエラーが出てしまい、OPFSが有効にならなかったのです。

Ignoring inability to install OPFS sqlite3_vfs: Cannot install OPFS: Missing SharedArrayBuffer and/or Atomics. The server must emit the COOP/COEP response headers to enable those

これを解決するために以下のライブラリを利用しています。

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

実際の使い方については先ほど紹介したTODOリストのリポジトリの gh-pages ブランチをみていただくのが良いかと思います。

これはGitHub Pagesだけではなく、Netlifyでも同様の挙動でしたので、恐らく他の似たようなホスティングサービスでも同じかと思います。

SQLite Wasm + OPFS でアプリケーションを作りたい方はチェックしておくとよいかと思います。

面白そうなWasm

最初にも書いた通り、Wasm界隈は現在進行系で色々と進んでいっている分野なので技術好きな人は興味深い分野かと思います。
そんな分野に触れつつもビジネス的にも機能する動きをしたいと思い、今回ここに書いた一連の流れを最近行っています。

まあWasmとはいえ、やっていることは所詮DB操作をフロント側に移しただけの伝統的な手法じゃん...というツッコミもありそうですが、そこは自分の知見が狭すぎるのが問題だと思うので、ほかにWasmを活用できそうなアイデアなどもありましたら教えていただけると嬉しいです。

長文になってしまいましたが、最後まで読んでいただき、ありがとうございました!

Discussion