@umituz/react-native-tanstack 1.2.17 → 1.2.18
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/README.md +143 -2
- package/package.json +1 -1
- package/src/domain/repositories/BaseRepository.ts +280 -0
- package/src/domain/repositories/RepositoryFactory.ts +135 -0
- package/src/domain/utils/ErrorHelpers.ts +154 -0
- package/src/domain/utils/TypeUtilities.ts +153 -0
- package/src/index.ts +57 -0
- package/src/infrastructure/monitoring/DevMonitor.ts +274 -0
- package/src/presentation/hooks/usePrefetch.ts +237 -0
|
@@ -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();
|