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}