Skip to content

Fix: Additional bugs and code quality improvements#4210

Merged
katspaugh merged 9 commits intomainfrom
fix/additional-bugs-and-improvements
Oct 1, 2025
Merged

Fix: Additional bugs and code quality improvements#4210
katspaugh merged 9 commits intomainfrom
fix/additional-bugs-and-improvements

Conversation

@katspaugh
Copy link
Owner

Summary

This PR addresses 9 additional bugs and code quality issues found during code analysis. These fixes improve error handling, resource management, and prevent edge case failures.

Issues Fixed

7. Constructor async error logging (src/wavesurfer.ts)

Severity: LOW

  • Problem: Initial load errors were silently swallowed
  • Fix: Added console.error for debugging visibility
  • Impact: Developers can now see initial load failures

8. RecordPlugin blob URL leak (src/plugins/record.ts)

Severity: MEDIUM

  • Problem: Blob URLs not revoked during multiple recordings
  • Fix: Track and revoke blob URLs before creating new ones
  • Impact: Prevents memory leaks from unreleased blobs

9. ResizeObserver reference leak (src/renderer.ts)

Severity: LOW

  • Problem: Observer reference not cleared after disconnect
  • Fix: Set resizeObserver to null after disconnect
  • Impact: Allows proper garbage collection

10. Debounce timeout leak (src/wavesurfer.ts)

Severity: LOW

  • Problem: Pending timeout not cleared on destroy
  • Fix: Clear timeout in cleanup function
  • Impact: Prevents callbacks after instance destruction

11. Region channel bounds check (src/plugins/regions.ts)

Severity: LOW

  • Problem: Division by zero if numberOfChannels is 0
  • Fix: Added check for numberOfChannels > 0
  • Impact: Prevents Infinity in CSS height values

12. Empty color array handling (src/renderer.ts)

Severity: LOW

13. AudioContext accumulation (src/plugins/record.ts)

Severity: MEDIUM

  • Problem: Multiple startMic() calls created contexts without closing previous
  • Fix: Call stopMic() before creating new context
  • Impact: Prevents audio resource exhaustion

14. Region content event listeners (src/plugins/regions.ts)

Severity: LOW-MEDIUM

  • Problem: Event listeners on content not removed when content changed
  • Fix: Store and remove listeners before changing content
  • Impact: Prevents memory leaks from detached DOM listeners

15. Fetcher infinite recursion (src/fetcher.ts)

Severity: LOW

  • Problem: No safeguard against malformed streams
  • Fix: Added maxIterations limit (100,000)
  • Impact: Protects against edge cases with never-ending streams

Testing Recommendations

  1. Multiple recordings: Test startMic()/stopMic() cycles
  2. Region content changes: Create regions and change content repeatedly
  3. Empty data edge cases: Test with empty color arrays, zero channels
  4. Long fetch operations: Monitor for proper termination
  5. Rapid destroy cycles: Ensure no orphaned timeouts/listeners

Notes

  • All fixes maintain backward compatibility
  • No breaking changes to public API
  • Improves overall stability and memory management

🤖 Generated with Claude Code

Co-Authored-By: Claude noreply@anthropic.com

katspaugh and others added 9 commits October 1, 2025 14:56
The constructor's initial load() call silently swallowed errors with
.catch(() => null), making debugging difficult when initial load fails.

Issues fixed:
- Added console.error to log the error for debugging
- Error event is still emitted inside load() as documented
- Developers can now see initial load failures in console
- No change to error event behavior

While errors cannot be caught from a constructor call, this provides
visibility into initial load failures for debugging purposes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
The RecordPlugin created blob URLs with URL.createObjectURL() but
never revoked them, causing memory leaks during multiple recordings.

Issues fixed:
- Added recordedBlobUrl property to track created blob URLs
- Revoke previous blob URL before creating new one
- Revoke blob URL in destroy() to free memory
- Prevents blob accumulation during multiple recording sessions

Each recording now properly cleans up its blob URL before creating
a new one, preventing memory leaks from unreleased blob references.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
The Renderer's destroy() method disconnected the ResizeObserver but
didn't clear the reference, causing minor memory retention.

Issues fixed:
- Set resizeObserver to null after disconnect()
- Ensures reference is cleared for garbage collection
- Prevents disconnected observer from remaining in memory

While the observer was disconnected, the reference prevented it from
being garbage collected. Now properly releases the reference.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
The drag handler had a pending setTimeout that wasn't cleared when
WaveSurfer was destroyed, potentially causing seekTo() calls on
destroyed instances.

Issues fixed:
- Store unsubscribeDrag separately from subscriptions array
- Add cleanup function that clears timeout and unsubscribes
- Prevents setTimeout callback from executing after destroy
- Avoids potential null reference errors

The debounce timeout is now properly cleared when the instance is
destroyed, preventing orphaned timeout callbacks.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
The region element initialization could divide by zero if
numberOfChannels was 0, resulting in Infinity for elementHeight.

Issues fixed:
- Added check for numberOfChannels > 0 before division
- Prevents Infinity from being set as CSS height value
- Falls back to default 100% height for invalid channel counts
- Ensures region rendering doesn't break with edge case data

The region now safely handles cases where numberOfChannels is 0
or undefined, preventing invalid CSS values.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
The convertColorValues() method returned empty string for empty
arrays, which could cause rendering issues. Now returns default color.

Issues fixed:
- Added explicit check for empty color array (length === 0)
- Returns default waveColor '#999' instead of empty string
- Ensures waveform always has a valid color value
- Prevents potential rendering failures with invalid colors

Empty color arrays now fall back to a sensible default rather than
an empty string that could break rendering.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
The startMic() method created new AudioContext instances without
closing previous ones when called multiple times, causing resource
exhaustion.

Issues fixed:
- Added check for existing micStream before starting new one
- Call stopMic() to clean up previous AudioContext
- Prevents AudioContext accumulation from multiple startMic() calls
- Ensures system audio resources are properly released

Each startMic() call now properly closes the previous AudioContext
before creating a new one, preventing audio resource exhaustion.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
The setContent() method removed old content elements without removing
their event listeners first, causing memory leaks from orphaned listeners
on detached DOM nodes.

Issues fixed:
- Store content event listeners in properties for later removal
- Remove listeners from old content before detaching it
- Re-add listeners to new content after setting it
- Prevents memory leaks from orphaned event listeners

Event listeners on region content are now properly cleaned up when
content is changed, preventing memory leaks from detached DOM nodes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
The watchProgress() recursive read() function had no safeguards against
malformed streams that never signal completion, potentially causing
infinite recursion.

Issues fixed:
- Added maxIterations limit (100,000) to prevent infinite loops
- Added iteration counter to track read attempts
- Logs error and exits if limit is reached
- Protects against malformed or never-ending streams

While reader.read() should eventually return done: true, this provides
a safety net for edge cases with malformed streams or protocol errors.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings October 1, 2025 13:07
@cloudflare-workers-and-pages
Copy link

Deploying wavesurfer-js with  Cloudflare Pages  Cloudflare Pages

Latest commit: d8addbe
Status: ✅  Deploy successful!
Preview URL: https://6271d33c.wavesurfer-js.pages.dev
Branch Preview URL: https://fix-additional-bugs-and-impr.wavesurfer-js.pages.dev

View logs

@katspaugh katspaugh merged commit 6d2a98b into main Oct 1, 2025
7 checks passed
@katspaugh katspaugh deleted the fix/additional-bugs-and-improvements branch October 1, 2025 13:10
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR tightens resource management and resilience across the library, addressing leaks and edge cases while improving error visibility.

  • Add cleanup for blob URLs, ResizeObserver, and drag debounce timeouts
  • Improve error handling and edge-case guards (constructor load logging, channel bounds, empty color arrays, fetch recursion limit)
  • Prevent audio resource accumulation by stopping prior mic streams before starting new ones

Reviewed Changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/wavesurfer.ts Logs initial load errors and ensures drag debounce timeout is cleared on destroy.
src/renderer.ts Properly disconnects and clears ResizeObserver and handles empty color arrays.
src/plugins/regions.ts Safeguards channel bounds and manages content event listeners during content updates.
src/plugins/record.ts Revokes old blob URLs and stops previous mic sessions to avoid resource accumulation.
src/fetcher.ts Adds a max-iteration guard to prevent infinite recursion when reading streams.

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.


const read = async () => {
// Safety check to prevent infinite recursion
if (iterations++ > maxIterations) {
Copy link

Copilot AI Oct 1, 2025

Choose a reason for hiding this comment

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

Off-by-one in the max-iteration guard: with iterations++ > maxIterations the loop allows maxIterations + 1 reads. Use >= with post-increment to stop after exactly maxIterations reads.

Suggested change
if (iterations++ > maxIterations) {
if (iterations++ >= maxIterations) {

Copilot uses AI. Check for mistakes.
const read = async () => {
// Safety check to prevent infinite recursion
if (iterations++ > maxIterations) {
console.error('Fetcher: Maximum iterations reached, stopping read loop')
Copy link

Copilot AI Oct 1, 2025

Choose a reason for hiding this comment

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

[nitpick] When aborting the read loop early, consider canceling the reader to free underlying resources. For example: await reader.cancel().

Suggested change
console.error('Fetcher: Maximum iterations reached, stopping read loop')
console.error('Fetcher: Maximum iterations reached, stopping read loop')
await reader.cancel()

Copilot uses AI. Check for mistakes.
Comment on lines +7 to +8
const maxIterations = 100000 // Safety limit to prevent infinite loops
let iterations = 0
Copy link

Copilot AI Oct 1, 2025

Choose a reason for hiding this comment

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

[nitpick] Magic number for iteration cap. Extract to a named constant (e.g., MAX_STREAM_READ_ITERATIONS) or make it configurable to clarify intent and ease tuning.

Copilot uses AI. Check for mistakes.
Comment on lines +204 to +205
// Log error for debugging while still emitting error event
console.error('WaveSurfer initial load error:', err)
Copy link

Copilot AI Oct 1, 2025

Choose a reason for hiding this comment

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

[nitpick] Direct console.error in a library can be noisy for consumers. Consider gating behind a debug flag or relying solely on the emitted 'error' event so integrators control logging.

Suggested change
// Log error for debugging while still emitting error event
console.error('WaveSurfer initial load error:', err)
// Error is emitted via the 'error' event; consumers can handle logging if desired

Copilot uses AI. Check for mistakes.

// Clear debounce timeout on destroy
this.subscriptions.push(() => {
clearTimeout(debounce)
Copy link

Copilot AI Oct 1, 2025

Choose a reason for hiding this comment

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

[nitpick] Guard clearTimeout to avoid passing undefined in some TS/lib combinations and to be explicit: if (debounce) clearTimeout(debounce).

Suggested change
clearTimeout(debounce)
if (debounce) clearTimeout(debounce)

Copilot uses AI. Check for mistakes.
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