Bit Torrentを使ってWebアプリでのサーバレスのデータ同期

忙しい人向け:こちらにデモ用のプログラムがあります。

ここのところ、私たちのチームはたくさんの革新的なウェブアプリを作り、アプリ作成のための考え方を示してきました。AirhornerVoice MemosGuitar TunerSVG-OMGがすぐ思い浮かぶでしょう。これらのWebサイトに共通しているのは、データを保管したり同期したりするためのサーバコンポーネントを持っていないことです。私たちはこれらのサイトを、Web上のある種のエクスペリエンスの例および実装リファレンスのために作成しました。ビジネス向けに作られる完全な”アプリ”を目指したわけではありません。

最近、新たなWebアプリのアイディアが浮かびました。Ben Thompsonのポッドキャストの未来に載っているポッドキャストにインスパイアされたのです。サイトを訪れて録音ボタンを押すというシンプルなポッドキャスティングを作りたいと思いました(何年か前に、FrinedBooという名で似たようなものを作りましたが、公衆回線網でしか使えませんでした)。

技術的には、必要なウェブプラットフォームの全パーツがそろっています。オフライン作業をService Workerで、ローカルにおける相当な量のデータ保管にはBlobとIndexed DBで、入力とファイルへの記録はMicrophoneを使ってMedia Stream Recorderで行います。最後のパーツは、ブラウザとインターネット接続があればどこからでもWebアプリにアクセスできるURLです。

いったんウェブサイトに接続すると、通常はあるデバイスから記録されたデータがどこかのサーバに保管され、以後はそのサイトにアクセスするマシンが何であろうと、あとからそのデータを利用できます。従来は、データをどこかのサーバに保管するWebサービスを作ると、全てのユーザをサポートするために全てのインフラを運営していたのです。

しかし、私はあるサービスからデータを保管したり検索したりするサービスを作る気はありません(これには法務部門の確認をとらなければならず、それは悪夢です)。なので、サービスではWebアプリ内でローカルに全てが保存される必要があります。大きく問題になるのは、他のデバイス上のWebアプリの他のインスタンスへデータを渡す方法と共有する方法です。

Paul Lewisが作ったVoice Memosはもう一つの例です。


全データはアプリ内でローカルに保管

データが録音され、ローカルに保管されます。つまり、データを自分の他のデバイスと同期したり、友人と共有したくても、不可能なのです。

これには解決策があります。例えば、base64でオーディオファイルを動的にエンコードして、カスタムURLを生成することができるかもしれません。だたし、拡張性はありません。

それはちょっとした難しい問題で、解決したいと思っていました。私が作りたいものに必要な簡単な要件を挙げておきます。

  • ユーザは自分たちのローカルデータを示す簡単なURLを共有できなければならない。
  • ユーザは、自分たちのブラウザまたはWebアプリ以外でデータを手動で保存する必要があってはならない。
  • データを蓄える”バックエンド”があってはいけない。
  • 同期は理想をいえば、P2Pで行われなければならない。

さて。話は6カ月前にさかのぼります。私は2015年ColdFrontカンファレンスに参加して、Feross Aboukhadijehの講演を聴きました。その内容はWeb RTCデータチャネルとBitTorrent、それに彼がどのようにしてWebTorrentと呼ばれるプロジェクトを始めたかについてでした。とても素晴らしい話だったのですが、彼が話していたことの潜在能力について、最近まで頭の中で抱えている課題と結び付いていませんでした。

今では、WebTorrent.io.を利用することにより、解決の糸口をつかんでいると思います。


Web Torrent

Web Torrentについては、「BitTorrentスタイルの分散データ配信とWebRTCとを組み合わせたもの」ということ以外は説明しませんが、かなり素晴らしい話です。是非ともその内容を確認することをみなさんにお勧めしたいのですけどね。

私が考えていた理論は次のようなものです。もしユーザクライアントがTorrentネットワークのピアとして振る舞うことができるならば、Webサイト固有のデータの一部をシードすることができます。そしてユーザは自分のもう一つのデバイスや他のユーザとも共有できるようなトレントリンクを生成することができ、Webアプリの”リモート”インスタンスは”クライアント”インスタンスからデータをフェッチします。

  • ユーザがページにアクセスする。
  • ユーザが何かしらの作業を行う。
  • ユーザがIndexedDBに作業内容を保存する。
  • ユーザが”共有”をクリックすると、”マグネット”URLを生成し、Audio Blobのシードを開始します。
  • ユーザは共有するか、他のブラウザでそのURLを開きます。
  • WebサイトはマグネットURLをパースしトラッカーに接続します。
  • Webサイトはトラッカーからピアを探し、ピアと接続してデータをダウンロードします。


データの大まかな流れ

1対1の接続に対しては、これは少し過剰かもしれません。単にWebRTCのシグナリングサービスを生成し、1台のクライアントから別のクライアントに対しデータを得るために別のインスタンスにメッセージを届けるということもできます。

このアプローチが興味深い点は、2人以上とデータを共有する時に、うまく拡散できるということです。

理論を現実の世界の実例に適用する

前に簡単にVoice Memosについて触れました。それは稼働するアプリに私の理論を組み込んでみた素晴らしいリファレンスアプリケーションでした。これは、ポッドキャスティングアプリの考えに近く、録音用にサーバベースの補助記憶装置を持っていないので、クライアント間でデータを同期させる方法が必要です。


Voice Memos

“共有”ボタンを追加する以外、あまり変更しませんでした。BitTorrent Voice Memosのデモで確認できます。簡単なオーディオファイルを録音して、それを保存し、共有してください。”共有”ボタンは、別のデバイスや他の人に送信できるURLを生成します。

出来栄えにはかなり満足しています。このデモの準備にかかった時間は、全部でわずか2、3時間です。

ここで、私が行った主な必須処理を幾つかざっと説明します。

全ての魔法を動かすために使われるwebtorrent.min.jsスクリプトを追加します。

<script src="https://cdn.jsdelivr.net/webtorrent/latest/webtorrent.min.js"></script>

他の全てのクラスで使用できるWebTorrentのAPIのシングルトンインスタンスを作成します。(Paul Lewisにインスパイアされています)

export default function TorrentInstance () {

  if (typeof window.TorrentInstance_ !== 'undefined')
    return Promise.resolve(window.TorrentInstance_);

  window.TorrentInstance_ = new WebTorrent();

  return Promise.resolve(window.TorrentInstance_);
}

全てのメモをシードします。これは一般には少しやり過ぎですが、デモではそうしました。this.memosは、IndexedDBに格納されていたVoiceMemosのArrayインスタンスです。

 seed () {
    this.memos.forEach((memo) => {
      // Seed the memo.
      TorrentInstance().then(torrentClient => {
        torrentClient.seed(
          memo.audio, // Audio is a blob, but that is ok.
          {
            name: `${memo.title}.webm`,
            comment: memo.description || '',
            creationDate: memo.time || Date.now()
          },
          torrent => {
            console.log(torrent);
            memo.torrentURL = torrent.magnetURI;
            memo.put();
          });
      });
    });
  }

ユーザが共有ボタンをクリックすると、カスタムURLが作成されます。そのURLには、トレントへアクセスし、別のクライアントにデータをストリーミングするために必要な情報が全て含まれます。

onShareButtonClick(e) {
    MemoModel.get(this.memoId).then( (memo) => {
      return `/share?seeds=${encodeURIComponent(memo.torrentURL)}`;
    }).then(uri => {
      history.pushState({}, "Share seed", uri)
    });
  }

トレント情報が含まれているURLをユーザが入力したことを検出した場合は、そのトレントをフェッチするために幾つかのカスタムロジックを実行する必要があります。Voice Memosアプリには、”/share”で始まるURLを探すカスタムルータがあります。

router.add('share',
          (url) => this.fetchTorrent(location.search.slice(1)),
          (out) => console.log(out)
         )

次にトレント情報をパースする必要があります。その後、WebTorrent APIを使って、BitTorrentのネットワークからファイルをフェッチします。

いったんファイルをフェッチしたら、その後、お決まりのXMLHttpRequestを使ってBlobからファイルデータを取得する必要があります。

fetchTorrent(url) {
  const seeds = new URLSearchParams(url);
  let seedURLs = seeds.getAll('seeds');

  for(let seedURL of seedURLs) {
    TorrentInstance().then(torrentClient => {

      torrentClient.add(seedURL, {}, torrent => {

          var file = torrent.files[0];

          file.getBlobURL((err, url) => {

          var xhr = new XMLHttpRequest();
          xhr.open('GET', url, true);
          xhr.responseType = 'blob';
          xhr.onload = function(e) {
              if (this.status == 200) {
              var audioData = this.response;

              const newMemo = new MemoModel({
                  audio: audioData,
                  volumeData: audioData, // Assuming pre-normalised
                  title: torrent.name.replace(/\.webm$/, ""),
                  torrentURL: torrent.magnetURI,
                  description: ""
              });

              newMemo.put();
              }
          };
          xhr.send();

          });

      });
    });
  }
}

まとめ

これはかなり大雑把に作成したものなので、プライベートなデータに使用すべきではないということをお話しして、この記事を終わりにしたいと思います。なぜなら、今もし、データがネットワーク上に公開されて、人々がトレントへのURLを持っていたら、データはアクセス可能になるからです。

この概念は重要だと思います。データを同期することができ、しかも仲介者もWebアプリ内にサーバロジックも要らないということは重要な概念であり、そのようなエクスペリエンスを構築する方法を積極的に検討する必要があります。

では、このプログラミングを続けるので、これで失礼します。

追記

  • シードチェックロジックを幾つか削除。