@umituz/react-native-firebase 3.0.2 → 3.0.5
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 +7 -1
- package/src/domains/account-deletion/index.ts +15 -10
- package/src/domains/account-deletion/infrastructure/services/account-deletion.service.ts +226 -26
- package/src/domains/account-deletion/infrastructure/services/reauthentication.service.ts +160 -0
- package/src/domains/auth/domain/value-objects/FirebaseAuthConfig.ts +1 -1
- package/src/domains/auth/index.ts +156 -6
- package/src/domains/auth/infrastructure/config/FirebaseAuthClient.ts +60 -48
- package/src/domains/auth/infrastructure/config/initializers/FirebaseAuthInitializer.ts +41 -5
- package/src/domains/auth/presentation/hooks/useGoogleOAuth.ts +115 -20
- package/src/domains/firestore/domain/constants/QuotaLimits.ts +101 -0
- package/src/domains/firestore/domain/entities/QuotaMetrics.ts +26 -0
- package/src/domains/firestore/domain/entities/RequestLog.ts +28 -0
- package/src/domains/firestore/domain/services/QuotaCalculator.ts +71 -0
- package/src/domains/firestore/index.ts +86 -28
- package/src/domains/firestore/infrastructure/config/FirestoreClient.ts +92 -19
- package/src/domains/firestore/infrastructure/config/initializers/FirebaseFirestoreInitializer.ts +249 -4
- package/src/domains/firestore/infrastructure/middleware/QueryDeduplicationMiddleware.ts +312 -0
- package/src/domains/firestore/infrastructure/middleware/QuotaTrackingMiddleware.ts +95 -0
- package/src/domains/firestore/infrastructure/repositories/BasePaginatedRepository.ts +7 -1
- package/src/domains/firestore/infrastructure/repositories/BaseQueryRepository.ts +34 -8
- package/src/domains/firestore/infrastructure/repositories/BaseRepository.ts +48 -9
- package/src/domains/firestore/infrastructure/services/RequestLoggerService.ts +165 -0
- package/src/domains/firestore/presentation/hooks/index.ts +10 -0
- package/src/domains/firestore/presentation/hooks/useFirestoreMutation.ts +1 -1
- package/src/domains/firestore/presentation/hooks/useFirestoreQuery.ts +1 -1
- package/src/domains/firestore/presentation/hooks/useSmartFirestoreSnapshot.ts +361 -0
- package/src/domains/firestore/presentation/query-keys/createFirestoreKeys.ts +32 -0
- package/src/domains/firestore/presentation/query-keys/index.ts +1 -0
- package/src/domains/firestore/utils/deduplication/pending-query-manager.util.ts +119 -0
- package/src/domains/firestore/utils/deduplication/query-key-generator.util.ts +34 -0
- package/src/domains/firestore/utils/deduplication/timer-manager.util.ts +83 -0
- package/src/index.ts +2 -30
- package/src/shared/domain/utils/calculation.util.ts +305 -17
- package/src/shared/domain/utils/error-handlers/error-messages.ts +0 -11
- package/src/shared/domain/utils/index.ts +5 -0
- package/src/shared/infrastructure/config/base/ClientStateManager.ts +82 -0
- package/src/shared/infrastructure/config/base/ServiceClientSingleton.ts +136 -20
- package/src/shared/infrastructure/config/clients/FirebaseClientSingleton.ts +1 -1
- package/src/shared/infrastructure/config/initializers/FirebaseAppInitializer.ts +9 -0
- package/src/shared/infrastructure/config/services/FirebaseInitializationService.ts +1 -1
- package/src/shared/infrastructure/config/state/FirebaseClientState.ts +14 -36
- package/src/application/auth/index.ts +0 -10
- package/src/application/auth/use-cases/index.ts +0 -6
- package/src/domains/account-deletion/domain/index.ts +0 -8
- package/src/domains/account-deletion/infrastructure/services/AccountDeletionExecutor.ts +0 -79
- package/src/domains/account-deletion/infrastructure/services/AccountDeletionTypes.ts +0 -32
- package/src/domains/auth/domain.ts +0 -16
- package/src/domains/auth/infrastructure/config/index.ts +0 -2
- package/src/domains/auth/infrastructure/config/initializers/index.ts +0 -1
- package/src/domains/auth/infrastructure/services/index.ts +0 -16
- package/src/domains/auth/infrastructure/services/utils/index.ts +0 -1
- package/src/domains/auth/infrastructure/stores/index.ts +0 -1
- package/src/domains/auth/infrastructure/utils/index.ts +0 -1
- package/src/domains/auth/infrastructure.ts +0 -11
- package/src/domains/auth/presentation/hooks/useAppleAuth.ts +0 -82
- package/src/domains/auth/presentation.ts +0 -31
- package/src/domains/firestore/domain/entities/Collection.ts +0 -122
- package/src/domains/firestore/domain/entities/CollectionFactory.ts +0 -55
- package/src/domains/firestore/domain/entities/CollectionHelpers.ts +0 -143
- package/src/domains/firestore/domain/entities/CollectionUtils.ts +0 -72
- package/src/domains/firestore/domain/entities/CollectionValidation.ts +0 -138
- package/src/domains/firestore/domain/index.ts +0 -61
- package/src/domains/firestore/domain/value-objects/QueryOptions.ts +0 -143
- package/src/domains/firestore/domain/value-objects/QueryOptionsFactory.ts +0 -95
- package/src/domains/firestore/domain/value-objects/QueryOptionsHelpers.ts +0 -110
- package/src/domains/firestore/domain/value-objects/WhereClause.ts +0 -114
- package/src/domains/firestore/domain/value-objects/WhereClauseFactory.ts +0 -101
- package/src/domains/firestore/domain/value-objects/WhereClauseHelpers.ts +0 -123
- package/src/domains/firestore/domain/value-objects/WhereClauseValidation.ts +0 -83
- package/src/shared/infrastructure/base/ErrorHandler.ts +0 -81
- package/src/shared/infrastructure/base/ServiceBase.ts +0 -62
- package/src/shared/infrastructure/base/TypedGuard.ts +0 -131
- package/src/shared/infrastructure/base/index.ts +0 -34
- package/src/shared/types/firebase.types.ts +0 -274
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pending Query Manager Utility
|
|
3
|
+
* Manages pending queries for deduplication
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
interface PendingQuery {
|
|
7
|
+
promise: Promise<unknown>;
|
|
8
|
+
timestamp: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class PendingQueryManager {
|
|
12
|
+
private pendingQueries = new Map<string, PendingQuery>();
|
|
13
|
+
private deduplicationWindowMs: number;
|
|
14
|
+
|
|
15
|
+
constructor(deduplicationWindowMs: number = 1000) {
|
|
16
|
+
this.deduplicationWindowMs = deduplicationWindowMs;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Update the deduplication window dynamically
|
|
21
|
+
* Used for quota-aware adaptive deduplication
|
|
22
|
+
*/
|
|
23
|
+
setWindow(windowMs: number): void {
|
|
24
|
+
this.deduplicationWindowMs = windowMs;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get current deduplication window
|
|
29
|
+
*/
|
|
30
|
+
getWindow(): number {
|
|
31
|
+
return this.deduplicationWindowMs;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Check if query is pending and not expired
|
|
36
|
+
*/
|
|
37
|
+
isPending(key: string): boolean {
|
|
38
|
+
const pending = this.pendingQueries.get(key);
|
|
39
|
+
if (!pending) return false;
|
|
40
|
+
|
|
41
|
+
const age = Date.now() - pending.timestamp;
|
|
42
|
+
if (age > this.deduplicationWindowMs) {
|
|
43
|
+
this.pendingQueries.delete(key);
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get pending query promise
|
|
52
|
+
*/
|
|
53
|
+
get(key: string): Promise<unknown> | null {
|
|
54
|
+
const pending = this.pendingQueries.get(key);
|
|
55
|
+
return pending ? pending.promise : null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Add query to pending list.
|
|
60
|
+
* Cleanup is handled by the caller's finally block in deduplicate().
|
|
61
|
+
* Also attaches cleanup handlers to prevent memory leaks.
|
|
62
|
+
*/
|
|
63
|
+
add(key: string, promise: Promise<unknown>): void {
|
|
64
|
+
// Attach cleanup handler to ensure query is removed from map
|
|
65
|
+
// even if caller's finally block doesn't execute (e.g., unhandled rejection)
|
|
66
|
+
promise.finally(() => {
|
|
67
|
+
// Immediate cleanup - no delay needed for better performance
|
|
68
|
+
this.pendingQueries.delete(key);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
this.pendingQueries.set(key, {
|
|
72
|
+
promise,
|
|
73
|
+
timestamp: Date.now(),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Remove a specific query from pending list
|
|
79
|
+
*/
|
|
80
|
+
remove(key: string): void {
|
|
81
|
+
this.pendingQueries.delete(key);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Clean up expired queries
|
|
86
|
+
* Uses current deduplication window (may be adjusted dynamically)
|
|
87
|
+
*/
|
|
88
|
+
cleanup(): void {
|
|
89
|
+
const now = Date.now();
|
|
90
|
+
const windowMs = this.deduplicationWindowMs; // Capture current window
|
|
91
|
+
|
|
92
|
+
for (const [key, query] of this.pendingQueries.entries()) {
|
|
93
|
+
if (now - query.timestamp > windowMs) {
|
|
94
|
+
this.pendingQueries.delete(key);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Clear all pending queries
|
|
101
|
+
*/
|
|
102
|
+
clear(): void {
|
|
103
|
+
this.pendingQueries.clear();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Get pending queries count
|
|
108
|
+
*/
|
|
109
|
+
size(): number {
|
|
110
|
+
return this.pendingQueries.size;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Check if there are any pending queries
|
|
115
|
+
*/
|
|
116
|
+
isEmpty(): boolean {
|
|
117
|
+
return this.pendingQueries.size === 0;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query Key Generator Utility
|
|
3
|
+
* Generates unique keys for query deduplication
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface QueryKey {
|
|
7
|
+
collection: string;
|
|
8
|
+
filters: string;
|
|
9
|
+
limit?: number;
|
|
10
|
+
orderBy?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Escape special characters in query key components
|
|
15
|
+
* Prevents key collisions when filter strings contain separator characters
|
|
16
|
+
*/
|
|
17
|
+
function escapeKeyComponent(component: string): string {
|
|
18
|
+
return component.replace(/%/g, '%25').replace(/\|/g, '%7C');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Generate a unique key from query parameters
|
|
23
|
+
* Uses URL encoding to prevent collisions from separator characters
|
|
24
|
+
*/
|
|
25
|
+
export function generateQueryKey(key: QueryKey): string {
|
|
26
|
+
const parts = [
|
|
27
|
+
escapeKeyComponent(key.collection),
|
|
28
|
+
escapeKeyComponent(key.filters),
|
|
29
|
+
key.limit?.toString() || '',
|
|
30
|
+
escapeKeyComponent(key.orderBy || ''),
|
|
31
|
+
];
|
|
32
|
+
return parts.join('|');
|
|
33
|
+
}
|
|
34
|
+
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timer Manager Utility
|
|
3
|
+
* Manages cleanup timers for deduplication middleware
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
interface TimerManagerOptions {
|
|
7
|
+
cleanupIntervalMs: number;
|
|
8
|
+
onCleanup: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class TimerManager {
|
|
12
|
+
private timer: ReturnType<typeof setInterval> | null = null;
|
|
13
|
+
private readonly options: TimerManagerOptions;
|
|
14
|
+
private destroyed = false;
|
|
15
|
+
|
|
16
|
+
constructor(options: TimerManagerOptions) {
|
|
17
|
+
this.options = options;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Start the cleanup timer
|
|
22
|
+
* Idempotent: safe to call multiple times
|
|
23
|
+
*/
|
|
24
|
+
start(): void {
|
|
25
|
+
if (this.destroyed) {
|
|
26
|
+
return; // Don't start if destroyed
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Clear existing timer if running (prevents duplicate timers)
|
|
30
|
+
if (this.timer) {
|
|
31
|
+
this.stop();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
this.timer = setInterval(() => {
|
|
35
|
+
if (this.destroyed) {
|
|
36
|
+
this.stop();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
this.options.onCleanup();
|
|
42
|
+
} catch (error) {
|
|
43
|
+
// Silently handle cleanup errors to prevent timer from causing issues
|
|
44
|
+
// Log error in development for debugging (use __DEV__ for React Native)
|
|
45
|
+
if (__DEV__) {
|
|
46
|
+
console.error('TimerManager cleanup error:', error);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
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
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Stop the cleanup timer
|
|
60
|
+
*/
|
|
61
|
+
stop(): void {
|
|
62
|
+
if (this.timer) {
|
|
63
|
+
clearInterval(this.timer);
|
|
64
|
+
this.timer = null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Check if timer is running
|
|
70
|
+
*/
|
|
71
|
+
isRunning(): boolean {
|
|
72
|
+
return this.timer !== null && !this.destroyed;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Destroy the timer manager
|
|
77
|
+
* Prevents timer from restarting
|
|
78
|
+
*/
|
|
79
|
+
destroy(): void {
|
|
80
|
+
this.destroyed = true;
|
|
81
|
+
this.stop();
|
|
82
|
+
}
|
|
83
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -2,11 +2,6 @@
|
|
|
2
2
|
* React Native Firebase - Unified Package
|
|
3
3
|
*
|
|
4
4
|
* Domain-Driven Design (DDD) Architecture
|
|
5
|
-
*
|
|
6
|
-
* IMPORTANT: This package does NOT import from 'firebase/app' or other firebase packages.
|
|
7
|
-
* Import firebase SDK directly in your app if you need those types.
|
|
8
|
-
*
|
|
9
|
-
* This package provides utilities and abstractions ONLY for React Native.
|
|
10
5
|
*/
|
|
11
6
|
|
|
12
7
|
// Core Errors
|
|
@@ -37,6 +32,8 @@ export type {
|
|
|
37
32
|
ServiceInitializationResult,
|
|
38
33
|
} from "./shared/infrastructure/config/services/FirebaseInitializationService";
|
|
39
34
|
|
|
35
|
+
export type { FirebaseApp } from "./shared/infrastructure/config/initializers/FirebaseAppInitializer";
|
|
36
|
+
|
|
40
37
|
// Type Guards
|
|
41
38
|
export {
|
|
42
39
|
isFirestoreError,
|
|
@@ -49,31 +46,6 @@ export {
|
|
|
49
46
|
getSafeErrorCode,
|
|
50
47
|
} from "./shared/domain/guards/firebase-error.guard";
|
|
51
48
|
|
|
52
|
-
// Shared Infrastructure Base Classes
|
|
53
|
-
export {
|
|
54
|
-
ServiceBase,
|
|
55
|
-
ErrorHandler,
|
|
56
|
-
defaultErrorHandler,
|
|
57
|
-
hasCodeProperty,
|
|
58
|
-
hasMessageProperty,
|
|
59
|
-
isFirebaseErrorLike,
|
|
60
|
-
hasNameProperty,
|
|
61
|
-
isErrorLike,
|
|
62
|
-
hasUidProperty,
|
|
63
|
-
hasEmailProperty,
|
|
64
|
-
hasProviderIdProperty,
|
|
65
|
-
isArray,
|
|
66
|
-
isString,
|
|
67
|
-
isFunction,
|
|
68
|
-
hasProperty,
|
|
69
|
-
hasProperties,
|
|
70
|
-
} from "./shared/infrastructure/base";
|
|
71
|
-
|
|
72
|
-
export type {
|
|
73
|
-
ServiceBaseOptions,
|
|
74
|
-
ErrorHandlerOptions,
|
|
75
|
-
} from "./shared/infrastructure/base";
|
|
76
|
-
|
|
77
49
|
// Domain Exports
|
|
78
50
|
export * from "./domains/auth";
|
|
79
51
|
export * from "./domains/account-deletion";
|
|
@@ -1,7 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Calculation Utilities
|
|
3
|
+
* Common mathematical operations used across the codebase
|
|
4
|
+
* Optimized for performance with minimal allocations
|
|
3
5
|
*/
|
|
4
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
|
+
*/
|
|
5
22
|
export function calculatePercentage(current: number, limit: number): number {
|
|
6
23
|
if (limit <= 0) return 0;
|
|
7
24
|
if (current <= 0) return 0;
|
|
@@ -9,56 +26,327 @@ export function calculatePercentage(current: number, limit: number): number {
|
|
|
9
26
|
return current / limit;
|
|
10
27
|
}
|
|
11
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
|
+
*/
|
|
12
43
|
export function calculateRemaining(current: number, limit: number): number {
|
|
13
44
|
return Math.max(0, limit - current);
|
|
14
45
|
}
|
|
15
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
|
+
*/
|
|
16
62
|
export function safeFloor(value: number, min: number): number {
|
|
17
63
|
const floored = Math.floor(value);
|
|
18
64
|
return floored < min ? min : floored;
|
|
19
65
|
}
|
|
20
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
|
+
*/
|
|
21
120
|
export function diffMs(date1: number | Date, date2: number | Date): number {
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
return
|
|
121
|
+
const ms1 = typeof date1 === 'number' ? date1 : date1.getTime();
|
|
122
|
+
const ms2 = typeof date2 === 'number' ? date2 : date2.getTime();
|
|
123
|
+
return ms1 - ms2;
|
|
25
124
|
}
|
|
26
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
|
+
*/
|
|
27
139
|
export function diffMinutes(date1: number | Date, date2: number | Date): number {
|
|
28
|
-
|
|
140
|
+
const msDiff = diffMs(date1, date2);
|
|
141
|
+
return Math.floor(msDiff / 60_000);
|
|
29
142
|
}
|
|
30
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
|
+
*/
|
|
31
157
|
export function diffHours(date1: number | Date, date2: number | Date): number {
|
|
32
|
-
|
|
158
|
+
const minsDiff = diffMinutes(date1, date2);
|
|
159
|
+
return Math.floor(minsDiff / 60);
|
|
33
160
|
}
|
|
34
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
|
+
*/
|
|
35
175
|
export function diffDays(date1: number | Date, date2: number | Date): number {
|
|
36
|
-
|
|
176
|
+
const hoursDiff = diffHours(date1, date2);
|
|
177
|
+
return Math.floor(hoursDiff / 24);
|
|
37
178
|
}
|
|
38
179
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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 [];
|
|
44
210
|
}
|
|
45
|
-
return result;
|
|
46
|
-
}
|
|
47
211
|
|
|
48
|
-
|
|
49
|
-
if (start < 0) return array.slice(0, end);
|
|
50
|
-
if (start >= array.length) return [];
|
|
51
|
-
return array.slice(start, end);
|
|
212
|
+
return array.slice(safeStart, safeEnd);
|
|
52
213
|
}
|
|
53
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
|
+
*/
|
|
54
227
|
export function getFetchLimit(pageLimit: number): number {
|
|
55
228
|
return pageLimit + 1;
|
|
56
229
|
}
|
|
57
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
|
+
*/
|
|
58
245
|
export function hasMore(itemsLength: number, pageLimit: number): boolean {
|
|
59
246
|
return itemsLength > pageLimit;
|
|
60
247
|
}
|
|
61
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
|
+
*/
|
|
62
263
|
export function getResultCount(itemsLength: number, pageLimit: number): number {
|
|
63
264
|
return Math.min(itemsLength, pageLimit);
|
|
64
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
|
+
}
|
|
@@ -13,11 +13,6 @@ export const ERROR_MESSAGES = {
|
|
|
13
13
|
INVALID_CREDENTIALS: 'Invalid credentials provided',
|
|
14
14
|
USER_CHANGED: 'User changed during operation',
|
|
15
15
|
OPERATION_IN_PROGRESS: 'Operation already in progress',
|
|
16
|
-
USER_NOT_FOUND: 'User not found',
|
|
17
|
-
EMAIL_ALREADY_IN_USE: 'Email is already in use',
|
|
18
|
-
TOO_MANY_REQUESTS: 'Too many requests. Please try again later',
|
|
19
|
-
USER_DISABLED: 'This account has been disabled',
|
|
20
|
-
GENERIC_ERROR: 'Authentication error occurred',
|
|
21
16
|
},
|
|
22
17
|
FIRESTORE: {
|
|
23
18
|
NOT_INITIALIZED: 'Firestore is not initialized',
|
|
@@ -30,12 +25,6 @@ export const ERROR_MESSAGES = {
|
|
|
30
25
|
PERMISSION_DENIED: 'Permission denied',
|
|
31
26
|
TRANSACTION_FAILED: 'Transaction failed',
|
|
32
27
|
NETWORK_ERROR: 'Network error occurred',
|
|
33
|
-
NOT_FOUND: 'Document not found',
|
|
34
|
-
UNAVAILABLE: 'Firestore service is unavailable',
|
|
35
|
-
GENERIC_ERROR: 'Firestore error occurred',
|
|
36
|
-
},
|
|
37
|
-
STORAGE: {
|
|
38
|
-
GENERIC_ERROR: 'Storage error occurred',
|
|
39
28
|
},
|
|
40
29
|
REPOSITORY: {
|
|
41
30
|
DESTROYED: 'Repository has been destroyed',
|
|
@@ -37,6 +37,8 @@ export {
|
|
|
37
37
|
calculatePercentage,
|
|
38
38
|
calculateRemaining,
|
|
39
39
|
safeFloor,
|
|
40
|
+
safeCeil,
|
|
41
|
+
clamp,
|
|
40
42
|
diffMs,
|
|
41
43
|
diffMinutes,
|
|
42
44
|
diffHours,
|
|
@@ -46,4 +48,7 @@ export {
|
|
|
46
48
|
hasMore,
|
|
47
49
|
getResultCount,
|
|
48
50
|
chunkArray,
|
|
51
|
+
sumArray,
|
|
52
|
+
averageArray,
|
|
53
|
+
roundToDecimals,
|
|
49
54
|
} from './calculation.util';
|