-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
v8.0.0: Complete Reactive Architecture Refactoring #4241
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
katspaugh
wants to merge
74
commits into
main
Choose a base branch
from
reactive/signals
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
- Add signal() for reactive values with automatic subscriber notifications - Add computed() for derived state that auto-updates on dependency changes - Add effect() for side effects with automatic cleanup - Include comprehensive unit tests (30 tests, 100% coverage) - All primitives support multiple subscribers and proper unsubscription - TypeScript types for type-safe reactive programming Implements task wavesurfer.js-61x from reactive refactoring epic
- Add fromEvent() to convert DOM events to reactive signals - Add map() for stream value transformations - Add filter() for conditional stream values - Add debounce() for delaying updates until quiet period - Add throttle() for limiting update frequency - Add cleanup() utility for proper resource cleanup - Include 26 comprehensive unit tests (100% coverage) - Support stream composition (map + filter + debounce) Implements task wavesurfer.js-32u from reactive refactoring epic
- Create createWaveSurferState() factory function - All playback, audio, and UI state in reactive signals - Computed values: isPaused, canPlay, isReady, progressPercent - State actions with validation and clamping - Complete state isolation between instances - Include 34 comprehensive unit tests (100% coverage) Implements task wavesurfer.js-trx from reactive refactoring epic
- Add createComponent() factory for creating reusable UI components - Component lifecycle: render(), update(), destroy() - Support for custom destroy functions - Partial prop updates for efficiency - Works seamlessly with reactive effects - Include 17 comprehensive unit tests - 75% statement coverage, 100% branch coverage Implements task wavesurfer.js-3mr from reactive refactoring epic
- Add createCursorComponent() for playback position visualization - Add createProgressComponent() for progress bar overlay - Both components update reactively via effects - Cursor: position (0-1), color, width, height props - Progress: progress (0-1), color, height props - Include 22 comprehensive tests (100% coverage on both) - Components work independently and support rapid updates Implements task wavesurfer.js-37m from reactive refactoring epic
- Add DeclarativeRenderer class that automatically updates UI - Uses reactive effects - no manual render() calls needed - Integrates cursor and progress components - Automatic cleanup of all effects on destroy - State changes trigger UI updates automatically - Backward compatible manual methods (renderProgress) - Include 24 comprehensive tests (100% statement coverage) - Works alongside existing Renderer via feature flag Key innovation: State → Implements task wavesurfer.js-725 from reactive refactoring epic
- Enhanced DeclarativeRenderer with auto-scroll support - Reactive effects for cursor and progress bar updates - Auto-scroll with autoCenter option during playback - Scroll management methods (getScroll, setScroll, setScrollPercentage) - No manual renderProgress() calls needed - automatic updates! - Added 5 new tests for scroll management (29 total, 100% coverage) - Created working example: examples/reactive-progress.html - Added comprehensive documentation in src/renderer/README.md Key innovation: UI updates automatically when state.currentTime changes! Old way: Timer tick → manual renderProgress() call New way: State change → reactive effect → automatic UI update Benefits: - Updates only when time actually changes (not every 16ms) - Automatic batching of rapid updates - Clean effect cleanup (no memory leaks) - Auto-scroll intelligence (center vs edge-tracking) - Easier to test and reason about Implements task wavesurfer.js-zgh from reactive refactoring epic
… tasks - Created epic wavesurfer.js-wpg: Refactor to Declarative/Reactive Architecture - Added 39 detailed implementation tasks across Phases 2-5: * Phase 2: Declarative Rendering (11 tasks) * Phase 3: Event Streams (10 tasks) * Phase 4: Pure Functions (10 tasks) * Phase 5: Integration & Polish (8 tasks) - Each task includes: * Detailed implementation descriptions with code examples * Clear dependency tracking (blocks, parent-child) * Success criteria and testing requirements * Priority assignments (P1, P2, P3) - Updated AGENTS.md with complete epic overview and navigation guide - All tasks stored in Beads issue tracker (no separate MD files) - Phase 1 (Reactive Foundation) already complete with 9 closed tasks - Estimated timeline: 11-17 weeks remaining for Phases 2-5 Total: 92 issues (83 open, 9 closed) Ready to start Phase 2 implementation
- Convert region state to signals (start, end, color, drag, resize) - Add computed signals for derived values (isMarker, position) - Implement declarative rendering via reactive effects - Remove imperative renderPosition() and setPart() methods - Automatic DOM updates when state changes - Backward compatible API via getters/setters Phase 2, Task 5: Convert Regions plugin rendering to declarative
- Add extractChannelData() - pure function to extract channels from AudioBuffer - Add findMaxAmplitude() - pure function to find max amplitude - Add needsNormalization() - pure function to check if normalization needed - Add normalizeChannelData() - pure function that returns new normalized data - Deprecate old normalize() function (kept for backward compatibility) - Separate pure functions from side-effecting operations - All functions are testable without AudioContext and can run in workers Tests: 244 passed Closes: wavesurfer.js-4dn
- Add findPeakInRange() - find peak in a range of audio data - Add calculatePeaks() - calculate peaks for segments - Add findPeaksInRange() - find peaks in two channels (stereo) - All functions are pure, testable without side effects - Can run in Web Workers for better performance Tests: 244 passed Closes: wavesurfer.js-039
- Refactor cursor.ts to use createElement() instead of verbose DOM manipulation - Refactor progress.ts to use createElement() with declarative style objects - More consistent with existing dom.ts patterns - Cleaner, more readable code Tests: 244 passed
- Add generateChannelPath() - generate path for single channel - Add generateWaveformPath() - high-level path generation with options - Refactor buildWaveformPathData() to use pure generateChannelPath() - Add WaveformPathOptions interface for configurable path generation - All functions are pure, testable without side effects - Can be used in different rendering contexts (Canvas, SVG, etc.) Tests: 244 passed Closes: wavesurfer.js-140
…tions - Add pixelToTime() - convert pixel position to time - Add timeToPixel() - convert time to pixel position - Add relativeToTime() - convert relative position (0-1) to time - Add timeToRelative() - convert time to relative position - Add normalizeToViewport() - normalize coordinates to viewport - Enhance getRelativePointerPosition() documentation - All transformations are pure and invertible - Easy to test with property-based tests Tests: 244 passed Closes: wavesurfer.js-cq2
- Add calculateZoomLevel() - calculate zoom with min/max constraints - Add calculateFitZoom() - calculate zoom to fit duration in width - Add calculateZoomScrollOffset() - maintain center point during zoom - Add calculateZoom() - high-level zoom with maintained center point - All functions are pure, testable without side effects - Enable smooth zooming with center point preservation Tests: 244 passed Closes: wavesurfer.js-5ya
- Add task selection guidelines (focus on conversion, not new features) - Add guidance on closing duplicate tasks - Add code quality practices (use existing utilities) - Add pure function extraction guidelines - Add reactive pattern guidelines - Clarify prioritization of tasks without dependencies Based on learnings from reactive refactoring session.
Implements signal-based drag stream utility to convert imperative drag handling into declarative reactive patterns. Based on existing draggable.ts but using signals instead of callbacks. Features: - Reactive drag events (start, move, end) via signals - Configurable threshold, mouse button, touch delay - Multi-touch prevention - Automatic cleanup of event listeners - Comprehensive test suite (7 tests) Closes wavesurfer.js-919 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Separate pure business logic from side effects in TimelinePlugin: - Extracted calculateTimeInterval() for notch spacing - Extracted calculatePrimaryLabelInterval() for label density - Extracted calculateSecondaryLabelInterval() for secondary labels - Extracted formatTimeLabel() for time formatting - Extracted isNotchVisible() for visibility checks All pure functions documented with JSDoc and "Pure function - no side effects" comment. Plugin remains fully encapsulated in single file. Partial work on wavesurfer.js-5qs (will revisit after core library complete) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Implements comprehensive color utility functions: - parseColor(): Parse CSS colors (hex, rgb/rgba, named) to RGBA - rgbaToString(), rgbaToHex(): Convert RGBA to CSS strings - interpolateColor(), interpolateColors(): Color interpolation - generateColorStops(): Create gradient stops from color arrays - createCanvasGradient(): Canvas gradient helper All functions are pure (no side effects) and fully tested: - 29 passing tests - 90%+ code coverage - JSDoc with "Pure function - no side effects" markers Closes wavesurfer.js-u2k 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Replace imperative scroll event listeners with reactive scroll streams. Changes: - Created createScrollStream() for reactive scroll handling - Pure functions: calculateScrollPercentages(), calculateScrollBounds() - Integrated into Renderer with effect-based event emission - Automatic cleanup via ScrollStream.cleanup() Benefits: - Scroll position is observable reactive state - Computed derived values (percentages, bounds) - Declarative event emission via effects - Easier testing and composition Test coverage: - 15 scroll-stream tests (100% coverage) - All 47 existing renderer tests still pass Closes wavesurfer.js-o9l 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Create bridge utility to convert HTMLMediaElement events → state actions. Changes: - Created bridgeMediaEvents() for automatic state sync - Handles all standard media events (play, pause, seeking, etc.) - bridgeMediaEventsWithHandler() for custom event handling - Clean separation: imperative media API → reactive state - Single source of truth for media → state mapping Benefits: - Centralized event-to-state mapping - Easy to test (mock media events) - Clear data flow - Automatic cleanup Test coverage: - 17 tests (100% coverage) - All event types tested - Cleanup verification Closes wavesurfer.js-drf 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Automatic event emission from reactive state changes. Changes: - setupStateEventEmission() - auto-emit all standard events from state - setupSignalEventEmission() - custom event emission from any signal - setupDebouncedEventEmission() - debounced event emission - setupConditionalEventEmission() - conditional event emission Events automatically emitted: - play/pause when isPlaying changes - timeupdate/audioprocess when currentTime changes - seeking when isSeeking changes - ready when isReady becomes true (once) - zoom when zoom level changes Benefits: - Events always in sync with state - No manual emit() calls needed - Can't forget to emit an event - Clear event source (state changes) - Easier testing Test coverage: - 19 tests (100% coverage) - All event types tested - Debounce and conditional logic tested Closes wavesurfer.js-m2m 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Integrate Phase 3 reactive utilities into WaveSurfer class: ## Changes 1. **Initialize Reactive State** - Add createWaveSurferState() call in constructor - Store wavesurferState and wavesurferActions properties - Track reactive cleanups array 2. **Bridge Media Events → Reactive State** - Call bridgeMediaEvents() to sync HTMLMediaElement events to state - Media events (play, pause, timeupdate, etc.) now update reactive state - Automatic state synchronization with media element 3. **Bridge Reactive State → Legacy Events** - Call setupStateEventEmission() to auto-emit events from state changes - State changes now automatically emit WaveSurfer events - No manual emit() calls needed for state-driven events 4. **Replace Timer with Reactive Animation** - Remove Timer class usage entirely - Implement initReactiveAnimation() with effect-based RAF loop - Animation starts/stops automatically based on isPlaying state - Proper cleanup with cancelAnimationFrame 5. **Cleanup Integration** - Call all reactive cleanups in destroy() - Ensures proper resource cleanup on unmount 6. **Fix regions.ts setPart() Method** - Restore missing setPart() method removed during refactoring - Sets 'part' attribute for CSS ::part() styling ## Benefits - Automatic state → event synchronization (no manual emit calls) - Automatic media → state synchronization (no manual state updates) - Simpler animation loop (no separate Timer class) - Better resource management (automatic cleanup via effects) - Single source of truth (reactive state) ## Test Results - All 350 unit tests passing - 100% coverage on reactive modules - No regressions in existing functionality 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Create event-stream-emitter.ts to bridge EventEmitter to reactive streams.
This provides a reactive streaming API on top of the traditional callback-based
EventEmitter while maintaining full backward compatibility.
## New Utilities
### `toStream(emitter, eventName)`
Convert a single event to a reactive signal/stream:
```typescript
const { stream, cleanup } = toStream(wavesurfer, 'play')
stream.subscribe(() => console.log('Playing!'))
```
### `toStreams(emitter, eventNames)`
Create multiple event streams at once:
```typescript
const streams = toStreams(wavesurfer, ['play', 'pause', 'timeupdate'])
streams.play.subscribe(() => console.log('Play'))
streams.timeupdate.subscribe(([time]) => console.log('Time:', time))
streams.cleanup() // Cleanup all
```
### `mergeStreams(emitter, eventNames)`
Combine multiple events into one stream:
```typescript
const { stream } = mergeStreams(wavesurfer, ['play', 'pause'])
stream.subscribe(({ event, args }) => {
console.log(`Event ${event} fired with`, args)
})
```
### `mapStream(source, mapper)`
Transform stream values:
```typescript
const { stream: timeStream } = toStream(wavesurfer, 'timeupdate')
const seconds = mapStream(timeStream, ([time]) => Math.floor(time))
```
### `filterStream(source, predicate)`
Filter stream values:
```typescript
const afterTen = filterStream(timeStream, ([time]) => time > 10)
```
## Benefits
- **Backward Compatible**: EventEmitter API unchanged
- **Type Safe**: Full TypeScript support
- **Composable**: Chain map/filter operations
- **Automatic Cleanup**: Easy resource management
- **Opt-in**: Users choose callback or stream API
## Test Coverage
- 20 tests covering all utilities
- 100% code coverage
- Tests for composition and integration
- All 370 unit tests passing
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Remove duplicate event emissions from initPlayerEvents() now that reactive state handles all event emission automatically. ## Problem With the reactive architecture, events were being emitted twice: 1. **Media Event** → bridgeMediaEvents() → **State Update** 2. **State Update** → setupStateEventEmission() → **Event Emission** ✅ 3. **Media Event** → initPlayerEvents() → **Event Emission** ❌ (duplicate!) This caused play/pause/timeupdate/seeking events to fire twice. ## Changes ### src/wavesurfer.ts - Remove duplicate emit() calls from initPlayerEvents() - Keep only necessary side effects (clearing stopAtPosition) - Add comment explaining reactive state handles emission ### src/reactive/state-event-emitter.ts - Add 'finish' event emission from reactive state - Emit when playback ends (was playing, reached duration, now stopped) - Proper state tracking with wasPlayingAtEnd flag ## Architecture **Before** (with duplication): ``` Media Event → State Update → Event Emission ✅ Media Event → Direct Emission ❌ (duplicate) ``` **After** (single source of truth): ``` Media Event → State Update → Event Emission ✅ ``` ## What Remains in initPlayerEvents Only side effects that aren't event emissions: - Clear `stopAtPosition` on pause/ended/emptied/error - Initial play state check ## What Remains in initRendererEvents All UI-driven events (not duplicated): - click, dblclick, drag, scroll - redraw, redrawcomplete, resize These come from the Renderer, not from reactive state. ## Test Results ✅ All 370 unit tests passing ✅ No behavioral changes ✅ Events fire once (not twice) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Phase 3: Transform Events to Reactive Streams is now complete. Updated phase status section with: - Phase 1: COMPLETE - Phase 2: COMPLETE - Phase 3: COMPLETE ✅ - Phase 4: IN PROGRESS - Phase 5: PLANNED All task tracking details remain in bd issue system. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Add public reactive signal streams for all Renderer events while maintaining
backward compatibility with EventEmitter API.
## New Public API
Renderer now exposes reactive streams for all UI events:
```typescript
// Reactive streams (new)
renderer.click$ // Signal<{x, y} | null>
renderer.dblclick$ // Signal<{x, y} | null>
renderer.drag$ // Signal<{x, type: 'start'|'move'|'end'} | null>
renderer.resize$ // Signal<void | null>
renderer.render$ // Signal<void | null>
renderer.rendered$ // Signal<void | null>
renderer.scrollStream // ScrollStream (already existed)
// Legacy EventEmitter (still works)
renderer.on('click', (x, y) => {})
renderer.on('drag', (x) => {})
// etc.
```
## Implementation
**Dual Emission Pattern:**
1. Update internal signal: `this._click$.set({x, y})`
2. Emit legacy event: `this.emit('click', x, y)`
Both signals and events fire simultaneously, ensuring backward compatibility.
## Benefits
- **Reactive**: Consumers can use effect() to subscribe
- **Type-safe**: Signals have proper TypeScript types
- **Composable**: Can map/filter streams
- **Backward Compatible**: Old .on() API still works
- **Single Source**: Events derived from same source as signals
## Architecture
```
DOM Event → Handler → Update Signal + Emit Event
↓ ↓
Stream API Callback API
```
## Next Step
Update WaveSurfer to use renderer streams instead of .on() callbacks.
## Test Results
✅ All 370 unit tests passing
✅ No behavioral changes
✅ Full backward compatibility
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Replace EventEmitter callbacks in WaveSurfer with reactive effect() subscriptions for all renderer events. This completes the reactive refactoring by making WaveSurfer's internal event handling fully reactive while maintaining backward compatibility with the public EventEmitter API. Changes: - Refactored initRendererEvents() to use effect() instead of .on() callbacks - All renderer events now use reactive streams: click$, dblclick$, scroll, drag$, render$, rendered$, resize$ - Made Renderer.scrollStream public to expose scroll percentages/bounds - Updated test mock to include all reactive stream properties - Public EventEmitter API unchanged for backward compatibility Benefits: - Fully reactive architecture end-to-end - Better composability and testability - Automatic cleanup via effect() unsubscribe - Consistent with reactive state management pattern All tests passing (386 passed). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
The Timeline plugin was only rendering notches in the first screen.
When scrolling, notches were not appearing.
Root cause: The reactive effect was subscribed to scrollStream.bounds,
but the effect's dependency array only included the signal itself, not
tracking the nested .value property correctly. This prevented the effect
from triggering when scroll bounds changed.
Fix:
1. Use wavesurfer.on('scroll') event directly instead of reactive stream
2. Ensure timeline is appended to DOM before measuring notch widths
3. Set timeline width to 100% to match parent
This restores the same scroll handling pattern as commit 31765ad while
keeping the virtual DOM optimization for notch rendering.
Add automatic seeking behavior when clicking the minimap. When users click on the minimap, the main waveform now seeks to the corresponding time position. This provides intuitive navigation similar to typical audio editor minimaps.
- Only hide cursor (cursorWidth = 0) in scrolling waveform mode - Cursor now visible during recording in continuous waveform and default modes - Fixes regression from commit 27be88e where cursor was hidden in all modes
Created test page to debug cursor visibility during recording. Tests continuous, scrolling, and default recording modes. Shows cursor element properties in real-time for debugging.
Documents the cursor visibility issue, applied fix, and need for DeclarativeRenderer migration. Includes testing steps and references.
Created detailed migration plan to replace old Renderer cursor/progress with DeclarativeRenderer. Tasks 1, 3, and 4 are ready to start. Tasks created: - wavesurfer.js-mdo: Extract canvas rendering - wavesurfer.js-2v4: Integrate DeclarativeRenderer - wavesurfer.js-awz: Enhance DeclarativeRenderer - wavesurfer.js-0jp: Wire up state - wavesurfer.js-358: Update plugins - wavesurfer.js-x8h: Integration tests Total effort: 3-4 weeks
The cursor fix attempt in Record plugin didn't work. The real solution requires migrating to DeclarativeRenderer (see MIGRATION_PLAN.md). Reverted changes from commit 32f66e3.
- Created src/renderer/canvas-renderer.ts with isolated waveform rendering logic - CanvasRenderer handles all canvas-specific rendering without cursor/progress UI dependencies - Old Renderer now delegates waveform rendering to CanvasRenderer - Removed duplicate rendering methods from Renderer (renderBarWaveform, renderLineWaveform, renderWaveform) - Extracted CanvasRenderOptions interface with only rendering-related options - All tests passing, ready for DeclarativeRenderer integration Task: wavesurfer.js-mdo
- Added setOptions() method for dynamic option updates - Support for cursorWidth = 0 to hide cursor - Implemented handleAutoScroll() for auto-scroll and auto-center during playback - Added setScrollable() method to control overflow behavior - Added hideScrollbar option with proper CSS styling - Reactive effects now handle auto-scroll based on playback state - All cursor/progress options can be updated dynamically Task: wavesurfer.js-awz
- Updated Renderer constructor to accept optional WaveSurferState parameter - WaveSurfer now passes state to Renderer on creation - Renderer stores state reference for future DeclarativeRenderer integration - Maintains backward compatibility - state parameter is optional - All tests passing Task: wavesurfer.js-0jp
- Renderer now uses reactive cursor and progress components when WaveSurferState is provided - Created setupReactiveCursorProgress() for reactive UI updates - Reactive effects automatically update cursor/progress based on state changes - Old cursor/progress elements hidden when reactive components are used - Maintains backward compatibility - legacy rendering when no state provided - Auto-scroll handled reactively during playback - All tests passing Task: wavesurfer.js-2v4
…ates - Record plugin now calls setOptions() instead of directly mutating options - Ensures cursor visibility changes trigger reactive component updates - cursorWidth=0 in scrollingWaveform mode now properly hides reactive cursor - Maintains backward compatibility and proper option restoration - All tests passing Task: wavesurfer.js-358
- Replaced makeDraggable with createDragStream for region dragging - Replaced makeDraggable with createDragStream for resize handles - Converted click/dblclick/mouseenter/mouseleave to fromEvent streams - More declarative interaction handling with reactive streams - Smoother drag/resize performance with stream-based approach - All tests passing Task: wavesurfer.js-4xj
- Created src/core/audio-processing.ts with pure functions - extractPeaks(): Extract peak values from AudioBuffer - normalizeAudioData(): Normalize peaks to 0-1 range - calculateWaveformPoints(): Calculate render points from peaks - splitChannelData(): Split multi-channel data for rendering - Utility functions: extractChannelData, findMaxAmplitude, needsNormalization, normalizeChannelData - All functions are pure (no side effects) and easily testable - Can be used in Web Workers for performance Task: wavesurfer.js-l1e
- Created src/core/calculations.ts with pure calculation functions - Time/Progress: calculateProgress, calculateTimeFromProgress, clampTime - Zoom: calculateZoomWidth, calculateMinPxPerSec - Scroll: calculateScrollPercentages, calculateScrollPosition - Canvas: calculateCanvasSize - Position: getRelativePointerPosition, clampToUnit, clamp - Layout: calculateWaveformLayout, pixelToTime, timeToPixel - All functions are pure (no side effects), type-safe, and well-documented - Includes JSDoc examples for easy testing Task: wavesurfer.js-dj5
- Created src/core/audio-loader.ts with pure audio loading logic - loadAudio(): Pure async function that returns AudioData without state mutations - fetchWithProgress(): Fetch blob with progress tracking - decodeAudioData(): Decode audio to AudioBuffer - createBufferFromPeaks(): Create mock buffer from pre-decoded peaks - loadAudioFromMediaElement(): Extract data from media element - Clean separation between data loading and state management - Easy to test without WaveSurfer instance - All functions handle errors gracefully Task: wavesurfer.js-1hq
- Updated renderer tests to use canvasRenderer instance - Tests now properly access CanvasRenderer methods - Canvas context setup corrected with width/height - All unit tests passing (426 passed, 4 skipped)
- Fixed scrollIntoView to work when using reactive cursor/progress components - renderProgress now always handles scrolling for explicit calls (e.g., setTime) - Removed duplicate scrollIntoView call from reactive effect to prevent conflicts - All e2e tests now passing (80 passing, 11 pending)
Progress was rendering as solid fill instead of waveform. The Progress component should be a clipping container (overflow:hidden) that holds the progress waveform canvases, not a colored overlay. Progress color is handled by CanvasRenderer when creating the progress canvases. - Remove backgroundColor from Progress component - Remove unused color prop from ProgressProps interface - Update Progress component instantiations to not pass color - Update tests to verify no backgroundColor exists - Update JSDoc to describe component as clipping container
When wavesurferState exists, progress waveform canvases were being appended to the hidden progressWrapper instead of the reactive Progress component's inner div, causing progress to appear empty. - Append progress canvases to Progress component's inner div - Clear Progress component's inner div on re-render - Update tests to reflect Progress has no backgroundColor - All 426 jest tests passing, 90/91 cypress tests passing The umd.cy.js failure is pre-existing and unrelated to this fix.
Progress animation was choppy because updates were double-batched: 1. Animation loop runs at 60fps via requestAnimationFrame 2. Effect schedules another requestAnimationFrame via scheduleRender This caused dropped frames. Now uses 'high' priority during playback which bypasses batching and renders immediately, syncing with the animation loop for smooth 60fps progress updates. - Use high priority when isPlaying is true - Keep normal priority when paused (for batching efficiency) - Apply fix to both Renderer and DeclarativeRenderer
Fixed two issues: 1. Choppy progress: During playback, progressPercent changes happen inside the animation loop's RAF callback. Scheduling another RAF causes double-batching and dropped frames. Now renders immediately when isPlaying is true (already in RAF), and only uses RAF batching when paused for efficiency. 2. Cursor overflow: At 100% position, cursor would extend beyond the right edge by its width. Now uses translateX(-width/2) to center the cursor on its position, preventing overflow at both edges. - Render immediately during playback (no RAF scheduling) - Batch with RAF when paused - Center cursor with translateX transform - Apply fixes to both Renderer and DeclarativeRenderer
The animation loop was calling renderer.renderProgress() directly instead of updating reactive state, bypassing the reactive rendering system. This caused choppy progress because: 1. renderProgress() was scheduling yet another RAF 2. State wasn't being updated, so reactive effects didn't fire properly 3. The timing was inconsistent between imperative and reactive updates Now the animation loop updates state with actions.setCurrentTime(), which triggers reactive effects that update cursor/progress smoothly within the same animation frame. Changes: - Animation loop updates state instead of calling renderProgress - setTime() updates state instead of calling updateProgress - Update test to verify state update instead of renderProgress call - Keep updateProgress() for backward compatibility
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
🎉 WaveSurfer.js v8.0.0 - Complete Reactive Architecture
This PR introduces a complete internal refactoring to a reactive architecture while maintaining 100% backward compatibility with the v7.x API.
Preview: https://reactive-signals.wavesurfer-js.pages.dev/
✨ Key Features
📊 Statistics
🏗️ Architecture Changes
Phase 1: Reactive Foundation ✅
src/reactive/store.ts)src/state/wavesurfer-state.ts)Phase 2: Declarative Rendering ✅
src/renderer/declarative-renderer.ts)src/renderer/waveform-renderer.ts)Phase 3: Event Streams ✅
src/reactive/event-streams.ts)src/reactive/scroll-stream.ts)src/reactive/drag-stream.ts)Phase 4: Reactive Core ✅
src/player.ts)src/renderer.ts)Phase 5: Plugin Migration & Testing ✅
getRenderer()🔄 Migrated Plugins
renderer.scrollStreamandrenderer.resize$renderer.rendered$andscrollStream.boundsrenderer.rendered$andscrollStream.percentagesrenderer.rendered$for automatic redraw💯 Backward Compatibility
This is a non-breaking release! All existing code continues to work:
.on(),.once(),.emit())🆕 Optional New APIs
Advanced users can optionally leverage new reactive features:
🧪 Testing
📝 Files Changed
New Modules:
src/reactive/*- Complete reactive system (8 modules + tests)src/renderer/declarative-renderer.ts- Component-based renderingsrc/renderer/waveform-renderer.ts- Pure rendering functionssrc/state/wavesurfer-state.ts- Centralized state managementsrc/__tests__/memory-leaks.test.ts- Memory leak detectionModified Core:
src/wavesurfer.ts- Thin shell over reactive coresrc/renderer.ts- Exposes reactive streamssrc/player.ts- Reactive media bridgesrc/plugins/*- 5 plugins migrated to reactive patternsRemoved:
src/timer.ts- Replaced by reactive time updates🔍 Breaking Changes
None! This release is 100% backward compatible.
🏷️ Release Notes
See CHANGELOG.md for comprehensive release notes.
📚 What's Next
Completed for v8.0.0:
Post-release (optional):
🙏 Credits
This major refactoring was completed with assistance from Claude Code, implementing a comprehensive reactive architecture while maintaining full backward compatibility.
🤖 Generated with Claude Code