Skip to content

Commit e7009cc

Browse files
committed
perf: improve initialization logic
1 parent 0505c2d commit e7009cc

File tree

2 files changed

+46
-26
lines changed

2 files changed

+46
-26
lines changed

src/app/hooks/useService.ts

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { aiService } from "@/server/service/ai";
44
import { chatService } from "@/server/service/chat";
55
import { type RequestContext, localUserId } from "@/server/service/context";
66
import { settingsService } from "@/server/service/settings";
7+
import { useMemo } from "react";
78
import useSWR from "swr";
89
import type { SWRResponse } from "swr";
910
import useSWRMutation from "swr/mutation";
@@ -17,7 +18,7 @@ type ExtractPromiseType<T> = T extends Promise<infer U> ? U : T;
1718
// Remove the last parameter (userId) from function parameters
1819
type RemoveLastParam<T extends readonly unknown[]> = T extends readonly [...infer Rest, any] ? Rest : T;
1920

20-
// Transform function type to remove last parameter
21+
// Transform function type to remove last parameter and preserve exact return type
2122
type TransformFunction<T> = T extends (...args: infer P) => infer R ? (...args: RemoveLastParam<P>) => R : T;
2223

2324
// Helper type to determine the correct argument type for SWR mutation
@@ -77,6 +78,7 @@ function createEnhancedService<T>(baseService: T): EnhancedService<T> {
7778
// Add SWR method - create a fetcher that calls the original method
7879
enhancedMethod.swr = (key: string | null, ...args: any[]) => {
7980
const fetcher = async () => {
81+
console.log("SWR fetcher called with args:", args);
8082
// Add localUserId as the last parameter
8183
return await originalMethod.call(target, ...args, requestContext);
8284
};
@@ -126,10 +128,16 @@ function createEnhancedService<T>(baseService: T): EnhancedService<T> {
126128
*/
127129
function useService<T extends Record<string, any>>(defaultService: T, apiService: T): EnhancedService<T> {
128130
const { isLogin } = useAuth();
129-
const service = isLogin ? apiService : defaultService;
130131

131-
// Return enhanced service (auth loading is guaranteed to be complete at root level)
132-
return createEnhancedService(service);
132+
// Memoize service selection to prevent unnecessary re-computations
133+
const service = useMemo(() => {
134+
return isLogin ? apiService : defaultService;
135+
}, [isLogin, apiService, defaultService]);
136+
137+
// Memoize enhanced service creation
138+
return useMemo(() => {
139+
return createEnhancedService(service);
140+
}, [service]);
133141
}
134142

135143
/**
@@ -158,12 +166,16 @@ async function doRequest<T>(apiCall: (...args: any[]) => Promise<Response>, ...a
158166
}
159167
}
160168

161-
/**
162-
* Generic service proxy that automatically maps interface methods to API calls
163-
* Dynamically resolves endpoints based on method names
164-
*/
165-
function createApiServiceProxy<T extends object>(apiEndpoints: Record<string, any>): T {
166-
return new Proxy({} as T, {
169+
// Service proxy cache to avoid recreating proxies
170+
const apiServiceCache = new WeakMap<Record<string, any>, any>();
171+
172+
function createApiServiceProxy<T extends Record<string, any>>(apiEndpoints: Record<string, any>): T {
173+
// Check cache first
174+
if (apiServiceCache.has(apiEndpoints)) {
175+
return apiServiceCache.get(apiEndpoints);
176+
}
177+
178+
const proxy = new Proxy({} as T, {
167179
get(target, prop: string | symbol) {
168180
if (typeof prop === "string") {
169181
// Look for a property with $post method
@@ -179,16 +191,23 @@ function createApiServiceProxy<T extends object>(apiEndpoints: Record<string, an
179191
return undefined;
180192
},
181193
});
194+
195+
// Cache the proxy
196+
apiServiceCache.set(apiEndpoints, proxy);
197+
return proxy;
182198
}
183199

184200
export function useChatService() {
185-
return useService(chatService, createApiServiceProxy(apiClient.api.chats));
201+
const apiService = useMemo(() => createApiServiceProxy<typeof chatService>(apiClient.api.chats), []);
202+
return useService(chatService, apiService);
186203
}
187204

188205
export function useSettingsService() {
189-
return useService(settingsService, createApiServiceProxy(apiClient.api.settings));
206+
const apiService = useMemo(() => createApiServiceProxy<typeof settingsService>(apiClient.api.settings), []);
207+
return useService(settingsService, apiService);
190208
}
191209

192210
export function useAiService() {
193-
return useService(aiService, createApiServiceProxy(apiClient.api.ai));
211+
const apiService = useMemo(() => createApiServiceProxy<typeof aiService>(apiClient.api.ai), []);
212+
return useService(aiService, apiService);
194213
}

src/app/routes/__root.tsx

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,9 @@ function RootComponent() {
2222
const { isLoading: authLoading } = useAuth();
2323
const [isInitialized, setIsInitialized] = useState(false);
2424
const [initError, setInitError] = useState<string | null>(null);
25-
const [isInitialAuthCheck, setIsInitialAuthCheck] = useState(true);
25+
const [hasAuthResolved, setHasAuthResolved] = useState(false);
2626
const { i18n, t } = useTranslation();
2727

28-
// Settings service for loading initial theme configuration
2928
const settingsService = useSettingsService();
3029

3130
// Apply theme and theme color with automatic system theme detection
@@ -121,16 +120,18 @@ function RootComponent() {
121120
};
122121
}
123122

124-
// Initialize database and load settings on app startup
123+
// Track when auth has resolved at least once
125124
useEffect(() => {
126-
// Wait for auth loading to complete before initializing, but only during initial auth check
127-
if (authLoading && isInitialAuthCheck) {
128-
return;
125+
if (!authLoading && !hasAuthResolved) {
126+
setHasAuthResolved(true);
129127
}
128+
}, [authLoading, hasAuthResolved]);
130129

131-
// Mark that initial auth check is complete once auth loading finishes for the first time
132-
if (!authLoading && isInitialAuthCheck) {
133-
setIsInitialAuthCheck(false);
130+
// Initialize database and load settings on app startup
131+
useEffect(() => {
132+
// Only initialize once auth has resolved for the first time
133+
if (!hasAuthResolved) {
134+
return;
134135
}
135136

136137
let isMobileCleanup: (() => void) | null = null;
@@ -163,11 +164,11 @@ function RootComponent() {
163164
isMobileCleanup();
164165
}
165166
};
166-
}, [authLoading, isInitialAuthCheck, setIsInitialAuthCheck]);
167+
}, [hasAuthResolved]);
167168

168-
// Show loading screen during initial auth loading or app initialization
169-
// After initial auth check, subsequent auth operations won't trigger global loading
170-
if ((authLoading && isInitialAuthCheck) || (!isInitialized && !initError)) {
169+
// Show loading screen only during initial app initialization
170+
// Don't block on auth loading after first resolution
171+
if (!isInitialized && !initError) {
171172
return (
172173
<div className="flex min-h-app items-center justify-center bg-background md:min-h-screen">
173174
<div className="space-y-4 text-center">

0 commit comments

Comments
 (0)