summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorAlan Pearce2023-09-17 17:31:18 +0200
committerAlan Pearce2023-09-17 17:31:18 +0200
commit602f249c2cfac0e7b6613fb63f5fb519aa1ca952 (patch)
treebe522208e3172e62b4777a66af4bd931677f7fcb
parent1a7abb3723d6b9db0d199c26d2a207e03636738a (diff)
downloadwebsite-602f249c2cfac0e7b6613fb63f5fb519aa1ca952.tar.lz
website-602f249c2cfac0e7b6613fb63f5fb519aa1ca952.tar.zst
website-602f249c2cfac0e7b6613fb63f5fb519aa1ca952.zip
Move servers into app.ts and export for testing
-rw-r--r--src/app.ts225
-rw-r--r--src/index.ts234
-rw-r--r--test/index.test.ts2
3 files changed, 231 insertions, 230 deletions
diff --git a/src/app.ts b/src/app.ts
new file mode 100644
index 0000000..b41de44
--- /dev/null
+++ b/src/app.ts
@@ -0,0 +1,225 @@
+import path from "node:path";
+import fs, { Stats } from "node:fs";
+import type { BunFile, Serve } from "bun";
+import * as Sentry from "@sentry/node";
+import prom from "bun-prometheus-client";
+
+import readConfig from "./config";
+
+Sentry.init({
+  release: `homestead@${Bun.env.FLY_MACHINE_VERSION}`,
+  tracesSampleRate: 1.0,
+});
+
+const base = ".";
+const publicDir = path.resolve(base, "public") + path.sep;
+
+const config = readConfig(base);
+const defaultHeaders = {
+  ...config.extra.headers,
+  vary: "Accept-Encoding",
+};
+
+type File = {
+  filename: string;
+  handle: BunFile;
+  relPath: string;
+  headers?: Record<string, string>;
+  type: string;
+  size: number;
+  mtime: Date;
+};
+
+const metrics = {
+  requests: new prom.Counter({
+    name: "homestead_requests",
+    help: "Number of requests by path, status code, and method",
+    labelNames: ["path", "status_code", "method"],
+  }),
+  requestDuration: new prom.Histogram({
+    name: "homestead_request_duration_seconds",
+    help: "Request duration in seconds",
+    labelNames: ["path"],
+  }),
+};
+
+let files = new Map<string, File>();
+
+function registerFile(
+  path: string,
+  pathname: string,
+  filename: string,
+  stat: Stats,
+): void {
+  pathname = "/" + (pathname === "." || pathname === "./" ? "" : pathname);
+
+  if (files.get(pathname) !== undefined) {
+    console.warn("File already registered:", pathname);
+  }
+  const handle = Bun.file(filename);
+  files.set(pathname, {
+    filename,
+    relPath: "/" + path,
+    handle: handle,
+    type: pathname.startsWith("/feed-styles.xsl") ? "text/xsl" : handle.type,
+    headers:
+      pathname === "/404.html"
+        ? Object.assign({}, defaultHeaders, { "cache-control": "no-cache" })
+        : undefined,
+    size: stat.size,
+    mtime: stat.mtime,
+  });
+}
+
+function walkDirectory(root: string, dir: string) {
+  const absDir = path.join(root, dir);
+  for (let pathname of fs.readdirSync(absDir)) {
+    const relPath = path.join(dir, pathname);
+    const absPath = path.join(absDir, pathname);
+    const stat = fs.statSync(absPath);
+    if (stat.isDirectory()) {
+      walkDirectory(root, relPath + path.sep);
+    } else if (stat.isFile()) {
+      if (pathname.startsWith("index.html")) {
+        const dir = relPath.replace("index.html", "");
+        registerFile(relPath, dir, absPath, stat);
+        if (dir !== "") {
+          registerFile(relPath, dir + path.sep, absPath, stat);
+        }
+      }
+      registerFile(relPath, relPath, absPath, stat);
+    }
+  }
+}
+
+walkDirectory(publicDir, "");
+
+async function serveFile(
+  file: File | undefined,
+  statusCode: number = 200,
+  extraHeaders: Record<string, string> = {},
+): Promise<Response> {
+  return new Response(await file.handle.arrayBuffer(), {
+    headers: {
+      "last-modified": file.mtime.toUTCString(),
+      ...extraHeaders,
+      ...(file.headers || defaultHeaders),
+    },
+    status: statusCode,
+  });
+}
+
+function parseIfModifiedSinceHeader(header: string | null): number {
+  return header ? new Date(header).getTime() + 999 : 0;
+}
+
+export const metricsServer = {
+  port: 9091,
+  fetch: async function (request) {
+    const pathname = new URL(request.url).pathname;
+    switch (pathname) {
+      case "/metrics":
+        return new Response(await prom.register.metrics());
+      default:
+        return new Response("", { status: 404 });
+    }
+  },
+} satisfies Serve;
+
+export const server = {
+  fetch: async function (request) {
+    const pathname = new URL(request.url).pathname.replace(/\/\/+/g, "/");
+    const endTimer = metrics.requestDuration.startTimer({ path: pathname });
+    const transaction = Sentry.startTransaction({
+      name: pathname,
+      op: "http.server",
+      description: `${request.method} ${pathname}`,
+      tags: {
+        url: request.url,
+        "http.method": request.method,
+        "http.user_agent": request.headers.get("user-agent"),
+      },
+    });
+    try {
+      const file = files.get(pathname);
+      metrics.requests.inc({ path: pathname });
+      if (file && (await file.handle.exists())) {
+        if (
+          parseIfModifiedSinceHeader(
+            request.headers.get("if-modified-since"),
+          ) >= file?.mtime.getTime()
+        ) {
+          metrics.requests.inc({
+            method: request.method,
+            path: pathname,
+            status_code: 304,
+          });
+          transaction.setHttpStatus(304);
+          return new Response("", { status: 304, headers: defaultHeaders });
+        }
+        metrics.requests.inc({
+          method: request.method,
+          path: pathname,
+          status_code: 200,
+        });
+        const encodings = (request.headers.get("accept-encoding") || "")
+          .split(",")
+          .map((x) => x.trim().toLowerCase());
+        if (encodings.includes("br") && files.has(file.relPath + ".br")) {
+          transaction.setHttpStatus(200);
+          transaction.setTag("http.content-encoding", "br");
+          return serveFile(files.get(file.relPath + ".br"), 200, {
+            "content-encoding": "br",
+            "content-type": file.type,
+          });
+        } else if (
+          encodings.includes("zstd") &&
+          files.has(file.relPath + ".zst")
+        ) {
+          transaction.setHttpStatus(200);
+          transaction.setTag("http.content-encoding", "zstd");
+          return serveFile(files.get(file.relPath + ".zst"), 200, {
+            "content-encoding": "zstd",
+            "content-type": file.type,
+          });
+        } else if (
+          encodings.includes("gzip") &&
+          files.has(file.relPath + ".gz")
+        ) {
+          transaction.setHttpStatus(200);
+          transaction.setTag("http.content-encoding", "gzip");
+          return serveFile(files.get(file.relPath + ".gz"), 200, {
+            "content-encoding": "gzip",
+            "content-type": file.type,
+          });
+        }
+        transaction.setHttpStatus(200);
+        transaction.setTag("http.content-encoding", "identity");
+        return serveFile(file);
+      } else {
+        metrics.requests.inc({
+          method: request.method,
+          path: pathname,
+          status_code: 404,
+        });
+        transaction.setHttpStatus(404);
+        transaction.setTag("http.content-encoding", "identity");
+        return serveFile(files.get("/404.html"), 404);
+      }
+    } catch (error) {
+      transaction.setTag("http.content-encoding", "identity");
+      transaction.setHttpStatus(503);
+      metrics.requests.inc({
+        method: request.method,
+        path: pathname,
+        status_code: 503,
+      });
+      Sentry.captureException(error);
+      return new Response("Something went wrong", { status: 503 });
+    } finally {
+      const seconds = endTimer();
+      metrics.requestDuration.observe(seconds);
+      transaction.finish();
+    }
+  },
+} satisfies Serve;
diff --git a/src/index.ts b/src/index.ts
index 450fe8f..83ad03d 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,231 +1,7 @@
-import path from "node:path";
-import fs, { Stats } from "node:fs";
-import type { BunFile, Serve } from "bun";
-import * as Sentry from "@sentry/node";
-import prom from "bun-prometheus-client";
+import { server, metricsServer } from "./app";
 
-import readConfig from "./config";
+const metricsServed = Bun.serve(metricsServer);
+console.info(`Metrics server started on port ${metricsServed.port}`);
 
-Sentry.init({
-  release: `homestead@${Bun.env.FLY_MACHINE_VERSION}`,
-  tracesSampleRate: 1.0,
-});
-
-const base = ".";
-const publicDir = path.resolve(base, "public") + path.sep;
-
-const config = readConfig(base);
-const defaultHeaders = {
-  ...config.extra.headers,
-  vary: "Accept-Encoding",
-};
-
-type File = {
-  filename: string;
-  handle: BunFile;
-  relPath: string;
-  headers?: Record<string, string>;
-  type: string;
-  size: number;
-  mtime: Date;
-};
-
-const metrics = {
-  requests: new prom.Counter({
-    name: "homestead_requests",
-    help: "Number of requests by path, status code, and method",
-    labelNames: ["path", "status_code", "method"],
-  }),
-  requestDuration: new prom.Histogram({
-    name: "homestead_request_duration_seconds",
-    help: "Request duration in seconds",
-    labelNames: ["path"],
-  }),
-};
-
-let files = new Map<string, File>();
-
-function registerFile(
-  path: string,
-  pathname: string,
-  filename: string,
-  stat: Stats,
-): void {
-  pathname = "/" + (pathname === "." || pathname === "./" ? "" : pathname);
-
-  if (files.get(pathname) !== undefined) {
-    console.warn("File already registered:", pathname);
-  }
-  const handle = Bun.file(filename);
-  files.set(pathname, {
-    filename,
-    relPath: "/" + path,
-    handle: handle,
-    type: pathname.startsWith("/feed-styles.xsl") ? "text/xsl" : handle.type,
-    headers:
-      pathname === "/404.html"
-        ? Object.assign({}, defaultHeaders, { "cache-control": "no-cache" })
-        : undefined,
-    size: stat.size,
-    mtime: stat.mtime,
-  });
-}
-
-function walkDirectory(root: string, dir: string) {
-  const absDir = path.join(root, dir);
-  for (let pathname of fs.readdirSync(absDir)) {
-    const relPath = path.join(dir, pathname);
-    const absPath = path.join(absDir, pathname);
-    const stat = fs.statSync(absPath);
-    if (stat.isDirectory()) {
-      walkDirectory(root, relPath + path.sep);
-    } else if (stat.isFile()) {
-      if (pathname.startsWith("index.html")) {
-        const dir = relPath.replace("index.html", "");
-        registerFile(relPath, dir, absPath, stat);
-        if (dir !== "") {
-          registerFile(relPath, dir + path.sep, absPath, stat);
-        }
-      }
-      registerFile(relPath, relPath, absPath, stat);
-    }
-  }
-}
-
-walkDirectory(publicDir, "");
-
-async function serveFile(
-  file: File | undefined,
-  statusCode: number = 200,
-  extraHeaders: Record<string, string> = {},
-): Promise<Response> {
-  return new Response(await file.handle.arrayBuffer(), {
-    headers: {
-      "last-modified": file.mtime.toUTCString(),
-      ...extraHeaders,
-      ...(file.headers || defaultHeaders),
-    },
-    status: statusCode,
-  });
-}
-
-function parseIfModifiedSinceHeader(header: string | null): number {
-  return header ? new Date(header).getTime() + 999 : 0;
-}
-
-const metricsServer = Bun.serve({
-  port: 9091,
-  fetch: async function (request) {
-    const pathname = new URL(request.url).pathname;
-    switch (pathname) {
-      case "/metrics":
-        return new Response(await prom.register.metrics());
-      default:
-        return new Response("", { status: 404 });
-    }
-  },
-});
-
-console.info(
-  `Serving metrics on http://${metricsServer.hostname}:${metricsServer.port}/metrics`,
-);
-
-const server = Bun.serve({
-  fetch: async function (request) {
-    const pathname = new URL(request.url).pathname.replace(/\/\/+/g, "/");
-    const endTimer = metrics.requestDuration.startTimer({ path: pathname });
-    const transaction = Sentry.startTransaction({
-      name: pathname,
-      op: "http.server",
-      description: `${request.method} ${pathname}`,
-      tags: {
-        url: request.url,
-        "http.method": request.method,
-        "http.user_agent": request.headers.get("user-agent"),
-      },
-    });
-    try {
-      const file = files.get(pathname);
-      metrics.requests.inc({ path: pathname });
-      if (file && (await file.handle.exists())) {
-        if (
-          parseIfModifiedSinceHeader(
-            request.headers.get("if-modified-since"),
-          ) >= file?.mtime.getTime()
-        ) {
-          metrics.requests.inc({
-            method: request.method,
-            path: pathname,
-            status_code: 304,
-          });
-          transaction.setHttpStatus(304);
-          return new Response("", { status: 304, headers: defaultHeaders });
-        }
-        metrics.requests.inc({
-          method: request.method,
-          path: pathname,
-          status_code: 200,
-        });
-        const encodings = (request.headers.get("accept-encoding") || "")
-          .split(",")
-          .map((x) => x.trim().toLowerCase());
-        if (encodings.includes("br") && files.has(file.relPath + ".br")) {
-          transaction.setHttpStatus(200);
-          transaction.setTag("http.content-encoding", "br");
-          return serveFile(files.get(file.relPath + ".br"), 200, {
-            "content-encoding": "br",
-            "content-type": file.type,
-          });
-        } else if (
-          encodings.includes("zstd") &&
-          files.has(file.relPath + ".zst")
-        ) {
-          transaction.setHttpStatus(200);
-          transaction.setTag("http.content-encoding", "zstd");
-          return serveFile(files.get(file.relPath + ".zst"), 200, {
-            "content-encoding": "zstd",
-            "content-type": file.type,
-          });
-        } else if (
-          encodings.includes("gzip") &&
-          files.has(file.relPath + ".gz")
-        ) {
-          transaction.setHttpStatus(200);
-          transaction.setTag("http.content-encoding", "gzip");
-          return serveFile(files.get(file.relPath + ".gz"), 200, {
-            "content-encoding": "gzip",
-            "content-type": file.type,
-          });
-        }
-        transaction.setHttpStatus(200);
-        transaction.setTag("http.content-encoding", "identity");
-        return serveFile(file);
-      } else {
-        metrics.requests.inc({
-          method: request.method,
-          path: pathname,
-          status_code: 404,
-        });
-        transaction.setHttpStatus(404);
-        transaction.setTag("http.content-encoding", "identity");
-        return serveFile(files.get("/404.html"), 404);
-      }
-    } catch (error) {
-      transaction.setTag("http.content-encoding", "identity");
-      transaction.setHttpStatus(503);
-      metrics.requests.inc({
-        method: request.method,
-        path: pathname,
-        status_code: 503,
-      });
-      Sentry.captureException(error);
-      return new Response("Something went wrong", { status: 503 });
-    } finally {
-      const seconds = endTimer();
-      metrics.requestDuration.observe(seconds);
-      transaction.finish();
-    }
-  },
-});
-
-console.info(`Serving website on http://${server.hostname}:${server.port}/`);
+const served = Bun.serve(server);
+console.info(`Serving website on http://${served.hostname}:${served.port}/`);
diff --git a/test/index.test.ts b/test/index.test.ts
index 2f682a9..6e29e3d 100644
--- a/test/index.test.ts
+++ b/test/index.test.ts
@@ -1,7 +1,7 @@
 import { type Server } from "bun";
 import { expect, test, beforeAll, afterAll } from "bun:test";
 
-import app from "../src/index";
+import { server as app } from "../src/app";
 
 const port = 33000;
 const base = `http://localhost:${port}/`;