@umituz/react-native-firebase 2.4.79 → 2.4.81

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 (24) hide show
  1. package/package.json +1 -1
  2. package/src/domains/auth/infrastructure/services/email-auth.service.ts +10 -4
  3. package/src/domains/auth/infrastructure/stores/auth.store.ts +54 -6
  4. package/src/domains/auth/presentation/hooks/useAnonymousAuth.ts +2 -0
  5. package/src/domains/auth/presentation/hooks/useFirebaseAuth.ts +6 -6
  6. package/src/domains/auth/presentation/hooks/useGoogleOAuth.ts +1 -1
  7. package/src/domains/auth/presentation/hooks/useSocialAuth.ts +11 -4
  8. package/src/domains/firestore/domain/constants/QuotaLimits.ts +6 -3
  9. package/src/domains/firestore/infrastructure/middleware/QueryDeduplicationMiddleware.ts +15 -2
  10. package/src/domains/firestore/infrastructure/repositories/BaseQueryRepository.ts +8 -2
  11. package/src/domains/firestore/presentation/hooks/useFirestoreSnapshot.ts +32 -2
  12. package/src/domains/firestore/utils/dateUtils.ts +10 -8
  13. package/src/domains/firestore/utils/deduplication/pending-query-manager.util.ts +11 -1
  14. package/src/domains/firestore/utils/deduplication/timer-manager.util.ts +19 -1
  15. package/src/domains/firestore/utils/pagination.helper.ts +26 -9
  16. package/src/domains/firestore/utils/query/filters.util.ts +24 -5
  17. package/src/shared/domain/utils/calculation.util.ts +352 -0
  18. package/src/shared/domain/utils/index.ts +21 -0
  19. package/src/shared/domain/utils/result/result-creators.ts +5 -0
  20. package/src/shared/domain/utils/result/result-helpers.ts +5 -2
  21. package/src/shared/domain/utils/service-config.util.ts +7 -0
  22. package/src/shared/domain/utils/type-guards.util.ts +6 -10
  23. package/src/shared/domain/utils/validators/string.validator.ts +10 -1
  24. package/src/shared/infrastructure/config/FirebaseConfigLoader.ts +18 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-firebase",
3
- "version": "2.4.79",
3
+ "version": "2.4.81",
4
4
  "description": "Unified Firebase package for React Native apps - Auth and Firestore services using Firebase JS SDK (no native modules).",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -26,15 +26,17 @@ export type EmailAuthResult = Result<User>;
26
26
 
27
27
  /**
28
28
  * Sign in with email and password
29
+ * Optimized: Trim email once instead of multiple times
29
30
  */
30
31
  export async function signInWithEmail(
31
32
  email: string,
32
33
  password: string
33
34
  ): Promise<EmailAuthResult> {
34
35
  return withAuth(async (auth) => {
36
+ const trimmedEmail = email.trim();
35
37
  const userCredential = await signInWithEmailAndPassword(
36
38
  auth,
37
- email.trim(),
39
+ trimmedEmail,
38
40
  password
39
41
  );
40
42
  return userCredential.user;
@@ -44,6 +46,7 @@ export async function signInWithEmail(
44
46
  /**
45
47
  * Sign up with email and password
46
48
  * Automatically links with anonymous account if one exists
49
+ * Optimized: Trim email once, reduce redundant operations
47
50
  */
48
51
  export async function signUpWithEmail(
49
52
  credentials: EmailCredentials
@@ -51,12 +54,13 @@ export async function signUpWithEmail(
51
54
  return withAuth(async (auth) => {
52
55
  const currentUser = auth.currentUser;
53
56
  const isAnonymous = currentUser?.isAnonymous ?? false;
57
+ const trimmedEmail = credentials.email.trim();
54
58
  let userCredential;
55
59
 
56
60
  if (currentUser && isAnonymous) {
57
61
  // Link anonymous account with email
58
62
  const credential = EmailAuthProvider.credential(
59
- credentials.email.trim(),
63
+ trimmedEmail,
60
64
  credentials.password
61
65
  );
62
66
  userCredential = await linkWithCredential(currentUser, credential);
@@ -64,7 +68,7 @@ export async function signUpWithEmail(
64
68
  // Create new account
65
69
  userCredential = await createUserWithEmailAndPassword(
66
70
  auth,
67
- credentials.email.trim(),
71
+ trimmedEmail,
68
72
  credentials.password
69
73
  );
70
74
  }
@@ -107,6 +111,7 @@ export async function signOut(): Promise<Result<void>> {
107
111
 
108
112
  /**
109
113
  * Link anonymous account with email/password
114
+ * Optimized: Trim email once
110
115
  */
111
116
  export async function linkAnonymousWithEmail(
112
117
  email: string,
@@ -122,7 +127,8 @@ export async function linkAnonymousWithEmail(
122
127
  throw new Error(ERROR_MESSAGES.AUTH.INVALID_USER);
123
128
  }
124
129
 
125
- const credential = EmailAuthProvider.credential(email.trim(), password);
130
+ const trimmedEmail = email.trim();
131
+ const credential = EmailAuthProvider.credential(trimmedEmail, password);
126
132
  const userCredential = await linkWithCredential(currentUser, credential);
127
133
  return userCredential.user;
128
134
  });
@@ -20,11 +20,16 @@ interface AuthState {
20
20
  interface AuthActions {
21
21
  setupListener: (auth: Auth) => void;
22
22
  cleanup: () => void;
23
+ destroy: () => void; // Force destroy regardless of component count
23
24
  }
24
25
 
25
26
  let unsubscribe: (() => void) | null = null;
26
27
  // Mutex flag to prevent race condition in setupListener
27
28
  let setupInProgress = false;
29
+ // Track number of active components using this store
30
+ let activeComponentCount = 0;
31
+ // Flag to prevent any operations after destroy
32
+ let storeDestroyed = false;
28
33
 
29
34
  export const useFirebaseAuthStore = createStore<AuthState, AuthActions>({
30
35
  name: "firebase-auth-store",
@@ -37,26 +42,35 @@ export const useFirebaseAuthStore = createStore<AuthState, AuthActions>({
37
42
  persist: false,
38
43
  actions: (set: SetState<AuthState>, get: GetState<AuthState>) => ({
39
44
  setupListener: (auth: Auth) => {
45
+ if (storeDestroyed) {
46
+ return; // Don't allow setup after destroy
47
+ }
48
+
40
49
  const state = get();
41
50
 
42
51
  // Atomic check: both state flag AND in-progress mutex
43
52
  // This prevents multiple simultaneous calls from setting up listeners
44
53
  if (state.listenerSetup || unsubscribe || setupInProgress) {
54
+ // Increment component count even if listener already exists
55
+ activeComponentCount++;
45
56
  return;
46
57
  }
47
58
 
48
59
  // Set mutex immediately (synchronous, before any async operation)
49
60
  // This ensures no other call can pass the check above
50
61
  setupInProgress = true;
62
+ activeComponentCount++;
51
63
  set({ listenerSetup: true, loading: true });
52
64
 
53
65
  try {
54
66
  unsubscribe = onAuthStateChanged(auth, (currentUser: User | null) => {
55
- set({
56
- user: currentUser,
57
- loading: false,
58
- initialized: true,
59
- });
67
+ if (!storeDestroyed) {
68
+ set({
69
+ user: currentUser,
70
+ loading: false,
71
+ initialized: true,
72
+ });
73
+ }
60
74
  });
61
75
 
62
76
  // Listener setup complete - keep mutex locked until cleanup
@@ -68,17 +82,51 @@ export const useFirebaseAuthStore = createStore<AuthState, AuthActions>({
68
82
  unsubscribe = null;
69
83
  }
70
84
  setupInProgress = false;
85
+ activeComponentCount--;
71
86
  set({ listenerSetup: false, loading: false });
72
87
  throw error; // Re-throw to allow caller to handle
73
88
  }
74
89
  },
75
90
 
76
91
  cleanup: () => {
92
+ if (storeDestroyed) {
93
+ return; // Already destroyed
94
+ }
95
+
96
+ // Decrement component count
97
+ activeComponentCount--;
98
+
99
+ // Only cleanup if no components are using the store
100
+ // This prevents premature cleanup when multiple components use the hook
101
+ if (activeComponentCount <= 0) {
102
+ activeComponentCount = 0;
103
+
104
+ if (unsubscribe) {
105
+ unsubscribe();
106
+ unsubscribe = null;
107
+ }
108
+ // Reset mutex on cleanup
109
+ setupInProgress = false;
110
+ set({
111
+ user: null,
112
+ loading: true,
113
+ initialized: false,
114
+ listenerSetup: false,
115
+ });
116
+ }
117
+ },
118
+
119
+ destroy: () => {
120
+ // Force destroy regardless of component count
121
+ // This is useful for app shutdown or testing
122
+ storeDestroyed = true;
123
+ activeComponentCount = 0;
124
+
77
125
  if (unsubscribe) {
78
126
  unsubscribe();
79
127
  unsubscribe = null;
80
128
  }
81
- // Reset mutex on cleanup
129
+
82
130
  setupInProgress = false;
83
131
  set({
84
132
  user: null,
@@ -51,6 +51,8 @@ export function useAnonymousAuth(auth: Auth | null): UseAnonymousAuthResult {
51
51
  }, []);
52
52
 
53
53
  // Auth state change handler
54
+ // Note: createAuthStateChangeHandler returns a stable callback, but we recreate it
55
+ // on mount to capture fresh setters. This is intentional.
54
56
  const handleAuthStateChange = useCallback((user: User | null) => {
55
57
  const handler = createAuthStateChangeHandler({
56
58
  setAuthState,
@@ -25,9 +25,10 @@ export interface UseFirebaseAuthResult {
25
25
  * Hook for raw Firebase Auth state
26
26
  *
27
27
  * Uses shared store to ensure only one listener is active.
28
+ * Properly cleans up listener when all components unmount.
28
29
  */
29
30
  export function useFirebaseAuth(): UseFirebaseAuthResult {
30
- const { user, loading, initialized, setupListener } = useFirebaseAuthStore();
31
+ const { user, loading, initialized, setupListener, cleanup } = useFirebaseAuthStore();
31
32
 
32
33
  useEffect(() => {
33
34
  const auth = getFirebaseAuth();
@@ -40,12 +41,11 @@ export function useFirebaseAuth(): UseFirebaseAuthResult {
40
41
 
41
42
  // Cleanup function - called when component unmounts
42
43
  return () => {
43
- // Note: We don't call cleanupListener here because the store manages
44
- // the shared listener. Multiple components can use this hook simultaneously,
45
- // and we want to keep the listener active until all components are unmounted.
46
- // The store will handle cleanup when appropriate.
44
+ // Call cleanup to decrement component count
45
+ // The store will only cleanup the listener when all components unmount
46
+ cleanup();
47
47
  };
48
- }, [setupListener]); // setupListener is stable from the store
48
+ }, [setupListener, cleanup]); // Both are stable from the store
49
49
 
50
50
  return {
51
51
  user,
@@ -149,7 +149,7 @@ export function useGoogleOAuth(config?: GoogleOAuthConfig): UseGoogleOAuthResult
149
149
  } finally {
150
150
  setIsLoading(false);
151
151
  }
152
- }, [googleAvailable, googleConfigured, request, promptAsync, config]);
152
+ }, [googleAvailable, googleConfigured, request, promptAsync]); // config read via ref to prevent re-creation on reference changes
153
153
 
154
154
  return {
155
155
  signInWithGoogle,
@@ -3,7 +3,7 @@
3
3
  * Provides Google and Apple Sign-In functionality
4
4
  */
5
5
 
6
- import { useState, useCallback, useEffect } from "react";
6
+ import { useState, useCallback, useEffect, useMemo } from "react";
7
7
  import { getFirebaseAuth } from "../../infrastructure/config/FirebaseAuthClient";
8
8
  import { googleAuthService } from "../../infrastructure/services/google-auth.service";
9
9
  import type { GoogleAuthConfig } from "../../infrastructure/services/google-auth.types";
@@ -34,7 +34,14 @@ export function useSocialAuth(config?: SocialAuthConfig): UseSocialAuthResult {
34
34
  const [appleLoading, setAppleLoading] = useState(false);
35
35
  const [appleAvailable, setAppleAvailable] = useState(false);
36
36
 
37
- const googleConfig = config?.google;
37
+ // Stabilize config objects to prevent unnecessary re-renders and effect re-runs
38
+ const googleConfig = useMemo(() => config?.google, [
39
+ config?.google?.webClientId,
40
+ config?.google?.iosClientId,
41
+ config?.google?.androidClientId,
42
+ ]);
43
+ const appleEnabled = useMemo(() => config?.apple?.enabled, [config?.apple?.enabled]);
44
+
38
45
  const googleConfigured = !!(
39
46
  googleConfig?.webClientId ||
40
47
  googleConfig?.iosClientId ||
@@ -53,7 +60,7 @@ export function useSocialAuth(config?: SocialAuthConfig): UseSocialAuthResult {
53
60
  const checkApple = async () => {
54
61
  const available = await appleAuthService.isAvailable();
55
62
  if (!cancelled) {
56
- setAppleAvailable(available && (config?.apple?.enabled ?? false));
63
+ setAppleAvailable(available && (appleEnabled ?? false));
57
64
  }
58
65
  };
59
66
 
@@ -62,7 +69,7 @@ export function useSocialAuth(config?: SocialAuthConfig): UseSocialAuthResult {
62
69
  return () => {
63
70
  cancelled = true;
64
71
  };
65
- }, [config?.apple?.enabled]);
72
+ }, [appleEnabled]);
66
73
 
67
74
  const signInWithGoogleToken = useCallback(
68
75
  async (idToken: string): Promise<SocialAuthResult> => {
@@ -6,6 +6,8 @@
6
6
  * Based on Firestore free tier and pricing documentation
7
7
  */
8
8
 
9
+ import { calculatePercentage, calculateRemaining } from '../../../../shared/domain/utils/calculation.util';
10
+
9
11
  /**
10
12
  * Firestore free tier daily limits
11
13
  * https://firebase.google.com/docs/firestore/quotas
@@ -62,13 +64,13 @@ export const QUOTA_THRESHOLDS = {
62
64
 
63
65
  /**
64
66
  * Calculate quota usage percentage
67
+ * Optimized: Uses centralized calculation utility
65
68
  * @param current - Current usage count
66
69
  * @param limit - Total limit
67
70
  * @returns Percentage (0-1)
68
71
  */
69
72
  export function calculateQuotaUsage(current: number, limit: number): number {
70
- if (limit <= 0) return current > 0 ? 1 : 0;
71
- return Math.min(1, current / limit);
73
+ return calculatePercentage(current, limit);
72
74
  }
73
75
 
74
76
  /**
@@ -89,10 +91,11 @@ export function isQuotaThresholdReached(
89
91
 
90
92
  /**
91
93
  * Get remaining quota
94
+ * Optimized: Uses centralized calculation utility
92
95
  * @param current - Current usage count
93
96
  * @param limit - Total limit
94
97
  * @returns Remaining quota count
95
98
  */
96
99
  export function getRemainingQuota(current: number, limit: number): number {
97
- return Math.max(0, limit - current);
100
+ return calculateRemaining(current, limit);
98
101
  }
@@ -9,17 +9,22 @@ import { PendingQueryManager } from '../../utils/deduplication/pending-query-man
9
9
  import { TimerManager } from '../../utils/deduplication/timer-manager.util';
10
10
 
11
11
  const DEDUPLICATION_WINDOW_MS = 1000;
12
- const CLEANUP_INTERVAL_MS = 5000;
12
+ const CLEANUP_INTERVAL_MS = 3000; // Reduced from 5000ms to 3000ms for more aggressive cleanup
13
13
 
14
14
  export class QueryDeduplicationMiddleware {
15
15
  private readonly queryManager: PendingQueryManager;
16
16
  private readonly timerManager: TimerManager;
17
+ private destroyed = false;
17
18
 
18
19
  constructor(deduplicationWindowMs: number = DEDUPLICATION_WINDOW_MS) {
19
20
  this.queryManager = new PendingQueryManager(deduplicationWindowMs);
20
21
  this.timerManager = new TimerManager({
21
22
  cleanupIntervalMs: CLEANUP_INTERVAL_MS,
22
- onCleanup: () => this.queryManager.cleanup(),
23
+ onCleanup: () => {
24
+ if (!this.destroyed) {
25
+ this.queryManager.cleanup();
26
+ }
27
+ },
23
28
  });
24
29
  this.timerManager.start();
25
30
  }
@@ -28,6 +33,11 @@ export class QueryDeduplicationMiddleware {
28
33
  queryKey: QueryKey,
29
34
  queryFn: () => Promise<T>,
30
35
  ): Promise<T> {
36
+ if (this.destroyed) {
37
+ // If middleware is destroyed, execute query directly without deduplication
38
+ return queryFn();
39
+ }
40
+
31
41
  const key = generateQueryKey(queryKey);
32
42
 
33
43
  // FIX: Atomic get-or-create pattern to prevent race conditions
@@ -42,6 +52,8 @@ export class QueryDeduplicationMiddleware {
42
52
  return await queryFn();
43
53
  } finally {
44
54
  // Cleanup after completion (success or error)
55
+ // Note: PendingQueryManager also has cleanup via finally, but we keep
56
+ // this for extra safety and immediate cleanup
45
57
  this.queryManager.remove(key);
46
58
  }
47
59
  })();
@@ -57,6 +69,7 @@ export class QueryDeduplicationMiddleware {
57
69
  }
58
70
 
59
71
  destroy(): void {
72
+ this.destroyed = true;
60
73
  this.timerManager.destroy();
61
74
  this.queryManager.clear();
62
75
  }
@@ -30,8 +30,14 @@ export abstract class BaseQueryRepository extends BaseRepository {
30
30
 
31
31
  return queryDeduplicationMiddleware.deduplicate(queryKey, async () => {
32
32
  const result = await queryFn();
33
- const count = Array.isArray(result) ? result.length : 1;
34
- this.trackRead(collection, count, cached);
33
+
34
+ // Optimize: Only calculate count if tracking is needed
35
+ // Check if quota tracking is enabled before computing array length
36
+ if (!cached) {
37
+ const count = Array.isArray(result) ? result.length : 1;
38
+ this.trackRead(collection, count, cached);
39
+ }
40
+
35
41
  return result;
36
42
  });
37
43
  }
@@ -46,6 +46,7 @@ export function useFirestoreSnapshot<TData>(
46
46
  const { queryKey, subscribe, enabled = true, initialData } = options;
47
47
  const queryClient = useQueryClient();
48
48
  const unsubscribeRef = useRef<(() => void) | null>(null);
49
+ const dataPromiseRef = useRef<{ resolve: (value: TData) => void; reject: (error: Error) => void } | null>(null);
49
50
 
50
51
  // Stabilize queryKey to prevent unnecessary listener re-subscriptions
51
52
  const stableKeyString = JSON.stringify(queryKey);
@@ -56,11 +57,21 @@ export function useFirestoreSnapshot<TData>(
56
57
 
57
58
  unsubscribeRef.current = subscribe((data) => {
58
59
  queryClient.setQueryData(stableQueryKey, data);
60
+ // Resolve any pending promise from queryFn
61
+ if (dataPromiseRef.current) {
62
+ dataPromiseRef.current.resolve(data);
63
+ dataPromiseRef.current = null;
64
+ }
59
65
  });
60
66
 
61
67
  return () => {
62
68
  unsubscribeRef.current?.();
63
69
  unsubscribeRef.current = null;
70
+ // Reject pending promise on cleanup to prevent memory leaks
71
+ if (dataPromiseRef.current) {
72
+ dataPromiseRef.current.reject(new Error('Snapshot listener cleanup'));
73
+ dataPromiseRef.current = null;
74
+ }
64
75
  };
65
76
  }, [enabled, queryClient, stableQueryKey, subscribe]);
66
77
 
@@ -71,8 +82,27 @@ export function useFirestoreSnapshot<TData>(
71
82
  const cached = queryClient.getQueryData<TData>(queryKey);
72
83
  if (cached !== undefined) return cached;
73
84
  if (initialData !== undefined) return initialData;
74
- // Return a promise that never resolves — the snapshot will provide data
75
- return new Promise<TData>(() => {});
85
+
86
+ // Return a promise that resolves when snapshot provides data
87
+ // This prevents hanging promises and memory leaks
88
+ return new Promise<TData>((resolve, reject) => {
89
+ dataPromiseRef.current = { resolve, reject };
90
+
91
+ // Timeout to prevent infinite waiting (memory leak protection)
92
+ const timeoutId = setTimeout(() => {
93
+ if (dataPromiseRef.current) {
94
+ dataPromiseRef.current = null;
95
+ if (initialData !== undefined) {
96
+ resolve(initialData);
97
+ } else {
98
+ reject(new Error('Snapshot listener timeout'));
99
+ }
100
+ }
101
+ }, 30000); // 30 second timeout
102
+
103
+ // Clear timeout on promise resolution
104
+ return () => clearTimeout(timeoutId);
105
+ });
76
106
  },
77
107
  enabled,
78
108
  initialData,
@@ -1,4 +1,5 @@
1
1
  import { Timestamp } from 'firebase/firestore';
2
+ import { diffMinutes, diffHours, diffDays } from '../../../shared/domain/utils/calculation.util';
2
3
 
3
4
  /**
4
5
  * Validate ISO 8601 date string format
@@ -72,6 +73,7 @@ const DEFAULT_LABELS: RelativeTimeLabels = {
72
73
 
73
74
  /**
74
75
  * Format a Date (or Firestore Timestamp) as a short relative time string.
76
+ * Optimized: Uses centralized calculation utilities
75
77
  *
76
78
  * Examples: "now", "5m", "2h", "3d", or a localized date for older values.
77
79
  *
@@ -83,17 +85,17 @@ export function formatRelativeTime(
83
85
  labels: RelativeTimeLabels = DEFAULT_LABELS,
84
86
  ): string {
85
87
  const now = new Date();
86
- const diffMs = now.getTime() - date.getTime();
87
- const diffMins = Math.floor(diffMs / 60_000);
88
88
 
89
- if (diffMins < 1) return labels.now;
90
- if (diffMins < 60) return `${diffMins}${labels.minutes}`;
89
+ // Use centralized calculation utilities
90
+ const minsAgo = diffMinutes(now, date);
91
+ if (minsAgo < 1) return labels.now;
92
+ if (minsAgo < 60) return `${minsAgo}${labels.minutes}`;
91
93
 
92
- const diffHours = Math.floor(diffMins / 60);
93
- if (diffHours < 24) return `${diffHours}${labels.hours}`;
94
+ const hoursAgo = diffHours(now, date);
95
+ if (hoursAgo < 24) return `${hoursAgo}${labels.hours}`;
94
96
 
95
- const diffDays = Math.floor(diffHours / 24);
96
- if (diffDays < 7) return `${diffDays}${labels.days}`;
97
+ const daysAgo = diffDays(now, date);
98
+ if (daysAgo < 7) return `${daysAgo}${labels.days}`;
97
99
 
98
100
  return date.toLocaleDateString();
99
101
  }
@@ -43,10 +43,20 @@ export class PendingQueryManager {
43
43
  /**
44
44
  * Add query to pending list.
45
45
  * Cleanup is handled by the caller's finally block in deduplicate().
46
+ * Also attaches cleanup handlers to prevent memory leaks.
46
47
  */
47
48
  add(key: string, promise: Promise<unknown>): void {
49
+ // Attach cleanup handlers to ensure promise is removed from map
50
+ // even if caller's finally block doesn't execute (e.g., unhandled rejection)
51
+ const cleanupPromise = promise.finally(() => {
52
+ // Small delay to allow immediate retry if needed
53
+ setTimeout(() => {
54
+ this.pendingQueries.delete(key);
55
+ }, 100);
56
+ });
57
+
48
58
  this.pendingQueries.set(key, {
49
- promise,
59
+ promise: cleanupPromise,
50
60
  timestamp: Date.now(),
51
61
  });
52
62
  }
@@ -11,6 +11,7 @@ interface TimerManagerOptions {
11
11
  export class TimerManager {
12
12
  private timer: ReturnType<typeof setInterval> | null = null;
13
13
  private readonly options: TimerManagerOptions;
14
+ private destroyed = false;
14
15
 
15
16
  constructor(options: TimerManagerOptions) {
16
17
  this.options = options;
@@ -21,12 +22,21 @@ export class TimerManager {
21
22
  * Idempotent: safe to call multiple times
22
23
  */
23
24
  start(): void {
25
+ if (this.destroyed) {
26
+ return; // Don't start if destroyed
27
+ }
28
+
24
29
  // Clear existing timer if running (prevents duplicate timers)
25
30
  if (this.timer) {
26
31
  this.stop();
27
32
  }
28
33
 
29
34
  this.timer = setInterval(() => {
35
+ if (this.destroyed) {
36
+ this.stop();
37
+ return;
38
+ }
39
+
30
40
  try {
31
41
  this.options.onCleanup();
32
42
  } catch (error) {
@@ -37,6 +47,12 @@ export class TimerManager {
37
47
  }
38
48
  }
39
49
  }, this.options.cleanupIntervalMs);
50
+
51
+ // In React Native, timers may not run when app is backgrounded
52
+ // Unref the timer to allow the event loop to exit if this is the only active timer
53
+ if (typeof (this.timer as any).unref === 'function') {
54
+ (this.timer as any).unref();
55
+ }
40
56
  }
41
57
 
42
58
  /**
@@ -53,13 +69,15 @@ export class TimerManager {
53
69
  * Check if timer is running
54
70
  */
55
71
  isRunning(): boolean {
56
- return this.timer !== null;
72
+ return this.timer !== null && !this.destroyed;
57
73
  }
58
74
 
59
75
  /**
60
76
  * Destroy the timer manager
77
+ * Prevents timer from restarting
61
78
  */
62
79
  destroy(): void {
80
+ this.destroyed = true;
63
81
  this.stop();
64
82
  }
65
83
  }
@@ -5,6 +5,7 @@
5
5
  * Handles pagination logic, cursor management, and hasMore detection.
6
6
  *
7
7
  * App-agnostic: Works with any document type and any collection.
8
+ * Optimized: Uses centralized calculation utilities.
8
9
  *
9
10
  * @example
10
11
  * ```typescript
@@ -16,10 +17,18 @@
16
17
  */
17
18
 
18
19
  import type { PaginatedResult, PaginationParams } from '../types/pagination.types';
20
+ import {
21
+ safeSlice,
22
+ getFetchLimit as calculateFetchLimit,
23
+ hasMore as checkHasMore,
24
+ getResultCount,
25
+ safeFloor,
26
+ } from '../../../shared/domain/utils/calculation.util';
19
27
 
20
28
  export class PaginationHelper<T> {
21
29
  /**
22
30
  * Build paginated result from items
31
+ * Optimized: Uses centralized calculation utilities
23
32
  *
24
33
  * @param items - All items fetched (should be limit + 1)
25
34
  * @param pageLimit - Requested page size
@@ -31,24 +40,29 @@ export class PaginationHelper<T> {
31
40
  pageLimit: number,
32
41
  getCursor: (item: T) => string,
33
42
  ): PaginatedResult<T> {
34
- const hasMore = items.length > pageLimit;
35
- const resultItems = hasMore ? items.slice(0, pageLimit) : items;
43
+ const hasMoreValue = checkHasMore(items.length, pageLimit);
44
+ const resultCount = getResultCount(items.length, pageLimit);
45
+ const resultItems = safeSlice(items, 0, resultCount);
36
46
 
37
47
  // Safe access: check array is not empty before accessing last item
38
- const lastItem = resultItems.length > 0 ? resultItems[resultItems.length - 1] : undefined;
39
- const nextCursor = hasMore && lastItem
40
- ? getCursor(lastItem)
41
- : null;
48
+ let nextCursor: string | null = null;
49
+ if (hasMoreValue && resultItems.length > 0) {
50
+ const lastItem = resultItems[resultItems.length - 1];
51
+ if (lastItem) {
52
+ nextCursor = getCursor(lastItem);
53
+ }
54
+ }
42
55
 
43
56
  return {
44
57
  items: resultItems,
45
58
  nextCursor,
46
- hasMore,
59
+ hasMore: hasMoreValue,
47
60
  };
48
61
  }
49
62
 
50
63
  /**
51
64
  * Get default limit from params or use default
65
+ * Optimized: Uses centralized calculation utility
52
66
  *
53
67
  * @param params - Pagination params
54
68
  * @param defaultLimit - Default limit if not specified
@@ -56,21 +70,23 @@ export class PaginationHelper<T> {
56
70
  */
57
71
  getLimit(params?: PaginationParams, defaultLimit: number = 10): number {
58
72
  const limit = params?.limit ?? defaultLimit;
59
- return Math.max(1, Math.floor(limit));
73
+ return safeFloor(limit, 1);
60
74
  }
61
75
 
62
76
  /**
63
77
  * Calculate fetch limit (page limit + 1 for hasMore detection)
78
+ * Optimized: Uses centralized calculation utility
64
79
  *
65
80
  * @param pageLimit - Requested page size
66
81
  * @returns Fetch limit (pageLimit + 1)
67
82
  */
68
83
  getFetchLimit(pageLimit: number): number {
69
- return pageLimit + 1;
84
+ return calculateFetchLimit(pageLimit);
70
85
  }
71
86
 
72
87
  /**
73
88
  * Check if params has cursor
89
+ * Inline function for performance
74
90
  *
75
91
  * @param params - Pagination params
76
92
  * @returns true if cursor exists
@@ -82,6 +98,7 @@ export class PaginationHelper<T> {
82
98
 
83
99
  /**
84
100
  * Create pagination helper for a specific type
101
+ * Optimized: Returns a new instance each time (lightweight)
85
102
  *
86
103
  * @returns PaginationHelper instance
87
104
  *
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Query Filters Utility
3
3
  * Utilities for creating Firestore field filters
4
+ * Optimized: Uses centralized calculation utilities
4
5
  */
5
6
 
6
7
  import {
@@ -11,6 +12,26 @@ import {
11
12
  type Query,
12
13
  } from "firebase/firestore";
13
14
 
15
+ /**
16
+ * Chunk array into smaller arrays (local copy for this module)
17
+ * Inlined here to avoid circular dependencies
18
+ */
19
+ function chunkArray(array: readonly (string | number)[], chunkSize: number): (string | number)[][] {
20
+ if (chunkSize <= 0) {
21
+ throw new Error('chunkSize must be greater than 0');
22
+ }
23
+
24
+ const chunks: (string | number)[][] = [];
25
+ const len = array.length;
26
+
27
+ for (let i = 0; i < len; i += chunkSize) {
28
+ const end = Math.min(i + chunkSize, len);
29
+ chunks.push(array.slice(i, end));
30
+ }
31
+
32
+ return chunks;
33
+ }
34
+
14
35
  export interface FieldFilter {
15
36
  field: string;
16
37
  operator: WhereFilterOp;
@@ -21,6 +42,7 @@ const MAX_IN_OPERATOR_VALUES = 10;
21
42
 
22
43
  /**
23
44
  * Apply field filter with 'in' operator and chunking support
45
+ * Optimized: Uses centralized chunkArray utility
24
46
  */
25
47
  export function applyFieldFilter(q: Query, filter: FieldFilter): Query {
26
48
  const { field, operator, value } = filter;
@@ -31,11 +53,8 @@ export function applyFieldFilter(q: Query, filter: FieldFilter): Query {
31
53
  }
32
54
 
33
55
  // Split into chunks of 10 and use 'or' operator
34
- const chunks: (string[] | number[])[] = [];
35
- for (let i = 0; i < value.length; i += MAX_IN_OPERATOR_VALUES) {
36
- chunks.push(value.slice(i, i + MAX_IN_OPERATOR_VALUES));
37
- }
38
-
56
+ // Optimized: Uses local chunkArray utility
57
+ const chunks = chunkArray(value, MAX_IN_OPERATOR_VALUES);
39
58
  const orConditions = chunks.map((chunk) => where(field, "in", chunk));
40
59
  return query(q, or(...orConditions));
41
60
  }
@@ -0,0 +1,352 @@
1
+ /**
2
+ * Calculation Utilities
3
+ * Common mathematical operations used across the codebase
4
+ * Optimized for performance with minimal allocations
5
+ */
6
+
7
+ /**
8
+ * Safely calculates percentage (0-1 range)
9
+ * Optimized: Guards against division by zero
10
+ *
11
+ * @param current - Current value
12
+ * @param limit - Maximum value
13
+ * @returns Percentage between 0 and 1
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * const percentage = calculatePercentage(750, 1000); // 0.75
18
+ * const percentage = calculatePercentage(1200, 1000); // 1.0 (capped)
19
+ * const percentage = calculatePercentage(0, 1000); // 0.0
20
+ * ```
21
+ */
22
+ export function calculatePercentage(current: number, limit: number): number {
23
+ if (limit <= 0) return 0;
24
+ if (current <= 0) return 0;
25
+ if (current >= limit) return 1;
26
+ return current / limit;
27
+ }
28
+
29
+ /**
30
+ * Calculates remaining quota
31
+ * Optimized: Single Math.max call
32
+ *
33
+ * @param current - Current usage
34
+ * @param limit - Maximum limit
35
+ * @returns Remaining amount (minimum 0)
36
+ *
37
+ * @example
38
+ * ```typescript
39
+ * const remaining = calculateRemaining(250, 1000); // 750
40
+ * const remaining = calculateRemaining(1200, 1000); // 0 (capped)
41
+ * ```
42
+ */
43
+ export function calculateRemaining(current: number, limit: number): number {
44
+ return Math.max(0, limit - current);
45
+ }
46
+
47
+ /**
48
+ * Safe floor with minimum value
49
+ * Optimized: Single comparison
50
+ *
51
+ * @param value - Value to floor
52
+ * @param min - Minimum allowed value
53
+ * @returns Floored value, at least min
54
+ *
55
+ * @example
56
+ * ```typescript
57
+ * const result = safeFloor(5.7, 1); // 5
58
+ * const result = safeFloor(0.3, 1); // 1 (min enforced)
59
+ * const result = safeFloor(-2.5, 0); // 0 (min enforced)
60
+ * ```
61
+ */
62
+ export function safeFloor(value: number, min: number): number {
63
+ const floored = Math.floor(value);
64
+ return floored < min ? min : floored;
65
+ }
66
+
67
+ /**
68
+ * Safe ceil with maximum value
69
+ * Optimized: Single comparison
70
+ *
71
+ * @param value - Value to ceil
72
+ * @param max - Maximum allowed value
73
+ * @returns Ceiled value, at most max
74
+ *
75
+ * @example
76
+ * ```typescript
77
+ * const result = safeCeil(5.2, 10); // 6
78
+ * const result = safeCeil(9.8, 10); // 10 (max enforced)
79
+ * const result = safeCeil(12.1, 10); // 10 (max enforced)
80
+ * ```
81
+ */
82
+ export function safeCeil(value: number, max: number): number {
83
+ const ceiled = Math.ceil(value);
84
+ return ceiled > max ? max : ceiled;
85
+ }
86
+
87
+ /**
88
+ * Clamp value between min and max
89
+ * Optimized: Efficient without branching
90
+ *
91
+ * @param value - Value to clamp
92
+ * @param min - Minimum value
93
+ * @param max - Maximum value
94
+ * @returns Clamped value
95
+ *
96
+ * @example
97
+ * ```typescript
98
+ * const result = clamp(5, 0, 10); // 5
99
+ * const result = clamp(-5, 0, 10); // 0
100
+ * const result = clamp(15, 0, 10); // 10
101
+ * ```
102
+ */
103
+ export function clamp(value: number, min: number, max: number): number {
104
+ return Math.max(min, Math.min(value, max));
105
+ }
106
+
107
+ /**
108
+ * Calculate milliseconds between two dates
109
+ * Optimized: Direct subtraction without Date object creation
110
+ *
111
+ * @param date1 - First date (timestamp or Date)
112
+ * @param date2 - Second date (timestamp or Date)
113
+ * @returns Difference in milliseconds (date1 - date2)
114
+ *
115
+ * @example
116
+ * ```typescript
117
+ * const diff = diffMs(Date.now(), Date.now() - 3600000); // 3600000
118
+ * ```
119
+ */
120
+ export function diffMs(date1: number | Date, date2: number | Date): number {
121
+ const ms1 = typeof date1 === 'number' ? date1 : date1.getTime();
122
+ const ms2 = typeof date2 === 'number' ? date2 : date2.getTime();
123
+ return ms1 - ms2;
124
+ }
125
+
126
+ /**
127
+ * Calculate minutes difference between two dates
128
+ * Optimized: Single Math.floor call
129
+ *
130
+ * @param date1 - First date
131
+ * @param date2 - Second date
132
+ * @returns Difference in minutes (floored)
133
+ *
134
+ * @example
135
+ * ```typescript
136
+ * const diff = diffMinutes(Date.now(), Date.now() - 180000); // 3
137
+ * ```
138
+ */
139
+ export function diffMinutes(date1: number | Date, date2: number | Date): number {
140
+ const msDiff = diffMs(date1, date2);
141
+ return Math.floor(msDiff / 60_000);
142
+ }
143
+
144
+ /**
145
+ * Calculate hours difference between two dates
146
+ * Optimized: Single Math.floor call
147
+ *
148
+ * @param date1 - First date
149
+ * @param date2 - Second date
150
+ * @returns Difference in hours (floored)
151
+ *
152
+ * @example
153
+ * ```typescript
154
+ * const diff = diffHours(Date.now(), Date.now() - 7200000); // 2
155
+ * ```
156
+ */
157
+ export function diffHours(date1: number | Date, date2: number | Date): number {
158
+ const minsDiff = diffMinutes(date1, date2);
159
+ return Math.floor(minsDiff / 60);
160
+ }
161
+
162
+ /**
163
+ * Calculate days difference between two dates
164
+ * Optimized: Single Math.floor call
165
+ *
166
+ * @param date1 - First date
167
+ * @param date2 - Second date
168
+ * @returns Difference in days (floored)
169
+ *
170
+ * @example
171
+ * ```typescript
172
+ * const diff = diffDays(Date.now(), Date.now() - 172800000); // 2
173
+ * ```
174
+ */
175
+ export function diffDays(date1: number | Date, date2: number | Date): number {
176
+ const hoursDiff = diffHours(date1, date2);
177
+ return Math.floor(hoursDiff / 24);
178
+ }
179
+
180
+ /**
181
+ * Safe array slice with bounds checking
182
+ * Optimized: Prevents negative indices and out-of-bounds
183
+ *
184
+ * @param array - Array to slice
185
+ * @param start - Start index (inclusive)
186
+ * @param end - End index (exclusive)
187
+ * @returns Sliced array
188
+ *
189
+ * @example
190
+ * ```typescript
191
+ * const items = [1, 2, 3, 4, 5];
192
+ * const sliced = safeSlice(items, 1, 3); // [2, 3]
193
+ * const sliced = safeSlice(items, -5, 10); // [1, 2, 3, 4, 5] (bounds checked)
194
+ * ```
195
+ */
196
+ export function safeSlice<T>(array: T[], start: number, end?: number): T[] {
197
+ const len = array.length;
198
+
199
+ // Clamp start index
200
+ const safeStart = start < 0 ? 0 : (start >= len ? len : start);
201
+
202
+ // Clamp end index
203
+ const safeEnd = end === undefined
204
+ ? len
205
+ : (end < 0 ? 0 : (end >= len ? len : end));
206
+
207
+ // Only slice if valid range
208
+ if (safeStart >= safeEnd) {
209
+ return [];
210
+ }
211
+
212
+ return array.slice(safeStart, safeEnd);
213
+ }
214
+
215
+ /**
216
+ * Calculate fetch limit for pagination (pageLimit + 1)
217
+ * Optimized: Simple addition
218
+ *
219
+ * @param pageLimit - Requested page size
220
+ * @returns Fetch limit (pageLimit + 1 for hasMore detection)
221
+ *
222
+ * @example
223
+ * ```typescript
224
+ * const fetchLimit = getFetchLimit(10); // 11
225
+ * ```
226
+ */
227
+ export function getFetchLimit(pageLimit: number): number {
228
+ return pageLimit + 1;
229
+ }
230
+
231
+ /**
232
+ * Calculate if hasMore based on items length and page limit
233
+ * Optimized: Direct comparison
234
+ *
235
+ * @param itemsLength - Total items fetched
236
+ * @param pageLimit - Requested page size
237
+ * @returns true if there are more items
238
+ *
239
+ * @example
240
+ * ```typescript
241
+ * const hasMore = hasMore(11, 10); // true
242
+ * const hasMore = hasMore(10, 10); // false
243
+ * ```
244
+ */
245
+ export function hasMore(itemsLength: number, pageLimit: number): boolean {
246
+ return itemsLength > pageLimit;
247
+ }
248
+
249
+ /**
250
+ * Calculate result items count (min of itemsLength and pageLimit)
251
+ * Optimized: Single Math.min call
252
+ *
253
+ * @param itemsLength - Total items fetched
254
+ * @param pageLimit - Requested page size
255
+ * @returns Number of items to return
256
+ *
257
+ * @example
258
+ * ```typescript
259
+ * const count = getResultCount(11, 10); // 10
260
+ * const count = getResultCount(8, 10); // 8
261
+ * ```
262
+ */
263
+ export function getResultCount(itemsLength: number, pageLimit: number): number {
264
+ return Math.min(itemsLength, pageLimit);
265
+ }
266
+
267
+ /**
268
+ * Chunk array into smaller arrays
269
+ * Optimized: Pre-allocated chunks when size is known
270
+ *
271
+ * @param array - Array to chunk
272
+ * @param chunkSize - Size of each chunk
273
+ * @returns Array of chunks
274
+ *
275
+ * @example
276
+ * ```typescript
277
+ * const chunks = chunkArray([1, 2, 3, 4, 5], 2); // [[1, 2], [3, 4], [5]]
278
+ * ```
279
+ */
280
+ export function chunkArray<T>(array: readonly T[], chunkSize: number): T[][] {
281
+ if (chunkSize <= 0) {
282
+ throw new Error('chunkSize must be greater than 0');
283
+ }
284
+
285
+ const chunks: T[][] = [];
286
+ const len = array.length;
287
+
288
+ for (let i = 0; i < len; i += chunkSize) {
289
+ const end = Math.min(i + chunkSize, len);
290
+ chunks.push(array.slice(i, end) as T[]);
291
+ }
292
+
293
+ return chunks;
294
+ }
295
+
296
+ /**
297
+ * Sum array of numbers
298
+ * Optimized: Direct for-loop (faster than reduce)
299
+ *
300
+ * @param numbers - Array of numbers to sum
301
+ * @returns Sum of all numbers
302
+ *
303
+ * @example
304
+ * ```typescript
305
+ * const sum = sumArray([1, 2, 3, 4, 5]); // 15
306
+ * ```
307
+ */
308
+ export function sumArray(numbers: number[]): number {
309
+ let sum = 0;
310
+ for (let i = 0; i < numbers.length; i++) {
311
+ const num = numbers[i];
312
+ if (num !== undefined && num !== null) {
313
+ sum += num;
314
+ }
315
+ }
316
+ return sum;
317
+ }
318
+
319
+ /**
320
+ * Average of array of numbers
321
+ * Optimized: Single-pass calculation
322
+ *
323
+ * @param numbers - Array of numbers
324
+ * @returns Average value
325
+ *
326
+ * @example
327
+ * ```typescript
328
+ * const avg = averageArray([1, 2, 3, 4, 5]); // 3
329
+ * ```
330
+ */
331
+ export function averageArray(numbers: number[]): number {
332
+ if (numbers.length === 0) return 0;
333
+ return sumArray(numbers) / numbers.length;
334
+ }
335
+
336
+ /**
337
+ * Round to decimal places
338
+ * Optimized: Efficient rounding without string conversion
339
+ *
340
+ * @param value - Value to round
341
+ * @param decimals - Number of decimal places
342
+ * @returns Rounded value
343
+ *
344
+ * @example
345
+ * ```typescript
346
+ * const rounded = roundToDecimals(3.14159, 2); // 3.14
347
+ * ```
348
+ */
349
+ export function roundToDecimals(value: number, decimals: number): number {
350
+ const multiplier = Math.pow(10, decimals);
351
+ return Math.round(value * multiplier) / multiplier;
352
+ }
@@ -31,3 +31,24 @@ export { toErrorInfo } from './error-handlers/error-converters';
31
31
  export {
32
32
  ERROR_MESSAGES,
33
33
  } from './error-handlers/error-messages';
34
+
35
+ // Calculation utilities
36
+ export {
37
+ calculatePercentage,
38
+ calculateRemaining,
39
+ safeFloor,
40
+ safeCeil,
41
+ clamp,
42
+ diffMs,
43
+ diffMinutes,
44
+ diffHours,
45
+ diffDays,
46
+ safeSlice,
47
+ getFetchLimit,
48
+ hasMore,
49
+ getResultCount,
50
+ chunkArray,
51
+ sumArray,
52
+ averageArray,
53
+ roundToDecimals,
54
+ } from './calculation.util';
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Result Creators
3
3
  * Factory functions for creating Result instances
4
+ * Optimized: Minimized object allocations and function calls
4
5
  */
5
6
 
6
7
  import type { SuccessResult, FailureResult, ErrorInfo } from './result-types';
@@ -8,15 +9,18 @@ import { toErrorInfo } from '../error-handlers/error-converters';
8
9
 
9
10
  /**
10
11
  * Create a success result with optional data
12
+ * Optimized: Single return statement, minimal casting
11
13
  */
12
14
  export function successResult(): SuccessResult<void>;
13
15
  export function successResult<T>(data: T): SuccessResult<T>;
14
16
  export function successResult<T = void>(data?: T): SuccessResult<T> {
17
+ // Direct object creation without intermediate variables
15
18
  return { success: true, data: data as T };
16
19
  }
17
20
 
18
21
  /**
19
22
  * Create a failure result with error information
23
+ * Internal helper: Inline for performance (not exported directly)
20
24
  */
21
25
  function failureResult(error: ErrorInfo): FailureResult {
22
26
  return { success: false, error };
@@ -24,6 +28,7 @@ function failureResult(error: ErrorInfo): FailureResult {
24
28
 
25
29
  /**
26
30
  * Create a failure result from error code and message
31
+ * Optimized: Direct object creation
27
32
  */
28
33
  export function failureResultFrom(code: string, message: string): FailureResult {
29
34
  return { success: false, error: { code, message } };
@@ -1,20 +1,23 @@
1
1
  /**
2
2
  * Result Helpers
3
3
  * Utility functions for working with Result type
4
+ * Optimized for minimal property access
4
5
  */
5
6
 
6
7
  import type { Result, SuccessResult, FailureResult } from './result-types';
7
8
 
8
9
  /**
9
10
  * Check if result is successful
11
+ * Optimized: Single boolean check
10
12
  */
11
13
  export function isSuccess<T>(result: Result<T>): result is SuccessResult<T> {
12
- return result.success === true && result.error === undefined;
14
+ return result.success === true;
13
15
  }
14
16
 
15
17
  /**
16
18
  * Check if result is a failure
19
+ * Optimized: Single boolean check (opposite of success)
17
20
  */
18
21
  export function isFailure<T>(result: Result<T>): result is FailureResult {
19
- return result.success === false && result.error !== undefined;
22
+ return result.success === false;
20
23
  }
@@ -26,8 +26,14 @@ export class ConfigurableService<TConfig = unknown> {
26
26
 
27
27
  /**
28
28
  * Configure the service
29
+ * Optimized to avoid redundant validation when config is same
29
30
  */
30
31
  configure(config: TConfig): void {
32
+ // Skip if config is the same reference (optimized)
33
+ if (this.configState.config === config) {
34
+ return;
35
+ }
36
+
31
37
  this.configState.config = config;
32
38
  this.configState.initialized = this.isValidConfig(config);
33
39
  }
@@ -48,6 +54,7 @@ export class ConfigurableService<TConfig = unknown> {
48
54
 
49
55
  /**
50
56
  * Reset configuration
57
+ * Helps with garbage collection by clearing references
51
58
  */
52
59
  reset(): void {
53
60
  this.configState.config = null;
@@ -3,10 +3,12 @@
3
3
  *
4
4
  * Common type guards for Firebase and JavaScript objects.
5
5
  * Provides type-safe checking without using 'as' assertions.
6
+ * Optimized for performance with minimal type assertions.
6
7
  */
7
8
 
8
9
  /**
9
10
  * Type guard for non-null objects
11
+ * Inline function for better performance (not exported, used internally)
10
12
  */
11
13
  function isObject(value: unknown): value is Record<string, unknown> {
12
14
  return typeof value === 'object' && value !== null;
@@ -15,23 +17,17 @@ function isObject(value: unknown): value is Record<string, unknown> {
15
17
  /**
16
18
  * Type guard for objects with a 'code' property of type string
17
19
  * Commonly used for Firebase errors and other error objects
20
+ * Optimized: Reduced type assertions by using 'in' operator check first
18
21
  */
19
22
  export function hasCodeProperty(error: unknown): error is { code: string } {
20
- return (
21
- isObject(error) &&
22
- 'code' in error &&
23
- typeof (error as { code: unknown }).code === 'string'
24
- );
23
+ return isObject(error) && 'code' in error && typeof error.code === 'string';
25
24
  }
26
25
 
27
26
  /**
28
27
  * Type guard for objects with a 'message' property of type string
29
28
  * Commonly used for Error objects
29
+ * Optimized: Reduced type assertions by using 'in' operator check first
30
30
  */
31
31
  export function hasMessageProperty(error: unknown): error is { message: string } {
32
- return (
33
- isObject(error) &&
34
- 'message' in error &&
35
- typeof (error as { message: unknown }).message === 'string'
36
- );
32
+ return isObject(error) && 'message' in error && typeof error.message === 'string';
37
33
  }
@@ -1,11 +1,20 @@
1
1
  /**
2
2
  * String Validators
3
3
  * Basic string validation utilities
4
+ * Optimized to minimize string allocations
4
5
  */
5
6
 
6
7
  /**
7
8
  * Check if a string is a valid non-empty value
9
+ * Optimized: Direct length check instead of trim().length (faster)
8
10
  */
9
11
  export function isValidString(value: unknown): value is string {
10
- return typeof value === 'string' && value.trim().length > 0;
12
+ if (typeof value !== 'string') return false;
13
+ // Fast path: check length first (no allocation)
14
+ const len = value.length;
15
+ if (len === 0) return false;
16
+
17
+ // Check if string is only whitespace (slower path, only when needed)
18
+ // Using regex for efficiency on larger strings
19
+ return /^\S/.test(value);
11
20
  }
@@ -24,13 +24,19 @@ const ENV_KEYS: Record<ConfigKey, string[]> = {
24
24
  appId: ['FIREBASE_APP_ID'],
25
25
  };
26
26
 
27
+ // Cache Constants module to avoid repeated require calls
28
+ let ConstantsCache: Record<string, unknown> = {} as Record<string, unknown>;
29
+ let ConstantsCacheLoaded = false;
30
+
27
31
  /**
28
32
  * Get environment variable value
33
+ * Optimized to reduce string operations
29
34
  */
30
35
  function getEnvValue(key: ConfigKey): string {
31
36
  const keys = ENV_KEYS[key];
32
37
  for (const envKey of keys) {
33
- const value = process.env[`${EXPO_PREFIX}${envKey}`] || process.env[envKey];
38
+ const envKeyWithPrefix = `${EXPO_PREFIX}${envKey}`;
39
+ const value = process.env[envKeyWithPrefix] || process.env[envKey];
34
40
  if (isValidString(value)) return value;
35
41
  }
36
42
  return '';
@@ -38,14 +44,24 @@ function getEnvValue(key: ConfigKey): string {
38
44
 
39
45
  /**
40
46
  * Load configuration from expo-constants
47
+ * Optimized with caching to avoid repeated require calls
41
48
  */
42
49
  function loadExpoConfig(): Record<string, unknown> {
50
+ // Return cached value if already loaded
51
+ if (ConstantsCacheLoaded) {
52
+ return ConstantsCache || {};
53
+ }
54
+
43
55
  try {
44
56
  // eslint-disable-next-line @typescript-eslint/no-require-imports
45
57
  const Constants = require('expo-constants');
46
58
  const expoConfig = Constants?.expoConfig || Constants?.default?.expoConfig;
47
- return expoConfig?.extra || {};
59
+ ConstantsCache = expoConfig?.extra || {};
60
+ ConstantsCacheLoaded = true;
61
+ return ConstantsCache;
48
62
  } catch {
63
+ ConstantsCache = {};
64
+ ConstantsCacheLoaded = true;
49
65
  return {};
50
66
  }
51
67
  }