コンテンツにスキップ

ADR-0077: 訪問者のファイルを、埋め込みの ID ごとに IndexedDB に保存し、Node の実行環境ではシークレットのファイル、ビルドの出力、.git、リンクを除き、上限を付ける

Status

承認

Context

訪問者が実行環境で変えたファイルを、再訪のときに開くための、保存する場所と形を決める必要がある。
Node の実行環境で、訪問者の作業ディレクトリのうち、どのファイルを IndexedDB に保存するかも決める必要がある。

Decision

1. 保存する場所と形

訪問者のファイルは、play.fukuemon.dev の IndexedDB に保存する。

  • 埋め込みの ID ごとに、1 つのプロジェクトを保存する。キーは、ツールの名前と、埋め込みの ID の組にする。
  • 記事を指定しないで開いたとき(デフォルトのプロジェクト)は、固定のキーで 1 つを保存する。
  • レコードは、ファイルのパスと中身の一覧、保存した時刻、初期ファイルのバージョンを持つ。
  • 1 つのファイルが 1 MB を超えるものと、テキストとして読めないものは、保存しない。
  • 記事の初期ファイルが変わったとき(バージョンが違うとき)は、保存したファイルを残し、「初期の状態に戻す」の操作を出す。
  • IndexedDB を読めないときは、保存したファイルが無いときと同じに扱い、初期ファイルで起動する。画面に、保存できないことを出す。IndexedDB を読めなかった後は、ツールのページは、ページを開いている間、IndexedDB に書き込まない。
  • 記事の iframe と、トップレベルで開いたページは、同じキーを使う。
  • SQL だけを動かすツールと、Go を動かすツールも、同じキーの形で保存する。SQL だけを動かすツールは、入力欄の SQL を保存する。Go を動かすツールは、ファイルを保存する。
  • DB を使うプロジェクトの Node の実行環境は、SQL のペインの入力欄の SQL も、同じレコードに保存する。

IndexedDB にする理由は、キャンバスとスライドのエディタと同じ保存先になることと、3 つのブラウザで読み書きできたことである。

2. Node の実行環境で保存する対象

  • 保存する対象は、訪問者の作業ディレクトリの全体のうち、次のものを除いたものにする。訪問者がコマンドで作ったファイルも保存する。
    • どの階層でも、node_modules、.git、dist、.next、.nuxt、.cache、.turbo という名前のディレクトリの下のファイルと、これらの名前のファイル。build と out は、ソースのディレクトリにも使う名前なので除かない。
    • どの階層でも、.env、.env. で始まる名前、.envrc、.npmrc、.netrc、.yarnrc、.yarnrc.yml、.git-credentials のファイル。シークレットを置くことが多いためである。
    • ファイルでもディレクトリでもないもの(シンボリックリンク)。作業ディレクトリの外のファイルの中身を保存しないためである。
    • プロジェクトの JSON の files のキーと同じ規則に合わないパス(区切りに __proto__ があるもの、\ を含むもの)。保存すると、次に開いたときに、レコードの全体を形に合わないものとして扱うためである。
  • 作業ディレクトリのファイルの変更を監視して保存する。監視で通知を受け取れないときは、一定の時間ごとに、作業ディレクトリを読み、前回と中身が違うときに保存する。
    • 一定の時間ごとにする理由は、訪問者がシェルで打ったコマンドの終了を、ツールのページが受け取れないためである。
  • ページを開いたときは、IndexedDB から読んだレコードから、保存しないファイルを除いてから起動する。
    • 除いたファイルがあれば、作業ディレクトリが保存の上限を超えていても、除いた後のレコードを IndexedDB に書き直す。
    • 書き直しが失敗したときは、除いた後のレコードをメモリに持ち、書けるまで書き直す。
      • 次に保存のために作業ディレクトリを読んだとき、上限を超えていなければ、読んだファイルのレコードを書く。
      • 上限を超えたとき、保存のための読み込みが失敗したとき、起動し直す前の読み込みが失敗したとき、起動に失敗した実行環境を起動し直すとき、保存しないパスだけの変更の通知が来たときは、除いた後のレコードを書く。
      • ただし、新しい保存がキューにあるか、成功したときは、除いた後のレコードを書かない。新しい保存を古いレコードで置き換えないためである。
      • 書き直しがキューにある間と、書き込みが終わるまでの間は、新しい書き直しをキューに足さない。保存しないパスだけの変更の通知が続くときに、同じ書き直しを重ねないためである。
      • 訪問者の変更の保存が失敗して出た「ファイルを保存できませんでした」の案内は、書き直しが成功しても消さない。書き直しが書くのは、開いたときに読んだレコードから保存しないファイルを除いたものである。訪問者の変更は、書き直しが成功しても保存されていないためである。
      • 書き直しの失敗だけで出た案内は、書き直しが成功したら消す。訪問者の変更の保存が失敗していないのに、保存に失敗したという案内が残らないようにするためである。
      • 持っているレコードは、保存が成功したときと、初期の状態に戻したときに捨てる。
    • 保存しないファイルの規則を入れる前に保存したレコードに、シークレットのファイルを残さないためである。
  • 除いたディレクトリの外のファイルが 1000 個を超えるか、合計が 10 MB を超えたら、保存しない。前に保存したレコードは残し、保存していないことを画面に出す。
    • 読めたシンボリックリンクも、数と合計に数える。一覧を読んだ後に消えたファイルと、読めないリンクは数えない。
    • 超えた時点で、作業ディレクトリを読むのをやめる。超えたプロジェクトでも、保存のたびに全部を読まないようにするためである。
    • 1000 個は、読む時間が 5 秒の上限の 10 分の 1 以下に収まる。10 MB は、10 個のファイルで、読む時間と書く時間がそれぞれ 20 ms 以下だった。

承認の条件

  • 作業ディレクトリのファイルの変更の監視で、コマンドが作ったファイルの通知を受け取れるかを確認している。受け取れないときは、一定の時間ごとに作業ディレクトリを読む形で、保存できることを確認している。
  • play.fukuemon.dev をデプロイした後に、記事の iframe と、トップレベルで開いたページが、同じ保存先を読むかを確認している。

Verification

保存する場所と、変更の監視

次の項目は、断りの無いものは、Playwright 1.65.0 の Chromium、WebKit、Firefox(headless)で、localhost のサブドメインを使って測った。
マシンは Apple M2 である。実機のブラウザは未確認である。

  • トップレベルで開いたページは、3 つのブラウザで、IndexedDB を読み書きできた。
  • OPFS は、Chromium と Firefox で読み書きできた。WebKit では、navigator.storage.getDirectory() が UnknownError で失敗した。
  • 記事の iframe と、トップレベルで開いたページが、同じ保存先を読むかは、判定できなかった。
    • Chromium と Firefox は、別のサイトのページの iframe でも、トップレベルと同じ値を読んだ。Playwright のブラウザでは、保存先の分離が働いていないとみられる。
    • WebKit は、同じサイトの役のホスト名(fukuemon.localhost と play.fukuemon.localhost)でも、IndexedDB が別だった。WebKit が、この 2 つを同じサイトとして扱っているかは、確認していない。
  • 作業ディレクトリのファイルの変更の監視(files.changes)は、シェルのコマンドが作ったファイルも通知した。
    • この項目だけは、Playwright 1.64.1 の Chromium(headless)で、Apple M2 の vp dev の /node/ を開いて測った。
    • node -e で作ったファイル、作ったディレクトリ、入れ子のディレクトリの下に作ったファイル、追記、削除、名前の変更の、どれも通知が来た。パスは作業ディレクトリからの相対で、名前の変更は古いパスと新しいパスの 2 つが来た。
    • WebKit と Firefox では測っていない。

Node の実行環境で保存する対象

次の項目は、Apple M2 の vp dev の /node/ を、Playwright 1.64.1 の Chromium(headless)で開いて測った。WebKit と Firefox での動きは未確認である。

  • シンボリックリンクとして、. を指すものと、作業ディレクトリの外のファイルを指すものを作った。
    • WebContainers の fs.readdir の withFileTypes の項目は、2 つとも isFile() も isDirectory() も false だった。isSymbolicLink は無い。
    • リンクを除かないと、リンクをファイルとして読み、作業ディレクトリの外のファイル(/tmp/fukuemon-playground/ports.json)の中身を保存した。
  • .env、.npmrc、dist/a.js、.git/HEAD と、index.js を指すリンクを作ると、どれもレコードに入らなかった。起動し直した後も、.env、.npmrc、dist、.git は作業ディレクトリに残った。
  • 開いたときにレコードから保存しないファイルを除くことは、単体テストの fake で確認した。ブラウザでは未確認である。
  • 書き直しが失敗した後にファイルを変えると、上限を超えていなければ読んだファイルのレコードを書き、上限を超えていれば除いた後のレコードを書くことは、単体テストの fake で確認した。ブラウザでは未確認である。
  • 書き直しが失敗した後に、保存のための読み込みが失敗したとき、起動し直す前の読み込みが失敗したとき、保存しないパスだけの通知が来たときに、除いた後のレコードを書くことは、単体テストの fake で確認した。ブラウザでは未確認である。
  • 新しい保存がキューにあるときと成功した後は、上限を超えても除いた後のレコードを書かないことは、単体テストの fake で確認した。
  • 起動に失敗した実行環境を起動し直すときに除いた後のレコードを書くことと、書き直しの書き込みが終わるまでは書き直しを足さないことは、単体テストの fake で確認した。ブラウザでは未確認である。
  • 書き直しが成功したときに、訪問者の変更の保存の失敗で出た案内を残し、書き直しの失敗だけで出た案内を消すことは、単体テストの fake で確認した。ブラウザでは未確認である。
  • 形に合わないレコードを、保存したファイルが無いときと同じに扱い、書き直さないことは、単体テストの fake で確認した。
  • .envrc、.netrc、.yarnrc、.yarnrc.yml、.git-credentials を保存しないことは、単体テストで確認した。ブラウザでは未確認である。
  • 1 つのディレクトリに置いたファイルを、同時に 32 個ずつ読む時間は、950 個(合計 9.5 MB)で 0.08〜0.13 秒、5000 個(合計 5 MB)で 0.53 秒、10000 個で 0.83 秒だった。
    • 同時に読む数を絞らないと、950 個で 0.35 秒、5000 個で 5.7 秒、10000 個で 24 秒だった。
    • 1000 個と 10 MB の上限を付けると、上限を超えて読むのをやめるまでの時間は、5000 個で 0.14 秒、1 MB のファイル 200 個で 0.04 秒だった。
  • 10 個(合計 10 MB)は、読むのに 16 ms、IndexedDB に書くのに 12 ms だった。

Considered Options

保存する場所の選択肢は次のとおり。

選択肢 強み 弱み
採用: IndexedDB キャンバスとスライドのエディタと、同じ保存先になる。3 つのブラウザで動いた プロジェクトの単位で読み書きする。ファイルを 1 つずつストリームで書けない
OPFS ファイルシステムの形で持てる。大きいファイルを、ストリームで書ける Playwright の WebKit で失敗した。保存先が、キャンバスとスライドのエディタと 2 つになる

保存する単位の選択肢は次のとおり。

選択肢 強み 弱み
採用: 埋め込みの ID ごとに、1 つのプロジェクトを保存する 一覧の画面が要らない。記事を開き直すと、前回保存したファイルが開く デフォルトのプロジェクトは、1 つだけである。保存の形が、ADR-0043 とそろわない
名前を付けた複数のプロジェクトを保存し、一覧から選ぶ 新しく作り始めても、前のプロジェクトが消えない 一覧、名前の変更、削除の画面が要る。記事の埋め込みで、どれを開くかの規則が要る

Node の実行環境で保存する対象の選択肢は次のとおり。

選択肢 強み 弱み
採用: 作業ディレクトリの全体から、シークレットのファイル、ビルドの出力、.git、リンクを除き、上限を付ける シークレットを置くことが多いファイルの中身と、作業ディレクトリの外のファイルの中身を残さない。ビルドの出力が大きくても、保存のたびに読む量が増えない ほかの名前のファイルに書いたシークレットは残る。上限を超えると、それより後の変更が保存されない。監視の通知から、保存までの処理が要る。通知を受け取れないときは、一定の時間ごとに読む
作業ディレクトリの全体(node_modules を除く) 訪問者がコマンドで作ったファイルも残る シークレットとリンクの先の中身が残る。大きな出力で、保存のたびに数 MB を読み書きする
エディタとファイルのツリーで変えたファイルだけ 処理が少ない コマンドで作ったファイルと、npm install で変わった package.json が残らない

ページを開いたときに読んだレコードに、保存しないファイルがあるときの選択肢は次のとおり。

選択肢 強み 弱み
採用: 除いてから起動し、除いた後のレコードを書き直す 上限を超えている間も、シークレットのファイルが IndexedDB に残らない 訪問者が操作する前に、IndexedDB に書く
除いてから起動し、書き直さない 開いただけでは IndexedDB に書かない 次に保存するまで、シークレットのファイルが IndexedDB に残る。上限を超えている間は残り続ける
何もしない 実装を変えない 保存しないファイルの規則を入れる前に保存したシークレットのファイルが、ページを開き直しても IndexedDB に残る

書き直しが失敗したときの選択肢は次のとおり。

選択肢 強み 弱み
採用: 除いた後のレコードを持ち、書けるまで、上限を超えたとき、読み込みが失敗したとき、起動に失敗した実行環境を起動し直すとき、保存しないパスだけの変更の通知が来たときにも書く 上限を超えている間も、読み込みが失敗し続ける間も、次の変更でシークレットのファイルを消す 持つ状態が 1 つ増える
除いた後のレコードを持ち、次に保存のために読めたときだけ書く 書く時が 1 つにまとまる 読み込みが失敗し続ける間と、保存しないパスだけを変える間は、シークレットのファイルが残る
次にページを開くまで書き直さない 持つ状態が増えない 上限を超えている間は、ページを閉じるまでシークレットのファイルが残る

上限を超えたときの選択肢は次のとおり。

選択肢 強み 弱み
採用: 保存せず、前に保存したレコードを残す 訪問者のファイルを消さない 再訪のときに、超える前のファイルで開く
前に保存したレコードを消す 再訪のときに、古いファイルで開かない 訪問者のファイルが消える
上限を持たず、IndexedDB が容量の超過で失敗したら警告を出す 上限の値を決めなくてよい 保存のたびに全部を読み、読み込みの 5 秒の上限を超えると保存されない

Consequences

Positive

  • 訪問者は、記事を開き直すと、前回保存したファイルから続けられる。
  • 訪問者がコマンドで作ったファイルも残る。
  • 保存先が、キャンバスとスライドのエディタと同じ IndexedDB になる。
  • 訪問者が Decision に挙げた名前のファイルに書いたトークンは、IndexedDB に残らない。
  • 作業ディレクトリの外のファイルの中身が、リンクを通して保存されない。
  • 保存しないファイルの規則を入れる前に保存したレコードにあるシークレットのファイルも、次にページを開いて書き直しが成功すれば、IndexedDB から消える。
  • ビルドの出力と .git があっても、保存のたびに読む量が増えない。

Negative

  • 記事の iframe と、トップレベルで開いたページが、別の保存先を読むブラウザでは、片方で変えたファイルが、もう片方に出ない。
  • 1 MB を超えるファイルと、テキストとして読めないファイルは、再訪のときに無い。
  • IndexedDB を読めないときは、訪問者が変えたファイルが、ページを閉じると残らない。
  • デフォルトのプロジェクトは 1 つで、訪問者は、2 つ目のプロジェクトを別に持てない。
  • node_modules は保存しないので、再訪のたびに npm install を実行する。
  • .env の中のシークレットでない値と、.env.example も、再訪のときに無い。
  • ほかの名前のファイルに書いたシークレットは、保存される。
  • 訪問者が作ったリンク、ビルドの出力、.git は、再訪のときに無い。
  • 保存しないファイルの規則を入れる前に保存したレコードの .env は、次にページを開いたときに消える。
    • 開いていない埋め込みの ID のレコードの .env は、その ID のページを開くまで IndexedDB に残る。
    • 形に合わないレコードは、保存したファイルが無いときと同じに扱い、書き直さない。中の .env は、次に保存して上書きするまで IndexedDB に残る。
    • 書き直しが失敗した後は、書き直しか次の保存が成功するまで、.env が IndexedDB に残る。変更の通知も、起動し直す操作も無い間は、書き直さない。
    • 容量の超過で書き直しが失敗し続ける間も同じである。.env だけを消す操作は無く、訪問者が消すには、「初期の状態に戻す」でレコードの全体を消す。
    • 保存の上限を超えている間は、書き直しが失敗しても「ファイルを保存できませんでした」の案内が出ず、上限の案内だけが出る。.env が IndexedDB に残っていることは、訪問者に伝わらない。
    • 通常のファイルとして保存したリンクの中身は、保存しない名前のものだけが消える。
    • ほかの名前のものは、リンクだったかを見分けられないので残る。
  • 上限を超えている間の変更は、保存されない。
  • プロジェクトの JSON の files のキーの規則に合わない名前のファイル(__proto__ という名前のディレクトリの下のファイルなど)は、再訪のときに無い。
  • テキストとして読めないファイル、1 MB を超えるファイル、シンボリックリンクは、保存しないが、上限の 10 MB には数える。大きなバイナリのファイルか、作業ディレクトリの外の大きなファイルを指すリンクが 1 つあると、作業ディレクトリの全体が保存されない。
  • 保存しないファイルだけを持つレコードは、除くと空になる。そのときは、空の作業ディレクトリで、再訪と同じに起動する。