@umituz/react-native-firebase 3.0.3 → 3.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +7 -1
- package/src/domains/account-deletion/index.ts +15 -10
- package/src/domains/account-deletion/infrastructure/services/account-deletion.service.ts +235 -26
- package/src/domains/account-deletion/infrastructure/services/reauthentication.service.ts +160 -0
- package/src/domains/auth/domain/value-objects/FirebaseAuthConfig.ts +1 -1
- package/src/domains/auth/index.ts +156 -6
- package/src/domains/auth/infrastructure/config/FirebaseAuthClient.ts +60 -48
- package/src/domains/auth/infrastructure/config/initializers/FirebaseAuthInitializer.ts +41 -5
- package/src/domains/auth/infrastructure/stores/auth.store.ts +4 -1
- package/src/domains/auth/presentation/hooks/useAnonymousAuth.ts +3 -1
- package/src/domains/auth/presentation/hooks/useGoogleOAuth.ts +115 -20
- package/src/domains/auth/presentation/hooks/utils/auth-state-change.handler.ts +5 -11
- package/src/domains/firestore/domain/constants/QuotaLimits.ts +101 -0
- package/src/domains/firestore/domain/entities/QuotaMetrics.ts +26 -0
- package/src/domains/firestore/domain/entities/RequestLog.ts +28 -0
- package/src/domains/firestore/domain/services/QuotaCalculator.ts +71 -0
- package/src/domains/firestore/index.ts +85 -31
- package/src/domains/firestore/infrastructure/config/FirestoreClient.ts +82 -45
- package/src/domains/firestore/infrastructure/config/initializers/FirebaseFirestoreInitializer.ts +249 -4
- package/src/domains/firestore/infrastructure/middleware/QueryDeduplicationMiddleware.ts +306 -0
- package/src/domains/firestore/infrastructure/middleware/QuotaTrackingMiddleware.ts +92 -0
- package/src/domains/firestore/infrastructure/repositories/BasePaginatedRepository.ts +9 -1
- package/src/domains/firestore/infrastructure/repositories/BaseQueryRepository.ts +34 -8
- package/src/domains/firestore/infrastructure/repositories/BaseRepository.ts +48 -9
- package/src/domains/firestore/infrastructure/services/RequestLoggerService.ts +168 -0
- package/src/domains/firestore/presentation/hooks/index.ts +10 -0
- package/src/domains/firestore/presentation/hooks/useFirestoreMutation.ts +1 -1
- package/src/domains/firestore/presentation/hooks/useFirestoreQuery.ts +1 -1
- package/src/domains/firestore/presentation/hooks/useFirestoreSnapshot.ts +2 -1
- package/src/domains/firestore/presentation/hooks/useSmartFirestoreSnapshot.ts +362 -0
- package/src/domains/firestore/presentation/query-keys/createFirestoreKeys.ts +32 -0
- package/src/domains/firestore/presentation/query-keys/index.ts +1 -0
- package/src/domains/firestore/utils/deduplication/pending-query-manager.util.ts +126 -0
- package/src/domains/firestore/utils/deduplication/query-key-generator.util.ts +41 -0
- package/src/domains/firestore/utils/deduplication/timer-manager.util.ts +83 -0
- package/src/domains/firestore/utils/pagination.helper.ts +5 -2
- package/src/domains/firestore/utils/transaction/transaction.util.ts +8 -2
- package/src/index.ts +324 -32
- package/src/shared/domain/utils/calculation.util.ts +305 -17
- package/src/shared/domain/utils/error-handlers/error-messages.ts +0 -15
- package/src/shared/domain/utils/index.ts +5 -0
- package/src/shared/infrastructure/config/base/ClientStateManager.ts +82 -0
- package/src/shared/infrastructure/config/base/ServiceClientSingleton.ts +136 -20
- package/src/shared/infrastructure/config/clients/FirebaseClientSingleton.ts +1 -1
- package/src/shared/infrastructure/config/initializers/FirebaseAppInitializer.ts +9 -0
- package/src/shared/infrastructure/config/services/FirebaseInitializationService.ts +1 -1
- package/src/shared/infrastructure/config/state/FirebaseClientState.ts +14 -36
- package/src/application/auth/index.ts +0 -10
- package/src/application/auth/use-cases/index.ts +0 -6
- package/src/domains/account-deletion/domain/index.ts +0 -8
- package/src/domains/account-deletion/infrastructure/services/AccountDeletionExecutor.ts +0 -79
- package/src/domains/account-deletion/infrastructure/services/AccountDeletionTypes.ts +0 -32
- package/src/domains/auth/domain.ts +0 -16
- package/src/domains/auth/infrastructure/config/index.ts +0 -2
- package/src/domains/auth/infrastructure/config/initializers/index.ts +0 -1
- package/src/domains/auth/infrastructure/services/index.ts +0 -16
- package/src/domains/auth/infrastructure/services/utils/index.ts +0 -1
- package/src/domains/auth/infrastructure/stores/index.ts +0 -1
- package/src/domains/auth/infrastructure/utils/index.ts +0 -1
- package/src/domains/auth/infrastructure.ts +0 -11
- package/src/domains/auth/presentation/hooks/useAppleAuth.ts +0 -82
- package/src/domains/auth/presentation.ts +0 -31
- package/src/domains/firestore/domain/entities/Collection.ts +0 -122
- package/src/domains/firestore/domain/entities/CollectionFactory.ts +0 -55
- package/src/domains/firestore/domain/entities/CollectionHelpers.ts +0 -143
- package/src/domains/firestore/domain/entities/CollectionUtils.ts +0 -72
- package/src/domains/firestore/domain/entities/CollectionValidation.ts +0 -138
- package/src/domains/firestore/domain/index.ts +0 -61
- package/src/domains/firestore/domain/value-objects/QueryOptions.ts +0 -143
- package/src/domains/firestore/domain/value-objects/QueryOptionsFactory.ts +0 -95
- package/src/domains/firestore/domain/value-objects/QueryOptionsHelpers.ts +0 -110
- package/src/domains/firestore/domain/value-objects/WhereClause.ts +0 -114
- package/src/domains/firestore/domain/value-objects/WhereClauseFactory.ts +0 -101
- package/src/domains/firestore/domain/value-objects/WhereClauseHelpers.ts +0 -123
- package/src/domains/firestore/domain/value-objects/WhereClauseValidation.ts +0 -83
- package/src/shared/infrastructure/base/ErrorHandler.ts +0 -81
- package/src/shared/infrastructure/base/ServiceBase.ts +0 -62
- package/src/shared/infrastructure/base/TypedGuard.ts +0 -131
- package/src/shared/infrastructure/base/index.ts +0 -34
- package/src/shared/types/firebase.types.ts +0 -274
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query Deduplication Middleware (Enhanced)
|
|
3
|
+
*
|
|
4
|
+
* Prevents duplicate Firestore queries within a configurable time window
|
|
5
|
+
* with quota-aware adaptive deduplication
|
|
6
|
+
*
|
|
7
|
+
* FEATURES:
|
|
8
|
+
* - Configurable deduplication window (default: 10s, was 1s)
|
|
9
|
+
* - Quota-aware adaptive window adjustment
|
|
10
|
+
* - Statistics and monitoring
|
|
11
|
+
* - Memory leak prevention
|
|
12
|
+
* - Automatic cleanup optimization
|
|
13
|
+
*
|
|
14
|
+
* COST SAVINGS: ~90% reduction in duplicate query reads
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { QueryKey } from '../../utils/deduplication/query-key-generator.util';
|
|
18
|
+
import { generateQueryKey } from '../../utils/deduplication/query-key-generator.util';
|
|
19
|
+
import { PendingQueryManager } from '../../utils/deduplication/pending-query-manager.util';
|
|
20
|
+
import { TimerManager } from '../../utils/deduplication/timer-manager.util';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Default configuration
|
|
24
|
+
* Optimized for cost savings while maintaining freshness
|
|
25
|
+
*/
|
|
26
|
+
const DEFAULT_DEDUPLICATION_WINDOW_MS = 10000; // 10s (was 1s)
|
|
27
|
+
const DEFAULT_CLEANUP_INTERVAL_MS = 15000; // 15s (was 3s)
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Quota-based window adjustment thresholds
|
|
31
|
+
*/
|
|
32
|
+
const QUOTA_THRESHOLDS = {
|
|
33
|
+
HIGH_USAGE: 0.80, // 80% - extend window to 60s (1 min)
|
|
34
|
+
MEDIUM_USAGE: 0.60, // 60% - extend window to 20s
|
|
35
|
+
NORMAL: 0.50, // < 50% - use default 10s
|
|
36
|
+
} as const;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Deduplication statistics
|
|
40
|
+
*/
|
|
41
|
+
export interface DeduplicationStatistics {
|
|
42
|
+
/** Total queries processed */
|
|
43
|
+
totalQueries: number;
|
|
44
|
+
/** Queries served from cache (deduplicated) */
|
|
45
|
+
cachedQueries: number;
|
|
46
|
+
/** Queries executed (not cached) */
|
|
47
|
+
executedQueries: number;
|
|
48
|
+
/** Current deduplication window in ms */
|
|
49
|
+
currentWindowMs: number;
|
|
50
|
+
/** Cache hit rate (0-1) */
|
|
51
|
+
cacheHitRate: number;
|
|
52
|
+
/** Memory usage (number of cached queries) */
|
|
53
|
+
pendingQueries: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Configuration options for deduplication middleware
|
|
58
|
+
*/
|
|
59
|
+
export interface QueryDeduplicationConfig {
|
|
60
|
+
/** Base deduplication window in ms (default: 10000) */
|
|
61
|
+
baseWindowMs?: number;
|
|
62
|
+
/** Cleanup interval in ms (default: 15000) */
|
|
63
|
+
cleanupIntervalMs?: number;
|
|
64
|
+
/** Enable quota-aware adaptive window (default: true) */
|
|
65
|
+
quotaAware?: boolean;
|
|
66
|
+
/** Maximum window size in ms (default: 60000 = 1 minute) */
|
|
67
|
+
maxWindowMs?: number;
|
|
68
|
+
/** Minimum window size in ms (default: 1000 = 1 second) */
|
|
69
|
+
minWindowMs?: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Enhanced Query Deduplication Middleware
|
|
74
|
+
* Prevents duplicate queries with adaptive quota-aware behavior
|
|
75
|
+
*/
|
|
76
|
+
export class QueryDeduplicationMiddleware {
|
|
77
|
+
private readonly queryManager: PendingQueryManager;
|
|
78
|
+
private readonly timerManager: TimerManager;
|
|
79
|
+
private readonly baseWindowMs: number;
|
|
80
|
+
private readonly maxWindowMs: number;
|
|
81
|
+
private readonly minWindowMs: number;
|
|
82
|
+
private readonly quotaAware: boolean;
|
|
83
|
+
private destroyed = false;
|
|
84
|
+
|
|
85
|
+
// Statistics tracking
|
|
86
|
+
private stats: DeduplicationStatistics = {
|
|
87
|
+
totalQueries: 0,
|
|
88
|
+
cachedQueries: 0,
|
|
89
|
+
executedQueries: 0,
|
|
90
|
+
currentWindowMs: DEFAULT_DEDUPLICATION_WINDOW_MS,
|
|
91
|
+
cacheHitRate: 0,
|
|
92
|
+
pendingQueries: 0,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
constructor(config: QueryDeduplicationConfig = {}) {
|
|
96
|
+
this.baseWindowMs = config.baseWindowMs ?? DEFAULT_DEDUPLICATION_WINDOW_MS;
|
|
97
|
+
this.maxWindowMs = config.maxWindowMs ?? 60000; // 1 minute max
|
|
98
|
+
this.minWindowMs = config.minWindowMs ?? 1000; // 1 second min
|
|
99
|
+
this.quotaAware = config.quotaAware ?? true;
|
|
100
|
+
|
|
101
|
+
const cleanupIntervalMs = config.cleanupIntervalMs ?? DEFAULT_CLEANUP_INTERVAL_MS;
|
|
102
|
+
|
|
103
|
+
this.queryManager = new PendingQueryManager(this.baseWindowMs);
|
|
104
|
+
this.timerManager = new TimerManager({
|
|
105
|
+
cleanupIntervalMs,
|
|
106
|
+
onCleanup: () => {
|
|
107
|
+
if (!this.destroyed) {
|
|
108
|
+
this.queryManager.cleanup();
|
|
109
|
+
this.updateStats();
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
this.timerManager.start();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Execute query with deduplication
|
|
118
|
+
* Returns cached result if available within window, otherwise executes
|
|
119
|
+
*/
|
|
120
|
+
async deduplicate<T>(
|
|
121
|
+
queryKey: QueryKey,
|
|
122
|
+
queryFn: () => Promise<T>,
|
|
123
|
+
): Promise<T> {
|
|
124
|
+
if (this.destroyed) {
|
|
125
|
+
// If middleware is destroyed, execute query directly without deduplication
|
|
126
|
+
return queryFn();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
this.stats.totalQueries++;
|
|
130
|
+
const key = generateQueryKey(queryKey);
|
|
131
|
+
|
|
132
|
+
// Check for existing promise (atomic get-or-create pattern)
|
|
133
|
+
const existingPromise = this.queryManager.get(key);
|
|
134
|
+
if (existingPromise) {
|
|
135
|
+
this.stats.cachedQueries++;
|
|
136
|
+
this.updateCacheHitRate();
|
|
137
|
+
return existingPromise as Promise<T>;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Create promise with cleanup on completion
|
|
141
|
+
this.stats.executedQueries++;
|
|
142
|
+
const promise = (async () => {
|
|
143
|
+
try {
|
|
144
|
+
return await queryFn();
|
|
145
|
+
} finally {
|
|
146
|
+
// Immediate cleanup after completion (success or error)
|
|
147
|
+
this.queryManager.remove(key);
|
|
148
|
+
this.stats.pendingQueries = this.queryManager.size();
|
|
149
|
+
}
|
|
150
|
+
})();
|
|
151
|
+
|
|
152
|
+
// Add before any await - this prevents race between check and add
|
|
153
|
+
this.queryManager.add(key, promise);
|
|
154
|
+
this.stats.pendingQueries = this.queryManager.size();
|
|
155
|
+
|
|
156
|
+
return promise;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Adjust deduplication window based on quota usage
|
|
161
|
+
* Call this periodically with current quota percentage
|
|
162
|
+
*
|
|
163
|
+
* @param quotaPercentage - Current quota usage (0-1)
|
|
164
|
+
*
|
|
165
|
+
* @example
|
|
166
|
+
* ```typescript
|
|
167
|
+
* const quotaStatus = getQuotaStatus();
|
|
168
|
+
* middleware.adjustWindowForQuota(quotaStatus.readPercentage / 100);
|
|
169
|
+
* ```
|
|
170
|
+
*/
|
|
171
|
+
adjustWindowForQuota(quotaPercentage: number): void {
|
|
172
|
+
if (!this.quotaAware || this.destroyed) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
let newWindowMs: number;
|
|
177
|
+
|
|
178
|
+
if (quotaPercentage >= QUOTA_THRESHOLDS.HIGH_USAGE) {
|
|
179
|
+
// High usage: extend window to maximum (1 minute)
|
|
180
|
+
newWindowMs = this.maxWindowMs;
|
|
181
|
+
} else if (quotaPercentage >= QUOTA_THRESHOLDS.MEDIUM_USAGE) {
|
|
182
|
+
// Medium usage: extend window to 20s
|
|
183
|
+
newWindowMs = Math.min(20000, this.maxWindowMs);
|
|
184
|
+
} else {
|
|
185
|
+
// Normal usage: use base window (10s)
|
|
186
|
+
newWindowMs = this.baseWindowMs;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Clamp to min/max bounds
|
|
190
|
+
newWindowMs = Math.max(this.minWindowMs, Math.min(newWindowMs, this.maxWindowMs));
|
|
191
|
+
|
|
192
|
+
// Only update if changed
|
|
193
|
+
if (newWindowMs !== this.stats.currentWindowMs) {
|
|
194
|
+
this.queryManager.setWindow(newWindowMs);
|
|
195
|
+
this.stats.currentWindowMs = newWindowMs;
|
|
196
|
+
|
|
197
|
+
if (__DEV__) {
|
|
198
|
+
console.log(
|
|
199
|
+
`[Deduplication] Adjusted window to ${newWindowMs}ms ` +
|
|
200
|
+
`(quota: ${(quotaPercentage * 100).toFixed(1)}%)`
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Get current deduplication statistics
|
|
208
|
+
*/
|
|
209
|
+
getStatistics(): DeduplicationStatistics {
|
|
210
|
+
return { ...this.stats };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Reset statistics
|
|
215
|
+
*/
|
|
216
|
+
resetStatistics(): void {
|
|
217
|
+
this.stats = {
|
|
218
|
+
totalQueries: 0,
|
|
219
|
+
cachedQueries: 0,
|
|
220
|
+
executedQueries: 0,
|
|
221
|
+
currentWindowMs: this.stats.currentWindowMs,
|
|
222
|
+
cacheHitRate: 0,
|
|
223
|
+
pendingQueries: this.queryManager.size(),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Update cache hit rate
|
|
229
|
+
*/
|
|
230
|
+
private updateCacheHitRate(): void {
|
|
231
|
+
this.stats.cacheHitRate =
|
|
232
|
+
this.stats.totalQueries > 0
|
|
233
|
+
? this.stats.cachedQueries / this.stats.totalQueries
|
|
234
|
+
: 0;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Update statistics
|
|
239
|
+
*/
|
|
240
|
+
private updateStats(): void {
|
|
241
|
+
this.stats.pendingQueries = this.queryManager.size();
|
|
242
|
+
this.updateCacheHitRate();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Clear all cached queries
|
|
247
|
+
*/
|
|
248
|
+
clear(): void {
|
|
249
|
+
this.queryManager.clear();
|
|
250
|
+
this.stats.pendingQueries = 0;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Destroy middleware and cleanup resources
|
|
255
|
+
*/
|
|
256
|
+
destroy(): void {
|
|
257
|
+
this.destroyed = true;
|
|
258
|
+
this.timerManager.destroy();
|
|
259
|
+
this.queryManager.clear();
|
|
260
|
+
this.stats.pendingQueries = 0;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Get number of pending queries
|
|
265
|
+
*/
|
|
266
|
+
getPendingCount(): number {
|
|
267
|
+
return this.queryManager.size();
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Default singleton instance with recommended settings
|
|
273
|
+
*/
|
|
274
|
+
export const queryDeduplicationMiddleware = new QueryDeduplicationMiddleware({
|
|
275
|
+
baseWindowMs: DEFAULT_DEDUPLICATION_WINDOW_MS,
|
|
276
|
+
cleanupIntervalMs: DEFAULT_CLEANUP_INTERVAL_MS,
|
|
277
|
+
quotaAware: true,
|
|
278
|
+
maxWindowMs: 60000, // 1 minute
|
|
279
|
+
minWindowMs: 1000, // 1 second
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Helper function to integrate deduplication with quota tracking
|
|
284
|
+
* Automatically adjusts window based on quota usage
|
|
285
|
+
*
|
|
286
|
+
* Note: This is NOT a React hook, but a helper function.
|
|
287
|
+
* Call this from your own hook or effect as needed.
|
|
288
|
+
*
|
|
289
|
+
* @example
|
|
290
|
+
* ```typescript
|
|
291
|
+
* // In your own hook or component:
|
|
292
|
+
* useEffect(() => {
|
|
293
|
+
* syncDeduplicationWithQuota(queryDeduplicationMiddleware, quotaMiddleware, quotaLimits);
|
|
294
|
+
* }, [quotaMiddleware.getCounts().reads]);
|
|
295
|
+
* ```
|
|
296
|
+
*/
|
|
297
|
+
export function syncDeduplicationWithQuota(
|
|
298
|
+
deduplication: QueryDeduplicationMiddleware,
|
|
299
|
+
quotaMiddleware: { getCounts: () => { reads: number; writes: number; deletes: number } },
|
|
300
|
+
quotaLimits: { dailyReadLimit: number }
|
|
301
|
+
): void {
|
|
302
|
+
// Adjust deduplication window based on quota
|
|
303
|
+
const counts = quotaMiddleware.getCounts();
|
|
304
|
+
const quotaPercentage = counts.reads / quotaLimits.dailyReadLimit;
|
|
305
|
+
deduplication.adjustWindowForQuota(quotaPercentage);
|
|
306
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quota Tracking Middleware
|
|
3
|
+
* Tracks Firestore operations for quota monitoring
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
interface OperationInfo {
|
|
7
|
+
type: 'read' | 'write' | 'delete';
|
|
8
|
+
collection: string;
|
|
9
|
+
count: number;
|
|
10
|
+
cached: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class QuotaTrackingMiddleware {
|
|
14
|
+
private readCount = 0;
|
|
15
|
+
private writeCount = 0;
|
|
16
|
+
private deleteCount = 0;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Track a Firestore operation
|
|
20
|
+
*/
|
|
21
|
+
async trackOperation<T>(
|
|
22
|
+
info: OperationInfo,
|
|
23
|
+
operation: () => Promise<T>
|
|
24
|
+
): Promise<T> {
|
|
25
|
+
try {
|
|
26
|
+
return await operation();
|
|
27
|
+
} finally {
|
|
28
|
+
switch (info.type) {
|
|
29
|
+
case 'read':
|
|
30
|
+
if (!info.cached) {
|
|
31
|
+
this.readCount += info.count;
|
|
32
|
+
}
|
|
33
|
+
break;
|
|
34
|
+
case 'write':
|
|
35
|
+
this.writeCount += info.count;
|
|
36
|
+
break;
|
|
37
|
+
case 'delete':
|
|
38
|
+
this.deleteCount += info.count;
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Track read operation
|
|
46
|
+
* @param count - Number of documents read
|
|
47
|
+
* @param cached - Whether result was from cache
|
|
48
|
+
*/
|
|
49
|
+
trackRead(count: number = 1, cached: boolean = false): void {
|
|
50
|
+
if (!cached) {
|
|
51
|
+
this.readCount += count;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Track write operation
|
|
57
|
+
* @param count - Number of documents written
|
|
58
|
+
*/
|
|
59
|
+
trackWrite(count: number = 1): void {
|
|
60
|
+
this.writeCount += count;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Track delete operation
|
|
65
|
+
* @param count - Number of documents deleted
|
|
66
|
+
*/
|
|
67
|
+
trackDelete(count: number = 1): void {
|
|
68
|
+
this.deleteCount += count;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get current counts
|
|
73
|
+
*/
|
|
74
|
+
getCounts(): { reads: number; writes: number; deletes: number } {
|
|
75
|
+
return {
|
|
76
|
+
reads: this.readCount,
|
|
77
|
+
writes: this.writeCount,
|
|
78
|
+
deletes: this.deleteCount,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Reset counts
|
|
84
|
+
*/
|
|
85
|
+
reset(): void {
|
|
86
|
+
this.readCount = 0;
|
|
87
|
+
this.writeCount = 0;
|
|
88
|
+
this.deleteCount = 0;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export const quotaTrackingMiddleware = new QuotaTrackingMiddleware();
|
|
@@ -43,9 +43,11 @@ export abstract class BasePaginatedRepository extends BaseQueryRepository {
|
|
|
43
43
|
|
|
44
44
|
const collectionRef = collection(db, collectionName);
|
|
45
45
|
let q: import("firebase/firestore").Query<DocumentData>;
|
|
46
|
+
let cursorKey = 'start';
|
|
46
47
|
|
|
47
48
|
// Handle cursor-based pagination
|
|
48
49
|
if (helper.hasCursor(params) && params?.cursor) {
|
|
50
|
+
cursorKey = params.cursor;
|
|
49
51
|
|
|
50
52
|
// FIX: Validate cursor and throw error instead of silent failure
|
|
51
53
|
validateCursorOrThrow(params.cursor);
|
|
@@ -75,13 +77,19 @@ export abstract class BasePaginatedRepository extends BaseQueryRepository {
|
|
|
75
77
|
);
|
|
76
78
|
}
|
|
77
79
|
|
|
80
|
+
// Generate a unique key for deduplication (after cursor is resolved)
|
|
81
|
+
// FIX: Escape cursor to prevent key collisions from special characters
|
|
82
|
+
const escapedCursor = cursorKey.replace(/[|]/g, '_');
|
|
83
|
+
const uniqueKey = `${collectionName}_list_${orderByField}_${orderDirection}_${fetchLimit}_${escapedCursor}`;
|
|
84
|
+
|
|
78
85
|
return this.executeQuery(
|
|
79
86
|
collectionName,
|
|
80
87
|
async () => {
|
|
81
88
|
const snapshot = await getDocs(q);
|
|
82
89
|
return snapshot.docs;
|
|
83
90
|
},
|
|
84
|
-
false
|
|
91
|
+
false,
|
|
92
|
+
uniqueKey
|
|
85
93
|
);
|
|
86
94
|
}
|
|
87
95
|
|
|
@@ -1,18 +1,44 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Base Repository - Query Operations
|
|
3
|
+
*
|
|
4
|
+
* Provides query and tracking operations for Firestore repositories.
|
|
5
|
+
* Extends BaseRepository with query-specific functionality.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { queryDeduplicationMiddleware } from "../middleware/QueryDeduplicationMiddleware";
|
|
9
|
+
import { BaseRepository } from "./BaseRepository";
|
|
2
10
|
|
|
3
11
|
export abstract class BaseQueryRepository extends BaseRepository {
|
|
12
|
+
/**
|
|
13
|
+
* Execute query with deduplication and quota tracking
|
|
14
|
+
* Prevents duplicate queries and tracks quota usage
|
|
15
|
+
*/
|
|
4
16
|
protected async executeQuery<T>(
|
|
5
17
|
collection: string,
|
|
6
18
|
queryFn: () => Promise<T>,
|
|
7
|
-
cached: boolean = false
|
|
19
|
+
cached: boolean = false,
|
|
20
|
+
uniqueKey?: string
|
|
8
21
|
): Promise<T> {
|
|
9
|
-
const
|
|
22
|
+
const safeKey = uniqueKey || `${collection}_query`;
|
|
23
|
+
|
|
24
|
+
const queryKey = {
|
|
25
|
+
collection,
|
|
26
|
+
filters: safeKey,
|
|
27
|
+
limit: undefined,
|
|
28
|
+
orderBy: undefined,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return queryDeduplicationMiddleware.deduplicate(queryKey, async () => {
|
|
32
|
+
const result = await queryFn();
|
|
10
33
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
34
|
+
// Optimize: Only calculate count if tracking is needed
|
|
35
|
+
// Check if quota tracking is enabled before computing array length
|
|
36
|
+
if (!cached) {
|
|
37
|
+
const count = Array.isArray(result) ? result.length : 1;
|
|
38
|
+
this.trackRead(collection, count, cached);
|
|
39
|
+
}
|
|
15
40
|
|
|
16
|
-
|
|
41
|
+
return result;
|
|
42
|
+
});
|
|
17
43
|
}
|
|
18
44
|
}
|
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
|
|
11
11
|
import type { Firestore, CollectionReference, DocumentReference, DocumentData } from 'firebase/firestore';
|
|
12
12
|
import { getFirestore, collection, doc } from 'firebase/firestore';
|
|
13
|
+
import { isQuotaError as checkQuotaError } from '../../../../shared/domain/utils/error-handlers/error-checkers';
|
|
14
|
+
import { ERROR_MESSAGES } from '../../../../shared/domain/utils/error-handlers/error-messages';
|
|
15
|
+
import { quotaTrackingMiddleware } from '../middleware/QuotaTrackingMiddleware';
|
|
13
16
|
|
|
14
17
|
enum RepositoryState {
|
|
15
18
|
ACTIVE = 'active',
|
|
@@ -46,11 +49,11 @@ export abstract class BaseRepository implements IPathResolver {
|
|
|
46
49
|
*/
|
|
47
50
|
protected getDbOrThrow(): Firestore {
|
|
48
51
|
if (this.state === RepositoryState.DESTROYED) {
|
|
49
|
-
throw new Error(
|
|
52
|
+
throw new Error(ERROR_MESSAGES.REPOSITORY.DESTROYED);
|
|
50
53
|
}
|
|
51
54
|
const db = getFirestore();
|
|
52
55
|
if (!db) {
|
|
53
|
-
throw new Error(
|
|
56
|
+
throw new Error(ERROR_MESSAGES.FIRESTORE.NOT_INITIALIZED);
|
|
54
57
|
}
|
|
55
58
|
return db;
|
|
56
59
|
}
|
|
@@ -104,19 +107,55 @@ export abstract class BaseRepository implements IPathResolver {
|
|
|
104
107
|
protected async executeOperation<T>(
|
|
105
108
|
operation: () => Promise<T>
|
|
106
109
|
): Promise<T> {
|
|
107
|
-
|
|
110
|
+
try {
|
|
111
|
+
return await operation();
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (checkQuotaError(error)) {
|
|
114
|
+
throw new Error(ERROR_MESSAGES.FIRESTORE.QUOTA_EXCEEDED);
|
|
115
|
+
}
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
108
118
|
}
|
|
109
119
|
|
|
110
|
-
|
|
111
|
-
|
|
120
|
+
/**
|
|
121
|
+
* Track read operation for quota monitoring
|
|
122
|
+
*
|
|
123
|
+
* @param collection - Collection name (for documentation purposes)
|
|
124
|
+
* @param count - Number of documents read
|
|
125
|
+
* @param cached - Whether the result is from cache
|
|
126
|
+
*/
|
|
127
|
+
protected trackRead(
|
|
128
|
+
collection: string,
|
|
129
|
+
count: number = 1,
|
|
130
|
+
cached: boolean = false,
|
|
131
|
+
): void {
|
|
132
|
+
quotaTrackingMiddleware.trackRead(count, cached);
|
|
112
133
|
}
|
|
113
134
|
|
|
114
|
-
|
|
115
|
-
|
|
135
|
+
/**
|
|
136
|
+
* Track write operation for quota monitoring
|
|
137
|
+
*
|
|
138
|
+
* @param collection - Collection name (for documentation purposes)
|
|
139
|
+
* @param count - Number of documents written
|
|
140
|
+
*/
|
|
141
|
+
protected trackWrite(
|
|
142
|
+
collection: string,
|
|
143
|
+
count: number = 1,
|
|
144
|
+
): void {
|
|
145
|
+
quotaTrackingMiddleware.trackWrite(count);
|
|
116
146
|
}
|
|
117
147
|
|
|
118
|
-
|
|
119
|
-
|
|
148
|
+
/**
|
|
149
|
+
* Track delete operation for quota monitoring
|
|
150
|
+
*
|
|
151
|
+
* @param collection - Collection name (for documentation purposes)
|
|
152
|
+
* @param count - Number of documents deleted
|
|
153
|
+
*/
|
|
154
|
+
protected trackDelete(
|
|
155
|
+
collection: string,
|
|
156
|
+
count: number = 1,
|
|
157
|
+
): void {
|
|
158
|
+
quotaTrackingMiddleware.trackDelete(count);
|
|
120
159
|
}
|
|
121
160
|
|
|
122
161
|
/**
|