This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

feat: add total takes count and optimize cache

+199 -152
+84 -68
src/features/api/routes/projects.ts
··· 1 1 import { db } from "../../../libs/db"; 2 - import { users as usersTable } from "../../../libs/schema"; 2 + import { users as usersTable, takes as takesTable } from "../../../libs/schema"; 3 3 import { handleApiError } from "../../../libs/apiError"; 4 - import { eq } from "drizzle-orm"; 5 - import { fetchUserData } from "../../../libs/cachet"; 4 + import { eq, count } from "drizzle-orm"; 5 + import { userService } from "../../../libs/cachet"; 6 6 7 7 export type Project = { 8 8 projectName: string; ··· 12 12 totalTakesTime: number; 13 13 userId: string; 14 14 userName?: string; 15 + /** Total number of takes */ 16 + takesCount: number; 15 17 }; 16 18 17 - // Cache for user data from cachet 18 - const userCache: Record<string, { name: string; timestamp: number }> = {}; 19 - const pendingRequests: Record<string, Promise<string>> = {}; 20 - const CACHE_TTL = 5 * 60 * 1000; // 5 minutes 21 - 22 - // Function to get user name from cache or fetch it 23 - async function getUserName(userId: string): Promise<string> { 24 - const now = Date.now(); 25 - 26 - // Check if user data is in cache and still valid 27 - if (userCache[userId] && now - userCache[userId].timestamp < CACHE_TTL) { 28 - return userCache[userId].name; 29 - } 30 - 31 - // If there's already a pending request for this user, return that promise 32 - // instead of creating a new request 33 - if (pendingRequests[userId]) { 34 - return pendingRequests[userId]; 35 - } 36 - 37 - // Create a new promise for this user and store it 38 - const fetchPromise = (async () => { 39 - try { 40 - const userData = await fetchUserData(userId); 41 - const userName = userData?.displayName || "Unknown User"; 42 - 43 - userCache[userId] = { 44 - name: userName, 45 - timestamp: now, 46 - }; 47 - 48 - return userName; 49 - } catch (error) { 50 - console.error("Error fetching user data:", error); 51 - return "Unknown User"; 52 - } finally { 53 - // Clean up the pending request when done 54 - delete pendingRequests[userId]; 55 - } 56 - })(); 57 - 58 - // Store the promise 59 - pendingRequests[userId] = fetchPromise; 60 - 61 - // Return the promise 62 - return fetchPromise; 63 - } 19 + // Project cache to reduce database queries 20 + const projectCache = new Map< 21 + string, 22 + { data: Project | Project[]; timestamp: number } 23 + >(); 24 + const PROJECT_CACHE_TTL = 60 * 1000; // 1 minute 64 25 65 26 export async function projects(url: URL): Promise<Response> { 66 27 const user = url.searchParams.get("user"); 67 28 try { 68 - const projects = await db 69 - .select({ 70 - projectName: usersTable.projectName, 71 - projectDescription: usersTable.projectDescription, 72 - projectBannerUrl: usersTable.projectBannerUrl, 73 - totalTakesTime: usersTable.totalTakesTime, 74 - userId: usersTable.id, 75 - }) 76 - .from(usersTable) 77 - .where(eq(usersTable.id, user ? user : usersTable.id)); 29 + // Check cache before database query 30 + const cacheKey = user || "all_projects"; 31 + const cached = projectCache.get(cacheKey); 32 + if (cached && Date.now() - cached.timestamp < PROJECT_CACHE_TTL) { 33 + return new Response( 34 + JSON.stringify({ 35 + projects: cached.data, 36 + }), 37 + { 38 + headers: { 39 + "Content-Type": "application/json", 40 + }, 41 + }, 42 + ); 43 + } 78 44 79 - if (projects.length === 0) { 45 + // Use a JOIN query to get projects and takes count in a single database operation 46 + let projectsWithCounts: { 47 + projectName: string; 48 + projectDescription: string; 49 + projectBannerUrl: string; 50 + totalTakesTime: number; 51 + userId: string; 52 + takesCount: number; 53 + }[]; 54 + 55 + if (user) { 56 + // For a single user, get their project data and takes count directly 57 + projectsWithCounts = await db 58 + .select({ 59 + projectName: usersTable.projectName, 60 + projectDescription: usersTable.projectDescription, 61 + projectBannerUrl: usersTable.projectBannerUrl, 62 + totalTakesTime: usersTable.totalTakesTime, 63 + userId: usersTable.id, 64 + takesCount: count(takesTable.id).as("takes_count"), 65 + }) 66 + .from(usersTable) 67 + .leftJoin(takesTable, eq(usersTable.id, takesTable.userId)) 68 + .where(eq(usersTable.id, user)) 69 + .groupBy(usersTable.id); 70 + } else { 71 + // For all users, get project data and takes count 72 + projectsWithCounts = await db 73 + .select({ 74 + projectName: usersTable.projectName, 75 + projectDescription: usersTable.projectDescription, 76 + projectBannerUrl: usersTable.projectBannerUrl, 77 + totalTakesTime: usersTable.totalTakesTime, 78 + userId: usersTable.id, 79 + takesCount: count(takesTable.id).as("takes_count"), 80 + }) 81 + .from(usersTable) 82 + .leftJoin(takesTable, eq(usersTable.id, takesTable.userId)) 83 + .groupBy(usersTable.id); 84 + } 85 + 86 + if (projectsWithCounts.length === 0) { 80 87 return new Response( 81 88 JSON.stringify({ 82 89 projects: [], ··· 90 97 } 91 98 92 99 // Get unique user IDs 93 - const userIds = [...new Set(projects.map((project) => project.userId))]; 100 + const userIds = [ 101 + ...new Set(projectsWithCounts.map((project) => project.userId)), 102 + ]; 94 103 95 - // Fetch all user names from cache or API 96 - const userNamesPromises = userIds.map((id) => getUserName(id)); 104 + // Fetch all user names from shared user service 105 + const userNamesPromises = userIds.map((id) => 106 + userService.getUserName(id), 107 + ); 97 108 const userNames = await Promise.all(userNamesPromises); 98 109 99 110 // Create a map of user names ··· 103 114 }); 104 115 105 116 // Add user names to projects 106 - const projectsWithUserNames = projects.map((project) => ({ 117 + const projectsWithUserNames = projectsWithCounts.map((project) => ({ 107 118 ...project, 108 119 userName: userNameMap[project.userId] || "Unknown User", 109 120 })); 110 121 122 + // Store in cache 123 + const result = user ? projectsWithUserNames[0] : projectsWithUserNames; 124 + projectCache.set(cacheKey, { 125 + data: result as Project | Project[], 126 + timestamp: Date.now(), 127 + }); 128 + 111 129 return new Response( 112 130 JSON.stringify({ 113 - projects: user 114 - ? projectsWithUserNames[0] 115 - : projectsWithUserNames, 131 + projects: result, 116 132 }), 117 133 { 118 134 headers: {
+64 -84
src/features/api/routes/recentTakes.ts
··· 1 - import { eq, desc, or } from "drizzle-orm"; 1 + import { eq, desc } from "drizzle-orm"; 2 2 import { db } from "../../../libs/db"; 3 3 import { takes as takesTable, users as usersTable } from "../../../libs/schema"; 4 4 import { handleApiError } from "../../../libs/apiError"; 5 - import { fetchUserData } from "../../../libs/cachet"; 5 + import { userService } from "../../../libs/cachet"; 6 6 7 7 export type RecentTake = { 8 8 id: string; ··· 18 18 userName?: string; // Add userName field 19 19 }; 20 20 21 - // Cache for user data from cachet 22 - const userCache: Record<string, { name: string; timestamp: number }> = {}; 23 - const CACHE_TTL = 5 * 60 * 1000; // 5 minutes 24 - // Track pending requests to avoid duplicate API calls 25 - const pendingRequests: Record<string, Promise<string>> = {}; 26 - 27 - // Function to get user name from cache or fetch it 28 - async function getUserName(userId: string): Promise<string> { 29 - const now = Date.now(); 30 - 31 - // Check if user data is in cache and still valid 32 - if (userCache[userId] && now - userCache[userId].timestamp < CACHE_TTL) { 33 - return userCache[userId].name; 34 - } 35 - 36 - // If there's already a pending request for this user, return that promise 37 - // instead of creating a new request 38 - if (pendingRequests[userId]) { 39 - return pendingRequests[userId]; 40 - } 41 - 42 - // Create a new promise for this user and store it 43 - const fetchPromise = (async () => { 44 - try { 45 - const userData = await fetchUserData(userId); 46 - const userName = userData?.displayName || "Unknown User"; 47 - 48 - userCache[userId] = { 49 - name: userName, 50 - timestamp: now, 51 - }; 52 - 53 - return userName; 54 - } catch (error) { 55 - console.error("Error fetching user data:", error); 56 - return "Unknown User"; 57 - } finally { 58 - // Clean up the pending request when done 59 - delete pendingRequests[userId]; 60 - } 61 - })(); 62 - 63 - // Store the promise 64 - pendingRequests[userId] = fetchPromise; 65 - 66 - // Return the promise 67 - return fetchPromise; 68 - } 21 + // Recent takes cache to reduce database queries 22 + const takesCache = new Map<string, { data: RecentTake[]; timestamp: number }>(); 23 + const TAKES_CACHE_TTL = 30 * 1000; // 30 seconds cache TTL - shorter since takes change frequently 69 24 70 25 export async function recentTakes(url: URL): Promise<Response> { 71 26 try { 72 27 const userId = url.searchParams.get("user"); 28 + 29 + // Check cache before querying database 30 + const cacheKey = userId || "all_takes"; 31 + const cached = takesCache.get(cacheKey); 32 + if (cached && Date.now() - cached.timestamp < TAKES_CACHE_TTL) { 33 + return new Response( 34 + JSON.stringify({ 35 + takes: cached.data, 36 + }), 37 + { 38 + headers: { 39 + "Content-Type": "application/json", 40 + }, 41 + }, 42 + ); 43 + } 73 44 74 45 if (userId) { 75 46 // Verify user exists if userId provided ··· 95 66 } 96 67 } 97 68 98 - const recentTakes = await db 99 - .select() 69 + // Use a JOIN query to get takes and user data in a single operation 70 + const takesWithUserData = await db 71 + .select({ 72 + take: { 73 + id: takesTable.id, 74 + userId: takesTable.userId, 75 + notes: takesTable.notes, 76 + createdAt: takesTable.createdAt, 77 + media: takesTable.media, 78 + elapsedTime: takesTable.elapsedTime, 79 + }, 80 + user: { 81 + projectName: usersTable.projectName, 82 + totalTakesTime: usersTable.totalTakesTime, 83 + }, 84 + }) 100 85 .from(takesTable) 86 + .leftJoin(usersTable, eq(takesTable.userId, usersTable.id)) 87 + .where(userId ? eq(takesTable.userId, userId) : undefined) 101 88 .orderBy(desc(takesTable.createdAt)) 102 - .where(eq(takesTable.userId, userId ? userId : takesTable.userId)) 103 89 .limit(40); 104 90 105 - if (recentTakes.length === 0) { 91 + if (takesWithUserData.length === 0) { 106 92 return new Response( 107 93 JSON.stringify({ 108 94 takes: [], ··· 116 102 } 117 103 118 104 // Get unique user IDs 119 - const userIds = [...new Set(recentTakes.map((take) => take.userId))]; 120 - 121 - // Query users from takes table 122 - const users = await db 123 - .select() 124 - .from(usersTable) 125 - .where(or(...userIds.map((id) => eq(usersTable.id, id)))); 105 + const userIds = [ 106 + ...new Set(takesWithUserData.map((item) => item.take.userId)), 107 + ]; 126 108 127 - // Create map of user data by ID 128 - const userMap = users.reduce( 129 - (acc, user) => { 130 - acc[user.id] = user; 131 - return acc; 132 - }, 133 - {} as Record<string, (typeof users)[number]>, 109 + // Fetch all user names from shared user service 110 + const userNamesPromises = userIds.map((id) => 111 + userService.getUserName(id), 134 112 ); 135 - 136 - // Fetch all user names from cache or API 137 - const userNamesPromises = userIds.map((id) => getUserName(id)); 138 113 const userNames = await Promise.all(userNamesPromises); 139 114 140 115 // Create a map of user names ··· 143 118 userNameMap[id] = userNames[index] || "unknown"; 144 119 }); 145 120 146 - const takes: RecentTake[] = 147 - recentTakes.map((take) => ({ 148 - id: take.id, 149 - userId: take.userId, 150 - notes: take.notes, 151 - createdAt: new Date(take.createdAt), 152 - mediaUrls: take.media ? JSON.parse(take.media) : [], 153 - elapsedTime: take.elapsedTime, 154 - project: userMap[take.userId]?.projectName || "unknown project", 155 - totalTakesTime: 156 - userMap[take.userId]?.totalTakesTime || take.elapsedTime, 157 - userName: userNameMap[take.userId] || "Unknown User", 158 - })) || []; 121 + // Map the joined results to the expected format 122 + const takes: RecentTake[] = takesWithUserData.map((item) => ({ 123 + id: item.take.id, 124 + userId: item.take.userId, 125 + notes: item.take.notes, 126 + createdAt: new Date(item.take.createdAt), 127 + mediaUrls: item.take.media ? JSON.parse(item.take.media) : [], 128 + elapsedTime: item.take.elapsedTime, 129 + project: item.user?.projectName || "unknown project", 130 + totalTakesTime: item.user?.totalTakesTime || item.take.elapsedTime, 131 + userName: userNameMap[item.take.userId] || "Unknown User", 132 + })); 133 + 134 + // Store results in cache 135 + takesCache.set(cacheKey, { 136 + data: takes, 137 + timestamp: Date.now(), 138 + }); 159 139 160 140 return new Response( 161 141 JSON.stringify({
+51
src/libs/cachet.ts
··· 10 10 image: json.image, 11 11 }; 12 12 } 13 + 14 + export const userService = { 15 + cache: {} as Record<string, { name: string; timestamp: number }>, 16 + pendingRequests: {} as Record<string, Promise<string>>, 17 + CACHE_TTL: 5 * 60 * 1000, 18 + 19 + async getUserName(userId: string): Promise<string> { 20 + const now = Date.now(); 21 + 22 + // Check if user data is in cache and still valid 23 + if ( 24 + this.cache[userId] && 25 + now - this.cache[userId].timestamp < this.CACHE_TTL 26 + ) { 27 + return this.cache[userId].name; 28 + } 29 + 30 + // If there's already a pending request for this user, return that promise 31 + // instead of creating a new request 32 + if (this.pendingRequests[userId]) { 33 + return this.pendingRequests[userId]; 34 + } 35 + 36 + // Create a new promise for this user and store it 37 + const fetchPromise = (async () => { 38 + try { 39 + const userData = await fetchUserData(userId); 40 + const userName = userData?.displayName || "Unknown User"; 41 + 42 + this.cache[userId] = { 43 + name: userName, 44 + timestamp: now, 45 + }; 46 + 47 + return userName; 48 + } catch (error) { 49 + console.error("Error fetching user data:", error); 50 + return "Unknown User"; 51 + } finally { 52 + // Clean up the pending request when done 53 + delete this.pendingRequests[userId]; 54 + } 55 + })(); 56 + 57 + // Store the promise 58 + this.pendingRequests[userId] = fetchPromise; 59 + 60 + // Return the promise 61 + return fetchPromise; 62 + }, 63 + };