Bugfix Caching
This commit is contained in:
parent
32e914fdc1
commit
f6db3ab866
93
README.md
93
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)
|
- [Route Config (Body Size Limit)](#route-config-body-size-limit)
|
||||||
- [Error Pages](#error-pages)
|
- [Error Pages](#error-pages)
|
||||||
- [Static Files](#static-files)
|
- [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)
|
- [Configuration](#configuration)
|
||||||
- [Middleware](#middleware)
|
- [Middleware](#middleware)
|
||||||
- [Environment Variables](#environment-variables)
|
- [Environment Variables](#environment-variables)
|
||||||
@ -135,8 +140,10 @@ my-app/
|
|||||||
│ └── api/
|
│ └── api/
|
||||||
│ └── users.ts # API route: /api/users
|
│ └── users.ts # API route: /api/users
|
||||||
├── public/ # Static files and bundler output
|
├── public/ # Static files and bundler output
|
||||||
│ └── pages/ # Generated by bundler (do not edit manually)
|
│ └── __bunext/
|
||||||
│ └── map.json # Artifact map used by production server
|
│ ├── 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
|
├── bunext.config.ts # Optional configuration
|
||||||
├── tsconfig.json
|
├── tsconfig.json
|
||||||
└── package.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
|
## Configuration
|
||||||
|
|
||||||
Create a `bunext.config.ts` file in your project root to configure Bunext:
|
Create a `bunext.config.ts` file in your project root to configure Bunext:
|
||||||
@ -493,6 +575,7 @@ export default config;
|
|||||||
| `assetsPrefix` | `string` | `_bunext/static` | URL prefix for static assets |
|
| `assetsPrefix` | `string` | `_bunext/static` | URL prefix for static assets |
|
||||||
| `globalVars` | `{ [k: string]: any }` | — | Variables injected globally at build time |
|
| `globalVars` | `{ [k: string]: any }` | — | Variables injected globally at build time |
|
||||||
| `development` | `boolean` | — | Overridden to `true` by `bunext dev` automatically |
|
| `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` | `(params: BunextConfigMiddlewareParams) => Response \| undefined \| Promise<...>` | — | Global middleware — see [Middleware](#middleware) |
|
||||||
|
|
||||||
### Middleware
|
### Middleware
|
||||||
@ -623,6 +706,9 @@ Request
|
|||||||
├── /favicon.* → Serve favicon from public/
|
├── /favicon.* → Serve favicon from public/
|
||||||
│
|
│
|
||||||
└── Everything else → Server-side render a page
|
└── 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
|
1. Match route via FileSystemRouter
|
||||||
2. Find bundled artifact in BUNDLER_CTX_MAP
|
2. Find bundled artifact in BUNDLER_CTX_MAP
|
||||||
3. Import page module (with cache-busting timestamp in dev)
|
3. Import page module (with cache-busting timestamp in dev)
|
||||||
@ -630,7 +716,8 @@ Request
|
|||||||
5. Resolve meta (static object or async function)
|
5. Resolve meta (static object or async function)
|
||||||
6. renderToString(component) → inject into HTML template
|
6. renderToString(component) → inject into HTML template
|
||||||
7. Inject window.__PAGE_PROPS__, hydration <script>, CSS <link>
|
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
|
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) {
|
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_name = `${parsed_key}.res.${paradigm}`;
|
||||||
const cache_meta_name = `${parsed_key}.meta.json`;
|
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];
|
const cached_item = cached_items[i];
|
||||||
if (!cached_item.endsWith(`.meta.json`)) continue;
|
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({
|
const trim_key = await trimCacheKey({
|
||||||
key: cache_key,
|
key: cache_key,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user