Cache¶
interface Cache<Identifier, Failure, Value>
Stores information temporarily to avoid unneeded network requests.
Cache state¶
Each piece of data in a cache can be in three different states:
-
Up-to-date: the last query was not long ago, we consider its result still valid,
-
Stale: the last query was long enough ago that it deserves to be checked again, but it probably is still valid. In this case, the cache returns the old value and starts a refresh request in the background.
-
Expired: the last query was too long ago for the data to still be valid. In this case, the cache starts a refresh request and will not return a value until the request finishes. All data that was never previously queried is in this state.
Different cache implementations differ on how they transition data from one state to another. Some cache implementations may not have a 'stale' state. As a user of the cache, you may want to force the state of a specific object if you have more knowledge than the cache, in this case you can use update and expire.
Cache chaining¶
Cache implementations can be chained. A possible scenario for some data that rarely changes can be:
-
Cache the data in memory for 5 minutes,
-
Cache the data in hard storage for 1 hour,
-
Query the data for real afterward.
Cache chaining is instantiated in the opposite order, like iterators (the last in the chain is the first checked, and delegates to the previous one if they do not have the value). The first element of the chain, and therefore the one responsible for actually starting the request, is cache or batchingCache. Note that both have a few implementation differences, it is not recommended to use them directly without chaining under another implementation.
Type Parameters¶
-
Identifier: An identifier representing a cached object. Two identifiers refer to the same object if their
equalsmethod returnstrue. -
Failure: The type of possible failures which may happen when requesting a value. If the cached operation cannot fail, or if you're using another error-handling strategy, see
InfallibleCache. -
Value: The type of cached object.
Functions¶
cachedInBrowserStorage¶
@ExperimentalCacheApi
fun <Identifier, Failure, Value : Any> Cache<Identifier, Failure, Value>.cachedInBrowserStorage(
storage: Storage,
keyPrefix: String,
serializeIdentifier: (Identifier) -> String?,
serializeValue: (Value) -> String?,
deserializeValue: (String) -> Value?
): Cache<Identifier, Failure, Value>
In-browser Cache layer.
General behavior
Updates from the previous cache are stored in the storage engine. When get is called, results are returned from the storage engine if available. Otherwise, the request is transmitted to the previous layer.
Elements are deleted when the storage engine when expire is called. To reduce the size of the cache automatically, add a subsequent layer for it (e.g. expireAfter).
Observability
Currently, the implementation is based on polling. If the value changes in the cache, all cache instances notice after a few seconds. This can be used to share values between tabs: if a cache instance in a tab gets a result, other instances in other tabs will notice them and avoid starting new requests.
However, the current implementation only stores values when they are successful, so if two tabs start the same request at the same time, they won't be deduplicated (both requests will go through). Failed values are not stored either.
Example
fun serializeKey(id: Int) = id.toString()
fun serializeValue(value: Double) = value.toString()
fun deserializeValue(value: String) = value.toDouble()
val squareRoot = cache<Int, Double> { sqrt(it) }
.cachedInBrowserStorage(window.localStorage, "sqrt", serializeKey, serializeValue, deserializeValue)
.cachedInMemory(…)
.expireAfter(…)
Parameters
-
storage: The browser storage engine to use.
-
keyPrefix: A short string that is appended before the identifier. The prefix is important to avoid naming conflicts with other values that may be stored in the
storageengine. -
serializeIdentifier: A function that converts an identifier to a
String. If the function returnsnull, this layer assumes the value cannot be stored and no-ops. -
serializeValue: A function that converts a cached value to a
String. If the function returnsnull, this layer assumes the value cannot be stored and no-ops. -
deserializeValue: A function that converts a string generated by
serializeValueinto its original value. If the function returnsnull, this layer assumes the value cannot be stored and no-ops.
cachedInLocalStorage¶
@ExperimentalCacheApi
fun <Identifier, Failure, Value : Any> Cache<Identifier, Failure, Value>.cachedInLocalStorage(
keyPrefix: String,
serializeIdentifier: (Identifier) -> String?,
serializeValue: (Value) -> String?,
deserializeValue: (String) -> Value?
): Cache<Identifier, Failure, Value>
In-browser Cache implementation that is shared between tabs and persists when the browser is closed.
This function is equivalent for cachedInBrowserStorage using the local storage engine. See the cachedInBrowserStorage documentation for more information.
cachedInMemory¶
fun <Identifier, Failure, Value> Cache<Identifier, Failure, Value>.cachedInMemory(job: Job): Cache<Identifier, Failure, Value>
In-memory Cache layer.
General behavior
Updates from the previous cache layer are stored in a dictionary. When get is called, results are returned from the dictionary if available. Otherwise, the request is transmitted to the previous layer.
This implementation frees elements only when expire is called. To free memory automatically, add a subsequent layer responsible for it (e.g. expireAfter).
Observability
If multiple callers request the same value concurrently, a single cache request is started. All subscribers receive all events as if they started the request themselves.
The flow returned by Cache.get is infinite: callers can subscribe to it for as long as they want. If a request is started, for any reason, all subscribers to the flow observe the progress events as well as the final result.
When a new request is started, all existing subscribers see the loading events of the new request with the old results. For example, if the previous request gave A, and the new request has three progress steps followed by the result B, an existing subscriber will see the values:
-
A, done -
A, 25% loading -
A, 50% loading -
A, 75% loading -
B, done
This is useful for GUIs: the application can communicate that a request is ongoing and the value may be outdated, while still having a value to show.
Example
val scope: CoroutineScope = …
val powersOfTwo = cache<Int, Int> { it * 2 }
.cachedInMemory(scope.coroutineContext.job)
.expireAfter(10.minutes, scope)
Parameters
- job: The
Jobinstance in which requests transmitted to the previous layers are started in. Cancelling this job makes the cache unable to query any new values, and cancels any ongoing request.
cachedInSessionStorage¶
@ExperimentalCacheApi
fun <Identifier, Failure, Value : Any> Cache<Identifier, Failure, Value>.cachedInSessionStorage(
keyPrefix: String,
serializeIdentifier: (Identifier) -> String?,
serializeValue: (Value) -> String?,
deserializeValue: (String) -> Value?
): Cache<Identifier, Failure, Value>
In-browser Cache implementation that is cleared when the tab is closed.
This function is equivalent for cachedInBrowserStorage using the session storage engine. See the cachedInBrowserStorage documentation for more information.
expire¶
open suspend fun expire(id: Identifier)
Tells the cache that the value it stores for id is out of date, and should be queried again the next time it is requested.
All layers are updated.
abstract suspend fun expire(ids: Collection<Identifier>)
Tells the cache that the value it stores for the given ids are out of date, and should be queried again the next time they are requested.
All layers are updated.
expireAfter¶
fun <Identifier, Failure, Value> Cache<Identifier, Failure, Value>.expireAfter(
duration: Duration,
scope: CoroutineScope,
clock: Clock
): Cache<Identifier, Failure, Value>
Age-based Cache expiration strategy.
General behavior
The cache starts a worker in scope. Every duration, all values which have not been updated for at least duration are expired in the previous layer.
This layer considers any non-loading value returned by get to be new.
If scope is cancelled, requests made to this cache continue to work as normal, but no values are ever expired anymore.
Example
expireAll¶
abstract suspend fun expireAll()
Tells the cache that all values are out of date, and should be queried again the next time they are requested.
All layers are updated.
get¶
abstract operator fun get(id: Identifier): ProgressiveFlow<Failure, Value>
update¶
open suspend fun update(id: Identifier, value: Value)
abstract suspend fun update(values: Collection<Pair<Identifier, Value>>)
Forces the cache to accept the given values as more recent for their associated identifier than whatever was previously stored.
If multiple values are provided for the same identifier, a cache implementation may either:
-
take only the last occurrence into account,
-
take each occurrence into account as subsequent updates, in the same order as they appear in the iterable.
If values is empty, this function does nothing.
All layers are updated.