Skip to content
Effect 4.0 is here. One ecosystem. Zero dependencies.

Cache

Caches values loaded by an Effect lookup function.

A cache stores successful and failed lookup results, shares an in-progress lookup when multiple callers request the same missing key, and limits entries by capacity and optional time-to-live rules. This module includes helpers for reading, setting, refreshing, invalidating, and inspecting cache contents.

17 exports Since v2.0.0 Source

Combinators

Since v4.0.0

Retrieves all key-value pairs from the cache as an iterable. This function only returns entries with successfully resolved values, filtering out any failed lookups or expired entries.

Gotchas

Expired entries are removed from the cache while entries filters them out.

See

  • keys for retrieving only cached keys
  • values for retrieving only cached values

Signature

declare function entries<Key, A, E, R>(self: Cache<Key, A, E, R>): Effect<Iterable<[Key, A], any, any>>
Since v4.0.0

Retrieves the value for a key, invoking the lookup function on a cache miss or expired entry.

Details

Concurrent get calls for the same missing key share the same pending lookup. The cache stores the lookup Exit, so failed lookups are cached and will fail again until the entry expires, is invalidated, or is refreshed.

Signature

declare const get: {
<Key, A>(key: Key): <E, R>(self: Cache<Key, A, E, R>) => Effect<A, E, R>;
<Key, A, E, R>(self: Cache<Key, A, E, R>, key: Key): Effect<A, E, R>;
}

Example Getting cached values

import { Cache, Effect } from "effect"
const program = Effect.gen(function*() {
const cache = yield* Cache.make({
capacity: 10,
lookup: (key: string) => Effect.succeed(key.length)
})
// Cache miss - triggers lookup function
const result1 = yield* Cache.get(cache, "hello")
// Cache hit - returns cached value without lookup
const result2 = yield* Cache.get(cache, "hello")
return { result1, result2 }
})
const actual = await Effect.runPromise(program)
actual // => { result1: 5, result2: 5 }

Example Handling lookup failures

import { Cache, Effect, Exit } from "effect"
// Error handling when lookup fails
const program = Effect.gen(function*() {
const cache = yield* Cache.make<string, number, string>({
capacity: 10,
lookup: (key: string) =>
key === "error"
? Effect.fail("Lookup failed")
: Effect.succeed(key.length)
})
// Successful lookup
const success = yield* Cache.get(cache, "hello")
// Failed lookup - returns error
const failure = yield* Effect.exit(Cache.get(cache, "error"))
return [success, failure] as const
})
const actual = await Effect.runPromise(program)
actual // => [5, Exit.fail("Lookup failed")]

Example Sharing concurrent lookups

import { Cache, Effect } from "effect"
// Concurrent access - multiple gets of same key only invoke lookup once
const program = Effect.gen(function*() {
let lookupCount = 0
const cache = yield* Cache.make({
capacity: 10,
lookup: (key: string) =>
Effect.sync(() => {
lookupCount++
return key.length
})
})
// Multiple concurrent gets
const results = yield* Effect.all([
Cache.get(cache, "hello"),
Cache.get(cache, "hello"),
Cache.get(cache, "hello")
], { concurrency: "unbounded" })
return { results, lookupCount }
})
const actual = await Effect.runPromise(program)
actual // => { results: [5, 5, 5], lookupCount: 1 }

Reads an existing cache entry without invoking the lookup function.

Details

Returns Option.none() when the key is missing or expired, and Option.some when a cached lookup has succeeded. If the entry is still pending, waits for it to complete. If the cached or pending lookup fails, this effect fails with the same error.

Signature

declare const getOption: {
<Key, A>(key: Key): <E, R>(self: Cache<Key, A, E, R>) => Effect<Option<A>, E>;
<Key, A, E, R>(self: Cache<Key, A, E, R>, key: Key): Effect<Option<A>, E>;
}

Example Reading cached values without lookup

import { Cache, Effect, Option } from "effect"
const program = Effect.gen(function*() {
const cache = yield* Cache.make({
capacity: 10,
lookup: (key: string) => Effect.succeed(key.length)
})
// No value in cache yet - returns None without lookup
const empty = yield* Cache.getOption(cache, "hello")
// Populate cache using get
yield* Cache.get(cache, "hello")
// Now getOption returns the cached value
const cached = yield* Cache.getOption(cache, "hello")
return [empty, cached] as const
})
const actual = await Effect.runPromise(program)
actual // => [Option.none(), Option.some(5)]

Example Skipping expired entries

import { Cache, Effect, Option } from "effect"
import { TestClock } from "effect/testing"
// Expired entries return None
const program = Effect.gen(function*() {
const cache = yield* Cache.make({
capacity: 10,
lookup: (key: string) => Effect.succeed(key.length),
timeToLive: "1 hour"
})
// Add value to cache
yield* Cache.get(cache, "hello")
// Value exists before expiration
const beforeExpiry = yield* Cache.getOption(cache, "hello")
// Simulate time passing
yield* TestClock.adjust("2 hours")
// Value expired - returns None
const afterExpiry = yield* Cache.getOption(cache, "hello")
return [beforeExpiry, afterExpiry] as const
})
const actual = await Effect.runPromise(Effect.provide(program, TestClock.layer()))
actual // => [Option.some(5), Option.none()]

Example Waiting for pending lookups

import { Cache, Deferred, Effect, Fiber, Option } from "effect"
// Waits for ongoing computation to complete
const program = Effect.gen(function*() {
const deferred = yield* Deferred.make<void>()
const cache = yield* Cache.make({
capacity: 10,
lookup: (_key: string) => Deferred.await(deferred).pipe(Effect.as(42))
})
// Start lookup in background
const getFiber = yield* Effect.forkChild(Cache.get(cache, "key"))
// getOption waits for ongoing computation
const optionFiber = yield* Effect.forkChild(Cache.getOption(cache, "key"))
// Complete the computation
yield* Deferred.succeed(deferred, void 0)
const result = yield* Fiber.join(optionFiber)
const value = yield* Fiber.join(getFiber)
return [result, value] as const
})
const actual = await Effect.runPromise(program)
actual // => [Option.some(42), 42]

Retrieves the value associated with the specified key from the cache, only if it contains a resolved successful value.

Details

This checks only an existing non-expired entry. It returns Option.some when the entry has already resolved successfully, and Option.none for missing, expired, failed, or still-pending entries.

See

  • get for triggering or awaiting the cache lookup
  • getOption for reading an existing entry as an optional effect

Signature

declare const getSuccess: {
<Key, A, R>(key: Key): <E>(self: Cache<Key, A, E, R>) => Effect<Option<A>>;
<Key, A, E, R>(self: Cache<Key, A, E, R>, key: Key): Effect<Option<A>>;
}
Since v4.0.0

Checks whether the cache contains an entry for the specified key.

Details

This checks for an existing non-expired entry without invoking the cache lookup function. Expired entries are treated as absent.

Signature

declare const has: {
<Key, A>(key: Key): <E, R>(self: Cache<Key, A, E, R>) => Effect<boolean>;
<Key, A, E, R>(self: Cache<Key, A, E, R>, key: Key): Effect<boolean>;
}

Example Checking for cached keys

import { Cache, Effect } from "effect"
const program = Effect.gen(function*() {
const cache = yield* Cache.make({
capacity: 100,
lookup: (key: string) => Effect.succeed(key.length)
})
// Check non-existent key
const missing = yield* Cache.has(cache, "missing")
// Add entry and check existence
yield* Cache.get(cache, "hello")
const present = yield* Cache.has(cache, "hello")
return [missing, present]
})
const actual = await Effect.runPromise(program)
actual // => [false, true]

Example Checking TTL expiration

import { Cache, Effect } from "effect"
import { TestClock } from "effect/testing"
// TTL expiration behavior
const program = Effect.gen(function*() {
const cache = yield* Cache.make({
capacity: 100,
lookup: (key: string) => Effect.succeed(key.length),
timeToLive: "1 hour"
})
// Add entry with TTL
yield* Cache.get(cache, "expires")
const initial = yield* Cache.has(cache, "expires")
// Still valid before expiration
yield* TestClock.adjust("30 minutes")
const beforeExpiry = yield* Cache.has(cache, "expires")
// Expired after TTL
yield* TestClock.adjust("31 minutes")
const afterExpiry = yield* Cache.has(cache, "expires")
return [initial, beforeExpiry, afterExpiry]
})
const actual = await Effect.runPromise(Effect.provide(program, TestClock.layer()))
actual // => [true, true, false]

Example Checking multiple keys

import { Cache, Effect } from "effect"
// Checking multiple keys efficiently
const program = Effect.gen(function*() {
const cache = yield* Cache.make({
capacity: 100,
lookup: (key: string) => Effect.succeed(key.length)
})
// Populate some entries
yield* Cache.set(cache, "apple", 5)
yield* Cache.set(cache, "banana", 6)
// Check multiple keys
const keys = ["apple", "banana", "cherry", "date"]
const results: Array<string> = []
for (const key of keys) {
const exists = yield* Cache.has(cache, key)
results.push(`${key}: ${exists}`)
}
return results
})
const actual = await Effect.runPromise(program)
actual // => ["apple: true", "banana: true", "cherry: false", "date: false"]

Invalidates the entry associated with the specified key in the cache.

Signature

declare const invalidate: {
<Key, A>(key: Key): <E, R>(self: Cache<Key, A, E, R>) => Effect<void>;
<Key, A, E, R>(self: Cache<Key, A, E, R>, key: Key): Effect<void>;
}

Example Invalidating cached entries

import { Cache, Effect } from "effect"
const program = Effect.gen(function*() {
const cache = yield* Cache.make({
capacity: 10,
lookup: (key: string) => Effect.succeed(key.length)
})
// Add a value to the cache
yield* Cache.get(cache, "hello")
const beforeInvalidation = yield* Cache.has(cache, "hello")
// Invalidate the entry
yield* Cache.invalidate(cache, "hello")
const afterInvalidation = yield* Cache.has(cache, "hello")
// Invalidating non-existent keys doesn't error
yield* Cache.invalidate(cache, "nonexistent")
// Get after invalidation will invoke lookup again
let lookupCount = 0
const cache2 = yield* Cache.make({
capacity: 10,
lookup: (key: string) =>
Effect.sync(() => {
lookupCount++
return key.length
})
})
yield* Cache.get(cache2, "test") // lookupCount = 1
yield* Cache.invalidate(cache2, "test")
yield* Cache.get(cache2, "test") // lookupCount = 2 (lookup called again)
return { beforeInvalidation, afterInvalidation, lookupCount }
})
const actual = await Effect.runPromise(program)
actual // => { beforeInvalidation: true, afterInvalidation: false, lookupCount: 2 }

Invalidates all entries in the cache.

Signature

declare function invalidateAll<Key, A, E, R>(self: Cache<Key, A, E, R>): Effect<void>

Example Invalidating all entries

import { Cache, Effect } from "effect"
// Clear all cached entries at once
const program = Effect.gen(function*() {
const cache = yield* Cache.make({
capacity: 10,
lookup: (key: string) => Effect.succeed(key.length)
})
// Populate cache with multiple entries
yield* Cache.get(cache, "apple")
yield* Cache.get(cache, "banana")
yield* Cache.get(cache, "cherry")
const sizeBeforeInvalidation = yield* Cache.size(cache)
const hasAppleBeforeInvalidation = yield* Cache.has(cache, "apple")
// Clear all entries
yield* Cache.invalidateAll(cache)
// Verify cache is empty
const sizeAfterInvalidation = yield* Cache.size(cache)
const hasAppleAfterInvalidation = yield* Cache.has(cache, "apple")
const hasBananaAfterInvalidation = yield* Cache.has(cache, "banana")
const hasCherryAfterInvalidation = yield* Cache.has(cache, "cherry")
return [
sizeBeforeInvalidation,
hasAppleBeforeInvalidation,
sizeAfterInvalidation,
hasAppleAfterInvalidation,
hasBananaAfterInvalidation,
hasCherryAfterInvalidation
]
})
const actual = await Effect.runPromise(program)
actual // => [3, true, 0, false, false, false]

Invalidates the entry associated with the specified key in the cache when the predicate returns true for the cached value.

Signature

declare const invalidateWhen: {
<Key, A>(key: Key, f: Predicate<A>): <E, R>(self: Cache<Key, A, E, R>) => Effect<boolean>;
<Key, A, E, R>(self: Cache<Key, A, E, R>, key: Key, f: Predicate<A>): Effect<boolean>;
}

Example Invalidating entries conditionally

import { Cache, Effect } from "effect"
const program = Effect.gen(function*() {
const cache = yield* Cache.make({
capacity: 10,
lookup: (key: string) => Effect.succeed(key.length)
})
// Add values to the cache
yield* Cache.get(cache, "hello") // value = 5
yield* Cache.get(cache, "hi") // value = 2
// Invalidate when value equals 5
const invalidated1 = yield* Cache.invalidateWhen(
cache,
"hello",
(value) => value === 5
)
const hasHello = yield* Cache.has(cache, "hello")
// Don't invalidate when predicate doesn't match
const invalidated2 = yield* Cache.invalidateWhen(
cache,
"hi",
(value) => value === 5
)
const hasHi = yield* Cache.has(cache, "hi")
// Returns false for non-existent keys
const invalidated3 = yield* Cache.invalidateWhen(
cache,
"nonexistent",
() => true
)
// Returns false for failed cached values
const cacheWithErrors = yield* Cache.make<string, number, string>({
capacity: 10,
lookup: (key: string) =>
key === "fail" ? Effect.fail("error") : Effect.succeed(key.length)
})
yield* Effect.exit(Cache.get(cacheWithErrors, "fail"))
const invalidated4 = yield* Cache.invalidateWhen(
cacheWithErrors,
"fail",
() => true
)
return [invalidated1, hasHello, invalidated2, hasHi, invalidated3, invalidated4]
})
const actual = await Effect.runPromise(program)
actual // => [true, false, false, true, false, false]
Since v4.0.0

Retrieves all active keys from the cache, automatically filtering out expired entries.

Signature

declare function keys<Key, A, E, R>(self: Cache<Key, A, E, R>): Effect<Iterable<Key, any, any>>

Example Reading active keys

import { Cache, Effect } from "effect"
// Basic key enumeration
const program = Effect.gen(function*() {
const cache = yield* Cache.make({
capacity: 10,
lookup: (key: string) => Effect.succeed(key.length)
})
// Add some entries to the cache
yield* Cache.get(cache, "hello")
yield* Cache.get(cache, "world")
yield* Cache.get(cache, "cache")
// Retrieve all active keys
const keys = yield* Cache.keys(cache)
return Array.from(keys).sort()
})
const actual = await Effect.runPromise(program)
actual // => ["cache", "hello", "world"]
Since v4.0.0

Forces a refresh of the value associated with the specified key in the cache.

Details

It will always invoke the lookup function to construct a new value, overwriting any existing value for that key.

Signature

declare const refresh: {
<Key, A>(key: Key): <E, R>(self: Cache<Key, A, E, R>) => Effect<A, E, R>;
<Key, A, E, R>(self: Cache<Key, A, E, R>, key: Key): Effect<A, E, R>;
}

Example Refreshing cached values

import { Cache, Effect } from "effect"
// Force refresh of existing cached values
const program = Effect.gen(function*() {
let counter = 0
const cache = yield* Cache.make({
capacity: 10,
lookup: (key: string) => Effect.sync(() => `${key}-${++counter}`)
})
// Initial cache population
const value1 = yield* Cache.get(cache, "user")
// Get from cache (no lookup)
const value2 = yield* Cache.get(cache, "user")
// Force refresh - always calls lookup
const refreshed = yield* Cache.refresh(cache, "user")
// Subsequent gets return refreshed value
const value3 = yield* Cache.get(cache, "user")
return [value1, value2, refreshed, value3, counter]
})
const actual = await Effect.runPromise(program)
actual // => ["user-1", "user-1", "user-2", "user-2", 2]

Example Resetting TTL on refresh

import { Cache, Effect } from "effect"
import { TestClock } from "effect/testing"
// Refresh resets TTL (Time To Live)
const program = Effect.gen(function*() {
const cache = yield* Cache.make({
capacity: 10,
lookup: (key: string) => Effect.succeed(key.length),
timeToLive: "1 hour"
})
yield* Cache.get(cache, "test")
yield* TestClock.adjust("45 minutes")
// Entry would normally expire in 15 minutes
const beforeRefresh = yield* Cache.has(cache, "test")
// Refresh resets the TTL to full 1 hour
yield* Cache.refresh(cache, "test")
yield* TestClock.adjust("30 minutes")
// Still valid because TTL was reset
const afterRefresh = yield* Cache.has(cache, "test")
return [beforeRefresh, afterRefresh]
})
const actual = await Effect.runPromise(Effect.provide(program, TestClock.layer()))
actual // => [true, true]

Example Refreshing missing keys

import { Cache, Effect } from "effect"
// Refresh non-existent keys
const program = Effect.gen(function*() {
const cache = yield* Cache.make({
capacity: 10,
lookup: (key: string) => Effect.succeed(`value-for-${key}`)
})
// Refresh non-existent key creates new entry
const result = yield* Cache.refresh(cache, "newKey")
// Verify it's now cached
const cached = yield* Cache.has(cache, "newKey")
return [result, cached]
})
const actual = await Effect.runPromise(program)
actual // => ["value-for-newKey", true]
Since v4.0.0

Sets the value associated with the specified key in the cache. This will overwrite any existing value for that key, skipping the lookup function.

Signature

declare const set: {
<Key, A>(key: Key, value: A): <E, R>(self: Cache<Key, A, E, R>) => Effect<void>;
<Key, A, E, R>(self: Cache<Key, A, E, R>, key: Key, value: A): Effect<void>;
}

Example Setting values directly

import { Cache, Effect } from "effect"
const program = Effect.gen(function*() {
const cache = yield* Cache.make({
capacity: 100,
lookup: (key: string) => Effect.succeed(key.length)
})
// Set a value directly without invoking lookup
yield* Cache.set(cache, "hello", 42)
return yield* Cache.get(cache, "hello")
})
const actual = await Effect.runPromise(program)
actual // => 42

Example Overwriting cached values

import { Cache, Effect } from "effect"
// Overwriting existing cached values
const program = Effect.gen(function*() {
const cache = yield* Cache.make({
capacity: 100,
lookup: (key: string) => Effect.succeed(key.length)
})
// First get populates via lookup
const original = yield* Cache.get(cache, "test") // 4
// Set overwrites the cached value
yield* Cache.set(cache, "test", 999)
const updated = yield* Cache.get(cache, "test") // 999
return { original, updated }
})
const actual = await Effect.runPromise(program)
actual // => { original: 4, updated: 999 }

Example Applying TTL to set values

import { Cache, Effect } from "effect"
import { TestClock } from "effect/testing"
// TTL behavior with set operations
const program = Effect.gen(function*() {
const cache = yield* Cache.make({
capacity: 100,
lookup: (key: string) => Effect.succeed(key.length),
timeToLive: "1 hour"
})
// Set value with TTL applied
yield* Cache.set(cache, "temporary", 123)
const beforeExpiry = yield* Cache.has(cache, "temporary")
// Advance time past TTL
yield* TestClock.adjust("2 hours")
const afterExpiry = yield* Cache.has(cache, "temporary")
return [beforeExpiry, afterExpiry]
})
const actual = await Effect.runPromise(Effect.provide(program, TestClock.layer()))
actual // => [true, false]

Example Enforcing capacity when setting values

import { Cache, Effect } from "effect"
// Capacity enforcement with set operations
const program = Effect.gen(function*() {
const cache = yield* Cache.make({
capacity: 2,
lookup: (key: string) => Effect.succeed(key.length)
})
// Fill cache to capacity
yield* Cache.set(cache, "a", 1)
yield* Cache.set(cache, "b", 2)
const sizeBeforeEviction = yield* Cache.size(cache)
// Adding another entry evicts oldest
yield* Cache.set(cache, "c", 3)
const sizeAfterEviction = yield* Cache.size(cache)
const hasOldest = yield* Cache.has(cache, "a")
const hasNewest = yield* Cache.has(cache, "c")
return [sizeBeforeEviction, sizeAfterEviction, hasOldest, hasNewest]
})
const actual = await Effect.runPromise(program)
actual // => [2, 2, false, true]
Since v4.0.0

Retrieves the approximate number of entries in the cache.

Details

Note that expired entries are counted until they are accessed and removed. The size reflects the current number of entries stored, not the number of valid entries.

Signature

declare function size<Key, A, E, R>(self: Cache<Key, A, E, R>): Effect<number>

Example Reading cache size

import { Cache, Effect } from "effect"
const program = Effect.gen(function*() {
const cache = yield* Cache.make({
capacity: 10,
lookup: (key: string) => Effect.succeed(key.length)
})
// Empty cache has size 0
const emptySize = yield* Cache.size(cache)
// Add entries and check size
yield* Cache.get(cache, "hello")
yield* Cache.get(cache, "world")
const sizeAfterAdding = yield* Cache.size(cache)
// Size decreases after invalidation
yield* Cache.invalidate(cache, "hello")
const sizeAfterInvalidation = yield* Cache.size(cache)
return [emptySize, sizeAfterAdding, sizeAfterInvalidation]
})
const actual = await Effect.runPromise(program)
actual // => [0, 2, 1]
Since v4.0.0

Retrieves all successfully cached values from the cache, excluding failed lookups and expired entries.

Signature

declare function values<Key, A, E, R>(self: Cache<Key, A, E, R>): Effect<Iterable<A, any, any>>

Example Reading all cached values

import { Cache, Effect } from "effect"
const program = Effect.gen(function*() {
const cache = yield* Cache.make({
capacity: 10,
lookup: (key: string) => Effect.succeed(key.length)
})
// Add some values to the cache
yield* Cache.get(cache, "a")
yield* Cache.get(cache, "ab")
yield* Cache.get(cache, "abc")
// Retrieve all cached values
const values = yield* Cache.values(cache)
return Array.from(values).sort()
})
const actual = await Effect.runPromise(program)
actual // => [1, 2, 3]

Constructors

Since v2.0.0

Creates a cache with a fixed time-to-live for all entries.

Details

This is the basic cache constructor where all entries share the same TTL. The lookup function will be called when a key is not found or has expired.

Signature

declare function make<Key, A, E = never, R = never, ServiceMode extends "lookup" | "construction" = never>(options: {
readonly capacity: number;
readonly lookup: (key: Key) => Effect<A, E, R>;
readonly requireServicesAt?: ServiceMode;
readonly timeToLive?: Input;
}): Effect<Cache<Key, A, E, "lookup" extends ServiceMode ? R : never>, never, "lookup" extends ServiceMode ? never : R>

Example Creating a basic cache

import { Cache, Effect } from "effect"
// Basic cache with string keys
const program = Effect.gen(function*() {
const cache = yield* Cache.make<string, number>({
capacity: 100,
lookup: (key) => Effect.succeed(key.length)
})
const result1 = yield* Cache.get(cache, "hello")
const result2 = yield* Cache.get(cache, "world")
return { result1, result2 }
})
const actual = await Effect.runPromise(program)
actual // => { result1: 5, result2: 5 }

Example Creating a cache with TTL

import { Cache, Effect } from "effect"
const program = Effect.gen(function*() {
const users = new Map([
[123, { name: "Ada", email: "ada@example.com" }],
[456, { name: "Grace", email: "grace@example.com" }]
])
const cache = yield* Cache.make<
number,
{ name: string; email: string },
string
>({
capacity: 500,
lookup: (userId) =>
Effect.suspend(() => {
const user = users.get(userId)
return user === undefined
? Effect.fail(`User ${userId} not found`)
: Effect.succeed(user)
}),
timeToLive: "15 minutes"
})
const user1 = yield* Cache.get(cache, 123)
const user2 = yield* Cache.get(cache, 123)
return [user1, user2, user1 === user2] as const
})
const actual = await Effect.runPromise(program)
actual // => [{ name: "Ada", email: "ada@example.com" }, { name: "Ada", email: "ada@example.com" }, true]
Since v2.0.0

Creates a cache with dynamic time-to-live based on the result and key.

When to use

Use when you need different cache entry lifetimes based on the lookup result or key characteristics.

Details

The timeToLive function receives both the exit result and the key, allowing for flexible TTL policies based on success/failure state and key characteristics.

See

  • make for a simpler cache constructor with a fixed time-to-live for all entries

Signature

declare function makeWith<Key, A, E = never, R = never, ServiceMode extends "lookup" | "construction" = never>(lookup: (key: Key) => Effect<A, E, R>, options: {
readonly capacity: number;
readonly requireServicesAt?: ServiceMode;
readonly timeToLive?: (exit: Exit<A, E>, key: Key) => Input;
}): Effect<Cache<Key, A, E, "lookup" extends ServiceMode ? R : never>, never, "lookup" extends ServiceMode ? never : R>

Example Configuring dynamic time to live

import { Cache, Effect, Exit } from "effect"
// Cache with TTL based on computed value
const program = Effect.gen(function*() {
const cache = yield* Cache.makeWith(
(id: number) => Effect.succeed({ id, active: id % 2 === 0 }),
{
capacity: 1000,
timeToLive(exit) {
if (Exit.isSuccess(exit)) {
const user = exit.value
return user.active ? "1 hour" : "5 minutes"
}
return "30 seconds"
}
}
)
return cache.capacity
})
const actual = await Effect.runPromise(program)
actual // => 1000

Models

Cache

interface
Since v2.0.0

A cache interface that provides a mutable key-value store with automatic TTL management, capacity limits, and lookup functions for cache misses.

Signature

interface Cache<in out Key, in out A, in out E = never, out R = never> extends Pipeable {
readonly "~effect/Cache": "~effect/Cache";
readonly capacity: number;
readonly lookup: (key: Key) => Effect<A, E, R>;
readonly map: MutableHashMap<Key, Entry<A, E>>;
readonly timeToLive: (exit: Exit<A, E>, key: Key) => Duration;
}

Example Creating a basic cache

import { Cache, Effect } from "effect"
// Basic cache with string keys and number values
const program = Effect.gen(function*() {
const cache = yield* Cache.make<string, number>({
capacity: 100,
lookup: (key: string) => Effect.succeed(key.length)
})
// Cache operations
const value1 = yield* Cache.get(cache, "hello") // 5
const value2 = yield* Cache.get(cache, "world") // 5
const value3 = yield* Cache.get(cache, "hello") // 5 (cached)
return [value1, value2, value3]
})
const actual = await Effect.runPromise(program)
actual // => [5, 5, 5]

Example Handling lookup failures

import { Cache, Effect, Exit } from "effect"
// Cache with error handling
const program = Effect.gen(function*() {
const cache = yield* Cache.make<string, number, string>({
capacity: 10,
lookup: (key: string) =>
key === "error"
? Effect.fail("Lookup failed")
: Effect.succeed(key.length)
})
// Handle successful and failed lookups
const success = yield* Cache.get(cache, "test")
const failure = yield* Effect.exit(Cache.get(cache, "error"))
return [success, failure] as const
})
const actual = await Effect.runPromise(program)
actual // => [4, Exit.fail("Lookup failed")]

Example Using complex keys with TTL

import { Cache, Data, Duration, Effect } from "effect"
// Cache with complex key types and TTL
class UserId extends Data.Class<{ id: number }> {}
const program = Effect.gen(function*() {
const userCache = yield* Cache.make<UserId, string>({
capacity: 1000,
lookup: (userId: UserId) => Effect.succeed(`User-${userId.id}`),
timeToLive: Duration.minutes(5)
})
const userId = new UserId({ id: 123 })
const userName = yield* Cache.get(userCache, userId)
return userName
})
const actual = await Effect.runPromise(program)
actual // => "User-123"

Entry

interface
Since v4.0.0

Represents a low-level cache entry containing a deferred lookup result and an optional expiration timestamp.

When to use

Use when inspecting a Cache's low-level map and you need the stored deferred lookup result or expiration timestamp for a key.

Details

An expiresAt value of undefined means the entry does not expire.

Pass the current fiber to await when calling it from inside Effect.withFiber, so the waiter is counted and its cleanup registered in that same step. Without it, await resolves the fiber itself.

See

  • Cache for the public cache API that manages entries through combinators

Signature

interface Entry<A, E> {
awaiters: number;
expiresAt: number | undefined;
readonly fiber: Fiber<A, E>;
await(this: Entry<A, E>, fiber?: Fiber<unknown, unknown>): Effect<A, E>;
}