Skip to content

Commit 622a478

Browse files
committed
Load upcoming games before full game history
1 parent bc8c875 commit 622a478

4 files changed

Lines changed: 160 additions & 46 deletions

File tree

app/build.gradle.kts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,8 @@ apollo {
109109
service("service") {
110110
packageName.set("com.example.score")
111111
introspection {
112-
endpointUrl.set("\"${secrets.getProperty("API_URL_DEV")}\"")
112+
endpointUrl.set(secrets.getProperty("API_URL_DEV"))
113113
schemaFile.set(file("src/main/graphql/schema.graphqls"))
114114
}
115115
}
116116
}
117-
Lines changed: 22 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,26 @@
11
query PagedGames($limit: Int!, $offset: Int!) {
22
games(limit: $limit, offset: $offset) {
3-
id
4-
city
5-
date
6-
gender
7-
location
8-
opponentId
9-
result
10-
sport
11-
state
12-
time
13-
scoreBreakdown
14-
utcDate
15-
team {
16-
id
17-
color
18-
image
19-
name
20-
}
21-
boxScore {
22-
team
23-
period
24-
time
25-
description
26-
scorer
27-
assist
28-
scoreBy
29-
corScore
30-
oppScore
31-
}
3+
...GameListItem
4+
}
5+
}
6+
7+
query InitialGames($startDate: DateTime!, $endDate: DateTime!) {
8+
gamesByDate(startDate: $startDate, endDate: $endDate) {
9+
...GameListItem
10+
}
11+
}
12+
13+
fragment GameListItem on GameType {
14+
id
15+
city
16+
date
17+
gender
18+
result
19+
sport
20+
time
21+
team {
22+
color
23+
image
24+
name
3225
}
3326
}

app/src/main/graphql/schema.graphqls

Lines changed: 82 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,21 @@ type Query {
99

1010
game(id: String!): GameType
1111

12-
gameByData(city: String!, date: String!, gender: String!, location: String, opponentId: String!, sport: String!, state: String!, time: String!, ticketLink: String): GameType
12+
gameByData(city: String!, date: String!, gender: String!, location: String, opponentId: String!, sport: String!, state: String!, time: String!): GameType
1313

1414
gamesBySport(sport: String!): [GameType]
1515

1616
gamesByGender(gender: String!): [GameType]
1717

1818
gamesBySportGender(sport: String!, gender: String!): [GameType]
1919

20+
gamesByDate(startDate: DateTime!, endDate: DateTime!): [GameType]
21+
22+
"""
23+
Current user's favorited games (requires auth).
24+
"""
25+
myFavoritedGames: [GameType]
26+
2027
teams: [TeamType]
2128

2229
team(id: String!): TeamType
@@ -55,9 +62,11 @@ Attributes:
5562
- id: The YouTube video ID (optional).
5663
- title: The title of the video.
5764
- description: The description of the video.
58-
- thumbnail: The URL of the video's thumbnail.
65+
- thumbnail: The URL of the video's thumbnail. (optional)
5966
- url: The URL to the video.
6067
- published_at: The date and time the video was published.
68+
- duration: The duration of the video (optional).
69+
- sportsType: The sport type extracted from the video title.
6170
"""
6271
type YoutubeVideoType {
6372
id: String
@@ -68,11 +77,15 @@ type YoutubeVideoType {
6877

6978
thumbnail: String!
7079

71-
b64Thumbnail: String!
80+
b64Thumbnail: String
7281

7382
url: String!
7483

7584
publishedAt: String!
85+
86+
duration: String
87+
88+
sportsType: String
7689
}
7790

7891
"""
@@ -181,6 +194,13 @@ type TeamType {
181194
name: String!
182195
}
183196

197+
"""
198+
The `DateTime` scalar type represents a DateTime
199+
value as specified by
200+
[iso8601](https://en.wikipedia.org/wiki/ISO_8601).
201+
"""
202+
scalar DateTime
203+
184204
type Mutation {
185205
"""
186206
Creates a new game.
@@ -195,12 +215,42 @@ type Mutation {
195215
"""
196216
Creates a new youtube video.
197217
"""
198-
createYoutubeVideo(b64Thumbnail: String!, description: String!, id: String!, publishedAt: String!, thumbnail: String!, title: String!, url: String!): CreateYoutubeVideo
218+
createYoutubeVideo(b64Thumbnail: String, description: String!, duration: String!, id: String!, publishedAt: String!, thumbnail: String!, title: String!, url: String!): CreateYoutubeVideo
199219

200220
"""
201221
Creates a new article.
202222
"""
203223
createArticle(image: String, publishedAt: String!, slug: String!, sportsType: String!, title: String!, url: String!): CreateArticle
224+
225+
"""
226+
Login by net_id; returns access_token and refresh_token.
227+
"""
228+
loginUser("User's net ID (e.g. Cornell netid)." netId: String!): LoginUser
229+
230+
"""
231+
Create a new user by net_id; returns access_token and refresh_token (no separate login needed).
232+
"""
233+
signupUser("Email address." email: String, "Display name." name: String, "User's net ID (e.g. Cornell netid)." netId: String!): SignupUser
234+
235+
"""
236+
Exchange a valid refresh token (in Authorization header) for a new access_token.
237+
"""
238+
refreshAccessToken: RefreshAccessToken
239+
240+
"""
241+
Revoke the current token (access or refresh). Send token in Authorization header.
242+
"""
243+
logoutUser: LogoutUser
244+
245+
"""
246+
Add a game to the current user's favorites (requires auth).
247+
"""
248+
addFavoriteGame("ID of the game to add to favorites." gameId: String!): AddFavoriteGame
249+
250+
"""
251+
Remove a game from the current user's favorites (requires auth).
252+
"""
253+
removeFavoriteGame("ID of the game to remove from favorites." gameId: String!): RemoveFavoriteGame
204254
}
205255

206256
type CreateGame {
@@ -219,6 +269,34 @@ type CreateArticle {
219269
article: ArticleType
220270
}
221271

272+
type LoginUser {
273+
accessToken: String
274+
275+
refreshToken: String
276+
}
277+
278+
type SignupUser {
279+
accessToken: String
280+
281+
refreshToken: String
282+
}
283+
284+
type RefreshAccessToken {
285+
newAccessToken: String
286+
}
287+
288+
type LogoutUser {
289+
success: Boolean
290+
}
291+
292+
type AddFavoriteGame {
293+
success: Boolean
294+
}
295+
296+
type RemoveFavoriteGame {
297+
success: Boolean
298+
}
299+
222300
"""
223301
A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation and subscription operations.
224302
"""

app/src/main/java/com/cornellappdev/score/model/ScoreRepository.kt

Lines changed: 55 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,18 @@ import com.cornellappdev.score.util.isValidSport
66
import com.cornellappdev.score.util.parseColor
77
import com.cornellappdev.score.util.parseResultScore
88
import com.example.score.GameByIdQuery
9+
import com.example.score.InitialGamesQuery
910
import com.example.score.GamesQuery
1011
import com.example.score.PagedGamesQuery
12+
import kotlinx.coroutines.CancellationException
1113
import kotlinx.coroutines.CoroutineScope
1214
import kotlinx.coroutines.flow.MutableStateFlow
1315
import kotlinx.coroutines.flow.asStateFlow
1416
import kotlinx.coroutines.flow.update
1517
import kotlinx.coroutines.launch
18+
import kotlinx.coroutines.withTimeoutOrNull
19+
import kotlinx.coroutines.sync.Mutex
20+
import java.time.LocalDate
1621
import kotlinx.coroutines.withTimeout
1722
import javax.inject.Inject
1823
import javax.inject.Singleton
@@ -24,15 +29,15 @@ private const val PAGE_TIMEOUT_MILLIS = 3000L
2429

2530
/**
2631
* This is a singleton responsible for fetching and caching all data for Score.
27-
* Right now, it makes a network request for all possible games. In the future,
28-
* we should limit this to games only in a certain time range, to prevent the
29-
* app from slowing down and improve load times.
32+
* Publishes a small date window first, then loads the full game history.
3033
*/
3134
@Singleton
3235
class ScoreRepository @Inject constructor(
3336
private val apolloClient: ApolloClient,
3437
private val appScope: CoroutineScope,
3538
) {
39+
private val gamesFetchMutex = Mutex()
40+
3641
private val _upcomingGamesFlow =
3742
MutableStateFlow<ApiResponse<List<Game>>>(ApiResponse.Loading)
3843
val upcomingGamesFlow = _upcomingGamesFlow.asStateFlow()
@@ -99,20 +104,44 @@ class ScoreRepository @Inject constructor(
99104
}
100105

101106
fun fetchGames() = appScope.launch {
107+
if (!gamesFetchMutex.tryLock()) return@launch
102108
_upcomingGamesFlow.value = ApiResponse.Loading
103109
val allGames = mutableListOf<Game>()
104110
var offset = 0
105111
var retries = 0
112+
var initialWindow = true
106113

107114
try {
108115
while (true) {
109-
val pageResult = runCatching {
110-
withTimeout(PAGE_TIMEOUT_MILLIS) {
111-
apolloClient.query(
112-
PagedGamesQuery(limit = PAGE_LIMIT, offset = offset)
113-
).execute().data?.games
116+
val pageResult = try {
117+
withTimeoutOrNull(PAGE_TIMEOUT_MILLIS) {
118+
if (initialWindow) {
119+
val today = LocalDate.now()
120+
apolloClient.query(
121+
InitialGamesQuery(
122+
today.atStartOfDay().toString(),
123+
today.plusDays(30).atStartOfDay().toString()
124+
)
125+
).execute().toResult().getOrNull()?.gamesByDate
126+
?.map { it?.gameListItem }
127+
} else {
128+
apolloClient.query(
129+
PagedGamesQuery(limit = PAGE_LIMIT, offset = offset)
130+
).execute().toResult().getOrNull()?.games
131+
?.map { it?.gameListItem }
132+
}
114133
}
115-
}.getOrNull()
134+
} catch (e: CancellationException) {
135+
throw e
136+
} catch (e: Exception) {
137+
null
138+
}
139+
140+
// A failed or empty date window falls back to the full fetch.
141+
if (initialWindow && pageResult.isNullOrEmpty()) {
142+
initialWindow = false
143+
continue
144+
}
116145

117146
if (pageResult == null) {
118147
if (retries < MAX_RETRIES) {
@@ -158,17 +187,32 @@ class ScoreRepository @Inject constructor(
158187

159188
allGames.addAll(pageGames)
160189

190+
if (initialWindow) {
191+
if (allGames.isNotEmpty()) {
192+
_upcomingGamesFlow.value = ApiResponse.Success(allGames.toList())
193+
}
194+
initialWindow = false
195+
continue
196+
}
197+
161198
if (pageResult.size < PAGE_LIMIT) break
162199
offset += PAGE_LIMIT
163200
}
164201

165202
_upcomingGamesFlow.value =
166-
if (allGames.isNotEmpty()) ApiResponse.Success(allGames)
203+
if (allGames.isNotEmpty()) ApiResponse.Success(allGames.asReversed().distinctBy { it.id }.asReversed())
204+
else if (_upcomingGamesFlow.value is ApiResponse.Success) _upcomingGamesFlow.value
167205
else ApiResponse.Error
168206

207+
} catch (e: CancellationException) {
208+
throw e
169209
} catch (e: Exception) {
170210
Log.e("ScoreRepository", "Error fetching upcoming games", e)
171-
_upcomingGamesFlow.value = ApiResponse.Error
211+
if (_upcomingGamesFlow.value !is ApiResponse.Success) {
212+
_upcomingGamesFlow.value = ApiResponse.Error
213+
}
214+
} finally {
215+
gamesFetchMutex.unlock()
172216
}
173217
}
174218

0 commit comments

Comments
 (0)