ADR-0077: 訪問者のファイルを、埋め込みの ID ごとに IndexedDB に保存し、Node の実行環境ではシークレットのファイル、ビルドの出力、.git、リンクを除き、上限を付ける¶
Status¶
承認
Context¶
訪問者が実行環境で変えたファイルを、再訪のときに開くための、保存する場所と形を決める必要がある。
Node の実行環境で、訪問者の作業ディレクトリのうち、どのファイルを IndexedDB に保存するかも決める必要がある。
- 訪問者が作ったファイルは、訪問者のブラウザに保存する。プレイグラウンドの中では、ツールの間で origin を分けない。
- キャンバスとスライドのエディタは、名前を付けた複数の図とスライドを、IndexedDB に保存する。Node の実行環境は、この決定の対象に入っていない。
- 1 つの埋め込みが持つファイルの一式と設定を、プロジェクトと呼ぶ。埋め込みの ID は、記事の
contentIdと、埋め込みの名前の組である。 - 記事に埋め込むページと、トップレベルで開くページは、同じ URL である。
- ブラウザは、iframe の中のページの保存先を、トップレベルのサイトと、iframe の origin の組で分ける。
fukuemon.devとplay.fukuemon.devは、同じサイトである。 - Node の実行環境は WebContainers で動かし、作業ディレクトリのファイルの一覧と中身は、WebContainers の
fsで読む。 - 訪問者のシェルは制限が無く、訪問者は作業ディレクトリに、どの名前のファイルでも、シンボリックリンクでも作れる。「実行環境を起動し直す」の操作では、破棄する前の実行環境から読んだ、作業ディレクトリのファイルで起動する。
- 実行環境が追加するファイルは、作業ディレクトリの外(
/tmp/fukuemon-playground/)に置く。 - 保存する対象を、作業ディレクトリの全体(
node_modulesを除く)にすると、訪問者が.envや.npmrcに書いたトークンが、平文で IndexedDB に残る。同じ origin のほかのツールのページも、その IndexedDB を読める。 - 同じ対象では、起動のコマンドがビルドの出力(
dist/、.next/、.cache/)を書くか、訪問者が.gitのディレクトリを作ると、ツールのページが、保存のたびにその全てを読んでレコードに入れる。 - 保存のために作業ディレクトリを読む処理には、5 秒の上限がある。
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 では測っていない。
- この項目だけは、Playwright 1.64.1 の Chromium(headless)で、Apple M2 の
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)の中身を保存した。
- WebContainers の
.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 に残っていることは、訪問者に伝わらない。 - 通常のファイルとして保存したリンクの中身は、保存しない名前のものだけが消える。
- ほかの名前のものは、リンクだったかを見分けられないので残る。
- 開いていない埋め込みの ID のレコードの
- 上限を超えている間の変更は、保存されない。
- プロジェクトの JSON の
filesのキーの規則に合わない名前のファイル(__proto__という名前のディレクトリの下のファイルなど)は、再訪のときに無い。 - テキストとして読めないファイル、1 MB を超えるファイル、シンボリックリンクは、保存しないが、上限の 10 MB には数える。大きなバイナリのファイルか、作業ディレクトリの外の大きなファイルを指すリンクが 1 つあると、作業ディレクトリの全体が保存されない。
- 保存しないファイルだけを持つレコードは、除くと空になる。そのときは、空の作業ディレクトリで、再訪と同じに起動する。