Cache & ScopedCache
A Cache<Key, A, E, R> is a mutable, effectful key-value store. You give it a
lookup function — (key: Key) => Effect<A, E, R> — and a capacity. Reading a
key with Cache.get returns the cached value if present, or runs the lookup and
stores the result. The headline feature is concurrency: when many fibers request
the same missing key at once, the lookup runs once and every caller awaits
the same result.
import { Cache, Effect } from "effect"
const program = Effect.gen(function*() { // The lookup is the only place the "real" work lives. Here we pretend // it is an expensive call; the cache makes sure it runs at most once // per key while the entry is alive. let lookups = 0 const cache = yield* Cache.make({ capacity: 100, lookup: (key: string) => Effect.sync(() => { lookups++ return key.length }) })
// Three concurrent gets of the same key: the lookup runs once, // and all three callers receive the same value. const results = yield* Effect.all( [Cache.get(cache, "hello"), Cache.get(cache, "hello"), Cache.get(cache, "hello")], { concurrency: "unbounded" } )
return { results, lookups } // { results: [5, 5, 5], lookups: 1 }})The lookup returns an Effect, so it covers both synchronous computation and
asynchronous I/O uniformly. The E and R type parameters flow from the
lookup: a cache whose lookup can fail with E produces a Cache<Key, A, E>, and
Cache.get fails with that same E.
Creating a cache
Section titled “Creating a cache”Use Cache.make when every entry should share one TTL, and Cache.makeWith
when the lifetime depends on the result or the key.
import { Cache, Effect } from "effect"
interface User { readonly id: number readonly name: string}
// A cache of users keyed by id. Entries live for 15 minutes, then the// next read re-runs the lookup. Capacity caps the number of entries.const makeUserCache = (fetchUser: (id: number) => Effect.Effect<User, string>) => Cache.make({ capacity: 500, timeToLive: "15 minutes", lookup: fetchUser })import { Cache, Effect, Exit } from "effect"
interface User { readonly id: number readonly active: boolean}
// makeWith receives the lookup Exit, so the TTL can depend on the// outcome: cache successes for longer, retry failures sooner.const makeUserCache = (fetchUser: (id: number) => Effect.Effect<User, string>) => Cache.makeWith(fetchUser, { capacity: 500, timeToLive: (exit) => Exit.isSuccess(exit) ? (exit.value.active ? "1 hour" : "5 minutes") : "30 seconds" // don't hammer a failing dependency, but retry soon })TTLs accept a Duration input — a string like "15 minutes", a
Duration value, or Duration.infinity (the default) for entries that never
expire on their own.
Capacity and eviction
Section titled “Capacity and eviction”capacity bounds how many entries the cache holds. The cache tracks access
order: reading an entry moves it to the back, and when the cache overflows the
oldest (least recently used) entries are evicted first. The size may briefly
exceed capacity between operations.
import { Cache, Effect } from "effect"
const program = Effect.gen(function*() { const cache = yield* Cache.make({ capacity: 2, lookup: (key: string) => Effect.succeed(key.length) })
yield* Cache.get(cache, "a") yield* Cache.get(cache, "b") yield* Cache.get(cache, "a") // touches "a", making "b" the oldest yield* Cache.get(cache, "c") // overflow: evicts "b"
return { a: yield* Cache.has(cache, "a"), // true b: yield* Cache.has(cache, "b"), // false (evicted) c: yield* Cache.has(cache, "c") // true }})Reading, seeding, and invalidating
Section titled “Reading, seeding, and invalidating”Beyond get, the cache exposes a full set of operations:
| Operation | What it does |
|---|---|
Cache.get | Return the cached value, or run the lookup on a miss/expiry. |
Cache.getOption | Read an existing entry without running the lookup; None if absent. |
Cache.getSuccess | Like getOption, but only for already-resolved successful entries. |
Cache.set | Insert or overwrite a value directly, skipping the lookup. |
Cache.refresh | Force a fresh lookup, resetting the TTL and overwriting the old value. |
Cache.invalidate / invalidateAll | Evict one entry, or clear the whole cache. |
Cache.invalidateWhen | Evict an entry only if its value matches a predicate. |
Cache.has, size, keys, values, entries | Inspect the cache contents. |
A common pattern is a service that wraps a cache and exposes only the operations callers should use:
import { Cache, Context, Effect, Layer } from "effect"
// A service backed by a cache. Construction builds the cache once and// captures it; the public method just delegates to Cache.get.class UserRepo extends Context.Service<UserRepo, { readonly byId: (id: number) => Effect.Effect<string, string> readonly invalidate: (id: number) => Effect.Effect<void>}>()("app/UserRepo") { static layer = Layer.effect( UserRepo, Effect.gen(function*() { const cache = yield* Cache.make({ capacity: 10_000, timeToLive: "5 minutes", // In a real app this would hit a database or HTTP API. lookup: (id: number): Effect.Effect<string, string> => id < 0 ? Effect.fail(`invalid id: ${id}`) : Effect.succeed(`user-${id}`) })
return UserRepo.of({ byId: (id) => Cache.get(cache, id), invalidate: (id) => Cache.invalidate(cache, id) }) }) )}refresh differs from invalidate: invalidate removes the entry so the next
get recomputes it, while refresh recomputes it now and keeps serving the
old value to any concurrent readers until the new one is ready.
Caching a single effect
Section titled “Caching a single effect”When there is no key — you just want to run one effect once and reuse its
result — reach for the Effect.cached family instead of building a whole cache.
Each returns an effect that, when run, yields a new memoized effect.
import { Effect } from "effect"
const program = Effect.gen(function*() { let runs = 0 const expensive = Effect.sync(() => ++runs)
// `cached` is an Effect<A, E, R> that reuses its first result. const cached = yield* Effect.cached(expensive)
const a = yield* cached // runs the effect -> 1 const b = yield* cached // reuses the cached result -> 1 return { a, b, runs } // { a: 1, b: 1, runs: 1 }})Two TTL-aware variants build on this:
Effect.cachedWithTTL(self, duration)— caches the result forduration, then recomputes on the next evaluation after it expires.Effect.cachedInvalidateWithTTL(self, duration)— same, but also returns aninvalidateeffect so you can clear the cache before it expires.
import { Effect } from "effect"
const program = Effect.gen(function*() { const expensive = Effect.sync(() => Math.random())
// Returns a tuple: the cached effect, plus an effect that clears it. const [cached, invalidate] = yield* Effect.cachedInvalidateWithTTL( expensive, "1 minute" )
const first = yield* cached const same = yield* cached // cached: identical to `first` yield* invalidate // drop the cached value early const fresh = yield* cached // recomputed
return { first, same, fresh }})ScopedCache: caching values that own resources
Section titled “ScopedCache: caching values that own resources”When a lookup acquires a resource — opens a connection, starts a worker,
subscribes to a stream — you cannot simply cache the value and forget about it,
because the resource must eventually be released. ScopedCache solves this: each
entry owns its own Scope, and that scope is closed when
the entry expires, is evicted, is invalidated, or when the cache’s owning scope
closes.
import { Effect, ScopedCache } from "effect"
// A pretend connection that logs when opened and closed.const openConnection = (host: string) => Effect.acquireRelease( Effect.as(Effect.log(`open ${host}`), { host }), () => Effect.log(`close ${host}`) )
const program = Effect.gen(function*() { // The cache itself is scoped: it requires a Scope and tears down all // remaining entries when that scope closes. const cache = yield* ScopedCache.make({ capacity: 4, timeToLive: "1 minute", lookup: openConnection })
// First get acquires the connection; the second shares it. const a = yield* ScopedCache.get(cache, "db-1") const b = yield* ScopedCache.get(cache, "db-1") // a and b are the same acquired connection.
// Invalidating closes that entry's scope, releasing the connection. yield* ScopedCache.invalidate(cache, "db-1")}).pipe(Effect.scoped)The API mirrors Cache — get, getOption, set, refresh, invalidate,
has, keys, and so on — but the lookup’s type is
(key: Key) => Effect<A, E, R | Scope>, and the resource lifecycle is managed
for you.
Cache API reference
Section titled “Cache API reference”This section walks every public export of the Cache module. Each entry has a
short description and a runnable snippet with inline // => result comments.
Cache.make
Section titled “Cache.make”Creates a cache with a single fixed TTL shared by every entry (defaulting to
Duration.infinity — entries never expire on their own). Returns
Effect<Cache<Key, A, E, R>>; the R of the lookup flows out as a requirement
of the constructor unless you capture it with requireServicesAt: "lookup".
import { Cache, Effect } from "effect"
const program = Effect.gen(function*() { const cache = yield* Cache.make({ capacity: 100, timeToLive: "15 minutes", lookup: (key: string) => Effect.succeed(key.length) })
return yield* Cache.get(cache, "hello") // => 5})Cache.makeWith
Section titled “Cache.makeWith”Like make, but the TTL is computed per entry from the lookup Exit and key:
(exit, key) => Duration.Input. Use Exit.isSuccess to give successes and
failures different lifetimes. Note the lookup is passed as the first argument
here (not inside the options object).
import { Cache, Effect, Exit } from "effect"
const program = Effect.gen(function*() { const cache = yield* Cache.makeWith( (id: number) => Effect.succeed({ id, active: id % 2 === 0 }), { capacity: 1000, // Cache active users for an hour, inactive for 5 minutes, // and retry failures quickly. timeToLive: (exit) => Exit.isSuccess(exit) ? (exit.value.active ? "1 hour" : "5 minutes") : "30 seconds" } )
return yield* Cache.get(cache, 2) // => { id: 2, active: true }})Cache.get
Section titled “Cache.get”Returns the cached value, or runs the lookup on a miss or expired entry.
Concurrent misses for the same key share one in-flight lookup, and both
successes and failures are cached as Exit values.
import { Cache, Effect } from "effect"
const program = Effect.gen(function*() { let lookups = 0 const cache = yield* Cache.make({ capacity: 10, lookup: (key: string) => Effect.sync(() => (lookups++, key.length)) })
yield* Cache.get(cache, "hello") // => 5 (runs the lookup) yield* Cache.get(cache, "hello") // => 5 (cached, no lookup) return lookups // => 1})Cache.getOption
Section titled “Cache.getOption”Reads an existing entry without triggering the lookup, as
Effect<Option<A>, E>. Returns Option.none() on a miss or expiry, awaits a
pending entry, and fails with E if the cached entry is a failure.
import { Cache, Effect } from "effect"
const program = Effect.gen(function*() { const cache = yield* Cache.make({ capacity: 10, lookup: (key: string) => Effect.succeed(key.length) })
const before = yield* Cache.getOption(cache, "hello") // => Option.none() yield* Cache.get(cache, "hello") const after = yield* Cache.getOption(cache, "hello") // => Option.some(5) return { before, after }})Cache.getSuccess
Section titled “Cache.getSuccess”Like getOption, but only for entries that have already resolved
successfully. Returns Option.none() for missing, expired, failed, or
still-pending entries — and never fails (Effect<Option<A>>).
import { Cache, Effect } from "effect"
const program = Effect.gen(function*() { const cache = yield* Cache.make<string, number, string>({ capacity: 10, lookup: (key) => key === "bad" ? Effect.fail("nope") : Effect.succeed(key.length) })
yield* Effect.exit(Cache.get(cache, "bad")) // cache the failure yield* Cache.get(cache, "ok")
return { ok: yield* Cache.getSuccess(cache, "ok"), // => Option.some(2) bad: yield* Cache.getSuccess(cache, "bad") // => Option.none() }})Cache.set
Section titled “Cache.set”Seeds or overwrites a key with a ready value, skipping the lookup. The cache’s
TTL policy is applied (using a successful Exit), and capacity is enforced.
import { Cache, Effect } from "effect"
const program = Effect.gen(function*() { const cache = yield* Cache.make({ capacity: 100, lookup: (key: string) => Effect.succeed(key.length) })
yield* Cache.set(cache, "hello", 42) return yield* Cache.get(cache, "hello") // => 42 (not 5 from the lookup)})Cache.has
Section titled “Cache.has”Returns whether a non-expired entry exists for the key, without running the
lookup. Expired entries report false.
import { Cache, Effect } from "effect"
const program = Effect.gen(function*() { const cache = yield* Cache.make({ capacity: 10, lookup: (key: string) => Effect.succeed(key.length) })
const before = yield* Cache.has(cache, "hello") // => false yield* Cache.get(cache, "hello") const after = yield* Cache.has(cache, "hello") // => true return { before, after }})Cache.invalidate
Section titled “Cache.invalidate”Drops a single key. Missing keys are a no-op; the next get recomputes the
value.
import { Cache, Effect } from "effect"
const program = Effect.gen(function*() { const cache = yield* Cache.make({ capacity: 10, lookup: (key: string) => Effect.succeed(key.length) })
yield* Cache.get(cache, "hello") yield* Cache.invalidate(cache, "hello") return yield* Cache.has(cache, "hello") // => false})Cache.invalidateWhen
Section titled “Cache.invalidateWhen”Drops a key only if a predicate holds for its cached value, returning whether it
removed anything. Returns false for missing, failed, or non-matching 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) })
yield* Cache.get(cache, "hello") // value 5
const dropped = yield* Cache.invalidateWhen(cache, "hello", (n) => n === 5) // => true const present = yield* Cache.has(cache, "hello") // => false return { dropped, present }})Cache.invalidateAll
Section titled “Cache.invalidateAll”Clears every entry from the cache at once.
import { Cache, Effect } from "effect"
const program = Effect.gen(function*() { const cache = yield* Cache.make({ capacity: 10, lookup: (key: string) => Effect.succeed(key.length) })
yield* Cache.get(cache, "a") yield* Cache.get(cache, "b") yield* Cache.invalidateAll(cache) return yield* Cache.size(cache) // => 0})Cache.refresh
Section titled “Cache.refresh”Recomputes a key by running the lookup again, overwriting the old value and
resetting its TTL. Unlike invalidate, the previous value keeps being served
to concurrent readers until the new lookup resolves.
import { Cache, Effect } from "effect"
const program = Effect.gen(function*() { let counter = 0 const cache = yield* Cache.make({ capacity: 10, lookup: (key: string) => Effect.sync(() => `${key}-${++counter}`) })
yield* Cache.get(cache, "user") // => "user-1" yield* Cache.refresh(cache, "user") // => "user-2" return yield* Cache.get(cache, "user") // => "user-2"})Cache.size
Section titled “Cache.size”The approximate number of stored entries. Expired entries are counted until a later operation observes and removes them.
import { Cache, Effect } from "effect"
const program = Effect.gen(function*() { const cache = yield* Cache.make({ capacity: 10, lookup: (key: string) => Effect.succeed(key.length) })
yield* Cache.get(cache, "a") yield* Cache.get(cache, "b") return yield* Cache.size(cache) // => 2})Cache.keys
Section titled “Cache.keys”Returns an Iterable<Key> of currently active keys, filtering out (and removing)
expired entries as it goes.
import { Cache, Effect } from "effect"
const program = Effect.gen(function*() { const cache = yield* Cache.make({ capacity: 10, lookup: (key: string) => Effect.succeed(key.length) })
yield* Cache.get(cache, "hello") yield* Cache.get(cache, "world") const keys = yield* Cache.keys(cache) return Array.from(keys).sort() // => ["hello", "world"]})Cache.values
Section titled “Cache.values”Returns an Iterable<A> of all successfully resolved, non-expired values
(failed and pending entries are excluded).
import { Cache, Effect } from "effect"
const program = Effect.gen(function*() { const cache = yield* Cache.make({ capacity: 10, lookup: (key: string) => Effect.succeed(key.length) })
yield* Cache.get(cache, "a") yield* Cache.get(cache, "abc") const values = yield* Cache.values(cache) return Array.from(values).sort() // => [1, 3]})Cache.entries
Section titled “Cache.entries”Returns an Iterable<[Key, A]> of all successfully resolved, non-expired
key-value pairs.
import { Cache, Effect } from "effect"
const program = Effect.gen(function*() { const cache = yield* Cache.make({ capacity: 10, lookup: (key: string) => Effect.succeed(key.length) })
yield* Cache.get(cache, "a") yield* Cache.get(cache, "bb") const entries = yield* Cache.entries(cache) return Array.from(entries).sort() // => [["a", 1], ["bb", 2]]})Cache (interface)
Section titled “Cache (interface)”Cache<Key, A, E, R> is the cache value itself. Its type parameters are the key
type, the success value, the lookup error E, and the lookup requirements R.
You normally pass it to the combinators above rather than touching its fields,
but it exposes capacity, lookup, timeToLive, and the underlying map.
import { Cache, Effect } from "effect"
// Annotate a cache you accept as a parameter.const total = (cache: Cache.Cache<string, number>) => Effect.map(Cache.values(cache), (vs) => Array.from(vs).reduce((a, b) => a + b, 0))Cache.Entry<A, E> is a low-level stored entry: an expiresAt timestamp
(undefined when it never expires) and a Deferred<A, E> holding the in-flight
or completed lookup result. You only encounter it when inspecting the cache’s
internal map.
import type { Cache } from "effect"
// The shape stored per key inside `cache.map`.type Entry = Cache.Entry<number, string>// => { expiresAt: number | undefined; deferred: Deferred<number, string> }ScopedCache API reference
Section titled “ScopedCache API reference”ScopedCache mirrors the Cache API almost name-for-name, with two key
differences: the lookup may acquire resources (its type is
(key) => Effect<A, E, R | Scope.Scope>), and the cache must be built inside a
Scope. Each entry owns its own scope, finalized when the entry is evicted,
expires, is invalidated/replaced, or when the owning scope closes. Reads,
inspection, and invalidation work just like Cache; only the lifecycle differs.
ScopedCache.make
Section titled “ScopedCache.make”Creates a scoped cache with a fixed TTL. Requires a Scope in context and yields
Effect<ScopedCache<Key, A, E, R>, never, R | Scope>.
import { Effect, ScopedCache } from "effect"
// Each cached value is an acquired connection; the cache releases// remaining connections when its owning scope closes.const openConnection = (host: string) => Effect.acquireRelease( Effect.as(Effect.log(`open ${host}`), { host }), () => Effect.log(`close ${host}`) )
const program = ScopedCache.make({ capacity: 4, timeToLive: "1 minute", lookup: openConnection}).pipe( Effect.flatMap((cache) => ScopedCache.get(cache, "db-1")), Effect.scoped)// => { host: "db-1" } (logs "open db-1", then "close db-1" on scope close)ScopedCache.makeWith
Section titled “ScopedCache.makeWith”The dynamic-TTL variant. The lookup goes inside the options object here (unlike
Cache.makeWith), and timeToLive is (exit, key) => Duration.Input.
import { Effect, Exit, ScopedCache } from "effect"
const program = ScopedCache.makeWith({ capacity: 100, lookup: (host: string) => Effect.acquireRelease( Effect.succeed({ host }), () => Effect.log(`close ${host}`) ), // Keep successful connections for an hour; expire failures quickly. timeToLive: (exit) => (Exit.isSuccess(exit) ? "1 hour" : "5 seconds")})ScopedCache.get
Section titled “ScopedCache.get”Returns the cached value or runs the lookup in a fresh entry scope on a miss. Concurrent misses share one lookup; the resource is acquired once and reused.
import { Effect, ScopedCache } from "effect"
const program = Effect.gen(function*() { let opened = 0 const cache = yield* ScopedCache.make({ capacity: 4, lookup: (host: string) => Effect.acquireRelease( Effect.sync(() => (opened++, { host })), () => Effect.void ) })
yield* ScopedCache.get(cache, "db-1") yield* ScopedCache.get(cache, "db-1") // shares the acquired resource return opened // => 1}).pipe(Effect.scoped)ScopedCache.getOption
Section titled “ScopedCache.getOption”Reads an existing unexpired entry without starting a lookup, as
Effect<Option<A>, E>. Option.none() on miss/expiry; awaits pending entries.
import { Effect, ScopedCache } from "effect"
const program = Effect.gen(function*() { const cache = yield* ScopedCache.make({ capacity: 4, lookup: (host: string) => Effect.succeed({ host }) })
return yield* ScopedCache.getOption(cache, "db-1") // => Option.none()}).pipe(Effect.scoped)ScopedCache.getSuccess
Section titled “ScopedCache.getSuccess”Inspects an already-resolved successful entry as Effect<Option<A>>.
Option.none() for missing, expired, failed, or pending entries; never fails.
import { Effect, ScopedCache } from "effect"
const program = Effect.gen(function*() { const cache = yield* ScopedCache.make({ capacity: 4, lookup: (host: string) => Effect.succeed({ host }) })
yield* ScopedCache.get(cache, "db-1") return yield* ScopedCache.getSuccess(cache, "db-1") // => Option.some({ host: "db-1" })}).pipe(Effect.scoped)ScopedCache.set
Section titled “ScopedCache.set”Seeds a key with a ready value in a fresh entry scope, closing any prior scope for that key and applying the TTL.
import { Effect, ScopedCache } from "effect"
const program = Effect.gen(function*() { const cache = yield* ScopedCache.make({ capacity: 4, lookup: (host: string) => Effect.succeed({ host }) })
yield* ScopedCache.set(cache, "db-1", { host: "preset" }) return yield* ScopedCache.get(cache, "db-1") // => { host: "preset" }}).pipe(Effect.scoped)ScopedCache.has
Section titled “ScopedCache.has”Whether a non-expired entry exists, without running the lookup. Closes scopes of expired entries it encounters.
import { Effect, ScopedCache } from "effect"
const program = Effect.gen(function*() { const cache = yield* ScopedCache.make({ capacity: 4, lookup: (host: string) => Effect.succeed({ host }) })
yield* ScopedCache.get(cache, "db-1") return yield* ScopedCache.has(cache, "db-1") // => true}).pipe(Effect.scoped)ScopedCache.invalidate
Section titled “ScopedCache.invalidate”Removes one key and closes its entry scope, releasing the resource. No-op if absent.
import { Effect, ScopedCache } from "effect"
const program = Effect.gen(function*() { const cache = yield* ScopedCache.make({ capacity: 4, lookup: (host: string) => Effect.acquireRelease( Effect.as(Effect.log(`open ${host}`), { host }), () => Effect.log(`close ${host}`) ) })
yield* ScopedCache.get(cache, "db-1") // logs "open db-1" yield* ScopedCache.invalidate(cache, "db-1") // logs "close db-1"}).pipe(Effect.scoped)ScopedCache.invalidateWhen
Section titled “ScopedCache.invalidateWhen”Removes a key (and closes its scope) only when a predicate holds for its
successful value. Returns whether it removed anything; false for failed,
missing, expired, or non-matching entries.
import { Effect, ScopedCache } from "effect"
const program = Effect.gen(function*() { const cache = yield* ScopedCache.make({ capacity: 4, lookup: (host: string) => Effect.succeed({ host, stale: host === "db-1" }) })
yield* ScopedCache.get(cache, "db-1") return yield* ScopedCache.invalidateWhen(cache, "db-1", (v) => v.stale) // => true (entry removed, scope closed)}).pipe(Effect.scoped)ScopedCache.refresh
Section titled “ScopedCache.refresh”Recomputes a key by running the lookup in a new scope, then closes the old entry’s scope once the new value resolves. The previous value stays available to concurrent readers until then.
import { Effect, ScopedCache } from "effect"
const program = Effect.gen(function*() { let n = 0 const cache = yield* ScopedCache.make({ capacity: 4, lookup: (host: string) => Effect.succeed({ host, gen: ++n }) })
yield* ScopedCache.get(cache, "db-1") // => { host: "db-1", gen: 1 } return yield* ScopedCache.refresh(cache, "db-1") // => { host: "db-1", gen: 2 }}).pipe(Effect.scoped)ScopedCache.invalidateAll
Section titled “ScopedCache.invalidateAll”Removes every entry and closes every entry scope.
import { Effect, ScopedCache } from "effect"
const program = Effect.gen(function*() { const cache = yield* ScopedCache.make({ capacity: 4, lookup: (host: string) => Effect.succeed({ host }) })
yield* ScopedCache.get(cache, "db-1") yield* ScopedCache.invalidateAll(cache) return yield* ScopedCache.size(cache) // => 0}).pipe(Effect.scoped)ScopedCache.size
Section titled “ScopedCache.size”Approximate number of stored entries (expired entries counted until observed);
returns 0 once the cache is closed.
import { Effect, ScopedCache } from "effect"
const program = Effect.gen(function*() { const cache = yield* ScopedCache.make({ capacity: 4, lookup: (host: string) => Effect.succeed({ host }) })
yield* ScopedCache.get(cache, "db-1") yield* ScopedCache.get(cache, "db-2") return yield* ScopedCache.size(cache) // => 2}).pipe(Effect.scoped)ScopedCache.keys
Section titled “ScopedCache.keys”An Array<Key> of active keys; closes scopes for expired entries while
filtering.
import { Effect, ScopedCache } from "effect"
const program = Effect.gen(function*() { const cache = yield* ScopedCache.make({ capacity: 4, lookup: (host: string) => Effect.succeed({ host }) })
yield* ScopedCache.get(cache, "db-1") yield* ScopedCache.get(cache, "db-2") return (yield* ScopedCache.keys(cache)).sort() // => ["db-1", "db-2"]}).pipe(Effect.scoped)ScopedCache.values
Section titled “ScopedCache.values”An Array<A> of successfully resolved, non-expired values.
import { Effect, ScopedCache } from "effect"
const program = Effect.gen(function*() { const cache = yield* ScopedCache.make({ capacity: 4, lookup: (host: string) => Effect.succeed({ host }) })
yield* ScopedCache.get(cache, "db-1") return yield* ScopedCache.values(cache) // => [{ host: "db-1" }]}).pipe(Effect.scoped)ScopedCache.entries
Section titled “ScopedCache.entries”An Array<[Key, A]> of successfully resolved, non-expired key-value pairs.
import { Effect, ScopedCache } from "effect"
const program = Effect.gen(function*() { const cache = yield* ScopedCache.make({ capacity: 4, lookup: (host: string) => Effect.succeed({ host }) })
yield* ScopedCache.get(cache, "db-1") return yield* ScopedCache.entries(cache) // => [["db-1", { host: "db-1" }]]}).pipe(Effect.scoped)ScopedCache (interface)
Section titled “ScopedCache (interface)”ScopedCache<Key, A, E, R> is the cache value. Its lookup type carries a Scope
requirement ((key) => Effect<A, E, R | Scope>); it exposes capacity,
lookup, timeToLive, and its current state.
import { Effect, ScopedCache } from "effect"
// Annotate a scoped cache passed in as a dependency.const firstKey = (cache: ScopedCache.ScopedCache<string, { host: string }>) => Effect.map(ScopedCache.keys(cache), (ks) => ks[0])ScopedCache.State<K, A, E> is the cache’s lifecycle state: either
{ _tag: "Open", map } holding the entries, or { _tag: "Closed" } after the
owning scope closes. Lookup operations interrupt once the state is Closed.
import type { ScopedCache } from "effect"
type State = ScopedCache.State<string, number, never>// => { _tag: "Open"; map: MutableHashMap<...> } | { _tag: "Closed" }ScopedCache.Entry<A, E> is a stored entry: an optional expiresAt, the shared
Deferred<A, E> lookup result, and the Scope.Closeable that owns the entry’s
resources. Removing the entry closes that scope.
import type { ScopedCache } from "effect"
type Entry = ScopedCache.Entry<{ host: string }, never>// => { expiresAt: number | undefined; deferred: Deferred<...>; scope: Scope.Closeable }When not to use Cache
Section titled “When not to use Cache”If you need to share a live resource (where identity and cleanup matter more
than memoizing a computed value), reach for reference counting instead — see
RcMap and RcRef. If you have a single value
that should be reloaded periodically, Resource is a
better fit.