@umituz/react-native-design-system 2.8.9 → 2.8.11

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 (26) hide show
  1. package/package.json +8 -1
  2. package/src/exports/tanstack.ts +1 -0
  3. package/src/index.ts +5 -0
  4. package/src/media/presentation/hooks/useCardMediaUpload.ts +1 -1
  5. package/src/media/presentation/hooks/useCardMultimediaFlashcard.ts +3 -3
  6. package/src/media/presentation/hooks/useMediaUpload.ts +1 -1
  7. package/src/media/presentation/hooks/useMultimediaFlashcard.ts +3 -3
  8. package/src/tanstack/domain/constants/CacheDefaults.ts +63 -0
  9. package/src/tanstack/domain/repositories/BaseRepository.ts +280 -0
  10. package/src/tanstack/domain/repositories/RepositoryFactory.ts +135 -0
  11. package/src/tanstack/domain/types/CacheStrategy.ts +115 -0
  12. package/src/tanstack/domain/utils/ErrorHelpers.ts +154 -0
  13. package/src/tanstack/domain/utils/QueryKeyFactory.ts +134 -0
  14. package/src/tanstack/domain/utils/TypeUtilities.ts +153 -0
  15. package/src/tanstack/index.ts +161 -0
  16. package/src/tanstack/infrastructure/config/PersisterConfig.ts +162 -0
  17. package/src/tanstack/infrastructure/config/QueryClientConfig.ts +154 -0
  18. package/src/tanstack/infrastructure/config/QueryClientSingleton.ts +69 -0
  19. package/src/tanstack/infrastructure/monitoring/DevMonitor.ts +274 -0
  20. package/src/tanstack/infrastructure/providers/TanstackProvider.tsx +105 -0
  21. package/src/tanstack/presentation/hooks/useInvalidateQueries.ts +128 -0
  22. package/src/tanstack/presentation/hooks/useOptimisticUpdate.ts +88 -0
  23. package/src/tanstack/presentation/hooks/usePaginatedQuery.ts +129 -0
  24. package/src/tanstack/presentation/hooks/usePrefetch.ts +237 -0
  25. package/src/tanstack/presentation/utils/RetryHelpers.ts +67 -0
  26. package/src/tanstack/types/global.d.ts +1 -0
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Persister Configuration
3
+ * Infrastructure layer - AsyncStorage persistence setup
4
+ *
5
+ * General-purpose persistence configuration for any React Native app
6
+ */
7
+
8
+ import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister';
9
+ import { storageService } from '@umituz/react-native-storage';
10
+ import { DEFAULT_GC_TIME } from '../../domain/constants/CacheDefaults';
11
+ import type { Persister } from '@tanstack/react-query-persist-client';
12
+
13
+ /**
14
+ * Persister factory options
15
+ */
16
+ export interface PersisterFactoryOptions {
17
+ /**
18
+ * Storage key prefix
19
+ * @default 'tanstack-query'
20
+ */
21
+ keyPrefix?: string;
22
+
23
+ /**
24
+ * Maximum age of cached data in milliseconds
25
+ * Data older than this will be discarded on restore
26
+ * @default 24 hours
27
+ */
28
+ maxAge?: number;
29
+
30
+ /**
31
+ * Cache version for invalidation
32
+ * Increment this to invalidate all existing caches
33
+ * @default 1
34
+ */
35
+ busterVersion?: string;
36
+
37
+ /**
38
+ * Throttle time for persistence writes (in ms)
39
+ * Prevents excessive writes to AsyncStorage
40
+ * @default 1000
41
+ */
42
+ throttleTime?: number;
43
+ }
44
+
45
+ /**
46
+ * Create an AsyncStorage persister for TanStack Query
47
+ *
48
+ * @example
49
+ * ```typescript
50
+ * const persister = createPersister({
51
+ * keyPrefix: 'myapp',
52
+ * maxAge: 24 * 60 * 60 * 1000, // 24 hours
53
+ * busterVersion: '1',
54
+ * });
55
+ * ```
56
+ */
57
+ export function createPersister(options: PersisterFactoryOptions = {}): Persister {
58
+ const {
59
+ keyPrefix = 'tanstack-query',
60
+ maxAge = DEFAULT_GC_TIME.LONG,
61
+ busterVersion = '1',
62
+ throttleTime = 1000,
63
+ } = options;
64
+
65
+ return createAsyncStoragePersister({
66
+ storage: storageService,
67
+ key: `${keyPrefix}-cache`,
68
+ throttleTime,
69
+ serialize: (data) => {
70
+ // Add metadata for cache validation
71
+ const persistData = {
72
+ version: busterVersion,
73
+ timestamp: Date.now(),
74
+ data,
75
+ };
76
+ return JSON.stringify(persistData);
77
+ },
78
+ deserialize: (cachedString) => {
79
+ try {
80
+ const parsed = JSON.parse(cachedString);
81
+
82
+ // Validate cache version
83
+ if (parsed.version !== busterVersion) {
84
+ if (__DEV__) {
85
+
86
+ console.log(
87
+ `[TanStack Query] Cache version mismatch. Expected: ${busterVersion}, Got: ${parsed.version}`,
88
+ );
89
+ }
90
+ return undefined;
91
+ }
92
+
93
+ // Validate cache age
94
+ const age = Date.now() - parsed.timestamp;
95
+ if (age > maxAge) {
96
+ if (__DEV__) {
97
+
98
+ console.log(`[TanStack Query] Cache expired. Age: ${age}ms, Max: ${maxAge}ms`);
99
+ }
100
+ return undefined;
101
+ }
102
+
103
+ return parsed.data;
104
+ } catch (error) {
105
+ if (__DEV__) {
106
+
107
+ console.error('[TanStack Query] Failed to deserialize cache:', error);
108
+ }
109
+ return undefined;
110
+ }
111
+ },
112
+ });
113
+ }
114
+
115
+ /**
116
+ * Clear all persisted cache data
117
+ * Useful for logout or cache reset scenarios
118
+ *
119
+ * @example
120
+ * ```typescript
121
+ * await clearPersistedCache('myapp');
122
+ * ```
123
+ */
124
+ export async function clearPersistedCache(keyPrefix: string = 'tanstack-query'): Promise<void> {
125
+ try {
126
+ await storageService.removeItem(`${keyPrefix}-cache`);
127
+ if (__DEV__) {
128
+
129
+ console.log(`[TanStack Query] Cleared persisted cache: ${keyPrefix}`);
130
+ }
131
+ } catch (error) {
132
+ if (__DEV__) {
133
+
134
+ console.error('[TanStack Query] Failed to clear persisted cache:', error);
135
+ }
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Get persisted cache size (in bytes)
141
+ * Useful for monitoring storage usage
142
+ *
143
+ * @example
144
+ * ```typescript
145
+ * const size = await getPersistedCacheSize('myapp');
146
+ * console.log(`Cache size: ${size} bytes`);
147
+ * ```
148
+ */
149
+ export async function getPersistedCacheSize(
150
+ keyPrefix: string = 'tanstack-query',
151
+ ): Promise<number> {
152
+ try {
153
+ const data = await storageService.getItem(`${keyPrefix}-cache`);
154
+ return data ? new Blob([data]).size : 0;
155
+ } catch (error) {
156
+ if (__DEV__) {
157
+
158
+ console.error('[TanStack Query] Failed to get cache size:', error);
159
+ }
160
+ return 0;
161
+ }
162
+ }
@@ -0,0 +1,154 @@
1
+ /**
2
+ * QueryClient Configuration
3
+ * Infrastructure layer - TanStack Query client setup
4
+ *
5
+ * General-purpose QueryClient configuration for any React Native app
6
+ */
7
+
8
+ import { QueryClient } from '@tanstack/react-query';
9
+ import {
10
+ DEFAULT_STALE_TIME,
11
+ DEFAULT_GC_TIME,
12
+ DEFAULT_RETRY,
13
+ } from '../../domain/constants/CacheDefaults';
14
+ import type { CacheConfig, CacheStrategyType } from '../../domain/types/CacheStrategy';
15
+
16
+ /**
17
+ * Cache strategy configurations
18
+ */
19
+ export const CacheStrategies: Record<CacheStrategyType, CacheConfig> = {
20
+ REALTIME: {
21
+ staleTime: DEFAULT_STALE_TIME.REALTIME,
22
+ gcTime: DEFAULT_GC_TIME.VERY_SHORT,
23
+ refetchOnMount: 'always',
24
+ refetchOnWindowFocus: true,
25
+ refetchOnReconnect: true,
26
+ retry: DEFAULT_RETRY.MINIMAL,
27
+ },
28
+ USER_DATA: {
29
+ staleTime: DEFAULT_STALE_TIME.MEDIUM,
30
+ gcTime: DEFAULT_GC_TIME.LONG,
31
+ refetchOnMount: false,
32
+ refetchOnWindowFocus: false,
33
+ refetchOnReconnect: true,
34
+ retry: DEFAULT_RETRY.STANDARD,
35
+ },
36
+ MASTER_DATA: {
37
+ staleTime: DEFAULT_STALE_TIME.VERY_LONG,
38
+ gcTime: DEFAULT_GC_TIME.VERY_LONG,
39
+ refetchOnMount: false,
40
+ refetchOnWindowFocus: false,
41
+ refetchOnReconnect: false,
42
+ retry: DEFAULT_RETRY.STANDARD,
43
+ },
44
+ PUBLIC_DATA: {
45
+ staleTime: DEFAULT_STALE_TIME.MEDIUM,
46
+ gcTime: DEFAULT_GC_TIME.LONG,
47
+ refetchOnMount: false,
48
+ refetchOnWindowFocus: false,
49
+ refetchOnReconnect: true,
50
+ retry: DEFAULT_RETRY.STANDARD,
51
+ },
52
+ CUSTOM: {
53
+ staleTime: DEFAULT_STALE_TIME.SHORT,
54
+ gcTime: DEFAULT_GC_TIME.MEDIUM,
55
+ refetchOnMount: false,
56
+ refetchOnWindowFocus: false,
57
+ refetchOnReconnect: true,
58
+ retry: DEFAULT_RETRY.STANDARD,
59
+ },
60
+ };
61
+
62
+ /**
63
+ * Retry function type
64
+ */
65
+ export type RetryFunction = (failureCount: number, error: Error) => boolean;
66
+
67
+ /**
68
+ * QueryClient factory options
69
+ */
70
+ export interface QueryClientFactoryOptions {
71
+ /**
72
+ * Default staleTime in milliseconds
73
+ * @default 5 minutes
74
+ */
75
+ defaultStaleTime?: number;
76
+
77
+ /**
78
+ * Default gcTime in milliseconds
79
+ * @default 24 hours
80
+ */
81
+ defaultGcTime?: number;
82
+
83
+ /**
84
+ * Default retry configuration
85
+ * Can be a boolean, number, or custom retry function
86
+ * @default 3
87
+ */
88
+ defaultRetry?: boolean | number | RetryFunction;
89
+
90
+ /**
91
+ * Enable development mode logging
92
+ * @default __DEV__
93
+ */
94
+ enableDevLogging?: boolean;
95
+ }
96
+
97
+ /**
98
+ * Create a configured QueryClient instance
99
+ *
100
+ * @example
101
+ * ```typescript
102
+ * const queryClient = createQueryClient({
103
+ * defaultStaleTime: 5 * 60 * 1000, // 5 minutes
104
+ * enableDevLogging: true,
105
+ * });
106
+ * ```
107
+ */
108
+ export function createQueryClient(options: QueryClientFactoryOptions = {}): QueryClient {
109
+ const {
110
+ defaultStaleTime = DEFAULT_STALE_TIME.SHORT,
111
+ defaultGcTime = DEFAULT_GC_TIME.LONG,
112
+ defaultRetry = DEFAULT_RETRY.STANDARD,
113
+ enableDevLogging = __DEV__,
114
+ } = options;
115
+
116
+ return new QueryClient({
117
+ defaultOptions: {
118
+ queries: {
119
+ staleTime: defaultStaleTime,
120
+ gcTime: defaultGcTime,
121
+ retry: defaultRetry,
122
+ refetchOnWindowFocus: false,
123
+ refetchOnMount: false,
124
+ refetchOnReconnect: true,
125
+ },
126
+ mutations: {
127
+ retry: DEFAULT_RETRY.MINIMAL,
128
+ onError: (error) => {
129
+ if (enableDevLogging) {
130
+
131
+ console.error('[TanStack Query] Mutation error:', error);
132
+ }
133
+ },
134
+ },
135
+ },
136
+ });
137
+ }
138
+
139
+ /**
140
+ * Get cache configuration for a specific strategy
141
+ *
142
+ * @example
143
+ * ```typescript
144
+ * const config = getCacheStrategy('PUBLIC_DATA');
145
+ * const { data } = useQuery({
146
+ * queryKey: ['posts'],
147
+ * queryFn: fetchPosts,
148
+ * ...config,
149
+ * });
150
+ * ```
151
+ */
152
+ export function getCacheStrategy(strategy: CacheStrategyType): CacheConfig {
153
+ return CacheStrategies[strategy];
154
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * QueryClient Singleton
3
+ * Infrastructure layer - Global QueryClient access
4
+ *
5
+ * Provides access to QueryClient instance outside of React component tree.
6
+ * Useful for services, utilities, and callbacks that need to invalidate cache.
7
+ *
8
+ * IMPORTANT: setGlobalQueryClient must be called before using getGlobalQueryClient.
9
+ * This is typically done in TanstackProvider.
10
+ */
11
+
12
+ import type { QueryClient } from '@tanstack/react-query';
13
+
14
+ let globalQueryClient: QueryClient | null = null;
15
+
16
+ /**
17
+ * Set the global QueryClient instance.
18
+ * Called automatically by TanstackProvider.
19
+ */
20
+ export function setGlobalQueryClient(client: QueryClient): void {
21
+ if (globalQueryClient && globalQueryClient !== client) {
22
+ if (__DEV__) {
23
+
24
+ console.warn(
25
+ '[TanStack] QueryClient instance changed. Ensure you are not creating multiple instances.',
26
+ );
27
+ }
28
+ }
29
+ globalQueryClient = client;
30
+ }
31
+
32
+ /**
33
+ * Get the global QueryClient instance.
34
+ * Use this in non-React contexts (services, utilities, callbacks).
35
+ *
36
+ * @throws Error if QueryClient has not been set
37
+ *
38
+ * @example
39
+ * ```typescript
40
+ * import { getGlobalQueryClient, creditsQueryKeys } from '@umituz/react-native-tanstack';
41
+ *
42
+ * const queryClient = getGlobalQueryClient();
43
+ * queryClient.invalidateQueries({ queryKey: creditsQueryKeys.user(userId) });
44
+ * ```
45
+ */
46
+ export function getGlobalQueryClient(): QueryClient {
47
+ if (!globalQueryClient) {
48
+ throw new Error(
49
+ '[TanStack] QueryClient not initialized. Ensure TanstackProvider is rendered before calling getGlobalQueryClient.',
50
+ );
51
+ }
52
+ return globalQueryClient;
53
+ }
54
+
55
+ /**
56
+ * Check if global QueryClient is available.
57
+ * Use this before calling getGlobalQueryClient to avoid throwing.
58
+ */
59
+ export function hasGlobalQueryClient(): boolean {
60
+ return globalQueryClient !== null;
61
+ }
62
+
63
+ /**
64
+ * Clear the global QueryClient reference.
65
+ * Useful for cleanup in tests.
66
+ */
67
+ export function clearGlobalQueryClient(): void {
68
+ globalQueryClient = null;
69
+ }
@@ -0,0 +1,274 @@
1
+ /**
2
+ * DevMonitor
3
+ * Infrastructure layer - Query performance monitoring (DEV only)
4
+ *
5
+ * Tracks query performance, cache hit rates, and slow queries.
6
+ * Only active in development mode (__DEV__).
7
+ */
8
+
9
+ import type { Query, QueryClient } from '@tanstack/react-query';
10
+
11
+ export interface QueryMetrics {
12
+ queryKey: readonly unknown[];
13
+ fetchCount: number;
14
+ totalFetchTime: number;
15
+ averageFetchTime: number;
16
+ slowFetchCount: number;
17
+ lastFetchTime: number | null;
18
+ }
19
+
20
+ export interface CacheStats {
21
+ totalQueries: number;
22
+ activeQueries: number;
23
+ cachedQueries: number;
24
+ staleQueries: number;
25
+ inactiveQueries: number;
26
+ }
27
+
28
+ export interface DevMonitorOptions {
29
+ /**
30
+ * Threshold for slow query detection (in ms)
31
+ * @default 1000
32
+ */
33
+ slowQueryThreshold?: number;
34
+
35
+ /**
36
+ * Enable console logging
37
+ * @default true
38
+ */
39
+ enableLogging?: boolean;
40
+
41
+ /**
42
+ * Log interval for stats (in ms)
43
+ * @default 30000 (30 seconds)
44
+ */
45
+ statsLogInterval?: number;
46
+ }
47
+
48
+ class DevMonitorClass {
49
+ private metrics: Map<string, QueryMetrics> = new Map();
50
+ private queryClient: QueryClient | null = null;
51
+ private options: Required<DevMonitorOptions>;
52
+ private statsInterval: ReturnType<typeof setInterval> | null = null;
53
+ private isEnabled: boolean;
54
+
55
+ constructor(options: DevMonitorOptions = {}) {
56
+ this.isEnabled = __DEV__ ?? false;
57
+ this.options = {
58
+ slowQueryThreshold: options.slowQueryThreshold ?? 1000,
59
+ enableLogging: options.enableLogging ?? true,
60
+ statsLogInterval: options.statsLogInterval ?? 30000,
61
+ };
62
+
63
+ if (this.isEnabled) {
64
+ this.init();
65
+ }
66
+ }
67
+
68
+ private init(): void {
69
+ if (!this.isEnabled) return;
70
+
71
+ if (this.options.enableLogging) {
72
+
73
+ console.log('[TanStack DevMonitor] Monitoring initialized');
74
+ }
75
+
76
+ this.startStatsLogging();
77
+ }
78
+
79
+ private getQueryKeyString(queryKey: readonly unknown[]): string {
80
+ return JSON.stringify(queryKey);
81
+ }
82
+
83
+ private trackQuery(query: Query): void {
84
+ if (!this.isEnabled) return;
85
+
86
+ const queryKeyString = this.getQueryKeyString(query.queryKey);
87
+
88
+ if (!this.metrics.has(queryKeyString)) {
89
+ this.metrics.set(queryKeyString, {
90
+ queryKey: query.queryKey,
91
+ fetchCount: 0,
92
+ totalFetchTime: 0,
93
+ averageFetchTime: 0,
94
+ slowFetchCount: 0,
95
+ lastFetchTime: null,
96
+ });
97
+ }
98
+
99
+ const metrics = this.metrics.get(queryKeyString)!;
100
+ const fetchTime = Date.now() - (query.state.dataUpdatedAt ?? Date.now());
101
+
102
+ metrics.fetchCount++;
103
+ metrics.totalFetchTime += fetchTime;
104
+ metrics.averageFetchTime = metrics.totalFetchTime / metrics.fetchCount;
105
+ metrics.lastFetchTime = fetchTime;
106
+
107
+ if (fetchTime > this.options.slowQueryThreshold) {
108
+ metrics.slowFetchCount++;
109
+
110
+ if (this.options.enableLogging) {
111
+
112
+ console.warn(
113
+ `[TanStack DevMonitor] Slow query detected: ${queryKeyString} (${fetchTime}ms)`,
114
+ );
115
+ }
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Attach monitor to query client
121
+ */
122
+ attach(queryClient: QueryClient): void {
123
+ if (!this.isEnabled) return;
124
+
125
+ this.queryClient = queryClient;
126
+
127
+ queryClient.getQueryCache().subscribe((query) => {
128
+ this.trackQuery(query as unknown as Query);
129
+ });
130
+
131
+ if (this.options.enableLogging) {
132
+
133
+ console.log('[TanStack DevMonitor] Attached to QueryClient');
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Get all query metrics
139
+ */
140
+ getMetrics(): QueryMetrics[] {
141
+ if (!this.isEnabled) return [];
142
+ return Array.from(this.metrics.values());
143
+ }
144
+
145
+ /**
146
+ * Get metrics for specific query
147
+ */
148
+ getQueryMetrics(queryKey: readonly unknown[]): QueryMetrics | undefined {
149
+ if (!this.isEnabled) return undefined;
150
+ const queryKeyString = this.getQueryKeyString(queryKey);
151
+ return this.metrics.get(queryKeyString);
152
+ }
153
+
154
+ /**
155
+ * Get slow queries
156
+ */
157
+ getSlowQueries(): QueryMetrics[] {
158
+ if (!this.isEnabled) return [];
159
+ return Array.from(this.metrics.values()).filter((m) => m.slowFetchCount > 0);
160
+ }
161
+
162
+ /**
163
+ * Get cache statistics
164
+ */
165
+ getCacheStats(): CacheStats | null {
166
+ if (!this.isEnabled || !this.queryClient) return null;
167
+
168
+ const cache = this.queryClient.getQueryCache();
169
+ const queries = cache.getAll();
170
+
171
+ return {
172
+ totalQueries: queries.length,
173
+ activeQueries: queries.filter((q) => q.observers.length > 0).length,
174
+ cachedQueries: queries.filter((q) => q.state.data !== undefined).length,
175
+ staleQueries: queries.filter((q) => q.isStale()).length,
176
+ inactiveQueries: queries.filter((q) => q.observers.length === 0).length,
177
+ };
178
+ }
179
+
180
+ /**
181
+ * Log performance report
182
+ */
183
+ logReport(): void {
184
+ if (!this.isEnabled || !this.options.enableLogging) return;
185
+
186
+ const stats = this.getCacheStats();
187
+ const slowQueries = this.getSlowQueries();
188
+
189
+
190
+ console.group('[TanStack DevMonitor] Performance Report');
191
+
192
+ if (stats) {
193
+
194
+ console.table({
195
+ 'Total Queries': stats.totalQueries,
196
+ 'Active Queries': stats.activeQueries,
197
+ 'Cached Queries': stats.cachedQueries,
198
+ 'Stale Queries': stats.staleQueries,
199
+ 'Inactive Queries': stats.inactiveQueries,
200
+ });
201
+ }
202
+
203
+ if (slowQueries.length > 0) {
204
+
205
+ console.warn(`Found ${slowQueries.length} slow queries:`);
206
+
207
+ console.table(
208
+ slowQueries.map((m) => ({
209
+ queryKey: JSON.stringify(m.queryKey),
210
+ fetchCount: m.fetchCount,
211
+ avgTime: `${m.averageFetchTime.toFixed(2)}ms`,
212
+ slowCount: m.slowFetchCount,
213
+ })),
214
+ );
215
+ }
216
+
217
+
218
+ console.groupEnd();
219
+ }
220
+
221
+ /**
222
+ * Start periodic stats logging
223
+ */
224
+ private startStatsLogging(): void {
225
+ if (!this.isEnabled || this.statsInterval !== null) return;
226
+
227
+ this.statsInterval = setInterval(() => {
228
+ this.logReport();
229
+ }, this.options.statsLogInterval);
230
+ }
231
+
232
+ /**
233
+ * Stop periodic stats logging
234
+ */
235
+ stopStatsLogging(): void {
236
+ if (this.statsInterval !== null) {
237
+ clearInterval(this.statsInterval);
238
+ this.statsInterval = null;
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Clear all metrics
244
+ */
245
+ clear(): void {
246
+ if (!this.isEnabled) return;
247
+ this.metrics.clear();
248
+
249
+ if (this.options.enableLogging) {
250
+
251
+ console.log('[TanStack DevMonitor] Metrics cleared');
252
+ }
253
+ }
254
+
255
+ /**
256
+ * Reset monitor
257
+ */
258
+ reset(): void {
259
+ if (!this.isEnabled) return;
260
+ this.stopStatsLogging();
261
+ this.clear();
262
+ this.queryClient = null;
263
+
264
+ if (this.options.enableLogging) {
265
+
266
+ console.log('[TanStack DevMonitor] Reset');
267
+ }
268
+ }
269
+ }
270
+
271
+ /**
272
+ * Global dev monitor instance
273
+ */
274
+ export const DevMonitor = new DevMonitorClass();