From 32e914fdc19a30c8a304e342e307b15a00169819 Mon Sep 17 00:00:00 2001 From: Benjamin Toby Date: Tue, 17 Mar 2026 21:47:12 +0100 Subject: [PATCH] Add Caching --- README.md | 32 ++++++---- package.json | 1 + src/data/app-data.ts | 5 ++ src/functions/bundler/all-pages-bundler.ts | 3 +- src/functions/cache/get-cache.ts | 26 ++++++++ src/functions/cache/grab-cache-names.ts | 12 ++++ src/functions/cache/trim-all-cache.ts | 24 +++++++ src/functions/cache/trim-cache-key.ts | 60 ++++++++++++++++++ src/functions/cache/write-cache.ts | 62 +++++++++++++++++++ src/functions/init.ts | 3 + src/functions/server/cron.tsx | 9 +++ src/functions/server/server-params-gen.ts | 15 ++++- src/functions/server/start-server.ts | 2 + .../server/web-pages/generate-web-html.tsx | 2 +- .../server/web-pages/handle-web-pages.tsx | 34 ++++++++++ src/types/index.ts | 21 +++++++ src/utils/grab-dir-names.ts | 5 +- 17 files changed, 298 insertions(+), 18 deletions(-) create mode 100644 src/data/app-data.ts create mode 100644 src/functions/cache/get-cache.ts create mode 100644 src/functions/cache/grab-cache-names.ts create mode 100644 src/functions/cache/trim-all-cache.ts create mode 100644 src/functions/cache/trim-cache-key.ts create mode 100644 src/functions/cache/write-cache.ts create mode 100644 src/functions/server/cron.tsx diff --git a/README.md b/README.md index 3ada3ec..40ec0c7 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ A Next.js-style full-stack meta-framework built on [Bun](https://bun.sh) and Rea - [Error Pages](#error-pages) - [Static Files](#static-files) - [Configuration](#configuration) - - [Middleware](#middleware) + - [Middleware](#middleware) - [Environment Variables](#environment-variables) - [How It Works](#how-it-works) - [Development Server](#development-server) @@ -402,7 +402,10 @@ The `ctx` parameter has the same shape as the page `server` function context — Export a `config` object to override the per-route request body limit (default: 10 MB): ```ts -import type { BunextServerRouteConfig, BunxRouteParams } from "bunext/src/types"; +import type { + BunextServerRouteConfig, + BunxRouteParams, +} from "bunext/src/types"; export const config: BunextServerRouteConfig = { maxRequestBodyMB: 50, // allow up to 50 MB @@ -482,15 +485,15 @@ const config: BunextConfig = { export default config; ``` -| Option | Type | Default | Description | -| -------------- | -------------------------------------------------------------------- | ---------------- | -------------------------------------------------- | -| `port` | `number` | `7000` | HTTP server port | -| `origin` | `string` | — | Canonical origin URL | -| `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) | +| Option | Type | Default | Description | +| -------------- | --------------------------------------------------------------------------------- | ---------------- | -------------------------------------------------- | +| `port` | `number` | `7000` | HTTP server port | +| `origin` | `string` | — | Canonical origin URL | +| `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) | ### Middleware @@ -501,7 +504,10 @@ Middleware runs on every request before any routing. Define it in `bunext.config ```ts // bunext.config.ts -import type { BunextConfig, BunextConfigMiddlewareParams } from "bunext/src/types"; +import type { + BunextConfig, + BunextConfigMiddlewareParams, +} from "bunext/src/types"; const config: BunextConfig = { middleware: async ({ req, url, server }) => { @@ -631,6 +637,6 @@ Request Server-rendered HTML includes: - `window.__PAGE_PROPS__` — the serialized server function return value, read by `hydrateRoot` on the client. -- A `\n`; if (bundledMap?.path) { - html += ` \n`; + html += ` \n`; } if (isDevelopment()) { diff --git a/src/functions/server/web-pages/handle-web-pages.tsx b/src/functions/server/web-pages/handle-web-pages.tsx index cb0b741..cb48f26 100644 --- a/src/functions/server/web-pages/handle-web-pages.tsx +++ b/src/functions/server/web-pages/handle-web-pages.tsx @@ -1,5 +1,7 @@ import type { GrabPageComponentRes } from "../../../types"; import isDevelopment from "../../../utils/is-development"; +import getCache from "../../cache/get-cache"; +import writeCache from "../../cache/write-cache"; import genWebHTML from "./generate-web-html"; import grabPageComponent from "./grab-page-component"; import grabPageErrorComponent from "./grab-page-error-component"; @@ -12,6 +14,24 @@ export default async function handleWebPages({ req, }: Params): Promise { try { + if (!isDevelopment()) { + const url = new URL(req.url); + const key = url.pathname + (url.search || ""); + + const existing_cache = getCache({ key, paradigm: "html" }); + + if (existing_cache) { + const res_opts: ResponseInit = { + headers: { + "Content-Type": "text/html", + "X-Bunext-Cache": "HIT", + }, + }; + + return new Response(existing_cache, res_opts); + } + } + const componentRes = await grabPageComponent({ req }); return await generateRes(componentRes); } catch (error: any) { @@ -65,6 +85,20 @@ async function generateRes({ }; } + const cache_page = + module.config?.cachePage || serverRes?.cachePage || false; + const expiry_seconds = module.config?.cacheExpiry || serverRes?.cacheExpiry; + + if (cache_page && routeParams?.url) { + const key = routeParams.url.pathname + (routeParams.url.search || ""); + writeCache({ + key, + value: html, + paradigm: "html", + expiry_seconds, + }); + } + const res = new Response(html, res_opts); if (routeParams?.resTransform) { diff --git a/src/types/index.ts b/src/types/index.ts index 45ff930..19e3fa9 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -51,6 +51,7 @@ export type BunextConfig = { middleware?: ( params: BunextConfigMiddlewareParams, ) => Promise | Response | undefined; + defaultCacheExpiry?: number; }; export type BunextConfigMiddlewareParams = { @@ -157,6 +158,7 @@ export type BunextPageModule = { server?: BunextPageServerFn; meta?: BunextPageModuleMeta | BunextPageModuleMetaFn; Head?: FC; + config?: BunextRouteConfig; }; export type BunextPageModuleMetaFn = (params: { @@ -197,6 +199,14 @@ export type BunextPageServerFn< ctx: Omit, ) => Promise>; +export type BunextRouteConfig = { + cachePage?: boolean; + /** + * Expiry time of the cache in seconds + */ + cacheExpiry?: number; +}; + export type BunextPageModuleServerReturn< T extends { [k: string]: any } = { [k: string]: any }, Q extends { [k: string]: any } = { [k: string]: any }, @@ -205,6 +215,11 @@ export type BunextPageModuleServerReturn< query?: Q; redirect?: BunextPageModuleServerRedirect; responseOptions?: ResponseInit; + cachePage?: boolean; + /** + * Expiry time of the cache in seconds + */ + cacheExpiry?: number; }; export type BunextPageModuleServerRedirect = { @@ -250,3 +265,9 @@ export type GlobalHMRControllerObject = { page_url: string; target_map?: BundlerCTXMap; }; + +export type BunextCacheFileMeta = { + date_created: number; + paradigm: "html" | "json"; + expiry_seconds?: number; +}; diff --git a/src/utils/grab-dir-names.ts b/src/utils/grab-dir-names.ts index a51da8e..106dc70 100644 --- a/src/utils/grab-dir-names.ts +++ b/src/utils/grab-dir-names.ts @@ -6,7 +6,9 @@ export default function grabDirNames() { const PAGES_DIR = path.join(SRC_DIR, "pages"); const API_DIR = path.join(PAGES_DIR, "api"); const PUBLIC_DIR = path.join(ROOT_DIR, "public"); - const HYDRATION_DST_DIR = path.join(PUBLIC_DIR, "pages"); + const BUNEXT_PUBLIC_DIR = path.join(PUBLIC_DIR, "__bunext"); + const HYDRATION_DST_DIR = path.join(BUNEXT_PUBLIC_DIR, "pages"); + const BUNEXT_CACHE_DIR = path.join(BUNEXT_PUBLIC_DIR, "cache"); const HYDRATION_DST_DIR_MAP_JSON_FILE = path.join( HYDRATION_DST_DIR, "map.json", @@ -54,5 +56,6 @@ export default function grabDirNames() { BUNX_ROOT_404_PRESET_COMPONENT, BUNX_ROOT_404_FILE_NAME, HYDRATION_DST_DIR_MAP_JSON_FILE, + BUNEXT_CACHE_DIR, }; }