Remix

Cloudflare上でトランザクションが成功している(ように見える)のに、DBに反映されていない現象に遭遇した

Cloudflareで動作する、個人開発プロジェクトで、次のような問題に遭遇しました。

手元マシンの開発環境でnpm run devで実行し、ブラウザでアクセスしているときは、バックエンド処理でDBトランザクションが行われて成功し、(当然)DBも更新されるのに、いざCloudflare同じ処理を動かすと、DBトランザクションが成功している(ように見える)のに、DBが更新されていない。

まず、Cloudflareの実行時間制限にかかっているのか、と疑いましたが、エラーも全く起きていないので、これが原因ではなさそうでした。

アプリケーション上で他の箇所でもトランザクションを使っているところがあり、それらはCloudflareとローカル開発環境で動作の違いは無いのに、ある特定のリクエストだけ現象が起きていました。

ざっくりと、以下のような処理です。

const deleteById = async (db: TiDBServerlessDatabase<typeof schema>, form: FormData) => {
    const id = form.get('id')
    try {
        db.transaction(async (tx) => {
            await tx.delete(TableA).where(eq(TableA.id, id))
            await tx.delete(TableB).where(eq(TableB.id, id))
        })
    } catch (e) {
        return { status: 'fail', msg: e }
    }
    return { status: 'success', msg: '削除しました' }
}

Remixのactionとして動作しており、returnが行われると、HTTP応答がブラウザに帰ります。

上記コードの問題は何でしょうか???? 正解は↓

const deleteById = async (db: TiDBServerlessDatabase<typeof schema>, form: FormData) => {
    const id = form.get('id')
    try {
        await db.transaction(async (tx) => {
            await tx.delete(TableA).where(eq(TableA.id, id))
            await tx.delete(TableB).where(eq(TableB.id, id))
        })
    } catch (e) {
        return { status: 'fail', msg: e }
    }
    return { status: 'success', msg: '削除しました' }
}

db.transactionの前にawaitが抜けていました。それで、実際の動作順としてはreturnでHTTP応答が行われた後にトランザクションが動作していたようです。Cloudflareだと、HTTPリクエストが完了すると、直ちにworkerのインスタンスが破棄されるようで、トランザクションが終わるところまで処理が行われていなかったと思われます。手元のNode.jsだと当然そういうこともないので、awaitが抜けていてもトランザクションが完了し、DBが更新されていたわけです。

Remixでは副作用のためのモジュールインポートは避けた方が良い

引き続きRemixで開発していたところ、クラアント側で動作させたいコードで、副作用モジュールインポートを行っていると問題が起きることがわかりました:

import 'hoge'

上記のようなimportです。このimportを行う側のコードでは、hogeの中身を名前で参照していませんので、このimportは、hogeのロード時1度限りのコード実行を目的としたものです。

今回このhogeの中身では、グローバルオブジェクトに新しいプロパティを付け加える処理を行っています。

$ npm run dev

で開発しているときには、上記のimportを行うコードは正しく実行されていたのですが、

$ npm run build
$ npm run start

にて、ビルドした成果物を動かすと、hogeが全く動作していませんでした(hogeで付け加えられるべきプロパティが存在しなかったため、エラーが起きた)。

散々悩んだ挙げ句、Module Constraintsで説明されている通り、Remixで上記のような副作用を目的としたモジュールのimportは行わない方が良さそうです。

今回の目的では、hogeの実行はimport時ではなく、ページ初期化後でも構わなかったため、hogeの中身をexport functionとし、副作用モジュールインポートを、関数のインポートに置き換えました。そして、当該functionをuseEffectの中から呼び出すようにました。この方法は、Lazy Initializationで紹介されているものです。

動作確認したバージョン

  • @remix-run/cloudflare: 2.10.3
  • remix-utils: 7.6.0
  • react: 18.3.1

RemixでSSRをバイパスするにはremix-utilsを使おう

Remixleafletを使用する開発で、SSRフレームワーク定番(参考1, 参考2)の"window is not defined"がサーバ側の端末で表示される現象に遭遇しました。

要するに、ブラウザでのみ動かしたいコードがサーバ側のSSRで動いてしまっているわけです。

Next.jsだとdynamicで囲って対処していましたが、Remixでどうやってやるのか調べました。

Remix Viteで一部をクライアントサイドのみにするでは、ReactのlazyとSuspenseを組み合わせて行けるとのことでしたが、私が試した限りではうまくいきませんでした(やり方が悪かっただけかもしれません)。

結局、remix-utilsClientOnlyを使って解決しました。

動作確認したバージョン

  • @remix-run/cloudflare: 2.10.2
  • remix-utils: 7.6.0
  • react: 18.3.1

フロントエンド界隈は開発が活発なので、この記事の内容もあっという間に陳腐化するかもしれませんが、備忘録として残しておきます。