Spaces:
Paused
Paused
| import type { BrowserRouteContext } from "../server-context.js"; | |
| import type { BrowserRequest, BrowserResponse, BrowserRouteRegistrar } from "./types.js"; | |
| import { registerBrowserRoutes } from "./index.js"; | |
| type BrowserDispatchRequest = { | |
| method: "GET" | "POST" | "DELETE"; | |
| path: string; | |
| query?: Record<string, unknown>; | |
| body?: unknown; | |
| }; | |
| type BrowserDispatchResponse = { | |
| status: number; | |
| body: unknown; | |
| }; | |
| type RouteEntry = { | |
| method: BrowserDispatchRequest["method"]; | |
| path: string; | |
| regex: RegExp; | |
| paramNames: string[]; | |
| handler: (req: BrowserRequest, res: BrowserResponse) => void | Promise<void>; | |
| }; | |
| function escapeRegex(value: string) { | |
| return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); | |
| } | |
| function compileRoute(path: string): { regex: RegExp; paramNames: string[] } { | |
| const paramNames: string[] = []; | |
| const parts = path.split("/").map((part) => { | |
| if (part.startsWith(":")) { | |
| const name = part.slice(1); | |
| paramNames.push(name); | |
| return "([^/]+)"; | |
| } | |
| return escapeRegex(part); | |
| }); | |
| return { regex: new RegExp(`^${parts.join("/")}$`), paramNames }; | |
| } | |
| function createRegistry() { | |
| const routes: RouteEntry[] = []; | |
| const register = | |
| (method: RouteEntry["method"]) => (path: string, handler: RouteEntry["handler"]) => { | |
| const { regex, paramNames } = compileRoute(path); | |
| routes.push({ method, path, regex, paramNames, handler }); | |
| }; | |
| const router: BrowserRouteRegistrar = { | |
| get: register("GET"), | |
| post: register("POST"), | |
| delete: register("DELETE"), | |
| }; | |
| return { routes, router }; | |
| } | |
| function normalizePath(path: string) { | |
| if (!path) { | |
| return "/"; | |
| } | |
| return path.startsWith("/") ? path : `/${path}`; | |
| } | |
| export function createBrowserRouteDispatcher(ctx: BrowserRouteContext) { | |
| const registry = createRegistry(); | |
| registerBrowserRoutes(registry.router, ctx); | |
| return { | |
| dispatch: async (req: BrowserDispatchRequest): Promise<BrowserDispatchResponse> => { | |
| const method = req.method; | |
| const path = normalizePath(req.path); | |
| const query = req.query ?? {}; | |
| const body = req.body; | |
| const match = registry.routes.find((route) => { | |
| if (route.method !== method) { | |
| return false; | |
| } | |
| return route.regex.test(path); | |
| }); | |
| if (!match) { | |
| return { status: 404, body: { error: "Not Found" } }; | |
| } | |
| const exec = match.regex.exec(path); | |
| const params: Record<string, string> = {}; | |
| if (exec) { | |
| for (const [idx, name] of match.paramNames.entries()) { | |
| const value = exec[idx + 1]; | |
| if (typeof value === "string") { | |
| params[name] = decodeURIComponent(value); | |
| } | |
| } | |
| } | |
| let status = 200; | |
| let payload: unknown = undefined; | |
| const res: BrowserResponse = { | |
| status(code) { | |
| status = code; | |
| return res; | |
| }, | |
| json(bodyValue) { | |
| payload = bodyValue; | |
| }, | |
| }; | |
| try { | |
| await match.handler( | |
| { | |
| params, | |
| query, | |
| body, | |
| }, | |
| res, | |
| ); | |
| } catch (err) { | |
| return { status: 500, body: { error: String(err) } }; | |
| } | |
| return { status, body: payload }; | |
| }, | |
| }; | |
| } | |
| export type { BrowserDispatchRequest, BrowserDispatchResponse }; | |