@umituz/react-native-firebase 1.13.3 → 1.13.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 +5 -8
- package/src/firestore/__tests__/BaseRepository.test.ts +133 -0
- package/src/firestore/__tests__/QueryDeduplicationMiddleware.test.ts +147 -0
- package/src/firestore/__tests__/mocks/react-native-firebase.ts +23 -0
- package/src/firestore/__tests__/setup.ts +36 -0
- package/src/firestore/domain/constants/QuotaLimits.ts +97 -0
- package/src/firestore/domain/entities/QuotaMetrics.ts +28 -0
- package/src/firestore/domain/entities/RequestLog.ts +30 -0
- package/src/firestore/domain/errors/FirebaseFirestoreError.ts +52 -0
- package/src/firestore/domain/services/QuotaCalculator.ts +70 -0
- package/src/firestore/index.ts +174 -0
- package/src/firestore/infrastructure/config/FirestoreClient.ts +181 -0
- package/src/firestore/infrastructure/config/initializers/FirebaseFirestoreInitializer.ts +46 -0
- package/src/firestore/infrastructure/middleware/QueryDeduplicationMiddleware.ts +153 -0
- package/src/firestore/infrastructure/middleware/QuotaTrackingMiddleware.ts +165 -0
- package/src/firestore/infrastructure/repositories/BasePaginatedRepository.ts +90 -0
- package/src/firestore/infrastructure/repositories/BaseQueryRepository.ts +80 -0
- package/src/firestore/infrastructure/repositories/BaseRepository.ts +147 -0
- package/src/firestore/infrastructure/services/QuotaMonitorService.ts +108 -0
- package/src/firestore/infrastructure/services/RequestLoggerService.ts +139 -0
- package/src/firestore/types/pagination.types.ts +60 -0
- package/src/firestore/utils/dateUtils.ts +31 -0
- package/src/firestore/utils/document-mapper.helper.ts +145 -0
- package/src/firestore/utils/pagination.helper.ts +93 -0
- package/src/firestore/utils/query-builder.ts +188 -0
- package/src/firestore/utils/quota-error-detector.util.ts +100 -0
- package/src/index.ts +8 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React Native Firestore Module
|
|
3
|
+
* Domain-Driven Design (DDD) Architecture
|
|
4
|
+
*
|
|
5
|
+
* This is the SINGLE SOURCE OF TRUTH for all Firestore operations.
|
|
6
|
+
* ALL imports from the Firestore module MUST go through this file.
|
|
7
|
+
*
|
|
8
|
+
* Architecture:
|
|
9
|
+
* - domain: Errors, Constants, Entities, Services
|
|
10
|
+
* - infrastructure: Firestore client, BaseRepository, utilities
|
|
11
|
+
* - utils: Date utilities, timestamp conversion, query builders
|
|
12
|
+
*
|
|
13
|
+
* This module is designed to be used across hundreds of apps.
|
|
14
|
+
* It provides a consistent interface for Firestore operations.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// =============================================================================
|
|
18
|
+
// DOMAIN LAYER - Errors
|
|
19
|
+
// =============================================================================
|
|
20
|
+
|
|
21
|
+
export {
|
|
22
|
+
FirebaseFirestoreError,
|
|
23
|
+
FirebaseFirestoreInitializationError,
|
|
24
|
+
FirebaseFirestoreQuotaError,
|
|
25
|
+
} from './domain/errors/FirebaseFirestoreError';
|
|
26
|
+
|
|
27
|
+
// =============================================================================
|
|
28
|
+
// INFRASTRUCTURE LAYER - Firestore Client
|
|
29
|
+
// =============================================================================
|
|
30
|
+
|
|
31
|
+
export {
|
|
32
|
+
initializeFirestore,
|
|
33
|
+
getFirestore,
|
|
34
|
+
isFirestoreInitialized,
|
|
35
|
+
getFirestoreInitializationError,
|
|
36
|
+
resetFirestoreClient,
|
|
37
|
+
firestoreClient,
|
|
38
|
+
} from './infrastructure/config/FirestoreClient';
|
|
39
|
+
|
|
40
|
+
export type { Firestore } from './infrastructure/config/FirestoreClient';
|
|
41
|
+
|
|
42
|
+
// =============================================================================
|
|
43
|
+
// INFRASTRUCTURE LAYER - BaseRepository
|
|
44
|
+
// =============================================================================
|
|
45
|
+
|
|
46
|
+
export { BaseRepository } from './infrastructure/repositories/BaseRepository';
|
|
47
|
+
export { BaseQueryRepository } from './infrastructure/repositories/BaseQueryRepository';
|
|
48
|
+
export { BasePaginatedRepository } from './infrastructure/repositories/BasePaginatedRepository';
|
|
49
|
+
|
|
50
|
+
// =============================================================================
|
|
51
|
+
// UTILS - Date Utilities
|
|
52
|
+
// =============================================================================
|
|
53
|
+
|
|
54
|
+
export {
|
|
55
|
+
isoToTimestamp,
|
|
56
|
+
timestampToISO,
|
|
57
|
+
timestampToDate,
|
|
58
|
+
getCurrentISOString,
|
|
59
|
+
} from './utils/dateUtils';
|
|
60
|
+
|
|
61
|
+
// =============================================================================
|
|
62
|
+
// UTILS - Query Builder
|
|
63
|
+
// =============================================================================
|
|
64
|
+
|
|
65
|
+
export {
|
|
66
|
+
buildQuery,
|
|
67
|
+
createInFilter,
|
|
68
|
+
createEqualFilter,
|
|
69
|
+
} from './utils/query-builder';
|
|
70
|
+
|
|
71
|
+
export type {
|
|
72
|
+
QueryBuilderOptions,
|
|
73
|
+
FieldFilter,
|
|
74
|
+
} from './utils/query-builder';
|
|
75
|
+
|
|
76
|
+
// =============================================================================
|
|
77
|
+
// UTILS - Pagination
|
|
78
|
+
// =============================================================================
|
|
79
|
+
|
|
80
|
+
export {
|
|
81
|
+
PaginationHelper,
|
|
82
|
+
createPaginationHelper,
|
|
83
|
+
} from './utils/pagination.helper';
|
|
84
|
+
|
|
85
|
+
export type {
|
|
86
|
+
PaginatedResult,
|
|
87
|
+
PaginationParams,
|
|
88
|
+
} from './types/pagination.types';
|
|
89
|
+
|
|
90
|
+
export { EMPTY_PAGINATED_RESULT } from './types/pagination.types';
|
|
91
|
+
|
|
92
|
+
// =============================================================================
|
|
93
|
+
// UTILS - Document Mapper
|
|
94
|
+
// =============================================================================
|
|
95
|
+
|
|
96
|
+
export {
|
|
97
|
+
DocumentMapperHelper,
|
|
98
|
+
createDocumentMapper,
|
|
99
|
+
} from './utils/document-mapper.helper';
|
|
100
|
+
|
|
101
|
+
// =============================================================================
|
|
102
|
+
// UTILS - Quota Error Detection
|
|
103
|
+
// =============================================================================
|
|
104
|
+
|
|
105
|
+
export {
|
|
106
|
+
isQuotaError,
|
|
107
|
+
isRetryableError,
|
|
108
|
+
getQuotaErrorMessage,
|
|
109
|
+
} from './utils/quota-error-detector.util';
|
|
110
|
+
|
|
111
|
+
// =============================================================================
|
|
112
|
+
// DOMAIN LAYER - Constants
|
|
113
|
+
// =============================================================================
|
|
114
|
+
|
|
115
|
+
export {
|
|
116
|
+
FREE_TIER_LIMITS,
|
|
117
|
+
QUOTA_THRESHOLDS,
|
|
118
|
+
calculateQuotaUsage,
|
|
119
|
+
isQuotaThresholdReached,
|
|
120
|
+
getRemainingQuota,
|
|
121
|
+
} from './domain/constants/QuotaLimits';
|
|
122
|
+
|
|
123
|
+
// =============================================================================
|
|
124
|
+
// DOMAIN LAYER - Entities
|
|
125
|
+
// =============================================================================
|
|
126
|
+
|
|
127
|
+
export type {
|
|
128
|
+
QuotaMetrics,
|
|
129
|
+
QuotaLimits,
|
|
130
|
+
QuotaStatus,
|
|
131
|
+
} from './domain/entities/QuotaMetrics';
|
|
132
|
+
|
|
133
|
+
export type {
|
|
134
|
+
RequestLog,
|
|
135
|
+
RequestStats,
|
|
136
|
+
RequestType,
|
|
137
|
+
} from './domain/entities/RequestLog';
|
|
138
|
+
|
|
139
|
+
// =============================================================================
|
|
140
|
+
// DOMAIN LAYER - Services
|
|
141
|
+
// =============================================================================
|
|
142
|
+
|
|
143
|
+
export { QuotaCalculator } from './domain/services/QuotaCalculator';
|
|
144
|
+
|
|
145
|
+
// =============================================================================
|
|
146
|
+
// INFRASTRUCTURE LAYER - Middleware
|
|
147
|
+
// =============================================================================
|
|
148
|
+
|
|
149
|
+
export {
|
|
150
|
+
QueryDeduplicationMiddleware,
|
|
151
|
+
queryDeduplicationMiddleware,
|
|
152
|
+
} from './infrastructure/middleware/QueryDeduplicationMiddleware';
|
|
153
|
+
|
|
154
|
+
export {
|
|
155
|
+
QuotaTrackingMiddleware,
|
|
156
|
+
quotaTrackingMiddleware,
|
|
157
|
+
} from './infrastructure/middleware/QuotaTrackingMiddleware';
|
|
158
|
+
|
|
159
|
+
// =============================================================================
|
|
160
|
+
// INFRASTRUCTURE LAYER - Services
|
|
161
|
+
// =============================================================================
|
|
162
|
+
|
|
163
|
+
export {
|
|
164
|
+
QuotaMonitorService,
|
|
165
|
+
quotaMonitorService,
|
|
166
|
+
} from './infrastructure/services/QuotaMonitorService';
|
|
167
|
+
|
|
168
|
+
export {
|
|
169
|
+
RequestLoggerService,
|
|
170
|
+
requestLoggerService,
|
|
171
|
+
} from './infrastructure/services/RequestLoggerService';
|
|
172
|
+
|
|
173
|
+
// Re-export Firestore types for convenience
|
|
174
|
+
export type { Timestamp } from 'firebase/firestore';
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Firestore Client - Infrastructure Layer
|
|
3
|
+
*
|
|
4
|
+
* Domain-Driven Design: Infrastructure implementation of Firestore client
|
|
5
|
+
* Singleton pattern for managing Firestore instance
|
|
6
|
+
*
|
|
7
|
+
* IMPORTANT: This package requires Firebase App to be initialized first.
|
|
8
|
+
* Use @umituz/react-native-firebase to initialize Firebase App.
|
|
9
|
+
*
|
|
10
|
+
* SOLID Principles:
|
|
11
|
+
* - Single Responsibility: Only manages Firestore initialization
|
|
12
|
+
* - Open/Closed: Extensible through configuration, closed for modification
|
|
13
|
+
* - Dependency Inversion: Depends on Firebase App from @umituz/react-native-firebase
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { Firestore } from 'firebase/firestore';
|
|
17
|
+
import { getFirebaseApp } from '../../../infrastructure/config/FirebaseClient';
|
|
18
|
+
import { FirebaseFirestoreInitializer } from './initializers/FirebaseFirestoreInitializer';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Firestore Client Singleton
|
|
22
|
+
* Manages Firestore initialization
|
|
23
|
+
*/
|
|
24
|
+
class FirestoreClientSingleton {
|
|
25
|
+
private static instance: FirestoreClientSingleton | null = null;
|
|
26
|
+
private firestore: Firestore | null = null;
|
|
27
|
+
private initializationError: string | null = null;
|
|
28
|
+
|
|
29
|
+
private constructor() {
|
|
30
|
+
// Private constructor to enforce singleton pattern
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get singleton instance
|
|
35
|
+
*/
|
|
36
|
+
static getInstance(): FirestoreClientSingleton {
|
|
37
|
+
if (!FirestoreClientSingleton.instance) {
|
|
38
|
+
FirestoreClientSingleton.instance = new FirestoreClientSingleton();
|
|
39
|
+
}
|
|
40
|
+
return FirestoreClientSingleton.instance;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Initialize Firestore
|
|
45
|
+
* Requires Firebase App to be initialized first via @umituz/react-native-firebase
|
|
46
|
+
*
|
|
47
|
+
* @returns Firestore instance or null if initialization fails
|
|
48
|
+
*/
|
|
49
|
+
initialize(): Firestore | null {
|
|
50
|
+
if (this.firestore) {
|
|
51
|
+
return this.firestore;
|
|
52
|
+
}
|
|
53
|
+
if (this.initializationError) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const app = getFirebaseApp(); // Get the core Firebase App
|
|
58
|
+
|
|
59
|
+
// Return null if Firebase App is not available (offline mode)
|
|
60
|
+
if (!app) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
this.firestore = FirebaseFirestoreInitializer.initialize(app);
|
|
65
|
+
return this.firestore;
|
|
66
|
+
} catch (error) {
|
|
67
|
+
this.initializationError =
|
|
68
|
+
error instanceof Error
|
|
69
|
+
? error.message
|
|
70
|
+
: 'Failed to initialize Firestore client';
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get Firestore instance
|
|
77
|
+
* Auto-initializes if Firebase App is available
|
|
78
|
+
* Returns null if config is not available (offline mode - no error)
|
|
79
|
+
* @returns Firestore instance or null if not initialized
|
|
80
|
+
*/
|
|
81
|
+
getFirestore(): Firestore | null {
|
|
82
|
+
// Auto-initialize if not already initialized
|
|
83
|
+
if (!this.firestore && !this.initializationError) {
|
|
84
|
+
try {
|
|
85
|
+
// Try to get Firebase App (will auto-initialize if config is available)
|
|
86
|
+
const app = getFirebaseApp();
|
|
87
|
+
if (app) {
|
|
88
|
+
this.initialize();
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
// Firebase App not available, return null (offline mode)
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Return null if not initialized (offline mode - no error)
|
|
97
|
+
return this.firestore || null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Check if Firestore is initialized
|
|
102
|
+
*/
|
|
103
|
+
isInitialized(): boolean {
|
|
104
|
+
return this.firestore !== null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Get initialization error if any
|
|
109
|
+
*/
|
|
110
|
+
getInitializationError(): string | null {
|
|
111
|
+
return this.initializationError;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Reset Firestore client instance
|
|
116
|
+
* Useful for testing
|
|
117
|
+
*/
|
|
118
|
+
reset(): void {
|
|
119
|
+
this.firestore = null;
|
|
120
|
+
this.initializationError = null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export const firestoreClient = FirestoreClientSingleton.getInstance();
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Initialize Firestore
|
|
128
|
+
* Requires Firebase App to be initialized first via @umituz/react-native-firebase
|
|
129
|
+
*
|
|
130
|
+
* @returns Firestore instance or null if initialization fails
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* ```typescript
|
|
134
|
+
* import { initializeFirebase } from '@umituz/react-native-firebase';
|
|
135
|
+
* import { initializeFirestore } from '@umituz/react-native-firestore';
|
|
136
|
+
*
|
|
137
|
+
* // Initialize Firebase App first
|
|
138
|
+
* const app = initializeFirebase(config);
|
|
139
|
+
*
|
|
140
|
+
* // Then initialize Firestore
|
|
141
|
+
* const db = initializeFirestore();
|
|
142
|
+
* ```
|
|
143
|
+
*/
|
|
144
|
+
export function initializeFirestore(): Firestore | null {
|
|
145
|
+
return firestoreClient.initialize();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Get Firestore instance
|
|
150
|
+
* Auto-initializes if Firebase App is available
|
|
151
|
+
* Returns null if config is not available (offline mode - no error)
|
|
152
|
+
* @returns Firestore instance or null if not initialized
|
|
153
|
+
*/
|
|
154
|
+
export function getFirestore(): Firestore | null {
|
|
155
|
+
return firestoreClient.getFirestore();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Check if Firestore is initialized
|
|
160
|
+
*/
|
|
161
|
+
export function isFirestoreInitialized(): boolean {
|
|
162
|
+
return firestoreClient.isInitialized();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Get Firestore initialization error if any
|
|
167
|
+
*/
|
|
168
|
+
export function getFirestoreInitializationError(): string | null {
|
|
169
|
+
return firestoreClient.getInitializationError();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Reset Firestore client instance
|
|
174
|
+
* Useful for testing
|
|
175
|
+
*/
|
|
176
|
+
export function resetFirestoreClient(): void {
|
|
177
|
+
firestoreClient.reset();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export type { Firestore } from 'firebase/firestore';
|
|
181
|
+
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Firebase Firestore Initializer
|
|
3
|
+
*
|
|
4
|
+
* Single Responsibility: Initialize Firestore instance
|
|
5
|
+
*
|
|
6
|
+
* NOTE: React Native does not support IndexedDB (browser API), so we use
|
|
7
|
+
* memoryLocalCache instead of persistentLocalCache. For client-side caching,
|
|
8
|
+
* use TanStack Query which works on all platforms.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
getFirestore,
|
|
13
|
+
initializeFirestore,
|
|
14
|
+
memoryLocalCache,
|
|
15
|
+
} from 'firebase/firestore';
|
|
16
|
+
import type { Firestore } from 'firebase/firestore';
|
|
17
|
+
import type { FirebaseApp } from 'firebase/app';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Initializes Firestore
|
|
21
|
+
* Platform-agnostic: Works on all platforms (Web, iOS, Android)
|
|
22
|
+
*/
|
|
23
|
+
export class FirebaseFirestoreInitializer {
|
|
24
|
+
/**
|
|
25
|
+
* Initialize Firestore with memory cache configuration
|
|
26
|
+
* React Native does not support IndexedDB, so we use memory cache
|
|
27
|
+
* For offline persistence, use TanStack Query with AsyncStorage
|
|
28
|
+
*/
|
|
29
|
+
static initialize(app: FirebaseApp): Firestore {
|
|
30
|
+
try {
|
|
31
|
+
// Use memory cache for React Native compatibility
|
|
32
|
+
// IndexedDB (persistentLocalCache) is not available in React Native
|
|
33
|
+
return initializeFirestore(app, {
|
|
34
|
+
localCache: memoryLocalCache(),
|
|
35
|
+
});
|
|
36
|
+
} catch (error: any) {
|
|
37
|
+
// If already initialized, get existing instance
|
|
38
|
+
if (error.code === 'failed-precondition') {
|
|
39
|
+
return getFirestore(app);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return getFirestore(app);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query Deduplication Middleware
|
|
3
|
+
* Prevents duplicate Firestore queries within a short time window
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
interface PendingQuery {
|
|
7
|
+
promise: Promise<unknown>;
|
|
8
|
+
timestamp: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface QueryKey {
|
|
12
|
+
collection: string;
|
|
13
|
+
filters: string;
|
|
14
|
+
limit?: number;
|
|
15
|
+
orderBy?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class QueryDeduplicationMiddleware {
|
|
19
|
+
private pendingQueries = new Map<string, PendingQuery>();
|
|
20
|
+
private readonly DEDUPLICATION_WINDOW_MS = 1000; // 1 second
|
|
21
|
+
private readonly CLEANUP_INTERVAL_MS = 5000; // 5 seconds
|
|
22
|
+
private cleanupTimer: NodeJS.Timeout | null = null;
|
|
23
|
+
|
|
24
|
+
constructor() {
|
|
25
|
+
this.startCleanupTimer();
|
|
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
|
+
this.cleanupExpiredQueries();
|
|
38
|
+
}, this.CLEANUP_INTERVAL_MS);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Clean up expired queries to prevent memory leaks
|
|
43
|
+
*/
|
|
44
|
+
private cleanupExpiredQueries(): void {
|
|
45
|
+
const now = Date.now();
|
|
46
|
+
for (const [key, query] of this.pendingQueries.entries()) {
|
|
47
|
+
if (now - query.timestamp > this.DEDUPLICATION_WINDOW_MS) {
|
|
48
|
+
this.pendingQueries.delete(key);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Generate query key from query parameters
|
|
55
|
+
*/
|
|
56
|
+
private generateQueryKey(key: QueryKey): string {
|
|
57
|
+
const parts = [
|
|
58
|
+
key.collection,
|
|
59
|
+
key.filters,
|
|
60
|
+
key.limit?.toString() || '',
|
|
61
|
+
key.orderBy || '',
|
|
62
|
+
];
|
|
63
|
+
return parts.join('|');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Check if query is already pending
|
|
68
|
+
*/
|
|
69
|
+
private isQueryPending(key: string): boolean {
|
|
70
|
+
const pending = this.pendingQueries.get(key);
|
|
71
|
+
if (!pending) return false;
|
|
72
|
+
|
|
73
|
+
const age = Date.now() - pending.timestamp;
|
|
74
|
+
if (age > this.DEDUPLICATION_WINDOW_MS) {
|
|
75
|
+
this.pendingQueries.delete(key);
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get pending query promise
|
|
84
|
+
*/
|
|
85
|
+
private getPendingQuery(key: string): Promise<unknown> | null {
|
|
86
|
+
const pending = this.pendingQueries.get(key);
|
|
87
|
+
return pending ? pending.promise : null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Add query to pending list
|
|
92
|
+
*/
|
|
93
|
+
private addPendingQuery(key: string, promise: Promise<unknown>): void {
|
|
94
|
+
this.pendingQueries.set(key, {
|
|
95
|
+
promise,
|
|
96
|
+
timestamp: Date.now(),
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
promise.finally(() => {
|
|
100
|
+
this.pendingQueries.delete(key);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Deduplicate a query
|
|
106
|
+
*/
|
|
107
|
+
async deduplicate<T>(
|
|
108
|
+
queryKey: QueryKey,
|
|
109
|
+
queryFn: () => Promise<T>,
|
|
110
|
+
): Promise<T> {
|
|
111
|
+
const key = this.generateQueryKey(queryKey);
|
|
112
|
+
|
|
113
|
+
if (this.isQueryPending(key)) {
|
|
114
|
+
const pendingPromise = this.getPendingQuery(key);
|
|
115
|
+
if (pendingPromise) {
|
|
116
|
+
return pendingPromise as Promise<T>;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const promise = queryFn();
|
|
121
|
+
this.addPendingQuery(key, promise);
|
|
122
|
+
|
|
123
|
+
return promise;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Clear all pending queries
|
|
128
|
+
*/
|
|
129
|
+
clear(): void {
|
|
130
|
+
this.pendingQueries.clear();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Destroy middleware and cleanup resources
|
|
135
|
+
*/
|
|
136
|
+
destroy(): void {
|
|
137
|
+
if (this.cleanupTimer) {
|
|
138
|
+
clearInterval(this.cleanupTimer);
|
|
139
|
+
this.cleanupTimer = null;
|
|
140
|
+
}
|
|
141
|
+
this.pendingQueries.clear();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Get pending queries count
|
|
146
|
+
*/
|
|
147
|
+
getPendingCount(): number {
|
|
148
|
+
return this.pendingQueries.size;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export const queryDeduplicationMiddleware = new QueryDeduplicationMiddleware();
|
|
153
|
+
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quota Tracking Middleware
|
|
3
|
+
* Tracks Firestore operations for quota monitoring
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { quotaMonitorService } from '../services/QuotaMonitorService';
|
|
7
|
+
import { requestLoggerService } from '../services/RequestLoggerService';
|
|
8
|
+
import type { RequestType } from '../../domain/entities/RequestLog';
|
|
9
|
+
|
|
10
|
+
interface TrackedOperation {
|
|
11
|
+
type: RequestType;
|
|
12
|
+
collection: string;
|
|
13
|
+
documentId?: string;
|
|
14
|
+
count: number;
|
|
15
|
+
cached?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class QuotaTrackingMiddleware {
|
|
19
|
+
/**
|
|
20
|
+
* Track a read operation
|
|
21
|
+
*/
|
|
22
|
+
trackRead(collection: string, count: number = 1, cached: boolean = false): void {
|
|
23
|
+
quotaMonitorService.incrementRead(count);
|
|
24
|
+
requestLoggerService.logRequest({
|
|
25
|
+
type: 'read',
|
|
26
|
+
collection,
|
|
27
|
+
success: true,
|
|
28
|
+
cached,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Track a write operation
|
|
34
|
+
*/
|
|
35
|
+
trackWrite(
|
|
36
|
+
collection: string,
|
|
37
|
+
documentId?: string,
|
|
38
|
+
count: number = 1,
|
|
39
|
+
): void {
|
|
40
|
+
quotaMonitorService.incrementWrite(count);
|
|
41
|
+
requestLoggerService.logRequest({
|
|
42
|
+
type: 'write',
|
|
43
|
+
collection,
|
|
44
|
+
documentId,
|
|
45
|
+
success: true,
|
|
46
|
+
cached: false,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Track a delete operation
|
|
52
|
+
*/
|
|
53
|
+
trackDelete(
|
|
54
|
+
collection: string,
|
|
55
|
+
documentId?: string,
|
|
56
|
+
count: number = 1,
|
|
57
|
+
): void {
|
|
58
|
+
quotaMonitorService.incrementDelete(count);
|
|
59
|
+
requestLoggerService.logRequest({
|
|
60
|
+
type: 'delete',
|
|
61
|
+
collection,
|
|
62
|
+
documentId,
|
|
63
|
+
success: true,
|
|
64
|
+
cached: false,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Track a listener operation
|
|
70
|
+
*/
|
|
71
|
+
trackListener(collection: string, documentId?: string): void {
|
|
72
|
+
requestLoggerService.logRequest({
|
|
73
|
+
type: 'listener',
|
|
74
|
+
collection,
|
|
75
|
+
documentId,
|
|
76
|
+
success: true,
|
|
77
|
+
cached: false,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Track a failed operation
|
|
83
|
+
*/
|
|
84
|
+
trackError(
|
|
85
|
+
type: RequestType,
|
|
86
|
+
collection: string,
|
|
87
|
+
error: string,
|
|
88
|
+
documentId?: string,
|
|
89
|
+
): void {
|
|
90
|
+
requestLoggerService.logRequest({
|
|
91
|
+
type,
|
|
92
|
+
collection,
|
|
93
|
+
documentId,
|
|
94
|
+
success: false,
|
|
95
|
+
error,
|
|
96
|
+
cached: false,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Track operation with timing
|
|
102
|
+
*/
|
|
103
|
+
async trackOperation<T>(
|
|
104
|
+
operation: TrackedOperation,
|
|
105
|
+
operationFn: () => Promise<T>,
|
|
106
|
+
): Promise<T> {
|
|
107
|
+
const startTime = Date.now();
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const result = await operationFn();
|
|
111
|
+
const duration = Date.now() - startTime;
|
|
112
|
+
|
|
113
|
+
if (operation.type === 'read') {
|
|
114
|
+
quotaMonitorService.incrementRead(operation.count);
|
|
115
|
+
requestLoggerService.logRequest({
|
|
116
|
+
type: 'read',
|
|
117
|
+
collection: operation.collection,
|
|
118
|
+
documentId: operation.documentId,
|
|
119
|
+
success: true,
|
|
120
|
+
cached: operation.cached || false,
|
|
121
|
+
duration,
|
|
122
|
+
});
|
|
123
|
+
} else if (operation.type === 'write') {
|
|
124
|
+
quotaMonitorService.incrementWrite(operation.count);
|
|
125
|
+
requestLoggerService.logRequest({
|
|
126
|
+
type: 'write',
|
|
127
|
+
collection: operation.collection,
|
|
128
|
+
documentId: operation.documentId,
|
|
129
|
+
success: true,
|
|
130
|
+
cached: false,
|
|
131
|
+
duration,
|
|
132
|
+
});
|
|
133
|
+
} else if (operation.type === 'delete') {
|
|
134
|
+
quotaMonitorService.incrementDelete(operation.count);
|
|
135
|
+
requestLoggerService.logRequest({
|
|
136
|
+
type: 'delete',
|
|
137
|
+
collection: operation.collection,
|
|
138
|
+
documentId: operation.documentId,
|
|
139
|
+
success: true,
|
|
140
|
+
cached: false,
|
|
141
|
+
duration,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return result;
|
|
146
|
+
} catch (error) {
|
|
147
|
+
const duration = Date.now() - startTime;
|
|
148
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
149
|
+
requestLoggerService.logRequest({
|
|
150
|
+
type: operation.type,
|
|
151
|
+
collection: operation.collection,
|
|
152
|
+
documentId: operation.documentId,
|
|
153
|
+
success: false,
|
|
154
|
+
error: errorMessage,
|
|
155
|
+
cached: false,
|
|
156
|
+
duration,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export const quotaTrackingMiddleware = new QuotaTrackingMiddleware();
|
|
165
|
+
|