# 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.

```ts
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`.
**Note:** Both successes **and** failures are cached as `Exit` values. A failed lookup is
remembered and will fail again on the next `get` until the entry expires, is
invalidated, or is refreshed. If the lookup is *interrupted*, the entry is
removed so the next `get` retries.

## 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.

```ts
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
  })
```

```ts
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`](https://effect.plants.sh/data-types/) 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

`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.

```ts
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

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:

```ts
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.
**Caution:** When keys are objects, use a structurally comparable type — a [`Data`](https://effect.plants.sh/data-types/)
class or another `Equal`/`Hash` type — so that two keys with the same contents
hit the same entry. Plain object literals compare by reference and will miss.

## 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*.

```ts
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 for `duration`,
  then recomputes on the next evaluation after it expires.
- `Effect.cachedInvalidateWithTTL(self, duration)` — same, but also returns an
  `invalidate` effect so you can clear the cache before it expires.

```ts
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

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`](https://effect.plants.sh/resource-management/), and that scope is closed when
the entry expires, is evicted, is invalidated, or when the cache's owning scope
closes.

```ts
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.
**Note:** Because the cache is created inside a scope, `ScopedCache.make` requires a
`Scope` in its context. Run the workflow with `Effect.scoped` (as above) or
inside an existing scoped operation. Once the owning scope closes, lookup-style
operations on the cache are interrupted.

## 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

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"`.

```ts
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

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).

```ts
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

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.

```ts
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

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.

```ts
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

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>>`).

```ts
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

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.

```ts
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

Returns whether a non-expired entry exists for the key, without running the
lookup. Expired entries report `false`.

```ts
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

Drops a single key. Missing keys are a no-op; the next `get` recomputes the
value.

```ts
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

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.

```ts
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

Clears every entry from the cache at once.

```ts
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

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.

```ts
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

The approximate number of stored entries. Expired entries are counted until a
later operation observes and removes them.

```ts
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

Returns an `Iterable<Key>` of currently active keys, filtering out (and removing)
expired entries as it goes.

```ts
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

Returns an `Iterable<A>` of all **successfully resolved**, non-expired values
(failed and pending entries are excluded).

```ts
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

Returns an `Iterable<[Key, A]>` of all successfully resolved, non-expired
key-value pairs.

```ts
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)

`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`.

```ts
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))
```

### Entry

`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`.

```ts
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

`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.
**Note:** `ScopedCache.keys`, `values`, and `entries` return an `Array` (not the lazy
`Iterable` that `Cache` returns), because they may close scopes for expired
entries as they run. Once the owning scope closes, lookup-style operations
(`get`, `getOption`, `set`, `has`, `invalidate`, `refresh`, `invalidateAll`)
**interrupt** instead of running.

### 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>`.

```ts
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

The dynamic-TTL variant. The lookup goes inside the options object here (unlike
`Cache.makeWith`), and `timeToLive` is `(exit, key) => Duration.Input`.

```ts
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

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.

```ts
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

Reads an existing unexpired entry without starting a lookup, as
`Effect<Option<A>, E>`. `Option.none()` on miss/expiry; awaits pending entries.

```ts
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

Inspects an already-resolved successful entry as `Effect<Option<A>>`.
`Option.none()` for missing, expired, failed, or pending entries; never fails.

```ts
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

Seeds a key with a ready value in a fresh entry scope, closing any prior scope
for that key and applying the TTL.

```ts
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

Whether a non-expired entry exists, without running the lookup. Closes scopes of
expired entries it encounters.

```ts
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

Removes one key and **closes its entry scope**, releasing the resource. No-op if
absent.

```ts
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

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.

```ts
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

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.

```ts
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

Removes every entry and closes every entry scope.

```ts
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

Approximate number of stored entries (expired entries counted until observed);
returns `0` once the cache is closed.

```ts
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

An `Array<Key>` of active keys; closes scopes for expired entries while
filtering.

```ts
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

An `Array<A>` of successfully resolved, non-expired values.

```ts
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

An `Array<[Key, A]>` of successfully resolved, non-expired key-value pairs.

```ts
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)

`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`.

```ts
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])
```

### State

`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`.

```ts
import type { ScopedCache } from "effect"

type State = ScopedCache.State<string, number, never>
// => { _tag: "Open"; map: MutableHashMap<...> } | { _tag: "Closed" }
```

### Entry

`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.

```ts
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

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`](https://effect.plants.sh/caching/reference-counting/). If you have a single value
that should be reloaded periodically, [`Resource`](https://effect.plants.sh/caching/resource/) is a
better fit.