Skip to content

Commit 8834e61

Browse files
authored
Merge pull request #164 from GenerateNU/event-performance
Speed up event render performance in frontend
2 parents e91fddd + 677138d commit 8834e61

4 files changed

Lines changed: 166 additions & 27 deletions

File tree

backend/src/controllers/local-events.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ export const getLocalEvents = async (req: Request, res: Response) => {
226226
orderBy: { time: 'asc' }
227227
});
228228

229+
// Process events in parallel with optimized geocoding
229230
const data = await Promise.all(events.map(async event => {
230231
const eventDate = event.time || new Date();
231232
const date = eventDate.toLocaleDateString('en-US', {
@@ -238,10 +239,10 @@ export const getLocalEvents = async (req: Request, res: Response) => {
238239
minute: '2-digit'
239240
});
240241

241-
return {
242+
// Create a base event object without location
243+
const baseEvent = {
242244
id: event.id,
243245
title: event.title,
244-
location: await reverseGeocode(event.lat, event.lon),
245246
date,
246247
time,
247248
genre: event.genre,
@@ -253,6 +254,23 @@ export const getLocalEvents = async (req: Request, res: Response) => {
253254
lon: event.lon,
254255
imageUrl: event.imageUrl,
255256
};
257+
258+
// Add location with timeout to prevent hanging
259+
try {
260+
const locationPromise = reverseGeocode(event.lat, event.lon);
261+
const timeoutPromise = new Promise<string>((_, reject) =>
262+
setTimeout(() => reject(new Error('Geocoding timeout')), 3000)
263+
);
264+
265+
const location = await Promise.race([locationPromise, timeoutPromise]);
266+
return { ...baseEvent, location };
267+
} catch (error) {
268+
// Fallback to coordinates if geocoding fails or times out
269+
return {
270+
...baseEvent,
271+
location: `${(event.lat ?? 0).toFixed(2)}, ${(event.lon ?? 0).toFixed(2)}`
272+
};
273+
}
256274
}));
257275

258276
res.status(200).json({ message: "Local events retrieved.", data });

backend/src/services/geocoding.ts

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
1-
// Simple in-memory cache to avoid repeated geocoding calls
2-
const locationCache = new Map<string, string>();
1+
// Enhanced in-memory cache with TTL
2+
interface CacheEntry {
3+
location: string;
4+
timestamp: number;
5+
}
6+
const locationCache = new Map<string, CacheEntry>();
7+
const CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours
8+
9+
// Clean up expired cache entries periodically
10+
setInterval(() => {
11+
const now = Date.now();
12+
for (const [key, entry] of locationCache.entries()) {
13+
if (now - entry.timestamp > CACHE_TTL) {
14+
locationCache.delete(key);
15+
}
16+
}
17+
}, 60 * 60 * 1000); // Clean up every hour
318

419
/**
520
* Reverse geocode lat/lon coordinates to a human-readable location string
@@ -15,17 +30,28 @@ export async function reverseGeocode(
1530
): Promise<string> {
1631
if (!lat || !lon) return 'Location TBD';
1732

18-
const cacheKey = `${lat},${lon}`;
19-
if (locationCache.has(cacheKey)) {
20-
return locationCache.get(cacheKey)!;
33+
const cacheKey = `${lat.toFixed(4)},${lon.toFixed(4)}`;
34+
const cached = locationCache.get(cacheKey);
35+
36+
// Return cached location if still valid
37+
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
38+
return cached.location;
2139
}
2240

2341
try {
42+
const controller = new AbortController();
43+
const timeoutId = setTimeout(() => controller.abort(), 2000); // 2 second timeout
44+
2445
const response = await fetch(
2546
`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lon}`,
26-
{ headers: { 'User-Agent': 'CineCircle/1.0' } }
47+
{
48+
headers: { 'User-Agent': 'CineCircle/1.0' },
49+
signal: controller.signal
50+
}
2751
);
2852

53+
clearTimeout(timeoutId);
54+
2955
if (!response.ok) throw new Error('Geocoding failed');
3056

3157
const data = await response.json();
@@ -36,11 +62,38 @@ export async function reverseGeocode(
3662
const region = address.state || address.country;
3763
const location = city && region ? `${city}, ${region}` : `${lat.toFixed(2)}, ${lon.toFixed(2)}`;
3864

39-
locationCache.set(cacheKey, location);
65+
// Cache the result
66+
locationCache.set(cacheKey, { location, timestamp: Date.now() });
4067
return location;
4168
} catch (error) {
4269
// Fallback to coordinates if geocoding fails
43-
return `${lat.toFixed(2)}, ${lon.toFixed(2)}`;
70+
const fallback = `${lat.toFixed(2)}, ${lon.toFixed(2)}`;
71+
// Cache fallback for a shorter period
72+
locationCache.set(cacheKey, { location: fallback, timestamp: Date.now() });
73+
return fallback;
74+
}
75+
}
76+
77+
/**
78+
* Batch reverse geocode multiple coordinates efficiently
79+
* @param coordinates - Array of {lat, lon} objects
80+
* @returns Promise resolving to array of location strings
81+
*/
82+
export async function batchReverseGeocode(
83+
coordinates: Array<{lat: number, lon: number}>
84+
): Promise<string[]> {
85+
// Process in parallel with concurrency limit to avoid overwhelming the API
86+
const BATCH_SIZE = 5;
87+
const results: string[] = [];
88+
89+
for (let i = 0; i < coordinates.length; i += BATCH_SIZE) {
90+
const batch = coordinates.slice(i, i + BATCH_SIZE);
91+
const batchResults = await Promise.all(
92+
batch.map(({lat, lon}) => reverseGeocode(lat, lon))
93+
);
94+
results.push(...batchResults);
4495
}
96+
97+
return results;
4598
}
4699

frontend/app/events/index.tsx

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState, useEffect } from 'react';
1+
import React, { useState, useEffect, useCallback } from 'react';
22
import {
33
StyleSheet,
44
View,
@@ -7,6 +7,7 @@ import {
77
TouchableOpacity,
88
ActivityIndicator,
99
SafeAreaView,
10+
RefreshControl,
1011
} from 'react-native';
1112
import { router } from 'expo-router';
1213
import { getLocalEvents, type LocalEvent } from '../../services/eventsService';
@@ -34,24 +35,31 @@ export default function Events() {
3435
dates: [],
3536
eventType: [],
3637
});
38+
const [refreshing, setRefreshing] = useState(false);
3739

38-
useEffect(() => {
39-
loadEvents();
40-
}, []);
41-
42-
const loadEvents = async () => {
40+
// Simple in-memory cache with 5-minute TTL
41+
const loadEvents = useCallback(async (forceRefresh = false) => {
4342
try {
44-
setLoading(true);
43+
if (!forceRefresh) {
44+
setLoading(true);
45+
} else {
46+
setRefreshing(true);
47+
}
4548
setError(null);
46-
const response = await getLocalEvents();
49+
const response = await getLocalEvents(forceRefresh);
4750
setEvents(response.data ?? []);
4851
} catch (err) {
4952
console.error('Failed to load events:', err);
5053
setError('Failed to load events. Please try again.');
5154
} finally {
5255
setLoading(false);
56+
setRefreshing(false);
5357
}
54-
};
58+
}, []);
59+
60+
useEffect(() => {
61+
loadEvents();
62+
}, [loadEvents]);
5563

5664
const handleEventPress = (eventId: string) => {
5765
router.push(`/events/eventDetail?eventId=${eventId}`);
@@ -74,7 +82,7 @@ export default function Events() {
7482
[filterKey]: selectedValues,
7583
});
7684
// TODO: Call backend API with filters
77-
loadEvents();
85+
loadEvents(true); // Force refresh when filters change
7886
};
7987

8088
if (loading) {
@@ -90,7 +98,7 @@ export default function Events() {
9098
return (
9199
<View style={[styles.container, styles.centered]}>
92100
<Text style={styles.errorText}>{error}</Text>
93-
<TouchableOpacity onPress={loadEvents} style={styles.retryButton}>
101+
<TouchableOpacity onPress={() => loadEvents(true)} style={styles.retryButton}>
94102
<Text style={styles.retryButtonText}>Retry</Text>
95103
</TouchableOpacity>
96104
</View>
@@ -158,6 +166,14 @@ export default function Events() {
158166
<ScrollView
159167
showsVerticalScrollIndicator={false}
160168
contentContainerStyle={{ paddingBottom: 96 }}
169+
refreshControl={
170+
<RefreshControl
171+
refreshing={refreshing}
172+
onRefresh={() => loadEvents(true)}
173+
tintColor="#333"
174+
colors={['#333']}
175+
/>
176+
}
161177
>
162178
<SearchBar
163179
placeholder="Search events..."

frontend/services/eventsService.ts

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,44 @@ type GetLocalEventResponse = components["schemas"]["GetLocalEventResponse"];
66
export type LocalEvent = components["schemas"]["LocalEvent"];
77
type GetUserEventsResponse = { data?: LocalEvent[]; message?: string };
88

9+
// Client-side cache with 5-minute TTL
10+
interface CacheEntry<T> {
11+
data: T;
12+
timestamp: number;
13+
}
14+
const cache = new Map<string, CacheEntry<any>>();
15+
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
16+
17+
function getCachedData<T>(key: string): T | null {
18+
const entry = cache.get(key);
19+
if (entry && Date.now() - entry.timestamp < CACHE_TTL) {
20+
return entry.data;
21+
}
22+
cache.delete(key);
23+
return null;
24+
}
25+
26+
function setCachedData<T>(key: string, data: T): void {
27+
cache.set(key, { data, timestamp: Date.now() });
28+
}
29+
930
/**
1031
* Fetch all local events
1132
* GET /api/local-events
1233
*/
13-
export async function getLocalEvents() {
14-
return api.get<GetLocalEventsResponse>("/api/local-events");
34+
export async function getLocalEvents(bypassCache = false) {
35+
const cacheKey = 'local-events';
36+
37+
if (!bypassCache) {
38+
const cached = getCachedData<GetLocalEventsResponse>(cacheKey);
39+
if (cached) {
40+
return cached;
41+
}
42+
}
43+
44+
const response = await api.get<GetLocalEventsResponse>("/api/local-events");
45+
setCachedData(cacheKey, response);
46+
return response;
1547
}
1648

1749
/**
@@ -27,19 +59,39 @@ export async function getLocalEvent(id: string) {
2759
* GET /api/user/events?user_id=:userId
2860
* Falls back to all local events if the endpoint is unavailable (404)
2961
*/
30-
export async function getUserEvents(userId: string): Promise<LocalEvent[]> {
62+
export async function getUserEvents(userId: string, bypassCache = false): Promise<LocalEvent[]> {
63+
const cacheKey = `user-events-${userId}`;
64+
65+
if (!bypassCache) {
66+
const cached = getCachedData<LocalEvent[]>(cacheKey);
67+
if (cached) {
68+
return cached;
69+
}
70+
}
71+
3172
try {
3273
const res = await api.get<GetUserEventsResponse>('/api/user/events', {
3374
user_id: userId,
3475
});
35-
return res.data ?? [];
76+
const events = res.data ?? [];
77+
setCachedData(cacheKey, events);
78+
return events;
3679
} catch (err) {
3780
const status = (err as ApiError)?.status;
3881
if (status && status !== 404) {
3982
throw err;
4083
}
4184
// Endpoint missing or not implemented on backend; return all local events as a fallback
42-
const fallback = await getLocalEvents();
43-
return (fallback as GetLocalEventsResponse)?.data ?? [];
85+
const fallback = await getLocalEvents(bypassCache);
86+
const events = (fallback as GetLocalEventsResponse)?.data ?? [];
87+
setCachedData(cacheKey, events);
88+
return events;
4489
}
4590
}
91+
92+
/**
93+
* Clear all cached events data
94+
*/
95+
export function clearEventsCache(): void {
96+
cache.clear();
97+
}

0 commit comments

Comments
 (0)