
# Invalidation & Expiration

A cached function from [`defineCachedFunction`](/docs/functions) carries three methods for purging or refreshing its entries. Each takes the same arguments as the cached function itself:

| Method                  | Effect                                                                                                       |
| ----------------------- | ------------------------------------------------------------------------------------------------------------ |
| `.invalidate(...args)`  | **Removes** the entries. The next call waits for a fresh value.                                              |
| `.expire(...args)`      | Marks the entries **stale**. With SWR, readers keep getting the stale value while a background refresh runs. |
| `.resolveKeys(...args)` | Returns the raw storage keys, one per `base` prefix.                                                         |

Standalone helpers — `invalidateCache`, `expireCache`, `resolveCacheKeys` — do the same when you have no reference to the cached function, for example from a webhook or another module. Cached **handlers** have equivalent methods that take a request event; see [the handler guide](/docs/handler#on-demand-revalidation).

## Invalidate

`.invalidate()` removes cached entries from **all base prefixes**. The next call runs the function again and waits for the fresh value:

```ts
import { defineCachedFunction } from "ocache";

const getUser = defineCachedFunction(async (id: string) => db.users.find(id), {
  name: "getUser",
  maxAge: 60,
  getKey: (id) => id,
});

await getUser("user-123"); // resolves and caches

// e.g. after the user updates their profile
await getUser.invalidate("user-123");

await getUser("user-123"); // re-invokes the function, waits for the fresh value
```

> [!NOTE]
> `.invalidate()` and `.expire()` also cancel a resolution that is already in flight for the key. The call that started it still receives its value, but that pre-purge value is no longer written to storage, and the next call resolves again. If a write already reached the backend, the purge waits for it to land first, so the write cannot undo the purge. This ordering holds within one process; a backend shared across processes needs its own compare-and-swap support. The standalone helpers can do neither, because they never see the cached function.

## Expire (SWR refresh)

Where `.invalidate()` makes the next reader wait, `.expire()` only marks the entry **stale**. With SWR enabled, readers continue to get the stale value instantly — still bounded by the original `staleMaxAge` window — while the next access starts a background refresh:

```ts
const getUser = defineCachedFunction(async (id: string) => db.users.find(id), {
  name: "getUser",
  maxAge: 60,
  staleMaxAge: 300,
  swr: true, // required to actually serve stale values
  getKey: (id) => id,
});

// Next reader gets the stale value instantly; a refresh runs in the background
await getUser.expire("user-123");
```

> [!TIP]
> Use `.expire()` to refresh without blocking readers. Use `.invalidate()` only when the next reader must get a guaranteed fresh value. Without `swr: true`, an expired entry is revalidated in the foreground, so `.expire()` behaves much like `.invalidate()` for the next reader.

## Standalone helpers

The helpers rebuild the cache key from the options you pass — they cannot see the original function. The `name`, `getKey`, `group`, and `base` must match what you cached with, or the helpers target a different key and silently do nothing.

`invalidateCache()` and `expireCache()` additionally need **the same `storage`** the entries were written to:

```ts
import { invalidateCache } from "ocache";

// `cacheOptions` is the very object `defineCachedFunction` was called with.
await invalidateCache({ options: cacheOptions, args: ["user-123"] });

// Or reconstruct it — including the storage the entries were written to.
await invalidateCache({
  options: { name: "getUser", getKey: (id: string) => id, storage },
  args: ["user-123"],
});
```

`expireCache()` should also receive the same lifetime options (`maxAge`, `swr`, `staleMaxAge`), so it preserves the entry's remaining storage TTL instead of extending it:

```ts
import { expireCache } from "ocache";

await expireCache({
  options: {
    name: "getUser",
    getKey: (id: string) => id,
    maxAge: 60,
    swr: true,
    staleMaxAge: 300,
    storage,
  },
  args: ["user-123"],
});
```

> [!IMPORTANT]
> Storage belongs to **each cached function** — there is no global fallback. `invalidateCache()` and `expireCache()` therefore **throw** when `options.storage` is unset, rather than "purging" an empty store while the stale entry stays available. ocache writes the resolved storage back onto the options object it was configured with, so passing that original object always works. A mismatched `name` or `getKey` is still a silent no-op; only a missing backend errors. And unlike the methods, the helpers cannot cancel an in-flight resolution — a call already running can write its pre-purge value after the helper returns. Prefer `.invalidate()` / `.expire()` whenever you have the cached function.

## Resolving keys

For advanced tasks — inspecting entries, custom storage operations — `.resolveKeys()` returns the raw storage keys for a set of arguments, one per base prefix:

```ts
const keys = await getUser.resolveKeys("user-123");
// ["/cache:functions:getUser:user-123.json"]
```

The standalone `resolveCacheKeys()` does the same without a function reference. It needs no storage — pass matching key options and read the entries from the storage the function uses:

```ts
import { createMemoryStorage, defineCachedFunction, resolveCacheKeys } from "ocache";

const storage = createMemoryStorage();
const getUser = defineCachedFunction(fetchUser, {
  name: "getUser",
  getKey: (id: string) => id,
  storage,
});

const keys = await resolveCacheKeys({
  options: { name: "getUser", getKey: (id: string) => id },
  args: ["user-123"],
});

for (const key of keys) {
  const entry = await storage.get(key);
  // ...inspect or manipulate the raw entry
}
```

See [Storage](/docs/storage) for the underlying store, and [Caching HTTP Handlers](/docs/handler#on-demand-revalidation) for invalidating cached responses.
