Skip to content

Commit 035eea9

Browse files
fix: Course screen requests optimization (#473)
* fix: removed requests duplication * feat: CoalescingCache * fix: offline fix * feat: Removes all cached values
1 parent 1d6048d commit 035eea9

File tree

14 files changed

+527
-301
lines changed

14 files changed

+527
-301
lines changed
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package org.openedx.course.data.repository
2+
3+
import kotlinx.coroutines.CompletableDeferred
4+
import java.util.concurrent.ConcurrentHashMap
5+
6+
/**
7+
* A cache with request coalescing support.
8+
*
9+
* When multiple callers request the same data simultaneously,
10+
* only one fetch operation is performed and all callers receive the same result.
11+
*
12+
* @param K the type of cache keys
13+
* @param V the type of cached values
14+
* @param fetch the suspend function to fetch data for a given key
15+
* @param persist optional callback invoked after successful fetch (e.g., to save to database)
16+
*/
17+
class CoalescingCache<K, V>(
18+
private val fetch: suspend (K) -> V,
19+
private val persist: (suspend (K, V) -> Unit)? = null
20+
) {
21+
private val cache = ConcurrentHashMap<K, V>()
22+
private val pending = ConcurrentHashMap<K, CompletableDeferred<V>>()
23+
24+
/**
25+
* Returns cached value for the key, or null if not cached.
26+
*/
27+
fun getCached(key: K): V? = cache[key]
28+
29+
/**
30+
* Manually sets a cached value.
31+
*/
32+
fun setCached(key: K, value: V) {
33+
cache[key] = value
34+
}
35+
36+
/**
37+
* Removes all cached values.
38+
*/
39+
fun clear() {
40+
cache.clear()
41+
}
42+
43+
/**
44+
* Gets the value from cache or fetches it.
45+
*
46+
* If [forceRefresh] is false and a cached value exists, returns it immediately.
47+
* Otherwise, fetches the value. If another fetch for the same key is already
48+
* in progress, waits for that result instead of making a duplicate request.
49+
*/
50+
suspend fun getOrFetch(key: K, forceRefresh: Boolean = false): V {
51+
if (!forceRefresh) {
52+
cache[key]?.let { return it }
53+
}
54+
55+
val (deferred, isOwner) = getOrCreateDeferred(key)
56+
return if (isOwner) {
57+
try {
58+
val result = fetch(key)
59+
cache[key] = result
60+
persist?.invoke(key, result)
61+
deferred.complete(result)
62+
result
63+
} catch (e: Exception) {
64+
deferred.completeExceptionally(e)
65+
throw e
66+
} finally {
67+
pending.remove(key)
68+
}
69+
} else {
70+
deferred.await()
71+
}
72+
}
73+
74+
private fun getOrCreateDeferred(key: K): Pair<CompletableDeferred<V>, Boolean> {
75+
pending[key]?.let { return it to false }
76+
val deferred = CompletableDeferred<V>()
77+
val existing = pending.putIfAbsent(key, deferred)
78+
return if (existing != null) existing to false else deferred to true
79+
}
80+
}

0 commit comments

Comments
 (0)