Files
fitaiProto/apps/mobile/src/contexts/RecommendationsContext.tsx
T

168 lines
4.7 KiB
TypeScript

import React, {
createContext,
useContext,
useState,
useCallback,
useEffect,
useRef,
} from "react";
import { useUser, useAuth } from "@clerk/clerk-expo";
import {
getRecommendations,
generateRecommendation,
generateSelfRecommendation,
type Recommendation,
type GenerateRecommendationRequest,
} from "../api/recommendations";
import log from "../utils/logger";
interface RecommendationsContextValue {
recommendations: Recommendation[];
loading: boolean;
error: Error | null;
refetchRecommendations: () => Promise<void>;
generateNewRecommendation: (
data: GenerateRecommendationRequest,
) => Promise<Recommendation>;
generateSelfPlan: () => Promise<Recommendation>;
clearCache: () => void;
}
const RecommendationsContext = createContext<
RecommendationsContextValue | undefined
>(undefined);
export function RecommendationsProvider({
children,
}: {
children: React.ReactNode;
}) {
const { user } = useUser();
const { getToken } = useAuth();
const [recommendations, setRecommendations] = useState<Recommendation[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const [lastFetchTime, setLastFetchTime] = useState<number>(0);
const fetchInProgress = useRef(false);
// Cache recommendations for 30 seconds
const CACHE_DURATION = 30000; // 30 seconds
const refetchRecommendations = useCallback(async () => {
if (!user?.id) return;
// Prevent concurrent fetches
if (fetchInProgress.current) {
log.debug("Recommendations fetch already in progress, skipping");
return;
}
// Check if we have recent cached data
const now = Date.now();
if (recommendations.length > 0 && now - lastFetchTime < CACHE_DURATION) {
log.debug("Using cached recommendations", {
count: recommendations.length,
age: now - lastFetchTime,
cacheRemaining: CACHE_DURATION - (now - lastFetchTime),
});
return;
}
try {
fetchInProgress.current = true;
setLoading(true);
setError(null);
log.debug("Fetching fresh recommendations", { userId: user.id });
const token = await getToken();
const data = await getRecommendations(user.id, token);
setRecommendations(data);
setLastFetchTime(now);
log.debug("Recommendations fetched and cached", { count: data.length });
} catch (err) {
const error = err instanceof Error ? err : new Error(String(err));
log.error("Failed to fetch recommendations", error);
setError(error);
} finally {
setLoading(false);
fetchInProgress.current = false;
}
}, [user?.id, getToken, recommendations.length, lastFetchTime]);
const generateNewRecommendation = useCallback(
async (data: GenerateRecommendationRequest): Promise<Recommendation> => {
if (!user?.id) throw new Error("User not authenticated");
const token = await getToken();
const newRecommendation = await generateRecommendation(data, token);
// Optimistically update cache
setRecommendations((prev) => [...prev, newRecommendation]);
setLastFetchTime(Date.now());
log.debug("Recommendation generated and added to cache", {
recommendationId: newRecommendation.id,
});
return newRecommendation;
},
[user?.id, getToken],
);
const generateSelfPlan = useCallback(async (): Promise<Recommendation> => {
if (!user?.id) throw new Error("User not authenticated");
const token = await getToken();
const recommendation = await generateSelfRecommendation(token);
setRecommendations((prev) => [recommendation, ...prev]);
setLastFetchTime(Date.now());
return recommendation;
}, [user?.id, getToken]);
const clearCache = useCallback(() => {
setRecommendations([]);
setLoading(false);
setLastFetchTime(0);
setError(null);
fetchInProgress.current = false;
log.debug("Recommendations cache cleared");
}, []);
useEffect(() => {
clearCache();
if (user?.id) {
log.debug("Recommendations cache reset for user", { userId: user.id });
} else {
log.debug("Recommendations cache reset on sign-out");
}
}, [user?.id, clearCache]);
return (
<RecommendationsContext.Provider
value={{
recommendations,
loading,
error,
refetchRecommendations,
generateNewRecommendation,
generateSelfPlan,
clearCache,
}}
>
{children}
</RecommendationsContext.Provider>
);
}
export function useRecommendations() {
const context = useContext(RecommendationsContext);
if (context === undefined) {
throw new Error(
"useRecommendations must be used within a RecommendationsProvider",
);
}
return context;
}