@umituz/react-native-firebase 2.4.100 → 2.5.1
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/domains/firestore/index.ts +0 -58
- package/src/domains/firestore/infrastructure/repositories/BasePaginatedRepository.ts +1 -7
- package/src/domains/firestore/infrastructure/repositories/BaseQueryRepository.ts +8 -34
- package/src/domains/firestore/infrastructure/repositories/BaseRepository.ts +9 -48
- package/src/domains/firestore/presentation/hooks/index.ts +0 -10
- package/src/domains/firestore/domain/constants/QuotaLimits.ts +0 -101
- package/src/domains/firestore/domain/entities/QuotaMetrics.ts +0 -26
- package/src/domains/firestore/domain/entities/RequestLog.ts +0 -28
- package/src/domains/firestore/domain/services/QuotaCalculator.ts +0 -71
- package/src/domains/firestore/infrastructure/middleware/QueryDeduplicationMiddleware.ts +0 -312
- package/src/domains/firestore/infrastructure/middleware/QuotaTrackingMiddleware.ts +0 -95
- package/src/domains/firestore/infrastructure/services/RequestLoggerService.ts +0 -165
- package/src/domains/firestore/presentation/hooks/useSmartFirestoreSnapshot.ts +0 -361
- package/src/domains/firestore/presentation/query-keys/createFirestoreKeys.ts +0 -32
- package/src/domains/firestore/presentation/query-keys/index.ts +0 -1
- package/src/domains/firestore/utils/deduplication/pending-query-manager.util.ts +0 -119
- package/src/domains/firestore/utils/deduplication/query-key-generator.util.ts +0 -34
- package/src/domains/firestore/utils/deduplication/timer-manager.util.ts +0 -83
|
@@ -1,312 +0,0 @@
|
|
|
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
|
-
}
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* @deprecated Use syncDeduplicationWithQuota instead (not a hook)
|
|
310
|
-
* This will be removed in a future version
|
|
311
|
-
*/
|
|
312
|
-
export const useDeduplicationWithQuota = syncDeduplicationWithQuota;
|
|
@@ -1,95 +0,0 @@
|
|
|
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 _collection - Collection name (reserved for future per-collection tracking)
|
|
47
|
-
* @param count - Number of documents read
|
|
48
|
-
* @param cached - Whether result was from cache
|
|
49
|
-
*/
|
|
50
|
-
trackRead(_collection: string, count: number = 1, cached: boolean = false): void {
|
|
51
|
-
if (!cached) {
|
|
52
|
-
this.readCount += count;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Track write operation
|
|
58
|
-
* @param _collection - Collection name (reserved for future per-collection tracking)
|
|
59
|
-
* @param count - Number of documents written
|
|
60
|
-
*/
|
|
61
|
-
trackWrite(_collection: string, count: number = 1): void {
|
|
62
|
-
this.writeCount += count;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Track delete operation
|
|
67
|
-
* @param _collection - Collection name (reserved for future per-collection tracking)
|
|
68
|
-
* @param count - Number of documents deleted
|
|
69
|
-
*/
|
|
70
|
-
trackDelete(_collection: string, count: number = 1): void {
|
|
71
|
-
this.deleteCount += count;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Get current counts
|
|
76
|
-
*/
|
|
77
|
-
getCounts(): { reads: number; writes: number; deletes: number } {
|
|
78
|
-
return {
|
|
79
|
-
reads: this.readCount,
|
|
80
|
-
writes: this.writeCount,
|
|
81
|
-
deletes: this.deleteCount,
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Reset counts
|
|
87
|
-
*/
|
|
88
|
-
reset(): void {
|
|
89
|
-
this.readCount = 0;
|
|
90
|
-
this.writeCount = 0;
|
|
91
|
-
this.deleteCount = 0;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export const quotaTrackingMiddleware = new QuotaTrackingMiddleware();
|
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Request Logger Service
|
|
3
|
-
* Infrastructure service for logging Firestore requests
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { RequestLog, RequestStats, RequestType } from '../../domain/entities/RequestLog';
|
|
7
|
-
import { generateUUID } from '@umituz/react-native-design-system/uuid';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Maximum number of logs to keep in memory
|
|
11
|
-
* Prevents unbounded memory growth
|
|
12
|
-
*/
|
|
13
|
-
export const DEFAULT_MAX_LOGS = 1000;
|
|
14
|
-
|
|
15
|
-
export class RequestLoggerService {
|
|
16
|
-
private logs: RequestLog[] = [];
|
|
17
|
-
private readonly maxLogs: number;
|
|
18
|
-
private listeners: Set<(log: RequestLog) => void> = new Set();
|
|
19
|
-
private static readonly LISTENER_ERROR_PREFIX = '[RequestLoggerService] Listener error:';
|
|
20
|
-
|
|
21
|
-
constructor(maxLogs: number = DEFAULT_MAX_LOGS) {
|
|
22
|
-
this.maxLogs = maxLogs;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Log a request
|
|
27
|
-
*/
|
|
28
|
-
logRequest(log: Omit<RequestLog, 'id' | 'timestamp'>): void {
|
|
29
|
-
const fullLog: RequestLog = {
|
|
30
|
-
...log,
|
|
31
|
-
id: generateUUID(),
|
|
32
|
-
timestamp: Date.now(),
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
this.logs.push(fullLog);
|
|
36
|
-
|
|
37
|
-
if (this.logs.length > this.maxLogs) {
|
|
38
|
-
this.logs.shift();
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
this.notifyListeners(fullLog);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Get all logs
|
|
46
|
-
*/
|
|
47
|
-
getLogs(): RequestLog[] {
|
|
48
|
-
return [...this.logs];
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Get logs by type
|
|
53
|
-
* Optimized: Return empty array early if no logs
|
|
54
|
-
*/
|
|
55
|
-
getLogsByType(type: RequestType): RequestLog[] {
|
|
56
|
-
if (this.logs.length === 0) return [];
|
|
57
|
-
return this.logs.filter((log) => log.type === type);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Get request statistics
|
|
62
|
-
* Optimized: Single-pass calculation O(n) instead of O(7n)
|
|
63
|
-
*/
|
|
64
|
-
getStats(): RequestStats {
|
|
65
|
-
let readRequests = 0;
|
|
66
|
-
let writeRequests = 0;
|
|
67
|
-
let deleteRequests = 0;
|
|
68
|
-
let listenerRequests = 0;
|
|
69
|
-
let cachedRequests = 0;
|
|
70
|
-
let failedRequests = 0;
|
|
71
|
-
let durationSum = 0;
|
|
72
|
-
let durationCount = 0;
|
|
73
|
-
|
|
74
|
-
// Single pass through logs for all statistics
|
|
75
|
-
for (const log of this.logs) {
|
|
76
|
-
switch (log.type) {
|
|
77
|
-
case 'read':
|
|
78
|
-
readRequests++;
|
|
79
|
-
break;
|
|
80
|
-
case 'write':
|
|
81
|
-
writeRequests++;
|
|
82
|
-
break;
|
|
83
|
-
case 'delete':
|
|
84
|
-
deleteRequests++;
|
|
85
|
-
break;
|
|
86
|
-
case 'listener':
|
|
87
|
-
listenerRequests++;
|
|
88
|
-
break;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (log.cached) cachedRequests++;
|
|
92
|
-
if (!log.success) failedRequests++;
|
|
93
|
-
|
|
94
|
-
if (log.duration !== undefined) {
|
|
95
|
-
durationSum += log.duration;
|
|
96
|
-
durationCount++;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return {
|
|
101
|
-
totalRequests: this.logs.length,
|
|
102
|
-
readRequests,
|
|
103
|
-
writeRequests,
|
|
104
|
-
deleteRequests,
|
|
105
|
-
listenerRequests,
|
|
106
|
-
cachedRequests,
|
|
107
|
-
failedRequests,
|
|
108
|
-
averageDuration: durationCount > 0 ? durationSum / durationCount : 0,
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Clear all logs
|
|
114
|
-
*/
|
|
115
|
-
clearLogs(): void {
|
|
116
|
-
this.logs = [];
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Add log listener
|
|
121
|
-
*/
|
|
122
|
-
addListener(listener: (log: RequestLog) => void): () => void {
|
|
123
|
-
this.listeners.add(listener);
|
|
124
|
-
return () => {
|
|
125
|
-
this.listeners.delete(listener);
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Remove all listeners
|
|
131
|
-
* Prevents memory leaks when service is destroyed
|
|
132
|
-
*/
|
|
133
|
-
removeAllListeners(): void {
|
|
134
|
-
this.listeners.clear();
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Destroy service and cleanup resources
|
|
139
|
-
*/
|
|
140
|
-
destroy(): void {
|
|
141
|
-
this.removeAllListeners();
|
|
142
|
-
this.clearLogs();
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Notify all listeners
|
|
147
|
-
*/
|
|
148
|
-
private notifyListeners(log: RequestLog): void {
|
|
149
|
-
this.listeners.forEach((listener) => {
|
|
150
|
-
try {
|
|
151
|
-
listener(log);
|
|
152
|
-
} catch (error) {
|
|
153
|
-
// Log listener errors in development to help debugging
|
|
154
|
-
// In production, silently ignore to prevent crashing the app
|
|
155
|
-
if (__DEV__) {
|
|
156
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
157
|
-
console.warn(`${RequestLoggerService.LISTENER_ERROR_PREFIX} ${errorMessage}`);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
export const requestLoggerService = new RequestLoggerService();
|
|
165
|
-
|