Skip to content

Feat: Refactor WebSocket server to use actix-ws with channel-based clients#212

Merged
SloMR merged 35 commits into
mainfrom
feat/server/remove-actors
Jun 10, 2026
Merged

Feat: Refactor WebSocket server to use actix-ws with channel-based clients#212
SloMR merged 35 commits into
mainfrom
feat/server/remove-actors

Conversation

@SloMR

@SloMR SloMR commented Jun 10, 2026

Copy link
Copy Markdown
Owner

Summary

Removes the Actix actor framework from the server's WebSocket layer. WsChatServer is now plain shared state behind a cloneable handle, and each connection runs as an actix-ws async task driven by tokio::select! (inbound frames, an outbound mpsc channel, and heartbeats). On top of the migration, this PR adds a round of correctness/resilience hardening on both the server and the web client.

No wire-protocol changes — the client/server contract is the unchanged WS_PREFIX_* text protocol.

Server — actix-ws migration

  • Drop actix, actix-broker, actix-web-actors; add actix-ws (0.4.0), tokio, futures-util.
  • New module layout:
    • chat_server.rsWsChatServer room/session state (a plain struct) + a cloneable ChatServerHandle (Arc<Mutex<…>>) that hides locking and recovers from poisoning.
    • session.rs — per-connection actix-ws task (WsChatSession::run).
    • session_store.rs — in-memory registry; performs the handshake and spawns the task.
  • Server→client delivery is an mpsc channel instead of actor messages.
  • Tightened visibility with #![warn(unreachable_pub)] (enforced as -D warnings in CI).

Server — correctness & resilience

  • Bounded outbound channel — replaces the unbounded sender so a slow/stalled client sheds load (drop-on-full) instead of growing memory without limit.
  • Rejected /join no longer strands the client — join before leaving and bail on the 0 sentinel, so a room/session-limit rejection leaves the client where it was.
  • Graceful oversize-signal handling — a transport ceiling above the app limit lets the handler reply with an error instead of the connection being torn down.
  • Flush queued frames before close — a break-path error notice now actually reaches the client.
  • Balance the client count on handshake failure — prevents leaked counts / private sessions that never expire.
  • Single-pass signal relay, unified broadcast helpers, a DEFAULT_ROOM constant, heartbeat MissedTickBehavior::Delay, and get_or_create_session_uuid returning Uuid.

Web client

  • WebRTC connection-establishment watchdog — automatically retries a peer connection that hangs in checking/never opens its data channel (a state that never fired a failed event), feeding the existing bounded reconnect path; the "can't connect — refresh" banner now appears only after auto-recovery has had a shot.
  • Stale-chunk reload after deploy — reloads once on a failed dynamic import, including the background-preload path that surfaces as an unhandled promise rejection and bypassed Angular's ErrorHandler.

Docker

  • Unpin wget in the web Dockerfile — Alpine keeps only the latest patch, so a pinned -rN version breaks builds once a newer patch ships.

SloMR added 30 commits June 8, 2026 00:36
- Replaced the deprecated actix-web-actors with actix-ws (0.3).
- Removed the now-unused actix-broker dependency.
- Promoted tokio (sync, macros, time) and futures-util from dev to runtime dependencies for the new async session task.
- Changed the Client alias from Recipient<ChatMessage> to a tokio mpsc UnboundedSender<String>, so the server pushes frames over a channel instead of an actor address.
- Removed the ChatMessage message type and renamed ClientMetadata.recipient to tx.
- Carried the channel sender in JoinRoom and made RelaySignalMessage hold a plain String payload.
- Removed `mod actor;` ahead of deleting the actor file.
- Dropped ChatMessage from the public re-exports now that the type is gone.
- Removed actor.rs: WsChatSession is no longer an actor, and the remaining WsChatServer Actor impl moves into server.rs.
- Dropped the dead actix-broker LeaveRoom subscription it carried (nothing ever published LeaveRoom to the broker).
- Removed the Handler<ChatMessage> impl now that sessions are not actors.
- Send client notifications over the mpsc channel instead of try_send, build the relay payload as a String.
- Logged the room/session-limit notification when the client has already disconnected.
- Relocated the WsChatServer Actor impl here from the deleted actor.rs.
- Switched all room broadcasts and relays from Recipient/try_send to the per-client mpsc sender (ClientMetadata.tx).
- Changed relay_message_to_user to take a String and logged failed room-list broadcasts.
- Replaced the WsChatSession actor (StreamHandler + WebsocketContext) with an async run() task that multiplexes inbound frames, the outbound mpsc channel, and the heartbeat via tokio::select!.
- Reassembled fragmented frames with aggregate_continuations and started the heartbeat one interval out to match the previous run_interval timing.
- Parsed client input into a typed UserCommand enum and routed all client replies through a deliver() helper that logs dropped frames.
- Logged failed server sends (join/leave/list) instead of ignoring them.
- Replaced the actix-web-actors WsResponseBuilder with actix_ws::handle, wiring an mpsc channel and spawning the session run() task.
- Supervised the spawned task so a panic is logged instead of dying silently.
- Logged handshake failures at error level so they surface in Sentry.
- Replaced the DummyActor/Recipient fixture in test_join_leave_room with a plain mpsc channel.
- Dropped the now-unused actix and ChatMessage imports.
- Drops `actix` from the dependency tree now that the WebSocket server is plain shared state rather than an Actix actor.
- Only the actix-web framework crates remain (actix-web, actix-http, actix-rt, actix-ws, …); `actix` and `actix_derive` are gone.
- Deletes the old low-level `impl WsChatServer` module; its room helpers (add_client_to_room, broadcasts, relay, cleanup) move into the new consolidated `chat_server.rs`.
- Deletes the Actix message handlers. With the actor gone, these became plain WsChatServer methods (join_room, leave_room, list_rooms, cleanup_session, validate_and_relay_signal) and now live in chat_server.rs.
- Deletes the misnamed module. Its shared types move out: WsChatServer, ChatServerHandle, Client/Room/ClientMetadata to chat_server.rs, and WsChatSession to session.rs.
- Consolidates the WebSocket server: WsChatServer is now plain shared state (no Actix actor), reached through a cloneable ChatServerHandle (Arc<Mutex<WsChatServer>>) that hides locking and recovers from poisoning.
- Merges the former server.rs helpers and handler.rs operations into a single impl, co-located with the types they act on.
- Tightens visibility: only WsChatServer, ChatServerHandle, add_client_to_room, the rooms field, and cleanup_stale_sessions are public; the rest is private or pub(crate). Drops the dead take_room / remove_empty_rooms.
- Logs the unknown-session/room paths in leave_room, list_rooms, cleanup_session.
- Replaces Addr<WsChatServer> with &ChatServerHandle; the helper methods (join_room, list_rooms, user_command, handle_text, …) are now synchronous since there is no actor mailbox to await.
- Moves the WsChatSession struct into this module (next to its impl) and makes it and its fields pub(crate)/private.
- Replaces WsChatServer::from_registry() with an owned ChatServerHandle field (moved to the top of the struct); start_websocket/remove_client use it.
- Routes every mutex lock through a lock_or_log helper that logs and bails on poisoning instead of panicking; is_code_expired now fails closed.
- Tightens visibility: SessionData, the non-shared fields, and the in-crate methods become pub(crate)/private
- Replaces the actor's run_interval cleanup with a background task that calls chat_server.cleanup_stale_sessions() every CLEANUP_INTERVAL, now that the server is plain shared state.
- MAX_ROOMS_PER_SESSION, MAX_SESSIONS, MAX_WS_MESSAGES_PER_SEC are only used in-crate and aren't re-exported, so make them pub(crate).
- The #[get(...)] handlers are public via the pub use re-export in lib.rs but unreachable_pub mis-flags them through the macro expansion, so allow the lint for this module.
- Swaps the deleted handler/message/server modules for chat_server.
- Updates re-exports: ChatServerHandle/WsChatServer from chat_server; drops the removed message types and the now-internal WsChatSession/ClientMetadata.
- Enables #![warn(unreachable_pub)] to flag pub items that could be pub(crate)(enforced as an error in CI via -D warnings).
- Updates the server tech stack in the root and server READMEs to reflect actix-ws (async, non-actor connections) instead of the old actor-based WebSocket support.
- Makes chat_server's visibility scoped to within the crate to ensure encapsulation.
- Moves session cleanup logic into `SessionStore` for cleaner encapsulation.
- Enhances maintainability by consolidating session management logic.
- Reduces code duplication and increases modularity.
- Removed unnecessary random ID generation for sessions because the id will be assign once the user joined a room.
… room

- MAX_CONTINUATION_SIZE: hard transport ceiling (2x MAX_SIGNAL_SIZE) so an oversize signaling message can be rejected gracefully by the handler before the transport tears the connection down
- OUTBOUND_CHANNEL_CAPACITY: bound on the per-client outbound queue so a slow consumer sheds load instead of growing memory without limit
- DEFAULT_ROOM: single source of truth for the auto-joined room name, replacing the duplicated "main" string literal
- Make Client a bounded mpsc::Sender; all server->client pushes use try_send so a slow consumer's frames are logged instead of growing memory unbounded
- Replace users_share_room + relay_message_to_user with a single-pass relay_to_shared_room, halving the work done under the lock per signal
- Funnel broadcast_room_list, broadcast_room_members, and send_join_message through one send_to_clients helper; drop send_join_message's unused id argument and its eager client eviction
- Reference the DEFAULT_ROOM constant in leave_room instead of "main"
- Switch the outbound receiver to a bounded mpsc::Receiver and use try_send in deliver, matching the bounded Client channel
- Join before leaving on /join: bail on the 0 rejection sentinel so a rejected join leaves the client in its current room instead of stranding it
- Raise the transport ceiling to MAX_CONTINUATION_SIZE so oversize signals get a graceful error from the handler rather than a dropped connection
- Drain and flush any queued frames after the loop, before close, so a break-path error notice actually reaches the client
- Auto-join the DEFAULT_ROOM constant instead of the "main" literal
- Set the heartbeat to MissedTickBehavior::Delay so ticks don't burst after a stall.
- Create the outbound channel with the bounded OUTBOUND_CHANNEL_CAPACITY
- Return Uuid from get_or_create_session_uuid instead of a String, dropping the redundant re-parse and the unreachable "Server configuration error" branch in start_websocket
- On a WebSocket handshake failure, call remove_client to undo the earlier count increment, so private sessions still expire instead of leaking a client
- Build the test client with a bounded mpsc::channel instead of unbounded_channel, matching the new bounded Client type
- Introduces a timeout mechanism to retry connections if not established on time.
- Enhances reliability by addressing connections that may hang.
- Updates connection warning delay for better UX.
SloMR added 5 commits June 11, 2026 01:08
- Extract stale-chunk reload logic to a separate utility for reusability.
- Reloads on unhandled promise rejections due to background chunk load failures.
- Enhances app reliability by automatically refreshing stale resources after a deploy.
- Remove specific version constraint for wget package.
- Prevents potential issues from version conflicts during package updates.
- Updates package version for minor improvements.
@SloMR SloMR self-assigned this Jun 10, 2026
@SloMR SloMR added enhancement New feature or request dependencies Pull requests that update a dependency file typescript Pull requests that update TypeScript code rust Pull requests that update Rust code labels Jun 10, 2026
@SloMR SloMR added this to PastePoint Jun 10, 2026
@SloMR SloMR added server Changes made for the Server side docker Modification for docker bump version Bump the project version web Any Web related things labels Jun 10, 2026
@SloMR SloMR moved this to testing in PastePoint Jun 10, 2026
@SloMR SloMR added the documentation Improvements or additions to documentation label Jun 10, 2026
@SloMR SloMR merged commit 787d855 into main Jun 10, 2026
25 checks passed
@SloMR SloMR deleted the feat/server/remove-actors branch June 10, 2026 22:22
@github-project-automation github-project-automation Bot moved this from testing to done in PastePoint Jun 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bump version Bump the project version dependencies Pull requests that update a dependency file docker Modification for docker documentation Improvements or additions to documentation enhancement New feature or request rust Pull requests that update Rust code server Changes made for the Server side typescript Pull requests that update TypeScript code web Any Web related things

Projects

Status: done

Development

Successfully merging this pull request may close these issues.

1 participant