@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.
- package/package.json +8 -1
- package/src/exports/tanstack.ts +1 -0
- package/src/index.ts +5 -0
- package/src/media/presentation/hooks/useCardMediaUpload.ts +1 -1
- package/src/media/presentation/hooks/useCardMultimediaFlashcard.ts +3 -3
- package/src/media/presentation/hooks/useMediaUpload.ts +1 -1
- package/src/media/presentation/hooks/useMultimediaFlashcard.ts +3 -3
- package/src/tanstack/domain/constants/CacheDefaults.ts +63 -0
- package/src/tanstack/domain/repositories/BaseRepository.ts +280 -0
- package/src/tanstack/domain/repositories/RepositoryFactory.ts +135 -0
- package/src/tanstack/domain/types/CacheStrategy.ts +115 -0
- package/src/tanstack/domain/utils/ErrorHelpers.ts +154 -0
- package/src/tanstack/domain/utils/QueryKeyFactory.ts +134 -0
- package/src/tanstack/domain/utils/TypeUtilities.ts +153 -0
- package/src/tanstack/index.ts +161 -0
- package/src/tanstack/infrastructure/config/PersisterConfig.ts +162 -0
- package/src/tanstack/infrastructure/config/QueryClientConfig.ts +154 -0
- package/src/tanstack/infrastructure/config/QueryClientSingleton.ts +69 -0
- package/src/tanstack/infrastructure/monitoring/DevMonitor.ts +274 -0
- package/src/tanstack/infrastructure/providers/TanstackProvider.tsx +105 -0
- package/src/tanstack/presentation/hooks/useInvalidateQueries.ts +128 -0
- package/src/tanstack/presentation/hooks/useOptimisticUpdate.ts +88 -0
- package/src/tanstack/presentation/hooks/usePaginatedQuery.ts +129 -0
- package/src/tanstack/presentation/hooks/usePrefetch.ts +237 -0
- package/src/tanstack/presentation/utils/RetryHelpers.ts +67 -0
- 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();
|