Scratch space for learning atproto app development
1import { createHttpTerminator } from 'http-terminator' 2import { once } from 'node:events' 3import type { 4 IncomingMessage, 5 RequestListener, 6 ServerResponse, 7} from 'node:http' 8import { createServer } from 'node:http' 9 10export type NextFunction = (err?: unknown) => void 11 12export type Handler< 13 Req extends IncomingMessage = IncomingMessage, 14 Res extends ServerResponse<Req> = ServerResponse<Req>, 15> = (req: Req, res: Res, next: NextFunction) => void 16 17export type AsyncHandler< 18 Req extends IncomingMessage = IncomingMessage, 19 Res extends ServerResponse<Req> = ServerResponse<Req>, 20> = (req: Req, res: Res, next: NextFunction) => Promise<void> 21 22// Helper function for defining routes 23export function handler< 24 Req extends IncomingMessage = IncomingMessage, 25 Res extends ServerResponse<Req> = ServerResponse<Req>, 26>(fn: Handler<Req, Res> | AsyncHandler<Req, Res>): Handler<Req, Res> { 27 return (req, res, next) => { 28 // NodeJS prefers objects over functions for garbage collection, 29 const nextSafe = nextOnce.bind({ next }) 30 try { 31 const result = fn(req, res, nextSafe) 32 if (result instanceof Promise) result.catch(nextSafe) 33 } catch (err) { 34 nextSafe(err) 35 } 36 } 37 38 function nextOnce(this: { next: NextFunction | null }, err?: unknown) { 39 const { next } = this 40 this.next = null 41 next?.(err) 42 } 43} 44 45export function formHandler< 46 Req extends IncomingMessage = IncomingMessage, 47 Res extends ServerResponse<Req & { body: unknown }> = ServerResponse< 48 Req & { body: unknown } 49 >, 50>(fn: AsyncHandler<Req & { body: unknown }, Res>): Handler<Req, Res> { 51 return handler(async (req, res, next) => { 52 if (req.method !== 'POST') { 53 return void res.writeHead(405).end() 54 } 55 if (req.headers['content-type'] !== 'application/x-www-form-urlencoded') { 56 return void res.writeHead(415).end('Unsupported Media Type') 57 } 58 59 // Read the request payload 60 const chunks: Uint8Array[] = [] 61 for await (const chunk of req) chunks.push(chunk) 62 const payload = Buffer.concat(chunks).toString('utf-8') 63 64 // Parse the Form URL-encoded payload 65 const body = payload ? Object.fromEntries(new URLSearchParams(payload)) : {} 66 67 // Define the body property on the request object 68 Object.defineProperty(req, 'body', { 69 value: body, 70 writable: false, 71 enumerable: true, 72 configurable: true, 73 }) 74 75 // Call the provided async handler with the modified request 76 return fn(req as Req & { body: unknown }, res, next) 77 }) 78} 79 80export async function startServer( 81 requestListener: RequestListener, 82 { 83 port, 84 gracefulTerminationTimeout, 85 }: { port?: number; gracefulTerminationTimeout?: number } = {}, 86) { 87 const server = createServer(requestListener) 88 const { terminate } = createHttpTerminator({ 89 gracefulTerminationTimeout, 90 server, 91 }) 92 server.listen(port) 93 await once(server, 'listening') 94 return { server, terminate } 95}