fix(ws): always echo a message to its author, even when unsubscribed (LC-397) #412

Merged
longjacksonle merged 1 commit from fix/lc-397-author-echo-unconditional into main 2026-06-20 09:29:21 +02:00

Fixes LC-397. The optimistic-echo "Sending..." sticking issue (LC-382) happens in enclave rooms but not DMs - your exact observation is what pinned it.

Root cause

The composer posts with hx-swap="none", so the canonical message render arrives only via the author's own WebSocket echo - which carries the client_id that removes the optimistic placeholder. But render_new_message_or_bump only sent that echo when the author's connection was already in the per-connection subscribed room set:

if is_subscribed { ... return render_new_message(..., client_id, ...); }
if message.user_id == viewer.id { return None; }   // <- unsubscribed author: NO echo

When the author was a broadcast recipient but their room subscription hadn't registered for that connection, they fell to that return None and got nothing, so the placeholder was never reconciled until the LC-382 watchdog/refresh. DMs dodged it because DM members are always in room_members and subscribe via that path; enclave rooms hit it.

Fix

The author posted the message and has a placeholder waiting, so they must always receive their own echo regardless of subscription. Handle the author before the subscription check and render the echo (with the client_id) unconditionally. The echo is an id-keyed OOB swap into #messages, so it self-limits to the tab actually viewing the room (other tabs drop it harmlessly) - the same self-limiting-OOB principle the codebase already relies on. Non-author delivery (subscribed -> mark-read + render; else sidebar bump) is unchanged.

Testing

  • New regression test author_gets_echo_even_when_not_subscribed: posts as the author, then renders render_new_message_or_bump with an empty subscription set and asserts the echo still carries data-lc-client-id. It would have failed before the fix (unsubscribed author returned None). render_new_message_or_bump is now pub, re-exported via routes (mirroring maybe_coyote_ban).
  • just check, just test, just test-saas all green (exit 0).
  • The end-to-end behavior (enclave send renders instantly) you'll confirm in the app.

This is the real root cause behind the LC-382 watchdog being needed in enclave rooms - the watchdog stays as a backstop, but the author's placeholder should now reconcile instantly from the echo.

Fixes LC-397. The optimistic-echo "Sending..." sticking issue (LC-382) happens in **enclave rooms but not DMs** - your exact observation is what pinned it. ## Root cause The composer posts with `hx-swap="none"`, so the canonical message render arrives **only** via the author's own WebSocket echo - which carries the `client_id` that removes the optimistic placeholder. But `render_new_message_or_bump` only sent that echo when the author's connection was already in the per-connection `subscribed` room set: ```rust if is_subscribed { ... return render_new_message(..., client_id, ...); } if message.user_id == viewer.id { return None; } // <- unsubscribed author: NO echo ``` When the author was a broadcast recipient but their room subscription hadn't registered for that connection, they fell to that `return None` and got nothing, so the placeholder was never reconciled until the LC-382 watchdog/refresh. DMs dodged it because DM members are always in `room_members` and subscribe via that path; enclave rooms hit it. ## Fix The author posted the message and has a placeholder waiting, so they must **always** receive their own echo regardless of subscription. Handle the author before the subscription check and render the echo (with the `client_id`) unconditionally. The echo is an id-keyed OOB swap into `#messages`, so it self-limits to the tab actually viewing the room (other tabs drop it harmlessly) - the same self-limiting-OOB principle the codebase already relies on. Non-author delivery (subscribed -> mark-read + render; else sidebar bump) is unchanged. ## Testing - New regression test `author_gets_echo_even_when_not_subscribed`: posts as the author, then renders `render_new_message_or_bump` with an **empty** subscription set and asserts the echo still carries `data-lc-client-id`. It would have failed before the fix (unsubscribed author returned `None`). `render_new_message_or_bump` is now `pub`, re-exported via `routes` (mirroring `maybe_coyote_ban`). - `just check`, `just test`, `just test-saas` all green (exit 0). - The end-to-end behavior (enclave send renders instantly) you'll confirm in the app. This is the real root cause behind the LC-382 watchdog being needed in enclave rooms - the watchdog stays as a backstop, but the author's placeholder should now reconcile instantly from the echo.
fix(ws): always echo a message to its author, even when unsubscribed (LC-397)
All checks were successful
check-secrets / TruffleHog (push) Successful in 4s
check-secrets / Kingfisher (push) Successful in 5s
check-secrets / Nosey parker (push) Successful in 3s
check-secrets / Nosey parker (pull_request) Successful in 3s
check-secrets / Kingfisher (pull_request) Successful in 4s
check-secrets / TruffleHog (pull_request) Successful in 5s
Create release / Create release from merged PR (pull_request) Has been skipped
Check / clippy + fmt + tests (pull_request) Successful in 4m12s
ea228b723b
The optimistic-echo "Sending..." stuck issue (LC-382) hit enclave rooms but not DMs. The composer posts with hx-swap="none", so the canonical render arrives only via the author's own WebSocket echo, which carries the client_id that removes the optimistic placeholder. But render_new_message_or_bump only sent that echo when the author's connection was already in the per-connection `subscribed` room set; an unsubscribed author fell to `if message.user_id == viewer.id { return None }` and got no echo, so the placeholder was never reconciled (until the LC-382 watchdog/refresh). DMs avoided it because DM members are always in room_members and subscribe via that path.

The author posted the message and has a placeholder waiting, so they must always receive their own echo. Handle the author before the subscription check and render the echo (with the client_id) unconditionally. The echo is an id-keyed OOB swap into #messages, so it self-limits to the tab actually viewing the room; other tabs drop it harmlessly. Non-author delivery (subscribed -> mark-read + render; else sidebar bump) is unchanged.

render_new_message_or_bump is now pub (re-exported via routes, mirroring maybe_coyote_ban) so a regression test asserts the author's echo carries the client_id with an empty subscription set.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
longjacksonle deleted branch fix/lc-397-author-echo-unconditional 2026-06-20 09:29:21 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/lets-chat!412
No description provided.