@umituz/react-native-sound 1.2.32 → 1.2.33

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 (36) hide show
  1. package/package.json +1 -1
  2. package/src/application/SoundCommands.ts +233 -0
  3. package/src/application/SoundEvents.ts +101 -0
  4. package/src/application/SoundService.ts +185 -0
  5. package/src/application/interfaces/IAudioService.ts +1 -1
  6. package/src/application/interfaces/ISoundCache.ts +4 -6
  7. package/src/domain/value-objects.ts +128 -0
  8. package/src/index.ts +35 -26
  9. package/src/infrastructure/AudioConfig.ts +0 -1
  10. package/src/infrastructure/AudioRepository.ts +216 -0
  11. package/src/presentation/SoundStore.ts +156 -0
  12. package/src/presentation/useSound.ts +183 -0
  13. package/src/types.ts +1 -2
  14. package/src/utils.ts +167 -5
  15. package/src/application/SoundServiceFacade.ts +0 -112
  16. package/src/application/presenters/SoundPresenter.ts +0 -78
  17. package/src/application/use-cases/BaseUseCase.ts +0 -36
  18. package/src/application/use-cases/ClearCacheUseCase.ts +0 -16
  19. package/src/application/use-cases/PauseSoundUseCase.ts +0 -26
  20. package/src/application/use-cases/PlaySoundUseCase.ts +0 -106
  21. package/src/application/use-cases/PreloadSoundUseCase.ts +0 -56
  22. package/src/application/use-cases/ResumeSoundUseCase.ts +0 -25
  23. package/src/application/use-cases/SeekSoundUseCase.ts +0 -26
  24. package/src/application/use-cases/SetRateUseCase.ts +0 -30
  25. package/src/application/use-cases/SetVolumeUseCase.ts +0 -30
  26. package/src/application/use-cases/StopSoundUseCase.ts +0 -27
  27. package/src/domain/entities/SoundState.ts +0 -83
  28. package/src/domain/value-objects/PlaybackPosition.ts +0 -30
  29. package/src/domain/value-objects/Rate.ts +0 -26
  30. package/src/domain/value-objects/SoundId.ts +0 -22
  31. package/src/domain/value-objects/SoundSource.ts +0 -30
  32. package/src/domain/value-objects/Volume.ts +0 -26
  33. package/src/infrastructure/ExpoAudioService.ts +0 -88
  34. package/src/infrastructure/SoundCache.ts +0 -70
  35. package/src/presentation/hooks/useSound.ts +0 -47
  36. package/src/presentation/store.ts +0 -58
package/src/index.ts CHANGED
@@ -1,41 +1,50 @@
1
1
  /**
2
2
  * @umituz/react-native-sound
3
3
  * DDD-based audio management for React Native
4
+ * Optimized architecture with minimal code duplication
4
5
  */
5
6
 
6
- // Public API
7
- export { useSound } from './presentation/hooks/useSound';
8
- export { useSoundStore } from './presentation/store';
7
+ // ===== Public API =====
8
+ export { useSound } from './presentation/useSound';
9
+ export { useSoundStore, setupEventListeners } from './presentation/SoundStore';
9
10
 
10
- // Types
11
+ // ===== Types =====
11
12
  export type { PlaybackOptions, SoundState, SoundSource } from './types';
13
+ export type { PlaybackStatus, PlaybackOptions as ServicePlaybackOptions } from './application/interfaces/IAudioService';
14
+ export type { CachedSound, ISoundCache } from './application/interfaces/ISoundCache';
12
15
 
13
- // Utilities (backward compatibility)
16
+ // ===== Domain (Advanced Usage) =====
17
+ export { SoundError, SoundErrorCode } from './domain/errors/SoundError';
18
+ export {
19
+ SoundId,
20
+ SoundSource as SoundSourceVO,
21
+ Volume,
22
+ Rate,
23
+ PlaybackPosition,
24
+ SoundValueObjects,
25
+ type SoundSourceValue,
26
+ } from './domain/value-objects';
27
+
28
+ // ===== Infrastructure (Testing/Customization) =====
29
+ export { Logger } from './infrastructure/Logger';
30
+ export { AudioConfig } from './infrastructure/AudioConfig';
31
+ export { AudioRepository } from './infrastructure/AudioRepository';
32
+
33
+ // ===== Application (Testing/Customization) =====
34
+ export { SoundService } from './application/SoundService';
35
+ export { SoundCommandProcessor, type SoundCommand, type PlayCommand, type PreloadCommand } from './application/SoundCommands';
36
+ export { SoundEvents, type SoundEvent, type PlaybackStartedEvent } from './application/SoundEvents';
37
+
38
+ // ===== Utilities =====
14
39
  export {
15
40
  clampVolume,
16
41
  clampRate,
17
42
  validateSoundId,
18
43
  isSoundSourceValid,
19
44
  isPlaybackStatusSuccess,
45
+ debounce,
46
+ throttle,
47
+ RateLimiter,
48
+ PeriodicTask,
49
+ AutoGCWeakCache,
20
50
  } from './utils';
21
-
22
- // Domain exports (for advanced usage)
23
- export { SoundError } from './domain/errors/SoundError';
24
- export { SoundId } from './domain/value-objects/SoundId';
25
- export { SoundSource as SoundSourceVO } from './domain/value-objects/SoundSource';
26
- export { Volume } from './domain/value-objects/Volume';
27
- export { Rate } from './domain/value-objects/Rate';
28
- export { PlaybackPosition } from './domain/value-objects/PlaybackPosition';
29
- export { SoundState as SoundStateEntity } from './domain/entities/SoundState';
30
-
31
- // Infrastructure exports (for testing/customization)
32
- export { Logger } from './infrastructure/Logger';
33
- export { AudioConfig } from './infrastructure/AudioConfig';
34
- export { ExpoAudioService } from './infrastructure/ExpoAudioService';
35
- export { SoundCache } from './infrastructure/SoundCache';
36
-
37
- // Application exports (for testing/customization)
38
- export { SoundServiceFacade } from './application/SoundServiceFacade';
39
- export type { IAudioService, PlaybackStatus } from './application/interfaces/IAudioService';
40
- export type { ISoundCache, CachedSound } from './application/interfaces/ISoundCache';
41
- export { SoundPresenter } from './application/presenters/SoundPresenter';
@@ -3,7 +3,6 @@
3
3
  */
4
4
 
5
5
  import { Audio, InterruptionModeIOS, InterruptionModeAndroid } from 'expo-av';
6
- import type { Audio as AudioType } from 'expo-av';
7
6
  import { Logger } from './Logger';
8
7
 
9
8
  export class AudioConfig {
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Audio Repository - Combines Cache + Audio Service
3
+ * Single responsibility: Audio resource management
4
+ */
5
+
6
+ import { Audio } from 'expo-av';
7
+ import type { AVPlaybackStatus } from 'expo-av';
8
+ import type { IAudioService, PlaybackStatus, PlaybackOptions } from '../application/interfaces/IAudioService';
9
+ import type { ISoundCache, CachedSound } from '../application/interfaces/ISoundCache';
10
+ import type { SoundSourceValue } from '../domain/value-objects';
11
+ import { SoundEvents } from '../application/SoundEvents';
12
+ import { Logger } from './Logger';
13
+ import { RateLimiter } from '../utils';
14
+
15
+ // ===== Audio Service Implementation =====
16
+ class ExpoAudioService implements IAudioService {
17
+ async createSound(
18
+ source: SoundSourceValue,
19
+ options: PlaybackOptions,
20
+ onStatusUpdate: (status: PlaybackStatus) => void
21
+ ): Promise<{ sound: Audio.Sound }> {
22
+ const { sound } = await Audio.Sound.createAsync(
23
+ source,
24
+ {
25
+ shouldPlay: options.shouldPlay,
26
+ isLooping: options.isLooping,
27
+ volume: options.volume,
28
+ rate: options.rate,
29
+ positionMillis: options.positionMillis,
30
+ },
31
+ (status) => onStatusUpdate(this.mapStatus(status))
32
+ );
33
+ Logger.debug('Sound created');
34
+ return { sound };
35
+ }
36
+
37
+ async play(sound: unknown): Promise<void> {
38
+ await (sound as Audio.Sound).playAsync();
39
+ }
40
+
41
+ async pause(sound: unknown): Promise<void> {
42
+ await (sound as Audio.Sound).pauseAsync();
43
+ }
44
+
45
+ async stop(sound: unknown): Promise<void> {
46
+ await (sound as Audio.Sound).stopAsync();
47
+ }
48
+
49
+ async unload(sound: unknown): Promise<void> {
50
+ await (sound as Audio.Sound).unloadAsync();
51
+ }
52
+
53
+ async setVolume(sound: unknown, volume: number): Promise<void> {
54
+ await (sound as Audio.Sound).setVolumeAsync(volume);
55
+ }
56
+
57
+ async setRate(sound: unknown, rate: number): Promise<void> {
58
+ await (sound as Audio.Sound).setRateAsync(rate, false);
59
+ }
60
+
61
+ async setPosition(sound: unknown, positionMillis: number): Promise<void> {
62
+ await (sound as Audio.Sound).setStatusAsync({ positionMillis });
63
+ }
64
+
65
+ async getStatus(sound: unknown): Promise<PlaybackStatus | null> {
66
+ const status = await (sound as Audio.Sound).getStatusAsync();
67
+ return status.isLoaded ? this.mapStatus(status) : null;
68
+ }
69
+
70
+ private mapStatus(status: AVPlaybackStatus): PlaybackStatus {
71
+ if (!status.isLoaded) {
72
+ const statusRecord = status as Record<string, unknown>;
73
+ const hasError = 'error' in statusRecord;
74
+ const error = hasError && statusRecord.error instanceof Error ? statusRecord.error.message : undefined;
75
+ return {
76
+ isLoaded: false,
77
+ isPlaying: false,
78
+ isBuffering: false,
79
+ positionMillis: 0,
80
+ durationMillis: 0,
81
+ isLooping: false,
82
+ didJustFinish: false,
83
+ error,
84
+ };
85
+ }
86
+ return {
87
+ isLoaded: true,
88
+ isPlaying: status.isPlaying,
89
+ isBuffering: status.isBuffering,
90
+ positionMillis: status.positionMillis,
91
+ durationMillis: status.durationMillis || 0,
92
+ rate: status.rate,
93
+ isLooping: status.isLooping,
94
+ didJustFinish: status.didJustFinish,
95
+ };
96
+ }
97
+ }
98
+
99
+ // ===== Sound Cache Configuration =====
100
+ const CACHE_CONFIG = {
101
+ MAX_SIZE: 3,
102
+ EXPIRE_MS: 5 * 60 * 1000, // 5 minutes
103
+ CLEAN_INTERVAL_MS: 30 * 1000, // 30 seconds
104
+ } as const;
105
+
106
+ // ===== Sound Cache Implementation =====
107
+ class SoundCache implements ISoundCache {
108
+ private cache = new Map<string, CachedSound>();
109
+ private accessOrder: string[] = [];
110
+ private readonly maxSize = CACHE_CONFIG.MAX_SIZE;
111
+ private readonly expireMs = CACHE_CONFIG.EXPIRE_MS;
112
+ private lastCleanTime = 0;
113
+ private readonly cleanIntervalMs = CACHE_CONFIG.CLEAN_INTERVAL_MS;
114
+ private cleanLimiter = new RateLimiter(this.cleanIntervalMs);
115
+
116
+ constructor(
117
+ private readonly audioService: IAudioService,
118
+ private readonly events: SoundEvents
119
+ ) {}
120
+
121
+ get(id: string): CachedSound | undefined {
122
+ const cached = this.cache.get(id);
123
+ if (cached) {
124
+ this.updateAccessOrder(id);
125
+ }
126
+ return cached;
127
+ }
128
+
129
+ async set(id: string, cached: CachedSound): Promise<void> {
130
+ await this.cleanExpiredThrottled();
131
+ await this.enforceLimit();
132
+ this.cache.set(id, cached);
133
+ this.updateAccessOrder(id);
134
+ Logger.debug(`Cached: ${id}`);
135
+ }
136
+
137
+ async delete(id: string): Promise<void> {
138
+ const cached = this.cache.get(id);
139
+ if (cached) {
140
+ try {
141
+ await this.audioService.unload(cached.sound);
142
+ } catch (error) {
143
+ Logger.warn(`Failed to unload: ${id}`, error);
144
+ }
145
+ }
146
+ this.cache.delete(id);
147
+ this.removeFromAccessOrder(id);
148
+ }
149
+
150
+ has(id: string): boolean {
151
+ return this.cache.has(id);
152
+ }
153
+
154
+ async clear(): Promise<void> {
155
+ const unloadPromises = Array.from(this.cache.values()).map((cached) =>
156
+ this.audioService.unload(cached.sound).catch((error) => {
157
+ Logger.warn('Failed to unload during clear', error);
158
+ })
159
+ );
160
+ await Promise.all(unloadPromises);
161
+ this.cache.clear();
162
+ this.accessOrder = [];
163
+ Logger.debug('Cache cleared');
164
+ }
165
+
166
+ private async cleanExpiredThrottled(): Promise<void> {
167
+ await this.cleanLimiter.execute(async () => {
168
+ const now = Date.now();
169
+ const expiredIds: string[] = [];
170
+ for (const [id, cached] of this.cache) {
171
+ if (now - cached.loadedAt > this.expireMs) {
172
+ expiredIds.push(id);
173
+ }
174
+ }
175
+ for (const id of expiredIds) {
176
+ await this.delete(id);
177
+ Logger.debug(`Expired cache removed: ${id}`);
178
+ }
179
+ });
180
+ }
181
+
182
+ private async enforceLimit(): Promise<void> {
183
+ while (this.cache.size >= this.maxSize && this.accessOrder.length > 0) {
184
+ const lruId = this.accessOrder[0];
185
+ if (lruId && this.cache.has(lruId)) {
186
+ await this.delete(lruId);
187
+ Logger.debug(`LRU enforced, removed: ${lruId}`);
188
+ } else {
189
+ break;
190
+ }
191
+ }
192
+ }
193
+
194
+ private updateAccessOrder(id: string): void {
195
+ this.removeFromAccessOrder(id);
196
+ this.accessOrder.push(id);
197
+ }
198
+
199
+ private removeFromAccessOrder(id: string): void {
200
+ const index = this.accessOrder.indexOf(id);
201
+ if (index > -1) {
202
+ this.accessOrder.splice(index, 1);
203
+ }
204
+ }
205
+ }
206
+
207
+ // ===== Repository Export =====
208
+ export class AudioRepository {
209
+ readonly audioService: IAudioService;
210
+ readonly cache: ISoundCache;
211
+
212
+ constructor(events: SoundEvents) {
213
+ this.audioService = new ExpoAudioService();
214
+ this.cache = new SoundCache(this.audioService, events);
215
+ }
216
+ }
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Event-Driven Sound Store - Eliminates Presenter Pattern
3
+ * Direct event subscription from SoundService
4
+ */
5
+
6
+ import { createStore } from '@umituz/react-native-design-system/storage';
7
+ import type { PlaybackStatus } from '../application/interfaces/IAudioService';
8
+ import type { SoundSourceValue } from '../domain/value-objects';
9
+ import type { SoundEvents } from '../application/SoundEvents';
10
+
11
+ // ===== Store State =====
12
+ interface SoundState {
13
+ isPlaying: boolean;
14
+ isBuffering: boolean;
15
+ positionMillis: number;
16
+ durationMillis: number;
17
+ volume: number;
18
+ rate: number;
19
+ isLooping: boolean;
20
+ error: string | null;
21
+ currentId: string | null;
22
+ currentSource: SoundSourceValue | null;
23
+ }
24
+
25
+ interface SoundActions {
26
+ setPlaying: (isPlaying: boolean) => void;
27
+ setBuffering: (isBuffering: boolean) => void;
28
+ setProgress: (position: number, duration: number) => void;
29
+ setError: (error: string | null) => void;
30
+ setCurrent: (id: string | null, source: SoundSourceValue | null) => void;
31
+ setVolumeState: (volume: number) => void;
32
+ setRateState: (rate: number) => void;
33
+ setLooping: (isLooping: boolean) => void;
34
+ reset: () => void;
35
+ }
36
+
37
+ const initialState: SoundState = {
38
+ isPlaying: false,
39
+ isBuffering: false,
40
+ positionMillis: 0,
41
+ durationMillis: 0,
42
+ volume: 1.0,
43
+ rate: 1.0,
44
+ isLooping: false,
45
+ error: null,
46
+ currentId: null,
47
+ currentSource: null,
48
+ };
49
+
50
+ // ===== Store Creation =====
51
+ export const useSoundStore = createStore<SoundState, SoundActions>({
52
+ name: 'sound-store',
53
+ initialState,
54
+ persist: false,
55
+ actions: (set) => ({
56
+ setPlaying: (isPlaying) => set({ isPlaying }),
57
+ setBuffering: (isBuffering) => set({ isBuffering }),
58
+ setProgress: (position, duration) => set({ positionMillis: position, durationMillis: duration }),
59
+ setError: (error) => set({ error }),
60
+ setCurrent: (currentId, currentSource) => set({ currentId, currentSource }),
61
+ setVolumeState: (volume) => set({ volume }),
62
+ setRateState: (rate) => set({ rate }),
63
+ setLooping: (isLooping) => set({ isLooping }),
64
+ reset: () => set(initialState),
65
+ }),
66
+ });
67
+
68
+ // ===== Event Integration =====
69
+ export function setupEventListeners(events: SoundEvents): () => void {
70
+ const unsubscribers: Array<() => void> = [];
71
+
72
+ // Playback started
73
+ unsubscribers.push(
74
+ events.on('PLAYBACK_STARTED', (payload) => {
75
+ const p = payload as { id: string; source: SoundSourceValue; volume: number; rate: number; isLooping: boolean };
76
+ const store = useSoundStore.getState();
77
+ store.setCurrent(p.id, p.source);
78
+ store.setError(null);
79
+ store.setVolumeState(p.volume);
80
+ store.setRateState(p.rate);
81
+ store.setLooping(p.isLooping);
82
+ store.setPlaying(true);
83
+ })
84
+ );
85
+
86
+ // Playback stopped
87
+ unsubscribers.push(
88
+ events.on('PLAYBACK_STOPPED', () => {
89
+ const store = useSoundStore.getState();
90
+ store.setPlaying(false);
91
+ })
92
+ );
93
+
94
+ // Status update
95
+ unsubscribers.push(
96
+ events.on('STATUS_UPDATE', (payload) => {
97
+ const status = payload as PlaybackStatus;
98
+ const store = useSoundStore.getState();
99
+ if (status.isLoaded) {
100
+ store.setPlaying(status.isPlaying);
101
+ store.setBuffering(status.isBuffering);
102
+ store.setProgress(status.positionMillis, status.durationMillis);
103
+
104
+ if (status.rate !== undefined) {
105
+ store.setRateState(status.rate);
106
+ }
107
+
108
+ if (status.didJustFinish && !status.isLooping) {
109
+ store.setPlaying(false);
110
+ store.setProgress(status.durationMillis, status.durationMillis);
111
+ }
112
+ }
113
+
114
+ if (status.error) {
115
+ store.setError(status.error);
116
+ store.setPlaying(false);
117
+ }
118
+ })
119
+ );
120
+
121
+ // Volume changed
122
+ unsubscribers.push(
123
+ events.on('VOLUME_CHANGED', (payload) => {
124
+ useSoundStore.getState().setVolumeState(payload as number);
125
+ })
126
+ );
127
+
128
+ // Rate changed
129
+ unsubscribers.push(
130
+ events.on('RATE_CHANGED', (payload) => {
131
+ useSoundStore.getState().setRateState(payload as number);
132
+ })
133
+ );
134
+
135
+ // State reset
136
+ unsubscribers.push(
137
+ events.on('STATE_RESET', () => {
138
+ useSoundStore.getState().reset();
139
+ })
140
+ );
141
+
142
+ // Error
143
+ unsubscribers.push(
144
+ events.on('ERROR', (payload) => {
145
+ const error = payload as string;
146
+ const store = useSoundStore.getState();
147
+ store.setError(error);
148
+ store.setPlaying(false);
149
+ })
150
+ );
151
+
152
+ // Return cleanup function
153
+ return () => {
154
+ unsubscribers.forEach((unsub) => unsub());
155
+ };
156
+ }
@@ -0,0 +1,183 @@
1
+ /**
2
+ * useSound Hook - Event-Driven with Proper Lifecycle Management
3
+ * Single hook for all sound functionality
4
+ */
5
+
6
+ import { useEffect, useCallback, useRef } from 'react';
7
+ import { useSoundStore, setupEventListeners } from './SoundStore';
8
+ import { SoundService } from '../application/SoundService';
9
+ import type { SoundSourceValue } from '../domain/value-objects';
10
+ import type { PlaybackOptions } from '../types';
11
+ import { Logger } from '../infrastructure/Logger';
12
+
13
+ let serviceInstance: SoundService | null = null;
14
+ let serviceRefCount = 0;
15
+ let cleanupEventListeners: (() => void) | null = null;
16
+
17
+ const getService = (): SoundService => {
18
+ if (!serviceInstance) {
19
+ serviceInstance = SoundService.getInstance();
20
+ // Access events through public API
21
+ cleanupEventListeners = setupEventListeners(serviceInstance.getEvents());
22
+ }
23
+ serviceRefCount++;
24
+ return serviceInstance;
25
+ };
26
+
27
+ const releaseService = async (): Promise<void> => {
28
+ serviceRefCount--;
29
+ if (serviceRefCount <= 0 && serviceInstance) {
30
+ try {
31
+ await serviceInstance.dispose();
32
+ } catch (error) {
33
+ Logger.warn('Error during service cleanup', error);
34
+ }
35
+ cleanupEventListeners?.();
36
+ cleanupEventListeners = null;
37
+ serviceInstance = null;
38
+ serviceRefCount = 0;
39
+ }
40
+ };
41
+
42
+ export const useSound = () => {
43
+ const state = useSoundStore();
44
+ const serviceRef = useRef<SoundService | null>(null);
45
+ const isMountedRef = useRef(true);
46
+
47
+ // Initialize service on mount
48
+ useEffect(() => {
49
+ serviceRef.current = getService();
50
+ isMountedRef.current = true;
51
+
52
+ return () => {
53
+ isMountedRef.current = false;
54
+ // Don't await in cleanup - fire and forget
55
+ releaseService().catch((error) => Logger.warn('Error in useSound cleanup', error));
56
+ };
57
+ }, []);
58
+
59
+ // Memoize all functions to prevent re-renders
60
+ const play = useCallback(
61
+ (id: string, source: SoundSourceValue, options?: PlaybackOptions) => {
62
+ if (isMountedRef.current && serviceRef.current) {
63
+ return serviceRef.current.play(id, source, options);
64
+ }
65
+ },
66
+ []
67
+ );
68
+
69
+ const pause = useCallback(() => {
70
+ if (isMountedRef.current && serviceRef.current) {
71
+ return serviceRef.current.pause();
72
+ }
73
+ }, []);
74
+
75
+ const resume = useCallback(() => {
76
+ if (isMountedRef.current && serviceRef.current) {
77
+ return serviceRef.current.resume();
78
+ }
79
+ }, []);
80
+
81
+ const stop = useCallback(() => {
82
+ if (isMountedRef.current && serviceRef.current) {
83
+ return serviceRef.current.stop();
84
+ }
85
+ }, []);
86
+
87
+ const seek = useCallback(
88
+ (positionMillis: number) => {
89
+ if (isMountedRef.current && serviceRef.current) {
90
+ return serviceRef.current.seek(positionMillis);
91
+ }
92
+ },
93
+ []
94
+ );
95
+
96
+ const setVolume = useCallback(
97
+ (volume: number) => {
98
+ if (isMountedRef.current && serviceRef.current) {
99
+ return serviceRef.current.setVolume(volume);
100
+ }
101
+ },
102
+ []
103
+ );
104
+
105
+ const setRate = useCallback(
106
+ (rate: number) => {
107
+ if (isMountedRef.current && serviceRef.current) {
108
+ return serviceRef.current.setRate(rate);
109
+ }
110
+ },
111
+ []
112
+ );
113
+
114
+ const preload = useCallback(
115
+ (id: string, source: SoundSourceValue, options?: PlaybackOptions) => {
116
+ if (isMountedRef.current && serviceRef.current) {
117
+ return serviceRef.current.preload(id, source, options);
118
+ }
119
+ },
120
+ []
121
+ );
122
+
123
+ const unload = useCallback(() => {
124
+ if (isMountedRef.current && serviceRef.current) {
125
+ return serviceRef.current.unload();
126
+ }
127
+ }, []);
128
+
129
+ const clearCache = useCallback(async () => {
130
+ if (isMountedRef.current && serviceRef.current) {
131
+ await serviceRef.current.clearCache();
132
+ }
133
+ }, []);
134
+
135
+ const isCached = useCallback(
136
+ (id: string) => {
137
+ if (serviceRef.current) {
138
+ return serviceRef.current.isCached(id);
139
+ }
140
+ return false;
141
+ },
142
+ []
143
+ );
144
+
145
+ const subscribe = useCallback(
146
+ (eventType: string, listener: (payload: unknown) => void) => {
147
+ if (serviceRef.current) {
148
+ return serviceRef.current.on(eventType, listener);
149
+ }
150
+ return () => {};
151
+ },
152
+ []
153
+ );
154
+
155
+ return {
156
+ // State
157
+ isPlaying: state.isPlaying,
158
+ isBuffering: state.isBuffering,
159
+ isLooping: state.isLooping,
160
+ position: state.positionMillis,
161
+ duration: state.durationMillis,
162
+ currentId: state.currentId,
163
+ error: state.error,
164
+ volume: state.volume,
165
+ rate: state.rate,
166
+
167
+ // Actions (memoized)
168
+ play,
169
+ pause,
170
+ resume,
171
+ stop,
172
+ seek,
173
+ setVolume,
174
+ setRate,
175
+ preload,
176
+ unload,
177
+ clearCache,
178
+ isCached,
179
+
180
+ // Advanced: Event subscription
181
+ on: subscribe,
182
+ };
183
+ };
package/src/types.ts CHANGED
@@ -2,8 +2,7 @@
2
2
  * Public Types
3
3
  */
4
4
 
5
- export type { SoundSourceValue } from './domain/value-objects/SoundSource';
6
- export type { SoundStateData } from './domain/entities/SoundState';
5
+ export type { SoundSourceValue } from './domain/value-objects';
7
6
 
8
7
  export interface PlaybackOptions {
9
8
  isLooping?: boolean;