Skip to content

Commit 8c72dd6

Browse files
committed
feat: introduce SimpleRedisCache for Redis-backed caching
Add `SimpleRedisCache` to support Redis-backed caching with JSON serialization, configurable TTLs, and coroutine-based methods. Extend `RedisApi` with helper functions to streamline cache creation using reified or explicit serializers.
1 parent eb74dfa commit 8c72dd6

File tree

2 files changed

+183
-0
lines changed

2 files changed

+183
-0
lines changed

src/main/kotlin/dev/slne/surf/redis/RedisApi.kt

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package dev.slne.surf.redis
22

33
import dev.slne.surf.redis.RedisApi.Companion.create
4+
import dev.slne.surf.redis.cache.SimpleRedisCache
45
import dev.slne.surf.redis.config.InternalConfig
56
import dev.slne.surf.redis.event.RedisEvent
67
import dev.slne.surf.redis.event.RedisEventBus
@@ -399,4 +400,46 @@ class RedisApi private constructor(
399400
syncStructures.add(structure)
400401
return structure
401402
}
403+
404+
/**
405+
* Create a [SimpleRedisCache] for the given `namespace` using an automatically derived
406+
* serializer for value type `V`.
407+
*
408+
* This inline variant uses a reified type parameter `V`, allowing the appropriate
409+
* `KSerializer<V>` to be obtained via `serializer()`.
410+
*
411+
* @param namespace Prefix placed before each Redis key.
412+
* @param ttl Time-to-live for cache entries as a [kotlin.time.Duration].
413+
* @param keyToString Function that converts a key of type `K` to its string representation.
414+
* Defaults to `toString()`.
415+
* @return A new [SimpleRedisCache] instance backed by this [RedisApi].
416+
* @see SimpleRedisCache
417+
*/
418+
inline fun <K : Any, reified V : Any> createSimpleCache(
419+
namespace: String,
420+
ttl: Duration,
421+
noinline keyToString: (K) -> String = { it.toString() }
422+
): SimpleRedisCache<K, V> = createSimpleCache(namespace, serializer(), ttl, keyToString)
423+
424+
/**
425+
* Create a [SimpleRedisCache] for the given `namespace` using the provided [KSerializer].
426+
*
427+
* This variant allows an explicit serializer for the value type `V` to be supplied.
428+
*
429+
* @param namespace Prefix placed before each Redis key.
430+
* @param serializer A [KSerializer] for the value type `V` used for JSON (de-)serialization.
431+
* @param ttl Time-to-live for cache entries as a [kotlin.time.Duration].
432+
* @param keyToString Function that converts a key of type `K` to its string representation.
433+
* Defaults to `toString()`.
434+
* @return A new [SimpleRedisCache] instance backed by this [RedisApi].
435+
* @see SimpleRedisCache
436+
*/
437+
fun <K : Any, V : Any> createSimpleCache(
438+
namespace: String,
439+
serializer: KSerializer<V>,
440+
ttl: Duration,
441+
keyToString: (K) -> String = { it.toString() }
442+
): SimpleRedisCache<K, V> {
443+
return SimpleRedisCache(namespace, serializer, keyToString, ttl, this)
444+
}
402445
}
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
package dev.slne.surf.redis.cache
2+
3+
import dev.slne.surf.redis.RedisApi
4+
import kotlinx.coroutines.reactive.awaitFirstOrNull
5+
import kotlinx.coroutines.reactive.awaitSingle
6+
import kotlinx.serialization.KSerializer
7+
import kotlin.time.Duration
8+
9+
/**
10+
* A simple Redis-backed cache for values of type [V] parameterized by key type [K].
11+
*
12+
* Values are stored as JSON under keys formatted as `<namespace>:<key>`. A configurable
13+
* TTL (time-to-live) is applied to each entry. `null` values can optionally be cached
14+
* using a sentinel marker.
15+
*
16+
* This class uses the reactive Redis commands exposed by [RedisApi] and is intended
17+
* for coroutine-based usage (suspending methods).
18+
*
19+
* @param namespace String prefix that is prepended to each Redis key.
20+
* @param serializer [KSerializer] used to (de-)serialize values of type [V] to/from JSON.
21+
* @param keyToString Function that converts a key of type `K` to its String representation.
22+
* Default is `toString()`.
23+
* @param ttl Time-to-live for cache entries.
24+
* @param api Instance of [RedisApi] used to access Redis.
25+
*/
26+
class SimpleRedisCache<K : Any, V : Any> internal constructor(
27+
private val namespace: String,
28+
private val serializer: KSerializer<V>,
29+
private val keyToString: (K) -> String = { it.toString() },
30+
private val ttl: Duration,
31+
private val api: RedisApi
32+
) {
33+
private companion object {
34+
private const val NULL_MARKER = "__NULL__"
35+
}
36+
37+
private val commands get() = api.connection.reactive()
38+
private fun redisKey(key: K): String = "$namespace:${keyToString(key)}"
39+
40+
/**
41+
* Retrieve a value from the cache.
42+
*
43+
* Returns `null` if no entry exists or if a cached `null` marker is present.
44+
*
45+
* @param key The cache key.
46+
* @return The cached value of type [V], or `null` if absent or explicitly cached as `null`.
47+
*/
48+
suspend fun getCached(key: K): V? {
49+
val raw = commands.get(redisKey(key)).awaitFirstOrNull() ?: return null
50+
51+
if (raw == NULL_MARKER) {
52+
return null
53+
}
54+
55+
return api.json.decodeFromString(serializer, raw)
56+
}
57+
58+
/**
59+
* Store a value in the cache.
60+
*
61+
* The value is serialized to JSON using the provided [KSerializer] and stored with
62+
* the configured TTL.
63+
*
64+
* @param key The cache key.
65+
* @param value The value to store.
66+
*/
67+
suspend fun put(key: K, value: V) {
68+
putRaw(key, api.json.encodeToString(serializer, value))
69+
}
70+
71+
/**
72+
* Internal helper to store already serialized data or sentinel markers.
73+
*
74+
* @param key The cache key.
75+
* @param raw The raw string to store (JSON or sentinel marker).
76+
*/
77+
private suspend fun putRaw(key: K, raw: String) {
78+
val redisKey = redisKey(key)
79+
80+
commands.psetex(
81+
redisKey,
82+
ttl.inWholeMilliseconds,
83+
raw
84+
).awaitSingle()
85+
}
86+
87+
/**
88+
* Return the cached value or load it if absent.
89+
*
90+
* If no entry exists, the provided suspending `loader` is invoked, the result is cached
91+
* and then returned.
92+
*
93+
* @param key The cache key.
94+
* @param loader Suspended lambda to load the value if it is not present in the cache.
95+
* @return The existing or newly loaded value.
96+
*/
97+
suspend fun cachedOrLoad(key: K, loader: suspend () -> V): V {
98+
getCached(key)?.let { return it }
99+
val loaded = loader()
100+
put(key, loaded)
101+
return loaded
102+
}
103+
104+
/**
105+
* Return the cached value or load it if absent (nullable variant).
106+
*
107+
* If the loader returns `null`, the `cacheNull` flag controls whether a sentinel
108+
* marker is stored so subsequent calls do not trigger the loader again.
109+
*
110+
* @param key The cache key.
111+
* @param cacheNull If `true`, `null` results are cached using a sentinel marker.
112+
* @param loader Suspended lambda to load the nullable value if it is not present.
113+
* @return The existing or newly loaded value, or `null`.
114+
*/
115+
suspend fun cachedOrLoadNullable(
116+
key: K,
117+
cacheNull: Boolean = false,
118+
loader: suspend () -> V?
119+
): V? {
120+
getCached(key)?.let { return it }
121+
122+
val loaded = loader()
123+
when {
124+
loaded != null -> put(key, loaded)
125+
cacheNull -> putRaw(key, NULL_MARKER)
126+
}
127+
128+
return loaded
129+
}
130+
131+
/**
132+
* Remove an entry from the cache.
133+
*
134+
* @param key The cache key to remove.
135+
* @return The number of keys removed (typically 0 or 1).
136+
*/
137+
suspend fun invalidate(key: K): Long {
138+
return commands.del(redisKey(key)).awaitSingle()
139+
}
140+
}

0 commit comments

Comments
 (0)