Skip to content

fix: call start bugs#308

Open
iparaskev wants to merge 2 commits intomainfrom
fix_call_started_bug
Open

fix: call start bugs#308
iparaskev wants to merge 2 commits intomainfrom
fix_call_started_bug

Conversation

@iparaskev
Copy link
Copy Markdown
Contributor

@iparaskev iparaskev commented May 8, 2026

Summary by CodeRabbit

Release Notes

  • New Features

    • Added call de-duplication to prevent duplicate call requests during a short ringing window (30-second TTL).
  • Improvements

    • Users now see a specific "Already calling, please wait" message when attempting to call during an active ringing window, replacing generic error states.
    • Enhanced responsiveness of call accept/reject handling through improved request throttling.

@iparaskev iparaskev requested a review from konsalex as a code owner May 8, 2026 17:53
@netlify
Copy link
Copy Markdown

netlify Bot commented May 8, 2026

Deploy Preview for hoppdocs ready!

Name Link
🔨 Latest commit cac68dd
🔍 Latest deploy log https://app.netlify.com/projects/hoppdocs/deploys/69fe2305e2e6d90008d9f3de
😎 Deploy Preview https://deploy-preview-308--hoppdocs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 8, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This PR implements call de-duplication using Redis to prevent simultaneous duplicate call initiation attempts during a ringing window. The backend acquires a symmetric SETNX key on call initiation, rejects subsequent attempts with an "already-calling" reason, and releases the slot on acceptance/rejection. The frontend throttles call actions and displays specific user feedback for de-duped rejections.

Changes

Call De-duplication Feature

Layer / File(s) Summary
Message Contract & Payload Definition
backend/internal/messages/messages.go, tauri/src/payloads.ts
Added "already-calling" to RejectReason validation enum in RejectCallMessage and PRejectCallMessage payload types.
Backend Dedupe Mechanism
backend/internal/handlers/websocketHandlers.go
Imported time for TTL support. Added dedupeCallKey helper for symmetric caller/callee key generation. Enhanced initiateCall with Redis SETNX acquisition (30s TTL); rejects with "already-calling" on slot collision and deletes key on error/offline paths. Updated rejectCall and acceptCall to delete the dedupe slot before publishing rejection/acceptance. Updated websocket switch to pass rejecting user ID to rejectCall.
Frontend Call Action Throttling
tauri/src/components/ui/call-banner.tsx
Replaced useCallback with lodash/throttle for both reject and accept handlers (6-second throttle window). Reject handler stops sound and sends call_reject. Accept handler registers tokens listener, sends call_accept, waits up to 5 seconds for tokens, and triggers throttled reject on timeout. Added unmount cleanup effect to cancel both throttled handlers.
Frontend UI Feedback for De-duped Calls
tauri/src/components/ui/participant-row-wo-livekit.tsx
Added "already-calling" case in call_reject handler to display dedicated toast ("Already calling …, please wait", 2500ms duration) before standard cleanup (mark resolved, clear calling state, stop ringing).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Hops to prevent the glare of two,
Redis keeps the calls from doubling through,
Throttled hands stay calm and true,
"Already calling" shows what's new,
One ring, not two—a cleaner view!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'fix: call start bugs' is vague and overly generic. While it references a real aspect of the changes (call handling), it doesn't clearly communicate what specific bugs are being fixed or what the primary change accomplishes. Make the title more specific by describing the actual fix, e.g., 'fix: prevent duplicate calls with Redis-backed deduplication' or 'fix: add call deduplication to prevent glare during ringing window'.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix_call_started_bug

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.1)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/internal/handlers/websocketHandlers.go (1)

360-377: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Release the dedupe slot on caller cancellation too.

The new cleanup only happens in acceptCall and rejectCall, but the caller-side cancel flow sends call_end. After a user aborts a ringing call, call:pending:* stays alive until TTL expiry, so an immediate retry to the same participant is incorrectly blocked.

Suggested fix
- endCall(c, server, *parsedMessage.CallEnd)
+ endCall(c, server, user.ID, *parsedMessage.CallEnd)
-func endCall(ctx echo.Context, s *common.ServerState, message messages.CallEndMessage) {
+func endCall(ctx echo.Context, s *common.ServerState, senderID string, message messages.CallEndMessage) {
+	s.Redis.Del(context.Background(), dedupeCallKey(senderID, message.Payload.ParticipantID))
+
 	// Publish a message to the other participant
 	payloadJSON, err := json.Marshal(message)
 	if err != nil {
 		ctx.Logger().Error(err)
 		return
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/internal/handlers/websocketHandlers.go` around lines 360 - 377, The
dedupe key is only removed in acceptCall and rejectCall, but not when the caller
cancels (call_end), leaving call:pending:* until TTL expires; update the
call-end handling branch that processes messages of type call_end (the function
or switch branch that handles messages.CallEndMessage / "call_end") to also call
s.Redis.Del(context.Background(), dedupeCallKey(message.Payload.CallerID,
<otherParticipantID>)) so the pending dedupe slot is released on caller
cancellation; reuse the same dedupeCallKey helper as used in
acceptCall/rejectCall and ensure you pass the correct counterpart ID (caller or
callee depending on the payload) before publishing or returning.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/internal/handlers/websocketHandlers.go`:
- Around line 275-278: The dedupe TTL (callDedupeTTL) is shorter than the
ringing window (CallBanner) so duplicate call requests can slip through; update
the TTL used in the SetNX call to be at least the ringing duration (e.g., 60s)
or reference the existing ringing-window constant used by CallBanner (use that
symbolic constant instead of 30s) so the dedupe lock (dedupeKey) remains set for
the full outbound/inbound ringing period when calling s.Redis.SetNX.

In `@tauri/src/components/ui/call-banner.tsx`:
- Around line 104-114: The timeout currently calls handleReject()
unconditionally if tokensReceived is false, which can send a call_reject after
an accept is already in flight; modify the logic to guard against that by
checking a call-accept state before rejecting: introduce or use an existing
boolean (e.g. tokensReceived and a new callAccepted flag or existing acceptCall
flow indicator), only call handleReject() when tokensReceived is false AND
callAccepted is false, and ensure you clear the timeout or set callAccepted when
acceptCall() starts so the timeout cannot race; still always remove the
"call_tokens_callback" handler on timeout/cleanup but do not emit a reject after
an accept is initiated.
- Around line 73-84: The call-accept path calls tauriUtils.getUserSettings()
without protection so a thrown error leaves tokensReceived true and the accept
timeout/toast stuck; wrap the settings lookup in a try/catch and fall back to a
safe default settings object (at least providing start_mic_on_call and
start_camera_on_call) before calling setCallTokens, and ensure any error does
not short-circuit the existing flow that clears the accept timeout/toast; update
the code around setCallTokens / tokensReceived handling to use the
caught-or-default settings and proceed normally.

---

Outside diff comments:
In `@backend/internal/handlers/websocketHandlers.go`:
- Around line 360-377: The dedupe key is only removed in acceptCall and
rejectCall, but not when the caller cancels (call_end), leaving call:pending:*
until TTL expires; update the call-end handling branch that processes messages
of type call_end (the function or switch branch that handles
messages.CallEndMessage / "call_end") to also call
s.Redis.Del(context.Background(), dedupeCallKey(message.Payload.CallerID,
<otherParticipantID>)) so the pending dedupe slot is released on caller
cancellation; reuse the same dedupeCallKey helper as used in
acceptCall/rejectCall and ensure you pass the correct counterpart ID (caller or
callee depending on the payload) before publishing or returning.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: aed72e76-5182-43c0-9859-e3495a8cd73c

📥 Commits

Reviewing files that changed from the base of the PR and between c75029a and cac68dd.

📒 Files selected for processing (5)
  • backend/internal/handlers/websocketHandlers.go
  • backend/internal/messages/messages.go
  • tauri/src/components/ui/call-banner.tsx
  • tauri/src/components/ui/participant-row-wo-livekit.tsx
  • tauri/src/payloads.ts

Comment on lines +275 to +278
// Dedupe: prevent duplicate/glare call requests within ringing window.
// Symmetric key: pending A->B also blocks B->A.
const callDedupeTTL = 30 * time.Second
acquired, err := s.Redis.SetNX(rdbCtx, dedupeKey, "1", callDedupeTTL).Result()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep the dedupe TTL at least as long as the ringing window.

CallBanner keeps the incoming call active for 60 seconds, but this Redis slot expires after 30 seconds. That means a second call_request can reacquire the key while the first banner is still ringing, which reopens the duplicate-call path this PR is trying to close.

Suggested fix
- const callDedupeTTL = 30 * time.Second
+ // Keep this aligned with the maximum ringing window.
+ const callDedupeTTL = 60 * time.Second
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Dedupe: prevent duplicate/glare call requests within ringing window.
// Symmetric key: pending A->B also blocks B->A.
const callDedupeTTL = 30 * time.Second
acquired, err := s.Redis.SetNX(rdbCtx, dedupeKey, "1", callDedupeTTL).Result()
// Dedupe: prevent duplicate/glare call requests within ringing window.
// Symmetric key: pending A->B also blocks B->A.
// Keep this aligned with the maximum ringing window.
const callDedupeTTL = 60 * time.Second
acquired, err := s.Redis.SetNX(rdbCtx, dedupeKey, "1", callDedupeTTL).Result()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/internal/handlers/websocketHandlers.go` around lines 275 - 278, The
dedupe TTL (callDedupeTTL) is shorter than the ringing window (CallBanner) so
duplicate call requests can slip through; update the TTL used in the SetNX call
to be at least the ringing duration (e.g., 60s) or reference the existing
ringing-window constant used by CallBanner (use that symbolic constant instead
of 30s) so the dedupe lock (dedupeKey) remains set for the full outbound/inbound
ringing period when calling s.Redis.SetNX.

Comment on lines +73 to +84
const settings = await tauriUtils.getUserSettings();
setCallTokens({
...data.payload,
timeStarted: new Date(),
hasAudioEnabled: settings.start_mic_on_call,
hasCameraEnabled: settings.start_camera_on_call,
role: ParticipantRole.NONE,
isRemoteControlEnabled: true,
participants: [],
isInitialisingCall: true,
micLevel: 0,
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard the settings lookup in the accept path.

tauriUtils.getUserSettings() is unprotected here, unlike the outgoing-call flow. If it throws, this handler exits after tokensReceived = true, so the timeout will not reject, the toast never dismisses, and the accepted call gets stuck locally.

Suggested fix
-              const settings = await tauriUtils.getUserSettings();
+              let settings = {
+                start_mic_on_call: false,
+                start_camera_on_call: false,
+              };
+              try {
+                settings = await tauriUtils.getUserSettings();
+              } catch {
+                // fall back to safe defaults
+              }
               setCallTokens({
                 ...data.payload,
                 timeStarted: new Date(),
                 hasAudioEnabled: settings.start_mic_on_call,
                 hasCameraEnabled: settings.start_camera_on_call,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const settings = await tauriUtils.getUserSettings();
setCallTokens({
...data.payload,
timeStarted: new Date(),
hasAudioEnabled: settings.start_mic_on_call,
hasCameraEnabled: settings.start_camera_on_call,
role: ParticipantRole.NONE,
isRemoteControlEnabled: true,
participants: [],
isInitialisingCall: true,
micLevel: 0,
});
let settings = {
start_mic_on_call: false,
start_camera_on_call: false,
};
try {
settings = await tauriUtils.getUserSettings();
} catch {
// fall back to safe defaults
}
setCallTokens({
...data.payload,
timeStarted: new Date(),
hasAudioEnabled: settings.start_mic_on_call,
hasCameraEnabled: settings.start_camera_on_call,
role: ParticipantRole.NONE,
isRemoteControlEnabled: true,
participants: [],
isInitialisingCall: true,
micLevel: 0,
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tauri/src/components/ui/call-banner.tsx` around lines 73 - 84, The
call-accept path calls tauriUtils.getUserSettings() without protection so a
thrown error leaves tokensReceived true and the accept timeout/toast stuck; wrap
the settings lookup in a try/catch and fall back to a safe default settings
object (at least providing start_mic_on_call and start_camera_on_call) before
calling setCallTokens, and ensure any error does not short-circuit the existing
flow that clears the accept timeout/toast; update the code around setCallTokens
/ tokensReceived handling to use the caught-or-default settings and proceed
normally.

Comment on lines +104 to +114
// Wait 5 seconds for tokens, otherwise show error and reject
setTimeout(() => {
if (!tokensReceived) {
toast.error("Failed to establish call. Please try again.", {
duration: 4_000,
});
handleReject();
}
// Clean up the socket listener after timeout regardless of success/failure
socketService.removeHandler("call_tokens_callback");
}, 5000);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Avoid sending call_reject after call_accept is already in flight.

By the time this timeout fires, the server may already be inside acceptCall and can still publish call_tokens later. That creates conflicting terminal events (call_acceptcall_rejectcall_tokens) when token generation is just slow, which can leave caller and callee in different states.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tauri/src/components/ui/call-banner.tsx` around lines 104 - 114, The timeout
currently calls handleReject() unconditionally if tokensReceived is false, which
can send a call_reject after an accept is already in flight; modify the logic to
guard against that by checking a call-accept state before rejecting: introduce
or use an existing boolean (e.g. tokensReceived and a new callAccepted flag or
existing acceptCall flow indicator), only call handleReject() when
tokensReceived is false AND callAccepted is false, and ensure you clear the
timeout or set callAccepted when acceptCall() starts so the timeout cannot race;
still always remove the "call_tokens_callback" handler on timeout/cleanup but do
not emit a reject after an accept is initiated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant