You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: AGENTS.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1,6 +1,6 @@
1
1
# ocache
2
2
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`.
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.
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.
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).
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).
`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 =awaitrenderPage(event.url??newURL(event.req.url));
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
-
awaitpage.expire(event); // ISR-style: serve the stale page once more, refresh in the background
222
-
awaitpage.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`:
> 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:
For advanced use cases, `.resolveKeys()` returns the raw storage keys:
282
-
283
-
```ts
284
-
const keys =awaitgetUser.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
-
awaitgetUser.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:
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 =awaitfetch(url);
316
-
returnres.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
-
importtype { StorageInterface } from"ocache";
334
-
335
-
const redisStorage:StorageInterface= {
336
-
get: async (key) => {
337
-
returnJSON.parse(awaitredis.get(key));
338
-
},
339
-
set: async (key, value, opts) => {
340
-
// Setting null/undefined deletes the entry (used for cache invalidation)
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:
> 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).
Copy file name to clipboardExpand all lines: docs/1.guide/1.index.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -5,7 +5,7 @@ title: Getting Started
5
5
6
6
# Getting Started
7
7
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`.
0 commit comments