|
1 | | -import * as base64 from 'https://deno.land/[email protected]/encoding/base64.ts'; |
| 1 | +const initStart = Date.now(); |
| 2 | + |
2 | 3 | import type { |
3 | 4 | Handler, |
4 | 5 | ConnInfo, |
5 | 6 | } from 'https://deno.land/[email protected]/http/server.ts'; |
6 | 7 |
|
7 | | -interface VercelRequestPayload { |
8 | | - method: string; |
9 | | - path: string; |
10 | | - headers: Record<string, string>; |
11 | | - body: string; |
12 | | -} |
13 | | - |
14 | | -type VercelResponseHeaders = Record<string, string | string[]>; |
15 | | - |
16 | | -interface VercelResponsePayload { |
17 | | - statusCode: number; |
18 | | - headers: VercelResponseHeaders; |
19 | | - encoding: 'base64'; |
20 | | - body: string; |
21 | | -} |
22 | | - |
23 | | -const RUNTIME_PATH = '2018-06-01/runtime'; |
24 | | - |
25 | | -const { _HANDLER, ENTRYPOINT, AWS_LAMBDA_RUNTIME_API } = Deno.env.toObject(); |
26 | | - |
27 | | -Deno.env.delete('SHLVL'); |
28 | | - |
29 | | -function fromVercelRequest(payload: VercelRequestPayload): Request { |
30 | | - const headers = new Headers(payload.headers); |
31 | | - const base = `${headers.get('x-forwarded-proto')}://${headers.get( |
32 | | - 'x-forwarded-host' |
33 | | - )}`; |
34 | | - const url = new URL(payload.path, base); |
35 | | - const body = payload.body ? base64.decode(payload.body) : undefined; |
36 | | - return new Request(url.href, { |
37 | | - method: payload.method, |
38 | | - headers, |
39 | | - body, |
40 | | - }); |
41 | | -} |
42 | | - |
43 | | -function headersToVercelHeaders(headers: Headers): VercelResponseHeaders { |
44 | | - const h: VercelResponseHeaders = {}; |
45 | | - for (const [name, value] of headers) { |
46 | | - const cur = h[name]; |
47 | | - if (typeof cur === 'string') { |
48 | | - h[name] = [cur, value]; |
49 | | - } else if (Array.isArray(cur)) { |
50 | | - cur.push(value); |
51 | | - } else { |
52 | | - h[name] = value; |
53 | | - } |
54 | | - } |
55 | | - return h; |
56 | | -} |
57 | | - |
58 | | -async function toVercelResponse(res: Response): Promise<VercelResponsePayload> { |
59 | | - let body = ''; |
60 | | - const bodyBuffer = await res.arrayBuffer(); |
61 | | - if (bodyBuffer.byteLength > 0) { |
62 | | - body = base64.encode(bodyBuffer); |
63 | | - } |
64 | | - |
65 | | - return { |
66 | | - statusCode: res.status, |
67 | | - headers: headersToVercelHeaders(res.headers), |
68 | | - encoding: 'base64', |
69 | | - body, |
70 | | - }; |
71 | | -} |
72 | | - |
73 | | -async function processEvents(): Promise<void> { |
74 | | - let handler: Handler | null = null; |
75 | | - |
76 | | - while (true) { |
77 | | - const { event, awsRequestId } = await nextInvocation(); |
78 | | - let result: VercelResponsePayload; |
79 | | - try { |
80 | | - if (!handler) { |
81 | | - const mod = await import(`./${_HANDLER}`); |
82 | | - handler = mod.default; |
83 | | - if (typeof handler !== 'function') { |
84 | | - throw new Error('Failed to load handler function'); |
85 | | - } |
86 | | - } |
87 | | - |
88 | | - const payload = JSON.parse(event.body) as VercelRequestPayload; |
89 | | - const req = fromVercelRequest(payload); |
| 8 | +const { _HANDLER, ENTRYPOINT, VERCEL_IPC_FD } = Deno.env.toObject(); |
90 | 9 |
|
91 | | - const connInfo: ConnInfo = { |
92 | | - // TODO: how to properly calculate these? |
93 | | - // @ts-ignore - `rid` is not on the `ConnInfo` interface, but it's required by Oak |
94 | | - rid: 0, |
95 | | - localAddr: { hostname: '127.0.0.1', port: 0, transport: 'tcp' }, |
96 | | - remoteAddr: { |
97 | | - hostname: '127.0.0.1', |
98 | | - port: 0, |
99 | | - transport: 'tcp', |
100 | | - }, |
101 | | - }; |
102 | | - |
103 | | - // Run user code |
104 | | - const res = await handler(req, connInfo); |
105 | | - result = await toVercelResponse(res); |
106 | | - } catch (e: unknown) { |
107 | | - const err = e instanceof Error ? e : new Error(String(e)); |
108 | | - console.error(err); |
109 | | - await invokeError(err, awsRequestId); |
110 | | - continue; |
111 | | - } |
112 | | - await invokeResponse(result, awsRequestId); |
113 | | - } |
| 10 | +function isNetAddr(v: any): v is Deno.NetAddr { |
| 11 | + return v && typeof v.port === 'number'; |
114 | 12 | } |
115 | 13 |
|
116 | | -async function nextInvocation() { |
117 | | - const res = await request('invocation/next'); |
118 | | - |
119 | | - if (res.status !== 200) { |
120 | | - throw new Error( |
121 | | - `Unexpected "/invocation/next" response: ${JSON.stringify(res)}` |
122 | | - ); |
123 | | - } |
124 | | - |
125 | | - const traceId = res.headers.get('lambda-runtime-trace-id'); |
126 | | - if (typeof traceId === 'string') { |
127 | | - Deno.env.set('_X_AMZN_TRACE_ID', traceId); |
128 | | - } else { |
129 | | - Deno.env.delete('_X_AMZN_TRACE_ID'); |
130 | | - } |
| 14 | +if (_HANDLER) { |
131 | 15 |
|
132 | | - const awsRequestId = res.headers.get('lambda-runtime-aws-request-id'); |
133 | | - if (typeof awsRequestId !== 'string') { |
134 | | - throw new Error( |
135 | | - 'Did not receive "lambda-runtime-aws-request-id" header' |
136 | | - ); |
| 16 | + const mod = await import(`./${_HANDLER}`); |
| 17 | + const handler: Handler = mod.default; |
| 18 | + if (typeof handler !== 'function') { |
| 19 | + throw new Error('Failed to load handler function'); |
137 | 20 | } |
138 | 21 |
|
139 | | - const event = JSON.parse(res.body); |
140 | | - return { event, awsRequestId }; |
141 | | -} |
| 22 | + // Spawn HTTP server on ephemeral port |
| 23 | + const listener = Deno.listen({ port: 3030 /* 0 */ }); |
142 | 24 |
|
143 | | -async function invokeResponse( |
144 | | - result: VercelResponsePayload, |
145 | | - awsRequestId: string |
146 | | -) { |
147 | | - const res = await request(`invocation/${awsRequestId}/response`, { |
148 | | - method: 'POST', |
149 | | - headers: { |
150 | | - 'Content-Type': 'application/json', |
151 | | - }, |
152 | | - body: JSON.stringify(result), |
153 | | - }); |
154 | | - if (res.status !== 202) { |
155 | | - throw new Error( |
156 | | - `Unexpected "/invocation/response" response: ${JSON.stringify(res)}` |
157 | | - ); |
| 25 | + if (!isNetAddr(listener.addr)) { |
| 26 | + throw new Error('Server not listening on TCP port'); |
158 | 27 | } |
159 | | -} |
160 | | - |
161 | | -function invokeError(err: Error, awsRequestId: string) { |
162 | | - return postError(`invocation/${awsRequestId}/error`, err); |
163 | | -} |
164 | | - |
165 | | -async function postError(path: string, err: Error): Promise<void> { |
166 | | - const lambdaErr = toLambdaErr(err); |
167 | | - const res = await request(path, { |
168 | | - method: 'POST', |
169 | | - headers: { |
170 | | - 'Content-Type': 'application/json', |
171 | | - 'Lambda-Runtime-Function-Error-Type': 'Unhandled', |
172 | | - }, |
173 | | - body: JSON.stringify(lambdaErr), |
174 | | - }); |
175 | | - if (res.status !== 202) { |
176 | | - throw new Error( |
177 | | - `Unexpected "${path}" response: ${JSON.stringify(res)}` |
178 | | - ); |
| 28 | + const { port } = listener.addr; |
| 29 | + console.log({ port }); |
| 30 | + console.log({ VERCEL_IPC_FD }); |
| 31 | + |
| 32 | + const ipcSock = await Deno.connect({ path: `/dev/fd/${VERCEL_IPC_FD}`, transport: "unix" }); |
| 33 | + ipcSock.write(new TextEncoder().encode(`${JSON.stringify({ |
| 34 | + "type": "server-started", |
| 35 | + "payload": { |
| 36 | + "initDuration": Date.now() - initStart, // duration to init the process, connect to the unix domain socket & start the HTTP server in milliseconds |
| 37 | + "httpPort": port // the port of the HTTP server |
| 38 | + } |
| 39 | + })}\0`)); |
| 40 | + |
| 41 | + // Serve HTTP requests to handler function |
| 42 | + const conn = await listener.accept(); |
| 43 | + const s = Deno.serveHttp(conn); |
| 44 | + for await (const req of s) { |
| 45 | + const connInfo: ConnInfo = { |
| 46 | + // @ts-ignore - `rid` is not on the `ConnInfo` interface, but it's required by Oak |
| 47 | + rid: conn.rid, |
| 48 | + localAddr: conn.localAddr, |
| 49 | + remoteAddr: conn.remoteAddr, |
| 50 | + }; |
| 51 | + const requestId = req.headers.get('x-vercel-internal-request-id'); |
| 52 | + const invocationId = req.headers.get('x-vercel-internal-invocation-id'); |
| 53 | + Promise.resolve(handler(req.request, connInfo)).then((res: Response) => { |
| 54 | + req.respondWith(res); |
| 55 | + // TODO: figure out how to wait for HTTP request to complete |
| 56 | + setTimeout(() => { |
| 57 | + const endPayload = { |
| 58 | + "type": "end", |
| 59 | + "payload": { |
| 60 | + "context": { |
| 61 | + invocationId, // invocation-id from the http request |
| 62 | + requestId // request-id from the http request |
| 63 | + }, |
| 64 | + "error": "" // optional |
| 65 | + } |
| 66 | + }; |
| 67 | + ipcSock.write(new TextEncoder().encode(`${JSON.stringify(endPayload)}\0`)); |
| 68 | + }, 1000); |
| 69 | + }); |
179 | 70 | } |
180 | | -} |
181 | | - |
182 | | -async function request(path: string, options?: RequestInit) { |
183 | | - const url = `http://${AWS_LAMBDA_RUNTIME_API}/${RUNTIME_PATH}/${path}`; |
184 | | - const res = await fetch(url, options); |
185 | | - const body = await res.text(); |
186 | | - return { |
187 | | - status: res.status, |
188 | | - headers: res.headers, |
189 | | - body, |
190 | | - }; |
191 | | -} |
192 | | - |
193 | | -function toLambdaErr({ name, message, stack }: Error) { |
194 | | - return { |
195 | | - errorType: name, |
196 | | - errorMessage: message, |
197 | | - stackTrace: (stack || '').split('\n').slice(1), |
198 | | - }; |
199 | | -} |
200 | | - |
201 | | -if (_HANDLER) { |
202 | | - // Runtime - execute the runtime loop |
203 | | - processEvents().catch((err) => { |
204 | | - console.error(err); |
205 | | - Deno.exit(1); |
206 | | - }); |
207 | 71 | } else { |
208 | 72 | // Build - import the entrypoint so that it gets cached |
209 | 73 | await import(ENTRYPOINT); |
|
0 commit comments