@umituz/react-native-firebase 3.0.3 → 3.0.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.
Files changed (74) hide show
  1. package/package.json +7 -1
  2. package/src/domains/account-deletion/index.ts +15 -10
  3. package/src/domains/account-deletion/infrastructure/services/account-deletion.service.ts +226 -26
  4. package/src/domains/account-deletion/infrastructure/services/reauthentication.service.ts +160 -0
  5. package/src/domains/auth/domain/value-objects/FirebaseAuthConfig.ts +1 -1
  6. package/src/domains/auth/index.ts +156 -6
  7. package/src/domains/auth/infrastructure/config/FirebaseAuthClient.ts +60 -48
  8. package/src/domains/auth/infrastructure/config/initializers/FirebaseAuthInitializer.ts +41 -5
  9. package/src/domains/auth/presentation/hooks/useGoogleOAuth.ts +115 -20
  10. package/src/domains/firestore/domain/constants/QuotaLimits.ts +101 -0
  11. package/src/domains/firestore/domain/entities/QuotaMetrics.ts +26 -0
  12. package/src/domains/firestore/domain/entities/RequestLog.ts +28 -0
  13. package/src/domains/firestore/domain/services/QuotaCalculator.ts +71 -0
  14. package/src/domains/firestore/index.ts +86 -31
  15. package/src/domains/firestore/infrastructure/config/FirestoreClient.ts +82 -45
  16. package/src/domains/firestore/infrastructure/config/initializers/FirebaseFirestoreInitializer.ts +249 -4
  17. package/src/domains/firestore/infrastructure/middleware/QueryDeduplicationMiddleware.ts +312 -0
  18. package/src/domains/firestore/infrastructure/middleware/QuotaTrackingMiddleware.ts +95 -0
  19. package/src/domains/firestore/infrastructure/repositories/BasePaginatedRepository.ts +7 -1
  20. package/src/domains/firestore/infrastructure/repositories/BaseQueryRepository.ts +34 -8
  21. package/src/domains/firestore/infrastructure/repositories/BaseRepository.ts +48 -9
  22. package/src/domains/firestore/infrastructure/services/RequestLoggerService.ts +165 -0
  23. package/src/domains/firestore/presentation/hooks/index.ts +10 -0
  24. package/src/domains/firestore/presentation/hooks/useFirestoreMutation.ts +1 -1
  25. package/src/domains/firestore/presentation/hooks/useFirestoreQuery.ts +1 -1
  26. package/src/domains/firestore/presentation/hooks/useSmartFirestoreSnapshot.ts +361 -0
  27. package/src/domains/firestore/presentation/query-keys/createFirestoreKeys.ts +32 -0
  28. package/src/domains/firestore/presentation/query-keys/index.ts +1 -0
  29. package/src/domains/firestore/utils/deduplication/pending-query-manager.util.ts +119 -0
  30. package/src/domains/firestore/utils/deduplication/query-key-generator.util.ts +34 -0
  31. package/src/domains/firestore/utils/deduplication/timer-manager.util.ts +83 -0
  32. package/src/index.ts +2 -30
  33. package/src/shared/domain/utils/calculation.util.ts +305 -17
  34. package/src/shared/domain/utils/error-handlers/error-messages.ts +0 -11
  35. package/src/shared/domain/utils/index.ts +5 -0
  36. package/src/shared/infrastructure/config/base/ClientStateManager.ts +82 -0
  37. package/src/shared/infrastructure/config/base/ServiceClientSingleton.ts +136 -20
  38. package/src/shared/infrastructure/config/clients/FirebaseClientSingleton.ts +1 -1
  39. package/src/shared/infrastructure/config/initializers/FirebaseAppInitializer.ts +9 -0
  40. package/src/shared/infrastructure/config/services/FirebaseInitializationService.ts +1 -1
  41. package/src/shared/infrastructure/config/state/FirebaseClientState.ts +14 -36
  42. package/src/application/auth/index.ts +0 -10
  43. package/src/application/auth/use-cases/index.ts +0 -6
  44. package/src/domains/account-deletion/domain/index.ts +0 -8
  45. package/src/domains/account-deletion/infrastructure/services/AccountDeletionExecutor.ts +0 -79
  46. package/src/domains/account-deletion/infrastructure/services/AccountDeletionTypes.ts +0 -32
  47. package/src/domains/auth/domain.ts +0 -16
  48. package/src/domains/auth/infrastructure/config/index.ts +0 -2
  49. package/src/domains/auth/infrastructure/config/initializers/index.ts +0 -1
  50. package/src/domains/auth/infrastructure/services/index.ts +0 -16
  51. package/src/domains/auth/infrastructure/services/utils/index.ts +0 -1
  52. package/src/domains/auth/infrastructure/stores/index.ts +0 -1
  53. package/src/domains/auth/infrastructure/utils/index.ts +0 -1
  54. package/src/domains/auth/infrastructure.ts +0 -11
  55. package/src/domains/auth/presentation/hooks/useAppleAuth.ts +0 -82
  56. package/src/domains/auth/presentation.ts +0 -31
  57. package/src/domains/firestore/domain/entities/Collection.ts +0 -122
  58. package/src/domains/firestore/domain/entities/CollectionFactory.ts +0 -55
  59. package/src/domains/firestore/domain/entities/CollectionHelpers.ts +0 -143
  60. package/src/domains/firestore/domain/entities/CollectionUtils.ts +0 -72
  61. package/src/domains/firestore/domain/entities/CollectionValidation.ts +0 -138
  62. package/src/domains/firestore/domain/index.ts +0 -61
  63. package/src/domains/firestore/domain/value-objects/QueryOptions.ts +0 -143
  64. package/src/domains/firestore/domain/value-objects/QueryOptionsFactory.ts +0 -95
  65. package/src/domains/firestore/domain/value-objects/QueryOptionsHelpers.ts +0 -110
  66. package/src/domains/firestore/domain/value-objects/WhereClause.ts +0 -114
  67. package/src/domains/firestore/domain/value-objects/WhereClauseFactory.ts +0 -101
  68. package/src/domains/firestore/domain/value-objects/WhereClauseHelpers.ts +0 -123
  69. package/src/domains/firestore/domain/value-objects/WhereClauseValidation.ts +0 -83
  70. package/src/shared/infrastructure/base/ErrorHandler.ts +0 -81
  71. package/src/shared/infrastructure/base/ServiceBase.ts +0 -62
  72. package/src/shared/infrastructure/base/TypedGuard.ts +0 -131
  73. package/src/shared/infrastructure/base/index.ts +0 -34
  74. package/src/shared/types/firebase.types.ts +0 -274
@@ -0,0 +1,165 @@
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
+
@@ -12,3 +12,13 @@ export {
12
12
  useFirestoreSnapshot,
13
13
  type UseFirestoreSnapshotOptions,
14
14
  } from './useFirestoreSnapshot';
15
+
16
+ export {
17
+ useSmartFirestoreSnapshot,
18
+ useSmartListenerControl,
19
+ } from './useSmartFirestoreSnapshot';
20
+
21
+ export type {
22
+ UseSmartFirestoreSnapshotOptions,
23
+ BackgroundStrategy,
24
+ } from './useSmartFirestoreSnapshot';
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * useFirestoreMutation
3
3
  *
4
- * TanStack Mutation integration for Firestore write operations.
4
+ * TanStack Mutation wrapper for Firestore write operations.
5
5
  * Automatically invalidates specified query keys on success.
6
6
  *
7
7
  * @example
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * useFirestoreQuery
3
3
  *
4
- * TanStack Query integration for Firestore data fetching.
4
+ * TanStack Query wrapper optimized for Firestore data fetching.
5
5
  * Provides Firestore-aware defaults, retry logic, and error handling.
6
6
  *
7
7
  * @example
@@ -0,0 +1,361 @@
1
+ /**
2
+ * useSmartFirestoreSnapshot Hook (Enhanced)
3
+ *
4
+ * Smart real-time listener with automatic lifecycle management
5
+ *
6
+ * FEATURES:
7
+ * - Automatic listener suspension when app backgrounds
8
+ * - Resume listeners when app foregrounds
9
+ * - Configurable background timeout (default: 30s)
10
+ * - Memory leak prevention
11
+ * - Battery and data savings
12
+ *
13
+ * COST SAVINGS: ~80% reduction in background listener reads
14
+ */
15
+
16
+ import { useEffect, useMemo, useRef, useState, useCallback } from 'react';
17
+ import {
18
+ useQuery,
19
+ useQueryClient,
20
+ type UseQueryResult,
21
+ type QueryKey,
22
+ } from '@tanstack/react-query';
23
+ import { AppState, AppStateStatus } from 'react-native';
24
+
25
+ /**
26
+ * Background behavior strategy
27
+ */
28
+ export type BackgroundStrategy =
29
+ | 'suspend' // Suspend listeners when app backgrounds
30
+ | 'keep' // Keep listeners active (default behavior)
31
+ | 'timeout'; // Keep listeners for timeout, then suspend
32
+
33
+ /**
34
+ * Smart snapshot options with enhanced lifecycle management
35
+ */
36
+ export interface UseSmartFirestoreSnapshotOptions<TData> {
37
+ /** Unique query key for caching */
38
+ queryKey: QueryKey;
39
+
40
+ /** Sets up the onSnapshot listener. Must return the unsubscribe function. */
41
+ subscribe: (onData: (data: TData) => void) => () => void;
42
+
43
+ /** Whether the subscription should be active */
44
+ enabled?: boolean;
45
+
46
+ /** Initial data before first snapshot arrives */
47
+ initialData?: TData;
48
+
49
+ /** Background behavior strategy (default: 'suspend') */
50
+ backgroundStrategy?: BackgroundStrategy;
51
+
52
+ /** Timeout in ms before suspending background listeners (default: 30000) */
53
+ backgroundTimeout?: number;
54
+
55
+ /** Delay in ms before resuming after foreground (default: 0) */
56
+ resumeDelay?: number;
57
+
58
+ /** Callback when listener is suspended */
59
+ onSuspend?: () => void;
60
+
61
+ /** Callback when listener is resumed */
62
+ onResume?: () => void;
63
+ }
64
+
65
+ /**
66
+ * Internal state for listener lifecycle
67
+ */
68
+ interface ListenerState {
69
+ isSuspended: boolean;
70
+ isBackgrounded: boolean;
71
+ suspendTimer: ReturnType<typeof setTimeout> | null;
72
+ }
73
+
74
+ /**
75
+ * Smart Firestore snapshot hook with automatic lifecycle management
76
+ *
77
+ * @example
78
+ * ```typescript
79
+ * const { data: matches, isLoading } = useSmartFirestoreSnapshot<Match[]>({
80
+ * queryKey: ["matches", userId],
81
+ * subscribe: (onData) => {
82
+ * if (!userId) return () => {};
83
+ * return onSnapshot(matchesCol(userId), (snap) => {
84
+ * onData(snap.docs.map(d => d.data() as Match));
85
+ * });
86
+ * },
87
+ * enabled: !!userId,
88
+ * initialData: [],
89
+ * backgroundStrategy: 'suspend', // Suspend when app backgrounds
90
+ * backgroundTimeout: 30000, // 30s timeout
91
+ * });
92
+ * ```
93
+ */
94
+ export function useSmartFirestoreSnapshot<TData>(
95
+ options: UseSmartFirestoreSnapshotOptions<TData>
96
+ ): UseQueryResult<TData, Error> {
97
+ const {
98
+ queryKey,
99
+ subscribe,
100
+ enabled = true,
101
+ initialData,
102
+ backgroundStrategy = 'suspend',
103
+ backgroundTimeout = 30000,
104
+ resumeDelay = 0,
105
+ onSuspend,
106
+ onResume,
107
+ } = options;
108
+
109
+ const queryClient = useQueryClient();
110
+ const unsubscribeRef = useRef<(() => void) | null>(null);
111
+ const dataPromiseRef = useRef<{ resolve: (value: TData) => void; reject: (error: Error) => void } | null>(null);
112
+ const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
113
+
114
+ // Listener state management
115
+ const [listenerState, setListenerState] = useState<ListenerState>({
116
+ isSuspended: false,
117
+ isBackgrounded: false,
118
+ suspendTimer: null,
119
+ });
120
+
121
+ // Stabilize queryKey to prevent unnecessary listener re-subscriptions
122
+ const stableKeyString = JSON.stringify(queryKey);
123
+ const stableQueryKey = useMemo(() => queryKey, [stableKeyString]);
124
+
125
+ /**
126
+ * Suspend the listener (stop receiving updates)
127
+ */
128
+ const suspendListener = useCallback(() => {
129
+ if (unsubscribeRef.current && !listenerState.isSuspended) {
130
+ unsubscribeRef.current();
131
+ unsubscribeRef.current = null;
132
+
133
+ setListenerState(prev => ({ ...prev, isSuspended: true }));
134
+
135
+ // Clear pending promise to prevent memory leaks
136
+ if (dataPromiseRef.current) {
137
+ dataPromiseRef.current.reject(new Error('Listener suspended'));
138
+ dataPromiseRef.current = null;
139
+ }
140
+
141
+ // Clear timeout
142
+ if (timeoutRef.current) {
143
+ clearTimeout(timeoutRef.current);
144
+ timeoutRef.current = null;
145
+ }
146
+
147
+ // Notify callback
148
+ onSuspend?.();
149
+
150
+ if (__DEV__) {
151
+ console.log(`[SmartSnapshot] Listener suspended for query:`, queryKey);
152
+ }
153
+ }
154
+ }, [queryKey, listenerState.isSuspended, onSuspend]);
155
+
156
+ /**
157
+ * Resume the listener (start receiving updates again)
158
+ */
159
+ const resumeListener = useCallback(() => {
160
+ if (!unsubscribeRef.current && enabled && !listenerState.isBackgrounded) {
161
+ setListenerState(prev => ({ ...prev, isSuspended: false }));
162
+
163
+ // Notify callback
164
+ onResume?.();
165
+
166
+ if (__DEV__) {
167
+ console.log(`[SmartSnapshot] Listener resumed for query:`, queryKey);
168
+ }
169
+ }
170
+ }, [enabled, listenerState.isBackgrounded, onResume, queryKey]);
171
+
172
+ /**
173
+ * Handle app state changes (foreground/background)
174
+ */
175
+ useEffect(() => {
176
+ const timers: ReturnType<typeof setTimeout>[] = [];
177
+
178
+ const handleAppStateChange = (nextAppState: AppStateStatus) => {
179
+ const isBackgrounded = nextAppState.match(/inactive|background/);
180
+
181
+ setListenerState(prev => {
182
+ // Clear existing suspend timer
183
+ if (prev.suspendTimer) {
184
+ clearTimeout(prev.suspendTimer);
185
+ }
186
+
187
+ // App entering background
188
+ if (isBackgrounded && !prev.isBackgrounded) {
189
+ if (__DEV__) {
190
+ console.log(`[SmartSnapshot] App entering background for query:`, queryKey);
191
+ }
192
+
193
+ switch (backgroundStrategy) {
194
+ case 'suspend':
195
+ // Suspend immediately - track timer for cleanup
196
+ const suspendTimer = setTimeout(() => suspendListener(), 0);
197
+ timers.push(suspendTimer);
198
+ break;
199
+
200
+ case 'timeout':
201
+ // Suspend after timeout - timer stored in state for cleanup
202
+ const timer = setTimeout(() => {
203
+ suspendListener();
204
+ }, backgroundTimeout);
205
+ return { ...prev, isBackgrounded: true, suspendTimer: timer };
206
+
207
+ case 'keep':
208
+ // Keep listener active
209
+ return { ...prev, isBackgrounded: true };
210
+ }
211
+
212
+ return { ...prev, isBackgrounded: true };
213
+ }
214
+
215
+ // App entering foreground
216
+ if (!isBackgrounded && prev.isBackgrounded) {
217
+ if (__DEV__) {
218
+ console.log(`[SmartSnapshot] App entering foreground for query:`, queryKey);
219
+ }
220
+
221
+ // Resume listener (with optional delay) - track timer for cleanup
222
+ const resumeTimer = setTimeout(() => {
223
+ resumeListener();
224
+ }, resumeDelay);
225
+ timers.push(resumeTimer);
226
+
227
+ return { ...prev, isBackgrounded: false, suspendTimer: null };
228
+ }
229
+
230
+ return prev;
231
+ });
232
+ };
233
+
234
+ const subscription = AppState.addEventListener('change', handleAppStateChange);
235
+
236
+ return () => {
237
+ subscription.remove();
238
+ // Clean up any pending timers
239
+ timers.forEach(timer => clearTimeout(timer));
240
+ };
241
+ }, [queryKey, backgroundStrategy, backgroundTimeout, resumeDelay, suspendListener, resumeListener]);
242
+
243
+ /**
244
+ * Setup the snapshot listener
245
+ * Automatically manages listener lifecycle based on app state
246
+ */
247
+ useEffect(() => {
248
+ // Don't subscribe if disabled, suspended, or backgrounded (unless 'keep' strategy)
249
+ if (!enabled || listenerState.isSuspended) {
250
+ return;
251
+ }
252
+
253
+ if (listenerState.isBackgrounded && backgroundStrategy !== 'keep') {
254
+ return;
255
+ }
256
+
257
+ // Setup listener
258
+ unsubscribeRef.current = subscribe((data) => {
259
+ queryClient.setQueryData(stableQueryKey, data);
260
+
261
+ // Resolve any pending promise from queryFn
262
+ if (dataPromiseRef.current) {
263
+ dataPromiseRef.current.resolve(data);
264
+ dataPromiseRef.current = null;
265
+ if (timeoutRef.current) {
266
+ clearTimeout(timeoutRef.current);
267
+ timeoutRef.current = null;
268
+ }
269
+ }
270
+ });
271
+
272
+ return () => {
273
+ if (unsubscribeRef.current) {
274
+ unsubscribeRef.current();
275
+ unsubscribeRef.current = null;
276
+ }
277
+
278
+ // Reject pending promise on cleanup to prevent memory leaks
279
+ if (dataPromiseRef.current) {
280
+ dataPromiseRef.current.reject(new Error('Snapshot listener cleanup'));
281
+ dataPromiseRef.current = null;
282
+ }
283
+
284
+ // Clear timeout on cleanup
285
+ if (timeoutRef.current) {
286
+ clearTimeout(timeoutRef.current);
287
+ timeoutRef.current = null;
288
+ }
289
+ };
290
+ }, [
291
+ enabled,
292
+ listenerState.isSuspended,
293
+ listenerState.isBackgrounded,
294
+ backgroundStrategy,
295
+ queryClient,
296
+ stableQueryKey,
297
+ subscribe,
298
+ ]);
299
+
300
+ /**
301
+ * TanStack Query integration
302
+ * Data comes from the snapshot listener, not from a fetch
303
+ */
304
+ return useQuery<TData, Error>({
305
+ queryKey,
306
+ queryFn: () => {
307
+ const cached = queryClient.getQueryData<TData>(queryKey);
308
+ if (cached !== undefined) return cached;
309
+ if (initialData !== undefined) return initialData;
310
+
311
+ // Return a promise that resolves when snapshot provides data
312
+ // This prevents hanging promises and memory leaks
313
+ return new Promise<TData>((resolve, reject) => {
314
+ dataPromiseRef.current = { resolve, reject };
315
+
316
+ // Timeout to prevent infinite waiting (memory leak protection)
317
+ timeoutRef.current = setTimeout(() => {
318
+ if (dataPromiseRef.current) {
319
+ dataPromiseRef.current = null;
320
+ timeoutRef.current = null;
321
+ if (initialData !== undefined) {
322
+ resolve(initialData);
323
+ } else {
324
+ reject(new Error('Snapshot listener timeout'));
325
+ }
326
+ }
327
+ }, 30000); // 30 second timeout
328
+ });
329
+ },
330
+ enabled,
331
+ initialData,
332
+ // Never refetch — data comes from the real-time listener
333
+ staleTime: Infinity,
334
+ refetchOnMount: false,
335
+ refetchOnWindowFocus: false,
336
+ refetchOnReconnect: false,
337
+ });
338
+ }
339
+
340
+ /**
341
+ * Hook to manually control listener lifecycle
342
+ * Useful for complex scenarios with custom logic
343
+ */
344
+ export function useSmartListenerControl() {
345
+ const [appState, setAppState] = useState<AppStateStatus>(AppState.currentState);
346
+
347
+ useEffect(() => {
348
+ const subscription = AppState.addEventListener('change', setAppState);
349
+ return () => subscription.remove();
350
+ }, []);
351
+
352
+ // Compute background status once to avoid duplicate regex matching
353
+ const isBackgrounded = appState.match(/inactive|background/);
354
+ const isForegrounded = !isBackgrounded;
355
+
356
+ return {
357
+ isBackgrounded,
358
+ isForegrounded,
359
+ appState,
360
+ };
361
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Firestore Query Key Factory
3
+ *
4
+ * Creates consistent, type-safe query keys for Firestore collections.
5
+ * Keys follow the pattern: [resource, scope, ...args]
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * const conversationKeys = createFirestoreKeys("conversations");
10
+ *
11
+ * conversationKeys.all() // ["conversations"]
12
+ * conversationKeys.lists() // ["conversations", "list"]
13
+ * conversationKeys.list({ status: "active" }) // ["conversations", "list", { status: "active" }]
14
+ * conversationKeys.detail("abc") // ["conversations", "detail", "abc"]
15
+ * conversationKeys.userScoped("uid123") // ["conversations", "user", "uid123"]
16
+ * ```
17
+ */
18
+
19
+ export function createFirestoreKeys(resource: string) {
20
+ return {
21
+ all: () => [resource] as const,
22
+ lists: () => [resource, 'list'] as const,
23
+ list: (filters?: Record<string, unknown>) =>
24
+ filters
25
+ ? ([resource, 'list', filters] as const)
26
+ : ([resource, 'list'] as const),
27
+ details: () => [resource, 'detail'] as const,
28
+ detail: (id: string | number) => [resource, 'detail', id] as const,
29
+ userScoped: (userId: string) => [resource, 'user', userId] as const,
30
+ custom: (...args: unknown[]) => [resource, ...args] as const,
31
+ };
32
+ }
@@ -0,0 +1 @@
1
+ export { createFirestoreKeys } from './createFirestoreKeys';