@umituz/react-native-sound 1.2.31 → 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.
@@ -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
@@ -1,10 +1,8 @@
1
1
  /**
2
- * @umituz/react-native-sound Types
2
+ * Public Types
3
3
  */
4
4
 
5
- import { AVPlaybackStatus } from 'expo-av';
6
-
7
- export type SoundSource = number | { uri: string; headers?: Record<string, string> } | null;
5
+ export type { SoundSourceValue } from './domain/value-objects';
8
6
 
9
7
  export interface PlaybackOptions {
10
8
  isLooping?: boolean;
@@ -22,6 +20,8 @@ export interface SoundState {
22
20
  rate: number;
23
21
  isLooping: boolean;
24
22
  error: string | null;
25
- currentSource: SoundSource | null;
23
+ currentSource: any;
26
24
  currentId: string | null;
27
25
  }
26
+
27
+ export type SoundSource = number | { uri: string; headers?: Record<string, string> } | null;
package/src/utils.ts CHANGED
@@ -1,30 +1,188 @@
1
1
  /**
2
- * @umituz/react-native-sound Utility Functions
2
+ * Utility Functions - Backward Compatibility + Performance Utilities
3
3
  */
4
4
 
5
- import { AVPlaybackStatus, AVPlaybackStatusSuccess } from 'expo-av';
6
- import type { SoundSource } from './types';
5
+ import { Volume } from './domain/value-objects';
6
+ import { Rate } from './domain/value-objects';
7
+ import type { PlaybackStatus } from './application/interfaces/IAudioService';
8
+ import { Logger } from './infrastructure/Logger';
7
9
 
8
- type PlaybackStatusSuccess = AVPlaybackStatusSuccess;
10
+ // ===== Backward Compatibility Functions =====
9
11
 
10
- export function isPlaybackStatusSuccess(status: AVPlaybackStatus): status is PlaybackStatusSuccess {
11
- return status.isLoaded;
12
+ export function clampVolume(volume: number): number {
13
+ return new Volume(volume).getValue();
14
+ }
15
+
16
+ export function clampRate(rate: number): number {
17
+ return new Rate(rate).getValue();
18
+ }
19
+
20
+ export function validateSoundId(id: string): boolean {
21
+ return typeof id === 'string' && id.trim().length > 0;
12
22
  }
13
23
 
14
- export function isSoundSourceValid(source: SoundSource): source is number | { uri: string; headers?: Record<string, string> } {
24
+ export function isSoundSourceValid(source: unknown): boolean {
15
25
  return source !== null && source !== undefined;
16
26
  }
17
27
 
18
- export function clampVolume(volume: number): number {
19
- if (!Number.isFinite(volume)) return 1.0;
20
- return Math.max(0, Math.min(1, volume));
28
+ export function isPlaybackStatusSuccess(status: PlaybackStatus): boolean {
29
+ return status.isLoaded === true;
21
30
  }
22
31
 
23
- export function clampRate(rate: number): number {
24
- if (!Number.isFinite(rate)) return 1.0;
25
- return Math.max(0.5, Math.min(2.0, rate));
32
+ // ===== Performance Utilities =====
33
+
34
+ /**
35
+ * Debounce function to limit execution frequency
36
+ * @param func Function to debounce
37
+ * @param wait Wait time in milliseconds
38
+ * @returns Debounced function
39
+ */
40
+ export function debounce<T extends (...args: unknown[]) => unknown>(
41
+ func: T,
42
+ wait: number
43
+ ): (...args: Parameters<T>) => void {
44
+ let timeoutId: ReturnType<typeof setTimeout> | null = null;
45
+
46
+ return function (this: unknown, ...args: Parameters<T>) {
47
+ if (timeoutId) {
48
+ clearTimeout(timeoutId);
49
+ }
50
+
51
+ timeoutId = setTimeout(() => {
52
+ func.apply(this, args);
53
+ timeoutId = null;
54
+ }, wait);
55
+ };
26
56
  }
27
57
 
28
- export function validateSoundId(id: string): boolean {
29
- return typeof id === 'string' && id.trim().length > 0;
58
+ /**
59
+ * Throttle function to limit execution frequency
60
+ * @param func Function to throttle
61
+ * @param limit Time limit in milliseconds
62
+ * @returns Throttled function
63
+ */
64
+ export function throttle<T extends (...args: unknown[]) => unknown>(
65
+ func: T,
66
+ limit: number
67
+ ): (...args: Parameters<T>) => void {
68
+ let inThrottle = false;
69
+ let lastArgs: Parameters<T> | null = null;
70
+ let lastContext: unknown = null;
71
+
72
+ return function (this: unknown, ...args: Parameters<T>) {
73
+ if (!inThrottle) {
74
+ func.apply(this, args);
75
+ inThrottle = true;
76
+ setTimeout(() => {
77
+ inThrottle = false;
78
+ if (lastArgs) {
79
+ func.apply(lastContext, lastArgs);
80
+ lastArgs = null;
81
+ lastContext = null;
82
+ }
83
+ }, limit);
84
+ } else {
85
+ lastArgs = args;
86
+ lastContext = this;
87
+ }
88
+ };
89
+ }
90
+
91
+ /**
92
+ * Memory-safe rate limiter for periodic operations
93
+ */
94
+ export class RateLimiter {
95
+ private lastExecution = 0;
96
+ private pendingExecution: (() => void) | null = null;
97
+
98
+ constructor(private readonly minInterval: number) {}
99
+
100
+ async execute(fn: () => void | Promise<void>): Promise<void> {
101
+ const now = Date.now();
102
+ const timeSinceLastExecution = now - this.lastExecution;
103
+
104
+ if (timeSinceLastExecution >= this.minInterval) {
105
+ this.lastExecution = now;
106
+ await fn();
107
+ } else {
108
+ // Schedule for later if not already pending
109
+ if (!this.pendingExecution) {
110
+ this.pendingExecution = fn;
111
+ const delay = this.minInterval - timeSinceLastExecution;
112
+ setTimeout(async () => {
113
+ if (this.pendingExecution) {
114
+ await this.pendingExecution();
115
+ this.pendingExecution = null;
116
+ }
117
+ this.lastExecution = Date.now();
118
+ }, delay);
119
+ }
120
+ }
121
+ }
122
+
123
+ clearPending(): void {
124
+ this.pendingExecution = null;
125
+ }
30
126
  }
127
+
128
+ /**
129
+ * Memory-safe periodic task scheduler
130
+ */
131
+ export class PeriodicTask {
132
+ private timerId: ReturnType<typeof setInterval> | null = null;
133
+ private isRunning = false;
134
+
135
+ constructor(
136
+ private readonly task: () => void | Promise<void>,
137
+ private readonly interval: number
138
+ ) {}
139
+
140
+ start(): void {
141
+ if (this.isRunning) return;
142
+
143
+ this.isRunning = true;
144
+ this.timerId = setInterval(async () => {
145
+ try {
146
+ await this.task();
147
+ } catch (error) {
148
+ Logger.error('Periodic task error', error);
149
+ }
150
+ }, this.interval);
151
+ }
152
+
153
+ stop(): void {
154
+ if (this.timerId) {
155
+ clearInterval(this.timerId);
156
+ this.timerId = null;
157
+ }
158
+ this.isRunning = false;
159
+ }
160
+
161
+ isActive(): boolean {
162
+ return this.isRunning;
163
+ }
164
+ }
165
+
166
+ /**
167
+ * WeakMap-based cache for automatic garbage collection
168
+ */
169
+ export class AutoGCWeakCache<K extends object, V> {
170
+ private cache = new WeakMap<K, V>();
171
+
172
+ set(key: K, value: V): void {
173
+ this.cache.set(key, value);
174
+ }
175
+
176
+ get(key: K): V | undefined {
177
+ return this.cache.get(key);
178
+ }
179
+
180
+ has(key: K): boolean {
181
+ return this.cache.has(key);
182
+ }
183
+
184
+ delete(key: K): boolean {
185
+ return this.cache.delete(key);
186
+ }
187
+ }
188
+