案が期待通り動作し、IndexedDBに情報が保存された様子

前回の記事はこちら

パソコンとスマートフォンの両方で同じデータにアクセスできるように、ブラウザ版の作成とブラウザ間でデータを同期するための仕組み作りを進めます。

サーバとブラウザで動作するクライアント間でデータを同期するのに、Web Workerを使う案を考え、それが実現できることを確認した、その記録です。

この記事中で作る内容

  • CORSを許すWeb APIを用意する
  • Web Workerでサーバからデータを受け取り、それをIndexedDBに保存する

Web APIを用意する

以前作成したDropbox風のファイル操作APIを応用して作ります。関数名が変わっていたりしますが、基本的に同一のものです。今はファイルをファイルとして保存していますが、後々何らかのデータベースに保存するように変更するつもりです。

このAPIをShishoと名付けました。司書です。

Ki6cooではVue.jsを動かすViteが3000番ポートを使用しています。次の番号という理由でShishoは3001番ポートを使用することにしました。するとブラウザで動くJavaScriptにとってのOriginはlocalhost:3000ですから、Shishoのlocalhost:3001へのアクセスは別Originへのアクセスとなります。リバースプロキシを用意すれば同Originにできますが、開発環境ではリバースプロキシの導入をしないことにしました。リバースプロキシよりもCORSの方が簡単だと思ったからです。

開発時にしか使わないので、access-control-allow-originが*と緩い指定になっています。

async fn allow_cors() -> impl IntoResponse {
   Headers([
      (header::ACCESS_CONTROL_ALLOW_ORIGIN, "*".to_string()),
      (header::ACCESS_CONTROL_ALLOW_METHODS, "POST".to_string()),
      (header::ACCESS_CONTROL_ALLOW_HEADERS, header::CONTENT_TYPE.to_string()),
   ])
}

このallow_corsがOPTIONSメソッドでアクセスされたときのハンドラーになるようにRouterを変更します。

let app = Router::new()
     .route("/files/list-directory", routing::post(list_directory).options(allow_cors))

JavaScriptでCORSするとき、まず呼び出したいURLがOPTIONSメソッドで呼び出されます。そのOPTIONSでのリクエストのレスポンスのaccess-control-allow-*ヘッダーにより、CORSリクエストで何が許されるのかを指定します。

access-control-allow-origin: *
access-control-allow-method: POST
access-control-allow-headers: Content-Type

allow_cors関数はこのヘッダーを返しているので、全OriginからのCORSを許可、CORSでPOSTメソッドの利用を許可、CORS時にContent-Typeの指定を許可となります。

この記事中では省略しましたが、CORSで実際に呼びたい関数もaccess-control-allow-originヘッダーは返さなければなりません。ここではlist_directoryです。

Web WorkerからShishoを呼び出し、IndexedDBに保存する

sync-worker.tsを以下の内容で作成しました。IndexedDBにアクセスするためのモジュールとShokoのAPIのTypeScript向けのクライアントモジュールは省略します。

import { openStorage } from '../libs/app/storages/indexed-db-storage'
import { Shisho } from '../libs/shisho'

export interface StartMessage {
   command: 'start'
   args: {
      indexedDbName: string
   }
}

/**
 * 同期処理を開始します。
 *
 * 同期処理は5秒ごとに繰り返されます。
 * @param message startコマンドを受け取ったときのStartMessageを与えてください。
 */
const start = (message: StartMessage): void => {
   // 動作確認のために5秒と短い間隔で繰り返しています。
   const INTERVAL_IN_MILLISECONDS = 5000

   const shisho = new Shisho()
   const storage = openStorage(message.args.indexedDbName)

   setInterval(async (): Promise<void> => {
      // 動作することを確認するための簡易実装です。
      // ファイルの存在確認や、ファイルの新しい方から古い方へコピーするように後に変更します。
      const listDirectoryResponse = await shisho.listDirectory('.')
      const syncPromises = listDirectoryResponse.entries.map(
         async (entry): Promise<void> => {
            try {
               await storage.readFile(entry.name)
            } catch {
               storage.writeFile(entry.name, await shisho.download(entry.name))
            }
         }
      )
      await Promise.all(syncPromises)
   }, INTERVAL_IN_MILLISECONDS)
}

addEventListener('message', (message: MessageEvent<StartMessage>): void => {
   // メッセージが一種類だけなので、Web Workerとして起動したときに自動的に処理を開始してほしいのですが、
   // Web Workerはmessageに対してしか動けないようです。
   if (message.data.command === 'start') {
      start(message.data)
   }
})

export default {}

これをWeb Workerとして動かします。これで本体の状態に関わらず、ファイルの同期を定期実行できます。

大雑把に、やっていることは以下です。

  1. ファイルの一覧を取得する
  2. 一覧の中にIndexedDBにないファイルがあるか確認する
  3. IndexedDBにないファイルがあれば、それを取得しIndexedDBに保存する
  4. この処理を5秒おきに繰り返す

繰り返しですが、まだ動作する最低限を作る段階なので、実装がお粗末です。しかし基本的な流れはこれで動作することが確認できる状態になりました。あとはこのWorkerをKi6coo起動時に起動するだけです。

案が期待通り動作し、IndexedDBに情報が保存された様子

Ki6cooを起動し、IndexedDBの状態を確認したスクリーンショットです。このスクリーンショットは、この記事のはじめにあったものと同一です。スクリーンショットの通り、IndexedDBにファイルが保存されることが確認できました。

次の予定

この方法でファイルを同期できることが実証できたので、詳細を作っていきます。