wavesurfer.js 7.11.0 → 7.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +1 -1
  2. package/dist/__tests__/memory-leaks.test.d.ts +7 -0
  3. package/dist/__tests__/memory-leaks.test.js +184 -0
  4. package/dist/__tests__/player.test.js +107 -0
  5. package/dist/__tests__/renderer-utils.test.d.ts +1 -0
  6. package/dist/__tests__/renderer-utils.test.js +373 -0
  7. package/dist/decoder.js +12 -4
  8. package/dist/draggable.d.ts +4 -0
  9. package/dist/draggable.js +20 -5
  10. package/dist/fetcher.js +14 -23
  11. package/dist/player.d.ts +21 -0
  12. package/dist/player.js +86 -0
  13. package/dist/plugins/envelope.cjs +1 -1
  14. package/dist/plugins/envelope.esm.js +1 -1
  15. package/dist/plugins/envelope.js +1 -1
  16. package/dist/plugins/envelope.min.js +1 -1
  17. package/dist/plugins/hover.cjs +1 -1
  18. package/dist/plugins/hover.d.ts +0 -3
  19. package/dist/plugins/hover.esm.js +1 -1
  20. package/dist/plugins/hover.js +1 -1
  21. package/dist/plugins/hover.min.js +1 -1
  22. package/dist/plugins/minimap.cjs +1 -1
  23. package/dist/plugins/minimap.esm.js +1 -1
  24. package/dist/plugins/minimap.js +1 -1
  25. package/dist/plugins/minimap.min.js +1 -1
  26. package/dist/plugins/record.cjs +1 -1
  27. package/dist/plugins/record.esm.js +1 -1
  28. package/dist/plugins/record.js +1 -1
  29. package/dist/plugins/record.min.js +1 -1
  30. package/dist/plugins/regions.cjs +1 -1
  31. package/dist/plugins/regions.esm.js +1 -1
  32. package/dist/plugins/regions.js +1 -1
  33. package/dist/plugins/regions.min.js +1 -1
  34. package/dist/plugins/timeline.cjs +1 -1
  35. package/dist/plugins/timeline.d.ts +1 -1
  36. package/dist/plugins/timeline.esm.js +1 -1
  37. package/dist/plugins/timeline.js +1 -1
  38. package/dist/plugins/timeline.min.js +1 -1
  39. package/dist/plugins/zoom.cjs +1 -1
  40. package/dist/plugins/zoom.d.ts +10 -1
  41. package/dist/plugins/zoom.esm.js +1 -1
  42. package/dist/plugins/zoom.js +1 -1
  43. package/dist/plugins/zoom.min.js +1 -1
  44. package/dist/reactive/__tests__/drag-stream.test.d.ts +1 -0
  45. package/dist/reactive/__tests__/drag-stream.test.js +199 -0
  46. package/dist/reactive/__tests__/event-stream-emitter.test.d.ts +1 -0
  47. package/dist/reactive/__tests__/event-stream-emitter.test.js +230 -0
  48. package/dist/reactive/__tests__/event-streams.test.d.ts +1 -0
  49. package/dist/reactive/__tests__/event-streams.test.js +278 -0
  50. package/dist/reactive/__tests__/media-event-bridge.test.d.ts +1 -0
  51. package/dist/reactive/__tests__/media-event-bridge.test.js +202 -0
  52. package/dist/reactive/__tests__/render-scheduler.test.d.ts +1 -0
  53. package/dist/reactive/__tests__/render-scheduler.test.js +216 -0
  54. package/dist/reactive/__tests__/scroll-stream.test.d.ts +1 -0
  55. package/dist/reactive/__tests__/scroll-stream.test.js +191 -0
  56. package/dist/reactive/__tests__/state-event-emitter.test.d.ts +1 -0
  57. package/dist/reactive/__tests__/state-event-emitter.test.js +234 -0
  58. package/dist/reactive/__tests__/store.test.d.ts +1 -0
  59. package/dist/reactive/__tests__/store.test.js +271 -0
  60. package/dist/reactive/drag-stream.d.ts +48 -0
  61. package/dist/reactive/drag-stream.js +146 -0
  62. package/dist/reactive/event-stream-emitter.d.ts +104 -0
  63. package/dist/reactive/event-stream-emitter.js +151 -0
  64. package/dist/reactive/event-streams.d.ts +65 -0
  65. package/dist/reactive/event-streams.js +146 -0
  66. package/dist/reactive/media-event-bridge.d.ts +51 -0
  67. package/dist/reactive/media-event-bridge.js +152 -0
  68. package/dist/reactive/render-scheduler.d.ts +43 -0
  69. package/dist/reactive/render-scheduler.js +75 -0
  70. package/dist/reactive/scroll-stream.d.ts +93 -0
  71. package/dist/reactive/scroll-stream.js +131 -0
  72. package/dist/reactive/state-event-emitter.d.ts +105 -0
  73. package/dist/reactive/state-event-emitter.js +213 -0
  74. package/dist/reactive/store.d.ts +66 -0
  75. package/dist/reactive/store.js +115 -0
  76. package/dist/renderer-utils.d.ts +126 -0
  77. package/dist/renderer-utils.js +252 -0
  78. package/dist/renderer.d.ts +4 -4
  79. package/dist/renderer.js +133 -197
  80. package/dist/state/__tests__/wavesurfer-state.test.d.ts +1 -0
  81. package/dist/state/__tests__/wavesurfer-state.test.js +268 -0
  82. package/dist/state/wavesurfer-state.d.ts +90 -0
  83. package/dist/state/wavesurfer-state.js +124 -0
  84. package/dist/types.d.ts +167 -1
  85. package/dist/wavesurfer.cjs +1 -1
  86. package/dist/wavesurfer.d.ts +18 -0
  87. package/dist/wavesurfer.esm.js +1 -1
  88. package/dist/wavesurfer.js +46 -8
  89. package/dist/wavesurfer.min.js +1 -1
  90. package/dist/webaudio.js +4 -0
  91. package/package.json +6 -6
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Reactive scroll stream utilities
3
+ *
4
+ * Provides declarative scroll handling using reactive streams.
5
+ * Automatically handles scroll event optimization and cleanup.
6
+ */
7
+ import { type Signal } from './store.js';
8
+ export interface ScrollData {
9
+ /** Current scroll position in pixels */
10
+ scrollLeft: number;
11
+ /** Total scrollable width in pixels */
12
+ scrollWidth: number;
13
+ /** Visible viewport width in pixels */
14
+ clientWidth: number;
15
+ }
16
+ export interface ScrollPercentages {
17
+ /** Start position as percentage (0-1) */
18
+ startX: number;
19
+ /** End position as percentage (0-1) */
20
+ endX: number;
21
+ }
22
+ /**
23
+ * Calculate visible percentages from scroll data
24
+ * Pure function - no side effects
25
+ *
26
+ * @param scrollData - Current scroll dimensions
27
+ * @returns Start and end positions as percentages (0-1)
28
+ */
29
+ export declare function calculateScrollPercentages(scrollData: ScrollData): ScrollPercentages;
30
+ /**
31
+ * Calculate scroll bounds in pixels
32
+ * Pure function - no side effects
33
+ *
34
+ * @param scrollData - Current scroll dimensions
35
+ * @returns Left and right scroll bounds in pixels
36
+ */
37
+ export declare function calculateScrollBounds(scrollData: ScrollData): {
38
+ left: number;
39
+ right: number;
40
+ };
41
+ export interface ScrollStream {
42
+ /** Signal containing current scroll data */
43
+ scrollData: Signal<ScrollData>;
44
+ /** Computed signal with visible percentages */
45
+ percentages: Signal<ScrollPercentages>;
46
+ /** Computed signal with scroll bounds */
47
+ bounds: Signal<{
48
+ left: number;
49
+ right: number;
50
+ }>;
51
+ /** Cleanup function to remove listeners */
52
+ cleanup: () => void;
53
+ }
54
+ /**
55
+ * Create a reactive scroll stream from an element
56
+ *
57
+ * Emits scroll data as the user scrolls the element.
58
+ * Automatically computes derived values (percentages, bounds).
59
+ *
60
+ * @example
61
+ * ```typescript
62
+ * const scrollStream = createScrollStream(container)
63
+ *
64
+ * effect(() => {
65
+ * const { startX, endX } = scrollStream.percentages.value
66
+ * console.log('Visible:', startX, 'to', endX)
67
+ * }, [scrollStream.percentages])
68
+ *
69
+ * scrollStream.cleanup()
70
+ * ```
71
+ *
72
+ * @param element - Scrollable element
73
+ * @returns Scroll stream with signals and cleanup
74
+ */
75
+ export declare function createScrollStream(element: HTMLElement): ScrollStream;
76
+ /**
77
+ * Create a scroll stream that automatically updates external state
78
+ *
79
+ * This is a convenience wrapper that connects scroll events to a state action.
80
+ *
81
+ * @example
82
+ * ```typescript
83
+ * const scrollStream = createScrollStreamWithAction(
84
+ * container,
85
+ * (scrollLeft) => actions.setScrollPosition(scrollLeft)
86
+ * )
87
+ * ```
88
+ *
89
+ * @param element - Scrollable element
90
+ * @param onScrollChange - Action to call when scroll changes
91
+ * @returns Scroll stream with signals and cleanup
92
+ */
93
+ export declare function createScrollStreamWithAction(element: HTMLElement, onScrollChange: (scrollLeft: number) => void): ScrollStream;
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Reactive scroll stream utilities
3
+ *
4
+ * Provides declarative scroll handling using reactive streams.
5
+ * Automatically handles scroll event optimization and cleanup.
6
+ */
7
+ import { signal, computed, effect } from './store.js';
8
+ import { cleanup } from './event-streams.js';
9
+ // ============================================================================
10
+ // Pure Scroll Calculation Functions
11
+ // ============================================================================
12
+ /**
13
+ * Calculate visible percentages from scroll data
14
+ * Pure function - no side effects
15
+ *
16
+ * @param scrollData - Current scroll dimensions
17
+ * @returns Start and end positions as percentages (0-1)
18
+ */
19
+ export function calculateScrollPercentages(scrollData) {
20
+ const { scrollLeft, scrollWidth, clientWidth } = scrollData;
21
+ if (scrollWidth === 0) {
22
+ return { startX: 0, endX: 1 };
23
+ }
24
+ const startX = scrollLeft / scrollWidth;
25
+ const endX = (scrollLeft + clientWidth) / scrollWidth;
26
+ return {
27
+ startX: Math.max(0, Math.min(1, startX)),
28
+ endX: Math.max(0, Math.min(1, endX)),
29
+ };
30
+ }
31
+ /**
32
+ * Calculate scroll bounds in pixels
33
+ * Pure function - no side effects
34
+ *
35
+ * @param scrollData - Current scroll dimensions
36
+ * @returns Left and right scroll bounds in pixels
37
+ */
38
+ export function calculateScrollBounds(scrollData) {
39
+ return {
40
+ left: scrollData.scrollLeft,
41
+ right: scrollData.scrollLeft + scrollData.clientWidth,
42
+ };
43
+ }
44
+ /**
45
+ * Create a reactive scroll stream from an element
46
+ *
47
+ * Emits scroll data as the user scrolls the element.
48
+ * Automatically computes derived values (percentages, bounds).
49
+ *
50
+ * @example
51
+ * ```typescript
52
+ * const scrollStream = createScrollStream(container)
53
+ *
54
+ * effect(() => {
55
+ * const { startX, endX } = scrollStream.percentages.value
56
+ * console.log('Visible:', startX, 'to', endX)
57
+ * }, [scrollStream.percentages])
58
+ *
59
+ * scrollStream.cleanup()
60
+ * ```
61
+ *
62
+ * @param element - Scrollable element
63
+ * @returns Scroll stream with signals and cleanup
64
+ */
65
+ export function createScrollStream(element) {
66
+ // Create signals
67
+ const scrollData = signal({
68
+ scrollLeft: element.scrollLeft,
69
+ scrollWidth: element.scrollWidth,
70
+ clientWidth: element.clientWidth,
71
+ });
72
+ // Computed derived values
73
+ const percentages = computed(() => {
74
+ return calculateScrollPercentages(scrollData.value);
75
+ }, [scrollData]);
76
+ const bounds = computed(() => {
77
+ return calculateScrollBounds(scrollData.value);
78
+ }, [scrollData]);
79
+ // Update scroll data on scroll event
80
+ const onScroll = () => {
81
+ scrollData.set({
82
+ scrollLeft: element.scrollLeft,
83
+ scrollWidth: element.scrollWidth,
84
+ clientWidth: element.clientWidth,
85
+ });
86
+ };
87
+ // Attach scroll listener
88
+ element.addEventListener('scroll', onScroll, { passive: true });
89
+ // Cleanup function
90
+ const cleanupFn = () => {
91
+ element.removeEventListener('scroll', onScroll);
92
+ cleanup(scrollData);
93
+ };
94
+ return {
95
+ scrollData,
96
+ percentages,
97
+ bounds,
98
+ cleanup: cleanupFn,
99
+ };
100
+ }
101
+ /**
102
+ * Create a scroll stream that automatically updates external state
103
+ *
104
+ * This is a convenience wrapper that connects scroll events to a state action.
105
+ *
106
+ * @example
107
+ * ```typescript
108
+ * const scrollStream = createScrollStreamWithAction(
109
+ * container,
110
+ * (scrollLeft) => actions.setScrollPosition(scrollLeft)
111
+ * )
112
+ * ```
113
+ *
114
+ * @param element - Scrollable element
115
+ * @param onScrollChange - Action to call when scroll changes
116
+ * @returns Scroll stream with signals and cleanup
117
+ */
118
+ export function createScrollStreamWithAction(element, onScrollChange) {
119
+ const stream = createScrollStream(element);
120
+ // Effect to update external state
121
+ const unsubscribe = effect(() => {
122
+ onScrollChange(stream.scrollData.value.scrollLeft);
123
+ }, [stream.scrollData]);
124
+ // Wrap cleanup to include effect cleanup
125
+ const originalCleanup = stream.cleanup;
126
+ stream.cleanup = () => {
127
+ unsubscribe();
128
+ originalCleanup();
129
+ };
130
+ return stream;
131
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * State-driven event emission utilities
3
+ *
4
+ * Automatically emit events when reactive state changes.
5
+ * Ensures events are always in sync with state and removes manual emit() calls.
6
+ */
7
+ import { type Signal } from './store.js';
8
+ import type { WaveSurferState } from '../state/wavesurfer-state.js';
9
+ export type EventEmitter = {
10
+ emit(event: string, ...args: any[]): void;
11
+ };
12
+ /**
13
+ * Setup automatic event emission from state changes
14
+ *
15
+ * This function subscribes to all relevant state signals and automatically
16
+ * emits corresponding events when state changes. This ensures:
17
+ * - Events are always in sync with state
18
+ * - No manual emit() calls needed
19
+ * - Can't forget to emit an event
20
+ * - Clear event sources (state changes)
21
+ *
22
+ * @example
23
+ * ```typescript
24
+ * const { state } = createWaveSurferState()
25
+ * const wavesurfer = new WaveSurfer()
26
+ *
27
+ * const cleanup = setupStateEventEmission(state, wavesurfer)
28
+ *
29
+ * // Now state changes automatically emit events
30
+ * state.isPlaying.set(true) // → wavesurfer.emit('play')
31
+ * ```
32
+ *
33
+ * @param state - Reactive state to observe
34
+ * @param emitter - Event emitter to emit events on
35
+ * @returns Cleanup function that removes all subscriptions
36
+ */
37
+ export declare function setupStateEventEmission(state: WaveSurferState, emitter: EventEmitter): () => void;
38
+ /**
39
+ * Setup custom event emission from signal changes
40
+ *
41
+ * This is a lower-level utility for setting up custom event emission
42
+ * from any signal. Useful when you need more control over event emission logic.
43
+ *
44
+ * @example
45
+ * ```typescript
46
+ * const volumeSignal = signal(1)
47
+ *
48
+ * const cleanup = setupSignalEventEmission(
49
+ * volumeSignal,
50
+ * emitter,
51
+ * (volume) => ['volume', volume]
52
+ * )
53
+ * ```
54
+ *
55
+ * @param signal - Signal to observe
56
+ * @param emitter - Event emitter
57
+ * @param getEventData - Function that returns [eventName, ...args]
58
+ * @returns Cleanup function
59
+ */
60
+ export declare function setupSignalEventEmission<T>(signal: Signal<T>, emitter: EventEmitter, getEventData: (value: T) => [string, ...any[]]): () => void;
61
+ /**
62
+ * Setup event emission with debouncing
63
+ *
64
+ * Useful for high-frequency events like scroll or timeupdate.
65
+ *
66
+ * @example
67
+ * ```typescript
68
+ * const cleanup = setupDebouncedEventEmission(
69
+ * state.scrollPosition,
70
+ * emitter,
71
+ * (pos) => ['scroll', pos],
72
+ * 100 // debounce 100ms
73
+ * )
74
+ * ```
75
+ *
76
+ * @param signal - Signal to observe
77
+ * @param emitter - Event emitter
78
+ * @param getEventData - Function that returns [eventName, ...args]
79
+ * @param debounceMs - Debounce delay in milliseconds
80
+ * @returns Cleanup function
81
+ */
82
+ export declare function setupDebouncedEventEmission<T>(signal: Signal<T>, emitter: EventEmitter, getEventData: (value: T) => [string, ...any[]], debounceMs: number): () => void;
83
+ /**
84
+ * Setup conditional event emission
85
+ *
86
+ * Only emit events when a condition is met.
87
+ *
88
+ * @example
89
+ * ```typescript
90
+ * // Only emit finish event when playing stops at end
91
+ * const cleanup = setupConditionalEventEmission(
92
+ * state.isPlaying,
93
+ * emitter,
94
+ * (isPlaying) => !isPlaying && state.currentTime.value >= state.duration.value,
95
+ * () => ['finish']
96
+ * )
97
+ * ```
98
+ *
99
+ * @param signal - Signal to observe
100
+ * @param emitter - Event emitter
101
+ * @param condition - Function that returns true when event should emit
102
+ * @param getEventData - Function that returns [eventName, ...args]
103
+ * @returns Cleanup function
104
+ */
105
+ export declare function setupConditionalEventEmission<T>(signal: Signal<T>, emitter: EventEmitter, condition: (value: T) => boolean, getEventData: (value: T) => [string, ...any[]]): () => void;
@@ -0,0 +1,213 @@
1
+ /**
2
+ * State-driven event emission utilities
3
+ *
4
+ * Automatically emit events when reactive state changes.
5
+ * Ensures events are always in sync with state and removes manual emit() calls.
6
+ */
7
+ import { effect } from './store.js';
8
+ /**
9
+ * Setup automatic event emission from state changes
10
+ *
11
+ * This function subscribes to all relevant state signals and automatically
12
+ * emits corresponding events when state changes. This ensures:
13
+ * - Events are always in sync with state
14
+ * - No manual emit() calls needed
15
+ * - Can't forget to emit an event
16
+ * - Clear event sources (state changes)
17
+ *
18
+ * @example
19
+ * ```typescript
20
+ * const { state } = createWaveSurferState()
21
+ * const wavesurfer = new WaveSurfer()
22
+ *
23
+ * const cleanup = setupStateEventEmission(state, wavesurfer)
24
+ *
25
+ * // Now state changes automatically emit events
26
+ * state.isPlaying.set(true) // → wavesurfer.emit('play')
27
+ * ```
28
+ *
29
+ * @param state - Reactive state to observe
30
+ * @param emitter - Event emitter to emit events on
31
+ * @returns Cleanup function that removes all subscriptions
32
+ */
33
+ export function setupStateEventEmission(state, emitter) {
34
+ const cleanups = [];
35
+ // ============================================================================
36
+ // Play/Pause Events
37
+ // ============================================================================
38
+ // Emit play/pause events when playing state changes
39
+ cleanups.push(effect(() => {
40
+ const isPlaying = state.isPlaying.value;
41
+ emitter.emit(isPlaying ? 'play' : 'pause');
42
+ }, [state.isPlaying]));
43
+ // ============================================================================
44
+ // Time Update Events
45
+ // ============================================================================
46
+ // Emit timeupdate when current time changes
47
+ cleanups.push(effect(() => {
48
+ const currentTime = state.currentTime.value;
49
+ emitter.emit('timeupdate', currentTime);
50
+ // Also emit audioprocess when playing
51
+ if (state.isPlaying.value) {
52
+ emitter.emit('audioprocess', currentTime);
53
+ }
54
+ }, [state.currentTime, state.isPlaying]));
55
+ // ============================================================================
56
+ // Seeking Events
57
+ // ============================================================================
58
+ // Emit seeking event when seeking state changes to true
59
+ cleanups.push(effect(() => {
60
+ const isSeeking = state.isSeeking.value;
61
+ if (isSeeking) {
62
+ emitter.emit('seeking', state.currentTime.value);
63
+ }
64
+ }, [state.isSeeking, state.currentTime]));
65
+ // ============================================================================
66
+ // Ready Event
67
+ // ============================================================================
68
+ // Emit ready when state becomes ready
69
+ let wasReady = false;
70
+ cleanups.push(effect(() => {
71
+ const isReady = state.isReady.value;
72
+ if (isReady && !wasReady) {
73
+ wasReady = true;
74
+ emitter.emit('ready', state.duration.value);
75
+ }
76
+ }, [state.isReady, state.duration]));
77
+ // ============================================================================
78
+ // Finish Event
79
+ // ============================================================================
80
+ // Emit finish when playback ends (reached duration and stopped)
81
+ let wasPlayingAtEnd = false;
82
+ cleanups.push(effect(() => {
83
+ const isPlaying = state.isPlaying.value;
84
+ const currentTime = state.currentTime.value;
85
+ const duration = state.duration.value;
86
+ // Check if we're at the end
87
+ const isAtEnd = duration > 0 && currentTime >= duration;
88
+ // Emit finish when we were playing at end and now stopped
89
+ if (wasPlayingAtEnd && !isPlaying && isAtEnd) {
90
+ emitter.emit('finish');
91
+ }
92
+ // Track if we're playing at the end
93
+ wasPlayingAtEnd = isPlaying && isAtEnd;
94
+ }, [state.isPlaying, state.currentTime, state.duration]));
95
+ // ============================================================================
96
+ // Zoom Events
97
+ // ============================================================================
98
+ // Emit zoom when zoom level changes
99
+ cleanups.push(effect(() => {
100
+ const zoom = state.zoom.value;
101
+ if (zoom > 0) {
102
+ emitter.emit('zoom', zoom);
103
+ }
104
+ }, [state.zoom]));
105
+ // Return cleanup function
106
+ return () => {
107
+ cleanups.forEach((cleanup) => cleanup());
108
+ };
109
+ }
110
+ /**
111
+ * Setup custom event emission from signal changes
112
+ *
113
+ * This is a lower-level utility for setting up custom event emission
114
+ * from any signal. Useful when you need more control over event emission logic.
115
+ *
116
+ * @example
117
+ * ```typescript
118
+ * const volumeSignal = signal(1)
119
+ *
120
+ * const cleanup = setupSignalEventEmission(
121
+ * volumeSignal,
122
+ * emitter,
123
+ * (volume) => ['volume', volume]
124
+ * )
125
+ * ```
126
+ *
127
+ * @param signal - Signal to observe
128
+ * @param emitter - Event emitter
129
+ * @param getEventData - Function that returns [eventName, ...args]
130
+ * @returns Cleanup function
131
+ */
132
+ export function setupSignalEventEmission(signal, emitter, getEventData) {
133
+ return effect(() => {
134
+ const value = signal.value;
135
+ const [eventName, ...args] = getEventData(value);
136
+ emitter.emit(eventName, ...args);
137
+ }, [signal]);
138
+ }
139
+ /**
140
+ * Setup event emission with debouncing
141
+ *
142
+ * Useful for high-frequency events like scroll or timeupdate.
143
+ *
144
+ * @example
145
+ * ```typescript
146
+ * const cleanup = setupDebouncedEventEmission(
147
+ * state.scrollPosition,
148
+ * emitter,
149
+ * (pos) => ['scroll', pos],
150
+ * 100 // debounce 100ms
151
+ * )
152
+ * ```
153
+ *
154
+ * @param signal - Signal to observe
155
+ * @param emitter - Event emitter
156
+ * @param getEventData - Function that returns [eventName, ...args]
157
+ * @param debounceMs - Debounce delay in milliseconds
158
+ * @returns Cleanup function
159
+ */
160
+ export function setupDebouncedEventEmission(signal, emitter, getEventData, debounceMs) {
161
+ let timeoutId = null;
162
+ const cleanup = effect(() => {
163
+ const value = signal.value;
164
+ // Clear previous timeout
165
+ if (timeoutId !== null) {
166
+ clearTimeout(timeoutId);
167
+ }
168
+ // Set new timeout
169
+ timeoutId = setTimeout(() => {
170
+ const [eventName, ...args] = getEventData(value);
171
+ emitter.emit(eventName, ...args);
172
+ timeoutId = null;
173
+ }, debounceMs);
174
+ }, [signal]);
175
+ // Return cleanup that also clears pending timeout
176
+ return () => {
177
+ if (timeoutId !== null) {
178
+ clearTimeout(timeoutId);
179
+ }
180
+ cleanup();
181
+ };
182
+ }
183
+ /**
184
+ * Setup conditional event emission
185
+ *
186
+ * Only emit events when a condition is met.
187
+ *
188
+ * @example
189
+ * ```typescript
190
+ * // Only emit finish event when playing stops at end
191
+ * const cleanup = setupConditionalEventEmission(
192
+ * state.isPlaying,
193
+ * emitter,
194
+ * (isPlaying) => !isPlaying && state.currentTime.value >= state.duration.value,
195
+ * () => ['finish']
196
+ * )
197
+ * ```
198
+ *
199
+ * @param signal - Signal to observe
200
+ * @param emitter - Event emitter
201
+ * @param condition - Function that returns true when event should emit
202
+ * @param getEventData - Function that returns [eventName, ...args]
203
+ * @returns Cleanup function
204
+ */
205
+ export function setupConditionalEventEmission(signal, emitter, condition, getEventData) {
206
+ return effect(() => {
207
+ const value = signal.value;
208
+ if (condition(value)) {
209
+ const [eventName, ...args] = getEventData(value);
210
+ emitter.emit(eventName, ...args);
211
+ }
212
+ }, [signal]);
213
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Reactive primitives for managing state in WaveSurfer
3
+ *
4
+ * This module provides signal-based reactivity similar to SolidJS signals.
5
+ * Signals are reactive values that notify subscribers when they change.
6
+ */
7
+ /**
8
+ * A reactive value that can be read and subscribed to
9
+ */
10
+ export interface Signal<T> {
11
+ /** Get the current value */
12
+ get value(): T;
13
+ /** Subscribe to changes. Returns an unsubscribe function. */
14
+ subscribe(callback: (value: T) => void): () => void;
15
+ }
16
+ /**
17
+ * A writable reactive value that can be updated
18
+ */
19
+ export interface WritableSignal<T> extends Signal<T> {
20
+ /** Set a new value. Only notifies if value changed. */
21
+ set(value: T): void;
22
+ /** Update value using a function. */
23
+ update(fn: (current: T) => T): void;
24
+ }
25
+ /**
26
+ * Create a reactive signal that notifies subscribers when its value changes
27
+ *
28
+ * @example
29
+ * ```typescript
30
+ * const count = signal(0)
31
+ * count.subscribe(val => console.log('Count:', val))
32
+ * count.set(5) // Logs: Count: 5
33
+ * ```
34
+ */
35
+ export declare function signal<T>(initialValue: T): WritableSignal<T>;
36
+ /**
37
+ * Create a computed value that automatically updates when its dependencies change
38
+ *
39
+ * @example
40
+ * ```typescript
41
+ * const count = signal(0)
42
+ * const doubled = computed(() => count.value * 2, [count])
43
+ * console.log(doubled.value) // 0
44
+ * count.set(5)
45
+ * console.log(doubled.value) // 10
46
+ * ```
47
+ */
48
+ export declare function computed<T>(fn: () => T, dependencies: Signal<any>[]): Signal<T>;
49
+ /**
50
+ * Run a side effect automatically when dependencies change
51
+ *
52
+ * @param fn - Effect function. Can return a cleanup function.
53
+ * @param dependencies - Signals that trigger the effect when they change
54
+ * @returns Unsubscribe function that stops the effect and runs cleanup
55
+ *
56
+ * @example
57
+ * ```typescript
58
+ * const count = signal(0)
59
+ * effect(() => {
60
+ * console.log('Count is:', count.value)
61
+ * return () => console.log('Cleanup')
62
+ * }, [count])
63
+ * count.set(5) // Logs: Cleanup, Count is: 5
64
+ * ```
65
+ */
66
+ export declare function effect(fn: () => void | (() => void), dependencies: Signal<any>[]): () => void;