# Cache Handler

Rebask routes server cache operations through a `CacheStorage` implementation. In the default Node
runtime, the production handler persists entries and tag invalidations on disk and adds a
per-process in-memory LRU. Multiple server instances each keep their own filesystem cache.

Use cacheHandler to connect Rebask to shared storage such as Redis. Platform adapters may provide
their own handler and route supported entries through platform-provided storage.

## Configure a handler

The `cacheHandler` option accepts a `CacheStorage` implementation, which defines the contract
between Rebask and the storage backend for reading, writing, deleting, and invalidating server cache
entries:

```ts
import type { RebaskConfig } from "@rebask/core"
import { CustomCacheHandler } from "./custom-handler"

export default {
  cacheHandler: new CustomCacheHandler(),
} satisfies RebaskConfig
```

For most backends, extend `BaseCacheStorage`. It handles entry expiration and staleness, so you only
implement what touches your store. Only reach for CacheStorage directly when you need to override
that behavior entirely.

Server caching goes through one `CacheStorage` implementation: the default filesystem handler, your
`cacheHandler`, or one provided by a platform adapter. Treat the keys you receive as opaque and
store each entry unchanged.

## `BaseCacheStorage`

`BaseCacheStorage` supplies `get` and `getEntryStatus` to handle entry expiration and staleness.
You can extend it and implement only the storage backend and your tag strategy:

```ts
import { BaseCacheStorage, type CacheEntry } from "@rebask/core/cache"

export class RedisCacheStorage extends BaseCacheStorage {
  protected async read(key: string) {
    const data = await redis.get(key)
    return data ? (JSON.parse(data) as CacheEntry) : null
  }

  async set(key: string, entry: CacheEntry) {
    await redis.set(key, JSON.stringify(entry))
  }

  async delete(key: string) {
    await redis.del(key)
  }

  // Lazy tag invalidation.
  async revalidateTag(tag: string) {
    await redis.set(`invalidated:${tag}`, String(Date.now()))
  }

  protected async invalidatedAt(tag: string): Promise<number | undefined> {
    const value = await redis.get(`invalidated:${tag}`)
    return value ? Number(value) : undefined
  }
}
```

The base `get` wraps your `read`: it removes expired entries and, if you override `invalidatedAt`,
entries whose tags were invalidated. Store tag invalidations in your backend so they survive
restarts and are shared across instances.

There are two common ways to handle tags:

- **Lazy** — hold tag invalidation timestamps separately. Override `invalidatedAt` to answer when a
  tag was last invalidated, and record it in `revalidateTag`.
- **Eager** — delete the tagged entries in `revalidateTag`, and leave `invalidatedAt` as its
  default.

## `CacheStorage`

`CacheStorage` is the contract between Rebask and your storage backend. Its methods cover entry
access, expiration, tag invalidation, and optional coordination of background revalidation:

```ts
import type { CacheEntry, CacheEntryStatus, CacheStorage } from "@rebask/core/cache"

interface CacheStorage {
  get(key: string): Promise<CacheEntry | null>
  set(key: string, entry: CacheEntry, options?: { durable?: boolean }): Promise<void>
  delete(key: string): Promise<void>
  revalidateTag(tag: string): Promise<void>
  getEntryStatus(entry: CacheEntry): CacheEntryStatus
  tryClaim?(key: string): Promise<(() => void | Promise<void>) | null>
}
```

### `get`

```ts
get(key: string): Promise<CacheEntry | null>
```

Returns the entry for `key`, or `null` when it is missing or expired.

Stale entries must still be returned. Rebask can serve a stale entry while it refreshes the value in
the background. An expired entry must never be returned.

Rebask wraps every configured handler with an expiry check using `getEntryStatus`. If a handler
returns an expired entry, Rebask deletes it and treats the lookup as a miss.

### `set`

```ts
set(key: string, entry: CacheEntry, options?: { durable?: boolean }): Promise<void>
```

Stores the complete `CacheEntry` under `key`. The entry contains its timestamp, lifetime, tags, type,
and payload.

`durable: true` requires the handler to resolve only after the entry is safely persisted.
[Build prerenders](./page-caching.md) rely on this because the process may exit immediately after
the write.

### `delete`

```ts
delete(key: string): Promise<void>
```

Removes the entry for `key`. Rebask calls it to clean up expired entries and entries that cannot be
deserialized.

### `revalidateTag`

```ts
revalidateTag(tag: string): Promise<void>
```

Marks entries carrying `tag` as expired for future reads. Tag invalidation does not need to scan and
delete every matching entry immediately.

### `getEntryStatus`

```ts
getEntryStatus(entry: CacheEntry): CacheEntryStatus
```

Returns whether an `entry` is stale or expired:

```ts
interface CacheEntryStatus {
	/** Entry is past its `revalidate` window but still within `expire`. Safe to serve, should be revalidated. */
	stale: boolean
	/** Entry is past its `expire` window. Should not be served. */
	expired: boolean
}
```

An expired entry is never also stale. Tag-invalidated entries are dropped by `get` before their
status is read. Rebask uses this status to decide whether to serve an entry, refresh it in the
background, or render a new value.

### `tryClaim`

```ts
tryClaim?(key: string): Promise<(() => void | Promise<void>) | null>
```

`tryClaim` is optional. Implement it when multiple instances need to coordinate
[background revalidation](./cache-lifetimes.md):

- Returns a release function when this instance claims `key`.
- Returns `null` when another instance already holds the claim or the handler does not persist that
  key.
- Makes the claim atomic across instances and gives it a TTL so a crashed instance cannot block the
  key forever.

When `tryClaim` is not implemented, Rebask deduplicates background revalidation only within the
current process. The release function may be asynchronous and is called after revalidation finishes.

## `CacheEntry`

```ts
import type { CacheEntry, CacheEntryType } from "@rebask/core/cache"
```

A `CacheEntry` is an object with shared metadata and fields that depend on the entry `type`:

| Field        | Description                                             | Available on                                                   |
| ------------ | ------------------------------------------------------- | -------------------------------------------------------------- |
| `type`       | Identifies the entry type                               | Every entry                                                    |
| `timestamp`  | Creation time in milliseconds                           | Every entry                                                    |
| `revalidate` | Number of seconds an entry remains fresh                | Every entry                                                    |
| `expire`     | Number of seconds after which an entry cannot be served | Every entry                                                    |
| `tags`       | Optional tags used by `revalidateTag`                   | Every entry                                                    |
| `html`       | Rendered HTML                                           | HTML entries                                                   |
| `postponed`  | Serialized resume state                                 | HTML entries with postponed state ([PPR](../rendering/ppr.md)) |
| `rsc`        | Encoded RSC payload                                     | RSC and `"use cache"` entries                                  |
| `data`       | Encoded JSON data                                       | `durableCache` entries                                         |

The handler should store and return each entry unchanged. The entry can be serialized as JSON.
