この記事は、Elixir Advent Calendar 2023 シリーズ7の10日目です。
今年話題になったChatGPT。
ChatGPTクローンアプリをElixirで実装しようという動機がありました。
実装時にちょうどよい参考記事を見つけたので、数回に分けてマイペースに紹介&解説します。
概要
本記事では、コードと共にLiveview StreamsとPubSubを解説します。
前回はこちら。
本記事で解説するコードは下記です。
準備
リポジトリをクローン
git clone https://github.com/SophieDeBenedetto/stream_chat.git
依存関係のインストール、マイグレーション、初期データの登録
cd stream_chat && mix setup
デフォルトで mix setup
は下記のようになっています
defp aliases do
[
setup: ["deps.get", "ecto.setup"],
"ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
"ecto.reset": ["ecto.drop", "ecto.setup"],
test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"],
"assets.deploy": ["tailwind default --minify", "esbuild default --minify", "phx.digest"]
]
end
コードだけ見る場合
GitHubのページを開いてキーボードで.
を押してブラウザでVS Codeを開きます。
Liveview Stream
socketにチャットメッセージリストを保存
チャットアプリでは、チャットメッセージが大量に生まれるのでstream
に保存します。
とは言っても、assign
の代わりに使うだけです。
def assign_active_room_messages(socket) do
messages = Chat.last_ten_messages_for(socket.assigns.room.id)
socket
|> stream(:messages, messages)
|> assign(:oldest_message_id, List.first(messages).id)
end
チャットメッセージを追加
このチャットアプリでは、下記のような流れでチャットメッセージを保存&ページに表示されています。
- メッセージをDBに保存
- DB保存後にPubSubで通知
- 通知を受け取ってsocketのstreamに追加
順番に解説します。
メッセージをDB保存
localhost/rooms
を開いてページ下部のフォームからメッセージを送信すると、handle_event("save", _, socket)
関数が呼び出されます。
ここでは、DBにデータを保存しているだけなのでまだページ中央のメッセージリストには反映されません。
def handle_event("save", %{"message" => %{"content" => content}}, socket) do
Chat.create_message(%{
content: content,
room_id: socket.assigns.room_id,
sender_id: socket.assigns.sender_id
})
{:noreply, assign_changeset(socket)}
end
DB保存後にPubSubで通知
PubSubの通知を送信する側はChat.create_message/1
に実装されています。
room:#{message.room_id}
の登録者にイベントnew_message
、データ%{message: message}
を送信します。
def create_message(attrs \\ %{}) do
%Message{}
|> Message.changeset(attrs)
|> Repo.insert()
|> publish_message_created()
end
def publish_message_created({:ok, message} = result) do
Endpoint.broadcast("room:#{message.room_id}", "new_message", %{message: message}) # ここ
result
end
通知を受け取ってsocketのstreamに追加
Pub Subの受信設定は、チャットルームを開いたときにEndpoint.subscribe("room:#{id}")
で行われています。
def handle_params(%{"id" => id}, _uri, %{assigns: %{live_action: :show}} = socket) do
if connected?(socket), do: Endpoint.subscribe("room:#{id}") # ここ
{:noreply,
socket
|> assign_active_room(id)
|> assign_active_room_messages()
|> assign_last_user_message()}
end
受信後の処理は、handle_info
で実装されています。
def handle_info(%{event: "new_message", payload: %{message: message}}, socket) do
{:noreply,
socket
|> insert_new_message(message)
|> assign_last_user_message(message)}
end
assign
とは違って、専用の追加関数が用意されています。
stream_insert
を使ってメッセージを追加します。1
def insert_new_message(socket, message) do
socket
|> stream_insert(:messages, Chat.preload_message_sender(message))
end
このチャットアプリでは、 slackやDiscordのようにスクロールでメッセージを追加読み込みするところまで実装されています。
参考にしたリポジトリでは、Liveview 0.18時点の実装であり、Liveview v0.19で追加されたスクロールに関する機能追加は取り込まれていませんので、本記事では解説しません m(_ _)m
終わりに
コードの解説をしました。
次回はスクロールによる追加読み込みを実装します。
-
Chat.preload_message_sender
は関連テーブルとして送信者のデータを取得しているだけです。 ↩