@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.
@@ -1,361 +0,0 @@
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
- }
@@ -1,32 +0,0 @@
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
- }
@@ -1 +0,0 @@
1
- export { createFirestoreKeys } from './createFirestoreKeys';
@@ -1,119 +0,0 @@
1
- /**
2
- * Pending Query Manager Utility
3
- * Manages pending queries for deduplication
4
- */
5
-
6
- interface PendingQuery {
7
- promise: Promise<unknown>;
8
- timestamp: number;
9
- }
10
-
11
- export class PendingQueryManager {
12
- private pendingQueries = new Map<string, PendingQuery>();
13
- private deduplicationWindowMs: number;
14
-
15
- constructor(deduplicationWindowMs: number = 1000) {
16
- this.deduplicationWindowMs = deduplicationWindowMs;
17
- }
18
-
19
- /**
20
- * Update the deduplication window dynamically
21
- * Used for quota-aware adaptive deduplication
22
- */
23
- setWindow(windowMs: number): void {
24
- this.deduplicationWindowMs = windowMs;
25
- }
26
-
27
- /**
28
- * Get current deduplication window
29
- */
30
- getWindow(): number {
31
- return this.deduplicationWindowMs;
32
- }
33
-
34
- /**
35
- * Check if query is pending and not expired
36
- */
37
- isPending(key: string): boolean {
38
- const pending = this.pendingQueries.get(key);
39
- if (!pending) return false;
40
-
41
- const age = Date.now() - pending.timestamp;
42
- if (age > this.deduplicationWindowMs) {
43
- this.pendingQueries.delete(key);
44
- return false;
45
- }
46
-
47
- return true;
48
- }
49
-
50
- /**
51
- * Get pending query promise
52
- */
53
- get(key: string): Promise<unknown> | null {
54
- const pending = this.pendingQueries.get(key);
55
- return pending ? pending.promise : null;
56
- }
57
-
58
- /**
59
- * Add query to pending list.
60
- * Cleanup is handled by the caller's finally block in deduplicate().
61
- * Also attaches cleanup handlers to prevent memory leaks.
62
- */
63
- add(key: string, promise: Promise<unknown>): void {
64
- // Attach cleanup handler to ensure query is removed from map
65
- // even if caller's finally block doesn't execute (e.g., unhandled rejection)
66
- promise.finally(() => {
67
- // Immediate cleanup - no delay needed for better performance
68
- this.pendingQueries.delete(key);
69
- });
70
-
71
- this.pendingQueries.set(key, {
72
- promise,
73
- timestamp: Date.now(),
74
- });
75
- }
76
-
77
- /**
78
- * Remove a specific query from pending list
79
- */
80
- remove(key: string): void {
81
- this.pendingQueries.delete(key);
82
- }
83
-
84
- /**
85
- * Clean up expired queries
86
- * Uses current deduplication window (may be adjusted dynamically)
87
- */
88
- cleanup(): void {
89
- const now = Date.now();
90
- const windowMs = this.deduplicationWindowMs; // Capture current window
91
-
92
- for (const [key, query] of this.pendingQueries.entries()) {
93
- if (now - query.timestamp > windowMs) {
94
- this.pendingQueries.delete(key);
95
- }
96
- }
97
- }
98
-
99
- /**
100
- * Clear all pending queries
101
- */
102
- clear(): void {
103
- this.pendingQueries.clear();
104
- }
105
-
106
- /**
107
- * Get pending queries count
108
- */
109
- size(): number {
110
- return this.pendingQueries.size;
111
- }
112
-
113
- /**
114
- * Check if there are any pending queries
115
- */
116
- isEmpty(): boolean {
117
- return this.pendingQueries.size === 0;
118
- }
119
- }
@@ -1,34 +0,0 @@
1
- /**
2
- * Query Key Generator Utility
3
- * Generates unique keys for query deduplication
4
- */
5
-
6
- export interface QueryKey {
7
- collection: string;
8
- filters: string;
9
- limit?: number;
10
- orderBy?: string;
11
- }
12
-
13
- /**
14
- * Escape special characters in query key components
15
- * Prevents key collisions when filter strings contain separator characters
16
- */
17
- function escapeKeyComponent(component: string): string {
18
- return component.replace(/%/g, '%25').replace(/\|/g, '%7C');
19
- }
20
-
21
- /**
22
- * Generate a unique key from query parameters
23
- * Uses URL encoding to prevent collisions from separator characters
24
- */
25
- export function generateQueryKey(key: QueryKey): string {
26
- const parts = [
27
- escapeKeyComponent(key.collection),
28
- escapeKeyComponent(key.filters),
29
- key.limit?.toString() || '',
30
- escapeKeyComponent(key.orderBy || ''),
31
- ];
32
- return parts.join('|');
33
- }
34
-
@@ -1,83 +0,0 @@
1
- /**
2
- * Timer Manager Utility
3
- * Manages cleanup timers for deduplication middleware
4
- */
5
-
6
- interface TimerManagerOptions {
7
- cleanupIntervalMs: number;
8
- onCleanup: () => void;
9
- }
10
-
11
- export class TimerManager {
12
- private timer: ReturnType<typeof setInterval> | null = null;
13
- private readonly options: TimerManagerOptions;
14
- private destroyed = false;
15
-
16
- constructor(options: TimerManagerOptions) {
17
- this.options = options;
18
- }
19
-
20
- /**
21
- * Start the cleanup timer
22
- * Idempotent: safe to call multiple times
23
- */
24
- start(): void {
25
- if (this.destroyed) {
26
- return; // Don't start if destroyed
27
- }
28
-
29
- // Clear existing timer if running (prevents duplicate timers)
30
- if (this.timer) {
31
- this.stop();
32
- }
33
-
34
- this.timer = setInterval(() => {
35
- if (this.destroyed) {
36
- this.stop();
37
- return;
38
- }
39
-
40
- try {
41
- this.options.onCleanup();
42
- } catch (error) {
43
- // Silently handle cleanup errors to prevent timer from causing issues
44
- // Log error in development for debugging (use __DEV__ for React Native)
45
- if (__DEV__) {
46
- console.error('TimerManager cleanup error:', error);
47
- }
48
- }
49
- }, this.options.cleanupIntervalMs);
50
-
51
- // In React Native, timers may not run when app is backgrounded
52
- // Unref the timer to allow the event loop to exit if this is the only active timer
53
- if (typeof (this.timer as any).unref === 'function') {
54
- (this.timer as any).unref();
55
- }
56
- }
57
-
58
- /**
59
- * Stop the cleanup timer
60
- */
61
- stop(): void {
62
- if (this.timer) {
63
- clearInterval(this.timer);
64
- this.timer = null;
65
- }
66
- }
67
-
68
- /**
69
- * Check if timer is running
70
- */
71
- isRunning(): boolean {
72
- return this.timer !== null && !this.destroyed;
73
- }
74
-
75
- /**
76
- * Destroy the timer manager
77
- * Prevents timer from restarting
78
- */
79
- destroy(): void {
80
- this.destroyed = true;
81
- this.stop();
82
- }
83
- }