Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 113 additions & 88 deletions src/handler.ts
Original file line number Diff line number Diff line change
@@ -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<string>, 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<string | Response> {
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<string>): 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)) {
Expand All @@ -53,85 +42,121 @@ export function createHandler(mgr: QueueManager<string>, 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<string>): 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<string>): 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<string>): 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<string>): 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<string>,
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<Response> {
return async function handler(
request: Request,
info?: Deno.ServeHandlerInfo,
): Promise<Response> {
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;
}
};
Expand Down