Dialupの状態管理は sessionassignsparams の3つに分かれている。

3種類の状態

フィールド設定者ライフサイクル
sessionlayout.mount/1WebSocketセッション全体で持続
assignspage.mount/2ナビゲーションごとにリセット
paramsフレームワーク自動ナビゲーションごとに更新

session

layout.mount/1 が設定する。ページ遷移をまたいで持続するため、ログインユーザー情報などを格納する。

defmodule Dialup.App.Layout do
  use Dialup.Layout

  def mount(session) do
    user = Repo.get(User, get_user_id_from_cookie())
    {:ok, Map.put(session, :current_user, user)}
  end
end

レイアウトがネストしている場合、親レイアウトの結果が子レイアウトの mount/1 に渡される。

assigns

page.mount/2 が設定する。ナビゲーションのたびにリセットされ、現在のページ固有の状態を保持する。

defmodule Dialup.App.Users.Id.Page do
  use Dialup.Page

  def mount(%{"id" => id}, assigns) do
    # assigns には session の内容が入っている(current_user 等を参照可能)
    post = Posts.get!(id, assigns.current_user)
    {:ok, %{post: post}}  # 返り値が page assigns になる
  end
end

mount/2 の返り値はそのまま page assigns になる(session キーを含める必要はない)。

params

URLパラメータ(パスパラメータとクエリパラメータの両方)をフレームワークが自動的に設定する。

# /users/123?tab=posts にアクセスした場合
def mount(params, assigns) do
  id  = params["id"]   # "123"(パスパラメータ)
  tab = params["tab"]  # "posts"(クエリパラメータ)
  ...
end

テンプレートでのアクセス

render 直前に session + assigns + params がマージされるため、テンプレートではすべて @ でアクセスできる。どのフィールド由来かを意識する必要はない。

<p>{@current_user.name}</p>  <!-- session 由来 -->
<h1>{@post.title}</h1>       <!-- assigns 由来 -->
<p>ページ: {@params["page"]}</p>  <!-- params -->

handle_event での状態更新

handle_event/3 には session + assigns + params がマージされた状態が渡される。返り値も同じ形式で返す。

def handle_event("follow", _, assigns) do
  # assigns には current_user(session由来)も post(assigns由来)も含まれる
  new_post = Posts.follow(assigns.post, assigns.current_user)
  {:update, Map.put(assigns, :post, new_post)}
end

フレームワークが session_keys を使って返り値を session と assigns に自動分割するため、開発者は意識不要。

session を更新する

handle_event の返り値に session キーを含めれば session も更新できる。

def handle_event("switch_locale", locale, assigns) do
  {:update, Map.put(assigns, :locale, locale)}
  # :locale が session_keys に含まれていれば session が更新される
end

ナビゲーション時の挙動

/users/123  /users/456 に遷移

session:  変化なしcurrent_user 等を保持
assigns:  リセット  page.mount/2 が再実行される
params:   新しいURLのパラメータに更新

session をリセットしたい場合は、handle_eventlayout.mount 内で明示的に行う。

状態の揮発性

assigns・session はメモリ上に保持される。以下の場合に失われる:

  • WebSocket切断後、タイムアウト(デフォルト5分)経過
  • サーバー再起動
  • プロセスクラッシュ

永続化が必要なデータはDBに保存すること。

セッション永続化(ETS ストア)

プロセスがタイムアウトで停止する際にセッション状態を ETS に保存し、再接続時に復元するオプション。use Dialupsession_store: :ets を指定する。

defmodule MyApp do
  use Application

  use Dialup,
    app_dir: __DIR__ <> "/app",
    title: "My App",
    session_store: :ets  # デフォルトは :memory(永続化なし)

  @impl Application
  def start(_type, _args) do
    children = [{Dialup, app: __MODULE__, port: 4000}]
    Supervisor.start_link(children, strategy: :one_for_one)
  end
end

:ets を指定すると:

  • プロセスタイムアウト時に session / assigns / path が ETS テーブルに保存される
  • 同じ session_id で再接続すると、保存された状態から復元される
  • 復元後、ETS のエントリは自動的に削除される

ETS はノード再起動で消えるため、完全な永続化が必要な場合は DB を使用すること。

設計指針

session に入れるもの: ページをまたいで共有したい状態(ログインユーザー、言語設定など)

assigns に入れるもの: 現在のページ固有の状態(表示中のデータ、フォーム入力、UIフラグ)

注意: session と assigns で同じキー名を使うと session が優先される。命名の衝突を避けること。