From d15613732acdd3c5ddc5dd11c7533f26c5d6a71a Mon Sep 17 00:00:00 2001 From: Joe Bew Date: Wed, 1 Jul 2026 08:31:53 +0200 Subject: [PATCH 1/9] docs: feature specification --- .feature-spec.md | 125 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 .feature-spec.md diff --git a/.feature-spec.md b/.feature-spec.md new file mode 100644 index 0000000..e1ff325 --- /dev/null +++ b/.feature-spec.md @@ -0,0 +1,125 @@ +# Feature Specification: Rename `session_id` / `user_session_id` to `user_id` + +GitHub Issue: https://github.com/joebew42/ex_chat/#12 + +## Summary + +Rename the identifier used for a user throughout the backend from +`session_id` / `session_ids` / `user_session` (when it holds the id value) +to `user_id` / `user_ids` for clarity and consistency. + +Several use cases (`SubscribeToUserSession`, `JoinChatRoom`, +`SendMessageToChatRoom`) already name this parameter `user_id`. The rest of +the codebase still uses `session_id`, `session_ids`, or `user_session`. This +change makes the naming consistent end-to-end. + +## Motivation + +The value flowing through the system as `session_id` is actually the user's +identifier (the `username` established in `CreateUser`). Naming it `session_id` +is misleading; `user_id` states what it is. + +## Scope + +This is a **purely internal refactor** of the Elixir backend. It is +behaviour-preserving — no functional changes. + +### In scope — rename the identifier + +Rename the *identifier variables, parameters, struct fields, and helper +function names* that denote the user identifier: + +- `session_id` → `user_id` +- `session_ids` → `user_ids` +- `user_session` (only where it holds the id value) → `user_id` + +Files affected: + +- `lib/ex_chat/web/websocket_controller.ex` — `session_id` and `user_session` + state variable → `user_id` +- `lib/ex_chat/user_sessions.ex` — `session_id` params/variables → `user_id` + (including the `UserSessionRegistry` key variable) +- `lib/ex_chat/chat_room.ex` — struct field `session_ids` → `user_ids`; + `session_id` params → `user_id`; helper `add_session_id` → `add_user_id`; + `joined?/2` params +- `lib/ex_chat/chat_rooms.ex` — `session_id` params/variables → `user_id` +- `lib/ex_chat/access_token_repository.ex` — `user_session` params/variables + → `user_id` +- `lib/ex_chat/use_cases/validate_access_token.ex` — `user_session` variable + → `user_id` +- `test/ex_chat/chat_room_test.exs` — `_user_session_id` → `_user_id` +- `test/ex_chat/user_sessions_test.exs` — cosmetic test fixture strings/vars + (e.g. `"a-session-id"`) → user-id equivalents for consistency +- `test/ex_chat/access_token_repository_test.exs` — align variable names +- `test/ex_chat/use_cases/validate_access_token_test.exs` — align variable names +- `README.md` — update the roadmap/notes references to reflect the new name + +### Out of scope — keep as-is + +- Module / process / registry names that name the *session process + abstraction* rather than the id: `ExChat.UserSession`, + `ExChat.UserSessions`, `UserSessionRegistry`, and the use case + `ExChat.UseCases.SubscribeToUserSession`. A "user session" is the actor + that holds a user's subscribed client PIDs; it is looked up *by* `user_id`. +- The public repository function name `find_user_session_by/1` and its + `:find_user_session_by` / `:add` message atoms — internal param variables + are renamed, but the function/message names are left untouched to keep the + change focused on the identifier. + +## Boundary / compatibility notes + +- The WebSocket wire protocol (JSON) does **not** contain a `session_id` + key. Client→server payloads use `command` / `room` / `message`; + server→client payloads use `from` / `room` / `message` / `error` / + `success`. The identifier is only ever carried in the `from` field (whose + key name is unchanged). Therefore **no frontend / `priv/static/chat.html` + changes are required** and there is no backward-compatibility concern. + +## Acceptance Criteria + +- No remaining references to `session_id`, `session_ids`, or + `user_session_id` as identifier names in `lib/` and `test/` (except the + intentionally out-of-scope module/function names listed above). +- The identifier is consistently named `user_id` / `user_ids` across + controllers, supervisors, GenServers, use cases, and tests. +- `mix test` passes with no failures. +- No behavioural change: chat create/join/send and user-session + subscription work exactly as before. + +## Implementation Notes + +- Behaviour-preserving rename only; no logic changes. +- Commit organization (inside-out): internal modules first + (`chat_room`, `user_sessions`, `access_token_repository`), then the + supervisor wrapper (`chat_rooms`), then use cases, then the + `websocket_controller`, with test-only changes grouped with the module + they cover; docs (`README.md`) last. + +## Progress + +Implemented (inside-out), each committed with its tests, full suite green after +every step (45 tests, 0 failures): + +- [x] `chat_room` — struct field `session_ids` → `user_ids`, `session_id` params + → `user_id`, helper `add_session_id` → `add_user_id`, `joined?/2` params; + test mock var `_user_session_id` → `_user_id` +- [x] `user_sessions` — `session_id` params/variables → `user_id` (incl. + `UserSessionRegistry` key var); test fixtures `"a-session-id"` → `"a-user-id"` +- [x] `access_token_repository` — `user_session` params/variables → `user_id`; + test fixture aligned to `"a-user-id"` +- [x] `chat_rooms` — `session_id` params/variables → `user_id` +- [x] `validate_access_token` — `user_session` variable → `user_id`; test fixture + aligned to `"a user id"` +- [x] `websocket_controller` — `session_id` and `user_session` state var → `user_id` +- [x] `README.md` — roadmap/notes references updated to the new name + +Key decisions: + +- Out-of-scope names left untouched as specified: `ExChat.UserSession(s)`, + `UserSessionRegistry`, `SubscribeToUserSession`, and the repository function + name `find_user_session_by/1` with its `:find_user_session_by` / `:add` message + atoms. Only the identifier-carrying params/variables/fields were renamed. +- The `:session_not_exists` error atom in `user_sessions` was kept as-is — it + names the *session process* abstraction, not the identifier. + +Remaining: none — feature complete. From cb1db12da916ca4c703f2adb62308e714cb2d896 Mon Sep 17 00:00:00 2001 From: Joe Bew Date: Wed, 1 Jul 2026 08:34:51 +0200 Subject: [PATCH 2/9] refactor: rename session_id to user_id in chat_room --- lib/ex_chat/chat_room.ex | 26 +++++++++++++------------- test/ex_chat/chat_room_test.exs | 2 +- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/lib/ex_chat/chat_room.ex b/lib/ex_chat/chat_room.ex index bde6f6e..8762202 100644 --- a/lib/ex_chat/chat_room.ex +++ b/lib/ex_chat/chat_room.ex @@ -3,7 +3,7 @@ defmodule ExChat.ChatRoom do alias ExChat.UserSessions - defstruct session_ids: [], name: nil + defstruct user_ids: [], name: nil def create(name = {:via, Registry, {_registry_name, chatroom_name}}) do GenServer.start_link(__MODULE__, %__MODULE__{name: chatroom_name}, name: name) @@ -18,31 +18,31 @@ defmodule ExChat.ChatRoom do {:ok, state} end - def join(pid, session_id) do - GenServer.call(pid, {:join, session_id}) + def join(pid, user_id) do + GenServer.call(pid, {:join, user_id}) end - def send(pid, message, [as: session_id]) do - :ok = GenServer.call(pid, {:send, message, :as, session_id}) + def send(pid, message, [as: user_id]) do + :ok = GenServer.call(pid, {:send, message, :as, user_id}) end - def handle_call({:join, session_id}, _from, state) do - {message, new_state} = case joined?(state.session_ids, session_id) do + def handle_call({:join, user_id}, _from, state) do + {message, new_state} = case joined?(state.user_ids, user_id) do true -> {{:error, :already_joined}, state} - false -> {:ok, add_session_id(state, session_id)} + false -> {:ok, add_user_id(state, user_id)} end {:reply, message, new_state} end - def handle_call({:send, message, :as, session_id}, _from, state = %__MODULE__{name: name}) do - Enum.each(state.session_ids, &UserSessions.notify(%{from: session_id, room: name, message: message}, to: &1)) + def handle_call({:send, message, :as, user_id}, _from, state = %__MODULE__{name: name}) do + Enum.each(state.user_ids, &UserSessions.notify(%{from: user_id, room: name, message: message}, to: &1)) {:reply, :ok, state} end - defp joined?(session_ids, session_id), do: Enum.member?(session_ids, session_id) + defp joined?(user_ids, user_id), do: Enum.member?(user_ids, user_id) - defp add_session_id(state = %__MODULE__{session_ids: session_ids}, session_id) do - %__MODULE__{state | session_ids: [session_id|session_ids]} + defp add_user_id(state = %__MODULE__{user_ids: user_ids}, user_id) do + %__MODULE__{state | user_ids: [user_id|user_ids]} end end diff --git a/test/ex_chat/chat_room_test.exs b/test/ex_chat/chat_room_test.exs index 5a4354c..92a9fb4 100644 --- a/test/ex_chat/chat_room_test.exs +++ b/test/ex_chat/chat_room_test.exs @@ -10,7 +10,7 @@ defmodule ExChat.ChatRoomTest do {:ok, chatroom} = ChatRoom.create("room_name") ChatRoom.join(chatroom, "a-user-session-id") - with_mock UserSessions, [notify: fn(_message, [to: _user_session_id]) -> :ok end] do + with_mock UserSessions, [notify: fn(_message, [to: _user_id]) -> :ok end] do expected_message = %{ from: "another-user-session-id", room: "room_name", From 39d4be306f629403acf6346a7d6059ff97c27947 Mon Sep 17 00:00:00 2001 From: Joe Bew Date: Wed, 1 Jul 2026 08:35:17 +0200 Subject: [PATCH 3/9] refactor: rename session_id to user_id in user_sessions --- lib/ex_chat/user_sessions.ex | 22 +++++++++++----------- test/ex_chat/user_sessions_test.exs | 28 ++++++++++++++-------------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/lib/ex_chat/user_sessions.ex b/lib/ex_chat/user_sessions.ex index 4fbc6a3..5c9e583 100644 --- a/lib/ex_chat/user_sessions.ex +++ b/lib/ex_chat/user_sessions.ex @@ -7,25 +7,25 @@ defmodule ExChat.UserSessions do # Client API # ############## - def create(session_id) do - case find(session_id) do + def create(user_id) do + case find(user_id) do nil -> - start(session_id) + start(user_id) :ok _pid -> {:error, :already_exists} end end - def subscribe(client_pid, [to: session_id]) do - case find(session_id) do + def subscribe(client_pid, [to: user_id]) do + case find(user_id) do nil -> {:error, :session_not_exists} pid -> UserSession.subscribe(pid, client_pid) end end - def notify(message, [to: session_id]) do - case find(session_id) do + def notify(message, [to: user_id]) do + case find(user_id) do nil -> {:error, :session_not_exists} pid -> UserSession.notify(pid, message) end @@ -43,14 +43,14 @@ defmodule ExChat.UserSessions do DynamicSupervisor.init(strategy: :one_for_one) end - defp start(session_id) do - name = {:via, Registry, {UserSessionRegistry, session_id}} + defp start(user_id) do + name = {:via, Registry, {UserSessionRegistry, user_id}} DynamicSupervisor.start_child(__MODULE__, {UserSession ,name}) end - defp find(session_id) do - case Registry.lookup(UserSessionRegistry, session_id) do + defp find(user_id) do + case Registry.lookup(UserSessionRegistry, user_id) do [] -> nil [{pid, nil}] -> pid end diff --git a/test/ex_chat/user_sessions_test.exs b/test/ex_chat/user_sessions_test.exs index 3e9b493..46d25e9 100644 --- a/test/ex_chat/user_sessions_test.exs +++ b/test/ex_chat/user_sessions_test.exs @@ -11,15 +11,15 @@ defmodule ExChat.UserSessionsTest do describe "when create a UserSession" do test "an error is received when the session already exist" do - UserSessions.create("a-session-id") + UserSessions.create("a-user-id") - result = UserSessions.create("a-session-id") + result = UserSessions.create("a-user-id") assert result == {:error, :already_exists} end test "an ok is received" do - result = UserSessions.create("a-session-id") + result = UserSessions.create("a-user-id") assert result == :ok end @@ -27,15 +27,15 @@ defmodule ExChat.UserSessionsTest do describe "when subscribe to a UserSession" do test "an error is received when the session does not exist" do - result = UserSessions.subscribe(self(), to: "a-session-id") + result = UserSessions.subscribe(self(), to: "a-user-id") assert result == {:error, :session_not_exists} end test "an ok is received when the session exists" do - UserSessions.create("a-session-id") + UserSessions.create("a-user-id") - result = UserSessions.subscribe(self(), to: "a-session-id") + result = UserSessions.subscribe(self(), to: "a-user-id") assert result == :ok end @@ -43,15 +43,15 @@ defmodule ExChat.UserSessionsTest do describe "when send a message to a UserSession" do test "an error is received when the session does not exist" do - result = UserSessions.notify("a message", to: "a-session-id") + result = UserSessions.notify("a message", to: "a-user-id") assert result == {:error, :session_not_exists} end test "a message is correctly delivered" do - UserSessions.create("a-session-id") + UserSessions.create("a-user-id") - result = UserSessions.notify("a message", to: "a-session-id") + result = UserSessions.notify("a message", to: "a-user-id") assert result == :ok end @@ -59,10 +59,10 @@ defmodule ExChat.UserSessionsTest do describe "when subscribed to a UserSession" do test "messages received are forwarded to subscribers" do - UserSessions.create("a-session-id") - UserSessions.subscribe(self(), to: "a-session-id") + UserSessions.create("a-user-id") + UserSessions.subscribe(self(), to: "a-user-id") - UserSessions.notify("a message", to: "a-session-id") + UserSessions.notify("a message", to: "a-user-id") assert_receive "a message" end @@ -70,9 +70,9 @@ defmodule ExChat.UserSessionsTest do describe "when not subscribed to a UserSession" do test "messages received are not forwarded" do - UserSessions.create("a-session-id") + UserSessions.create("a-user-id") - UserSessions.notify("a message", to: "a-session-id") + UserSessions.notify("a message", to: "a-user-id") refute_receive "a message" end From 7f33bf9430dc76e513f23c753e64e5832e9d14a3 Mon Sep 17 00:00:00 2001 From: Joe Bew Date: Wed, 1 Jul 2026 08:35:42 +0200 Subject: [PATCH 4/9] refactor: rename user_session to user_id in access_token_repository --- lib/ex_chat/access_token_repository.ex | 8 ++++---- test/ex_chat/access_token_repository_test.exs | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/ex_chat/access_token_repository.ex b/lib/ex_chat/access_token_repository.ex index 0f33229..f8f477d 100644 --- a/lib/ex_chat/access_token_repository.ex +++ b/lib/ex_chat/access_token_repository.ex @@ -5,8 +5,8 @@ defmodule ExChat.AccessTokenRepository do # Client API # ############## - def add(access_token, user_session) do - :ok = GenServer.call(:access_token_repository, {:add, access_token, user_session}) + def add(access_token, user_id) do + :ok = GenServer.call(:access_token_repository, {:add, access_token, user_id}) end def find_user_session_by(access_token) do @@ -25,8 +25,8 @@ defmodule ExChat.AccessTokenRepository do {:ok, %{}} end - def handle_call({:add, access_token, user_session}, _from, state) do - {:reply, :ok, Map.put(state, access_token, user_session)} + def handle_call({:add, access_token, user_id}, _from, state) do + {:reply, :ok, Map.put(state, access_token, user_id)} end def handle_call({:find_user_session_by, access_token}, _from, state) do diff --git a/test/ex_chat/access_token_repository_test.exs b/test/ex_chat/access_token_repository_test.exs index afae262..3b6df14 100644 --- a/test/ex_chat/access_token_repository_test.exs +++ b/test/ex_chat/access_token_repository_test.exs @@ -13,8 +13,8 @@ defmodule ExChat.AccessTokenRepositoryTest do end test "return the user session associated to the token" do - AccessTokenRepository.add("a-token", "a-user-session") + AccessTokenRepository.add("a-token", "a-user-id") - assert AccessTokenRepository.find_user_session_by("a-token") == "a-user-session" + assert AccessTokenRepository.find_user_session_by("a-token") == "a-user-id" end end \ No newline at end of file From 9bcf27bb8f680ac9e03b4f67cd321dc471ce6f72 Mon Sep 17 00:00:00 2001 From: Joe Bew Date: Wed, 1 Jul 2026 08:35:54 +0200 Subject: [PATCH 5/9] refactor: rename session_id to user_id in chat_rooms --- lib/ex_chat/chat_rooms.ex | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/ex_chat/chat_rooms.ex b/lib/ex_chat/chat_rooms.ex index 9c4cd06..ada5918 100644 --- a/lib/ex_chat/chat_rooms.ex +++ b/lib/ex_chat/chat_rooms.ex @@ -17,24 +17,24 @@ defmodule ExChat.ChatRooms do end end - def join(room, [as: session_id]) do + def join(room, [as: user_id]) do case find(room) do {:ok, pid} -> - try_join_chatroom(pid, session_id) + try_join_chatroom(pid, user_id) {:error, :unexisting_room} -> {:error, :unexisting_room} end end - def send(message, [to: room, as: session_id]) do + def send(message, [to: room, as: user_id]) do case find(room) do - {:ok, pid} -> ChatRoom.send(pid, message, as: session_id) + {:ok, pid} -> ChatRoom.send(pid, message, as: user_id) error -> error end end - defp try_join_chatroom(chatroom_pid, session_id) do - case ChatRoom.join(chatroom_pid, session_id) do + defp try_join_chatroom(chatroom_pid, user_id) do + case ChatRoom.join(chatroom_pid, user_id) do :ok -> :ok {:error, :already_joined} -> From 82ca69af992bdc021d3188efc450d54aaac96179 Mon Sep 17 00:00:00 2001 From: Joe Bew Date: Wed, 1 Jul 2026 08:36:07 +0200 Subject: [PATCH 6/9] refactor: rename user_session to user_id in validate_access_token --- lib/ex_chat/use_cases/validate_access_token.ex | 4 ++-- test/ex_chat/use_cases/validate_access_token_test.exs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/ex_chat/use_cases/validate_access_token.ex b/lib/ex_chat/use_cases/validate_access_token.ex index 4046c0c..6afa3f5 100644 --- a/lib/ex_chat/use_cases/validate_access_token.ex +++ b/lib/ex_chat/use_cases/validate_access_token.ex @@ -6,8 +6,8 @@ defmodule ExChat.UseCases.ValidateAccessToken do case AccessTokenRepository.find_user_session_by(access_token) do nil -> {:error, :access_token_not_valid} - user_session -> - {:ok, user_session} + user_id -> + {:ok, user_id} end end end \ No newline at end of file diff --git a/test/ex_chat/use_cases/validate_access_token_test.exs b/test/ex_chat/use_cases/validate_access_token_test.exs index 6c79351..6ba4be1 100644 --- a/test/ex_chat/use_cases/validate_access_token_test.exs +++ b/test/ex_chat/use_cases/validate_access_token_test.exs @@ -14,8 +14,8 @@ defmodule ExChat.UseCases.ValidateAccessTokenTest do end test "return the user session when there is an access token" do - with_mock(AccessTokenRepository, find_user_session_by: fn(_) -> "a user session" end) do - assert ValidateAccessToken.on("a valid token") == {:ok, "a user session"} + with_mock(AccessTokenRepository, find_user_session_by: fn(_) -> "a user id" end) do + assert ValidateAccessToken.on("a valid token") == {:ok, "a user id"} assert called AccessTokenRepository.find_user_session_by("a valid token") end end From ddfaf18fc32c5388386b165736bc4c9b0fc14e86 Mon Sep 17 00:00:00 2001 From: Joe Bew Date: Wed, 1 Jul 2026 08:36:22 +0200 Subject: [PATCH 7/9] refactor: rename session_id and user_session to user_id in websocket_controller --- lib/ex_chat/web/websocket_controller.ex | 54 ++++++++++++------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/lib/ex_chat/web/websocket_controller.ex b/lib/ex_chat/web/websocket_controller.ex index cf1a795..d3720cc 100644 --- a/lib/ex_chat/web/websocket_controller.ex +++ b/lib/ex_chat/web/websocket_controller.ex @@ -13,72 +13,72 @@ defmodule ExChat.Web.WebSocketController do access_token = access_token_from(req) case ValidateAccessToken.on(access_token) do - {:ok, user_session} -> - {:cowboy_websocket, req, user_session, %{idle_timeout: idle_timeout()}} + {:ok, user_id} -> + {:cowboy_websocket, req, user_id, %{idle_timeout: idle_timeout()}} {:error, :access_token_not_valid} -> {:ok, :cowboy_req.reply(400, req), state} end end - def websocket_init(user_session) do - SubscribeToUserSession.on(self(), user_session) + def websocket_init(user_id) do + SubscribeToUserSession.on(self(), user_id) schedule_ping() - {:ok, user_session} + {:ok, user_id} end - def websocket_handle({:text, command_as_json}, session_id) do + def websocket_handle({:text, command_as_json}, user_id) do case from_json(command_as_json) do - {:error, _reason} -> {:ok, session_id} - {:ok, command} -> handle(command, session_id) + {:error, _reason} -> {:ok, user_id} + {:ok, command} -> handle(command, user_id) end end - def websocket_handle(_message, session_id) do - {:ok, session_id} + def websocket_handle(_message, user_id) do + {:ok, user_id} end - def websocket_info(:ping, session_id) do + def websocket_info(:ping, user_id) do schedule_ping() - {:reply, {:ping, ""}, session_id} + {:reply, {:ping, ""}, user_id} end - def websocket_info(message, session_id) do - {:reply, {:text, to_json(message)}, session_id} + def websocket_info(message, user_id) do + {:reply, {:text, to_json(message)}, user_id} end - defp handle(%{"command" => "join", "room" => room}, session_id) do - case JoinChatRoom.on(room, session_id) do + defp handle(%{"command" => "join", "room" => room}, user_id) do + case JoinChatRoom.on(room, user_id) do :ok -> - {:ok, session_id} + {:ok, user_id} {:error, message} -> - {:reply, {:text, to_json(%{error: message})}, session_id} + {:reply, {:text, to_json(%{error: message})}, user_id} end end - defp handle(command = %{"command" => "join"}, session_id) do - handle(Map.put(command, "room", "default"), session_id) + defp handle(command = %{"command" => "join"}, user_id) do + handle(Map.put(command, "room", "default"), user_id) end - defp handle(%{"room" => room, "message" => message}, session_id) do - case SendMessageToChatRoom.on(message, room, session_id) do + defp handle(%{"room" => room, "message" => message}, user_id) do + case SendMessageToChatRoom.on(message, room, user_id) do {:error, message} -> - {:reply, {:text, to_json(%{ error: message })}, session_id} + {:reply, {:text, to_json(%{ error: message })}, user_id} :ok -> - {:ok, session_id} + {:ok, user_id} end end - defp handle(%{"command" => "create", "room" => room}, session_id) do + defp handle(%{"command" => "create", "room" => room}, user_id) do response = case CreateChatRoom.on(room) do {:ok, message} -> %{success: message} {:error, message} -> %{error: message} end - {:reply, {:text, to_json(response)}, session_id} + {:reply, {:text, to_json(response)}, user_id} end - defp handle(_not_handled_command, session_id), do: {:ok, session_id} + defp handle(_not_handled_command, user_id), do: {:ok, user_id} defp to_json(response), do: Poison.encode!(response) defp from_json(json), do: Poison.decode(json) From 73330a4fc41168a8dc3da1d7409f34ab03ce1adf Mon Sep 17 00:00:00 2001 From: Joe Bew Date: Wed, 1 Jul 2026 08:36:51 +0200 Subject: [PATCH 8/9] docs: update roadmap references to user_id --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a704263..97aef27 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ See [open issues](https://github.com/joebew42/ex_chat/issues) for the full list - `bar_user` associated to the token `bar_token` - Maybe the `AuthenticationService` is a "Repository" instead. Consider to rename it - Provide a real implementation of the `AuthenticationService` -- Extract a collaborator for the `WebSocketClient` that will be responsible to understand if there is an existing user_session for a given access_token +- Extract a collaborator for the `WebSocketClient` that will be responsible to understand if there is an existing user_id for a given access_token - Update the UI in order to handle the user id - It seems that we have a [websocket idle timeout issue](https://ninenines.eu/docs/en/cowboy/2.4/guide/ws_handlers/#_keeping_the_connection_alive). Increase the idle timeout to 10 minutes - Handle the connection when the provided access token is empty or not valid (no user session associated) @@ -83,9 +83,9 @@ See [open issues](https://github.com/joebew42/ex_chat/issues) for the full list - The module `ChatRooms` should be reorganized like the `UserSessions` - As a `ChatRoom` I can notify of new messages to all the subscribed `UserSession`s - rename the `UserSessions.send` to `UserSessions.notify` -- think to rename `clients` to `session_ids` in the `ChatRoom` process +- think to rename `clients` to `user_ids` in the `ChatRoom` process - Rename `ExChat.Registry` in `ExChat.ChatRoomRegistry` -- rename `user_session_id` to `session_id` +- rename `user_session_id` to `user_id` - Maybe the `UserSessions` and `UserSessionSupervisor` could be merged in a single module named `UserSessions` - Fix the names used for the user sessions in the `UserSessionsTest` - Try to find a way to remove the shared state (the `UserSessionRegistry`) from the `UserSessions` Tests From a3b2084ac9ecf37d3ae0b1f6d2873fbf84b598dd Mon Sep 17 00:00:00 2001 From: Joe Bew Date: Wed, 1 Jul 2026 08:39:08 +0200 Subject: [PATCH 9/9] revert: docs: feature specification --- .feature-spec.md | 125 ----------------------------------------------- 1 file changed, 125 deletions(-) delete mode 100644 .feature-spec.md diff --git a/.feature-spec.md b/.feature-spec.md deleted file mode 100644 index e1ff325..0000000 --- a/.feature-spec.md +++ /dev/null @@ -1,125 +0,0 @@ -# Feature Specification: Rename `session_id` / `user_session_id` to `user_id` - -GitHub Issue: https://github.com/joebew42/ex_chat/#12 - -## Summary - -Rename the identifier used for a user throughout the backend from -`session_id` / `session_ids` / `user_session` (when it holds the id value) -to `user_id` / `user_ids` for clarity and consistency. - -Several use cases (`SubscribeToUserSession`, `JoinChatRoom`, -`SendMessageToChatRoom`) already name this parameter `user_id`. The rest of -the codebase still uses `session_id`, `session_ids`, or `user_session`. This -change makes the naming consistent end-to-end. - -## Motivation - -The value flowing through the system as `session_id` is actually the user's -identifier (the `username` established in `CreateUser`). Naming it `session_id` -is misleading; `user_id` states what it is. - -## Scope - -This is a **purely internal refactor** of the Elixir backend. It is -behaviour-preserving — no functional changes. - -### In scope — rename the identifier - -Rename the *identifier variables, parameters, struct fields, and helper -function names* that denote the user identifier: - -- `session_id` → `user_id` -- `session_ids` → `user_ids` -- `user_session` (only where it holds the id value) → `user_id` - -Files affected: - -- `lib/ex_chat/web/websocket_controller.ex` — `session_id` and `user_session` - state variable → `user_id` -- `lib/ex_chat/user_sessions.ex` — `session_id` params/variables → `user_id` - (including the `UserSessionRegistry` key variable) -- `lib/ex_chat/chat_room.ex` — struct field `session_ids` → `user_ids`; - `session_id` params → `user_id`; helper `add_session_id` → `add_user_id`; - `joined?/2` params -- `lib/ex_chat/chat_rooms.ex` — `session_id` params/variables → `user_id` -- `lib/ex_chat/access_token_repository.ex` — `user_session` params/variables - → `user_id` -- `lib/ex_chat/use_cases/validate_access_token.ex` — `user_session` variable - → `user_id` -- `test/ex_chat/chat_room_test.exs` — `_user_session_id` → `_user_id` -- `test/ex_chat/user_sessions_test.exs` — cosmetic test fixture strings/vars - (e.g. `"a-session-id"`) → user-id equivalents for consistency -- `test/ex_chat/access_token_repository_test.exs` — align variable names -- `test/ex_chat/use_cases/validate_access_token_test.exs` — align variable names -- `README.md` — update the roadmap/notes references to reflect the new name - -### Out of scope — keep as-is - -- Module / process / registry names that name the *session process - abstraction* rather than the id: `ExChat.UserSession`, - `ExChat.UserSessions`, `UserSessionRegistry`, and the use case - `ExChat.UseCases.SubscribeToUserSession`. A "user session" is the actor - that holds a user's subscribed client PIDs; it is looked up *by* `user_id`. -- The public repository function name `find_user_session_by/1` and its - `:find_user_session_by` / `:add` message atoms — internal param variables - are renamed, but the function/message names are left untouched to keep the - change focused on the identifier. - -## Boundary / compatibility notes - -- The WebSocket wire protocol (JSON) does **not** contain a `session_id` - key. Client→server payloads use `command` / `room` / `message`; - server→client payloads use `from` / `room` / `message` / `error` / - `success`. The identifier is only ever carried in the `from` field (whose - key name is unchanged). Therefore **no frontend / `priv/static/chat.html` - changes are required** and there is no backward-compatibility concern. - -## Acceptance Criteria - -- No remaining references to `session_id`, `session_ids`, or - `user_session_id` as identifier names in `lib/` and `test/` (except the - intentionally out-of-scope module/function names listed above). -- The identifier is consistently named `user_id` / `user_ids` across - controllers, supervisors, GenServers, use cases, and tests. -- `mix test` passes with no failures. -- No behavioural change: chat create/join/send and user-session - subscription work exactly as before. - -## Implementation Notes - -- Behaviour-preserving rename only; no logic changes. -- Commit organization (inside-out): internal modules first - (`chat_room`, `user_sessions`, `access_token_repository`), then the - supervisor wrapper (`chat_rooms`), then use cases, then the - `websocket_controller`, with test-only changes grouped with the module - they cover; docs (`README.md`) last. - -## Progress - -Implemented (inside-out), each committed with its tests, full suite green after -every step (45 tests, 0 failures): - -- [x] `chat_room` — struct field `session_ids` → `user_ids`, `session_id` params - → `user_id`, helper `add_session_id` → `add_user_id`, `joined?/2` params; - test mock var `_user_session_id` → `_user_id` -- [x] `user_sessions` — `session_id` params/variables → `user_id` (incl. - `UserSessionRegistry` key var); test fixtures `"a-session-id"` → `"a-user-id"` -- [x] `access_token_repository` — `user_session` params/variables → `user_id`; - test fixture aligned to `"a-user-id"` -- [x] `chat_rooms` — `session_id` params/variables → `user_id` -- [x] `validate_access_token` — `user_session` variable → `user_id`; test fixture - aligned to `"a user id"` -- [x] `websocket_controller` — `session_id` and `user_session` state var → `user_id` -- [x] `README.md` — roadmap/notes references updated to the new name - -Key decisions: - -- Out-of-scope names left untouched as specified: `ExChat.UserSession(s)`, - `UserSessionRegistry`, `SubscribeToUserSession`, and the repository function - name `find_user_session_by/1` with its `:find_user_session_by` / `:add` message - atoms. Only the identifier-carrying params/variables/fields were renamed. -- The `:session_not_exists` error atom in `user_sessions` was kept as-is — it - names the *session process* abstraction, not the identifier. - -Remaining: none — feature complete.