Add Caching

This commit is contained in:
2026-03-17 21:47:12 +01:00
parent 35930857fd
commit 32e914fdc1
17 changed files with 298 additions and 18 deletions
+5
View File
@@ -0,0 +1,5 @@
export const AppData = {
DefaultCacheExpiryTimeSeconds: 60 * 60,
DefaultCronInterval: 30000,
BunextStaticFilesCacheExpiry: 60 * 60 * 24 * 7,
} as const;
+2 -1
View File
@@ -165,7 +165,7 @@ export default async function allPagesBundler(params?: Params) {
entryPoints: Object.keys(virtualEntries).map((k) => `virtual:${k}`),
outdir: HYDRATION_DST_DIR,
bundle: true,
minify: !dev,
minify: true,
format: "esm",
target: "es2020",
platform: "browser",
@@ -178,6 +178,7 @@ export default async function allPagesBundler(params?: Params) {
metafile: true,
plugins: [tailwindPlugin, virtualPlugin, artifactTracker],
jsx: "automatic",
splitting: true,
});
await ctx.rebuild();
+26
View File
@@ -0,0 +1,26 @@
import { readFileSync } from "fs";
import grabDirNames from "../../utils/grab-dir-names";
import grabCacheNames from "./grab-cache-names";
import path from "path";
type Params = {
key: string;
paradigm?: "html" | "json";
};
export default function getCache({ key, paradigm }: Params) {
try {
const { BUNEXT_CACHE_DIR } = grabDirNames();
const { cache_name } = grabCacheNames({ key, paradigm });
const content = readFileSync(
path.join(BUNEXT_CACHE_DIR, cache_name),
"utf-8",
);
return content;
} catch (error) {
return undefined;
}
}
+12
View File
@@ -0,0 +1,12 @@
type Params = {
key: string;
paradigm?: "html" | "json";
};
export default function grabCacheNames({ key, paradigm = "html" }: Params) {
const parsed_key = key.replace(/\//g, "-");
const cache_name = `${parsed_key}.res.${paradigm}`;
const cache_meta_name = `${parsed_key}.meta.json`;
return { cache_name, cache_meta_name };
}
+24
View File
@@ -0,0 +1,24 @@
import { readdirSync } from "fs";
import grabDirNames from "../../utils/grab-dir-names";
import trimCacheKey from "./trim-cache-key";
export default async function trimAllCache() {
try {
const { BUNEXT_CACHE_DIR } = grabDirNames();
const cached_items = readdirSync(BUNEXT_CACHE_DIR);
for (let i = 0; i < cached_items.length; i++) {
const cached_item = cached_items[i];
if (!cached_item.endsWith(`.meta.json`)) continue;
const cache_key = cached_item.replace(/\.meta\.json/, "");
const trim_key = await trimCacheKey({
key: cache_key,
});
}
} catch (error) {
return undefined;
}
}
+60
View File
@@ -0,0 +1,60 @@
import { readFileSync, unlinkSync } from "fs";
import grabDirNames from "../../utils/grab-dir-names";
import grabCacheNames from "./grab-cache-names";
import path from "path";
import type { APIResponseObject, BunextCacheFileMeta } from "../../types";
import { AppData } from "../../data/app-data";
type Params = {
key: string;
};
export default async function trimCacheKey({
key,
}: Params): Promise<APIResponseObject> {
try {
const { BUNEXT_CACHE_DIR } = grabDirNames();
const { cache_name, cache_meta_name } = grabCacheNames({
key,
});
const config = global.CONFIG;
const default_expiry_time_seconds =
config.defaultCacheExpiry ||
AppData["DefaultCacheExpiryTimeSeconds"];
const default_expiry_time_milliseconds =
default_expiry_time_seconds * 1000;
const cache_content_path = path.join(BUNEXT_CACHE_DIR, cache_name);
const cache_meta_path = path.join(BUNEXT_CACHE_DIR, cache_meta_name);
const cache_meta: BunextCacheFileMeta = JSON.parse(
readFileSync(cache_meta_path, "utf-8"),
);
const expiry_milliseconds = cache_meta.expiry_seconds
? cache_meta.expiry_seconds * 1000
: default_expiry_time_milliseconds;
if (Date.now() - cache_meta.date_created < expiry_milliseconds) {
return {
success: false,
msg: `Cache has not expired yet`,
};
}
unlinkSync(cache_content_path);
unlinkSync(cache_meta_path);
return {
success: true,
};
} catch (error: any) {
return {
success: false,
msg: `Trim cache key ERROR: ${error.message}`,
};
}
}
+62
View File
@@ -0,0 +1,62 @@
import { existsSync, writeFileSync } from "fs";
import grabDirNames from "../../utils/grab-dir-names";
import grabCacheNames from "./grab-cache-names";
import type { APIResponseObject, BunextCacheFileMeta } from "../../types";
import path from "path";
type Params = {
key: string;
value: string;
paradigm?: "html" | "json";
expiry_seconds?: number;
};
export default async function writeCache({
key,
value,
paradigm = "html",
expiry_seconds,
}: Params): Promise<APIResponseObject> {
try {
const { BUNEXT_CACHE_DIR } = grabDirNames();
const { cache_meta_name, cache_name } = grabCacheNames({
key,
paradigm,
});
const target_path = path.join(BUNEXT_CACHE_DIR, cache_name);
if (existsSync(target_path)) {
return {
success: false,
msg: `Cache entry already exists`,
};
}
writeFileSync(path.join(target_path), value);
const cache_file_meta: BunextCacheFileMeta = {
date_created: Date.now(),
paradigm,
};
if (expiry_seconds) {
cache_file_meta.expiry_seconds = expiry_seconds;
}
writeFileSync(
path.join(BUNEXT_CACHE_DIR, cache_meta_name),
JSON.stringify(cache_file_meta),
);
return {
success: true,
};
} catch (error: any) {
return {
success: false,
msg: error.message,
};
}
}
+3
View File
@@ -1,9 +1,12 @@
import { existsSync, mkdirSync, statSync, writeFileSync } from "fs";
import grabDirNames from "../utils/grab-dir-names";
import { execSync } from "child_process";
export default async function () {
const dirNames = grabDirNames();
execSync(`rm -rf ${dirNames.BUNEXT_CACHE_DIR}`);
const keys = Object.keys(dirNames) as (keyof ReturnType<
typeof grabDirNames
>)[];
+9
View File
@@ -0,0 +1,9 @@
import { AppData } from "../../data/app-data";
import trimAllCache from "../cache/trim-all-cache";
export default async function cron() {
while (true) {
await trimAllCache();
await Bun.sleep(AppData["DefaultCronInterval"]);
}
}
+13 -2
View File
@@ -6,6 +6,7 @@ import handleWebPages from "./web-pages/handle-web-pages";
import handleRoutes from "./handle-routes";
import isDevelopment from "../../utils/is-development";
import grabConstants from "../../utils/grab-constants";
import { AppData } from "../../data/app-data";
type Params = {
dev?: boolean;
@@ -15,6 +16,8 @@ export default async function (params?: Params): Promise<ServeOptions> {
const port = grabAppPort();
const { PUBLIC_DIR } = grabDirNames();
const is_dev = isDevelopment();
return {
async fetch(req, server) {
try {
@@ -34,7 +37,7 @@ export default async function (params?: Params): Promise<ServeOptions> {
}
}
if (url.pathname === "/__hmr" && isDevelopment()) {
if (url.pathname === "/__hmr" && is_dev) {
const referer_url = new URL(
req.headers.get("referer") || "",
);
@@ -95,7 +98,15 @@ export default async function (params?: Params): Promise<ServeOptions> {
),
);
return new Response(file);
let res_opts: ResponseInit = {};
if (!is_dev && url.pathname.match(/__bunext/)) {
res_opts.headers = {
"Cache-Control": `public, max-age=${AppData["BunextStaticFilesCacheExpiry"]}, must-revalidate`,
};
}
return new Response(file, res_opts);
}
if (url.pathname.startsWith("/favicon.")) {
+2
View File
@@ -8,6 +8,7 @@ import grabDirNames from "../../utils/grab-dir-names";
import EJSON from "../../utils/ejson";
import { readFileSync } from "fs";
import type { BundlerCTXMap } from "../../types";
import cron from "./cron";
const { HYDRATION_DST_DIR_MAP_JSON_FILE } = grabDirNames();
@@ -36,6 +37,7 @@ export default async function startServer(params?: Params) {
}
global.BUNDLER_CTX_MAP = artifacts;
global.IS_FIRST_BUNDLE_READY = true;
cron();
}
let bundle_ready_retries = 0;
@@ -46,7 +46,7 @@ export default async function genWebHTML({
}</script>\n`;
if (bundledMap?.path) {
html += ` <script src="/${bundledMap.path}" type="module" defer></script>\n`;
html += ` <script src="/${bundledMap.path}" type="module" async></script>\n`;
}
if (isDevelopment()) {
@@ -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<Response> {
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) {
+21
View File
@@ -51,6 +51,7 @@ export type BunextConfig = {
middleware?: (
params: BunextConfigMiddlewareParams,
) => Promise<Response | undefined> | Response | undefined;
defaultCacheExpiry?: number;
};
export type BunextConfigMiddlewareParams = {
@@ -157,6 +158,7 @@ export type BunextPageModule = {
server?: BunextPageServerFn;
meta?: BunextPageModuleMeta | BunextPageModuleMetaFn;
Head?: FC<BunextPageHeadFCProps>;
config?: BunextRouteConfig;
};
export type BunextPageModuleMetaFn = (params: {
@@ -197,6 +199,14 @@ export type BunextPageServerFn<
ctx: Omit<BunxRouteParams, "body">,
) => Promise<BunextPageModuleServerReturn<T>>;
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;
};
+4 -1
View File
@@ -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,
};
}