@umituz/react-native-firebase 1.13.120 → 1.13.122
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 +3 -3
- package/src/auth/infrastructure/services/account-deletion.service.ts +4 -4
- package/src/auth/infrastructure/services/auth-utils.service.ts +2 -1
- package/src/auth/infrastructure/services/reauthentication.service.ts +15 -28
- package/src/domain/utils/error-handler.util.ts +90 -0
- package/src/domain/utils/id-generator.util.ts +50 -0
- package/src/domain/utils/validation.util.ts +133 -0
- package/src/firestore/infrastructure/middleware/QueryDeduplicationMiddleware.ts +26 -117
- package/src/firestore/infrastructure/services/RequestLoggerService.ts +2 -8
- package/src/firestore/utils/deduplication/index.ts +13 -0
- package/src/firestore/utils/deduplication/pending-query-manager.util.ts +93 -0
- package/src/firestore/utils/deduplication/query-key-generator.util.ts +41 -0
- package/src/firestore/utils/deduplication/timer-manager.util.ts +59 -0
- package/src/firestore/utils/document-mapper.helper.ts +45 -37
- package/src/firestore/utils/firestore-helper.ts +21 -85
- package/src/firestore/utils/mapper/base-mapper.util.ts +42 -0
- package/src/firestore/utils/mapper/enrichment-mapper.util.ts +82 -0
- package/src/firestore/utils/mapper/index.ts +8 -0
- package/src/firestore/utils/mapper/multi-enrichment-mapper.util.ts +39 -0
- package/src/firestore/utils/operation/operation-executor.util.ts +49 -0
- package/src/firestore/utils/query/filters.util.ts +75 -0
- package/src/firestore/utils/query/index.ts +10 -0
- package/src/firestore/utils/query/modifiers.util.ts +65 -0
- package/src/firestore/utils/query-builder.ts +7 -108
- package/src/firestore/utils/result/result.util.ts +45 -0
- package/src/firestore/utils/transaction/transaction.util.ts +45 -0
- package/src/index.ts +0 -1
- package/src/infrastructure/config/FirebaseConfigLoader.ts +9 -13
- package/src/storage/deleter/README.md +0 -370
- package/src/storage/deleter.ts +0 -109
- package/src/storage/index.ts +0 -8
- package/src/storage/storage-instance.ts +0 -11
- package/src/storage/types/README.md +0 -313
- package/src/storage/types.ts +0 -19
- package/src/storage/uploader.ts +0 -106
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.122",
|
|
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",
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
"expo-apple-authentication": ">=6.0.0",
|
|
37
37
|
"expo-crypto": ">=13.0.0",
|
|
38
38
|
"firebase": ">=10.0.0",
|
|
39
|
-
"react": ">=
|
|
40
|
-
"react-native": ">=0.
|
|
39
|
+
"react": ">=19.0.0",
|
|
40
|
+
"react-native": ">=0.81.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@expo/vector-icons": "^15.0.3",
|
|
@@ -9,8 +9,8 @@ import {
|
|
|
9
9
|
reauthenticateWithApple,
|
|
10
10
|
reauthenticateWithPassword,
|
|
11
11
|
reauthenticateWithGoogle,
|
|
12
|
-
toAuthError,
|
|
13
12
|
} from "./reauthentication.service";
|
|
13
|
+
import { toAuthErrorInfo } from "../../../domain/utils/error-handler.util";
|
|
14
14
|
import type { AccountDeletionResult, AccountDeletionOptions } from "./reauthentication.types";
|
|
15
15
|
|
|
16
16
|
export type { AccountDeletionResult, AccountDeletionOptions } from "./reauthentication.types";
|
|
@@ -34,7 +34,7 @@ export async function deleteCurrentUser(
|
|
|
34
34
|
await deleteUser(user);
|
|
35
35
|
return { success: true };
|
|
36
36
|
} catch (error: unknown) {
|
|
37
|
-
const authErr =
|
|
37
|
+
const authErr = toAuthErrorInfo(error);
|
|
38
38
|
if (authErr.code === "auth/requires-recent-login" && (options.autoReauthenticate || options.password || options.googleIdToken)) {
|
|
39
39
|
const reauth = await attemptReauth(user, options);
|
|
40
40
|
if (reauth) return reauth;
|
|
@@ -64,7 +64,7 @@ async function attemptReauth(user: User, options: AccountDeletionOptions): Promi
|
|
|
64
64
|
await deleteUser(user);
|
|
65
65
|
return { success: true };
|
|
66
66
|
} catch (err: unknown) {
|
|
67
|
-
const authErr =
|
|
67
|
+
const authErr = toAuthErrorInfo(err);
|
|
68
68
|
return { success: false, error: { ...authErr, requiresReauth: false } };
|
|
69
69
|
}
|
|
70
70
|
}
|
|
@@ -77,7 +77,7 @@ export async function deleteUserAccount(user: User | null): Promise<AccountDelet
|
|
|
77
77
|
await deleteUser(user);
|
|
78
78
|
return { success: true };
|
|
79
79
|
} catch (error: unknown) {
|
|
80
|
-
const authErr =
|
|
80
|
+
const authErr = toAuthErrorInfo(error);
|
|
81
81
|
return { success: false, error: { ...authErr, requiresReauth: authErr.code === "auth/requires-recent-login" } };
|
|
82
82
|
}
|
|
83
83
|
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import type { User, Auth } from 'firebase/auth';
|
|
7
7
|
import { getFirebaseAuth } from '../config/FirebaseAuthClient';
|
|
8
|
+
import { isValidString } from '../../../domain/utils/validation.util';
|
|
8
9
|
|
|
9
10
|
export interface AuthCheckResult {
|
|
10
11
|
isAuthenticated: boolean;
|
|
@@ -120,7 +121,7 @@ export function isValidUser(user: unknown): user is User {
|
|
|
120
121
|
typeof user === 'object' &&
|
|
121
122
|
user !== null &&
|
|
122
123
|
'uid' in user &&
|
|
123
|
-
|
|
124
|
+
isValidString(user.uid)
|
|
124
125
|
);
|
|
125
126
|
}
|
|
126
127
|
|
|
@@ -13,32 +13,19 @@ import {
|
|
|
13
13
|
import * as AppleAuthentication from "expo-apple-authentication";
|
|
14
14
|
import { Platform } from "react-native";
|
|
15
15
|
import { generateNonce, hashNonce } from "./crypto.util";
|
|
16
|
-
import
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
import { toAuthErrorInfo, isCancelledError } from "../../../domain/utils/error-handler.util";
|
|
17
|
+
import type {
|
|
18
|
+
ReauthenticationResult,
|
|
19
|
+
AuthProviderType,
|
|
20
|
+
ReauthCredentialResult
|
|
20
21
|
} from "./reauthentication.types";
|
|
21
22
|
|
|
22
|
-
export type {
|
|
23
|
-
ReauthenticationResult,
|
|
24
|
-
AuthProviderType,
|
|
25
|
-
ReauthCredentialResult
|
|
23
|
+
export type {
|
|
24
|
+
ReauthenticationResult,
|
|
25
|
+
AuthProviderType,
|
|
26
|
+
ReauthCredentialResult
|
|
26
27
|
} from "./reauthentication.types";
|
|
27
28
|
|
|
28
|
-
export function toAuthError(error: unknown): { code: string; message: string } {
|
|
29
|
-
if (error instanceof Error) {
|
|
30
|
-
const firebaseErr = error as { code?: string };
|
|
31
|
-
return {
|
|
32
|
-
code: firebaseErr.code || "auth/failed",
|
|
33
|
-
message: error.message,
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
return {
|
|
37
|
-
code: "auth/failed",
|
|
38
|
-
message: typeof error === 'string' ? error : "Unknown error",
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
29
|
export function getUserAuthProvider(user: User): AuthProviderType {
|
|
43
30
|
if (user.isAnonymous) return "anonymous";
|
|
44
31
|
const data = user.providerData;
|
|
@@ -54,7 +41,7 @@ export async function reauthenticateWithGoogle(user: User, idToken: string): Pro
|
|
|
54
41
|
await reauthenticateWithCredential(user, GoogleAuthProvider.credential(idToken));
|
|
55
42
|
return { success: true };
|
|
56
43
|
} catch (error: unknown) {
|
|
57
|
-
const err =
|
|
44
|
+
const err = toAuthErrorInfo(error);
|
|
58
45
|
return { success: false, error: err };
|
|
59
46
|
}
|
|
60
47
|
}
|
|
@@ -65,7 +52,7 @@ export async function reauthenticateWithPassword(user: User, pass: string): Prom
|
|
|
65
52
|
await reauthenticateWithCredential(user, EmailAuthProvider.credential(user.email, pass));
|
|
66
53
|
return { success: true };
|
|
67
54
|
} catch (error: unknown) {
|
|
68
|
-
const err =
|
|
55
|
+
const err = toAuthErrorInfo(error);
|
|
69
56
|
return { success: false, error: err };
|
|
70
57
|
}
|
|
71
58
|
}
|
|
@@ -73,7 +60,7 @@ export async function reauthenticateWithPassword(user: User, pass: string): Prom
|
|
|
73
60
|
export async function getAppleReauthCredential(): Promise<ReauthCredentialResult> {
|
|
74
61
|
if (Platform.OS !== "ios") return { success: false, error: { code: "auth/ios-only", message: "iOS only" } };
|
|
75
62
|
try {
|
|
76
|
-
if (!(await AppleAuthentication.isAvailableAsync()))
|
|
63
|
+
if (!(await AppleAuthentication.isAvailableAsync()))
|
|
77
64
|
return { success: false, error: { code: "auth/unavailable", message: "Unavailable" } };
|
|
78
65
|
|
|
79
66
|
const nonce = await generateNonce();
|
|
@@ -86,8 +73,8 @@ export async function getAppleReauthCredential(): Promise<ReauthCredentialResult
|
|
|
86
73
|
if (!apple.identityToken) return { success: false, error: { code: "auth/no-token", message: "No token" } };
|
|
87
74
|
return { success: true, credential: new OAuthProvider("apple.com").credential({ idToken: apple.identityToken, rawNonce: nonce }) };
|
|
88
75
|
} catch (error: unknown) {
|
|
89
|
-
const err =
|
|
90
|
-
const code = err
|
|
76
|
+
const err = toAuthErrorInfo(error);
|
|
77
|
+
const code = isCancelledError(err) ? "auth/cancelled" : err.code;
|
|
91
78
|
return { success: false, error: { code, message: err.message } };
|
|
92
79
|
}
|
|
93
80
|
}
|
|
@@ -99,7 +86,7 @@ export async function reauthenticateWithApple(user: User): Promise<Reauthenticat
|
|
|
99
86
|
await reauthenticateWithCredential(user, res.credential);
|
|
100
87
|
return { success: true };
|
|
101
88
|
} catch (error: unknown) {
|
|
102
|
-
const err =
|
|
89
|
+
const err = toAuthErrorInfo(error);
|
|
103
90
|
return { success: false, error: err };
|
|
104
91
|
}
|
|
105
92
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error Handler Utility
|
|
3
|
+
* Centralized error handling utilities for Firebase operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Standard error structure with code and message
|
|
8
|
+
*/
|
|
9
|
+
export interface ErrorInfo {
|
|
10
|
+
code: string;
|
|
11
|
+
message: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Convert unknown error to standard error info
|
|
16
|
+
* Handles Error objects, strings, and unknown types
|
|
17
|
+
*/
|
|
18
|
+
export function toErrorInfo(error: unknown): ErrorInfo {
|
|
19
|
+
if (error instanceof Error) {
|
|
20
|
+
const firebaseErr = error as { code?: string };
|
|
21
|
+
return {
|
|
22
|
+
code: firebaseErr.code || 'unknown',
|
|
23
|
+
message: error.message,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
code: 'unknown',
|
|
28
|
+
message: typeof error === 'string' ? error : 'Unknown error',
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Convert unknown error to auth error info
|
|
34
|
+
* Auth-specific error codes are prefixed with 'auth/'
|
|
35
|
+
*/
|
|
36
|
+
export function toAuthErrorInfo(error: unknown): ErrorInfo {
|
|
37
|
+
if (error instanceof Error) {
|
|
38
|
+
const firebaseErr = error as { code?: string };
|
|
39
|
+
return {
|
|
40
|
+
code: firebaseErr.code || 'auth/failed',
|
|
41
|
+
message: error.message,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
code: 'auth/failed',
|
|
46
|
+
message: typeof error === 'string' ? error : 'Unknown error',
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Check if error info has a specific error code
|
|
52
|
+
*/
|
|
53
|
+
export function hasErrorCode(error: ErrorInfo, code: string): boolean {
|
|
54
|
+
return error.code === code;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Check if error is a cancelled/auth cancelled error
|
|
59
|
+
*/
|
|
60
|
+
export function isCancelledError(error: ErrorInfo): boolean {
|
|
61
|
+
return error.code === 'auth/cancelled' || error.message.includes('ERR_CANCELED');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Check if error is a quota exceeded error
|
|
66
|
+
*/
|
|
67
|
+
export function isQuotaError(error: ErrorInfo): boolean {
|
|
68
|
+
return (
|
|
69
|
+
error.code === 'quota-exceeded' ||
|
|
70
|
+
error.code.includes('quota') ||
|
|
71
|
+
error.message.toLowerCase().includes('quota')
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Check if error is a network error
|
|
77
|
+
*/
|
|
78
|
+
export function isNetworkError(error: ErrorInfo): boolean {
|
|
79
|
+
return (
|
|
80
|
+
error.code.includes('network') ||
|
|
81
|
+
error.message.toLowerCase().includes('network')
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Check if error is an authentication error
|
|
87
|
+
*/
|
|
88
|
+
export function isAuthError(error: ErrorInfo): boolean {
|
|
89
|
+
return error.code.startsWith('auth/');
|
|
90
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ID Generator Utility
|
|
3
|
+
* Centralized ID generation utilities for unique identifiers
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Generate a unique ID using timestamp and random string
|
|
8
|
+
* Format: timestamp-randomstring
|
|
9
|
+
* @returns Unique identifier string
|
|
10
|
+
*/
|
|
11
|
+
export function generateUniqueId(): string {
|
|
12
|
+
return `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Generate a short random ID
|
|
17
|
+
* Useful for temporary identifiers where uniqueness within scope is sufficient
|
|
18
|
+
* @returns Random identifier string
|
|
19
|
+
*/
|
|
20
|
+
export function generateShortId(length: number = 8): string {
|
|
21
|
+
return Math.random().toString(36).substring(2, 2 + length);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Generate a UUID-like ID
|
|
26
|
+
* Format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
|
|
27
|
+
* @returns UUID-like identifier string
|
|
28
|
+
*/
|
|
29
|
+
export function generateUUID(): string {
|
|
30
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
31
|
+
const r = (Math.random() * 16) | 0;
|
|
32
|
+
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
33
|
+
return v.toString(16);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Generate a nanoid-like ID
|
|
39
|
+
* Uses URL-safe characters for better compatibility
|
|
40
|
+
* @param length - Length of the ID (default: 21)
|
|
41
|
+
* @returns URL-safe unique identifier string
|
|
42
|
+
*/
|
|
43
|
+
export function generateNanoId(length: number = 21): string {
|
|
44
|
+
const chars = 'ModuleSymbhasOwnPr-0123456789ABCDEFGHNRVfgctiUvz_KqYTJkLxpZXIjQW';
|
|
45
|
+
let result = '';
|
|
46
|
+
for (let i = 0; i < length; i++) {
|
|
47
|
+
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation Utility
|
|
3
|
+
* Centralized validation utilities for common patterns
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Check if a string is a valid non-empty value
|
|
8
|
+
*/
|
|
9
|
+
export function isValidString(value: unknown): value is string {
|
|
10
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Check if a string is empty or whitespace only
|
|
15
|
+
*/
|
|
16
|
+
export function isEmptyString(value: unknown): boolean {
|
|
17
|
+
return typeof value === 'string' && value.trim().length === 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Validate Firebase API key format
|
|
22
|
+
* Firebase API keys typically start with "AIza" followed by 35 characters
|
|
23
|
+
*/
|
|
24
|
+
export function isValidFirebaseApiKey(apiKey: string): boolean {
|
|
25
|
+
if (!isValidString(apiKey)) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
const apiKeyPattern = /^AIza[0-9A-Za-z_-]{35}$/;
|
|
29
|
+
return apiKeyPattern.test(apiKey.trim());
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Validate Firebase authDomain format
|
|
34
|
+
* Expected format: "projectId.firebaseapp.com" or "projectId.web.app"
|
|
35
|
+
*/
|
|
36
|
+
export function isValidFirebaseAuthDomain(authDomain: string): boolean {
|
|
37
|
+
if (!isValidString(authDomain)) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
const trimmed = authDomain.trim();
|
|
41
|
+
return (
|
|
42
|
+
trimmed.includes('.firebaseapp.com') ||
|
|
43
|
+
trimmed.includes('.web.app')
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Validate Firebase projectId format
|
|
49
|
+
* Project IDs must be 6-30 characters, lowercase, alphanumeric, and may contain hyphens
|
|
50
|
+
*/
|
|
51
|
+
export function isValidFirebaseProjectId(projectId: string): boolean {
|
|
52
|
+
if (!isValidString(projectId)) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
const pattern = /^[a-z0-9][a-z0-9-]{4,28}[a-z0-9]$/;
|
|
56
|
+
return pattern.test(projectId.trim());
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Validate URL format
|
|
61
|
+
*/
|
|
62
|
+
export function isValidUrl(url: string): boolean {
|
|
63
|
+
if (!isValidString(url)) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
new URL(url.trim());
|
|
68
|
+
return true;
|
|
69
|
+
} catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Validate HTTPS URL
|
|
76
|
+
*/
|
|
77
|
+
export function isValidHttpsUrl(url: string): boolean {
|
|
78
|
+
if (!isValidString(url)) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
const urlObj = new URL(url.trim());
|
|
83
|
+
return urlObj.protocol === 'https:';
|
|
84
|
+
} catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Validate email format
|
|
91
|
+
*/
|
|
92
|
+
export function isValidEmail(email: string): boolean {
|
|
93
|
+
if (!isValidString(email)) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
97
|
+
return emailPattern.test(email.trim());
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Check if value is defined (not null or undefined)
|
|
102
|
+
*/
|
|
103
|
+
export function isDefined<T>(value: T | null | undefined): value is T {
|
|
104
|
+
return value !== null && value !== undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Check if array is not empty
|
|
109
|
+
*/
|
|
110
|
+
export function isNonEmptyArray<T>(value: unknown): value is [T, ...T[]] {
|
|
111
|
+
return Array.isArray(value) && value.length > 0;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Check if number is in range
|
|
116
|
+
*/
|
|
117
|
+
export function isInRange(value: number, min: number, max: number): boolean {
|
|
118
|
+
return typeof value === 'number' && value >= min && value <= max;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Check if number is positive
|
|
123
|
+
*/
|
|
124
|
+
export function isPositive(value: number): boolean {
|
|
125
|
+
return typeof value === 'number' && value > 0;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Check if number is non-negative
|
|
130
|
+
*/
|
|
131
|
+
export function isNonNegative(value: number): boolean {
|
|
132
|
+
return typeof value === 'number' && value >= 0;
|
|
133
|
+
}
|
|
@@ -3,116 +3,25 @@
|
|
|
3
3
|
* Prevents duplicate Firestore queries within a short time window
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
6
|
+
import type { QueryKey } from '../../utils/deduplication/query-key-generator.util';
|
|
7
|
+
import { generateQueryKey } from '../../utils/deduplication/query-key-generator.util';
|
|
8
|
+
import { PendingQueryManager } from '../../utils/deduplication/pending-query-manager.util';
|
|
9
|
+
import { TimerManager } from '../../utils/deduplication/timer-manager.util';
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
filters: string;
|
|
14
|
-
limit?: number;
|
|
15
|
-
orderBy?: string;
|
|
16
|
-
}
|
|
11
|
+
const DEDUPLICATION_WINDOW_MS = 1000; // 1 second
|
|
12
|
+
const CLEANUP_INTERVAL_MS = 5000; // 5 seconds
|
|
17
13
|
|
|
18
14
|
export class QueryDeduplicationMiddleware {
|
|
19
|
-
private
|
|
20
|
-
private readonly
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Start cleanup timer to prevent memory leaks
|
|
30
|
-
*/
|
|
31
|
-
private startCleanupTimer(): void {
|
|
32
|
-
if (this.cleanupTimer) {
|
|
33
|
-
clearInterval(this.cleanupTimer);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
this.cleanupTimer = setInterval(() => {
|
|
37
|
-
try {
|
|
38
|
-
this.cleanupExpiredQueries();
|
|
39
|
-
} catch {
|
|
40
|
-
// Silently handle cleanup errors to prevent timer from causing issues
|
|
41
|
-
// Clear all queries if cleanup fails to prevent memory leak
|
|
42
|
-
this.pendingQueries.clear();
|
|
43
|
-
}
|
|
44
|
-
}, this.CLEANUP_INTERVAL_MS);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Clean up expired queries to prevent memory leaks
|
|
49
|
-
*/
|
|
50
|
-
private cleanupExpiredQueries(): void {
|
|
51
|
-
const now = Date.now();
|
|
52
|
-
for (const [key, query] of this.pendingQueries.entries()) {
|
|
53
|
-
if (now - query.timestamp > this.DEDUPLICATION_WINDOW_MS) {
|
|
54
|
-
this.pendingQueries.delete(key);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Generate query key from query parameters
|
|
61
|
-
*/
|
|
62
|
-
private generateQueryKey(key: QueryKey): string {
|
|
63
|
-
const parts = [
|
|
64
|
-
key.collection,
|
|
65
|
-
key.filters,
|
|
66
|
-
key.limit?.toString() || '',
|
|
67
|
-
key.orderBy || '',
|
|
68
|
-
];
|
|
69
|
-
return parts.join('|');
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Check if query is already pending
|
|
74
|
-
*/
|
|
75
|
-
private isQueryPending(key: string): boolean {
|
|
76
|
-
const pending = this.pendingQueries.get(key);
|
|
77
|
-
if (!pending) return false;
|
|
78
|
-
|
|
79
|
-
const age = Date.now() - pending.timestamp;
|
|
80
|
-
if (age > this.DEDUPLICATION_WINDOW_MS) {
|
|
81
|
-
this.pendingQueries.delete(key);
|
|
82
|
-
return false;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return true;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Get pending query promise
|
|
90
|
-
*/
|
|
91
|
-
private getPendingQuery(key: string): Promise<unknown> | null {
|
|
92
|
-
const pending = this.pendingQueries.get(key);
|
|
93
|
-
return pending ? pending.promise : null;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Add query to pending list with guaranteed cleanup
|
|
98
|
-
*/
|
|
99
|
-
private addPendingQuery(key: string, promise: Promise<unknown>): void {
|
|
100
|
-
// Wrap the promise to ensure cleanup happens regardless of outcome
|
|
101
|
-
const wrappedPromise = promise
|
|
102
|
-
.catch((error) => {
|
|
103
|
-
// Re-throw to maintain original promise behavior
|
|
104
|
-
throw error;
|
|
105
|
-
})
|
|
106
|
-
.finally(() => {
|
|
107
|
-
// Guaranteed cleanup - runs for both resolve and reject
|
|
108
|
-
this.pendingQueries.delete(key);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
// Store the wrapped promise with the finally handler attached
|
|
112
|
-
this.pendingQueries.set(key, {
|
|
113
|
-
promise: wrappedPromise,
|
|
114
|
-
timestamp: Date.now(),
|
|
15
|
+
private readonly queryManager: PendingQueryManager;
|
|
16
|
+
private readonly timerManager: TimerManager;
|
|
17
|
+
|
|
18
|
+
constructor(deduplicationWindowMs: number = DEDUPLICATION_WINDOW_MS) {
|
|
19
|
+
this.queryManager = new PendingQueryManager(deduplicationWindowMs);
|
|
20
|
+
this.timerManager = new TimerManager({
|
|
21
|
+
cleanupIntervalMs: CLEANUP_INTERVAL_MS,
|
|
22
|
+
onCleanup: () => this.queryManager.cleanup(),
|
|
115
23
|
});
|
|
24
|
+
this.timerManager.start();
|
|
116
25
|
}
|
|
117
26
|
|
|
118
27
|
/**
|
|
@@ -122,17 +31,17 @@ export class QueryDeduplicationMiddleware {
|
|
|
122
31
|
queryKey: QueryKey,
|
|
123
32
|
queryFn: () => Promise<T>,
|
|
124
33
|
): Promise<T> {
|
|
125
|
-
const key =
|
|
34
|
+
const key = generateQueryKey(queryKey);
|
|
126
35
|
|
|
127
|
-
if (this.
|
|
128
|
-
const pendingPromise = this.
|
|
36
|
+
if (this.queryManager.isPending(key)) {
|
|
37
|
+
const pendingPromise = this.queryManager.get(key);
|
|
129
38
|
if (pendingPromise) {
|
|
130
39
|
return pendingPromise as Promise<T>;
|
|
131
40
|
}
|
|
132
41
|
}
|
|
133
42
|
|
|
134
43
|
const promise = queryFn();
|
|
135
|
-
this.
|
|
44
|
+
this.queryManager.add(key, promise);
|
|
136
45
|
|
|
137
46
|
return promise;
|
|
138
47
|
}
|
|
@@ -141,27 +50,27 @@ export class QueryDeduplicationMiddleware {
|
|
|
141
50
|
* Clear all pending queries
|
|
142
51
|
*/
|
|
143
52
|
clear(): void {
|
|
144
|
-
this.
|
|
53
|
+
this.queryManager.clear();
|
|
145
54
|
}
|
|
146
55
|
|
|
147
56
|
/**
|
|
148
57
|
* Destroy middleware and cleanup resources
|
|
149
58
|
*/
|
|
150
59
|
destroy(): void {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
this.cleanupTimer = null;
|
|
154
|
-
}
|
|
155
|
-
this.pendingQueries.clear();
|
|
60
|
+
this.timerManager.destroy();
|
|
61
|
+
this.queryManager.clear();
|
|
156
62
|
}
|
|
157
63
|
|
|
158
64
|
/**
|
|
159
65
|
* Get pending queries count
|
|
160
66
|
*/
|
|
161
67
|
getPendingCount(): number {
|
|
162
|
-
return this.
|
|
68
|
+
return this.queryManager.size();
|
|
163
69
|
}
|
|
164
70
|
}
|
|
165
71
|
|
|
166
72
|
export const queryDeduplicationMiddleware = new QueryDeduplicationMiddleware();
|
|
167
73
|
|
|
74
|
+
// Re-export types for convenience
|
|
75
|
+
export type { QueryKey };
|
|
76
|
+
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { RequestLog, RequestStats, RequestType } from '../../domain/entities/RequestLog';
|
|
7
|
+
import { generateUniqueId } from '../../../domain/utils/id-generator.util';
|
|
7
8
|
|
|
8
9
|
export class RequestLoggerService {
|
|
9
10
|
private logs: RequestLog[] = [];
|
|
@@ -16,7 +17,7 @@ export class RequestLoggerService {
|
|
|
16
17
|
logRequest(log: Omit<RequestLog, 'id' | 'timestamp'>): void {
|
|
17
18
|
const fullLog: RequestLog = {
|
|
18
19
|
...log,
|
|
19
|
-
id:
|
|
20
|
+
id: generateUniqueId(),
|
|
20
21
|
timestamp: Date.now(),
|
|
21
22
|
};
|
|
22
23
|
|
|
@@ -92,13 +93,6 @@ export class RequestLoggerService {
|
|
|
92
93
|
};
|
|
93
94
|
}
|
|
94
95
|
|
|
95
|
-
/**
|
|
96
|
-
* Generate unique ID
|
|
97
|
-
*/
|
|
98
|
-
private generateId(): string {
|
|
99
|
-
return `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
96
|
/**
|
|
103
97
|
* Notify all listeners
|
|
104
98
|
*/
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deduplication Utilities
|
|
3
|
+
* Utilities for query deduplication
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { TimerManager } from './timer-manager.util';
|
|
7
|
+
export type { TimerManagerOptions } from './timer-manager.util';
|
|
8
|
+
|
|
9
|
+
export { generateQueryKey, createQueryKey } from './query-key-generator.util';
|
|
10
|
+
export type { QueryKey } from './query-key-generator.util';
|
|
11
|
+
|
|
12
|
+
export { PendingQueryManager } from './pending-query-manager.util';
|
|
13
|
+
export type { PendingQuery } from './pending-query-manager.util';
|