Skip to content

Conversation

@yujonglee
Copy link
Contributor

@yujonglee yujonglee commented Dec 4, 2025

refactor(ws, ws-utils): implement 7-phase refactoring plan

Summary

This PR implements a comprehensive refactoring of the ws and ws-utils crates based on a Claude Code analysis. Key changes include:

Error Handling: Replaced generic Unknown error variant with structured types (Timeout with duration context, DataSend with context, InvalidRequest, ParseError). Silent let _ = error ignoring replaced with proper logging.

Configuration: Extracted hardcoded values (5 retries, 500ms delay, 8s timeout, 5s grace period) into ConnectionConfig, RetryConfig, and KeepAliveConfig types.

API Changes (Breaking):

  • WebSocketIO::from_message(msg) -> Option<T> changed to decode(msg) -> Result<T, DecodeError>
  • from_audio now returns (stream, handle, SendTask) instead of (stream, handle) for proper shutdown handling
  • ConnectionManager::acquire_connection is now async (uses RwLock instead of Mutex)

Updates since last revision

  • Updated owhisper-client to implement decode method instead of from_message for ListenClientIO and ListenClientDualIO
  • Updated all from_audio call sites in owhisper-client to handle the new 3-tuple return value (stream, handle, SendTask)
  • Updated transcribe-whisper-local to await the now-async acquire_connection() call
  • Removed unused Phase 3 abstractions (AudioSamples, SampleBuffer, SampleSource, BufferedAudioStream) - these were scaffolding for future deduplication work but weren't integrated; will be done in a follow-up PR

Review & Testing Checklist for Human

  • Verify _send_task handling is intentional: The SendTask return values are being discarded with _ prefix in owhisper-client. Confirm this is acceptable or if proper shutdown handling via send_task.wait() should be added.
  • Test ConnectionManager async behavior: The change from sync Mutex to async RwLock should be tested in real usage scenarios to ensure no deadlocks or behavior changes.
  • Verify no other consumers were missed: Search for other usages of from_audio, from_message, or acquire_connection that may need updating.

Recommended test plan: Run cargo test -p ws -p ws-utils, then manually test code paths that use ConnectionManager::acquire_connection or implement WebSocketIO (e.g., the STT streaming functionality).

Notes

  • serde_json moved from dev-dependencies to regular dependencies (required for DecodeError)
  • Module-level documentation added to both crates
  • Phase 3 audio source deduplication deferred to follow-up PR

Link to Devin run: https://app.devin.ai/sessions/151775d46b90483bb19252faa1e936fd
Requested by: yujonglee (@yujonglee)

devin-ai-integration bot and others added 2 commits December 4, 2025 09:28
Phase 1: Enhanced error types with context (Timeout, DataSend, ParseError)
Phase 2.1: Extract ConnectionConfig, RetryConfig, KeepAliveConfig types
Phase 3: Create SampleBuffer, SampleSource trait, AudioSamples enum
Phase 4.1: Return SendTask handle from from_audio for proper shutdown
Phase 4.3: Improve ConnectionManager with RwLock and async methods
Phase 5.1: Fix unwrap() and add InvalidRequest error variant
Phase 5.2: Simplify WebSocketIO trait with DecodeError
Phase 6: Update tests for API changes
Phase 7: Add module-level documentation

Breaking changes:
- WebSocketIO trait: from_message -> decode with Result<T, DecodeError>
- from_audio return type now includes SendTask handle
- ConnectionManager::acquire_connection is now async

Co-Authored-By: yujonglee <[email protected]>
- Fix is_empty for Stereo to check both channels
- Fix potential busy-wait when source returns empty vectors

Co-Authored-By: yujonglee <[email protected]>
@devin-ai-integration
Copy link
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR that start with 'DevinAI' or '@devin'.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@netlify
Copy link

netlify bot commented Dec 4, 2025

Deploy Preview for hyprnote-storybook ready!

Name Link
🔨 Latest commit cb55742
🔍 Latest deploy log https://app.netlify.com/projects/hyprnote-storybook/deploys/69315b30e12d960008d89bef
😎 Deploy Preview https://deploy-preview-2112--hyprnote-storybook.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.

@netlify
Copy link

netlify bot commented Dec 4, 2025

Deploy Preview for hyprnote ready!

Name Link
🔨 Latest commit cb55742
🔍 Latest deploy log https://app.netlify.com/projects/hyprnote/deploys/69315b30f71583000897a403
😎 Deploy Preview https://deploy-preview-2112--hyprnote.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
Contributor

coderabbitai bot commented Dec 4, 2025

📝 Walkthrough

Walkthrough

Adds audio conversion utilities and a buffered f32 sample stream to ws-utils; makes ConnectionManager async with an RwLock; introduces a client-side config module, DecodeError and SendTask, richer ws error variants; updates call sites and tests to new APIs and async acquisition.

Changes

Cohort / File(s) Summary
Audio utilities
crates/ws-utils/src/audio.rs
Adds AudioSamples (Mono/Stereo) with to_mono() and is_empty(); adds deinterleave_stereo() and bytes_to_mono() converting interleaved 16-bit LE bytes into f32 samples (uses hypr_audio_utils).
Buffered streaming
crates/ws-utils/src/buffered_stream.rs
Adds SampleBuffer, SampleSource trait, and generic BufferedAudioStream<S> implementing Stream<Item = f32> and kalosm_sound::AsyncSource; buffers Vec chunks and yields single f32 samples.
ws-utils crate root
crates/ws-utils/src/lib.rs
Declares modules and re-exports: AudioSamples, BufferedAudioStream, SampleBuffer, SampleSource; retains existing manager exports.
Connection management
crates/ws-utils/src/manager.rs
Replaces Arc<Mutex<Option<CancellationToken>>> with Arc<RwLock<Option<CancellationToken>>>; acquire_connection() becomes async; adds cancel_all(); ConnectionGuard gains is_cancelled() and child_token().
WebSocket config module
crates/ws/src/config.rs, crates/ws/src/lib.rs
Adds config module (client feature): ConnectionConfig, RetryConfig, KeepAliveConfig with Default impls and public fields; exposes module under feature.
WebSocket client
crates/ws/src/client.rs
Re-exports config types; replaces WebSocketIO::from_message() with decode() -> Result<_, DecodeError>; adds DecodeError and SendTask; WebSocketClient gains config, with_config(), with_keep_alive(); from_audio() now returns (stream, handle, SendTask) and uses config-driven retry/timeout/close behavior; improved error propagation.
Error API
crates/ws/src/error.rs
Reworks error enum with structured variants (Timeout { source, timeout }, ControlSend, DataSend { context }, UnexpectedClose, InvalidRequest, ParseError, updated Connection) and helper constructors.
Cargo manifest
crates/ws/Cargo.toml
Moves serde_json = { workspace = true } into [dependencies] and removes prior [dev-dependencies] block.
Tests & call sites
crates/ws/tests/client_tests.rs, crates/transcribe-whisper-local/src/service/streaming.rs, owhisper/owhisper-client/src/live.rs
Tests and call sites updated to decode()/DecodeError; from_audio() callsites destructure (raw_stream, handle, send_task); acquire_connection() awaited where required.

Sequence Diagram(s)

sequenceDiagram
    participant Consumer as Stream Consumer
    participant Stream as BufferedAudioStream
    participant Buffer as SampleBuffer
    participant Source as SampleSource

    Consumer->>Stream: poll_next(cx)
    alt buffer has samples
        Stream->>Buffer: next_sample()
        Buffer-->>Stream: Some(f32)
        Stream-->>Consumer: Ready(Some(f32))
    else buffer empty
        Stream->>Source: poll_samples(cx)
        alt Ready(Some(samples))
            Stream->>Buffer: push_samples(samples)
            Stream->>Buffer: next_sample()
            Buffer-->>Stream: Some(f32)
            Stream-->>Consumer: Ready(Some(f32))
        else Ready(None)
            Stream-->>Consumer: Ready(None)
        else Pending
            Stream-->>Consumer: Pending
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Areas needing extra attention:
    • crates/ws-utils/src/buffered_stream.rs — Poll implementation, buffer edge cases and waking semantics.
    • crates/ws/src/client.rs — decoding semantics, SendTask lifecycle, error-channel propagation, and config-driven retry/timeout mapping.
    • crates/ws-utils/src/manager.rs — async RwLock usage, acquire/cancel race conditions and CancellationToken handling.
    • crates/ws/src/error.rs — conversions and propagation of new error variants across call sites.

Possibly related PRs

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.33% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'refactor(ws, ws-utils): implement 7-phase refactoring plan' accurately summarizes the main change—a comprehensive refactoring of the ws and ws-utils crates following a structured plan.
Description check ✅ Passed The PR description comprehensively explains the refactoring plan, detailing error handling improvements, configuration extraction, breaking API changes, and updates across multiple crates.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch devin/1764839595-ws-refactoring

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
Contributor

@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: 1

🧹 Nitpick comments (2)
crates/ws-utils/src/audio.rs (1)

24-39: Consider handling odd-length input explicitly.

The function silently discards trailing bytes if data.len() is not a multiple of 2, and silently discards a trailing sample if the total sample count is odd. This may be intentional for audio data, but consider documenting this behavior or returning an error/warning for malformed input.

crates/ws/src/client.rs (1)

206-211: Grace period sleep occurs unconditionally before close.

The close_grace_period sleep happens even if the loop exited due to an error. For error cases, an immediate close might be more appropriate. Consider making the sleep conditional on successful completion.

-           tracing::debug!("draining remaining messages before close");
-           tokio::time::sleep(close_grace_period).await;
+           // Only wait for grace period on clean exit (not errors)
+           if error_tx.is_empty() { // or track exit reason
+               tracing::debug!("draining remaining messages before close");
+               tokio::time::sleep(close_grace_period).await;
+           }
            if let Err(e) = ws_sender.close().await {
                tracing::debug!("ws_close_failed: {:?}", e);
            }

Note: The exact condition depends on how you want to track whether the exit was clean. This is a minor optimization.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1f513e0 and d933b76.

📒 Files selected for processing (10)
  • crates/ws-utils/src/audio.rs (1 hunks)
  • crates/ws-utils/src/buffered_stream.rs (1 hunks)
  • crates/ws-utils/src/lib.rs (1 hunks)
  • crates/ws-utils/src/manager.rs (2 hunks)
  • crates/ws/Cargo.toml (1 hunks)
  • crates/ws/src/client.rs (9 hunks)
  • crates/ws/src/config.rs (1 hunks)
  • crates/ws/src/error.rs (1 hunks)
  • crates/ws/src/lib.rs (1 hunks)
  • crates/ws/tests/client_tests.rs (6 hunks)
🧰 Additional context used
🧬 Code graph analysis (7)
crates/ws/src/lib.rs (1)
crates/ws/tests/client_tests.rs (4)
  • client (103-103)
  • client (120-120)
  • client (174-174)
  • client (221-221)
crates/ws/src/config.rs (2)
crates/ws-utils/src/buffered_stream.rs (1)
  • default (45-47)
crates/ws-utils/src/manager.rs (1)
  • default (11-15)
crates/ws/src/error.rs (1)
crates/ws/src/client.rs (1)
  • tokio (138-138)
crates/ws-utils/src/lib.rs (1)
plugins/local-stt/src/ext.rs (1)
  • manager (449-449)
crates/ws-utils/src/audio.rs (1)
crates/audio-utils/src/lib.rs (2)
  • bytes_to_f32_samples (78-85)
  • mix_audio_f32 (91-100)
crates/ws/tests/client_tests.rs (1)
crates/ws/src/client.rs (1)
  • decode (70-70)
crates/ws/src/client.rs (3)
crates/ws/tests/client_tests.rs (3)
  • to_input (24-26)
  • to_message (28-30)
  • decode (32-39)
crates/ws/src/config.rs (2)
  • default (12-18)
  • default (28-33)
crates/ws/src/error.rs (1)
  • timeout (32-37)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: Redirect rules - hyprnote
  • GitHub Check: Header rules - hyprnote
  • GitHub Check: Pages changed - hyprnote
  • GitHub Check: Devin
  • GitHub Check: desktop_ci (linux, depot-ubuntu-24.04-8)
  • GitHub Check: desktop_ci (macos, depot-macos-14)
  • GitHub Check: desktop_ci (linux, depot-ubuntu-22.04-8)
  • GitHub Check: fmt
🔇 Additional comments (25)
crates/ws/Cargo.toml (1)

14-14: LGTM!

Moving serde_json from dev-dependencies to regular dependencies is appropriate since it's now used in the main crate code for DecodeError::DeserializationError.

crates/ws/src/lib.rs (1)

1-11: LGTM!

Clear crate-level documentation and proper feature-gating for the new config module alongside the existing client module.

crates/ws-utils/src/lib.rs (1)

1-12: LGTM!

Clear crate documentation and well-organized module structure with appropriate re-exports for the new audio and buffered_stream modules.

crates/ws/tests/client_tests.rs (2)

32-39: LGTM!

The decode implementation correctly handles Text messages with proper error mapping via DecodeError::DeserializationError, and returns DecodeError::UnsupportedType for non-text message types.


103-103: LGTM!

The tests correctly capture the new triple return (output, handle, send_task) from from_audio. The _send_task is appropriately prefixed to indicate intentional non-use in these test scenarios.

crates/ws-utils/src/audio.rs (2)

3-6: LGTM!

Clean enum design with clear separation between mono and stereo audio representations.


8-22: LGTM!

The to_mono method correctly leverages mix_audio_f32 for stereo-to-mono conversion, and is_empty properly checks both channels for stereo data.

crates/ws/src/config.rs (3)

4-19: LGTM!

Well-structured configuration with sensible defaults. The 8-second connect timeout and 5-second close grace period are reasonable for production WebSocket connections.


21-34: LGTM!

The retry configuration with 5 attempts and 500ms delay provides a good balance between responsiveness and avoiding connection storms.


36-40: Intentionally no Default for KeepAliveConfig.

This is appropriate since the message field has no sensible default value and must be explicitly configured by the caller.

crates/ws/src/error.rs (2)

3-29: LGTM!

Well-designed error enum with clear, actionable messages. The #[source] attribute on Timeout properly preserves the error chain, and structured variants like DataSend provide useful context for debugging.


31-50: LGTM!

Clean helper constructors that reduce boilerplate at call sites while maintaining type safety with impl Into<String> for flexible input.

crates/ws-utils/src/manager.rs (4)

1-8: LGTM! Clean struct definition with appropriate synchronization primitive.

Using tokio::sync::RwLock is correct for async contexts. The Arc<RwLock<Option<CancellationToken>>> pattern allows shared ownership with async-safe interior mutability.


19-30: Proper connection acquisition with automatic cancellation of previous tokens.

The logic correctly takes exclusive write access, cancels any existing token, and atomically replaces it with a new one. This ensures only one active connection at a time.


32-37: LGTM! Explicit cancellation method added.

The cancel_all method provides a clean way to cancel the current token without acquiring a new connection.


45-55: LGTM! Useful accessors added to ConnectionGuard.

The is_cancelled() and child_token() methods extend the API surface appropriately, enabling synchronous cancellation checks and hierarchical token management.

crates/ws-utils/src/buffered_stream.rs (3)

6-9: LGTM! Simple and effective buffer structure.

The SampleBuffer struct with position-based iteration is a clean approach for buffering audio samples.


33-36: Confirm that replacing the buffer (not appending) is intentional.

push_samples replaces the entire buffer contents rather than appending. This is appropriate for a streaming model where each poll returns a new chunk, but verify this matches the expected usage pattern of SampleSource implementations.


97-105: Implementation is correct and trait-compatible.

BufferedAudioStream properly implements Stream<Item = f32>, so returning self from as_stream() is valid. This follows the same pattern as other kalosm_sound::AsyncSource implementations in the codebase (WebSocketAudioSource, ChannelAudioSource).

crates/ws/src/client.rs (6)

35-52: LGTM! Clean SendTask wrapper with proper panic propagation.

The wait() method correctly handles:

  • Normal completion
  • Panic propagation via resume_unwind
  • Cancellation mapping to UnexpectedClose

54-61: LGTM! Well-structured DecodeError enum.

The DecodeError enum provides clear distinction between unsupported message types and deserialization failures, with proper thiserror derivation.


88-96: LGTM! Builder pattern extensions for configuration.

The with_config and with_keep_alive methods provide clean configuration APIs following the builder pattern.


141-212: Consider returning early on initial message failure instead of continuing to the drain phase.

When the initial message fails (lines 143-151), the task returns an Err but still proceeds to the drain/close logic at lines 206-211 in the happy path. Since this is inside the spawned task, the early return skips the drain. This is actually correct behavior—verify this is intentional.

The error handling pattern (check error_tx.send().is_err()) is consistent throughout.


224-232: Decoding errors silently drop messages without yielding errors.

When T::decode(msg) returns UnsupportedType or DeserializationError, the message is logged but not propagated to the output stream. If consumers need to know about decode failures, consider yielding them as errors.

Verify this behavior is intentional—some use cases may want to surface decode errors to callers rather than silently dropping them.


270-280: LGTM! Robust connection handling with timeout and error mapping.

The try_connect method properly:

  • Validates the request with clear error mapping
  • Applies configurable connection timeout
  • Maps timeout and connection errors to domain-specific error types

- Update ListenClientIO and ListenClientDualIO to implement decode instead of from_message
- Update from_audio calls to handle new SendTask return value
- Update acquire_connection call to be async

Co-Authored-By: yujonglee <[email protected]>
Copy link
Contributor

@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: 0

🧹 Nitpick comments (1)
owhisper/owhisper-client/src/live.rs (1)

197-199: Clarify or retain SendTask to make audio send lifecycle explicit

The new from_audio triple is destructured but the SendTask parts (_send_task, _mic_send_task, _spk_send_task) are immediately dropped. If SendTask is a detached/background task that continues running after drop, this is fine but non-obvious; if drop cancels or aborts the send loop, this would prematurely stop audio transmission.

I’d recommend either:

  • Adding a short comment at these sites explaining that the task is intentionally dropped because it runs detached, or
  • Storing the SendTask inside SingleHandle / DualHandle so its lifetime clearly matches the streaming session.

Please double‑check the ws client’s SendTask semantics and pick the appropriate option.

Also applies to: 239-241, 278-279

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d933b76 and 4e54403.

📒 Files selected for processing (2)
  • crates/transcribe-whisper-local/src/service/streaming.rs (1 hunks)
  • owhisper/owhisper-client/src/live.rs (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
owhisper/owhisper-client/src/live.rs (2)
crates/ws/tests/client_tests.rs (5)
  • decode (32-39)
  • client (103-103)
  • client (120-120)
  • client (174-174)
  • client (221-221)
crates/ws/src/client.rs (1)
  • decode (70-70)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: Redirect rules - hyprnote
  • GitHub Check: Header rules - hyprnote
  • GitHub Check: Pages changed - hyprnote
  • GitHub Check: fmt
  • GitHub Check: desktop_ci (linux, depot-ubuntu-24.04-8)
  • GitHub Check: desktop_ci (linux, depot-ubuntu-22.04-8)
  • GitHub Check: desktop_ci (macos, depot-macos-14)
  • GitHub Check: Devin
🔇 Additional comments (2)
crates/transcribe-whisper-local/src/service/streaming.rs (1)

116-116: Async acquire_connection().await usage looks correct

Cloning the ConnectionManager and awaiting acquire_connection() before wiring on_upgrade is consistent with the new async API and keeps the guard lifetime aligned with the websocket session; no additional changes needed here.

owhisper/owhisper-client/src/live.rs (1)

136-141: WebSocketIO::decode implementations align with new DecodeError contract

Mapping Message::Text to String and treating all other variants as DecodeError::UnsupportedType is consistent with the updated WebSocketIO::decode semantics and the example usage in the ws client tests; this looks good.

Also applies to: 170-175

devin-ai-integration bot and others added 3 commits December 4, 2025 09:49
Returning Poll::Pending without registering a waker violates the async
contract and can cause the stream to never wake up. Reverting to the
original 'continue' behavior which loops and polls again immediately.

Co-Authored-By: yujonglee <[email protected]>
- Document the contract that implementations must return non-empty samples
- Add debug_assert to catch contract violations during development
- Explain why empty samples are problematic (busy-loop risk)

Co-Authored-By: yujonglee <[email protected]>
Remove AudioSamples, SampleBuffer, SampleSource, and BufferedAudioStream
as they were not integrated with existing code. The deduplication of
WebSocketAudioSource and ChannelAudioSource can be done in a follow-up PR.

Co-Authored-By: yujonglee <[email protected]>
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.

2 participants