dave08
02/21/2024, 11:52 AMCLOVIS
02/22/2024, 9:02 AMdave08
02/22/2024, 10:36 AMsuspend fun getUser(userId: Int) = cache("user-cache", userId) {
// some db query to retrieve user... this will be cached the first
// time for any unique userId and will be retrieved from the cache
// if without re-running the query if it's still there.
}
but when invalidating you need to re-use it:
suspend fun someOperationThatNeedsToRefreshTheUserInTheCacheFirst(userId: Int) = cache.invalidate("user-cache", userId) {
...
}
I could rely on the user to define a constant for user-cache
, or force them to use an Enum instead, or maybe instead of initializing the caches when constructing the Kacheable instance:
val cache = Kacheable(RedisKacheableStore(conn),mapOf("user-cache", CacheConfig(...)))
maybe use the CacheConfig
instance as the key itself... or maybe just make it generic, and let the user define (while I call toString() for the name...). There's pros and cons for each one of these possibilities...dave08
02/22/2024, 10:40 AMdave08
02/22/2024, 10:43 AMCLOVIS
02/22/2024, 10:50 AMdave08
02/22/2024, 10:51 AMinterface Kacheable {
suspend fun <R> invalidate(vararg keys: Pair<String, List<Any>>, block: suspend () -> R): R
suspend fun <R> invoke(
name: String,
type: KSerializer<R>,
vararg params: Any,
saveResultIf: (R) -> Boolean = { true },
block: suspend () -> R
): R
}
suspend inline operator fun <reified R> Kacheable.invoke(
name: String,
vararg params: Any,
noinline saveResultIf: (R) -> Boolean = { true },
noinline block: suspend () -> R
): R =
invoke(name, serializer<R>(), *params, saveResultIf = saveResultIf, block = block)
suspend inline fun <reified R> Kacheable.cache(
name: String,
vararg params: Any,
noinline shouldSaveResult: (R) -> Boolean = { true },
noinline block: suspend () -> R
): R =
invoke(name, serializer(), *params, saveResultIf = shouldSaveResult, block = block)
CLOVIS
02/22/2024, 10:51 AMdave08
02/22/2024, 10:52 AMdave08
02/22/2024, 10:55 AMCLOVIS
02/22/2024, 10:56 AMdave08
02/22/2024, 10:56 AMdave08
02/22/2024, 10:58 AMdave08
02/22/2024, 10:58 AMdata class CacheConfig(
val name: String,
val expiryType: ExpiryType = ExpiryType.none,
val expiry: Duration = Duration.INFINITE,
/**
If this is a real null, the cache entry will not be saved at all.
This should ONLY be set if the function's return type is nullable!
*/
val nullPlaceholder: String? = null,
)
CLOVIS
02/22/2024, 10:59 AM@JvmInline value class CacheKey(val name: String)
this way users would write
val userCache = CacheKey("user-cache")
suspend fun getUser(id: String) = cache(userCache, id) { … }
cache.invalidate(userCache, "123")
dave08
02/22/2024, 11:01 AMCLOVIS
02/22/2024, 11:01 AMval mainCache = // your existing cache
val userCache = mainCache.withKey("user-cache")
suspend fun getUser(id: String) = userCache(id) { … }
userCache.invalide("123")
I think it's easier to understand, and it looks as if the cache instances are nicely segregated by operation (even if they're all stored together for real).dave08
02/22/2024, 11:24 AM// I mean (but then I couldn't use that for invalidate...):
fun <P1 : Any, R> Kacheable.withKeyAndP1(name: String): (P1) -> R ...
dave08
02/22/2024, 11:33 AMfun <P1 : Any, R> Kacheable.withCacheAndInvalidate1(name: String): Pair<(P1) -> R, (P1) -> R) ...
val (userCache, userCacheInvalidate) = mainCache.withCacheAndInvalidate1<Int>("user-cache")
but would that be something that a user could understand on the first look? Is that a good practice (there is such a thing in React, and some Android Compose libraries)...CLOVIS
02/22/2024, 11:41 AMdave08
02/22/2024, 11:42 AMsuspend inline operator fun <reified R> Kacheable.invoke(
name: String,
vararg params: Any, // <--- I'd be nice if this could be type safe per cache name...
noinline saveResultIf: (R) -> Boolean = { true },
noinline block: suspend () -> R
): R
dave08
02/22/2024, 11:44 AMsuspend fun <R> invalidate(vararg keys: Pair<String, List<Any>>, block: suspend () -> R): R
there's those keys (in this implementation a user could invalidate entries in multiple caches -- Pair<CacheName>, List<Any>> where List<Any> are the paramsdave08
02/22/2024, 11:44 AMCLOVIS
02/22/2024, 11:45 AMclass KeyedKachable<R>(
val cache: Kacheable,
val key: String,
) {
operator fun invoke(…) = cache(key, …)
fun invalidate(…) = cache.invalidate(key, …)
}
fun <R> Kacheable.withKey(key: String) = KeyedKacheable<R>(ths, key)
so the result R
type parameter is ok.
Indeed, if you want to have typesafe varargs, you will need to copy-paste KeyedKacheable
for each number of argument you accept, but the result will be typesafe.
At the very least, KeyedCacheable
is very small, since all it does is store the key and delegate to Kacheable
, so it's not a big problem if it's duplicated.CLOVIS
02/22/2024, 11:48 AMList
or Collection
whenever you have a vararg
: https://kotlinlang.org/docs/jvm-api-guidelines-predictability.html#avoid-varargsdave08
02/22/2024, 11:53 AMwithKey
would still be a good name here then? I'd have to find a way to name these things clearly enough for the type params too.dave08
02/22/2024, 12:41 PMsuspend inline operator fun <reified R> Kacheable.invoke(
name: String,
vararg params: Any,
noinline saveResultIf: (R) -> Boolean = { true },
noinline block: suspend () -> R
): R =
invoke(name, serializer<R>(), *params, saveResultIf = saveResultIf, block = block)
suspend inline fun <reified R> Kacheable.cache(
name: String,
vararg params: Any,
noinline shouldSaveResult: (R) -> Boolean = { true },
noinline block: suspend () -> R
): R =
invoke(name, serializer(), *params, saveResultIf = shouldSaveResult, block = block)
the receiver is on the original Kacheable interface... so I guess I'd have to add a KSerializer parameter to withKey... or maybe a type parameter for it. But all in all this seems to solve the problem very nicely, thanks a lot!