Bugfix Caching
This commit is contained in:
parent
32e914fdc1
commit
f6db3ab866
97
README.md
97
README.md
@ -25,6 +25,11 @@ A Next.js-style full-stack meta-framework built on [Bun](https://bun.sh) and Rea
|
||||
- [Route Config (Body Size Limit)](#route-config-body-size-limit)
|
||||
- [Error Pages](#error-pages)
|
||||
- [Static Files](#static-files)
|
||||
- [Caching](#caching)
|
||||
- [Enabling Cache Per Page](#enabling-cache-per-page)
|
||||
- [Dynamic Cache Control from Server Function](#dynamic-cache-control-from-server-function)
|
||||
- [Cache Expiry](#cache-expiry)
|
||||
- [Cache Behavior and Limitations](#cache-behavior-and-limitations)
|
||||
- [Configuration](#configuration)
|
||||
- [Middleware](#middleware)
|
||||
- [Environment Variables](#environment-variables)
|
||||
@ -135,8 +140,10 @@ my-app/
|
||||
│ └── api/
|
||||
│ └── users.ts # API route: /api/users
|
||||
├── public/ # Static files and bundler output
|
||||
│ └── pages/ # Generated by bundler (do not edit manually)
|
||||
│ └── map.json # Artifact map used by production server
|
||||
│ └── __bunext/
|
||||
│ ├── pages/ # Generated by bundler (do not edit manually)
|
||||
│ │ └── map.json # Artifact map used by production server
|
||||
│ └── cache/ # File-based HTML cache (production only)
|
||||
├── bunext.config.ts # Optional configuration
|
||||
├── tsconfig.json
|
||||
└── package.json
|
||||
@ -463,6 +470,81 @@ public/
|
||||
|
||||
---
|
||||
|
||||
## Caching
|
||||
|
||||
Bunext includes a file-based HTML cache for production. Caching is **disabled in development** — every request renders fresh. In production, a cron job runs every 30 seconds to delete expired cache entries.
|
||||
|
||||
Cache files are stored in `public/__bunext/cache/`. Each cached page produces two files:
|
||||
|
||||
| File | Contents |
|
||||
|---------------------------|----------------------------------------------|
|
||||
| `<key>.res.html` | The cached HTML response body |
|
||||
| `<key>.meta.json` | Metadata: creation timestamp, expiry, paradigm |
|
||||
|
||||
The cache is **cold on first request**: the first visitor triggers a full server render and writes the cache. Every subsequent request within the expiry window receives the cached HTML directly, bypassing the server function, component render, and bundler lookup. A cache hit is indicated by the response header `X-Bunext-Cache: HIT`.
|
||||
|
||||
### Enabling Cache Per Page
|
||||
|
||||
Export a `config` object from a page file to opt that page into caching:
|
||||
|
||||
```tsx
|
||||
// src/pages/products.tsx
|
||||
import type { BunextRouteConfig } from "bunext/src/types";
|
||||
|
||||
export const config: BunextRouteConfig = {
|
||||
cachePage: true,
|
||||
cacheExpiry: 300, // seconds — optional, overrides the global default
|
||||
};
|
||||
|
||||
export default function ProductsPage() {
|
||||
return <h1>Products</h1>;
|
||||
}
|
||||
```
|
||||
|
||||
### Dynamic Cache Control from Server Function
|
||||
|
||||
Cache settings can also be returned from the `server` function, which lets you conditionally enable caching based on request data:
|
||||
|
||||
```tsx
|
||||
import type { BunextPageServerFn } from "bunext/src/types";
|
||||
|
||||
export const server: BunextPageServerFn = async (ctx) => {
|
||||
const data = await fetchProducts();
|
||||
|
||||
return {
|
||||
props: { data },
|
||||
cachePage: true,
|
||||
cacheExpiry: 600, // 10 minutes
|
||||
};
|
||||
};
|
||||
|
||||
export default function ProductsPage({ props }: any) {
|
||||
return <ul>{props.data.map((p: any) => <li key={p.id}>{p.name}</li>)}</ul>;
|
||||
}
|
||||
```
|
||||
|
||||
If both `module.config.cachePage` and `serverRes.cachePage` are set, `module.config` takes precedence.
|
||||
|
||||
### Cache Expiry
|
||||
|
||||
Expiry resolution order (first truthy value wins):
|
||||
|
||||
1. `cacheExpiry` on the page `config` export or `server` return (per-page, in seconds)
|
||||
2. `defaultCacheExpiry` in `bunext.config.ts` (global default, in seconds)
|
||||
3. Built-in default: **3600 seconds (1 hour)**
|
||||
|
||||
The cron job checks all cache entries every 30 seconds and deletes any whose age exceeds their expiry. Static bundled assets (JS/CSS in `public/__bunext/`) receive a separate HTTP `Cache-Control: public, max-age=604800` header (7 days) via the browser cache — this is independent of the page HTML cache.
|
||||
|
||||
### Cache Behavior and Limitations
|
||||
|
||||
- **Production only.** Caching never activates in development (`bunext dev`).
|
||||
- **Cold start required.** The cache is populated on the first request; there is no pre-warming step.
|
||||
- **Immutable within the expiry window.** Once a page is cached, `writeCache` skips all subsequent write attempts for that key until the cron job deletes the expired entry. There is no manual invalidation API.
|
||||
- **Cache is not cleared on rebuild.** Deploying a new build does not automatically flush `public/__bunext/cache/`. Stale HTML files referencing old JS bundles can be served until they expire. Clear the cache directory as part of your deploy process if needed.
|
||||
- **Key collision with dashes.** Cache keys are derived by replacing every `/` in the URL path with `-`. This means `/foo/bar` and `/foo-bar` produce the same cache filename and will share a cache entry. Avoid enabling `cachePage` on routes where a nested path and a dash-separated path could collide.
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
Create a `bunext.config.ts` file in your project root to configure Bunext:
|
||||
@ -492,8 +574,9 @@ export default config;
|
||||
| `distDir` | `string` | `.bunext` | Internal artifact directory |
|
||||
| `assetsPrefix` | `string` | `_bunext/static` | URL prefix for static assets |
|
||||
| `globalVars` | `{ [k: string]: any }` | — | Variables injected globally at build time |
|
||||
| `development` | `boolean` | — | Overridden to `true` by `bunext dev` automatically |
|
||||
| `middleware` | `(params: BunextConfigMiddlewareParams) => Response \| undefined \| Promise<...>` | — | Global middleware — see [Middleware](#middleware) |
|
||||
| `development` | `boolean` | — | Overridden to `true` by `bunext dev` automatically |
|
||||
| `defaultCacheExpiry`| `number` | `3600` | Global page cache expiry in seconds |
|
||||
| `middleware` | `(params: BunextConfigMiddlewareParams) => Response \| undefined \| Promise<...>` | — | Global middleware — see [Middleware](#middleware) |
|
||||
|
||||
### Middleware
|
||||
|
||||
@ -623,6 +706,9 @@ Request
|
||||
├── /favicon.* → Serve favicon from public/
|
||||
│
|
||||
└── Everything else → Server-side render a page
|
||||
[Production only] Check public/__bunext/cache/ for key = pathname + search
|
||||
Cache HIT → return cached HTML with X-Bunext-Cache: HIT header
|
||||
Cache MISS → continue ↓
|
||||
1. Match route via FileSystemRouter
|
||||
2. Find bundled artifact in BUNDLER_CTX_MAP
|
||||
3. Import page module (with cache-busting timestamp in dev)
|
||||
@ -630,7 +716,8 @@ Request
|
||||
5. Resolve meta (static object or async function)
|
||||
6. renderToString(component) → inject into HTML template
|
||||
7. Inject window.__PAGE_PROPS__, hydration <script>, CSS <link>
|
||||
8. Return HTML response
|
||||
8. If module.config.cachePage or serverRes.cachePage → write to cache
|
||||
9. Return HTML response
|
||||
On error → render 404 or 500 error page
|
||||
```
|
||||
|
||||
|
||||
2
src/functions/cache/grab-cache-names.ts
vendored
2
src/functions/cache/grab-cache-names.ts
vendored
@ -4,7 +4,7 @@ type Params = {
|
||||
};
|
||||
|
||||
export default function grabCacheNames({ key, paradigm = "html" }: Params) {
|
||||
const parsed_key = key.replace(/\//g, "-");
|
||||
const parsed_key = encodeURIComponent(key);
|
||||
const cache_name = `${parsed_key}.res.${paradigm}`;
|
||||
const cache_meta_name = `${parsed_key}.meta.json`;
|
||||
|
||||
|
||||
2
src/functions/cache/trim-all-cache.ts
vendored
2
src/functions/cache/trim-all-cache.ts
vendored
@ -12,7 +12,7 @@ export default async function trimAllCache() {
|
||||
const cached_item = cached_items[i];
|
||||
if (!cached_item.endsWith(`.meta.json`)) continue;
|
||||
|
||||
const cache_key = cached_item.replace(/\.meta\.json/, "");
|
||||
const cache_key = decodeURIComponent(cached_item.replace(/\.meta\.json/, ""));
|
||||
|
||||
const trim_key = await trimCacheKey({
|
||||
key: cache_key,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user