Skip to content

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 equals method returns true.

  • 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 storage engine.

  • serializeIdentifier: A function that converts an identifier to a String. If the function returns null, 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 returns null, this layer assumes the value cannot be stored and no-ops.

  • deserializeValue: A function that converts a string generated by serializeValue into its original value. If the function returns null, 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

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 Job instance 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

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

val scope: CoroutineScope = 

val powersOfTwo = cache<Int, Int> { it * 2 }
    .cachedInMemory(scope.coroutineContext.job)
    .expireAfter(10.minutes, scope, clock)

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>

Gets the value associated with an id in this cache.

This function returns a Flow instance synchronously: it is safe to call in synchronous-only areas of the program, such as inside the body of a UI component. You can then subscribe to the Flow to access the actual values.

update

open suspend fun update(id: Identifier, value: Value)

Forces the cache to accept value as a more recent value for the given id than whatever it was previously storing.

All layers are updated.

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.

open suspend fun update(vararg values: Pair<Identifier, Value>)

Forces the cache to accept the given values as more recent for their associated identifier than whatever was previously stored.

This overload is provided for convenience, see update.