This commit is contained in:
Rizky 2024-12-20 17:26:33 +07:00
parent a724792eb8
commit b4d31bc897
12 changed files with 306 additions and 59 deletions

BIN
bun.lockb

Binary file not shown.

View File

@ -6,19 +6,6 @@ export const prasi_content_deploy: PrasiContent = {
async prepare(site_id) {
await ensureDeployExists(site_id);
},
async comps(comp_ids) {
return [];
},
async file(url, options) {
return { body: "", compression: "none" };
},
async layouts() {
return [];
},
async page_urls() {
return {};
},
async pages(page_ids) {
return [];
},
async staticFile(ctx) {},
async route(ctx) {},
};

View File

@ -1,22 +1,18 @@
import { g } from "utils/global";
import type { PrasiContent } from "./types";
export const prasi_content_ipc: PrasiContent = {
prepare(site_id) {
console.log("mantap jiwa");
},
async comps(comp_ids) {
return [];
},
async file(url, options) {
return { body: "", compression: "none" };
},
async layouts() {
return [];
},
async page_urls() {
return {};
},
async pages(page_ids) {
return [];
async staticFile(ctx) {
const asset = g.mode === "site" && g.ipc?.asset!;
if (asset) {
const response = asset.serve(ctx);
if (response) {
return response;
}
}
},
async route(ctx) {},
};

View File

@ -1,5 +1,5 @@
import { g } from "utils/global";
export const PrasiContent = () => {
export const prasiContent = () => {
return g.mode === "site" ? g.content : null;
};

View File

@ -1,15 +1,9 @@
import type { ServerCtx } from "utils/server-ctx";
export type PrasiContent = {
prepare: (site_id: string) => void | Promise<void>;
page_urls: () => Promise<Record<string, string>>;
pages: (page_ids: string[]) => Promise<IPage[]>;
comps: (comp_ids: string[]) => Promise<IComp[]>;
layouts: () => Promise<ILayout[]>;
file: (
url: string,
options?: {
accept: ("gzip" | "br" | "zstd")[];
}
) => Promise<{ body: any; compression: "none" | "gzip" | "br" | "zstd" }>;
staticFile: (ctx: ServerCtx) => Promise<Response | void>;
route: (ctx: ServerCtx) => Promise<Response | void>;
};
export type ILayout = {

View File

@ -4,6 +4,9 @@ import { prasi_content_ipc } from "../content/content-ipc";
import { prasi_content_deploy } from "../content/content-deploy";
import { loadCurrentDeploy } from "../content/deploy/load";
import { ipcSend } from "../content/ipc/send";
import { staticFile } from "utils/static";
import type { ServerCtx } from "utils/server-ctx";
import { prasiContent } from "../content/content";
startup("site", async () => {
await config.init("site:site.json");
@ -12,28 +15,63 @@ startup("site", async () => {
if (g.ipc) {
ipcSend({ type: "init" });
process.on("message", (msg: { type: "start" }) => {
if (g.mode === "site") {
if (g.server) {
console.log("restarting...");
process.exit();
} else {
process.on(
"message",
async (msg: { type: "start"; path: { asset: string } }) => {
if (g.mode === "site" && g.ipc) {
if (msg.type === "start") {
g.server = Bun.serve({
fetch(request, server) {},
websocket: { message(ws, message) {} },
port: 0,
});
g.ipc.asset = await staticFile(msg.path.asset);
startGlobalServer();
ipcSend({ type: "ready", port: g.server.port });
}
}
});
}
);
}
} else {
const ts = config.current?.deploy.current;
if (ts) {
await loadCurrentDeploy(ts);
}
g.server = Bun.serve({
fetch(request, server) {},
websocket: { message(ws, message) {} },
});
startGlobalServer();
}
}
});
const startGlobalServer = async () => {
if (g.mode === "site") {
g.server = Bun.serve({
async fetch(req, server) {
const content = prasiContent();
if (content) {
const url = new URL(req.url);
const pathname = req.url.split(url.host).pop() || "";
const ctx: ServerCtx = {
server,
req: req,
url: { pathname, raw: url },
};
const response = await content.staticFile(ctx);
if (response) {
return response;
}
const routed = await content.route(ctx);
if (routed) {
return routed;
}
return new Response("Not Found", { status: 404 });
}
},
websocket: { message(ws, message) {} },
port: 0,
});
}
};

View File

@ -26,7 +26,7 @@ startup("supervisor", async () => {
await ensureDBReady();
} else {
g.mode = "site";
if (g.mode === "site") g.ipc = true;
if (g.mode === "site") g.ipc = {};
}
await ensureServerReady(is_dev);

View File

@ -5,6 +5,7 @@ import { fs } from "./fs";
import type { PrasiSpawn, spawn } from "./spawn";
import type { prasi_content_ipc } from "../content/content-ipc";
import type { prasi_content_deploy } from "../content/content-deploy";
import type { StaticFile } from "./static";
if (!(globalThis as any).prasi) {
(globalThis as any).prasi = {};
@ -16,7 +17,10 @@ export const g = (globalThis as any).prasi as unknown as {
| {
mode: "site";
server: Server;
ipc: boolean;
ipc?: {
asset?: StaticFile;
};
static_cache: any;
content: typeof prasi_content_ipc & typeof prasi_content_deploy;
site?: {
db?: SiteConfig["db"];

View File

@ -0,0 +1,10 @@
import type { Serve } from "bun";
export type ServerCtx = {
server: Serve;
url: {
pathname: string;
raw: URL;
};
req: Request;
};

181
internal/utils/static.ts Normal file
View File

@ -0,0 +1,181 @@
import * as zstd from "@bokuweb/zstd-wasm";
import { Glob, gzipSync } from "bun";
import { BunSqliteKeyValue } from "bun-sqlite-key-value";
import { exists, existsAsync } from "fs-jetpack";
import mime from "mime";
import { readFileSync } from "node:fs";
import { join } from "path";
import { addRoute, createRouter, findRoute } from "rou3";
import type { ServerCtx } from "./server-ctx";
import { g } from "./global";
import { waitUntil } from "./wait-until";
await zstd.init();
export type StaticFile = Awaited<ReturnType<typeof staticFile>>;
export const staticFile = async (
path: string,
opt?: { index?: string; debug?: boolean }
) => {
if (g.mode !== "site") return;
if (!g.static_cache) {
g.static_cache = {} as any;
if (!g.static_cache.gz) {
g.static_cache.gz = new BunSqliteKeyValue(":memory:");
}
if (!g.static_cache.zstd) {
g.static_cache.zstd = new BunSqliteKeyValue(":memory:");
}
}
const store = g.static_cache;
const glob = new Glob("**");
const internal = {
indexPath: "",
rescan_timeout: null as any,
router: createRouter<{
mime: string | null;
fullpath: string;
path: string;
}>(),
};
const static_file = {
scanning: false,
paths: new Set<string>(),
// rescan will be overwritten below.
async rescan(arg?: { immediatly?: boolean }) {},
exists(rpath: string, arg?: { prefix?: string; debug?: boolean }) {
let pathname = rpath;
if (arg?.prefix && pathname) {
pathname = pathname.substring(arg.prefix.length);
}
const found = findRoute(internal.router, undefined, path + pathname);
return !!found;
},
serve: (ctx: ServerCtx, arg?: { prefix?: string; debug?: boolean }) => {
let pathname = ctx.url.pathname || "";
if (arg?.prefix && pathname) {
pathname = pathname.substring(arg.prefix.length);
}
const found = findRoute(internal.router, undefined, pathname);
if (found) {
const { fullpath, mime } = found.data;
if (exists(fullpath)) {
const { headers, content } = cachedResponse(
ctx,
fullpath,
mime,
store
);
headers["cache-control"] = "public, max-age=604800, immutable";
return new Response(content, {
headers,
});
} else {
store.gz.delete(fullpath);
store.zstd.delete(fullpath);
}
}
if (opt?.index) {
const { headers, content } = cachedResponse(
ctx,
internal.indexPath,
"text/html",
store
);
return new Response(content, { headers });
}
},
};
const scan = async () => {
if (static_file.scanning) {
await waitUntil(() => !static_file.scanning);
return;
}
static_file.scanning = true;
if (await existsAsync(path)) {
if (static_file.paths.size > 0) {
store.gz.delete([...static_file.paths]);
store.zstd.delete([...static_file.paths]);
}
for await (const file of glob.scan(path)) {
if (file === opt?.index) internal.indexPath = join(path, file);
static_file.paths.add(join(path, file));
let type = mime.getType(file);
if (file.endsWith(".ts")) {
type = "application/javascript";
}
addRoute(internal.router, undefined, `/${file}`, {
mime: type,
path: file,
fullpath: join(path, file),
});
}
}
static_file.scanning = false;
};
await scan();
static_file.rescan = (arg?: { immediatly?: boolean }) => {
return new Promise<void>((resolve) => {
clearTimeout(internal.rescan_timeout);
internal.rescan_timeout = setTimeout(
async () => {
await scan();
resolve();
},
arg?.immediatly ? 0 : 300
);
});
};
return static_file;
};
const cachedResponse = (
ctx: ServerCtx,
file_path: string,
mime: string | null,
store: any
) => {
const accept = ctx.req.headers.get("accept-encoding") || "";
const headers: any = {
"content-type": mime || "",
};
let content = null as any;
if (accept.includes("zstd")) {
content = store.zstd.get(file_path);
if (!content) {
content = zstd.compress(
new Uint8Array(readFileSync(file_path)) as any,
10
);
store.zstd.set(file_path, content);
}
headers["content-encoding"] = "zstd";
}
if (!content && accept.includes("gz")) {
content = store.gz.get(file_path);
if (!content) {
content = gzipSync(new Uint8Array(readFileSync(file_path)));
store.gz.set(file_path, content);
}
headers["content-encoding"] = "gzip";
}
return { content, headers };
};

View File

@ -0,0 +1,33 @@
export const waitUntil = (
condition: number | (() => any),
arg?: { timeout?: number; interval?: number }
) => {
return new Promise<void>(async (resolve) => {
if (typeof condition === "function") {
let tout = null as any;
if (arg?.timeout) {
tout = setTimeout(resolve, arg?.timeout);
}
if (await condition()) {
clearTimeout(tout);
resolve();
return;
}
let count = 0;
const c = setInterval(async () => {
if (await condition()) {
if (tout) clearTimeout(tout);
clearInterval(c);
resolve();
}
if (count > 100) {
clearInterval(c);
}
}, arg?.interval || 10);
} else if (typeof condition === "number") {
setTimeout(() => {
resolve();
}, condition);
}
});
};

View File

@ -4,7 +4,7 @@
"type": "module",
"scripts": {
"dev": "bun run --watch internal/supervisor.ts --dev",
"ipc": "bun run --watch internal/supervisor.ts --dev --ipc"
"ipc": "bun run --hot internal/supervisor.ts --dev --ipc"
},
"devDependencies": {
"@types/bun": "latest"
@ -13,12 +13,16 @@
"typescript": "^5.0.0"
},
"dependencies": {
"@bokuweb/zstd-wasm": "^0.0.22",
"@types/lodash.get": "^4.4.9",
"@types/lodash.set": "^4.3.9",
"bun-sqlite-key-value": "^1.13.1",
"dayjs": "^1.11.13",
"fs-jetpack": "^5.1.0",
"lodash.get": "^4.4.2",
"lodash.set": "^4.3.2",
"msgpackr": "^1.11.2"
"mime": "^4.0.6",
"msgpackr": "^1.11.2",
"rou3": "^0.5.1"
}
}