diff --git a/src/handler.ts b/src/handler.ts index 6f5d4d8..7285a14 100644 --- a/src/handler.ts +++ b/src/handler.ts @@ -1,45 +1,34 @@ import QueueManager, { QueueNameTooLongError } from "./manager.ts"; import { RateLimiter } from "./rate_limiter.ts"; import { withAuth, withRateLimit } from "./middleware.ts"; -import { Router } from "./router.ts"; +import { RouteHandler, Router } from "./router.ts"; const MAX_BODY_SIZE = 1024 * 1024; // 1 MB +const LOG_ENCODER = new TextEncoder(); -export function createHandler(mgr: QueueManager, apiToken: string, rateLimitRequests?: number) { - const rateLimiter = new RateLimiter(rateLimitRequests ?? 100); - const router = new Router(); - - router.get("/health", () => { - return new Response(JSON.stringify({ status: "ok" }), { - status: 200, - headers: { "Content-Type": "application/json" }, - }); - }); - - router.get("/queues", () => { - const queueNames = mgr.listQueues(); - return new Response(JSON.stringify(queueNames), { - status: 200, - headers: { "Content-Type": "application/json" }, - }); - }); - - router.post("/enqueue/:queue", async (request, match) => { - const queueName = match.pathname.groups.queue as string; - - const contentLength = request.headers.get("content-length"); - if (contentLength && parseInt(contentLength) > MAX_BODY_SIZE) { - return new Response("Payload too large", { status: 413 }); - } - let body: string; - try { - body = await request.text(); - } catch { - return new Response("Payload too large", { status: 413 }); - } +async function readRequestBody(request: Request): Promise { + const contentLength = request.headers.get("content-length"); + if (contentLength && parseInt(contentLength) > MAX_BODY_SIZE) { + return new Response("Payload too large", { status: 413 }); + } + try { + const body = await request.text(); if (body.length > MAX_BODY_SIZE) { return new Response("Payload too large", { status: 413 }); } + return body; + } catch { + return new Response("Payload too large", { status: 413 }); + } +} + +function enqueueHandler(mgr: QueueManager): RouteHandler { + return async (request, match) => { + const queueName = match.pathname.groups.queue as string; + const body = await readRequestBody(request); + if (body instanceof Response) { + return body; + } try { const json = JSON.parse(body); if (!("payload" in json)) { @@ -53,85 +42,121 @@ export function createHandler(mgr: QueueManager, apiToken: string, rateL } mgr.enqueue(queueName, json.payload); return new Response(`Payload successfully queued onto ${queueName}.`); - } catch (e) { - if (e instanceof SyntaxError) { + } catch (error) { + if (error instanceof SyntaxError) { return new Response("Invalid JSON", { status: 400 }); } - if (e instanceof QueueNameTooLongError) { - return new Response("Queue name too long", { status: 400 }); - } - throw e; + return queueNameErrorResponse(error); } - }); + }; +} - router.get("/dequeue/:queue", (_request, match) => { - const queueName = match.pathname.groups.queue as string; +function queueNameErrorResponse(error: unknown): Response { + if (error instanceof QueueNameTooLongError) { + return new Response("Queue name too long", { status: 400 }); + } + throw error; +} + +function itemResponse(item: unknown): Response { + if (item === undefined) { + return new Response(null, { status: 204 }); + } + if (typeof item === "object" && item !== null) { + return new Response(JSON.stringify(item), { + headers: { "Content-Type": "application/json" }, + }); + } + return new Response(String(item)); +} + +function dequeueHandler(mgr: QueueManager): RouteHandler { + return (request, match) => { + void request; try { - const item = mgr.dequeue(queueName); - if (item === undefined) { - return new Response(null, { status: 204 }); - } - if (typeof item === "object" && item !== null) { - return new Response(JSON.stringify(item), { - headers: { "Content-Type": "application/json" }, - }); - } - return new Response(item); - } catch (e) { - if (e instanceof QueueNameTooLongError) { - return new Response("Queue name too long", { status: 400 }); - } - throw e; + return itemResponse(mgr.dequeue(match.pathname.groups.queue as string)); + } catch (error) { + return queueNameErrorResponse(error); } - }); + }; +} - router.get("/peek/:queue", (_request, match) => { - const queueName = match.pathname.groups.queue as string; +function peekHandler(mgr: QueueManager): RouteHandler { + return (request, match) => { + void request; try { - const item = mgr.peek(queueName); - if (item === undefined) { - return new Response(null, { status: 204 }); - } - if (typeof item === "object" && item !== null) { - return new Response(JSON.stringify(item), { - headers: { "Content-Type": "application/json" }, - }); - } - return new Response(item); - } catch (e) { - if (e instanceof QueueNameTooLongError) { - return new Response("Queue name too long", { status: 400 }); - } - throw e; + return itemResponse(mgr.peek(match.pathname.groups.queue as string)); + } catch (error) { + return queueNameErrorResponse(error); } - }); + }; +} - router.get("/length/:queue", (_request, match) => { - const queueName = match.pathname.groups.queue as string; +function lengthHandler(mgr: QueueManager): RouteHandler { + return (request, match) => { + void request; try { - const len = mgr.length(queueName); - return new Response(`${len}`); - } catch (e) { - if (e instanceof QueueNameTooLongError) { - return new Response("Queue name too long", { status: 400 }); - } - throw e; + const length = mgr.length(match.pathname.groups.queue as string); + return new Response(`${length}`); + } catch (error) { + return queueNameErrorResponse(error); } + }; +} + +function registerRoutes(router: Router, mgr: QueueManager): void { + router.get("/health", () => { + return new Response(JSON.stringify({ status: "ok" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + }); + router.get("/queues", () => { + return new Response(JSON.stringify(mgr.listQueues()), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); }); + router.post("/enqueue/:queue", enqueueHandler(mgr)); + router.get("/dequeue/:queue", dequeueHandler(mgr)); + router.get("/peek/:queue", peekHandler(mgr)); + router.get("/length/:queue", lengthHandler(mgr)); +} +function writeLog(destination: { writeSync(data: Uint8Array): number }, message: string): void { + destination.writeSync(LOG_ENCODER.encode(`${message}\n`)); +} + +export function createHandler( + mgr: QueueManager, + apiToken: string, + rateLimitRequests?: number, +) { + const rateLimiter = new RateLimiter(rateLimitRequests ?? 100); + const router = new Router(); + registerRoutes(router, mgr); const handlerWithAuth = withAuth(apiToken)(router.handle); const handlerWithRateLimit = withRateLimit(rateLimiter)(handlerWithAuth); - return async function handler(request: Request, info?: Deno.ServeHandlerInfo): Promise { + return async function handler( + request: Request, + info?: Deno.ServeHandlerInfo, + ): Promise { const start = performance.now(); try { const response = await handlerWithRateLimit(request, info); const duration = performance.now() - start; - console.log(`${request.method} ${request.url} ${response.status} ${duration.toFixed(2)}ms`); + writeLog( + Deno.stdout, + `${request.method} ${request.url} ${response.status} ${duration.toFixed(2)}ms`, + ); return response; } catch (error) { const duration = performance.now() - start; - console.error(`${request.method} ${request.url} 500 ${duration.toFixed(2)}ms`); + writeLog( + Deno.stderr, + `${request.method} ${request.url} 500 ${duration.toFixed(2)}ms`, + ); throw error; } };