fix brotli cache

This commit is contained in:
Rizky 2024-06-26 16:32:24 +07:00
parent 2b9c84270f
commit d781756ab9
8 changed files with 123 additions and 47 deletions

View File

@ -13,7 +13,7 @@ export const _ = {
const { req, res } = apiContext(this); const { req, res } = apiContext(this);
res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Headers", "content-type"); res.setHeader("Access-Control-Allow-Headers", "content-type");
const gz = g.deploy.gz; const gz = g.deploy.content;
const parts = req.params._.split("/"); const parts = req.params._.split("/");
const action = { const action = {

View File

@ -1,7 +1,6 @@
import brotliPromise from "brotli-wasm"; // Import the default export
import { simpleHash } from "utils/cache"; import { simpleHash } from "utils/cache";
import { g } from "utils/global"; import { g } from "utils/global";
const brotli = await brotliPromise; import { loadCachedBr } from "utils/br-load";
const parseQueryParams = (ctx: any) => { const parseQueryParams = (ctx: any) => {
const pageHref = ctx.req.url; const pageHref = ctx.req.url;
@ -29,7 +28,7 @@ export const apiContext = (ctx: any) => {
res: { res: {
...ctx.res, ...ctx.res,
send: (body, cache_accept?: string) => { send: (body, cache_accept?: string) => {
ctx.res = createResponse(ctx.res, body, cache_accept); ctx.res = createResponse(body, { cache_accept, res: ctx.res });
}, },
sendStatus: (code: number) => { sendStatus: (code: number) => {
ctx.res._status = code; ctx.res._status = code;
@ -50,30 +49,45 @@ export const apiContext = (ctx: any) => {
}; };
export const createResponse = ( export const createResponse = (
existingRes: any,
body: any, body: any,
cache_accept?: string opt?: {
cache_accept?: string;
headers?: any;
res?: any;
}
) => { ) => {
const status = const status =
typeof existingRes._status === "number" ? existingRes._status : undefined; typeof opt?.res?._status === "number" ? opt?.res?._status : undefined;
let content: any = typeof body === "string" ? body : JSON.stringify(body); let content: any = typeof body === "string" ? body : JSON.stringify(body);
const headers = {} as Record<string, string>; const headers = { ...(opt?.headers || {}) } as Record<string, string>;
if (cache_accept) { if (opt?.cache_accept) {
if (g.mode === "prod" && cache_accept.toLowerCase().includes("br")) { let cached = false;
if (opt.cache_accept.toLowerCase().includes("br")) {
const content_hash = simpleHash(content); const content_hash = simpleHash(content);
if (!g.cache.br[content_hash]) {
loadCachedBr(content_hash, content);
}
if (g.cache.br[content_hash]) { if (g.cache.br[content_hash]) {
cached = true;
content = g.cache.br[content_hash]; content = g.cache.br[content_hash];
headers["content-encoding"] = "br"; headers["content-encoding"] = "br";
} else { }
if (!g.cache.br_timeout.has(content_hash)) { }
g.cache.br_timeout.add(content_hash);
setTimeout(() => { if (!cached && opt.cache_accept.toLowerCase().includes("gz")) {
g.cache.br[content_hash] = brotli.compress(Buffer.from(content)); const content_hash = simpleHash(content);
g.cache.br_timeout.delete(content_hash);
}); if (!g.cache.gz[content_hash]) {
} g.cache.gz[content_hash] = Bun.gzipSync(content);
}
if (g.cache.gz[content_hash]) {
cached = true;
content = g.cache.gz[content_hash];
headers["content-encoding"] = "gzip";
} }
} }
} }
@ -90,10 +104,12 @@ export const createResponse = (
for (const [k, v] of Object.entries(headers)) { for (const [k, v] of Object.entries(headers)) {
res.headers.append(k, v); res.headers.append(k, v);
} }
const cur = existingRes as Response; const cur = opt?.res as Response;
cur.headers.forEach((value, key) => { if (cur) {
res.headers.append(key, value); cur.headers.forEach((value, key) => {
}); res.headers.append(key, value);
});
}
if (typeof body === "object" && !res.headers.has("content-type")) { if (typeof body === "object" && !res.headers.has("content-type")) {
res.headers.append("content-type", "application/json"); res.headers.append("content-type", "application/json");

View File

@ -14,7 +14,12 @@ export const createServer = async () => {
g.api = {}; g.api = {};
g.cache = { g.cache = {
br: {}, br: {},
br_timeout: new Set(), gz: {},
br_progress: {
pending: {},
running: false,
timeout: null,
},
}; };
const scan = async (path: string, root?: string) => { const scan = async (path: string, root?: string) => {
const apis = await listAsync(path); const apis = await listAsync(path);
@ -71,7 +76,7 @@ export const createServer = async () => {
maxRequestBodySize: 1024 * 1024 * 128, maxRequestBodySize: 1024 * 1024 * 128,
async fetch(req) { async fetch(req) {
const url = new URL(req.url) as URL; const url = new URL(req.url) as URL;
url.pathname = url.pathname.replace(/\/+/g, '/') url.pathname = url.pathname.replace(/\/+/g, "/");
const prasi = {}; const prasi = {};
const index = prodIndex(g.deploy.config.site_id, prasi); const index = prodIndex(g.deploy.config.site_id, prasi);
@ -89,12 +94,13 @@ export const createServer = async () => {
return await serveWeb({ return await serveWeb({
content: index.render(), content: index.render(),
pathname: "index.html", pathname: "index.html",
cache_accept: req.headers.get("accept-encoding") || "",
}); });
} }
if (g.deploy.gz) { if (g.deploy.content) {
const core = g.deploy.gz.code.core; const core = g.deploy.content.code.core;
const site = g.deploy.gz.code.site; const site = g.deploy.content.code.site;
let pathname = url.pathname; let pathname = url.pathname;
if (url.pathname[0] === "/") pathname = pathname.substring(1); if (url.pathname[0] === "/") pathname = pathname.substring(1);
@ -107,6 +113,7 @@ export const createServer = async () => {
return await serveWeb({ return await serveWeb({
content: index.render(), content: index.render(),
pathname: "index.html", pathname: "index.html",
cache_accept: req.headers.get("accept-encoding") || "",
}); });
} }
@ -116,7 +123,11 @@ export const createServer = async () => {
else if (site[pathname]) content = site[pathname]; else if (site[pathname]) content = site[pathname];
if (content) { if (content) {
return await serveWeb({ content, pathname }); return await serveWeb({
content,
pathname,
cache_accept: req.headers.get("accept-encoding") || "",
});
} }
} }
} }

View File

@ -34,7 +34,7 @@ export const serveAPI = async (url: URL, req: Request) => {
} }
} }
} else { } else {
for (const [k, v] of Object.entries(json)) { for (const [k, v] of Object.entries(json as object)) {
params[k] = v; params[k] = v;
} }
for (const [k, v] of Object.entries(params)) { for (const [k, v] of Object.entries(params)) {

View File

@ -1,9 +1,14 @@
import mime from "mime"; import mime from "mime";
import { createResponse } from "service-srv";
export const serveWeb = async (arg: { pathname: string; content: string }) => { export const serveWeb = async (arg: {
pathname: string;
content: string;
cache_accept: string;
}) => {
const type = mime.getType(arg.pathname); const type = mime.getType(arg.pathname);
return createResponse(arg.content, {
return new Response(arg.content, { cache_accept: arg.cache_accept,
headers: !type ? undefined : { "content-type": type }, headers: !type ? undefined : { "content-type": type },
}); });
}; };

43
pkgs/utils/br-load.ts Normal file
View File

@ -0,0 +1,43 @@
import brotliPromise from "brotli-wasm"; // Import the default export
import { g } from "./global";
import { dir } from "./dir";
const encoder = new TextEncoder();
const brotli = await brotliPromise;
export const loadCachedBr = (hash: string, content: string) => {
if (!g.cache.br[hash]) {
if (!g.cache.br_progress.pending[hash]) {
g.cache.br_progress.pending[hash] = content;
recurseCompressBr();
}
}
};
const recurseCompressBr = () => {
clearTimeout(g.cache.br_progress.timeout);
g.cache.br_progress.timeout = setTimeout(async () => {
if (g.cache.br_progress.running) {
return;
}
g.cache.br_progress.running = true;
const entries = Object.entries(g.cache.br_progress.pending);
if (entries.length > 0) {
const [hash, content] = entries.shift() as [string, string | Uint8Array];
const file = Bun.file(dir(`${g.datadir}/br-cache/${hash}`));
if (await file.exists()) {
g.cache.br[hash] = new Uint8Array(await file.arrayBuffer());
} else {
g.cache.br[hash] = brotli.compress(
typeof content === "string" ? encoder.encode(content) : content,
{ quality: 11 }
);
await Bun.write(file, g.cache.br[hash]);
}
delete g.cache.br_progress.pending[hash];
g.cache.br_progress.running = false;
recurseCompressBr();
}
}, 50);
};

View File

@ -27,7 +27,7 @@ export const deploy = {
console.log(`Loading site: ${this.config.site_id} ${ts}`); console.log(`Loading site: ${this.config.site_id} ${ts}`);
try { try {
g.deploy.gz = JSON.parse( g.deploy.content = JSON.parse(
decoder.decode( decoder.decode(
await gunzipAsync( await gunzipAsync(
new Uint8Array( new Uint8Array(
@ -37,45 +37,45 @@ export const deploy = {
) )
); );
if (g.deploy.gz) { if (g.deploy.content) {
if (exists(dir("public"))) { if (exists(dir("public"))) {
await removeAsync(dir("public")); await removeAsync(dir("public"));
if (g.deploy.gz.public) { if (g.deploy.content.public) {
await dirAsync(dir("public")); await dirAsync(dir("public"));
for (const [k, v] of Object.entries(g.deploy.gz.public)) { for (const [k, v] of Object.entries(g.deploy.content.public)) {
await writeAsync(dir(`public/${k}`), v); await writeAsync(dir(`public/${k}`), v);
} }
} }
} }
for (const page of g.deploy.gz.layouts) { for (const page of g.deploy.content.layouts) {
if (page.is_default_layout) { if (page.is_default_layout) {
g.deploy.layout = page.content_tree; g.deploy.layout = page.content_tree;
break; break;
} }
} }
if (!g.deploy.layout && g.deploy.gz.layouts.length > 0) { if (!g.deploy.layout && g.deploy.content.layouts.length > 0) {
g.deploy.layout = g.deploy.gz.layouts[0].content_tree; g.deploy.layout = g.deploy.content.layouts[0].content_tree;
} }
g.deploy.router = createRouter(); g.deploy.router = createRouter();
g.deploy.pages = {}; g.deploy.pages = {};
for (const page of g.deploy.gz.pages) { for (const page of g.deploy.content.pages) {
g.deploy.pages[page.id] = page; g.deploy.pages[page.id] = page;
g.deploy.router.insert(page.url, page); g.deploy.router.insert(page.url, page);
} }
g.deploy.comps = {}; g.deploy.comps = {};
for (const comp of g.deploy.gz.comps) { for (const comp of g.deploy.content.comps) {
g.deploy.comps[comp.id] = comp.content_tree; g.deploy.comps[comp.id] = comp.content_tree;
} }
if (g.deploy.gz.code.server) { if (g.deploy.content.code.server) {
setTimeout(async () => { setTimeout(async () => {
if (g.deploy.gz) { if (g.deploy.content) {
delete require.cache[dir(`app/web/server/index.js`)]; delete require.cache[dir(`app/web/server/index.js`)];
await removeAsync(dir(`app/web/server`)); await removeAsync(dir(`app/web/server`));
await dirAsync(dir(`app/web/server`)); await dirAsync(dir(`app/web/server`));
for (const [k, v] of Object.entries(g.deploy.gz.code.server)) { for (const [k, v] of Object.entries(g.deploy.content.code.server)) {
await writeAsync(dir(`app/web/server/${k}`), v); await writeAsync(dir(`app/web/server/${k}`), v);
} }
@ -139,7 +139,7 @@ export const deploy = {
config: { deploy: { ts: "" }, site_id: "" }, config: { deploy: { ts: "" }, site_id: "" },
init: false, init: false,
raw: null, raw: null,
gz: null, content: null,
server: null, server: null,
}; };
} }

View File

@ -70,7 +70,8 @@ export const g = global as unknown as {
}; };
cache: { cache: {
br: Record<string, Uint8Array>; br: Record<string, Uint8Array>;
br_timeout: Set<string>; br_progress: { pending: Record<string, any>; running: boolean; timeout: any };
gz: Record<string, Uint8Array>;
}; };
createServer: ( createServer: (
arg: PrasiServer & { api: any; db: any } arg: PrasiServer & { api: any; db: any }
@ -85,7 +86,7 @@ export const g = global as unknown as {
string, string,
{ id: string; url: string; name: true; content_tree: any } { id: string; url: string; name: true; content_tree: any }
>; >;
gz: null | { content: null | {
layouts: SinglePage[]; layouts: SinglePage[];
pages: SinglePage[]; pages: SinglePage[];
site: any; site: any;