Skip to content

Cache

In many applications, we may encounter scenarios where overlapping work is performed. For example, if we are developing a service that handles incoming requests, it is essential to avoid processing duplicate requests. By using the Cache module, we can enhance our application’s performance by preventing redundant work.

Key Features of Cache:

  • Compositionality: Cache allows different parts of our application to perform overlapping work while still benefiting from compositional programming principles.

  • Unification of Synchronous and Asynchronous Caches: The compositional definition of a cache through a lookup function unifies both synchronous and asynchronous caches, allowing the lookup function to compute values either synchronously or asynchronously.

  • Deep Effect Integration: Cache is designed to work natively with the Effect library, supporting concurrent lookups, failure handling, and interruption without losing the power of Effect.

  • Cache/Entry Statistics: Cache tracks metrics such as entries, hits, misses, helping us to assess and optimize cache performance.

A cache is defined by a lookup function that describes how to compute the value associated with a key if it is not already in the cache.

type Lookup<Key, Value, Error = never, Environment = never> = (
key: Key
) => Effect.Effect<Value, Error, Environment>

The lookup function takes a key of type Key and returns an Effect that requires an environment of type Environment and can fail with an error of type Error or succeed with a value of type Value. Since the lookup function returns an Effect, it can describe both synchronous and asynchronous workflows.

In short, if you can describe it with an Effect, you can use it as the lookup function for a cache.

We construct a cache using a lookup function along with a maximum size and a time to live.

declare const make: <
Key,
Value,
Error = never,
Environment = never
>(options: {
readonly capacity: number
readonly timeToLive: Duration.DurationInput
readonly lookup: Lookup<Key, Value, Error, Environment>
}) => Effect.Effect<Cache<Key, Value, Error>, never, Environment>

Once a cache is created, the most idiomatic way to work with it is the get operator. The get operator returns the current value in the cache if it exists, or computes a new value, puts it in the cache, and returns it.

If multiple concurrent processes request the same value, it will only be computed once. All other processes will receive the computed value as soon as it is available. This is managed using Effect’s fiber-based concurrency model without blocking the underlying thread.

In this example, we call timeConsumingEffect three times in parallel with the same key. The Cache runs this effect only once, so concurrent lookups will wait until the value is available:

1
import {
import Effect
Effect
,
import Cache
Cache
,
import Duration
Duration
} from "effect"
2
3
const
const timeConsumingEffect: (key: string) => Effect.Effect<number, never, never>
timeConsumingEffect
= (
(parameter) key: string
key
: string) =>
4
import Effect
Effect
.
const sleep: (duration: Duration.DurationInput) => Effect.Effect<void>

Returns an effect that suspends for the specified duration. This method is asynchronous, and does not actually block the fiber executing the effect.

sleep
("2 seconds").
(method) Pipeable.pipe<Effect.Effect<void, never, never>, Effect.Effect<number, never, never>>(this: Effect.Effect<...>, ab: (_: Effect.Effect<void, never, never>) => Effect.Effect<number, never, never>): Effect.Effect<...> (+21 overloads)
pipe
(
import Effect
Effect
.
const as: <number>(value: number) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<number, E, R> (+1 overload)

This function maps the success value of an `Effect` value to a specified constant value.

as
(
(parameter) key: string
key
.
(property) String.length: number

Returns the length of a String object.

length
))
5
6
const
const program: Effect.Effect<void, never, never>
program
=
import Effect
Effect
.
const gen: <YieldWrap<Effect.Effect<Cache.Cache<string, number, never>, never, never>> | YieldWrap<Effect.Effect<[[number, number], number], never, never>> | YieldWrap<...>, void>(f: (resume: Effect.Adapter) => Generator<...>) => Effect.Effect<...> (+1 overload)
gen
(function* () {
7
const
const cache: Cache.Cache<string, number, never>
cache
= yield*
import Cache
Cache
.
const make: <string, number, never, never>(options: { readonly capacity: number; readonly timeToLive: Duration.DurationInput; readonly lookup: Cache.Lookup<string, number, never, never>; }) => Effect.Effect<...>

Constructs a new cache with the specified capacity, time to live, and lookup function.

make
({
8
(property) capacity: number
capacity
: 100,
9
(property) timeToLive: Duration.DurationInput
timeToLive
:
import Duration
Duration
.
const infinity: Duration.Duration
infinity
,
10
(property) lookup: Cache.Lookup<string, number, never, never>
lookup
:
const timeConsumingEffect: (key: string) => Effect.Effect<number, never, never>
timeConsumingEffect
11
})
12
const
const result: [[number, number], number]
result
= yield*
const cache: Cache.Cache<string, number, never>
cache
13
.
(method) Cache<string, number, never>.get(key: string): Effect.Effect<number, never, never>

Retrieves the value associated with the specified key if it exists. Otherwise computes the value with the lookup function, puts it in the cache, and returns it.

get
("key1")
14
.
(method) Pipeable.pipe<Effect.Effect<number, never, never>, Effect.Effect<[number, number], never, never>, Effect.Effect<[[number, number], number], never, never>>(this: Effect.Effect<...>, ab: (_: Effect.Effect<...>) => Effect.Effect<...>, bc: (_: Effect.Effect<...>) => Effect.Effect<...>): Effect.Effect<...> (+21 overloads)
pipe
(
15
import Effect
Effect
.
const zip: <number, never, never>(that: Effect.Effect<number, never, never>, options?: { readonly concurrent?: boolean | undefined; readonly batching?: boolean | "inherit" | undefined; readonly concurrentFinalizers?: boolean | undefined; } | undefined) => <A, E, R>(self: Effect.Effect<...>) => Effect.Effect<...> (+1 overload)

The `Effect.zip` function allows you to combine two effects into a single effect. This combined effect yields a tuple containing the results of both input effects once they succeed. Note that `Effect.zip` processes effects sequentially: it first completes the effect on the left and then the effect on the right. If you want to run the effects concurrently, you can use the `concurrent` option.

zip
(
const cache: Cache.Cache<string, number, never>
cache
.
(method) Cache<string, number, never>.get(key: string): Effect.Effect<number, never, never>

Retrieves the value associated with the specified key if it exists. Otherwise computes the value with the lookup function, puts it in the cache, and returns it.

get
("key1"), {
(property) concurrent?: boolean | undefined
concurrent
: true }),
16
import Effect
Effect
.
const zip: <number, never, never>(that: Effect.Effect<number, never, never>, options?: { readonly concurrent?: boolean | undefined; readonly batching?: boolean | "inherit" | undefined; readonly concurrentFinalizers?: boolean | undefined; } | undefined) => <A, E, R>(self: Effect.Effect<...>) => Effect.Effect<...> (+1 overload)

The `Effect.zip` function allows you to combine two effects into a single effect. This combined effect yields a tuple containing the results of both input effects once they succeed. Note that `Effect.zip` processes effects sequentially: it first completes the effect on the left and then the effect on the right. If you want to run the effects concurrently, you can use the `concurrent` option.

zip
(
const cache: Cache.Cache<string, number, never>
cache
.
(method) Cache<string, number, never>.get(key: string): Effect.Effect<number, never, never>

Retrieves the value associated with the specified key if it exists. Otherwise computes the value with the lookup function, puts it in the cache, and returns it.

get
("key1"), {
(property) concurrent?: boolean | undefined
concurrent
: true })
17
)
18
namespace console var console: Console

The `console` module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers. The module exports two specific components: * A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream. * A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v22.x/api/process.html#processstdout) and [`process.stderr`](https://nodejs.org/docs/latest-v22.x/api/process.html#processstderr). The global `console` can be used without importing the `node:console` module. _**Warning**_: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v22.x/api/process.html#a-note-on-process-io) for more information. Example using the global `console`: ```js console.log('hello world'); // Prints: hello world, to stdout console.log('hello %s', 'world'); // Prints: hello world, to stdout console.error(new Error('Whoops, something bad happened')); // Prints error message and stack trace to stderr: // Error: Whoops, something bad happened // at [eval]:5:15 // at Script.runInThisContext (node:vm:132:18) // at Object.runInThisContext (node:vm:309:38) // at node:internal/process/execution:77:19 // at [eval]-wrapper:6:22 // at evalScript (node:internal/process/execution:76:60) // at node:internal/main/eval_string:23:3 const name = 'Will Robinson'; console.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to stderr ``` Example using the `Console` class: ```js const out = getStreamSomehow(); const err = getStreamSomehow(); const myConsole = new console.Console(out, err); myConsole.log('hello world'); // Prints: hello world, to out myConsole.log('hello %s', 'world'); // Prints: hello world, to out myConsole.error(new Error('Whoops, something bad happened')); // Prints: [Error: Whoops, something bad happened], to err const name = 'Will Robinson'; myConsole.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to err ```

console
.
(method) Console.log(message?: any, ...optionalParams: any[]): void

Prints to `stdout` with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to [`printf(3)`](http://man7.org/linux/man-pages/man3/printf.3.html) (the arguments are all passed to [`util.format()`](https://nodejs.org/docs/latest-v22.x/api/util.html#utilformatformat-args)). ```js const count = 5; console.log('count: %d', count); // Prints: count: 5, to stdout console.log('count:', count); // Prints: count: 5, to stdout ``` See [`util.format()`](https://nodejs.org/docs/latest-v22.x/api/util.html#utilformatformat-args) for more information.

log
(
19
"Result of parallel execution of three effects" +
20
`with the same key: ${
const result: [[number, number], number]
result
}`
21
)
22
23
const
const hits: number
hits
= yield*
const cache: Cache.Cache<string, number, never>
cache
.
(property) ConsumerCache<string, number, never>.cacheStats: Effect.Effect<Cache.CacheStats, never, never>

Returns statistics for this cache.

cacheStats
.
(method) Pipeable.pipe<Effect.Effect<Cache.CacheStats, never, never>, Effect.Effect<number, never, never>>(this: Effect.Effect<...>, ab: (_: Effect.Effect<Cache.CacheStats, never, never>) => Effect.Effect<...>): Effect.Effect<...> (+21 overloads)
pipe
(
24
import Effect
Effect
.
const map: <Cache.CacheStats, number>(f: (a: Cache.CacheStats) => number) => <E, R>(self: Effect.Effect<Cache.CacheStats, E, R>) => Effect.Effect<number, E, R> (+1 overload)
map
((
(parameter) stats: Cache.CacheStats
stats
) =>
(parameter) stats: Cache.CacheStats
stats
.
(property) CacheStats.hits: number
hits
)
25
)
26
const
const misses: number
misses
= yield*
const cache: Cache.Cache<string, number, never>
cache
.
(property) ConsumerCache<string, number, never>.cacheStats: Effect.Effect<Cache.CacheStats, never, never>

Returns statistics for this cache.

cacheStats
.
(method) Pipeable.pipe<Effect.Effect<Cache.CacheStats, never, never>, Effect.Effect<number, never, never>>(this: Effect.Effect<...>, ab: (_: Effect.Effect<Cache.CacheStats, never, never>) => Effect.Effect<...>): Effect.Effect<...> (+21 overloads)
pipe
(
27
import Effect
Effect
.
const map: <Cache.CacheStats, number>(f: (a: Cache.CacheStats) => number) => <E, R>(self: Effect.Effect<Cache.CacheStats, E, R>) => Effect.Effect<number, E, R> (+1 overload)
map
((
(parameter) stats: Cache.CacheStats
stats
) =>
(parameter) stats: Cache.CacheStats
stats
.
(property) CacheStats.misses: number
misses
)
28
)
29
namespace console var console: Console

The `console` module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers. The module exports two specific components: * A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream. * A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v22.x/api/process.html#processstdout) and [`process.stderr`](https://nodejs.org/docs/latest-v22.x/api/process.html#processstderr). The global `console` can be used without importing the `node:console` module. _**Warning**_: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v22.x/api/process.html#a-note-on-process-io) for more information. Example using the global `console`: ```js console.log('hello world'); // Prints: hello world, to stdout console.log('hello %s', 'world'); // Prints: hello world, to stdout console.error(new Error('Whoops, something bad happened')); // Prints error message and stack trace to stderr: // Error: Whoops, something bad happened // at [eval]:5:15 // at Script.runInThisContext (node:vm:132:18) // at Object.runInThisContext (node:vm:309:38) // at node:internal/process/execution:77:19 // at [eval]-wrapper:6:22 // at evalScript (node:internal/process/execution:76:60) // at node:internal/main/eval_string:23:3 const name = 'Will Robinson'; console.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to stderr ``` Example using the `Console` class: ```js const out = getStreamSomehow(); const err = getStreamSomehow(); const myConsole = new console.Console(out, err); myConsole.log('hello world'); // Prints: hello world, to out myConsole.log('hello %s', 'world'); // Prints: hello world, to out myConsole.error(new Error('Whoops, something bad happened')); // Prints: [Error: Whoops, something bad happened], to err const name = 'Will Robinson'; myConsole.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to err ```

console
.
(method) Console.log(message?: any, ...optionalParams: any[]): void

Prints to `stdout` with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to [`printf(3)`](http://man7.org/linux/man-pages/man3/printf.3.html) (the arguments are all passed to [`util.format()`](https://nodejs.org/docs/latest-v22.x/api/util.html#utilformatformat-args)). ```js const count = 5; console.log('count: %d', count); // Prints: count: 5, to stdout console.log('count:', count); // Prints: count: 5, to stdout ``` See [`util.format()`](https://nodejs.org/docs/latest-v22.x/api/util.html#utilformatformat-args) for more information.

log
(`Number of cache hits: ${
const hits: number
hits
}`)
30
namespace console var console: Console

The `console` module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers. The module exports two specific components: * A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream. * A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v22.x/api/process.html#processstdout) and [`process.stderr`](https://nodejs.org/docs/latest-v22.x/api/process.html#processstderr). The global `console` can be used without importing the `node:console` module. _**Warning**_: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v22.x/api/process.html#a-note-on-process-io) for more information. Example using the global `console`: ```js console.log('hello world'); // Prints: hello world, to stdout console.log('hello %s', 'world'); // Prints: hello world, to stdout console.error(new Error('Whoops, something bad happened')); // Prints error message and stack trace to stderr: // Error: Whoops, something bad happened // at [eval]:5:15 // at Script.runInThisContext (node:vm:132:18) // at Object.runInThisContext (node:vm:309:38) // at node:internal/process/execution:77:19 // at [eval]-wrapper:6:22 // at evalScript (node:internal/process/execution:76:60) // at node:internal/main/eval_string:23:3 const name = 'Will Robinson'; console.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to stderr ``` Example using the `Console` class: ```js const out = getStreamSomehow(); const err = getStreamSomehow(); const myConsole = new console.Console(out, err); myConsole.log('hello world'); // Prints: hello world, to out myConsole.log('hello %s', 'world'); // Prints: hello world, to out myConsole.error(new Error('Whoops, something bad happened')); // Prints: [Error: Whoops, something bad happened], to err const name = 'Will Robinson'; myConsole.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to err ```

console
.
(method) Console.log(message?: any, ...optionalParams: any[]): void

Prints to `stdout` with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to [`printf(3)`](http://man7.org/linux/man-pages/man3/printf.3.html) (the arguments are all passed to [`util.format()`](https://nodejs.org/docs/latest-v22.x/api/util.html#utilformatformat-args)). ```js const count = 5; console.log('count: %d', count); // Prints: count: 5, to stdout console.log('count:', count); // Prints: count: 5, to stdout ``` See [`util.format()`](https://nodejs.org/docs/latest-v22.x/api/util.html#utilformatformat-args) for more information.

log
(`Number of cache misses: ${
const misses: number
misses
}`)
31
})
32
33
import Effect
Effect
.
const runPromise: <void, never>(effect: Effect.Effect<void, never, never>, options?: { readonly signal?: AbortSignal; } | undefined) => Promise<void>

Runs an `Effect` workflow, returning a `Promise` which resolves with the result of the workflow or rejects with an error.

runPromise
(
const program: Effect.Effect<void, never, never>
program
)
34
/*
35
Output:
36
Result of parallel execution of three effects with the same key: 4,4,4
37
Number of cache hits: 2
38
Number of cache misses: 1
39
*/

The cache is designed to be safe for concurrent access and efficient under concurrent conditions. If two concurrent processes request the same value and it is not in the cache, the value will be computed once and provided to both processes as soon as it is available. Concurrent processes will wait for the value without blocking operating system threads.

If the lookup function fails or is interrupted, the error will be propagated to all concurrent processes waiting for the value. Failures are cached to prevent repeated computation of the same failed value. If interrupted, the key will be removed from the cache, so subsequent calls will attempt to compute the value again.

A cache is created with a specified capacity. When the cache reaches capacity, the least recently accessed values will be removed first. The cache size may slightly exceed the specified capacity between operations.

A cache can also have a specified time to live (TTL). Values older than the TTL will not be returned. The age is calculated from when the value was loaded into the cache.

In addition to get, Cache provides several other operators:

  • refresh: Similar to get, but triggers a re-computation of the value without invalidating it, allowing requests to the associated key to be served while the value is being re-computed.
  • size: Returns the current size of the cache. Under concurrent access, the size is approximate.
  • contains: Checks if a value associated with a specified key exists in the cache. Under concurrent access, the result is valid as of the check time but may change immediately after.
  • invalidate: Evicts the value associated with a specified key.
  • invalidateAll: Evicts all values in the cache.