Open5

Immer over WebSocket で Immutable な状態をリアルタイム同期する

やまゆやまゆ

https://immerjs.github.io/immer/

Immerはイミュータブルなデータ構造の取り扱いを簡素化します。

Immerは、イミュータブルなデータ構造を使用する必要があるあらゆるコンテキストで使用することができます。例えば、Reactの状態、ReactやReduxのリデューサ、構成管理などと組み合わせて使うことができます。イミュータブルなデータ構造は、(効率的な)変更検出を可能にします。オブジェクトへの参照が変更されていなければ、オブジェクト自体も変更されていないことになります。さらに、クローンを比較的安価に作成することができる。データツリーの変更されていない部分はコピーする必要がなく、同じ状態の古いバージョンとメモリ上で共有されます。

一般に、オブジェクトや配列、マップのプロパティを決して変更せず、常に変更されたコピーを作成することで、これらの利点を実現することができます。しかし実際には、このような制約を設けると、コードが非常に煩雑になり、また誤って制約を破ってしまうこともあります。Immerは、これらのペインポイントに対処することで、イミュータブル・データ・パラダイムに従うことを支援します。

  1. Immerは偶発的な変異を検出し、エラーを投げます。
  2. Immerは、不変オブジェクトに対する深い更新を作成する際に必要となる典型的な定型的なコードを不要にします。Immerがなければ、オブジェクトのコピーはすべてのレベルで手作業で作成する必要があります。Immerがなければ、オブジェクトのコピーはすべてのレベルで手作業で行う必要があります。通常、多くの ... スプレッド操作を使用します。Immerを使用する場合、変更はドラフトオブジェクトに行われます。ドラフトオブジェクトは変更を記録し、元のオブジェクトに影響を与えることなく、必要なコピーを作成することを請け負います。
  3. Immer を使用する場合、このパラダイムの恩恵を受けるために、専用の API やデータ構造を学ぶ必要はありません。Immer を使えば、普通の JavaScript データ構造を使い、よく知られた mutable JavaScript API を安全に使用することができます。

www.DeepL.com/Translator(無料版)で翻訳しました。

やまゆやまゆ
const baseState = [
    {
        title: "Learn TypeScript",
        done: true,
    },
    {
        title: "Try Immer",
        done: false,
    },
];

// この状態に「要素1の done を true にする」「要素2を追加する」を実施したい。

Without Immer

const nextState = baseState.slice() // array を shallow クローン
nextState[1] = {
    ...nextState[1], // 要素1 を shallow クローン
    done: true // 更新内容を結合
}
// nextStateは新しくクローンされたので、pushを使用しても安全です。
// しかし,将来の任意の時期に同じことをすると
// 不変原則に反し、バグが発生します。
nextState.push({title: "Tweet about it"})

With Immer

import produce from "immer"

const nextState = produce(baseState, draft => {
    draft[1].done = true
    draft.push({title: "Tweet about it"})
});

安全に状態をコピーし、追記することが出来る。

TypeScript で Readonly な状態でも、 draft 状態では上書きすることが可能。

現在の状態、次の状態は immutableで、 produce 関数内の draft 変数だけ編集可能になる。ここで更新処理を行えば、安全に状態を次の状態に移行できる。

やまゆやまゆ

今回はこの Immer ライブラリを利用し、 3D シーン状態を WebSocket でリアルタイム同期するシステムを構築してみる。

※ WebSocket なのは現状のサーバ実装都合なだけなので、 WebTransport がうまく入ってきたら乗り換えられるよう Transport 層として実装を分ける。