From ee60a5c55cdafc96268efdebb73aedb3a2ac88fa Mon Sep 17 00:00:00 2001 From: Ashcon Partovi Date: Wed, 22 Feb 2023 10:34:16 -0800 Subject: [PATCH] Add runtime layer for Bun on AWS Lambda (#2009) --- packages/bun-lambda/.gitignore | 5 + packages/bun-lambda/README.md | 92 +++ packages/bun-lambda/bootstrap | 3 + packages/bun-lambda/bun.lockb | Bin 0 -> 174029 bytes packages/bun-lambda/example/lambda.ts | 33 + packages/bun-lambda/package.json | 16 + packages/bun-lambda/runtime.ts | 821 +++++++++++++++++++ packages/bun-lambda/scripts/build-layer.ts | 101 +++ packages/bun-lambda/scripts/publish-layer.ts | 91 ++ packages/bun-lambda/tsconfig.json | 12 + 10 files changed, 1174 insertions(+) create mode 100644 packages/bun-lambda/.gitignore create mode 100644 packages/bun-lambda/README.md create mode 100755 packages/bun-lambda/bootstrap create mode 100755 packages/bun-lambda/bun.lockb create mode 100644 packages/bun-lambda/example/lambda.ts create mode 100644 packages/bun-lambda/package.json create mode 100755 packages/bun-lambda/runtime.ts create mode 100644 packages/bun-lambda/scripts/build-layer.ts create mode 100644 packages/bun-lambda/scripts/publish-layer.ts create mode 100644 packages/bun-lambda/tsconfig.json diff --git a/packages/bun-lambda/.gitignore b/packages/bun-lambda/.gitignore new file mode 100644 index 0000000000..0400121300 --- /dev/null +++ b/packages/bun-lambda/.gitignore @@ -0,0 +1,5 @@ +.DS_Store +.serverless/ +node_modules/ +bun-lambda-layer/ +*.zip diff --git a/packages/bun-lambda/README.md b/packages/bun-lambda/README.md new file mode 100644 index 0000000000..015a6e26c7 --- /dev/null +++ b/packages/bun-lambda/README.md @@ -0,0 +1,92 @@ +# bun-lambda + +A custom runtime layer that runs Bun on AWS Lambda. + +## Setup + +First, you will need to deploy the layer to your AWS account. Clone this repository and run the `publish-layer` script to get started. + +```sh +git clone git@github.com:oven-sh/bun.git +cd packages/bun-lambda +bun install +bun run publish-layer +``` + +### `bun run build-layer` + +Builds a Lambda layer for Bun and saves it to a `.zip` file. + +| Flag | Description | Default | +| ----------- | -------------------------------------------------------------------- | ---------------------- | +| `--arch` | The architecture, either: "x64" or "aarch64" | aarch64 | +| `--release` | The release of Bun, either: "latest", "canary", or a release "x.y.z" | latest | +| `--output` | The path to write the layer as a `.zip`. | ./bun-lambda-layer.zip | + +Example: + +```sh +bun run build-layer -- \ + --arch x64 \ + --release canary \ + --output /path/to/layer.zip +``` + +### `bun run publish-layer` + +Builds a Lambda layer for Bun then publishes it to your AWS account. + +| Flag | Description | Default | +| ---------- | ----------------------------------------- | ------- | +| `--layer` | The layer name. | bun | +| `--region` | The region name, or "\*" for all regions. | | +| `--public` | If the layer should be public. | false | + +Example: + +```sh +bun run publish-layer -- \ + --arch aarch64 \ + --release latest \ + --output /path/to/layer.zip \ + --region us-east-1 +``` + +## Usage + +Once you publish the layer to your AWS account, you can create a Lambda function that uses the layer. + +Here's an example function that can run on Lambda using the layer for Bun: + +### HTTP events + +When an event is triggered from [API Gateway](https://docs.aws.amazon.com/lambda/latest/dg/services-apigateway.html), the layer transforms the event payload into a [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request). This means you can test your Lambda function locally using `bun run`, without any code changes. + +```ts +export default { + async fetch(request: Request): Promise { + console.log(request.headers.get("x-amzn-function-arn")); + // ... + return new Response("Hello from Lambda!", { + status: 200, + headers: { + "Content-Type": "text/plain", + }, + }); + }, +}; +``` + +### Non-HTTP events + +For non-HTTP events — S3, SQS, EventBridge, etc. — the event payload is the body of the [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request). + +```ts +export default { + async fetch(request: Request): Promise { + const event = await request.json(); + // ... + return new Response(); + }, +}; +``` diff --git a/packages/bun-lambda/bootstrap b/packages/bun-lambda/bootstrap new file mode 100755 index 0000000000..38387b2c9c --- /dev/null +++ b/packages/bun-lambda/bootstrap @@ -0,0 +1,3 @@ +#! /bin/sh +export BUN_INSTALL_CACHE_DIR=/tmp/bun/cache +exec /opt/bun --cwd $LAMBDA_TASK_ROOT /opt/runtime.ts diff --git a/packages/bun-lambda/bun.lockb b/packages/bun-lambda/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..d826da88d3ce771882f099a68bcd1bcb633d9f14 GIT binary patch literal 174029 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 DSmwiK literal 0 HcmV?d00001 diff --git a/packages/bun-lambda/example/lambda.ts b/packages/bun-lambda/example/lambda.ts new file mode 100644 index 0000000000..ec96cf6c91 --- /dev/null +++ b/packages/bun-lambda/example/lambda.ts @@ -0,0 +1,33 @@ +import type { Server, ServerWebSocket } from "bun"; + +export default { + async fetch(request: Request, server: Server): Promise { + console.log("Request", { + url: request.url, + method: request.method, + headers: request.headers.toJSON(), + body: request.body ? await request.text() : null, + }); + if (server.upgrade(request)) { + console.log("WebSocket upgraded"); + return; + } + return new Response("Hello from Bun on Lambda!", { + status: 200, + headers: { + "Content-Type": "text/plain;charset=utf-8", + }, + }); + }, + websocket: { + async open(ws: ServerWebSocket): Promise { + console.log("WebSocket opened"); + }, + async message(ws: ServerWebSocket, message: string): Promise { + console.log("WebSocket message", message); + }, + async close(ws: ServerWebSocket, code: number, reason?: string): Promise { + console.log("WebSocket closed", { code, reason }); + }, + }, +}; diff --git a/packages/bun-lambda/package.json b/packages/bun-lambda/package.json new file mode 100644 index 0000000000..b558b15fa4 --- /dev/null +++ b/packages/bun-lambda/package.json @@ -0,0 +1,16 @@ +{ + "devDependencies": { + "bun-types": "^0.4.0", + "jszip": "^3.10.1", + "oclif": "^3.6.5", + "prettier": "^2.8.2" + }, + "dependencies": { + "aws4fetch": "^1.0.17" + }, + "scripts": { + "build-layer": "bun scripts/build-layer.ts", + "publish-layer": "bun scripts/publish-layer.ts", + "format": "prettier --write ." + } +} diff --git a/packages/bun-lambda/runtime.ts b/packages/bun-lambda/runtime.ts new file mode 100755 index 0000000000..c29462b9b1 --- /dev/null +++ b/packages/bun-lambda/runtime.ts @@ -0,0 +1,821 @@ +import type { Server, ServerWebSocket } from "bun"; +import { AwsClient } from "aws4fetch"; + +type Lambda = { + fetch: (request: Request, server: Server) => Promise; + error?: (error: unknown) => Promise; + websocket?: { + open?: (ws: ServerWebSocket) => Promise; + message?: (ws: ServerWebSocket, message: string) => Promise; + close?: (ws: ServerWebSocket, code: number, reason: string) => Promise; + }; +}; + +let requestId: string | undefined; +let traceId: string | undefined; +let functionArn: string | undefined; +let aws: AwsClient | undefined; + +let logger = console.log; + +function log(level: string, ...args: any[]): void { + if (!args.length) { + return; + } + const message = Bun.inspect(...args).replace(/\n/g, "\r"); + if (requestId === undefined) { + logger(level, message); + } else { + logger(level, `RequestId: ${requestId}`, message); + } +} + +console.log = (...args: any[]) => log("INFO", ...args); +console.info = (...args: any[]) => log("INFO", ...args); +console.warn = (...args: any[]) => log("WARN", ...args); +console.error = (...args: any[]) => log("ERROR", ...args); +console.debug = (...args: any[]) => log("DEBUG", ...args); +console.trace = (...args: any[]) => log("TRACE", ...args); + +let warnings: Set | undefined; + +function warnOnce(message: string, ...args: any[]): void { + if (warnings === undefined) { + warnings = new Set(); + } + if (warnings.has(message)) { + return; + } + warnings.add(message); + console.warn(message, ...args); +} + +function reset(): void { + requestId = undefined; + traceId = undefined; + warnings = undefined; +} + +function exit(...cause: any[]): never { + console.error(...cause); + process.exit(1); +} + +function env(name: string, fallback?: string): string { + const value = process.env[name] ?? fallback ?? null; + if (value === null) { + exit(`Runtime failed to find the '${name}' environment variable`); + } + return value; +} + +const runtimeUrl = new URL(`http://${env("AWS_LAMBDA_RUNTIME_API")}/2018-06-01/`); + +async function fetch(url: string, options?: RequestInit): Promise { + const { href } = new URL(url, runtimeUrl); + const response = await globalThis.fetch(href, { + ...options, + timeout: false, + }); + if (!response.ok) { + exit(`Runtime failed to send request to Lambda [status: ${response.status}]`); + } + return response; +} + +async function fetchAws(url: string, options?: RequestInit): Promise { + if (aws === undefined) { + aws = new AwsClient({ + accessKeyId: env("AWS_ACCESS_KEY_ID"), + secretAccessKey: env("AWS_SECRET_ACCESS_KEY"), + sessionToken: env("AWS_SESSION_TOKEN"), + region: env("AWS_REGION"), + }); + } + return aws.fetch(url, options); +} + +type LambdaError = { + readonly errorType: string; + readonly errorMessage: string; + readonly stackTrace?: string[]; +}; + +function formatError(error: unknown): LambdaError { + if (error instanceof Error) { + return { + errorType: error.name, + errorMessage: error.message, + stackTrace: error.stack?.split("\n").filter(line => !line.includes(" /opt/runtime.ts")), + }; + } + return { + errorType: "Error", + errorMessage: Bun.inspect(error), + }; +} + +async function sendError(type: string, cause: unknown): Promise { + console.error(cause); + await fetch(requestId === undefined ? "runtime/init/error" : `runtime/invocation/${requestId}/error`, { + method: "POST", + headers: { + "Content-Type": "application/vnd.aws.lambda.error+json", + "Lambda-Runtime-Function-Error-Type": `Bun.${type}`, + }, + body: JSON.stringify(formatError(cause)), + }); +} + +async function throwError(type: string, cause: unknown): Promise { + await sendError(type, cause); + exit(); +} + +async function init(): Promise { + const handlerName = env("_HANDLER"); + const index = handlerName.lastIndexOf("."); + const fileName = handlerName.substring(0, index); + const filePath = `${env("LAMBDA_TASK_ROOT")}/${fileName}`; + let file; + try { + file = await import(filePath); + } catch (cause) { + if (cause instanceof Error && cause.message.startsWith("Cannot find module")) { + return throwError("FileDoesNotExist", `Did not find a file named '${fileName}'`); + } + return throwError("InitError", cause); + } + const moduleName = handlerName.substring(index + 1) || "default"; + let module = file[moduleName]; + if (moduleName !== "default") { + module = { + fetch: module, + }; + } + const { fetch, websocket } = module; + if (typeof fetch !== "function") { + return throwError( + fetch === undefined ? "MethodDoesNotExist" : "MethodIsNotAFunction", + moduleName === "default" + ? `${fileName} does not have a default export with a function named 'fetch'` + : `${fileName} does not export a function named '${moduleName}'`, + ); + } + if (websocket === undefined) { + return module; + } + for (const name of ["open", "message", "close"]) { + const method = websocket[name]; + if (method === undefined) { + continue; + } + if (typeof method !== "function") { + return throwError( + "MethodIsNotAFunction", + `${fileName} does not have a function named '${name}' on the default 'websocket' property`, + ); + } + } + return module; +} + +type LambdaRequest = { + readonly requestId: string; + readonly traceId: string; + readonly functionArn: string; + readonly deadlineMs: number | null; + readonly event: E; +}; + +async function receiveRequest(): Promise { + const response = await fetch("runtime/invocation/next"); + requestId = response.headers.get("Lambda-Runtime-Aws-Request-Id") ?? undefined; + if (requestId === undefined) { + exit("Runtime received a request without a request ID"); + } + traceId = response.headers.get("Lambda-Runtime-Trace-Id") ?? undefined; + if (traceId === undefined) { + exit("Runtime received a request without a trace ID"); + } + process.env["_X_AMZN_TRACE_ID"] = traceId; + functionArn = response.headers.get("Lambda-Runtime-Invoked-Function-Arn") ?? undefined; + if (functionArn === undefined) { + exit("Runtime received a request without a function ARN"); + } + const deadlineMs = parseInt(response.headers.get("Lambda-Runtime-Deadline-Ms") ?? "0") || null; + let event; + try { + event = await response.json(); + } catch (cause) { + exit("Runtime received a request with invalid JSON", cause); + } + return { + requestId, + traceId, + functionArn, + deadlineMs, + event, + }; +} + +type LambdaResponse = { + readonly statusCode: number; + readonly headers?: Record; + readonly isBase64Encoded?: boolean; + readonly body?: string; + readonly multiValueHeaders?: Record; + readonly cookies?: string[]; +}; + +async function formatResponse(response: Response): Promise { + const statusCode = response.status; + const headers = response.headers.toJSON(); + if (statusCode === 101) { + const protocol = headers["sec-websocket-protocol"]; + if (protocol === undefined) { + return { + statusCode: 200, + }; + } + return { + statusCode: 200, + headers: { + "Sec-WebSocket-Protocol": protocol, + }, + }; + } + const mime = headers["content-type"]; + const isBase64Encoded = !mime || (!mime.startsWith("text/") && !mime.startsWith("application/json")); + const body = isBase64Encoded ? Buffer.from(await response.arrayBuffer()).toString("base64") : await response.text(); + delete headers["set-cookie"]; + const cookies = response.headers.getAll("Set-Cookie"); + if (cookies.length === 0) { + return { + statusCode, + headers, + isBase64Encoded, + body, + }; + } + return { + statusCode, + headers, + cookies, + multiValueHeaders: { + "Set-Cookie": cookies, + }, + isBase64Encoded, + body, + }; +} + +async function sendResponse(response: unknown): Promise { + if (requestId === undefined) { + exit("Runtime attempted to send a response without a request ID"); + } + await fetch(`runtime/invocation/${requestId}/response`, { + method: "POST", + body: response === null ? null : JSON.stringify(response), + }); +} + +function formatBody(body?: string, isBase64Encoded?: boolean): string | null { + if (body === undefined) { + return null; + } + if (!isBase64Encoded) { + return body; + } + return Buffer.from(body).toString("base64"); +} + +type HttpEventV1 = { + readonly version: "1.0"; + readonly requestContext: { + readonly requestId: string; + readonly domainName: string; + readonly httpMethod: string; + readonly path: string; + }; + readonly headers: Record; + readonly multiValueHeaders?: Record; + readonly queryStringParameters?: Record; + readonly multiValueQueryStringParameters?: Record; + readonly isBase64Encoded: boolean; + readonly body?: string; +}; + +function isHttpEventV1(event: any): event is HttpEventV1 { + return event.version === "1.0" && typeof event.requestContext === "object"; +} + +function formatHttpEventV1(event: HttpEventV1): Request { + const request = event.requestContext; + const headers = new Headers(); + for (const [name, value] of Object.entries(event.headers)) { + headers.append(name, value); + } + for (const [name, values] of Object.entries(event.multiValueHeaders ?? {})) { + for (const value of values) { + headers.append(name, value); + } + } + const hostname = headers.get("Host") ?? request.domainName; + const proto = headers.get("X-Forwarded-Proto") ?? "http"; + const url = new URL(request.path, `${proto}://${hostname}/`); + for (const [name, value] of new URLSearchParams(event.queryStringParameters)) { + url.searchParams.append(name, value); + } + for (const [name, values] of Object.entries(event.multiValueQueryStringParameters ?? {})) { + for (const value of values ?? []) { + url.searchParams.append(name, value); + } + } + return new Request(url.toString(), { + method: request.httpMethod, + headers, + body: formatBody(event.body, event.isBase64Encoded), + }); +} + +type HttpEventV2 = { + readonly version: "2.0"; + readonly requestContext: { + readonly requestId: string; + readonly domainName: string; + readonly http: { + readonly method: string; + readonly path: string; + }; + }; + readonly headers: Record; + readonly queryStringParameters?: Record; + readonly cookies?: string[]; + readonly isBase64Encoded: boolean; + readonly body?: string; +}; + +function isHttpEventV2(event: any): event is HttpEventV2 { + return event.version === "2.0" && typeof event.requestContext === "object"; +} + +function formatHttpEventV2(event: HttpEventV2): Request { + const request = event.requestContext; + const headers = new Headers(); + for (const [name, values] of Object.entries(event.headers)) { + for (const value of values.split(",")) { + headers.append(name, value); + } + } + for (const [name, values] of Object.entries(event.queryStringParameters ?? {})) { + for (const value of values.split(",")) { + headers.append(name, value); + } + } + for (const cookie of event.cookies ?? []) { + headers.append("Set-Cookie", cookie); + } + const hostname = headers.get("Host") ?? request.domainName; + const proto = headers.get("X-Forwarded-Proto") ?? "http"; + const url = new URL(request.http.path, `${proto}://${hostname}/`); + return new Request(url.toString(), { + method: request.http.method, + headers, + body: formatBody(event.body, event.isBase64Encoded), + }); +} + +type WebSocketEvent = { + readonly headers: Record; + readonly multiValueHeaders: Record; + readonly isBase64Encoded: boolean; + readonly body?: string; + readonly requestContext: { + readonly apiId: string; + readonly requestId: string; + readonly connectionId: string; + readonly domainName: string; + readonly stage: string; + readonly identity: { + readonly sourceIp: string; + }; + } & ( + | { + readonly eventType: "CONNECT"; + } + | { + readonly eventType: "MESSAGE"; + } + | { + readonly eventType: "DISCONNECT"; + readonly disconnectStatusCode: number; + readonly disconnectReason: string; + } + ); +}; + +function isWebSocketEvent(event: any): event is WebSocketEvent { + return typeof event.requestContext === "object" && typeof event.requestContext.connectionId === "string"; +} + +function isWebSocketUpgrade(event: any): event is WebSocketEvent { + return isWebSocketEvent(event) && event.requestContext.eventType === "CONNECT"; +} + +function formatWebSocketUpgrade(event: WebSocketEvent): Request { + const request = event.requestContext; + const headers = new Headers(); + headers.set("Upgrade", "websocket"); + headers.set("x-amzn-connection-id", request.connectionId); + for (const [name, values] of Object.entries(event.multiValueHeaders as any)) { + for (const value of (values as any) ?? []) { + headers.append(name, value); + } + } + const hostname = headers.get("Host") ?? request.domainName; + const proto = headers.get("X-Forwarded-Proto") ?? "http"; + const url = new URL(`${proto}://${hostname}/${request.stage}`); + return new Request(url.toString(), { + headers, + body: formatBody(event.body, event.isBase64Encoded), + }); +} + +function formatUnknownEvent(event: unknown): Request { + return new Request("https://lambda/", { + method: "POST", + body: JSON.stringify(event), + headers: { + "Content-Type": "application/json;charset=utf-8", + }, + }); +} + +function formatRequest(input: LambdaRequest): Request | undefined { + const { event, requestId, traceId, functionArn, deadlineMs } = input; + let request: Request; + if (isHttpEventV2(event)) { + request = formatHttpEventV2(event); + } else if (isHttpEventV1(event)) { + request = formatHttpEventV1(event); + } else if (isWebSocketEvent(event)) { + if (!isWebSocketUpgrade(event)) { + return undefined; + } + request = formatWebSocketUpgrade(event); + } else { + request = formatUnknownEvent(input); + } + request.headers.set("x-amzn-requestid", requestId); + request.headers.set("x-amzn-trace-id", traceId); + request.headers.set("x-amzn-function-arn", functionArn); + if (deadlineMs !== null) { + request.headers.set("x-amzn-deadline-ms", `${deadlineMs}`); + } + // @ts-ignore: Attach the original event to the Request + request.aws = event; + return request; +} + +class LambdaServer implements Server { + #lambda: Lambda; + #webSockets: Map; + #upgrade: Response | null; + pendingRequests: number; + pendingWebSockets: number; + port: number; + hostname: string; + development: boolean; + + constructor(lambda: Lambda) { + this.#lambda = lambda; + this.#webSockets = new Map(); + this.#upgrade = null; + this.pendingRequests = 0; + this.pendingWebSockets = 0; + this.port = 80; + this.hostname = "lambda"; + this.development = false; + } + + async accept(request: LambdaRequest): Promise { + const deadlineMs = request.deadlineMs === null ? Date.now() + 60_000 : request.deadlineMs; + const durationMs = Math.max(1, deadlineMs - Date.now()); + let response: unknown; + try { + response = await Promise.race([ + new Promise(resolve => setTimeout(resolve, durationMs)), + this.#acceptRequest(request), + ]); + } catch (cause) { + await sendError("RequestError", cause); + return; + } + if (response === undefined) { + await sendError("TimeoutError", "Function timed out"); + return; + } + return response; + } + + async #acceptRequest(event: LambdaRequest): Promise { + const request = formatRequest(event); + let response: Response | undefined; + if (request === undefined) { + await this.#acceptWebSocket(event.event); + } else { + response = await this.fetch(request); + if (response.status === 101) { + await this.#acceptWebSocket(event.event); + } + } + if (response === undefined) { + return { + statusCode: 200, + }; + } + if (!request?.headers.has("Host")) { + return response.text(); + } + return formatResponse(response); + } + + async #acceptWebSocket(event: WebSocketEvent): Promise { + const request = event.requestContext; + const { connectionId, eventType } = request; + const webSocket = this.#webSockets.get(connectionId); + if (webSocket === undefined || this.#lambda.websocket === undefined) { + return; + } + const { open, message, close } = this.#lambda.websocket; + switch (eventType) { + case "CONNECT": { + if (open) { + await open(webSocket); + } + break; + } + case "MESSAGE": { + if (message) { + const body = formatBody(event.body, event.isBase64Encoded); + if (body !== null) { + await message(webSocket, body); + } + } + break; + } + case "DISCONNECT": { + try { + if (close) { + const { disconnectStatusCode: code, disconnectReason: reason } = request; + await close(webSocket, code, reason); + } + } finally { + this.#webSockets.delete(connectionId); + this.pendingWebSockets--; + } + break; + } + } + } + + stop(): void { + exit("Runtime exited because Server.stop() was called"); + } + + reload(options: any): void { + this.#lambda = { + fetch: options.fetch ?? this.#lambda.fetch, + error: options.error ?? this.#lambda.error, + websocket: options.websocket ?? this.#lambda.websocket, + }; + this.port = + typeof options.port === "number" + ? options.port + : typeof options.port === "string" + ? parseInt(options.port) + : this.port; + this.hostname = options.hostname ?? this.hostname; + this.development = options.development ?? this.development; + } + + async fetch(request: Request): Promise { + this.pendingRequests++; + try { + let response = await this.#lambda.fetch(request, this); + if (response instanceof Response) { + return response; + } + if (response === undefined && this.#upgrade !== null) { + return this.#upgrade; + } + throw new Error("fetch() did not return a Response"); + } catch (cause) { + console.error(cause); + if (this.#lambda.error !== undefined) { + try { + return await this.#lambda.error(cause); + } catch (cause) { + console.error(cause); + } + } + return new Response(null, { status: 500 }); + } finally { + this.pendingRequests--; + this.#upgrade = null; + } + } + + upgrade( + request: Request, + options?: { + headers?: HeadersInit; + data?: T; + }, + ): boolean { + if (request.method === "GET" && request.headers.get("Upgrade")?.toLowerCase() === "websocket") { + this.#upgrade = new Response(null, { + status: 101, + headers: options?.headers, + }); + if ("aws" in request && isWebSocketUpgrade(request.aws)) { + const { connectionId } = request.aws.requestContext; + this.#webSockets.set(connectionId, new LambdaWebSocket(request.aws, options?.data)); + this.pendingWebSockets++; + } + return true; + } + return false; + } + + publish(topic: string, data: string | ArrayBuffer | ArrayBufferView, compress?: boolean): number { + let count = 0; + for (const webSocket of this.#webSockets.values()) { + count += webSocket.publish(topic, data, compress) ? 1 : 0; + } + return count; + } +} + +class LambdaWebSocket implements ServerWebSocket { + #connectionId: string; + #url: string; + #invokeArn: string; + #topics: Set | null; + remoteAddress: string; + readyState: 0 | 2 | 1 | -1 | 3; + binaryType?: "arraybuffer" | "uint8array"; + data: any; + + constructor(event: WebSocketEvent, data?: any) { + const request = event.requestContext; + this.#connectionId = `${request.connectionId}`; + this.#url = `https://${request.domainName}/${request.stage}/@connections/${this.#connectionId}`; + const [region, accountId] = (functionArn ?? "").split(":").slice(3, 5); + this.#invokeArn = `arn:aws:execute-api:${region}:${accountId}:${request.apiId}/${request.stage}/*`; + this.#topics = null; + this.remoteAddress = request.identity.sourceIp; + this.readyState = 1; // WebSocket.OPEN + this.data = data; + } + + send(data: string | ArrayBuffer | ArrayBufferView, compress?: boolean): number { + if (typeof data === "string") { + return this.sendText(data, compress); + } + if (data instanceof ArrayBuffer) { + return this.sendBinary(new Uint8Array(data), compress); + } + const buffer = new Uint8Array(data.buffer, data.byteOffset, data.byteLength); + return this.sendBinary(buffer, compress); + } + + sendText(data: string, compress?: boolean): number { + fetchAws(this.#url, { + method: "POST", + body: data, + }) + .then(({ status }) => { + if (status === 403) { + warnOnce( + "Failed to send WebSocket message due to insufficient IAM permissions", + `Assign the following IAM policy to ${functionArn} to fix this issue:`, + { + Version: "2012-10-17", + Statement: [ + { + Effect: "Allow", + Action: ["execute-api:Invoke"], + Resource: [this.#invokeArn], + }, + ], + }, + ); + } else { + warnOnce(`Failed to send WebSocket message due to a ${status} error`); + } + }) + .catch(error => { + warnOnce("Failed to send WebSocket message", error); + }); + return data.length; + } + + sendBinary(data: Uint8Array, compress?: boolean): number { + warnOnce( + "Lambda does not support binary WebSocket messages", + "https://docs.aws.amazon.com/apigateway/latest/developerguide/websocket-api-develop-binary-media-types.html", + ); + const base64 = Buffer.from(data).toString("base64"); + return this.sendText(base64, compress); + } + + publish(topic: string, data: string | ArrayBuffer | ArrayBufferView, compress?: boolean): number { + if (this.isSubscribed(topic)) { + return this.send(data, compress); + } + return -1; + } + + publishText(topic: string, data: string, compress?: boolean): number { + if (this.isSubscribed(topic)) { + return this.sendText(data, compress); + } + return -1; + } + + publishBinary(topic: string, data: Uint8Array, compress?: boolean): number { + if (this.isSubscribed(topic)) { + return this.sendBinary(data, compress); + } + return -1; + } + + close(code?: number, reason?: string): void { + // TODO: code? reason? + fetchAws(this.#url, { + method: "DELETE", + }) + .then(({ status }) => { + if (status === 403) { + warnOnce( + "Failed to close WebSocket due to insufficient IAM permissions", + `Assign the following IAM policy to ${functionArn} to fix this issue:`, + { + Version: "2012-10-17", + Statement: [ + { + Effect: "Allow", + Action: ["execute-api:Invoke"], + Resource: [this.#invokeArn], + }, + ], + }, + ); + } else { + warnOnce(`Failed to close WebSocket due to a ${status} error`); + } + }) + .catch(error => { + warnOnce("Failed to close WebSocket", error); + }); + this.readyState = 3; // WebSocket.CLOSED; + } + + subscribe(topic: string): void { + if (this.#topics === null) { + this.#topics = new Set(); + } + this.#topics.add(topic); + } + + unsubscribe(topic: string): void { + if (this.#topics !== null) { + this.#topics.delete(topic); + } + } + + isSubscribed(topic: string): boolean { + return this.#topics !== null && this.#topics.has(topic); + } + + cork(callback: (ws: ServerWebSocket) => any): void | Promise { + // Lambda does not support sending multiple messages at a time. + return callback(this); + } +} + +const lambda = await init(); +const server = new LambdaServer(lambda); +while (true) { + try { + const request = await receiveRequest(); + const response = await server.accept(request); + if (response !== undefined) { + await sendResponse(response); + } + } finally { + reset(); + } +} diff --git a/packages/bun-lambda/scripts/build-layer.ts b/packages/bun-lambda/scripts/build-layer.ts new file mode 100644 index 0000000000..65eeac0838 --- /dev/null +++ b/packages/bun-lambda/scripts/build-layer.ts @@ -0,0 +1,101 @@ +// HACK: https://github.com/oven-sh/bun/issues/2081 +process.stdout.getWindowSize = () => [80, 80]; +process.stderr.getWindowSize = () => [80, 80]; + +import { createReadStream, createWriteStream } from "node:fs"; +import { join } from "node:path"; +import { Command, Flags } from "@oclif/core"; +import JSZip from "jszip"; + +export class BuildCommand extends Command { + static summary = "Build a custom Lambda layer for Bun."; + + static flags = { + arch: Flags.string({ + description: "The architecture type to support.", + options: ["x64", "aarch64"], + default: "aarch64", + }), + release: Flags.string({ + description: "The release of Bun to install.", + default: "latest", + }), + url: Flags.string({ + description: "A custom URL to download Bun.", + exclusive: ["release"], + }), + output: Flags.file({ + exists: false, + default: async () => "bun-lambda-layer.zip", + }), + layer: Flags.string({ + description: "The name of the Lambda layer.", + multiple: true, + default: ["bun"], + }), + region: Flags.string({ + description: "The region to publish the layer.", + multiple: true, + default: [], + }), + public: Flags.boolean({ + description: "If the layer should be public.", + default: false, + }), + }; + + async run() { + const result = await this.parse(BuildCommand); + const { flags } = result; + this.debug("Options:", flags); + const { arch, release, url, output } = flags; + const { href } = new URL(url ?? `https://bun.sh/download/${release}/linux/${arch}?avx2=true`); + this.log("Downloading...", href); + const response = await fetch(href, { + headers: { + "User-Agent": "bun-lambda", + }, + }); + if (response.url !== href) { + this.debug("Redirected URL:", response.url); + } + this.debug("Response:", response.status, response.statusText); + if (!response.ok) { + const reason = await response.text(); + this.error(reason, { exit: 1 }); + } + this.log("Extracting..."); + const buffer = await response.arrayBuffer(); + let archive; + try { + archive = await JSZip.loadAsync(buffer); + } catch (cause) { + this.debug(cause); + this.error("Failed to unzip file:", { exit: 1 }); + } + this.debug("Extracted archive:", Object.keys(archive.files)); + const bun = archive.filter((_, { dir, name }) => !dir && name.endsWith("bun"))[0]; + if (!bun) { + this.error("Failed to find executable in zip", { exit: 1 }); + } + const cwd = bun.name.split("/")[0]; + archive = archive.folder(cwd) ?? archive; + for (const filename of ["bootstrap", "runtime.ts"]) { + const path = join(__dirname, "..", filename); + archive.file(filename, createReadStream(path)); + } + this.log("Saving...", output); + archive + .generateNodeStream({ + streamFiles: true, + compression: "DEFLATE", + compressionOptions: { + level: 9, + }, + }) + .pipe(createWriteStream(output)); + this.log("Saved"); + } +} + +await BuildCommand.run(process.argv.slice(2)); diff --git a/packages/bun-lambda/scripts/publish-layer.ts b/packages/bun-lambda/scripts/publish-layer.ts new file mode 100644 index 0000000000..b7129fc50b --- /dev/null +++ b/packages/bun-lambda/scripts/publish-layer.ts @@ -0,0 +1,91 @@ +import { spawnSync } from "node:child_process"; +import { BuildCommand } from "./build-layer"; + +export class PublishCommand extends BuildCommand { + static summary = "Publish a custom Lambda layer for Bun."; + + #aws(args: string[]): string { + this.debug("$", "aws", ...args); + const { status, stdout, stderr } = spawnSync("aws", args, { + stdio: "pipe", + }); + const result = stdout.toString("utf-8").trim(); + if (status === 0) { + return result; + } + const reason = stderr.toString("utf-8").trim() || result; + throw new Error(`aws ${args.join(" ")} exited with ${status}: ${reason}`); + } + + async run() { + const { flags } = await this.parse(PublishCommand); + this.debug("Options:", flags); + try { + const version = this.#aws(["--version"]); + this.debug("AWS CLI:", version); + } catch (error) { + this.debug(error); + this.error( + "Install the `aws` CLI to continue: https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html", + { exit: 1 }, + ); + } + const { layer, region, arch, output, public: isPublic } = flags; + if (region.includes("*")) { + // prettier-ignore + const result = this.#aws([ + "ec2", + "describe-regions", + "--query", "Regions[].RegionName", + "--output", "json" + ]); + region.length = 0; + for (const name of JSON.parse(result)) { + region.push(name); + } + } else if (!region.length) { + // prettier-ignore + region.push(this.#aws([ + "configure", + "get", + "region" + ])); + } + this.log("Publishing..."); + for (const regionName of region) { + for (const layerName of layer) { + // prettier-ignore + const result = this.#aws([ + "lambda", + "publish-layer-version", + "--layer-name", layerName, + "--region", regionName, + "--description", "Bun is an incredibly fast JavaScript runtime, bundler, transpiler, and package manager.", + "--license-info", "MIT", + "--compatible-architectures", arch === "x64" ? "x86_64" : "arm64", + "--compatible-runtimes", "provided.al2", "provided", + "--zip-file", `fileb://${output}`, + "--output", "json", + ]); + const { LayerVersionArn } = JSON.parse(result); + this.log("Published", LayerVersionArn); + if (isPublic) { + // prettier-ignore + this.#aws([ + "lambda", + "add-layer-version-permission", + "--layer-name", layerName, + "--region", regionName, + "--version-number", LayerVersionArn.split(":").pop(), + "--statement-id", `${layerName}-public`, + "--action", "lambda:GetLayerVersion", + "--principal", "*", + ]); + } + } + } + this.log("Done"); + } +} + +await PublishCommand.run(process.argv.slice(2)); diff --git a/packages/bun-lambda/tsconfig.json b/packages/bun-lambda/tsconfig.json new file mode 100644 index 0000000000..8d1637c760 --- /dev/null +++ b/packages/bun-lambda/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "lib": ["ESNext"], + "module": "ESNext", + "target": "ESNext", + "moduleResolution": "node", + "types": ["bun-types"], + "esModuleInterop": true, + "allowJs": true, + "strict": true + } +}