@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.
- package/package.json +1 -1
- package/src/domains/auth/infrastructure/services/email-auth.service.ts +10 -4
- package/src/domains/auth/infrastructure/stores/auth.store.ts +54 -6
- package/src/domains/auth/presentation/hooks/useAnonymousAuth.ts +2 -0
- package/src/domains/auth/presentation/hooks/useFirebaseAuth.ts +6 -6
- package/src/domains/auth/presentation/hooks/useGoogleOAuth.ts +1 -1
- package/src/domains/auth/presentation/hooks/useSocialAuth.ts +11 -4
- package/src/domains/firestore/domain/constants/QuotaLimits.ts +6 -3
- package/src/domains/firestore/infrastructure/middleware/QueryDeduplicationMiddleware.ts +15 -2
- package/src/domains/firestore/infrastructure/repositories/BaseQueryRepository.ts +8 -2
- package/src/domains/firestore/presentation/hooks/useFirestoreSnapshot.ts +32 -2
- package/src/domains/firestore/utils/dateUtils.ts +10 -8
- package/src/domains/firestore/utils/deduplication/pending-query-manager.util.ts +11 -1
- package/src/domains/firestore/utils/deduplication/timer-manager.util.ts +19 -1
- package/src/domains/firestore/utils/pagination.helper.ts +26 -9
- package/src/domains/firestore/utils/query/filters.util.ts +24 -5
- package/src/shared/domain/utils/calculation.util.ts +352 -0
- package/src/shared/domain/utils/index.ts +21 -0
- package/src/shared/domain/utils/result/result-creators.ts +5 -0
- package/src/shared/domain/utils/result/result-helpers.ts +5 -2
- package/src/shared/domain/utils/service-config.util.ts +7 -0
- package/src/shared/domain/utils/type-guards.util.ts +6 -10
- package/src/shared/domain/utils/validators/string.validator.ts +10 -1
- 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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
44
|
-
//
|
|
45
|
-
|
|
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]); //
|
|
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
|
|
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
|
-
|
|
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 && (
|
|
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
|
-
}, [
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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: () =>
|
|
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
|
-
|
|
34
|
-
|
|
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
|
-
|
|
75
|
-
|
|
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
|
-
|
|
90
|
-
|
|
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
|
|
93
|
-
if (
|
|
94
|
+
const hoursAgo = diffHours(now, date);
|
|
95
|
+
if (hoursAgo < 24) return `${hoursAgo}${labels.hours}`;
|
|
94
96
|
|
|
95
|
-
const
|
|
96
|
-
if (
|
|
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
|
|
35
|
-
const
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
35
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
}
|