refactor: 迁移 Bun fullstack 架构
This commit is contained in:
@@ -1,91 +0,0 @@
|
||||
import type { RuntimeMode } from "../shared/api";
|
||||
import type { ProbeStore } from "./checker/store";
|
||||
|
||||
import { createApiError, jsonResponse } from "./helpers";
|
||||
import { guardGetHead } from "./middleware";
|
||||
import { handleHealth } from "./routes/health";
|
||||
import { handleHistory } from "./routes/history";
|
||||
import { handleMeta } from "./routes/meta";
|
||||
import { handleSummary } from "./routes/summary";
|
||||
import { handleTargets } from "./routes/targets";
|
||||
import { handleTrend } from "./routes/trend";
|
||||
import { serveStaticAsset } from "./static";
|
||||
|
||||
export interface AppOptions {
|
||||
mode: RuntimeMode;
|
||||
staticAssets?: StaticAssets;
|
||||
store?: ProbeStore;
|
||||
}
|
||||
|
||||
export interface StaticAssets {
|
||||
files: Record<string, Blob>;
|
||||
indexHtml: Blob;
|
||||
}
|
||||
|
||||
export function createFetchHandler(options: AppOptions) {
|
||||
return (request: Request): Response => {
|
||||
const url = new URL(request.url);
|
||||
|
||||
if (url.pathname === "/health") {
|
||||
return handleHealth(request.method, options.mode);
|
||||
}
|
||||
|
||||
if (url.pathname === "/api/meta") {
|
||||
return handleMetaRoute(request, options.mode);
|
||||
}
|
||||
|
||||
if (url.pathname.startsWith("/api/") && options.store) {
|
||||
return handleApiRoute(url, request, options.store, options.mode);
|
||||
}
|
||||
|
||||
if (url.pathname.startsWith("/api/")) {
|
||||
return jsonResponse(createApiError("Service not ready", 503), {
|
||||
method: request.method,
|
||||
mode: options.mode,
|
||||
status: 503,
|
||||
});
|
||||
}
|
||||
|
||||
if (options.staticAssets) {
|
||||
return serveStaticAsset(url.pathname, options.staticAssets, options.mode);
|
||||
}
|
||||
|
||||
return new Response("开发期请通过 Vite 前端地址访问页面。", {
|
||||
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
||||
status: 404,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function handleApiRoute(url: URL, request: Request, store: ProbeStore, mode: RuntimeMode): Response {
|
||||
const guardResult = guardGetHead(request.method, mode);
|
||||
if (guardResult) return guardResult;
|
||||
|
||||
const method = request.method;
|
||||
|
||||
if (url.pathname === "/api/summary") {
|
||||
return handleSummary(store, method, mode);
|
||||
}
|
||||
|
||||
if (url.pathname === "/api/targets") {
|
||||
return handleTargets(store, method, mode);
|
||||
}
|
||||
|
||||
const historyMatch = /^\/api\/targets\/([^/]+)\/history$/.exec(url.pathname);
|
||||
if (historyMatch) {
|
||||
return handleHistory(historyMatch[1]!, url, method, store, mode);
|
||||
}
|
||||
|
||||
const trendMatch = /^\/api\/targets\/([^/]+)\/trend$/.exec(url.pathname);
|
||||
if (trendMatch) {
|
||||
return handleTrend(trendMatch[1]!, url, method, store, mode);
|
||||
}
|
||||
|
||||
return jsonResponse(createApiError("API route not found", 404), { method, mode, status: 404 });
|
||||
}
|
||||
|
||||
function handleMetaRoute(request: Request, mode: RuntimeMode): Response {
|
||||
const guardResult = guardGetHead(request.method, mode);
|
||||
if (guardResult) return guardResult;
|
||||
return handleMeta(request.method, mode);
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { join } from "node:path";
|
||||
|
||||
import type { RuntimeMode } from "../shared/api";
|
||||
import type { StaticAssets } from "./app";
|
||||
import type { StartServerOptions } from "./server";
|
||||
|
||||
import { loadConfig, type ResolvedConfig } from "./checker/config-loader";
|
||||
@@ -27,7 +26,6 @@ export interface BootstrapDependencies {
|
||||
export interface BootstrapOptions {
|
||||
configPath: string;
|
||||
mode: RuntimeMode;
|
||||
staticAssets?: StaticAssets;
|
||||
}
|
||||
|
||||
type BootstrapEngine = Pick<ProbeEngine, "start" | "stop">;
|
||||
@@ -71,7 +69,6 @@ export async function bootstrap(options: BootstrapOptions, dependencies: Bootstr
|
||||
serve({
|
||||
config: { host: config.host, port: config.port },
|
||||
mode: options.mode,
|
||||
staticAssets: options.staticAssets,
|
||||
store,
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import type { ApiErrorResponse, CheckFailure, CheckResult, HealthResponse, RuntimeMode } from "../shared/api";
|
||||
import type { StoredCheckResult } from "./checker/types";
|
||||
|
||||
export function allowsGetHead(method: string): boolean {
|
||||
return method === "GET" || method === "HEAD";
|
||||
}
|
||||
|
||||
export function createApiError(error: string, status: number): ApiErrorResponse {
|
||||
return { error, status };
|
||||
}
|
||||
@@ -36,15 +32,14 @@ export function formatDuration(ms: number): string {
|
||||
|
||||
export function jsonResponse(
|
||||
body: unknown,
|
||||
options: { headers?: HeadersInit; method?: string; mode: RuntimeMode; status?: number },
|
||||
options: { headers?: HeadersInit; mode: RuntimeMode; status?: number },
|
||||
): Response {
|
||||
const headers = createHeaders(options.mode, {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
...options.headers,
|
||||
});
|
||||
const responseBody = options.method === "HEAD" ? null : JSON.stringify(body);
|
||||
|
||||
return new Response(responseBody, {
|
||||
return new Response(JSON.stringify(body), {
|
||||
headers,
|
||||
status: options.status,
|
||||
});
|
||||
@@ -69,11 +64,3 @@ export function mapCheckResult(row: StoredCheckResult): CheckResult {
|
||||
timestamp: row.timestamp,
|
||||
};
|
||||
}
|
||||
|
||||
export function methodNotAllowedResponse(allow: string[], mode: RuntimeMode): Response {
|
||||
return jsonResponse(createApiError("Method not allowed", 405), {
|
||||
headers: { Allow: allow.join(", ") },
|
||||
mode,
|
||||
status: 405,
|
||||
});
|
||||
}
|
||||
|
||||
12
src/server/main.ts
Normal file
12
src/server/main.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { bootstrap } from "./bootstrap";
|
||||
import { readRuntimeConfig } from "./config";
|
||||
|
||||
async function main() {
|
||||
const { configPath } = readRuntimeConfig();
|
||||
await bootstrap({ configPath, mode: "production" });
|
||||
}
|
||||
|
||||
void main().catch((error) => {
|
||||
console.error("启动失败:", error instanceof Error ? error.message : error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,16 +1,9 @@
|
||||
import type { RuntimeMode } from "../shared/api";
|
||||
|
||||
import { allowsGetHead, createApiError, jsonResponse, methodNotAllowedResponse } from "./helpers";
|
||||
import { createApiError, jsonResponse } from "./helpers";
|
||||
|
||||
const MAX_PAGE_SIZE = 200;
|
||||
|
||||
export function guardGetHead(method: string, mode: RuntimeMode): null | Response {
|
||||
if (!allowsGetHead(method)) {
|
||||
return methodNotAllowedResponse(["GET", "HEAD"], mode);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function validatePagination(
|
||||
pageParam: null | string,
|
||||
pageSizeParam: null | string,
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import type { RuntimeMode } from "../../shared/api";
|
||||
|
||||
import { allowsGetHead, createHealthResponse, jsonResponse, methodNotAllowedResponse } from "../helpers";
|
||||
import { createHealthResponse, jsonResponse } from "../helpers";
|
||||
|
||||
export function handleHealth(method: string, mode: RuntimeMode): Response {
|
||||
if (!allowsGetHead(method)) {
|
||||
return methodNotAllowedResponse(["GET", "HEAD"], mode);
|
||||
}
|
||||
|
||||
return jsonResponse(createHealthResponse(), { method, mode });
|
||||
export function handleHealth(mode: RuntimeMode): Response {
|
||||
return jsonResponse(createHealthResponse(), { mode });
|
||||
}
|
||||
|
||||
@@ -4,13 +4,13 @@ import type { ProbeStore } from "../checker/store";
|
||||
import { jsonResponse, mapCheckResult } from "../helpers";
|
||||
import { validatePagination, validateTargetId, validateTimeRange } from "../middleware";
|
||||
|
||||
export function handleHistory(idStr: string, url: URL, method: string, store: ProbeStore, mode: RuntimeMode): Response {
|
||||
export function handleHistory(idStr: string, url: URL, store: ProbeStore, mode: RuntimeMode): Response {
|
||||
const idResult = validateTargetId(idStr, mode);
|
||||
if (idResult instanceof Response) return idResult;
|
||||
|
||||
const target = store.getTargetById(idResult.id);
|
||||
if (!target) {
|
||||
return jsonResponse({ error: "Target not found", status: 404 } as const, { method, mode, status: 404 });
|
||||
return jsonResponse({ error: "Target not found", status: 404 } as const, { mode, status: 404 });
|
||||
}
|
||||
|
||||
const timeResult = validateTimeRange(url.searchParams.get("from"), url.searchParams.get("to"), mode);
|
||||
@@ -27,5 +27,5 @@ export function handleHistory(idStr: string, url: URL, method: string, store: Pr
|
||||
total: result.total,
|
||||
};
|
||||
|
||||
return jsonResponse(response, { method, mode });
|
||||
return jsonResponse(response, { mode });
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@ import type { MetaResponse, RuntimeMode } from "../../shared/api";
|
||||
import { checkerRegistry } from "../checker/runner";
|
||||
import { jsonResponse } from "../helpers";
|
||||
|
||||
export function handleMeta(method: string, mode: RuntimeMode): Response {
|
||||
export function handleMeta(mode: RuntimeMode): Response {
|
||||
const response: MetaResponse = {
|
||||
checkerTypes: checkerRegistry.supportedTypes,
|
||||
};
|
||||
|
||||
return jsonResponse(response, { method, mode });
|
||||
return jsonResponse(response, { mode });
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { ProbeStore } from "../checker/store";
|
||||
|
||||
import { jsonResponse } from "../helpers";
|
||||
|
||||
export function handleSummary(store: ProbeStore, method: string, mode: RuntimeMode): Response {
|
||||
export function handleSummary(store: ProbeStore, mode: RuntimeMode): Response {
|
||||
const summary = store.getSummary();
|
||||
const response: SummaryResponse = {
|
||||
down: summary.down,
|
||||
@@ -12,5 +12,5 @@ export function handleSummary(store: ProbeStore, method: string, mode: RuntimeMo
|
||||
up: summary.up,
|
||||
};
|
||||
|
||||
return jsonResponse(response, { method, mode });
|
||||
return jsonResponse(response, { mode });
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { ProbeStore } from "../checker/store";
|
||||
|
||||
import { formatDuration, jsonResponse, mapCheckResult } from "../helpers";
|
||||
|
||||
export function handleTargets(store: ProbeStore, method: string, mode: RuntimeMode): Response {
|
||||
export function handleTargets(store: ProbeStore, mode: RuntimeMode): Response {
|
||||
const targets = store.getTargets();
|
||||
const latestChecksMap = store.getLatestChecksMap();
|
||||
const allStats = store.getAllTargetStats();
|
||||
@@ -34,5 +34,5 @@ export function handleTargets(store: ProbeStore, method: string, mode: RuntimeMo
|
||||
};
|
||||
});
|
||||
|
||||
return jsonResponse(result, { method, mode });
|
||||
return jsonResponse(result, { mode });
|
||||
}
|
||||
|
||||
@@ -4,13 +4,13 @@ import type { ProbeStore } from "../checker/store";
|
||||
import { jsonResponse } from "../helpers";
|
||||
import { validateTargetId, validateTimeRange } from "../middleware";
|
||||
|
||||
export function handleTrend(idStr: string, url: URL, method: string, store: ProbeStore, mode: RuntimeMode): Response {
|
||||
export function handleTrend(idStr: string, url: URL, store: ProbeStore, mode: RuntimeMode): Response {
|
||||
const idResult = validateTargetId(idStr, mode);
|
||||
if (idResult instanceof Response) return idResult;
|
||||
|
||||
const target = store.getTargetById(idResult.id);
|
||||
if (!target) {
|
||||
return jsonResponse({ error: "Target not found", status: 404 } as const, { method, mode, status: 404 });
|
||||
return jsonResponse({ error: "Target not found", status: 404 } as const, { mode, status: 404 });
|
||||
}
|
||||
|
||||
const timeResult = validateTimeRange(url.searchParams.get("from"), url.searchParams.get("to"), mode);
|
||||
@@ -23,5 +23,5 @@ export function handleTrend(idStr: string, url: URL, method: string, store: Prob
|
||||
totalChecks: row.totalChecks,
|
||||
}));
|
||||
|
||||
return jsonResponse(trend, { method, mode });
|
||||
return jsonResponse(trend, { mode });
|
||||
}
|
||||
|
||||
@@ -1,27 +1,54 @@
|
||||
import type { RuntimeMode } from "../shared/api";
|
||||
import type { StaticAssets } from "./app";
|
||||
import type { ProbeStore } from "./checker/store";
|
||||
import type { RuntimeConfig } from "./config";
|
||||
|
||||
import { createFetchHandler } from "./app";
|
||||
import homepage from "../web/index.html";
|
||||
import { createApiError, jsonResponse } from "./helpers";
|
||||
import { handleHealth } from "./routes/health";
|
||||
import { handleHistory } from "./routes/history";
|
||||
import { handleMeta } from "./routes/meta";
|
||||
import { handleSummary } from "./routes/summary";
|
||||
import { handleTargets } from "./routes/targets";
|
||||
import { handleTrend } from "./routes/trend";
|
||||
|
||||
export interface StartServerOptions {
|
||||
config: RuntimeConfig;
|
||||
mode: RuntimeMode;
|
||||
staticAssets?: StaticAssets;
|
||||
store?: ProbeStore;
|
||||
store: ProbeStore;
|
||||
}
|
||||
|
||||
export function startServer(options: StartServerOptions) {
|
||||
const { config, mode, staticAssets, store } = options;
|
||||
const { config, mode, store } = options;
|
||||
|
||||
const server = Bun.serve({
|
||||
fetch: createFetchHandler({
|
||||
mode,
|
||||
staticAssets,
|
||||
store,
|
||||
}),
|
||||
development: mode === "development" ? { console: true, hmr: true } : false,
|
||||
fetch() {
|
||||
return new Response("Not found", { status: 404 });
|
||||
},
|
||||
hostname: config.host,
|
||||
port: config.port,
|
||||
routes: {
|
||||
"/*": homepage,
|
||||
"/api/*": () => jsonResponse(createApiError("API route not found", 404), { mode, status: 404 }),
|
||||
"/api/meta": {
|
||||
GET: () => handleMeta(mode),
|
||||
},
|
||||
"/api/summary": {
|
||||
GET: () => handleSummary(store, mode),
|
||||
},
|
||||
"/api/targets": {
|
||||
GET: () => handleTargets(store, mode),
|
||||
},
|
||||
"/api/targets/:id/history": {
|
||||
GET: (req) => handleHistory(req.params.id, new URL(req.url), store, mode),
|
||||
},
|
||||
"/api/targets/:id/trend": {
|
||||
GET: (req) => handleTrend(req.params.id, new URL(req.url), store, mode),
|
||||
},
|
||||
"/health": {
|
||||
GET: () => handleHealth(mode),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`DiAL listening on ${server.url}`);
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import type { RuntimeMode } from "../shared/api";
|
||||
import type { StaticAssets } from "./app";
|
||||
|
||||
import { createHeaders } from "./helpers";
|
||||
|
||||
export function contentTypeFor(pathname: string): string {
|
||||
if (pathname.endsWith(".js") || pathname.endsWith(".mjs")) return "text/javascript; charset=utf-8";
|
||||
if (pathname.endsWith(".css")) return "text/css; charset=utf-8";
|
||||
if (pathname.endsWith(".svg")) return "image/svg+xml";
|
||||
if (pathname.endsWith(".json")) return "application/json; charset=utf-8";
|
||||
if (pathname.endsWith(".png")) return "image/png";
|
||||
if (pathname.endsWith(".jpg") || pathname.endsWith(".jpeg")) return "image/jpeg";
|
||||
if (pathname.endsWith(".ico")) return "image/x-icon";
|
||||
|
||||
return "application/octet-stream";
|
||||
}
|
||||
|
||||
export function hasFileExtension(pathname: string): boolean {
|
||||
return /\/[^/]+\.[^/]+$/.test(pathname);
|
||||
}
|
||||
|
||||
export function htmlResponse(indexHtml: Blob, mode: RuntimeMode): Response {
|
||||
return new Response(indexHtml, {
|
||||
headers: createHeaders(mode, {
|
||||
"Cache-Control": "no-cache",
|
||||
"Content-Type": "text/html; charset=utf-8",
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
export function serveStaticAsset(pathname: string, staticAssets: StaticAssets, mode: RuntimeMode): Response {
|
||||
if (pathname === "/") {
|
||||
return htmlResponse(staticAssets.indexHtml, mode);
|
||||
}
|
||||
|
||||
const asset = staticAssets.files[pathname];
|
||||
|
||||
if (asset) {
|
||||
return new Response(asset, {
|
||||
headers: createHeaders(mode, {
|
||||
"Cache-Control": "public, max-age=31536000, immutable",
|
||||
"Content-Type": contentTypeFor(pathname),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
if (pathname.startsWith("/assets/") || hasFileExtension(pathname)) {
|
||||
return new Response("Not Found", {
|
||||
headers: createHeaders(mode, { "Content-Type": "text/plain; charset=utf-8" }),
|
||||
status: 404,
|
||||
});
|
||||
}
|
||||
|
||||
return htmlResponse(staticAssets.indexHtml, mode);
|
||||
}
|
||||
Reference in New Issue
Block a user