Skip to content

Commit ab46d29

Browse files
committed
chore: update docs
1 parent 4ffe207 commit ab46d29

4 files changed

Lines changed: 19 additions & 303 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# ocache
22

3-
Standalone caching utilities extracted from [nitro](https://github.com/nitrojs/nitro). Zero framework dependencies — works with any runtime that has standard `Request`/`Response`.
3+
Composable caching primitives. works with any runtime that has standard `Request`/`Response`.
44

55
## Project Structure
66

README.md

Lines changed: 16 additions & 300 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,19 @@
77

88
<!-- /automd -->
99

10+
Composable caching primitives with TTL, stale-while-revalidate, and HTTP response caching. Zero framework dependencies — works with any runtime that has standard `Request`/`Response`.
11+
12+
> [!TIP]
13+
> 📖 Head to the [documentation](https://ocache.unjs.io/guide) to learn more.
14+
15+
## Features
16+
17+
- 🗃️ **[Function caching](https://ocache.unjs.io/guide/functions)** — wrap any function with TTL, stale-while-revalidate, and request deduplication.
18+
- 🌐 **[HTTP response caching](https://ocache.unjs.io/guide/handler)** — automatic `etag`, `last-modified`, and `304 Not Modified` support.
19+
- 🔑 **[Smart cache keys](https://ocache.unjs.io/guide/query-params)** — derived from arguments or request URL, with per-header and per-query variance.
20+
- 🔌 **[Pluggable storage](https://ocache.unjs.io/guide/storage)** — bring your own backend via a minimal `get`/`set` interface.
21+
- ♻️ **[Invalidation & expiration](https://ocache.unjs.io/guide/invalidation)** — remove or mark entries stale on demand, with SWR background refresh.
22+
1023
## Usage
1124

1225
### Caching Functions
@@ -31,56 +44,8 @@ const cachedFetch = defineCachedFunction(
3144
const data = await cachedFetch("https://api.example.com/data");
3245
```
3346

34-
#### Options
35-
36-
```ts
37-
const cached = defineCachedFunction(fn, {
38-
name: "my-fn", // Cache key name (defaults to function name)
39-
maxAge: 10, // TTL in seconds (default: 1)
40-
swr: false, // Stale-while-revalidate (default: false — opt in to serve stale)
41-
staleMaxAge: 60, // Max seconds to serve stale content
42-
getMaxAge: (entry) => entry.value?.expires_in, // Per-entry TTL from the resolved value
43-
base: "/cache", // Base prefix for cache keys (string or string[] for multi-tier)
44-
group: "my-group", // Cache key group (default: "functions")
45-
getKey: (...args) => "custom-key", // Custom cache key generator
46-
shouldBypassCache: (...args) => false, // Skip cache entirely when true
47-
shouldInvalidateCache: (...args) => false, // Force refresh when true
48-
validate: (entry) => entry.value !== undefined, // Custom validation
49-
serialize: (entry) => entry.value, // Prepare value for storage (transform restores it on read)
50-
transform: (entry) => entry.value, // Transform before returning
51-
onError: (error) => console.error(error), // Error handler
52-
});
53-
```
54-
55-
#### Dynamic TTL
56-
57-
Some cached values carry their own expiry — an OAuth token with `expires_in`, an upstream response with `Cache-Control: max-age`. Use `getMaxAge` to derive the lifetime from the resolved value instead of a fixed constant. It runs after the resolver and returns either a number (seconds, shorthand for `maxAge`) or `{ maxAge?, staleMaxAge? }` to also override the stale window. The resolved values override the static options for that entry and are used for both the freshness check and the storage TTL. Return `undefined` (or omit a field) to fall back to the static option.
58-
59-
```ts
60-
const getToken = defineCachedFunction(
61-
() => fetchToken(), // resolves { access_token, expires_in }
62-
{
63-
// Cache each token for exactly its own lifetime (minus a small safety margin)
64-
getMaxAge: (entry) => Math.max(1, (entry.value?.expires_in ?? 60) - 5),
65-
},
66-
);
67-
```
68-
69-
#### Custom Serialization
70-
71-
Some resolver outputs can't be persisted as-is — a `ReadableStream`, a class instance. Use `serialize` to convert the value to a storable form on write, and `transform` to reconstruct the usable value on read. `serialize` runs exactly once per resolution, right after the resolver (and after `getMaxAge`, so that hook still sees the raw value) — shared across concurrent deduplicated calls, so consuming a one-shot source like a stream is safe.
72-
73-
```ts
74-
const getReport = defineCachedFunction(
75-
() => generateReportStream(), // resolves a one-shot ReadableStream
76-
{
77-
// Persist the stream as a string...
78-
serialize: (entry) => streamToString(entry.value),
79-
// ...and recreate a fresh stream on every read.
80-
transform: (entry) => stringToStream(entry.value),
81-
},
82-
);
83-
```
47+
> [!NOTE]
48+
> Learn more in the [Caching Functions](https://ocache.unjs.io/guide/functions) guide, and see [Invalidation & Expiration](https://ocache.unjs.io/guide/invalidation) and [Storage](https://ocache.unjs.io/guide/storage).
8449
8550
### Caching HTTP Handlers
8651

@@ -108,257 +73,8 @@ const handler = defineCachedHandler(
10873
);
10974
```
11075

111-
#### Query Parameters
112-
113-
By default the full query string varies the cache key, so `?color=red` and `?color=red&utm=x` are cached separately and unknown params can bust the cache. Set `allowQuery` to an allowlist of param names so only those affect the key — all other params are ignored. Ignored params are also stripped from the URL the handler receives (like non-`varies` headers), so a handler can never accidentally produce output that depends on a param outside the key. Param order is normalized, and repeated (array) params like `?color=red&color=blue` are matched regardless of order. Passing an empty array (`allowQuery: []`) varies by nothing — every query shares one entry. If you set a custom `getKey`, it controls the key entirely and `allowQuery` no longer affects it, but non-allowlisted params are still stripped from the URL the handler receives:
114-
115-
```ts
116-
const handler = defineCachedHandler(myHandler, {
117-
maxAge: 300,
118-
allowQuery: ["color"], // ?color=red&lang=en and ?color=red&lang=de share one entry
119-
});
120-
```
121-
122-
#### Cookies
123-
124-
**By default no cookies participate in caching, in both directions.** This is a secure default: the `Cookie` request header is stripped before the handler runs (so it can never produce cookie-dependent output that gets cached and served to other users), cookies never vary the cache key, and any `Set-Cookie` the handler sets is stripped from the response before it is cached or returned — mirroring how shared caches (CDNs, Varnish) drop `Set-Cookie` on cacheable responses. This prevents a per-request cookie (such as a session id) from ever reaching another user, whether via a later cache hit or a concurrent request coalesced onto the same resolution. The rest of the response is still cached normally. Stored entries carrying a disallowed `Set-Cookie` (e.g. cached before this behavior existed) are likewise rejected on read instead of replayed.
125-
126-
This only applies to cacheable requests (`GET`/`HEAD`). Methods that bypass caching entirely (e.g. `POST`) reach the handler with their request untouched — cookies, headers, query, and body included — and their `Set-Cookie` is passed through.
127-
128-
Set `allowCookies` to an allowlist of cookie names to opt specific cookies back in. Only the listed cookies survive in the `Cookie` header the handler sees, and their name/value pairs vary the cache key — sorted and order-independent, like `allowQuery`, so only the relevant cookie subset is hashed rather than the entire raw `Cookie` header. On the response side, only allowlisted `Set-Cookie`s survive; any others are stripped and the rest of the response is still cached. Cookie names are case-sensitive. `allowCookies` supersedes `varies: ["cookie"]`.
129-
130-
```ts
131-
const handler = defineCachedHandler(myHandler, {
132-
maxAge: 300,
133-
allowCookies: ["theme"], // theme=dark and theme=light cache separately; sid is ignored
134-
});
135-
```
136-
137-
Two caveats:
138-
139-
- **Custom `getKey`.** As with `allowQuery`, a custom `getKey` controls the cache key entirely, so allowlisted cookies no longer vary it automatically — if your handler's output depends on a cookie, incorporate it into `getKey` yourself (the handler-visible `Cookie` header is still filtered to the allowlist regardless).
140-
- **Allowlisted cookies are shared — keep them cache-safe.** An allowlisted cookie participates in caching: it varies the key, and its `Set-Cookie` is cached and replayed to every caller that resolves to the same key (concurrent requests are coalesced into one handler call and share its response). It is your responsibility to only allowlist cookies whose value is safe to share across the users that share a cache key — a `theme`/`locale` preference that is _part of_ the key. **Never allowlist a per-user secret such as a session id**: coalescing plus caching would share that one value across users. A handler that _mints_ a per-request cookie (e.g. initializing an anonymous session with a fresh `Set-Cookie`) must give it a user-specific `getKey`/`varies` so each user keys to a distinct entry — otherwise don't cache it. (With no `allowCookies`, such a cookie is simply stripped, so the default never leaks; this caveat applies only once you opt a cookie back in.)
141-
142-
#### Headers-only Mode
143-
144-
Use `headersOnly` to handle conditional requests without caching the full response:
145-
146-
```ts
147-
const handler = defineCachedHandler(myHandler, {
148-
headersOnly: true,
149-
maxAge: 60,
150-
});
151-
```
152-
153-
#### Private / non-cacheable responses
154-
155-
`defineCachedHandler` honors an explicit `Cache-Control` on the response:
156-
157-
- If the handler sets `Cache-Control: no-store` or `private`, the response is returned to the caller but never written to the cache — the handler runs on every request.
158-
- If the handler sets any other `Cache-Control`, it is preserved verbatim. The synthesized `s-maxage` / `stale-while-revalidate` / `max-age` directives are only added when the handler didn't set a `Cache-Control` of its own.
159-
160-
> [!NOTE]
161-
> This only governs what is **stored**. Concurrent requests are still coalesced by cache key, so per-user responses must be keyed correctly (e.g. via `varies`) — `no-store` / `private` prevents caching, it does not by itself partition the cache key.
162-
163-
#### Server-only caching (`sendCacheControl`)
164-
165-
Sometimes you want to cache a response **in storage** (to save re-computing it) while telling clients and CDNs _not_ to cache it — for example a personalized page that is cheap to serve from your own cache but must always be revalidated by the browser. Reaching for `Cache-Control: no-store`/`private` doesn't work here: those also disqualify the response from storage caching.
166-
167-
Set `sendCacheControl: false` to decouple the two. The response is still stored and served from cache (SWR, `etag`, and `last-modified` are unaffected), but no `Cache-Control` header is synthesized:
168-
169-
```ts
170-
const handler = defineCachedHandler(myHandler, {
171-
maxAge: 60,
172-
swr: true,
173-
sendCacheControl: false, // stored & served from cache, but no Cache-Control sent downstream
174-
});
175-
```
176-
177-
This only governs ocache's own synthesis — a `Cache-Control` the handler sets explicitly is still preserved and sent.
178-
179-
#### Custom cache eligibility (`shouldCache`)
180-
181-
The built-in response validation already rejects `4xx`/`5xx` statuses, `Cache-Control: no-store`/`private`, empty bodies, and responses missing `etag`/`last-modified`. Use `shouldCache` to add your own rejection rule on top — for example to keep `3xx` redirects out of the cache:
182-
183-
```ts
184-
const handler = defineCachedHandler(myHandler, {
185-
maxAge: 60,
186-
// Return false to skip caching this response (it is still returned to the caller).
187-
shouldCache: (res) => res.status < 300 || res.status >= 400,
188-
});
189-
```
190-
191-
`shouldCache` receives the serialized response entry, may be async, and is **ANDed** with the built-in checks — it can only narrow what gets cached, never force-cache a response the built-ins reject. It gates both storing a fresh response and serving a stored one, and a throwing hook fails closed (treated as non-cacheable) and is reported via `onError`.
192-
193-
#### Incremental Static Regeneration (ISR)
194-
195-
you can reproduce a similar ISR behavior with `defineCachedHandler`: serve a cached page instantly, regenerate it in the background after it goes stale, and keep serving the last-good version until the refresh lands:
196-
197-
```ts
198-
const page = defineCachedHandler(
199-
async (event) => {
200-
const html = await renderPage(event.url ?? new URL(event.req.url));
201-
return new Response(html, { headers: { "content-type": "text/html" } });
202-
},
203-
{
204-
swr: true, // serve stale instantly, refresh in the background
205-
maxAge: 60, // "revalidate" window: fresh for 60s, then refresh on next request
206-
// no staleMaxAge → stale is served indefinitely until the refresh succeeds
207-
},
208-
);
209-
```
210-
211-
The two options that make it ISR-like:
212-
213-
- **`swr: true`** turns on stale-while-revalidate: once an entry is older than `maxAge`, the next request gets the stale page immediately while a fresh render runs in the background.
214-
- **Omit `staleMaxAge`.** This is the important part. Leaving it unset means there's no point at which the entry becomes "too old to serve" — the last successful render is served forever until a refresh replaces it, exactly like ISR. (If instead you _set_ `staleMaxAge`, you get a hard cutoff: after `maxAge + staleMaxAge` the entry is dropped and the next request blocks on a fresh render.)
215-
216-
With this config the handler also emits `Cache-Control: s-maxage=60, stale-while-revalidate`, so any shared/CDN cache in front of it revalidates on the same schedule.
217-
218-
**On-demand revalidation** (the equivalent of `revalidatePath` / `revalidateTag`) uses the methods on the returned handler:
219-
220-
```ts
221-
await page.expire(event); // ISR-style: serve the stale page once more, refresh in the background
222-
await page.invalidate(event); // hard purge: next request blocks on a fresh render
223-
```
224-
225-
Prefer `.expire()` for the ISR feel — there's no blocking gap for visitors. Reach for `.invalidate()` only when the next reader must get a guaranteed-fresh render.
226-
227-
**Per-route revalidate windows.** If different pages need different refresh intervals (like Next's per-fetch `revalidate`), use `getMaxAge` to derive the window from the response — for example an `x-revalidate` header your handler sets. `entry.value` is the standard `Response`:
228-
229-
```ts
230-
const page = defineCachedHandler(
231-
async (event) => {
232-
const url = event.url ?? new URL(event.req.url);
233-
const { html, revalidate } = await renderPage(url);
234-
return new Response(html, {
235-
headers: { "content-type": "text/html", "x-revalidate": String(revalidate) },
236-
});
237-
},
238-
{
239-
swr: true,
240-
getMaxAge: (entry) => Number(entry.value.headers.get("x-revalidate")) || 60,
241-
},
242-
);
243-
```
244-
24576
> [!NOTE]
246-
> Two things differ from CDN managed ISR. **(1) Background refresh is coalesced per instance**, not globally — across multiple servers/serverless instances the origin can see one refresh per instance. Add a distributed lock in your [custom storage](#custom-storage) if regeneration is expensive. **(2) Entries never auto-expire** with `staleMaxAge` omitted, so storage grows until you `.invalidate()` — or set a large `staleMaxAge` to trade exact ISR semantics for eventual cleanup.
247-
248-
### Cache Invalidation
249-
250-
Cached functions have an `.invalidate()` method that removes cached entries across all base prefixes:
251-
252-
```ts
253-
import { defineCachedFunction } from "ocache";
254-
255-
const getUser = defineCachedFunction(async (id: string) => db.users.find(id), {
256-
name: "getUser",
257-
maxAge: 60,
258-
getKey: (id: string) => id,
259-
});
260-
261-
const user = await getUser("user-123");
262-
263-
// Invalidate a specific entry
264-
await getUser.invalidate("user-123");
265-
266-
// Next call will re-invoke the function
267-
const freshUser = await getUser("user-123");
268-
```
269-
270-
You can also use the standalone `invalidateCache()` when you don't have a reference to the cached function — just pass the same options:
271-
272-
```ts
273-
import { invalidateCache } from "ocache";
274-
275-
await invalidateCache({
276-
options: { name: "getUser", getKey: (id: string) => id },
277-
args: ["user-123"],
278-
});
279-
```
280-
281-
For advanced use cases, `.resolveKeys()` returns the raw storage keys:
282-
283-
```ts
284-
const keys = await getUser.resolveKeys("user-123");
285-
// ["/cache:functions:getUser:user-123.json"]
286-
```
287-
288-
### Cache Expiration (SWR refresh)
289-
290-
While `.invalidate()` removes an entry entirely (the next call must wait for a fresh value), `.expire()` only marks it as stale. With SWR enabled, stale values keep being served — still bounded by the originally configured `staleMaxAge` window — and the next access triggers a background refresh:
291-
292-
```ts
293-
// Mark the entry stale: next call serves the stale value and refetches in the background
294-
await getUser.expire("user-123");
295-
```
296-
297-
The standalone `expireCache()` works like `invalidateCache()` — pass the same `maxAge` / `swr` / `staleMaxAge` options you cache with so the remaining storage TTL is preserved:
298-
299-
```ts
300-
import { expireCache } from "ocache";
301-
302-
await expireCache({
303-
options: { name: "getUser", getKey: (id: string) => id, maxAge: 60, staleMaxAge: 300 },
304-
args: ["user-123"],
305-
});
306-
```
307-
308-
### Multi-tier Caching
309-
310-
Use an array of `base` prefixes to enable multi-tier caching. On read, each prefix is tried in order and the first hit is used. On write, the entry is written to all prefixes:
311-
312-
```ts
313-
const cachedFetch = defineCachedFunction(
314-
async (url: string) => {
315-
const res = await fetch(url);
316-
return res.json();
317-
},
318-
{
319-
maxAge: 60,
320-
base: ["/tmp", "/cache"],
321-
},
322-
);
323-
```
324-
325-
This is useful for layered cache setups (e.g., fast local cache + shared remote cache) where you want reads to prefer the nearest tier while keeping all tiers populated on writes.
326-
327-
### Custom Storage
328-
329-
By default, ocache uses an in-memory `Map`-based storage. You can provide a custom storage implementation:
330-
331-
```ts
332-
import { setStorage } from "ocache";
333-
import type { StorageInterface } from "ocache";
334-
335-
const redisStorage: StorageInterface = {
336-
get: async (key) => {
337-
return JSON.parse(await redis.get(key));
338-
},
339-
set: async (key, value, opts) => {
340-
// Setting null/undefined deletes the entry (used for cache invalidation)
341-
if (value === null || value === undefined) {
342-
await redis.del(key);
343-
return;
344-
}
345-
await redis.set(key, JSON.stringify(value), opts?.ttl ? { EX: opts.ttl } : undefined);
346-
},
347-
};
348-
349-
setStorage(redisStorage);
350-
```
351-
352-
The built-in memory storage keeps at most `10 000` entries by default, evicting the least-recently-used entries once the ceiling is exceeded (LRU). Pass `maxSize` to change the ceiling, or `Infinity` to disable it and grow unbounded:
353-
354-
```ts
355-
import { createMemoryStorage, setStorage } from "ocache";
356-
357-
setStorage(createMemoryStorage({ maxSize: 10_000 }));
358-
359-
// Opt out of the ceiling entirely (previous unbounded behavior)
360-
setStorage(createMemoryStorage({ maxSize: Infinity }));
361-
```
77+
> Learn more in the [Caching HTTP Handlers](https://ocache.unjs.io/guide/handler) guide, and see [Query Parameters](https://ocache.unjs.io/guide/query-params), [Cookies](https://ocache.unjs.io/guide/cookies), [Cache-Control & Eligibility](https://ocache.unjs.io/guide/cache-control), and [Incremental Static Regeneration](https://ocache.unjs.io/guide/isr).
36278
36379
## API
36480

docs/1.guide/1.index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ title: Getting Started
55

66
# Getting Started
77

8-
ocache is a set of standalone caching utilities. It has zero framework dependencies and works with any runtime that has standard `Request`/`Response`.
8+
ocache is a set of composable caching primitives. It has zero framework dependencies and works with any runtime that has standard `Request`/`Response`.
99

1010
It gives you two primary tools:
1111

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "ocache",
33
"version": "0.1.5",
4-
"description": "Standalone caching utilities with TTL, SWR, and HTTP response caching",
4+
"description": "Composable caching primitives with TTL, SWR, and HTTP response caching",
55
"license": "MIT",
66
"repository": "unjs/ocache",
77
"files": [

0 commit comments

Comments
 (0)