時間旅行による同期不能状態


私はオープンソースのビジュアル開発プラットフォームで働いています.
WebStudioのデザイナーのためのUIの一部として、非常にインタラクティブなインターフェイス、私はアンドゥ/リドゥ機能を必要とし、クライアント、サーバー、および将来のリリースで複数のクライアント間で同期状態のライブラリを作成しました.

問題1 -不変性


不変性はECMAScriptの解決問題であるかロングショットではない.
私たちは、抽象化を使用した初期の試みから始めましたimmutable これは相互運用性の厳しい欠点があります.それから、私たちは手動でクローン化しているオブジェクトと配列のようになりましたspread operator , これは、複雑な構造を変更することはできません.たとえば、入れ子になったオブジェクトやオブジェクトを持つ配列を使用すると、変更したいすべてのオブジェクトを移動しなければならないので、オブジェクトを変更不能にしておくために、パス内のすべてのオブジェクトをクローン化する必要があります.
Structured cloning APIは私が非常に前向きに見えますが、それは同じ問題を解決していません.
Immer 相互運用可能な方法でimmutabilityを解決する最初の人気のライブラリです.それはあなたが定期的なオブジェクトで動作するようにするので、iMmerでロックインを持っていないと何かを変異することなく、それらを与える何かを変異バニラ機能を使用することができます.私のためのImmerの最も重要な特徴は、パッチの世代です.
import produce from "immer"
const nextState = produce(draft => {
  // Mutable behavior is allowed on the draft.
  draft[1].done = true
  draft.push({title: "Tweet about it"})
})(previousState)

問題2 -サーバーにUI状態を同期させる


Immutableデータ構造体を使用する場合は、サーバーへの変更を保存する方法について、2つのオプションしかありません.

  • 任意のAPIを使用してサーバーにカスタム更新を送信します.
    これはおそらく最も一般的なアプローチで、単純な場合にはうまく動作するかもしれませんが、アプリケーションが成長するにつれてスケールアップされません.
    それは通常、サーバーに送信するデータを準備する各更新プログラムのカスタムロジックを終了するため、各更新プログラムを正しく実装に依存する必要があります.また、クライアントにサーバーの状態を反映させるようにエラーを処理する必要があります.特に、楽観的なUIを使用するのは難しいです.時間とともに、このアプローチは、潜在的エラーの大きな表面を提供する.

  • それは簡単にはできません.
    あなたがサーバーに送信されたデータが手動でこの更新プログラムの準備ができた場合、更新は、同じ更新を元に戻すカスタムコードを記述する必要がなくなります.場合によっては、特に複数のエンドポイントと話をしなければならないときには非常に複雑になり、すべてのエンドポイントも変更を取り消す方法を持っていなければなりません.
  • 問題3 -アンドゥ/リドゥのUI状態


    抽象化のようないくつかの集中的なストアがあなたのUI状態を管理しないならば、あなたが特定の州のすべての表現が更新されていることを確認しなければならないので、それを取り消すことは問題になります.
    それに加えて、複数の状態の型を区別する必要があります.
  • データの状態-本質的に、どのようなサーバーは、あなたのアプリについて知っているとundoする必要があります
  • UI state -サーバに反映されていないUIの現在の状態
  • 一時的な状態-一時的にUIに反映されて、選択できない要素、例えばundoableであるべきでない状態
  • 状態を管理するための抽象化のようなストアを使用すると、手動で状態とアクションがどのような状態になっているかを定義する必要があります.
    多くのエッジケースがあり、大規模なアプリケーションでの管理は些細ではない.

    問題4 -サーバーに最小限の更新を送信する


    それは超簡単にスナップショットを使用してサーバーを更新するには、永久に保存するすべてを反映しています.これは、サーバーとクライアントのデータが一貫していることを確認し、それは超簡単に時間をかけて、この方法を維持することです.
    下方
  • あなたは潜在的に頻繁に同期することは不可能に、サーバーに大量のペイロードを送信する必要があります
  • 2つのクライアントが同じ状態を更新することができますので、簡単に協力的な機能を構築することはできません、スナップショットを使用して紛争解決作業を行う方法を理解する必要があります.我々はすでに誰が何を変更したかについての情報を失った.
  • こうすることで、2つの別々のタブを開くことによっても、競合にすばやく実行することができます.また、それらのいずれかが状態へのアクセスを必要とする場合、彼らは彼らに全体の状態を送信することなく簡単に同期することはできませんので、あなたがiFrameやWebワーカーを使用する能力に制限されています.これは、Webワーカーを使用するすべての潜在的な利点を否定するので、実際のブレーカです.

    私がWebStudioのために構築した解決


    WebStudio UIを構築しながら、上記のすべての問題は私にとって問題でした.私は、ネットワークの上に送られる変化の最小限の量を必要とします.私は簡単に変更/変更を元に戻す方法が必要です.私は、iframeの中で変化を反映する能力を必要とします.私は、協調的な編集経験のためにタブとウインドウの向こう側の変化を反映する必要があります.
    紹介しますImmerhin .
    iMinhinは、基本的にすべての状態管理(小売店などのような)を使用することができますマーマーの上に非常に薄い抽象化されます.私は個人的にこの小さな図書館を使っていますreact nano state それで.
    iMinhinは、複数の状態を更新することが単一のユーザーアクションを識別するトランザクションを宣言することができます.immerのおかげで、それはあなたがimmutabilityを失うことなく、すべてのそれらの状態を変異することができます.用途patches すべての消費者に変更の最小限の量を送信します.
    iMinhinの現在の開発状態は戦闘テストではなく、まだ多くの機能を欠いているので、将来のリリースや非生産プロジェクトの使用に従うプロジェクトとして考えてください.あなたはこれに更新のためのrepoまたはアカウントに従うことができます.
    ここに小さなCodesandbox demo それはどのように2つの別々のリストの間の共有状態を持っていて、アンドゥ/リドゥのために管理する余分のロジックなしでアンドゥ/リドゥ機能を持っているかを示します.
    以下は、イマジンの使い方です.

    コンテナの作成


    コンテナはオブジェクトです.value 現在の値と.dispatch(nextValue) これは、すべてのコンテナの加入者を更新します.イマジンはそれらの2つのプロパティについてのみ注意します.どのように残りを行うにはあなた次第です.
    反応ナノ状態の使用例
    import {createContainer} from 'react-nano-state';
    const container1 = createContainer(initialValue);
    const container2 = createContainer(initialValue);
    

    トランザクションを作成する


    トランザクションは、どのコンテナを更新し、どのパッチを適用するかを知ることができます.これらは自動アンドゥと同期を実装するために使用されます.
    import store from 'immerhin';
    
    // - generate patches
    // - update states
    // - inform all subscribers
    // - register a transaction for potential undo/redo and sync calls
    store.createTransaction(
      [container1, container2, ...rest],
      (value1, value2, ...rest) => {
        mutateValue(value1);
        mutateValue(value2);
        // ...
      }
    );
    

    変更の送信


    それはあなたにどのように変更を送信する次第です.イマジンはsync() すべての蓄積された変更をこれまでに与えて、スタックをクリアする機能.
    // Setup periodic sync with a fetch, or do this with Websocket
    setInterval(async () => {
      const entries = sync();
      await fetch("/patch", { method: "POST", payload: JSON.stringify(entries) });
    }, 1000);
    

    元に戻す/変更をやり直し


    以来iMinhinトランザクションからすべてを知って、あなただけのコールする必要がありますstore.undo() or store.redo() Immerからの修正パッチを使用してこれらのコンテナを更新します.

    イマジンとダウンサイド


    任意のソリューションと同様に、トレードオフがあります.ここでは、私はこれまでに来ることができるいくつかです:
  • 潜在的に、消費者のうちの1人がパッチを受けないならば、彼らの州は同期しなくなっています.現在、Imphhinはこのあたりで働く方法を提供しません.あなたが素晴らしいアイデアを持っている場合は、それらを共有してくださいissues . 何かが不足していて、クライアントが再フェッチしなければならないなら、各々のトランザクションの上の順序IDはエラーを投げるのに潜在的に使用されるかもしれません.
  • immerパッチはパスを含んでいますが、パスは本来のオブジェクトに対して本質的です.矛盾がなければ、経路は間違っているでしょう.潜在的に、我々は経路の代わりにIDSを使用できました.また、以前の問題が解決されている場合は、これは大きな契約ではないので、すぐに更新プログラムが同期しているので、拒否することができます.
  • Immerはパッチ仕様JSON patch に違いがあるpath 配列で、文字列ではない.
  • イマジンの将来


    が不足している機能の束私は本当に将来的に実装し、WebStudioが必要です.
  • 紛争解決
  • iframeとタブの間の同期メカニズム
  • ボタンをクリックしてくださいwebstudio.is , イマジンを使用するデザイナーインターフェイスが表示されます.UIは非常にアルファですが、私はあなたが既にアンドゥ/リドゥ機能で明らかなバグを見つける場合好奇心旺盛です.