@umituz/react-native-tanstack 1.2.17 → 1.2.19

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.
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Error Helpers
3
+ * Domain layer - Error handling utilities
4
+ *
5
+ * General-purpose error handling for TanStack Query
6
+ */
7
+
8
+ /**
9
+ * Check if error is a QueryError (from TanStack Query)
10
+ */
11
+ export function isQueryError(error: unknown): boolean {
12
+ return (
13
+ error !== null &&
14
+ typeof error === 'object' &&
15
+ 'message' in error &&
16
+ typeof error.message === 'string'
17
+ );
18
+ }
19
+
20
+ /**
21
+ * Check if error is a MutationError (from TanStack Query)
22
+ */
23
+ export function isMutationError(error: unknown): boolean {
24
+ return isQueryError(error);
25
+ }
26
+
27
+ /**
28
+ * Check if error is a network error
29
+ */
30
+ export function isNetworkError(error: unknown): boolean {
31
+ if (!isQueryError(error)) return false;
32
+
33
+ const message = (error as { message: string }).message.toLowerCase();
34
+ return (
35
+ message.includes('network') ||
36
+ message.includes('fetch') ||
37
+ message.includes('connection')
38
+ );
39
+ }
40
+
41
+ /**
42
+ * Check if error is an abort error
43
+ */
44
+ export function isAbortError(error: unknown): boolean {
45
+ return error instanceof Error && error.name === 'AbortError';
46
+ }
47
+
48
+ /**
49
+ * Extract error message from error
50
+ */
51
+ export function getErrorMessage(error: unknown): string {
52
+ if (error instanceof Error) {
53
+ return error.message;
54
+ }
55
+
56
+ if (isQueryError(error)) {
57
+ return (error as { message: string }).message;
58
+ }
59
+
60
+ if (typeof error === 'string') {
61
+ return error;
62
+ }
63
+
64
+ return 'An unknown error occurred';
65
+ }
66
+
67
+ /**
68
+ * Get user-friendly error message
69
+ * Maps technical errors to user-friendly messages
70
+ */
71
+ export function getUserFriendlyMessage(error: unknown): string {
72
+ const message = getErrorMessage(error).toLowerCase();
73
+
74
+ if (isNetworkError(error)) {
75
+ return 'Network connection failed. Please check your internet connection.';
76
+ }
77
+
78
+ if (isAbortError(error)) {
79
+ return 'Request was cancelled.';
80
+ }
81
+
82
+ if (message.includes('unauthorized') || message.includes('401')) {
83
+ return 'You are not authorized to perform this action.';
84
+ }
85
+
86
+ if (message.includes('forbidden') || message.includes('403')) {
87
+ return 'You do not have permission to access this resource.';
88
+ }
89
+
90
+ if (message.includes('not found') || message.includes('404')) {
91
+ return 'The requested resource was not found.';
92
+ }
93
+
94
+ if (message.includes('validation') || message.includes('400')) {
95
+ return 'Please check your input and try again.';
96
+ }
97
+
98
+ if (message.includes('server') || message.includes('500')) {
99
+ return 'A server error occurred. Please try again later.';
100
+ }
101
+
102
+ if (message.includes('timeout')) {
103
+ return 'Request timed out. Please try again.';
104
+ }
105
+
106
+ return 'An error occurred. Please try again.';
107
+ }
108
+
109
+ /**
110
+ * Parse error response (for API errors with structured data)
111
+ */
112
+ export interface ErrorResponse {
113
+ message: string;
114
+ errors?: Record<string, string[]>;
115
+ code?: string;
116
+ }
117
+
118
+ export function parseErrorResponse(error: unknown): ErrorResponse | null {
119
+ if (!isQueryError(error)) return null;
120
+
121
+ const errorObj = error as { response?: { data?: ErrorResponse } };
122
+
123
+ if (errorObj.response?.data) {
124
+ return errorObj.response.data;
125
+ }
126
+
127
+ return null;
128
+ }
129
+
130
+ /**
131
+ * Get validation errors from error response
132
+ */
133
+ export function getValidationErrors(error: unknown): Record<string, string[]> | null {
134
+ const response = parseErrorResponse(error);
135
+ return response?.errors ?? null;
136
+ }
137
+
138
+ /**
139
+ * Get error code from error response
140
+ */
141
+ export function getErrorCode(error: unknown): string | null {
142
+ const response = parseErrorResponse(error);
143
+ return response?.code ?? null;
144
+ }
145
+
146
+ /**
147
+ * Log error in development
148
+ */
149
+ export function logError(context: string, error: unknown): void {
150
+ if (__DEV__) {
151
+ // eslint-disable-next-line no-console
152
+ console.error(`[${context}]`, error);
153
+ }
154
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Type Utilities
3
+ * Domain layer - Type extractors and helpers
4
+ *
5
+ * General-purpose type utilities for TanStack Query
6
+ */
7
+
8
+ import type { UseQueryResult, UseMutationResult } from '@tanstack/react-query';
9
+
10
+ /**
11
+ * Extract data type from UseQueryResult
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * const result = useQuery({ queryKey: ['user'], queryFn: fetchUser });
16
+ * type User = ExtractQueryDataType<typeof result>; // User
17
+ * ```
18
+ */
19
+ export type ExtractQueryDataType<TQuery extends UseQueryResult<unknown, unknown>> =
20
+ TQuery extends UseQueryResult<infer TData, unknown> ? TData : never;
21
+
22
+ /**
23
+ * Extract error type from UseQueryResult
24
+ *
25
+ * @example
26
+ * ```typescript
27
+ * const result = useQuery({ queryKey: ['user'], queryFn: fetchUser });
28
+ * type Error = ExtractQueryErrorType<typeof result>; // Error
29
+ * ```
30
+ */
31
+ export type ExtractQueryErrorType<TQuery extends UseQueryResult<unknown, unknown>> =
32
+ TQuery extends UseQueryResult<unknown, infer TError> ? TError : never;
33
+
34
+ /**
35
+ * Extract data type from UseMutationResult
36
+ *
37
+ * @example
38
+ * ```typescript
39
+ * const mutation = useMutation({ mutationFn: createUser });
40
+ * type User = ExtractMutationDataType<typeof mutation>; // User
41
+ * ```
42
+ */
43
+ export type ExtractMutationDataType<TMutation extends UseMutationResult<unknown, unknown, unknown>> =
44
+ TMutation extends UseMutationResult<infer TData, unknown, unknown> ? TData : never;
45
+
46
+ /**
47
+ * Extract error type from UseMutationResult
48
+ *
49
+ * @example
50
+ * ```typescript
51
+ * const mutation = useMutation({ mutationFn: createUser });
52
+ * type Error = ExtractMutationErrorType<typeof mutation>; // Error
53
+ * ```
54
+ */
55
+ export type ExtractMutationErrorType<TMutation extends UseMutationResult<unknown, unknown, unknown>> =
56
+ TMutation extends UseMutationResult<unknown, infer TError, unknown> ? TError : never;
57
+
58
+ /**
59
+ * Extract variables type from UseMutationResult
60
+ *
61
+ * @example
62
+ * ```typescript
63
+ * const mutation = useMutation({ mutationFn: createUser });
64
+ * type Variables = ExtractMutationVariables<typeof mutation>; // CreateUserVars
65
+ * ```
66
+ */
67
+ export type ExtractMutationVariables<TMutation extends UseMutationResult<unknown, unknown, unknown>> =
68
+ TMutation extends UseMutationResult<unknown, unknown, infer TVariables> ? TVariables : never;
69
+
70
+ /**
71
+ * Extract data type from infinite query
72
+ *
73
+ * @example
74
+ * ```typescript
75
+ * const result = useInfiniteQuery({ queryKey: ['posts'], queryFn: fetchPosts });
76
+ * type Posts = ExtractInfiniteDataType<typeof result>; // InfiniteData<PostsResponse>
77
+ * ```
78
+ */
79
+ export type ExtractInfiniteDataType<TQuery extends UseQueryResult<unknown, unknown>> =
80
+ TQuery extends UseQueryResult<infer TData, unknown> ? TData : never;
81
+
82
+ /**
83
+ * Extract page data type from infinite query
84
+ *
85
+ * @example
86
+ * ```typescript
87
+ * const result = useInfiniteQuery({ queryKey: ['posts'], queryFn: fetchPosts });
88
+ * type Page = ExtractInfinitePageType<typeof result>; // PostsResponse
89
+ * ```
90
+ */
91
+ export type ExtractInfinitePageType<TQuery> = TQuery extends {
92
+ data: { pages: infer TPages };
93
+ }
94
+ ? TPages extends Array<infer TPage>
95
+ ? TPage
96
+ : never
97
+ : never;
98
+
99
+ /**
100
+ * Make specific keys required from a type
101
+ *
102
+ * @example
103
+ * ```typescript
104
+ * type User = { id?: number; name: string; email?: string };
105
+ * type UserWithId = RequireKeys<User, 'id' | 'email'>;
106
+ * // { id: number; name: string; email: string }
107
+ * ```
108
+ */
109
+ export type RequireKeys<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;
110
+
111
+ /**
112
+ * Make specific keys optional from a type
113
+ *
114
+ * @example
115
+ * ```typescript
116
+ * type User = { id: number; name: string; email: string };
117
+ * type PartialUser = OptionalKeys<User, 'id' | 'email'>;
118
+ * // { id?: number; name: string; email?: string }
119
+ * ```
120
+ */
121
+ export type OptionalKeys<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
122
+
123
+ /**
124
+ * Deep partial type
125
+ *
126
+ * @example
127
+ * ```typescript
128
+ * type User = { id: number; profile: { name: string; email: string } };
129
+ * type PartialUser = DeepPartial<User>;
130
+ * // { id?: number; profile?: { name?: string; email?: string } }
131
+ * ```
132
+ */
133
+ export type DeepPartial<T> = T extends object
134
+ ? {
135
+ [K in keyof T]?: DeepPartial<T[K]>;
136
+ }
137
+ : T;
138
+
139
+ /**
140
+ * Deep required type
141
+ *
142
+ * @example
143
+ * ```typescript
144
+ * type User = { id?: number; profile?: { name?: string; email?: string } };
145
+ * type RequiredUser = DeepRequired<User>;
146
+ * // { id: number; profile: { name: string; email: string } }
147
+ * ```
148
+ */
149
+ export type DeepRequired<T> = T extends object
150
+ ? {
151
+ [K in keyof T]-?: DeepRequired<T[K]>;
152
+ }
153
+ : T;
package/src/index.ts CHANGED
@@ -31,6 +31,47 @@ export {
31
31
  matchQueryKey,
32
32
  } from './domain/utils/QueryKeyFactory';
33
33
 
34
+ // Domain - Type Utilities
35
+ export type {
36
+ ExtractQueryDataType,
37
+ ExtractQueryErrorType,
38
+ ExtractMutationDataType,
39
+ ExtractMutationErrorType,
40
+ ExtractMutationVariables,
41
+ ExtractInfiniteDataType,
42
+ ExtractInfinitePageType,
43
+ RequireKeys,
44
+ OptionalKeys,
45
+ DeepPartial,
46
+ DeepRequired,
47
+ } from './domain/utils/TypeUtilities';
48
+
49
+ // Domain - Error Helpers
50
+ export {
51
+ isQueryError,
52
+ isMutationError,
53
+ isNetworkError,
54
+ isAbortError,
55
+ getErrorMessage,
56
+ getUserFriendlyMessage,
57
+ parseErrorResponse,
58
+ getValidationErrors,
59
+ getErrorCode,
60
+ logError,
61
+ type ErrorResponse,
62
+ } from './domain/utils/ErrorHelpers';
63
+
64
+ // Domain - Repositories
65
+ export {
66
+ BaseRepository,
67
+ type CreateParams,
68
+ type UpdateParams,
69
+ type ListParams,
70
+ type RepositoryOptions,
71
+ } from './domain/repositories/BaseRepository';
72
+
73
+ export { RepositoryFactory } from './domain/repositories/RepositoryFactory';
74
+
34
75
  // Infrastructure - Config
35
76
  export {
36
77
  CacheStrategies,
@@ -53,6 +94,14 @@ export {
53
94
  clearGlobalQueryClient,
54
95
  } from './infrastructure/config/QueryClientSingleton';
55
96
 
97
+ // Infrastructure - Monitoring
98
+ export {
99
+ DevMonitor,
100
+ type QueryMetrics,
101
+ type CacheStats,
102
+ type DevMonitorOptions,
103
+ } from './infrastructure/monitoring/DevMonitor';
104
+
56
105
  // Infrastructure - Providers
57
106
  export { TanstackProvider, type TanstackProviderProps } from './infrastructure/providers/TanstackProvider';
58
107
 
@@ -79,6 +128,14 @@ export {
79
128
  type OptimisticUpdateConfig,
80
129
  } from './presentation/hooks/useOptimisticUpdate';
81
130
 
131
+ export {
132
+ usePrefetchQuery,
133
+ usePrefetchInfiniteQuery,
134
+ usePrefetchOnMount,
135
+ usePrefetchMultiple,
136
+ type PrefetchOptions,
137
+ } from './presentation/hooks/usePrefetch';
138
+
82
139
  // Presentation - Utils
83
140
  export {
84
141
  createConditionalRetry,
@@ -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
+ // eslint-disable-next-line no-console
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
+ // eslint-disable-next-line no-console
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
+ // eslint-disable-next-line no-console
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
+ // eslint-disable-next-line no-console
190
+ console.group('[TanStack DevMonitor] Performance Report');
191
+
192
+ if (stats) {
193
+ // eslint-disable-next-line no-console
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
+ // eslint-disable-next-line no-console
205
+ console.warn(`Found ${slowQueries.length} slow queries:`);
206
+ // eslint-disable-next-line no-console
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
+ // eslint-disable-next-line no-console
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
+ // eslint-disable-next-line no-console
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
+ // eslint-disable-next-line no-console
266
+ console.log('[TanStack DevMonitor] Reset');
267
+ }
268
+ }
269
+ }
270
+
271
+ /**
272
+ * Global dev monitor instance
273
+ */
274
+ export const DevMonitor = new DevMonitorClass();