Last active
August 17, 2023 15:16
-
-
Save yongjhih/8283082022a27f5ff6fc25cd79a138a7 to your computer and use it in GitHub Desktop.
Memoized Cache Interceptor
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Intercepts and caches memoized responses based on the provided LruCache. | |
* | |
* @property cache an instance of LruCache to store the responses. | |
*/ | |
class MemoizedInterceptor(private val cache: LruCache<String, Response>) : Interceptor { | |
/** | |
* Callback that will be invoked when a cached response is found. | |
* @param request the request being executed. | |
* @param isFresh `true` if the cached response is still fresh, `false` otherwise. | |
* @param remainingAge the remaining time before the cached response expires, or `null` if not applicable. | |
*/ | |
var onCached: (Request, Boolean, Long?) -> Unit = { _, _, _ -> } | |
/** | |
* Intercepts the request and checks if it has a memoized response. If available in the cache, the cached | |
* response is returned. Otherwise, a network call is made to fetch the data. | |
* | |
* @param chain the interceptor chain. | |
* @return the response either from cache or fetched from network. | |
*/ | |
override fun intercept(chain: Interceptor.Chain): Response { | |
val request = chain.request() | |
val invocation = request.tag<Invocation>() | |
val memoized = invocation?.method()?.annotation<Memoized>() | |
val cacheControl = memoized?.value | |
?.toSet() | |
?.toHeaders() | |
?.let { CacheControl.parse(it).build(request.cacheControl) } | |
val maxAgeSeconds = cacheControl?.maxAgeSeconds | |
?.takeIf { it >= 0 } | |
?: Int.MAX_VALUE | |
val onlyIfCached = cacheControl?.onlyIfCached == true | |
val maxStaleSeconds = cacheControl?.maxStaleSeconds | |
?.takeIf { it >= 0 } | |
?: Int.MAX_VALUE | |
return if (memoized != null) { | |
val cachedResponse = cache.get(request.key) | |
if (onlyIfCached) { | |
cachedResponse?.ifFresh(maxStaleSeconds.seconds.inWholeMilliseconds, cache, onCached) | |
?: request.cacheErrorResponse(cachedResponse?.protocol ?: chain.connection()?.protocol() ?: Protocol.HTTP_1_1) | |
} else { | |
cachedResponse?.ifFresh(maxAgeSeconds.seconds.inWholeMilliseconds, cache, onCached) | |
?: chain.proceed(request) | |
} | |
} else chain.proceed(request) | |
} | |
} | |
fun ResponseBody.clone(size: Long = Long.MAX_VALUE, onSource: BufferedSource.() -> Unit = {}): ResponseBody = source().apply { | |
request(size) | |
onSource() | |
}.buffer.clone().asResponseBody(contentType(), contentLength()) | |
fun Request.build(onBuild: Request.Builder.(Request) -> Unit) = newBuilder().also { it.onBuild(this) }.build() | |
fun Response.build(onBuild: Response.Builder.(Response) -> Unit) = newBuilder().also { it.onBuild(this) }.build() | |
fun HttpUrl.build(onBuild: HttpUrl.Builder.(HttpUrl) -> Unit) = newBuilder().also { it.onBuild(this) }.build() | |
inline fun <reified T> Request.tag() = tag(T::class.java) | |
inline fun <reified T : Annotation> Method.annotation(): T? = getAnnotation(T::class.java) | |
fun Iterable<String>.toHeaders(): Headers = Headers.Builder().apply { | |
mapNotNull { it.toPairBy() }.forEach { | |
val (key, value) = it | |
if (value != null) add(key, value) | |
} | |
}.build() | |
fun String.toPairBy(delimiter: String = ":", trim: Boolean = true): Pair<String, String?>? { | |
val index: Int = indexOf(delimiter) | |
if (index == -1 || index == 0) { | |
return null | |
} | |
if (index == length - 1) { | |
return substring(0, index) to null | |
} | |
return substring(0, index) to if (trim) substring(index + 1).trim() else substring(index + 1) | |
} | |
fun RequestBody.string(charset: Charset = Charsets.UTF_8) = Buffer().let { | |
writeTo(it) | |
//it.readUtf8() | |
it.readString(contentType()?.charset() ?: charset) | |
} | |
fun Response.isExpired(maxAge: Long, from: Long = System.currentTimeMillis()): Boolean = | |
remainingAge(maxAge, from) < 0 | |
fun Response.isFresh(maxAge: Long, from: Long = System.currentTimeMillis()): Boolean = | |
!isExpired(maxAge, from) | |
fun Response.age(from: Long = System.currentTimeMillis()): Long = | |
from - receivedResponseAtMillis | |
fun Response.remainingAge(maxAge: Long, from: Long = System.currentTimeMillis()): Long = | |
maxAge - age(from) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment