ATlast — you'll never need to find your favorites on another platform again. Find your favs in the ATmosphere.
atproto

add pagination

Changed files
+90 -16
netlify
src
+37 -4
netlify/functions/get-upload-details.ts
··· 3 3 import { getDbClient } from './db'; 4 4 import cookie from 'cookie'; 5 5 6 + const DEFAULT_PAGE_SIZE = 50; 7 + const MAX_PAGE_SIZE = 100; 8 + 6 9 export const handler: Handler = async (event: HandlerEvent): Promise<HandlerResponse> => { 7 10 try { 8 11 const uploadId = event.queryStringParameters?.uploadId; 12 + const page = parseInt(event.queryStringParameters?.page || '1'); 13 + const pageSize = Math.min( 14 + parseInt(event.queryStringParameters?.pageSize || String(DEFAULT_PAGE_SIZE)), 15 + MAX_PAGE_SIZE 16 + ); 9 17 10 18 if (!uploadId) { 11 19 return { ··· 15 23 }; 16 24 } 17 25 26 + if (page < 1 || pageSize < 1) { 27 + return { 28 + statusCode: 400, 29 + headers: { 'Content-Type': 'application/json' }, 30 + body: JSON.stringify({ error: 'Invalid page or pageSize parameters' }), 31 + }; 32 + } 33 + 18 34 // Get session from cookie 19 35 const cookies = event.headers.cookie ? cookie.parse(event.headers.cookie) : {}; 20 36 const sessionId = cookies.atlast_session; ··· 39 55 40 56 const sql = getDbClient(); 41 57 42 - // Verify upload belongs to user 58 + // Verify upload belongs to user and get total count 43 59 const uploadCheck = await sql` 44 - SELECT upload_id FROM user_uploads 60 + SELECT upload_id, total_users FROM user_uploads 45 61 WHERE upload_id = ${uploadId} AND did = ${userSession.did} 46 62 `; 47 63 ··· 53 69 }; 54 70 } 55 71 56 - // Fetch detailed results for this upload 72 + const totalUsers = (uploadCheck as any[])[0].total_users; 73 + const totalPages = Math.ceil(totalUsers / pageSize); 74 + const offset = (page - 1) * pageSize; 75 + 76 + // Fetch paginated results with optimized query 57 77 const results = await sql` 58 78 SELECT 59 79 sa.source_username, ··· 72 92 LEFT JOIN user_match_status ums ON am.id = ums.atproto_match_id AND ums.did = ${userSession.did} 73 93 WHERE usf.upload_id = ${uploadId} 74 94 ORDER BY sa.source_username 95 + LIMIT ${pageSize} 96 + OFFSET ${offset} 75 97 `; 76 98 77 99 // Group results by source username ··· 110 132 headers: { 111 133 'Content-Type': 'application/json', 112 134 'Access-Control-Allow-Origin': '*', 135 + 'Cache-Control': 'private, max-age=600', // 10 minute browser cache 113 136 }, 114 - body: JSON.stringify({ results: searchResults }), 137 + body: JSON.stringify({ 138 + results: searchResults, 139 + pagination: { 140 + page, 141 + pageSize, 142 + totalPages, 143 + totalUsers, 144 + hasNextPage: page < totalPages, 145 + hasPrevPage: page > 1 146 + } 147 + }), 115 148 }; 116 149 117 150 } catch (error) {
+53 -12
src/lib/apiClient.ts
··· 164 164 return data; 165 165 }, 166 166 167 - async getUploadDetails(uploadId: string): Promise<{ 167 + async getUploadDetails( 168 + uploadId: string, 169 + page: number = 1, 170 + pageSize: number = 50 171 + ): Promise<{ 168 172 results: SearchResult[]; 173 + pagination?: { 174 + page: number; 175 + pageSize: number; 176 + totalPages: number; 177 + totalUsers: number; 178 + hasNextPage: boolean; 179 + hasPrevPage: boolean; 180 + }; 169 181 }> { 170 - // Check cache first 171 - const cacheKey = `upload-details-${uploadId}`; 172 - const cached = cache.get<any>(cacheKey, 10 * 60 * 1000); // 10 minute cache for specific upload 182 + // Check cache first (cache by page) 183 + const cacheKey = `upload-details-${uploadId}-p${page}-s${pageSize}`; 184 + const cached = cache.get<any>(cacheKey, 10 * 60 * 1000); 173 185 if (cached) { 174 - console.log('Returning cached upload details for', uploadId); 186 + console.log('Returning cached upload details for', uploadId, 'page', page); 175 187 return cached; 176 188 } 177 189 178 - const res = await fetch(`/.netlify/functions/get-upload-details?uploadId=${uploadId}`, { 179 - credentials: 'include' 180 - }); 190 + const res = await fetch( 191 + `/.netlify/functions/get-upload-details?uploadId=${uploadId}&page=${page}&pageSize=${pageSize}`, 192 + { credentials: 'include' } 193 + ); 181 194 182 195 if (!res.ok) { 183 196 throw new Error('Failed to fetch upload details'); ··· 185 198 186 199 const data = await res.json(); 187 200 188 - // Cache upload details for 10 minutes 201 + // Cache upload details page for 10 minutes 189 202 cache.set(cacheKey, data, 10 * 60 * 1000); 190 203 191 204 return data; 192 205 }, 193 206 207 + // Helper to load all pages (for backwards compatibility) 208 + async getAllUploadDetails(uploadId: string): Promise<{ results: SearchResult[] }> { 209 + const firstPage = await this.getUploadDetails(uploadId, 1, 100); 210 + 211 + if (!firstPage.pagination || firstPage.pagination.totalPages === 1) { 212 + return { results: firstPage.results }; 213 + } 214 + 215 + // Load remaining pages 216 + const allResults = [...firstPage.results]; 217 + const promises = []; 218 + 219 + for (let page = 2; page <= firstPage.pagination.totalPages; page++) { 220 + promises.push(this.getUploadDetails(uploadId, page, 100)); 221 + } 222 + 223 + const remainingPages = await Promise.all(promises); 224 + for (const pageData of remainingPages) { 225 + allResults.push(...pageData.results); 226 + } 227 + 228 + return { results: allResults }; 229 + }, 230 + 194 231 // Search Operations 195 232 async batchSearchActors(usernames: string[]): Promise<{ results: BatchSearchResult[] }> { 196 233 // Create cache key from sorted usernames (so order doesn't matter) 197 234 const cacheKey = `search-${usernames.slice().sort().join(',')}`; 198 - const cached = cache.get<any>(cacheKey, 10 * 60 * 1000); // 10 minute cache for search results 235 + const cached = cache.get<any>(cacheKey, 10 * 60 * 1000); 199 236 if (cached) { 200 237 console.log('Returning cached search results for', usernames.length, 'users'); 201 238 return cached; ··· 241 278 242 279 const data = await res.json(); 243 280 281 + // Invalidate uploads cache after following 282 + cache.invalidate('uploads'); 283 + cache.invalidatePattern('upload-details'); 284 + 244 285 return data; 245 286 }, 246 287 ··· 275 316 const data = await res.json(); 276 317 console.log(`Successfully saved ${data.matchedUsers} matches`); 277 318 278 - // Invalidate uploads cache after saving 319 + // Invalidate caches after saving 279 320 cache.invalidate('uploads'); 280 - cache.set(`upload-details-${uploadId}`, { results }, 10 * 60 * 1000); 321 + cache.invalidatePattern('upload-details'); 281 322 282 323 return data; 283 324 } else {