@umituz/react-native-firebase 1.13.136 → 1.13.138
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/auth/infrastructure/config/FirebaseAuthClient.ts +39 -43
- package/src/auth/infrastructure/services/apple-auth.service.ts +10 -20
- package/src/auth/infrastructure/services/base/base-auth.service.ts +104 -0
- package/src/auth/infrastructure/services/google-auth.service.ts +8 -23
- package/src/domain/guards/firebase-error.guard.ts +37 -28
- package/src/domain/utils/error-handler.util.ts +97 -9
- package/src/domain/utils/type-guards.util.ts +66 -0
- package/src/firestore/infrastructure/config/FirestoreClient.ts +29 -40
- package/src/firestore/infrastructure/middleware/QueryDeduplicationMiddleware.ts +5 -2
- package/src/firestore/utils/operation/operation-executor.util.ts +2 -2
- package/src/firestore/utils/quota-error-detector.util.ts +9 -78
- package/src/firestore/utils/result/result.util.ts +11 -1
- package/src/firestore/utils/transaction/transaction.util.ts +3 -3
- package/src/infrastructure/config/base/ClientStateManager.ts +82 -0
- package/src/infrastructure/config/base/ServiceClientSingleton.ts +152 -0
- package/src/infrastructure/config/state/FirebaseClientState.ts +5 -23
- package/src/infrastructure/config/validators/FirebaseConfigValidator.ts +15 -18
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-firebase",
|
|
3
|
-
"version": "1.13.
|
|
3
|
+
"version": "1.13.138",
|
|
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",
|
|
@@ -1,72 +1,68 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Firebase Auth Client - Infrastructure Layer
|
|
3
|
+
*
|
|
4
|
+
* Manages Firebase Authentication instance initialization
|
|
3
5
|
*/
|
|
4
6
|
|
|
5
7
|
import type { Auth } from 'firebase/auth';
|
|
6
8
|
import { getFirebaseApp } from '../../../infrastructure/config/FirebaseClient';
|
|
7
9
|
import { FirebaseAuthInitializer } from './initializers/FirebaseAuthInitializer';
|
|
8
10
|
import type { FirebaseAuthConfig } from '../../domain/value-objects/FirebaseAuthConfig';
|
|
11
|
+
import { ServiceClientSingleton } from '../../../infrastructure/config/base/ServiceClientSingleton';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Firebase Auth Client Singleton
|
|
15
|
+
*/
|
|
16
|
+
class FirebaseAuthClientSingleton extends ServiceClientSingleton<Auth, FirebaseAuthConfig> {
|
|
17
|
+
private constructor() {
|
|
18
|
+
super({
|
|
19
|
+
serviceName: 'FirebaseAuth',
|
|
20
|
+
initializer: (config?: FirebaseAuthConfig) => {
|
|
21
|
+
const app = getFirebaseApp();
|
|
22
|
+
if (!app) {
|
|
23
|
+
this.setError('Firebase App is not initialized');
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
const auth = FirebaseAuthInitializer.initialize(app, config);
|
|
27
|
+
if (!auth) {
|
|
28
|
+
this.setError('Auth initialization returned null');
|
|
29
|
+
}
|
|
30
|
+
return auth;
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
}
|
|
9
34
|
|
|
10
|
-
class FirebaseAuthClientSingleton {
|
|
11
35
|
private static instance: FirebaseAuthClientSingleton | null = null;
|
|
12
|
-
private auth: Auth | null = null;
|
|
13
|
-
private initializationError: string | null = null;
|
|
14
36
|
|
|
15
37
|
static getInstance(): FirebaseAuthClientSingleton {
|
|
16
38
|
if (!this.instance) this.instance = new FirebaseAuthClientSingleton();
|
|
17
39
|
return this.instance;
|
|
18
40
|
}
|
|
19
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Initialize Auth with optional configuration
|
|
44
|
+
*/
|
|
20
45
|
initialize(config?: FirebaseAuthConfig): Auth | null {
|
|
21
|
-
|
|
22
|
-
if (this.initializationError) return null;
|
|
23
|
-
|
|
24
|
-
try {
|
|
25
|
-
const app = getFirebaseApp();
|
|
26
|
-
if (!app) return null;
|
|
27
|
-
this.auth = FirebaseAuthInitializer.initialize(app, config);
|
|
28
|
-
if (!this.auth) {
|
|
29
|
-
this.initializationError = "Auth initialization returned null";
|
|
30
|
-
}
|
|
31
|
-
return this.auth;
|
|
32
|
-
} catch (error: unknown) {
|
|
33
|
-
const message = error instanceof Error ? error.message : "Unknown error";
|
|
34
|
-
if (__DEV__) console.error('[FirebaseAuth] Init error:', message);
|
|
35
|
-
this.initializationError = message;
|
|
36
|
-
return null;
|
|
37
|
-
}
|
|
46
|
+
return super.initialize(config);
|
|
38
47
|
}
|
|
39
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Get Auth instance
|
|
51
|
+
*/
|
|
40
52
|
getAuth(): Auth | null {
|
|
41
|
-
//
|
|
42
|
-
if (this.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
// Don't retry if Firebase app is not available
|
|
48
|
-
const app = getFirebaseApp();
|
|
49
|
-
if (!app) return null;
|
|
50
|
-
|
|
51
|
-
// Attempt initialization
|
|
52
|
-
this.initialize();
|
|
53
|
-
return this.auth;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
getInitializationError(): string | null {
|
|
57
|
-
return this.initializationError;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
reset(): void {
|
|
61
|
-
this.auth = null;
|
|
62
|
-
this.initializationError = null;
|
|
53
|
+
// Attempt initialization if not already initialized
|
|
54
|
+
if (!this.isInitialized() && !this.getInitializationError()) {
|
|
55
|
+
const app = getFirebaseApp();
|
|
56
|
+
if (app) this.initialize();
|
|
57
|
+
}
|
|
58
|
+
return this.getInstance();
|
|
63
59
|
}
|
|
64
60
|
}
|
|
65
61
|
|
|
66
62
|
export const firebaseAuthClient = FirebaseAuthClientSingleton.getInstance();
|
|
67
63
|
export const initializeFirebaseAuth = (c?: FirebaseAuthConfig) => firebaseAuthClient.initialize(c);
|
|
68
64
|
export const getFirebaseAuth = () => firebaseAuthClient.getAuth();
|
|
69
|
-
export const isFirebaseAuthInitialized = () => firebaseAuthClient.
|
|
65
|
+
export const isFirebaseAuthInitialized = () => firebaseAuthClient.isInitialized();
|
|
70
66
|
export const getFirebaseAuthInitializationError = () => firebaseAuthClient.getInitializationError();
|
|
71
67
|
export const resetFirebaseAuthClient = () => firebaseAuthClient.reset();
|
|
72
68
|
|
|
@@ -12,6 +12,12 @@ import * as AppleAuthentication from "expo-apple-authentication";
|
|
|
12
12
|
import { Platform } from "react-native";
|
|
13
13
|
import { generateNonce, hashNonce } from "./crypto.util";
|
|
14
14
|
import type { AppleAuthResult } from "./apple-auth.types";
|
|
15
|
+
import {
|
|
16
|
+
createSuccessResult,
|
|
17
|
+
createFailureResult,
|
|
18
|
+
logAuthError,
|
|
19
|
+
isCancellationError,
|
|
20
|
+
} from "./base/base-auth.service";
|
|
15
21
|
|
|
16
22
|
export class AppleAuthService {
|
|
17
23
|
async isAvailable(): Promise<boolean> {
|
|
@@ -61,17 +67,9 @@ export class AppleAuthService {
|
|
|
61
67
|
});
|
|
62
68
|
|
|
63
69
|
const userCredential = await signInWithCredential(auth, credential);
|
|
64
|
-
|
|
65
|
-
userCredential.user.metadata.creationTime ===
|
|
66
|
-
userCredential.user.metadata.lastSignInTime;
|
|
67
|
-
|
|
68
|
-
return {
|
|
69
|
-
success: true,
|
|
70
|
-
userCredential,
|
|
71
|
-
isNewUser,
|
|
72
|
-
};
|
|
70
|
+
return createSuccessResult(userCredential);
|
|
73
71
|
} catch (error) {
|
|
74
|
-
if (error
|
|
72
|
+
if (isCancellationError(error)) {
|
|
75
73
|
return {
|
|
76
74
|
success: false,
|
|
77
75
|
error: "Apple Sign-In was cancelled",
|
|
@@ -79,16 +77,8 @@ export class AppleAuthService {
|
|
|
79
77
|
};
|
|
80
78
|
}
|
|
81
79
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const errorCode = (error as { code?: string })?.code || 'unknown';
|
|
85
|
-
const errorMessage = error instanceof Error ? error.message : 'Apple sign-in failed';
|
|
86
|
-
|
|
87
|
-
return {
|
|
88
|
-
success: false,
|
|
89
|
-
error: errorMessage,
|
|
90
|
-
code: errorCode,
|
|
91
|
-
};
|
|
80
|
+
logAuthError('Apple Sign-In', error);
|
|
81
|
+
return createFailureResult(error);
|
|
92
82
|
}
|
|
93
83
|
}
|
|
94
84
|
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base Auth Service
|
|
3
|
+
*
|
|
4
|
+
* Provides common authentication service functionality
|
|
5
|
+
* Handles error processing, result formatting, and credential management
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { UserCredential } from 'firebase/auth';
|
|
9
|
+
import { toAuthErrorInfo } from '../../../../domain/utils/error-handler.util';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Base authentication result interface
|
|
13
|
+
*/
|
|
14
|
+
export interface BaseAuthResult {
|
|
15
|
+
readonly success: boolean;
|
|
16
|
+
readonly error?: string;
|
|
17
|
+
readonly code?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Successful authentication result
|
|
22
|
+
*/
|
|
23
|
+
export interface AuthSuccessResult extends BaseAuthResult {
|
|
24
|
+
readonly success: true;
|
|
25
|
+
readonly userCredential: UserCredential;
|
|
26
|
+
readonly isNewUser: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Failed authentication result
|
|
31
|
+
*/
|
|
32
|
+
export interface AuthFailureResult extends BaseAuthResult {
|
|
33
|
+
readonly success: false;
|
|
34
|
+
readonly error: string;
|
|
35
|
+
readonly code: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Combined auth result type
|
|
40
|
+
*/
|
|
41
|
+
export type AuthResult = AuthSuccessResult | AuthFailureResult;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Check if user is new based on metadata
|
|
45
|
+
*/
|
|
46
|
+
export function checkIsNewUser(userCredential: UserCredential): boolean {
|
|
47
|
+
return (
|
|
48
|
+
userCredential.user.metadata.creationTime ===
|
|
49
|
+
userCredential.user.metadata.lastSignInTime
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Extract error information from unknown error
|
|
55
|
+
*/
|
|
56
|
+
export function extractAuthError(error: unknown): { code: string; message: string } {
|
|
57
|
+
const errorInfo = toAuthErrorInfo(error);
|
|
58
|
+
return {
|
|
59
|
+
code: errorInfo.code,
|
|
60
|
+
message: errorInfo.message,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Check if error is a cancellation error
|
|
66
|
+
*/
|
|
67
|
+
export function isCancellationError(error: unknown): boolean {
|
|
68
|
+
if (error instanceof Error) {
|
|
69
|
+
return error.message.includes('ERR_CANCELED');
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Create failure result from error
|
|
76
|
+
*/
|
|
77
|
+
export function createFailureResult(error: unknown): AuthFailureResult {
|
|
78
|
+
const { code, message } = extractAuthError(error);
|
|
79
|
+
return {
|
|
80
|
+
success: false,
|
|
81
|
+
error: message,
|
|
82
|
+
code,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Create success result from user credential
|
|
88
|
+
*/
|
|
89
|
+
export function createSuccessResult(userCredential: UserCredential): AuthSuccessResult {
|
|
90
|
+
return {
|
|
91
|
+
success: true,
|
|
92
|
+
userCredential,
|
|
93
|
+
isNewUser: checkIsNewUser(userCredential),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Log auth error in development mode
|
|
99
|
+
*/
|
|
100
|
+
export function logAuthError(serviceName: string, error: unknown): void {
|
|
101
|
+
if (__DEV__) {
|
|
102
|
+
console.error(`[Firebase Auth] ${serviceName} failed:`, error);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -9,6 +9,11 @@ import {
|
|
|
9
9
|
type Auth,
|
|
10
10
|
} from "firebase/auth";
|
|
11
11
|
import type { GoogleAuthConfig, GoogleAuthResult } from "./google-auth.types";
|
|
12
|
+
import {
|
|
13
|
+
createSuccessResult,
|
|
14
|
+
createFailureResult,
|
|
15
|
+
logAuthError,
|
|
16
|
+
} from "./base/base-auth.service";
|
|
12
17
|
|
|
13
18
|
/**
|
|
14
19
|
* Google Auth Service
|
|
@@ -41,30 +46,10 @@ export class GoogleAuthService {
|
|
|
41
46
|
try {
|
|
42
47
|
const credential = GoogleAuthProvider.credential(idToken);
|
|
43
48
|
const userCredential = await signInWithCredential(auth, credential);
|
|
44
|
-
|
|
45
|
-
const isNewUser =
|
|
46
|
-
userCredential.user.metadata.creationTime ===
|
|
47
|
-
userCredential.user.metadata.lastSignInTime;
|
|
48
|
-
|
|
49
|
-
return {
|
|
50
|
-
success: true,
|
|
51
|
-
userCredential,
|
|
52
|
-
isNewUser,
|
|
53
|
-
};
|
|
49
|
+
return createSuccessResult(userCredential);
|
|
54
50
|
} catch (error) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Extract error code for better error handling
|
|
60
|
-
const errorCode = (error as { code?: string })?.code || 'unknown';
|
|
61
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
62
|
-
|
|
63
|
-
return {
|
|
64
|
-
success: false,
|
|
65
|
-
error: errorMessage,
|
|
66
|
-
code: errorCode,
|
|
67
|
-
};
|
|
51
|
+
logAuthError('Google Sign-In', error);
|
|
52
|
+
return createFailureResult(error);
|
|
68
53
|
}
|
|
69
54
|
}
|
|
70
55
|
}
|
|
@@ -8,6 +8,15 @@
|
|
|
8
8
|
|
|
9
9
|
import type { FirestoreError } from 'firebase/firestore';
|
|
10
10
|
import type { AuthError } from 'firebase/auth';
|
|
11
|
+
import { hasCodeProperty } from '../utils/type-guards.util';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Firebase error base interface
|
|
15
|
+
*/
|
|
16
|
+
interface FirebaseErrorBase {
|
|
17
|
+
code: string;
|
|
18
|
+
message: string;
|
|
19
|
+
}
|
|
11
20
|
|
|
12
21
|
/**
|
|
13
22
|
* Check if error is a Firebase Firestore error
|
|
@@ -33,54 +42,54 @@ export function isAuthError(error: unknown): error is AuthError {
|
|
|
33
42
|
);
|
|
34
43
|
}
|
|
35
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Check if error is a Firebase error (either Firestore or Auth)
|
|
47
|
+
*/
|
|
48
|
+
export function isFirebaseError(error: unknown): error is FirebaseErrorBase {
|
|
49
|
+
return isFirestoreError(error) || isAuthError(error);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Check if error has a specific code
|
|
54
|
+
*/
|
|
55
|
+
export function hasErrorCode(error: unknown, code: string): boolean {
|
|
56
|
+
return hasCodeProperty(error) && error.code === code;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Check if error code matches any of the provided codes
|
|
61
|
+
*/
|
|
62
|
+
export function hasAnyErrorCode(error: unknown, codes: string[]): boolean {
|
|
63
|
+
if (!hasCodeProperty(error)) return false;
|
|
64
|
+
return codes.includes(error.code);
|
|
65
|
+
}
|
|
66
|
+
|
|
36
67
|
/**
|
|
37
68
|
* Check if error is a network error
|
|
38
69
|
*/
|
|
39
70
|
export function isNetworkError(error: unknown): boolean {
|
|
40
|
-
|
|
41
|
-
return false;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const code = (error as FirestoreError | AuthError).code;
|
|
45
|
-
return (
|
|
46
|
-
code === 'unavailable' ||
|
|
47
|
-
code === 'network-request-failed' ||
|
|
48
|
-
code === 'timeout'
|
|
49
|
-
);
|
|
71
|
+
return hasAnyErrorCode(error, ['unavailable', 'network-request-failed', 'timeout']);
|
|
50
72
|
}
|
|
51
73
|
|
|
52
74
|
/**
|
|
53
75
|
* Check if error is a permission denied error
|
|
54
76
|
*/
|
|
55
77
|
export function isPermissionDeniedError(error: unknown): boolean {
|
|
56
|
-
|
|
57
|
-
return false;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const code = (error as FirestoreError | AuthError).code;
|
|
61
|
-
return code === 'permission-denied' || code === 'unauthorized';
|
|
78
|
+
return hasAnyErrorCode(error, ['permission-denied', 'unauthorized']);
|
|
62
79
|
}
|
|
63
80
|
|
|
64
81
|
/**
|
|
65
82
|
* Check if error is a not found error
|
|
66
83
|
*/
|
|
67
84
|
export function isNotFoundError(error: unknown): boolean {
|
|
68
|
-
|
|
69
|
-
return false;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return (error as FirestoreError).code === 'not-found';
|
|
85
|
+
return hasErrorCode(error, 'not-found');
|
|
73
86
|
}
|
|
74
87
|
|
|
75
88
|
/**
|
|
76
89
|
* Check if error is a quota exceeded error
|
|
77
90
|
*/
|
|
78
91
|
export function isQuotaExceededError(error: unknown): boolean {
|
|
79
|
-
|
|
80
|
-
return false;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return (error as FirestoreError).code === 'resource-exhausted';
|
|
92
|
+
return hasErrorCode(error, 'resource-exhausted');
|
|
84
93
|
}
|
|
85
94
|
|
|
86
95
|
/**
|
|
@@ -88,7 +97,7 @@ export function isQuotaExceededError(error: unknown): boolean {
|
|
|
88
97
|
* Returns error message or unknown error message
|
|
89
98
|
*/
|
|
90
99
|
export function getSafeErrorMessage(error: unknown): string {
|
|
91
|
-
if (
|
|
100
|
+
if (isFirebaseError(error)) {
|
|
92
101
|
return error.message;
|
|
93
102
|
}
|
|
94
103
|
|
|
@@ -108,7 +117,7 @@ export function getSafeErrorMessage(error: unknown): string {
|
|
|
108
117
|
* Returns error code or unknown error code
|
|
109
118
|
*/
|
|
110
119
|
export function getSafeErrorCode(error: unknown): string {
|
|
111
|
-
if (
|
|
120
|
+
if (hasCodeProperty(error)) {
|
|
112
121
|
return error.code;
|
|
113
122
|
}
|
|
114
123
|
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* Centralized error handling utilities for Firebase operations
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { hasCodeProperty, hasMessageProperty, hasCodeAndMessageProperties } from './type-guards.util';
|
|
7
|
+
|
|
6
8
|
/**
|
|
7
9
|
* Standard error structure with code and message
|
|
8
10
|
*/
|
|
@@ -11,15 +13,39 @@ export interface ErrorInfo {
|
|
|
11
13
|
message: string;
|
|
12
14
|
}
|
|
13
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Quota error codes
|
|
18
|
+
*/
|
|
19
|
+
const QUOTA_ERROR_CODES = [
|
|
20
|
+
'resource-exhausted',
|
|
21
|
+
'quota-exceeded',
|
|
22
|
+
'RESOURCE_EXHAUSTED',
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Quota error message patterns
|
|
27
|
+
*/
|
|
28
|
+
const QUOTA_ERROR_MESSAGES = [
|
|
29
|
+
'quota exceeded',
|
|
30
|
+
'quota limit',
|
|
31
|
+
'daily limit',
|
|
32
|
+
'resource exhausted',
|
|
33
|
+
'too many requests',
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Retryable error codes
|
|
38
|
+
*/
|
|
39
|
+
const RETRYABLE_ERROR_CODES = ['unavailable', 'deadline-exceeded', 'aborted'];
|
|
40
|
+
|
|
14
41
|
/**
|
|
15
42
|
* Convert unknown error to standard error info
|
|
16
43
|
* Handles Error objects, strings, and unknown types
|
|
17
44
|
*/
|
|
18
45
|
export function toErrorInfo(error: unknown): ErrorInfo {
|
|
19
46
|
if (error instanceof Error) {
|
|
20
|
-
const firebaseErr = error as { code?: string };
|
|
21
47
|
return {
|
|
22
|
-
code:
|
|
48
|
+
code: hasCodeProperty(error) ? error.code : 'unknown',
|
|
23
49
|
message: error.message,
|
|
24
50
|
};
|
|
25
51
|
}
|
|
@@ -35,9 +61,8 @@ export function toErrorInfo(error: unknown): ErrorInfo {
|
|
|
35
61
|
*/
|
|
36
62
|
export function toAuthErrorInfo(error: unknown): ErrorInfo {
|
|
37
63
|
if (error instanceof Error) {
|
|
38
|
-
const firebaseErr = error as { code?: string };
|
|
39
64
|
return {
|
|
40
|
-
code:
|
|
65
|
+
code: hasCodeProperty(error) && error.code ? error.code : 'auth/failed',
|
|
41
66
|
message: error.message,
|
|
42
67
|
};
|
|
43
68
|
}
|
|
@@ -58,13 +83,16 @@ export function hasErrorCode(error: ErrorInfo, code: string): boolean {
|
|
|
58
83
|
* Check if error is a cancelled/auth cancelled error
|
|
59
84
|
*/
|
|
60
85
|
export function isCancelledError(error: ErrorInfo): boolean {
|
|
61
|
-
return
|
|
86
|
+
return (
|
|
87
|
+
error.code === 'auth/cancelled' ||
|
|
88
|
+
error.message.includes('ERR_CANCELED')
|
|
89
|
+
);
|
|
62
90
|
}
|
|
63
91
|
|
|
64
92
|
/**
|
|
65
|
-
* Check if error is a quota
|
|
93
|
+
* Check if error info is a quota error
|
|
66
94
|
*/
|
|
67
|
-
export function
|
|
95
|
+
export function isQuotaErrorInfo(error: ErrorInfo): boolean {
|
|
68
96
|
return (
|
|
69
97
|
error.code === 'quota-exceeded' ||
|
|
70
98
|
error.code.includes('quota') ||
|
|
@@ -73,7 +101,7 @@ export function isQuotaError(error: ErrorInfo): boolean {
|
|
|
73
101
|
}
|
|
74
102
|
|
|
75
103
|
/**
|
|
76
|
-
* Check if error is a network error
|
|
104
|
+
* Check if error info is a network error
|
|
77
105
|
*/
|
|
78
106
|
export function isNetworkError(error: ErrorInfo): boolean {
|
|
79
107
|
return (
|
|
@@ -83,8 +111,68 @@ export function isNetworkError(error: ErrorInfo): boolean {
|
|
|
83
111
|
}
|
|
84
112
|
|
|
85
113
|
/**
|
|
86
|
-
* Check if error is an authentication error
|
|
114
|
+
* Check if error info is an authentication error
|
|
87
115
|
*/
|
|
88
116
|
export function isAuthError(error: ErrorInfo): boolean {
|
|
89
117
|
return error.code.startsWith('auth/');
|
|
90
118
|
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Check if unknown error is a Firestore quota error
|
|
122
|
+
* Enhanced type guard with proper error checking
|
|
123
|
+
*/
|
|
124
|
+
export function isQuotaError(error: unknown): boolean {
|
|
125
|
+
if (!error || typeof error !== 'object') return false;
|
|
126
|
+
|
|
127
|
+
if (hasCodeProperty(error)) {
|
|
128
|
+
const code = error.code;
|
|
129
|
+
return QUOTA_ERROR_CODES.some(
|
|
130
|
+
(c) => code === c || code.endsWith(`/${c}`) || code.startsWith(`${c}/`)
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (hasMessageProperty(error)) {
|
|
135
|
+
const message = error.message.toLowerCase();
|
|
136
|
+
return QUOTA_ERROR_MESSAGES.some((m) => {
|
|
137
|
+
const pattern = m.toLowerCase();
|
|
138
|
+
return (
|
|
139
|
+
message.includes(` ${pattern} `) ||
|
|
140
|
+
message.startsWith(`${pattern} `) ||
|
|
141
|
+
message.endsWith(` ${pattern}`) ||
|
|
142
|
+
message === pattern
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Check if error is retryable
|
|
152
|
+
*/
|
|
153
|
+
export function isRetryableError(error: unknown): boolean {
|
|
154
|
+
if (!error || typeof error !== 'object') return false;
|
|
155
|
+
|
|
156
|
+
if (hasCodeProperty(error)) {
|
|
157
|
+
return RETRYABLE_ERROR_CODES.some((code) => error.code.includes(code));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Get user-friendly quota error message
|
|
165
|
+
*/
|
|
166
|
+
export function getQuotaErrorMessage(): string {
|
|
167
|
+
return 'Daily quota exceeded. Please try again tomorrow or upgrade your plan.';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Get user-friendly retryable error message
|
|
172
|
+
*/
|
|
173
|
+
export function getRetryableErrorMessage(): string {
|
|
174
|
+
return 'Temporary error occurred. Please try again.';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Re-export type guards for convenience
|
|
178
|
+
export { hasCodeProperty, hasMessageProperty, hasCodeAndMessageProperties };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type Guard Utilities
|
|
3
|
+
*
|
|
4
|
+
* Common type guards for Firebase and JavaScript objects.
|
|
5
|
+
* Provides type-safe checking without using 'as' assertions.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Type guard for objects with a 'code' property of type string
|
|
10
|
+
* Commonly used for Firebase errors and other error objects
|
|
11
|
+
*/
|
|
12
|
+
export function hasCodeProperty(error: unknown): error is { code: string } {
|
|
13
|
+
return (
|
|
14
|
+
typeof error === 'object' &&
|
|
15
|
+
error !== null &&
|
|
16
|
+
'code' in error &&
|
|
17
|
+
typeof (error as { code: unknown }).code === 'string'
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Type guard for objects with a 'message' property of type string
|
|
23
|
+
* Commonly used for Error objects
|
|
24
|
+
*/
|
|
25
|
+
export function hasMessageProperty(error: unknown): error is { message: string } {
|
|
26
|
+
return (
|
|
27
|
+
typeof error === 'object' &&
|
|
28
|
+
error !== null &&
|
|
29
|
+
'message' in error &&
|
|
30
|
+
typeof (error as { message: unknown }).message === 'string'
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Type guard for objects with both 'code' and 'message' properties
|
|
36
|
+
* Commonly used for Firebase error objects
|
|
37
|
+
*/
|
|
38
|
+
export function hasCodeAndMessageProperties(error: unknown): error is { code: string; message: string } {
|
|
39
|
+
return hasCodeProperty(error) && hasMessageProperty(error);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Type guard for objects with a 'name' property of type string
|
|
44
|
+
* Commonly used for Error objects
|
|
45
|
+
*/
|
|
46
|
+
export function hasNameProperty(error: unknown): error is { name: string } {
|
|
47
|
+
return (
|
|
48
|
+
typeof error === 'object' &&
|
|
49
|
+
error !== null &&
|
|
50
|
+
'name' in error &&
|
|
51
|
+
typeof (error as { name: unknown }).name === 'string'
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Type guard for objects with a 'stack' property of type string
|
|
57
|
+
* Commonly used for Error objects
|
|
58
|
+
*/
|
|
59
|
+
export function hasStackProperty(error: unknown): error is { stack: string } {
|
|
60
|
+
return (
|
|
61
|
+
typeof error === 'object' &&
|
|
62
|
+
error !== null &&
|
|
63
|
+
'stack' in error &&
|
|
64
|
+
typeof (error as { stack: unknown }).stack === 'string'
|
|
65
|
+
);
|
|
66
|
+
}
|
|
@@ -11,17 +11,33 @@
|
|
|
11
11
|
import type { Firestore } from 'firebase/firestore';
|
|
12
12
|
import { getFirebaseApp } from '../../../infrastructure/config/FirebaseClient';
|
|
13
13
|
import { FirebaseFirestoreInitializer } from './initializers/FirebaseFirestoreInitializer';
|
|
14
|
+
import { ServiceClientSingleton } from '../../../infrastructure/config/base/ServiceClientSingleton';
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Firestore Client Singleton
|
|
17
18
|
* Manages Firestore initialization
|
|
18
19
|
*/
|
|
19
|
-
class FirestoreClientSingleton {
|
|
20
|
-
private
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
class FirestoreClientSingleton extends ServiceClientSingleton<Firestore> {
|
|
21
|
+
private constructor() {
|
|
22
|
+
super({
|
|
23
|
+
serviceName: 'Firestore',
|
|
24
|
+
initializer: () => {
|
|
25
|
+
const app = getFirebaseApp();
|
|
26
|
+
if (!app) {
|
|
27
|
+
this.setError('Firebase App is not initialized');
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
return FirebaseFirestoreInitializer.initialize(app);
|
|
31
|
+
},
|
|
32
|
+
autoInitializer: () => {
|
|
33
|
+
const app = getFirebaseApp();
|
|
34
|
+
if (!app) return null;
|
|
35
|
+
return FirebaseFirestoreInitializer.initialize(app);
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
}
|
|
23
39
|
|
|
24
|
-
private
|
|
40
|
+
private static instance: FirestoreClientSingleton | null = null;
|
|
25
41
|
|
|
26
42
|
static getInstance(): FirestoreClientSingleton {
|
|
27
43
|
if (!FirestoreClientSingleton.instance) {
|
|
@@ -30,44 +46,18 @@ class FirestoreClientSingleton {
|
|
|
30
46
|
return FirestoreClientSingleton.instance;
|
|
31
47
|
}
|
|
32
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Initialize Firestore
|
|
51
|
+
*/
|
|
33
52
|
initialize(): Firestore | null {
|
|
34
|
-
|
|
35
|
-
if (this.initializationError) return null;
|
|
36
|
-
|
|
37
|
-
try {
|
|
38
|
-
const app = getFirebaseApp();
|
|
39
|
-
if (!app) return null;
|
|
40
|
-
|
|
41
|
-
this.firestore = FirebaseFirestoreInitializer.initialize(app);
|
|
42
|
-
return this.firestore;
|
|
43
|
-
} catch (error) {
|
|
44
|
-
this.initializationError =
|
|
45
|
-
error instanceof Error
|
|
46
|
-
? error.message
|
|
47
|
-
: 'Failed to initialize Firestore client';
|
|
48
|
-
return null;
|
|
49
|
-
}
|
|
53
|
+
return super.initialize();
|
|
50
54
|
}
|
|
51
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Get Firestore instance with auto-initialization
|
|
58
|
+
*/
|
|
52
59
|
getFirestore(): Firestore | null {
|
|
53
|
-
|
|
54
|
-
const app = getFirebaseApp();
|
|
55
|
-
if (app) this.initialize();
|
|
56
|
-
}
|
|
57
|
-
return this.firestore || null;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
isInitialized(): boolean {
|
|
61
|
-
return this.firestore !== null;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
getInitializationError(): string | null {
|
|
65
|
-
return this.initializationError;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
reset(): void {
|
|
69
|
-
this.firestore = null;
|
|
70
|
-
this.initializationError = null;
|
|
60
|
+
return this.getInstance(true);
|
|
71
61
|
}
|
|
72
62
|
}
|
|
73
63
|
|
|
@@ -94,4 +84,3 @@ export function resetFirestoreClient(): void {
|
|
|
94
84
|
}
|
|
95
85
|
|
|
96
86
|
export type { Firestore } from 'firebase/firestore';
|
|
97
|
-
|
|
@@ -33,7 +33,11 @@ export class QueryDeduplicationMiddleware {
|
|
|
33
33
|
if (this.queryManager.isPending(key)) {
|
|
34
34
|
const pendingPromise = this.queryManager.get(key);
|
|
35
35
|
if (pendingPromise) {
|
|
36
|
-
|
|
36
|
+
// Type assertion is safe here because the same key was used to store the promise
|
|
37
|
+
return Promise.race([pendingPromise]).then(() => {
|
|
38
|
+
// Retry the original query after pending completes
|
|
39
|
+
return queryFn();
|
|
40
|
+
});
|
|
37
41
|
}
|
|
38
42
|
}
|
|
39
43
|
|
|
@@ -59,4 +63,3 @@ export class QueryDeduplicationMiddleware {
|
|
|
59
63
|
|
|
60
64
|
export const queryDeduplicationMiddleware = new QueryDeduplicationMiddleware();
|
|
61
65
|
export type { QueryKey };
|
|
62
|
-
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import type { Firestore } from "../../infrastructure/config/FirestoreClient";
|
|
7
7
|
import { getFirestore } from "../../infrastructure/config/FirestoreClient";
|
|
8
8
|
import type { FirestoreResult } from "../result/result.util";
|
|
9
|
-
import {
|
|
9
|
+
import { createNoDbErrorResult } from "../result/result.util";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Execute a Firestore operation with automatic null check
|
|
@@ -17,7 +17,7 @@ export async function withFirestore<T>(
|
|
|
17
17
|
): Promise<FirestoreResult<T>> {
|
|
18
18
|
const db = getFirestore();
|
|
19
19
|
if (!db) {
|
|
20
|
-
return
|
|
20
|
+
return createNoDbErrorResult<T>();
|
|
21
21
|
}
|
|
22
22
|
return operation(db);
|
|
23
23
|
}
|
|
@@ -1,82 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Quota Error Detection Utilities
|
|
3
|
+
*
|
|
4
|
+
* Re-exports centralized quota error detection from error-handler.util.ts
|
|
5
|
+
* This maintains backwards compatibility while using a single source of truth.
|
|
3
6
|
*/
|
|
4
7
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const QUOTA_ERROR_MESSAGES = [
|
|
12
|
-
'quota exceeded',
|
|
13
|
-
'quota limit',
|
|
14
|
-
'daily limit',
|
|
15
|
-
'resource exhausted',
|
|
16
|
-
'too many requests',
|
|
17
|
-
];
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Type guard for error with code property
|
|
21
|
-
*/
|
|
22
|
-
function hasCodeProperty(error: unknown): error is { code: string } {
|
|
23
|
-
return typeof error === 'object' && error !== null && 'code' in error && typeof (error as { code: unknown }).code === 'string';
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Type guard for error with message property
|
|
28
|
-
*/
|
|
29
|
-
function hasMessageProperty(error: unknown): error is { message: string } {
|
|
30
|
-
return typeof error === 'object' && error !== null && 'message' in error && typeof (error as { message: unknown }).message === 'string';
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Check if error is a Firestore quota error
|
|
35
|
-
*/
|
|
36
|
-
export function isQuotaError(error: unknown): boolean {
|
|
37
|
-
if (!error || typeof error !== 'object') return false;
|
|
38
|
-
|
|
39
|
-
if (hasCodeProperty(error)) {
|
|
40
|
-
const code = error.code;
|
|
41
|
-
// Use more specific matching - exact match or ends with pattern
|
|
42
|
-
return QUOTA_ERROR_CODES.some((c) =>
|
|
43
|
-
code === c || code.endsWith(`/${c}`) || code.startsWith(`${c}/`)
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (hasMessageProperty(error)) {
|
|
48
|
-
const message = error.message;
|
|
49
|
-
const lowerMessage = message.toLowerCase();
|
|
50
|
-
// Use word boundaries to avoid partial matches
|
|
51
|
-
return QUOTA_ERROR_MESSAGES.some((m) =>
|
|
52
|
-
lowerMessage.includes(` ${m} `) ||
|
|
53
|
-
lowerMessage.startsWith(`${m} `) ||
|
|
54
|
-
lowerMessage.endsWith(` ${m}`) ||
|
|
55
|
-
lowerMessage === m
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return false;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Check if error is retryable
|
|
64
|
-
*/
|
|
65
|
-
export function isRetryableError(error: unknown): boolean {
|
|
66
|
-
if (!error || typeof error !== 'object') return false;
|
|
67
|
-
|
|
68
|
-
if (hasCodeProperty(error)) {
|
|
69
|
-
const code = error.code;
|
|
70
|
-
const retryableCodes = ['unavailable', 'deadline-exceeded', 'aborted'];
|
|
71
|
-
return retryableCodes.some((c) => code.includes(c));
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return false;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Get user-friendly quota error message
|
|
79
|
-
*/
|
|
80
|
-
export function getQuotaErrorMessage(): string {
|
|
81
|
-
return 'Daily quota exceeded. Please try again tomorrow or upgrade your plan.';
|
|
82
|
-
}
|
|
8
|
+
export {
|
|
9
|
+
isQuotaError,
|
|
10
|
+
isRetryableError,
|
|
11
|
+
getQuotaErrorMessage,
|
|
12
|
+
getRetryableErrorMessage,
|
|
13
|
+
} from '../../domain/utils/error-handler.util';
|
|
@@ -26,10 +26,17 @@ export function createErrorResult<T>(message: string, code: string): FirestoreRe
|
|
|
26
26
|
/**
|
|
27
27
|
* Create a standard success result
|
|
28
28
|
*/
|
|
29
|
-
export function
|
|
29
|
+
export function createFirestoreSuccessResult<T>(data?: T): FirestoreResult<T> {
|
|
30
30
|
return { success: true, data };
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Create no-db error result with proper typing
|
|
35
|
+
*/
|
|
36
|
+
export function createNoDbErrorResult<T>(): FirestoreResult<T> {
|
|
37
|
+
return { success: false, error: NO_DB_ERROR.error };
|
|
38
|
+
}
|
|
39
|
+
|
|
33
40
|
/**
|
|
34
41
|
* Check if result is successful
|
|
35
42
|
*/
|
|
@@ -43,3 +50,6 @@ export function isSuccess<T>(result: FirestoreResult<T>): result is FirestoreRes
|
|
|
43
50
|
export function isError<T>(result: FirestoreResult<T>): result is FirestoreResult<T> & { success: false } {
|
|
44
51
|
return !result.success;
|
|
45
52
|
}
|
|
53
|
+
|
|
54
|
+
// Keep old function name for backwards compatibility
|
|
55
|
+
export const createSuccessResult = createFirestoreSuccessResult;
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
type Transaction,
|
|
10
10
|
} from "firebase/firestore";
|
|
11
11
|
import { getFirestore } from "../../infrastructure/config/FirestoreClient";
|
|
12
|
-
import
|
|
12
|
+
import { hasCodeProperty } from "../../../domain/utils/type-guards.util";
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Execute a transaction with automatic DB instance check.
|
|
@@ -23,10 +23,10 @@ export async function runTransaction<T>(
|
|
|
23
23
|
throw new Error("[runTransaction] Firestore database is not initialized. Please ensure Firebase is properly initialized before running transactions.");
|
|
24
24
|
}
|
|
25
25
|
try {
|
|
26
|
-
return await fbRunTransaction(db
|
|
26
|
+
return await fbRunTransaction(db, updateFunction);
|
|
27
27
|
} catch (error) {
|
|
28
28
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
29
|
-
const errorCode = error
|
|
29
|
+
const errorCode = hasCodeProperty(error) ? error.code : 'unknown';
|
|
30
30
|
|
|
31
31
|
if (__DEV__) {
|
|
32
32
|
console.error(`[runTransaction] Transaction failed (Code: ${errorCode}):`, errorMessage);
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client State Manager
|
|
3
|
+
*
|
|
4
|
+
* Generic state management for Firebase service clients.
|
|
5
|
+
* Provides centralized state tracking for initialization status, errors, and instances.
|
|
6
|
+
*
|
|
7
|
+
* @template TInstance - The service instance type (e.g., FirebaseApp, Firestore, Auth)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface ClientState<TInstance> {
|
|
11
|
+
instance: TInstance | null;
|
|
12
|
+
initializationError: string | null;
|
|
13
|
+
isInitialized: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Generic client state manager
|
|
18
|
+
* Handles initialization state, error tracking, and instance management
|
|
19
|
+
*/
|
|
20
|
+
export class ClientStateManager<TInstance> {
|
|
21
|
+
private state: ClientState<TInstance>;
|
|
22
|
+
|
|
23
|
+
constructor() {
|
|
24
|
+
this.state = {
|
|
25
|
+
instance: null,
|
|
26
|
+
initializationError: null,
|
|
27
|
+
isInitialized: false,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get the current instance
|
|
33
|
+
*/
|
|
34
|
+
getInstance(): TInstance | null {
|
|
35
|
+
return this.state.instance;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Set the instance
|
|
40
|
+
*/
|
|
41
|
+
setInstance(instance: TInstance | null): void {
|
|
42
|
+
this.state.instance = instance;
|
|
43
|
+
this.state.isInitialized = instance !== null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Check if the service is initialized
|
|
48
|
+
*/
|
|
49
|
+
isInitialized(): boolean {
|
|
50
|
+
return this.state.isInitialized;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get the initialization error if any
|
|
55
|
+
*/
|
|
56
|
+
getInitializationError(): string | null {
|
|
57
|
+
return this.state.initializationError;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Set the initialization error
|
|
62
|
+
*/
|
|
63
|
+
setInitializationError(error: string | null): void {
|
|
64
|
+
this.state.initializationError = error;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Reset the state
|
|
69
|
+
*/
|
|
70
|
+
reset(): void {
|
|
71
|
+
this.state.instance = null;
|
|
72
|
+
this.state.initializationError = null;
|
|
73
|
+
this.state.isInitialized = false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get the current state (read-only)
|
|
78
|
+
*/
|
|
79
|
+
getState(): Readonly<ClientState<TInstance>> {
|
|
80
|
+
return this.state;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service Client Singleton Base Class
|
|
3
|
+
*
|
|
4
|
+
* Provides a generic singleton pattern for Firebase service clients.
|
|
5
|
+
* Eliminates code duplication across FirebaseClient, FirestoreClient, FirebaseAuthClient.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Generic singleton pattern
|
|
9
|
+
* - Initialization state management
|
|
10
|
+
* - Error handling and tracking
|
|
11
|
+
* - Automatic cleanup
|
|
12
|
+
*
|
|
13
|
+
* @template TInstance - The service instance type (e.g., Firestore, Auth)
|
|
14
|
+
* @template TConfig - The configuration type (optional)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export interface ServiceClientState<TInstance> {
|
|
18
|
+
instance: TInstance | null;
|
|
19
|
+
initializationError: string | null;
|
|
20
|
+
isInitialized: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ServiceClientOptions<TInstance, TConfig = unknown> {
|
|
24
|
+
serviceName: string;
|
|
25
|
+
initializer?: (config?: TConfig) => TInstance | null;
|
|
26
|
+
autoInitializer?: () => TInstance | null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Generic service client singleton base class
|
|
31
|
+
* Provides common initialization, state management, and error handling
|
|
32
|
+
*/
|
|
33
|
+
export class ServiceClientSingleton<TInstance, TConfig = unknown> {
|
|
34
|
+
private static instances = new Map<string, ServiceClientSingleton<unknown, unknown>>();
|
|
35
|
+
|
|
36
|
+
protected state: ServiceClientState<TInstance>;
|
|
37
|
+
private readonly options: ServiceClientOptions<TInstance, TConfig>;
|
|
38
|
+
|
|
39
|
+
constructor(options: ServiceClientOptions<TInstance, TConfig>) {
|
|
40
|
+
this.options = options;
|
|
41
|
+
this.state = {
|
|
42
|
+
instance: null,
|
|
43
|
+
initializationError: null,
|
|
44
|
+
isInitialized: false,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Initialize the service with optional configuration
|
|
50
|
+
*/
|
|
51
|
+
initialize(config?: TConfig): TInstance | null {
|
|
52
|
+
if (this.state.isInitialized && this.state.instance) {
|
|
53
|
+
return this.state.instance;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (this.state.initializationError) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const instance = this.options.initializer ? this.options.initializer(config) : null;
|
|
62
|
+
if (instance) {
|
|
63
|
+
this.state.instance = instance;
|
|
64
|
+
this.state.isInitialized = true;
|
|
65
|
+
}
|
|
66
|
+
return instance;
|
|
67
|
+
} catch (error) {
|
|
68
|
+
const errorMessage = error instanceof Error ? error.message : `Failed to initialize ${this.options.serviceName}`;
|
|
69
|
+
this.state.initializationError = errorMessage;
|
|
70
|
+
|
|
71
|
+
if (__DEV__) {
|
|
72
|
+
console.error(`[${this.options.serviceName}] Initialization failed:`, errorMessage);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get the service instance, auto-initializing if needed
|
|
81
|
+
*/
|
|
82
|
+
getInstance(autoInit: boolean = false): TInstance | null {
|
|
83
|
+
if (this.state.instance) {
|
|
84
|
+
return this.state.instance;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (this.state.initializationError) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (autoInit && this.options.autoInitializer) {
|
|
92
|
+
try {
|
|
93
|
+
const instance = this.options.autoInitializer();
|
|
94
|
+
if (instance) {
|
|
95
|
+
this.state.instance = instance;
|
|
96
|
+
this.state.isInitialized = true;
|
|
97
|
+
}
|
|
98
|
+
return instance;
|
|
99
|
+
} catch (error) {
|
|
100
|
+
const errorMessage = error instanceof Error ? error.message : `Failed to initialize ${this.options.serviceName}`;
|
|
101
|
+
this.state.initializationError = errorMessage;
|
|
102
|
+
|
|
103
|
+
if (__DEV__) {
|
|
104
|
+
console.error(`[${this.options.serviceName}] Auto-initialization failed:`, errorMessage);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Check if the service is initialized
|
|
114
|
+
*/
|
|
115
|
+
isInitialized(): boolean {
|
|
116
|
+
return this.state.isInitialized;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Get the initialization error if any
|
|
121
|
+
*/
|
|
122
|
+
getInitializationError(): string | null {
|
|
123
|
+
return this.state.initializationError;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Reset the service state
|
|
128
|
+
*/
|
|
129
|
+
reset(): void {
|
|
130
|
+
this.state.instance = null;
|
|
131
|
+
this.state.initializationError = null;
|
|
132
|
+
this.state.isInitialized = false;
|
|
133
|
+
|
|
134
|
+
if (__DEV__) {
|
|
135
|
+
console.log(`[${this.options.serviceName}] Service reset`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Get the current instance without initialization
|
|
141
|
+
*/
|
|
142
|
+
protected getCurrentInstance(): TInstance | null {
|
|
143
|
+
return this.state.instance;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Set initialization error
|
|
148
|
+
*/
|
|
149
|
+
protected setError(error: string): void {
|
|
150
|
+
this.state.initializationError = error;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -3,36 +3,18 @@
|
|
|
3
3
|
* Manages the state of Firebase initialization
|
|
4
4
|
*
|
|
5
5
|
* Single Responsibility: Only manages initialization state
|
|
6
|
+
* Uses generic ClientStateManager for shared functionality
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import type { FirebaseApp } from '../initializers/FirebaseAppInitializer';
|
|
10
|
+
import { ClientStateManager } from '../base/ClientStateManager';
|
|
9
11
|
|
|
10
|
-
export class FirebaseClientState {
|
|
11
|
-
private app: FirebaseApp | null = null;
|
|
12
|
-
private initializationError: string | null = null;
|
|
13
|
-
|
|
12
|
+
export class FirebaseClientState extends ClientStateManager<FirebaseApp> {
|
|
14
13
|
getApp(): FirebaseApp | null {
|
|
15
|
-
return this.
|
|
14
|
+
return this.getInstance();
|
|
16
15
|
}
|
|
17
16
|
|
|
18
17
|
setApp(app: FirebaseApp | null): void {
|
|
19
|
-
this.app
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
isInitialized(): boolean {
|
|
23
|
-
return this.app !== null;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
getInitializationError(): string | null {
|
|
27
|
-
return this.initializationError;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
setInitializationError(error: string | null): void {
|
|
31
|
-
this.initializationError = error;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
reset(): void {
|
|
35
|
-
this.app = null;
|
|
36
|
-
this.initializationError = null;
|
|
18
|
+
this.setInstance(app);
|
|
37
19
|
}
|
|
38
20
|
}
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
* Firebase Configuration Validator
|
|
3
3
|
*
|
|
4
4
|
* Single Responsibility: Validates Firebase configuration
|
|
5
|
+
* Uses centralized validation utilities from validation.util.ts
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import type { FirebaseConfig } from '../../../domain/value-objects/FirebaseConfig';
|
|
8
9
|
import { FirebaseConfigurationError } from '../../../domain/errors/FirebaseError';
|
|
10
|
+
import { isValidString, isValidFirebaseApiKey, isValidFirebaseProjectId } from '../../../domain/utils/validation.util';
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* Validation rule interface
|
|
@@ -15,25 +17,28 @@ interface ValidationRule {
|
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
/**
|
|
18
|
-
* Required field validation rule
|
|
20
|
+
* Required field validation rule using centralized validation
|
|
19
21
|
*/
|
|
20
22
|
class RequiredFieldRule implements ValidationRule {
|
|
21
23
|
constructor(
|
|
22
24
|
private fieldName: string,
|
|
23
|
-
private getter: (config: FirebaseConfig) => string | undefined
|
|
25
|
+
private getter: (config: FirebaseConfig) => string | undefined,
|
|
26
|
+
private customValidator?: (value: string) => boolean
|
|
24
27
|
) {}
|
|
25
28
|
|
|
26
29
|
validate(config: FirebaseConfig): void {
|
|
27
30
|
const value = this.getter(config);
|
|
28
|
-
|
|
29
|
-
if (!value
|
|
31
|
+
|
|
32
|
+
if (!isValidString(value)) {
|
|
30
33
|
throw new FirebaseConfigurationError(
|
|
31
|
-
`Firebase ${this.fieldName} is required and must be a string`
|
|
34
|
+
`Firebase ${this.fieldName} is required and must be a non-empty string`
|
|
32
35
|
);
|
|
33
36
|
}
|
|
34
37
|
|
|
35
|
-
if (
|
|
36
|
-
throw new FirebaseConfigurationError(
|
|
38
|
+
if (this.customValidator && !this.customValidator(value)) {
|
|
39
|
+
throw new FirebaseConfigurationError(
|
|
40
|
+
`Firebase ${this.fieldName} format is invalid`
|
|
41
|
+
);
|
|
37
42
|
}
|
|
38
43
|
}
|
|
39
44
|
}
|
|
@@ -50,7 +55,7 @@ class PlaceholderRule implements ValidationRule {
|
|
|
50
55
|
|
|
51
56
|
validate(config: FirebaseConfig): void {
|
|
52
57
|
const value = this.getter(config);
|
|
53
|
-
|
|
58
|
+
|
|
54
59
|
if (value && value.includes(this.placeholder)) {
|
|
55
60
|
throw new FirebaseConfigurationError(
|
|
56
61
|
`Please replace placeholder values with actual Firebase credentials for ${this.fieldName}`
|
|
@@ -64,9 +69,9 @@ class PlaceholderRule implements ValidationRule {
|
|
|
64
69
|
*/
|
|
65
70
|
export class FirebaseConfigValidator {
|
|
66
71
|
private static rules: ValidationRule[] = [
|
|
67
|
-
new RequiredFieldRule('API Key', config => config.apiKey),
|
|
72
|
+
new RequiredFieldRule('API Key', config => config.apiKey, isValidFirebaseApiKey),
|
|
68
73
|
new RequiredFieldRule('Auth Domain', config => config.authDomain),
|
|
69
|
-
new RequiredFieldRule('Project ID', config => config.projectId),
|
|
74
|
+
new RequiredFieldRule('Project ID', config => config.projectId, isValidFirebaseProjectId),
|
|
70
75
|
new PlaceholderRule('API Key', config => config.apiKey, 'your_firebase_api_key'),
|
|
71
76
|
new PlaceholderRule('Project ID', config => config.projectId, 'your-project-id'),
|
|
72
77
|
];
|
|
@@ -81,11 +86,3 @@ export class FirebaseConfigValidator {
|
|
|
81
86
|
}
|
|
82
87
|
}
|
|
83
88
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|