diff --git a/docs/api/websockets.md b/docs/api/websockets.md index 7b49686e94..a7439c9725 100644 --- a/docs/api/websockets.md +++ b/docs/api/websockets.md @@ -114,8 +114,7 @@ type WebSocketData = { authToken: string; }; -// TypeScript: specify the type of `data` -Bun.serve({ +Bun.serve({ fetch(req, server) { const cookies = new Bun.CookieMap(req.headers.get("cookie")!); @@ -131,8 +130,12 @@ Bun.serve({ return undefined; }, websocket: { + // TypeScript: specify the type of ws.data like this + data: {} as WebSocketData, + // handler called when a message is received async message(ws, message) { + // ws.data is now properly typed as WebSocketData const user = getUserFromToken(ws.data.authToken); await saveMessageToDatabase({ @@ -164,7 +167,7 @@ socket.addEventListener("message", event => { Bun's `ServerWebSocket` implementation implements a native publish-subscribe API for topic-based broadcasting. Individual sockets can `.subscribe()` to a topic (specified with a string identifier) and `.publish()` messages to all other subscribers to that topic (excluding itself). This topic-based broadcast API is similar to [MQTT](https://en.wikipedia.org/wiki/MQTT) and [Redis Pub/Sub](https://redis.io/topics/pubsub). ```ts -const server = Bun.serve<{ username: string }>({ +const server = Bun.serve({ fetch(req, server) { const url = new URL(req.url); if (url.pathname === "/chat") { @@ -179,6 +182,9 @@ const server = Bun.serve<{ username: string }>({ return new Response("Hello world"); }, websocket: { + // TypeScript: specify the type of ws.data like this + data: {} as { username: string }, + open(ws) { const msg = `${ws.data.username} has entered the chat`; ws.subscribe("the-group-chat"); diff --git a/docs/guides/websocket/context.md b/docs/guides/websocket/context.md index ef6fe9b009..2dc35e6925 100644 --- a/docs/guides/websocket/context.md +++ b/docs/guides/websocket/context.md @@ -7,7 +7,7 @@ When building a WebSocket server, it's typically necessary to store some identif With [Bun.serve()](https://bun.com/docs/api/websockets#contextual-data), this "contextual data" is set when the connection is initially upgraded by passing a `data` parameter in the `server.upgrade()` call. ```ts -Bun.serve<{ socketId: number }>({ +Bun.serve({ fetch(req, server) { const success = server.upgrade(req, { data: { @@ -20,6 +20,9 @@ Bun.serve<{ socketId: number }>({ // ... }, websocket: { + // TypeScript: specify the type of ws.data like this + data: {} as { socketId: number }, + // define websocket handlers async message(ws, message) { // the contextual data is available as the `data` property @@ -41,8 +44,7 @@ type WebSocketData = { userId: string; }; -// TypeScript: specify the type of `data` -Bun.serve({ +Bun.serve({ async fetch(req, server) { // use a library to parse cookies const cookies = parseCookies(req.headers.get("Cookie")); @@ -60,6 +62,9 @@ Bun.serve({ if (upgraded) return undefined; }, websocket: { + // TypeScript: specify the type of ws.data like this + data: {} as WebSocketData, + async message(ws, message) { // save the message to a database await saveMessageToDatabase({ diff --git a/docs/guides/websocket/pubsub.md b/docs/guides/websocket/pubsub.md index b291a2214e..b0adb26d65 100644 --- a/docs/guides/websocket/pubsub.md +++ b/docs/guides/websocket/pubsub.md @@ -7,7 +7,7 @@ Bun's server-side `WebSocket` API provides a native pub-sub API. Sockets can be This code snippet implements a simple single-channel chat server. ```ts -const server = Bun.serve<{ username: string }>({ +const server = Bun.serve({ fetch(req, server) { const cookies = req.headers.get("cookie"); const username = getUsernameFromCookies(cookies); @@ -17,6 +17,9 @@ const server = Bun.serve<{ username: string }>({ return new Response("Hello world"); }, websocket: { + // TypeScript: specify the type of ws.data like this + data: {} as { username: string }, + open(ws) { const msg = `${ws.data.username} has entered the chat`; ws.subscribe("the-group-chat"); diff --git a/docs/guides/websocket/simple.md b/docs/guides/websocket/simple.md index b14434deb0..81b2876ddc 100644 --- a/docs/guides/websocket/simple.md +++ b/docs/guides/websocket/simple.md @@ -7,7 +7,7 @@ Start a simple WebSocket server using [`Bun.serve`](https://bun.com/docs/api/htt Inside `fetch`, we attempt to upgrade incoming `ws:` or `wss:` requests to WebSocket connections. ```ts -const server = Bun.serve<{ authToken: string }>({ +const server = Bun.serve({ fetch(req, server) { const success = server.upgrade(req); if (success) { diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index 3425900c86..5215fda802 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -18,7 +18,7 @@ declare module "bun" { type ArrayBufferView = | NodeJS.TypedArray | DataView; - type BufferSource = NodeJS.TypedArray | DataView | ArrayBufferLike; + type BufferSource = NodeJS.TypedArray | DataView | ArrayBufferLike; type StringOrBuffer = string | NodeJS.TypedArray | ArrayBufferLike; type XMLHttpRequestBodyInit = Blob | BufferSource | FormData | URLSearchParams | string; type ReadableStreamController = ReadableStreamDefaultController; @@ -90,6 +90,12 @@ declare module "bun" { }; type Merge = MergeInner & MergeInner; type DistributedMerge = T extends T ? Merge> : never; + + type Without = A & { + [Key in Exclude]?: never; + }; + + type XOR = Without | Without; } interface ErrorEventInit extends EventInit { @@ -2780,852 +2786,6 @@ declare module "bun" { */ function build(config: BuildConfig): Promise; - /** - * A status that represents the outcome of a sent message. - * - * - if **0**, the message was **dropped**. - * - if **-1**, there is **backpressure** of messages. - * - if **>0**, it represents the **number of bytes sent**. - * - * @example - * ```js - * const status = ws.send("Hello!"); - * if (status === 0) { - * console.log("Message was dropped"); - * } else if (status === -1) { - * console.log("Backpressure was applied"); - * } else { - * console.log(`Success! Sent ${status} bytes`); - * } - * ``` - */ - type ServerWebSocketSendStatus = number; - - /** - * A state that represents if a WebSocket is connected. - * - * - `WebSocket.CONNECTING` is `0`, the connection is pending. - * - `WebSocket.OPEN` is `1`, the connection is established and `send()` is possible. - * - `WebSocket.CLOSING` is `2`, the connection is closing. - * - `WebSocket.CLOSED` is `3`, the connection is closed or couldn't be opened. - * - * @link https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState - */ - type WebSocketReadyState = 0 | 1 | 2 | 3; - - /** - * A fast WebSocket designed for servers. - * - * Features: - * - **Message compression** - Messages can be compressed - * - **Backpressure** - If the client is not ready to receive data, the server will tell you. - * - **Dropped messages** - If the client cannot receive data, the server will tell you. - * - **Topics** - Messages can be {@link ServerWebSocket.publish}ed to a specific topic and the client can {@link ServerWebSocket.subscribe} to topics - * - * This is slightly different than the browser {@link WebSocket} which Bun supports for clients. - * - * Powered by [uWebSockets](https://github.com/uNetworking/uWebSockets). - * - * @example - * ```ts - * Bun.serve({ - * websocket: { - * open(ws) { - * console.log("Connected", ws.remoteAddress); - * }, - * message(ws, data) { - * console.log("Received", data); - * ws.send(data); - * }, - * close(ws, code, reason) { - * console.log("Disconnected", code, reason); - * }, - * } - * }); - * ``` - * - * @category HTTP & Networking - */ - interface ServerWebSocket { - /** - * Sends a message to the client. - * - * @param data The data to send. - * @param compress Should the data be compressed? If the client does not support compression, this is ignored. - * @example - * ws.send("Hello!"); - * ws.send("Compress this.", true); - * ws.send(new Uint8Array([1, 2, 3, 4])); - */ - send(data: string | BufferSource, compress?: boolean): ServerWebSocketSendStatus; - - /** - * Sends a text message to the client. - * - * @param data The data to send. - * @param compress Should the data be compressed? If the client does not support compression, this is ignored. - * @example - * ws.send("Hello!"); - * ws.send("Compress this.", true); - */ - sendText(data: string, compress?: boolean): ServerWebSocketSendStatus; - - /** - * Sends a binary message to the client. - * - * @param data The data to send. - * @param compress Should the data be compressed? If the client does not support compression, this is ignored. - * @example - * ws.send(new TextEncoder().encode("Hello!")); - * ws.send(new Uint8Array([1, 2, 3, 4]), true); - */ - sendBinary(data: BufferSource, compress?: boolean): ServerWebSocketSendStatus; - - /** - * Closes the connection. - * - * Here is a list of close codes: - * - `1000` means "normal closure" **(default)** - * - `1009` means a message was too big and was rejected - * - `1011` means the server encountered an error - * - `1012` means the server is restarting - * - `1013` means the server is too busy or the client is rate-limited - * - `4000` through `4999` are reserved for applications (you can use it!) - * - * To close the connection abruptly, use `terminate()`. - * - * @param code The close code to send - * @param reason The close reason to send - */ - close(code?: number, reason?: string): void; - - /** - * Abruptly close the connection. - * - * To gracefully close the connection, use `close()`. - */ - terminate(): void; - - /** - * Sends a ping. - * - * @param data The data to send - */ - ping(data?: string | BufferSource): ServerWebSocketSendStatus; - - /** - * Sends a pong. - * - * @param data The data to send - */ - pong(data?: string | BufferSource): ServerWebSocketSendStatus; - - /** - * Sends a message to subscribers of the topic. - * - * @param topic The topic name. - * @param data The data to send. - * @param compress Should the data be compressed? If the client does not support compression, this is ignored. - * @example - * ws.publish("chat", "Hello!"); - * ws.publish("chat", "Compress this.", true); - * ws.publish("chat", new Uint8Array([1, 2, 3, 4])); - */ - publish(topic: string, data: string | BufferSource, compress?: boolean): ServerWebSocketSendStatus; - - /** - * Sends a text message to subscribers of the topic. - * - * @param topic The topic name. - * @param data The data to send. - * @param compress Should the data be compressed? If the client does not support compression, this is ignored. - * @example - * ws.publish("chat", "Hello!"); - * ws.publish("chat", "Compress this.", true); - */ - publishText(topic: string, data: string, compress?: boolean): ServerWebSocketSendStatus; - - /** - * Sends a binary message to subscribers of the topic. - * - * @param topic The topic name. - * @param data The data to send. - * @param compress Should the data be compressed? If the client does not support compression, this is ignored. - * @example - * ws.publish("chat", new TextEncoder().encode("Hello!")); - * ws.publish("chat", new Uint8Array([1, 2, 3, 4]), true); - */ - publishBinary(topic: string, data: BufferSource, compress?: boolean): ServerWebSocketSendStatus; - - /** - * Subscribes a client to the topic. - * - * @param topic The topic name. - * @example - * ws.subscribe("chat"); - */ - subscribe(topic: string): void; - - /** - * Unsubscribes a client to the topic. - * - * @param topic The topic name. - * @example - * ws.unsubscribe("chat"); - */ - unsubscribe(topic: string): void; - - /** - * Is the client subscribed to a topic? - * - * @param topic The topic name. - * @example - * ws.subscribe("chat"); - * console.log(ws.isSubscribed("chat")); // true - */ - isSubscribed(topic: string): boolean; - - /** - * Batches `send()` and `publish()` operations, which makes it faster to send data. - * - * The `message`, `open`, and `drain` callbacks are automatically corked, so - * you only need to call this if you are sending messages outside of those - * callbacks or in async functions. - * - * @param callback The callback to run. - * @example - * ws.cork((ctx) => { - * ctx.send("These messages"); - * ctx.sendText("are sent"); - * ctx.sendBinary(new TextEncoder().encode("together!")); - * }); - */ - cork(callback: (ws: ServerWebSocket) => T): T; - - /** - * The IP address of the client. - * - * @example - * console.log(socket.remoteAddress); // "127.0.0.1" - */ - readonly remoteAddress: string; - - /** - * The ready state of the client. - * - * - if `0`, the client is connecting. - * - if `1`, the client is connected. - * - if `2`, the client is closing. - * - if `3`, the client is closed. - * - * @example - * console.log(socket.readyState); // 1 - */ - readonly readyState: WebSocketReadyState; - - /** - * Sets how binary data is returned in events. - * - * - if `nodebuffer`, binary data is returned as `Buffer` objects. **(default)** - * - if `arraybuffer`, binary data is returned as `ArrayBuffer` objects. - * - if `uint8array`, binary data is returned as `Uint8Array` objects. - * - * @example - * let ws: WebSocket; - * ws.binaryType = "uint8array"; - * ws.addEventListener("message", ({ data }) => { - * console.log(data instanceof Uint8Array); // true - * }); - */ - binaryType?: "nodebuffer" | "arraybuffer" | "uint8array"; - - /** - * Custom data that you can assign to a client, can be read and written at any time. - * - * @example - * import { serve } from "bun"; - * - * serve({ - * fetch(request, server) { - * const data = { - * accessToken: request.headers.get("Authorization"), - * }; - * if (server.upgrade(request, { data })) { - * return; - * } - * return new Response(); - * }, - * websocket: { - * open(ws) { - * console.log(ws.data.accessToken); - * } - * } - * }); - */ - data: T; - - getBufferedAmount(): number; - } - - /** - * Compression options for WebSocket messages. - */ - type WebSocketCompressor = - | "disable" - | "shared" - | "dedicated" - | "3KB" - | "4KB" - | "8KB" - | "16KB" - | "32KB" - | "64KB" - | "128KB" - | "256KB"; - - /** - * Create a server-side {@link ServerWebSocket} handler for use with {@link Bun.serve} - * - * @category HTTP & Networking - * - * @example - * ```ts - * import { websocket, serve } from "bun"; - * - * serve<{name: string}>({ - * port: 3000, - * websocket: { - * open: (ws) => { - * console.log("Client connected"); - * }, - * message: (ws, message) => { - * console.log(`${ws.data.name}: ${message}`); - * }, - * close: (ws) => { - * console.log("Client disconnected"); - * }, - * }, - * - * fetch(req, server) { - * const url = new URL(req.url); - * if (url.pathname === "/chat") { - * const upgraded = server.upgrade(req, { - * data: { - * name: new URL(req.url).searchParams.get("name"), - * }, - * }); - * if (!upgraded) { - * return new Response("Upgrade failed", { status: 400 }); - * } - * return; - * } - * return new Response("Hello World"); - * }, - * }); - * ``` - */ - interface WebSocketHandler { - /** - * Called when the server receives an incoming message. - * - * If the message is not a `string`, its type is based on the value of `binaryType`. - * - if `nodebuffer`, then the message is a `Buffer`. - * - if `arraybuffer`, then the message is an `ArrayBuffer`. - * - if `uint8array`, then the message is a `Uint8Array`. - * - * @param ws The websocket that sent the message - * @param message The message received - */ - message(ws: ServerWebSocket, message: string | Buffer): void | Promise; - - /** - * Called when a connection is opened. - * - * @param ws The websocket that was opened - */ - open?(ws: ServerWebSocket): void | Promise; - - /** - * Called when a connection was previously under backpressure, - * meaning it had too many queued messages, but is now ready to receive more data. - * - * @param ws The websocket that is ready for more data - */ - drain?(ws: ServerWebSocket): void | Promise; - - /** - * Called when a connection is closed. - * - * @param ws The websocket that was closed - * @param code The close code - * @param reason The close reason - */ - close?(ws: ServerWebSocket, code: number, reason: string): void | Promise; - - /** - * Called when a ping is sent. - * - * @param ws The websocket that received the ping - * @param data The data sent with the ping - */ - ping?(ws: ServerWebSocket, data: Buffer): void | Promise; - - /** - * Called when a pong is received. - * - * @param ws The websocket that received the ping - * @param data The data sent with the ping - */ - pong?(ws: ServerWebSocket, data: Buffer): void | Promise; - - /** - * Sets the maximum size of messages in bytes. - * - * Default is 16 MB, or `1024 * 1024 * 16` in bytes. - */ - maxPayloadLength?: number; - - /** - * Sets the maximum number of bytes that can be buffered on a single connection. - * - * Default is 16 MB, or `1024 * 1024 * 16` in bytes. - */ - backpressureLimit?: number; - - /** - * Sets if the connection should be closed if `backpressureLimit` is reached. - * - * Default is `false`. - */ - closeOnBackpressureLimit?: boolean; - - /** - * Sets the the number of seconds to wait before timing out a connection - * due to no messages or pings. - * - * Default is 2 minutes, or `120` in seconds. - */ - idleTimeout?: number; - - /** - * Should `ws.publish()` also send a message to `ws` (itself), if it is subscribed? - * - * Default is `false`. - */ - publishToSelf?: boolean; - - /** - * Should the server automatically send and respond to pings to clients? - * - * Default is `true`. - */ - sendPings?: boolean; - - /** - * Sets the compression level for messages, for clients that supports it. By default, compression is disabled. - * - * Default is `false`. - */ - perMessageDeflate?: - | boolean - | { - /** - * Sets the compression level. - */ - compress?: WebSocketCompressor | boolean; - /** - * Sets the decompression level. - */ - decompress?: WebSocketCompressor | boolean; - }; - } - - namespace RouterTypes { - type ExtractRouteParams = T extends `${string}:${infer Param}/${infer Rest}` - ? { [K in Param]: string } & ExtractRouteParams - : T extends `${string}:${infer Param}` - ? { [K in Param]: string } - : T extends `${string}*` - ? {} - : {}; - - type RouteHandler = (req: BunRequest, server: Server) => Response | Promise; - - type RouteHandlerWithWebSocketUpgrade = ( - req: BunRequest, - server: Server, - ) => Response | undefined | void | Promise; - - type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "HEAD" | "OPTIONS"; - - type RouteHandlerObject = { - [K in HTTPMethod]?: RouteHandler; - }; - - type RouteHandlerWithWebSocketUpgradeObject = { - [K in HTTPMethod]?: RouteHandlerWithWebSocketUpgrade; - }; - - type RouteValue = - | Response - | false - | RouteHandler - | RouteHandlerObject - | HTMLBundle - | BunFile; - type RouteValueWithWebSocketUpgrade = - | RouteValue - | RouteHandlerWithWebSocketUpgrade - | RouteHandlerWithWebSocketUpgradeObject; - } - - interface BunRequest extends Request { - params: RouterTypes.ExtractRouteParams; - readonly cookies: CookieMap; - - clone(): BunRequest; - } - - interface GenericServeOptions { - /** - * What URI should be used to make {@link Request.url} absolute? - * - * By default, looks at {@link hostname}, {@link port}, and whether or not SSL is enabled to generate one - * - * @example - * ```js - * "http://my-app.com" - * ``` - * - * @example - * ```js - * "https://wongmjane.com/" - * ``` - * - * This should be the public, absolute URL – include the protocol and {@link hostname}. If the port isn't 80 or 443, then include the {@link port} too. - * - * @example - * "http://localhost:3000" - */ - // baseURI?: string; - - /** - * What is the maximum size of a request body? (in bytes) - * @default 1024 * 1024 * 128 // 128MB - */ - maxRequestBodySize?: number; - - /** - * Render contextual errors? This enables bun's error page - * @default process.env.NODE_ENV !== 'production' - */ - development?: - | boolean - | { - /** - * Enable Hot Module Replacement for routes (including React Fast Refresh, if React is in use) - * - * @default true if process.env.NODE_ENV !== 'production' - * - */ - hmr?: boolean; - - /** - * Enable console log streaming from browser to server - * @default false - */ - console?: boolean; - - /** - * Enable automatic workspace folders for Chrome DevTools - * - * This lets you persistently edit files in the browser. It works by adding the following route to the server: - * `/.well-known/appspecific/com.chrome.devtools.json` - * - * The response is a JSON object with the following shape: - * ```json - * { - * "workspace": { - * "root": "", - * "uuid": "" - * } - * } - * ``` - * - * The `root` field is the current working directory of the server. - * The `"uuid"` field is a hash of the file that started the server and a hash of the current working directory. - * - * For security reasons, if the remote socket address is not from localhost, 127.0.0.1, or ::1, the request is ignored. - * @default true - */ - chromeDevToolsAutomaticWorkspaceFolders?: boolean; - }; - - error?: (this: Server, error: ErrorLike) => Response | Promise | void | Promise; - - /** - * Uniquely identify a server instance with an ID - * - * --- - * - * **When bun is started with the `--hot` flag**: - * - * This string will be used to hot reload the server without interrupting - * pending requests or websockets. If not provided, a value will be - * generated. To disable hot reloading, set this value to `null`. - * - * **When bun is not started with the `--hot` flag**: - * - * This string will currently do nothing. But in the future it could be useful for logs or metrics. - */ - id?: string | null; - } - - interface ServeOptions extends GenericServeOptions { - /** - * What port should the server listen on? - * @default process.env.PORT || "3000" - */ - port?: string | number; - - /** - * Whether the `SO_REUSEPORT` flag should be set. - * - * This allows multiple processes to bind to the same port, which is useful for load balancing. - * - * @default false - */ - reusePort?: boolean; - - /** - * Whether the `IPV6_V6ONLY` flag should be set. - * @default false - */ - ipv6Only?: boolean; - - /** - * What hostname should the server listen on? - * - * @default - * ```js - * "0.0.0.0" // listen on all interfaces - * ``` - * @example - * ```js - * "127.0.0.1" // Only listen locally - * ``` - * @example - * ```js - * "remix.run" // Only listen on remix.run - * ```` - * - * note: hostname should not include a {@link port} - */ - hostname?: string; - - /** - * If set, the HTTP server will listen on a unix socket instead of a port. - * (Cannot use unix with port + hostname) - */ - unix?: never; - - /** - * Sets the the number of seconds to wait before timing out a connection - * due to inactivity. - * - * Default is `10` seconds. - */ - idleTimeout?: number; - - /** - * Handle HTTP requests - * - * Respond to {@link Request} objects with a {@link Response} object. - */ - fetch(this: Server, request: Request, server: Server): Response | Promise; - } - - interface UnixServeOptions extends GenericServeOptions { - /** - * If set, the HTTP server will listen on a unix socket instead of a port. - */ - unix: string; - - /** - * If set, the HTTP server will listen on this port - * (Cannot use port with unix) - */ - port?: never; - - /** - * If set, the HTTP server will listen on this hostname - * (Cannot use hostname with unix) - */ - hostname?: never; - - /** - * Handle HTTP requests - * - * Respond to {@link Request} objects with a {@link Response} object. - */ - fetch(this: Server, request: Request, server: Server): Response | Promise; - } - - interface WebSocketServeOptions extends GenericServeOptions { - /** - * What port should the server listen on? - * @default process.env.PORT || "3000" - */ - port?: string | number; - - /** - * What hostname should the server listen on? - * - * @default - * ```js - * "0.0.0.0" // listen on all interfaces - * ``` - * @example - * ```js - * "127.0.0.1" // Only listen locally - * ``` - * @example - * ```js - * "remix.run" // Only listen on remix.run - * ```` - * - * note: hostname should not include a {@link port} - */ - hostname?: string; - - /** - * Enable websockets with {@link Bun.serve} - * - * For simpler type safety, see {@link Bun.websocket} - * - * @example - * ```js - * Bun.serve({ - * websocket: { - * open: (ws) => { - * console.log("Client connected"); - * }, - * message: (ws, message) => { - * console.log("Client sent message", message); - * }, - * close: (ws) => { - * console.log("Client disconnected"); - * }, - * }, - * fetch(req, server) { - * const url = new URL(req.url); - * if (url.pathname === "/chat") { - * const upgraded = server.upgrade(req); - * if (!upgraded) { - * return new Response("Upgrade failed", { status: 400 }); - * } - * } - * return new Response("Hello World"); - * }, - * }); - * ``` - * Upgrade a {@link Request} to a {@link ServerWebSocket} via {@link Server.upgrade} - * - * Pass `data` in @{link Server.upgrade} to attach data to the {@link ServerWebSocket.data} property - */ - websocket: WebSocketHandler; - - /** - * Handle HTTP requests or upgrade them to a {@link ServerWebSocket} - * - * Respond to {@link Request} objects with a {@link Response} object. - */ - fetch( - this: Server, - request: Request, - server: Server, - ): Response | undefined | void | Promise; - } - - interface UnixWebSocketServeOptions extends GenericServeOptions { - /** - * If set, the HTTP server will listen on a unix socket instead of a port. - * (Cannot be used with hostname+port) - */ - unix: string; - - /** - * Enable websockets with {@link Bun.serve} - * - * For simpler type safety, see {@link Bun.websocket} - * - * @example - * ```js - * import { serve } from "bun"; - * serve({ - * websocket: { - * open: (ws) => { - * console.log("Client connected"); - * }, - * message: (ws, message) => { - * console.log("Client sent message", message); - * }, - * close: (ws) => { - * console.log("Client disconnected"); - * }, - * }, - * fetch(req, server) { - * const url = new URL(req.url); - * if (url.pathname === "/chat") { - * const upgraded = server.upgrade(req); - * if (!upgraded) { - * return new Response("Upgrade failed", { status: 400 }); - * } - * } - * return new Response("Hello World"); - * }, - * }); - * ``` - * Upgrade a {@link Request} to a {@link ServerWebSocket} via {@link Server.upgrade} - * - * Pass `data` in @{link Server.upgrade} to attach data to the {@link ServerWebSocket.data} property - */ - websocket: WebSocketHandler; - - /** - * Handle HTTP requests or upgrade them to a {@link ServerWebSocket} - * - * Respond to {@link Request} objects with a {@link Response} object. - */ - fetch(this: Server, request: Request, server: Server): Response | undefined | Promise; - } - - interface TLSWebSocketServeOptions - extends WebSocketServeOptions, - TLSOptionsAsDeprecated { - unix?: never; - tls?: TLSOptions | TLSOptions[]; - } - - interface UnixTLSWebSocketServeOptions - extends UnixWebSocketServeOptions, - TLSOptionsAsDeprecated { - /** - * If set, the HTTP server will listen on a unix socket instead of a port. - * (Cannot be used with hostname+port) - */ - unix: string; - tls?: TLSOptions | TLSOptions[]; - } - - interface TLSServeOptions extends ServeOptions, TLSOptionsAsDeprecated { - tls?: TLSOptions | TLSOptions[]; - } - - interface UnixTLSServeOptions extends UnixServeOptions, TLSOptionsAsDeprecated { - tls?: TLSOptions | TLSOptions[]; - } - interface ErrorLike extends Error { code?: string; errno?: number; @@ -3716,632 +2876,23 @@ declare module "bun" { clientRenegotiationWindow?: number; } - // Note for contributors: TLSOptionsAsDeprecated should be considered immutable - // and new TLS option keys should only be supported on the `.tls` property (which comes - // from the TLSOptions interface above). - /** - * This exists because Bun.serve() extends the TLSOptions object, but - * they're now considered deprecated. You should be passing the - * options on `.tls` instead. - * - * @example - * ```ts - * //// OLD //// - * Bun.serve({ - * fetch: () => new Response("Hello World"), - * passphrase: "secret", - * }); - * - * //// NEW //// - * Bun.serve({ - * fetch: () => new Response("Hello World"), - * tls: { - * passphrase: "secret", - * }, - * }); - * ``` - */ - interface TLSOptionsAsDeprecated { - /** - * Passphrase for the TLS key - * - * @deprecated Use `.tls.passphrase` instead - */ - passphrase?: string; - - /** - * File path to a .pem file custom Diffie Helman parameters - * - * @deprecated Use `.tls.dhParamsFile` instead - */ - dhParamsFile?: string; - - /** - * Explicitly set a server name - * - * @deprecated Use `.tls.serverName` instead - */ - serverName?: string; - - /** - * This sets `OPENSSL_RELEASE_BUFFERS` to 1. - * It reduces overall performance but saves some memory. - * @default false - * - * @deprecated Use `.tls.lowMemoryMode` instead - */ - lowMemoryMode?: boolean; - - /** - * If set to `false`, any certificate is accepted. - * Default is `$NODE_TLS_REJECT_UNAUTHORIZED` environment variable, or `true` if it is not set. - * - * @deprecated Use `.tls.rejectUnauthorized` instead - */ - rejectUnauthorized?: boolean; - - /** - * If set to `true`, the server will request a client certificate. - * - * Default is `false`. - * - * @deprecated Use `.tls.requestCert` instead - */ - requestCert?: boolean; - - /** - * Optionally override the trusted CA certificates. Default is to trust - * the well-known CAs curated by Mozilla. Mozilla's CAs are completely - * replaced when CAs are explicitly specified using this option. - * - * @deprecated Use `.tls.ca` instead - */ - ca?: string | Buffer | BunFile | Array | undefined; - /** - * Cert chains in PEM format. One cert chain should be provided per - * private key. Each cert chain should consist of the PEM formatted - * certificate for a provided private key, followed by the PEM - * formatted intermediate certificates (if any), in order, and not - * including the root CA (the root CA must be pre-known to the peer, - * see ca). When providing multiple cert chains, they do not have to - * be in the same order as their private keys in key. If the - * intermediate certificates are not provided, the peer will not be - * able to validate the certificate, and the handshake will fail. - * - * @deprecated Use `.tls.cert` instead - */ - cert?: string | Buffer | BunFile | Array | undefined; - /** - * Private keys in PEM format. PEM allows the option of private keys - * being encrypted. Encrypted keys will be decrypted with - * options.passphrase. Multiple keys using different algorithms can be - * provided either as an array of unencrypted key strings or buffers, - * or an array of objects in the form {pem: [, - * passphrase: ]}. The object form can only occur in an array. - * object.passphrase is optional. Encrypted keys will be decrypted with - * object.passphrase if provided, or options.passphrase if it is not. - * - * @deprecated Use `.tls.key` instead - */ - key?: string | Buffer | BunFile | Array | undefined; - /** - * Optionally affect the OpenSSL protocol behavior, which is not - * usually necessary. This should be used carefully if at all! Value is - * a numeric bitmask of the SSL_OP_* options from OpenSSL Options - * - * @deprecated `Use .tls.secureOptions` instead - */ - secureOptions?: number | undefined; // Value is a numeric bitmask of the `SSL_OP_*` options - } - interface SocketAddress { /** * The IP address of the client. */ address: string; + /** * The port of the client. */ port: number; + /** * The IP family ("IPv4" or "IPv6"). */ family: "IPv4" | "IPv6"; } - /** - * HTTP & HTTPS Server - * - * To start the server, see {@link serve} - * - * For performance, Bun pre-allocates most of the data for 2048 concurrent requests. - * That means starting a new server allocates about 500 KB of memory. Try to - * avoid starting and stopping the server often (unless it's a new instance of bun). - * - * Powered by a fork of [uWebSockets](https://github.com/uNetworking/uWebSockets). Thank you \@alexhultman. - * - * @category HTTP & Networking - */ - interface Server extends Disposable { - /* - * Closes all connections connected to this server which are not sending a request or waiting for a response. Does not close the listen socket. - */ - closeIdleConnections(): void; - - /** - * Stop listening to prevent new connections from being accepted. - * - * By default, it does not cancel in-flight requests or websockets. That means it may take some time before all network activity stops. - * - * @param closeActiveConnections Immediately terminate in-flight requests, websockets, and stop accepting new connections. - * @default false - */ - stop(closeActiveConnections?: boolean): Promise; - - /** - * Update the `fetch` and `error` handlers without restarting the server. - * - * This is useful if you want to change the behavior of your server without - * restarting it or for hot reloading. - * - * @example - * - * ```js - * // create the server - * const server = Bun.serve({ - * fetch(request) { - * return new Response("Hello World v1") - * } - * }); - * - * // Update the server to return a different response - * server.reload({ - * fetch(request) { - * return new Response("Hello World v2") - * } - * }); - * ``` - * - * Passing other options such as `port` or `hostname` won't do anything. - */ - reload }>( - options: ServeFunctionOptions & { - /** - * @deprecated Use `routes` instead in new code. This will continue to work for awhile though. - */ - static?: R; - }, - ): Server; - - /** - * Mock the fetch handler for a running server. - * - * This feature is not fully implemented yet. It doesn't normalize URLs - * consistently in all cases and it doesn't yet call the `error` handler - * consistently. This needs to be fixed - */ - fetch(request: Request | string): Response | Promise; - - /** - * Upgrade a {@link Request} to a {@link ServerWebSocket} - * - * @param request The {@link Request} to upgrade - * @param options Pass headers or attach data to the {@link ServerWebSocket} - * - * @returns `true` if the upgrade was successful and `false` if it failed - * - * @example - * ```js - * import { serve } from "bun"; - * serve({ - * websocket: { - * open: (ws) => { - * console.log("Client connected"); - * }, - * message: (ws, message) => { - * console.log("Client sent message", message); - * }, - * close: (ws) => { - * console.log("Client disconnected"); - * }, - * }, - * fetch(req, server) { - * const url = new URL(req.url); - * if (url.pathname === "/chat") { - * const upgraded = server.upgrade(req); - * if (!upgraded) { - * return new Response("Upgrade failed", { status: 400 }); - * } - * } - * return new Response("Hello World"); - * }, - * }); - * ``` - * What you pass to `data` is available on the {@link ServerWebSocket.data} property - */ - // eslint-disable-next-line @definitelytyped/no-unnecessary-generics - upgrade( - request: Request, - options?: { - /** - * Send any additional headers while upgrading, like cookies - */ - headers?: HeadersInit; - /** - * This value is passed to the {@link ServerWebSocket.data} property - */ - data?: T; - }, - ): boolean; - - /** - * Send a message to all connected {@link ServerWebSocket} subscribed to a topic - * - * @param topic The topic to publish to - * @param data The data to send - * @param compress Should the data be compressed? Ignored if the client does not support compression. - * - * @returns 0 if the message was dropped, -1 if backpressure was applied, or the number of bytes sent. - * - * @example - * - * ```js - * server.publish("chat", "Hello World"); - * ``` - * - * @example - * ```js - * server.publish("chat", new Uint8Array([1, 2, 3, 4])); - * ``` - * - * @example - * ```js - * server.publish("chat", new ArrayBuffer(4), true); - * ``` - * - * @example - * ```js - * server.publish("chat", new DataView(new ArrayBuffer(4))); - * ``` - */ - publish( - topic: string, - data: string | ArrayBufferView | ArrayBuffer | SharedArrayBuffer, - compress?: boolean, - ): ServerWebSocketSendStatus; - - /** - * A count of connections subscribed to a given topic - * - * This operation will loop through each topic internally to get the count. - * - * @param topic the websocket topic to check how many subscribers are connected to - * @returns the number of subscribers - */ - subscriberCount(topic: string): number; - - /** - * Returns the client IP address and port of the given Request. If the request was closed or is a unix socket, returns null. - * - * @example - * ```js - * export default { - * async fetch(request, server) { - * return new Response(server.requestIP(request)); - * } - * } - * ``` - */ - requestIP(request: Request): SocketAddress | null; - - /** - * Reset the idleTimeout of the given Request to the number in seconds. 0 means no timeout. - * - * @example - * ```js - * export default { - * async fetch(request, server) { - * server.timeout(request, 60); - * await Bun.sleep(30000); - * return new Response("30 seconds have passed"); - * } - * } - * ``` - */ - timeout(request: Request, seconds: number): void; - /** - * Undo a call to {@link Server.unref} - * - * If the Server has already been stopped, this does nothing. - * - * If {@link Server.ref} is called multiple times, this does nothing. Think of it as a boolean toggle. - */ - ref(): void; - - /** - * Don't keep the process alive if this server is the only thing left. - * Active connections may continue to keep the process alive. - * - * By default, the server is ref'd. - * - * To prevent new connections from being accepted, use {@link Server.stop} - */ - unref(): void; - - /** - * How many requests are in-flight right now? - */ - readonly pendingRequests: number; - - /** - * How many {@link ServerWebSocket}s are in-flight right now? - */ - readonly pendingWebSockets: number; - - readonly url: URL; - - /** - * The port the server is listening on. - * - * This will be undefined when the server is listening on a unix socket. - * - * @example - * ```js - * 3000 - * ``` - */ - readonly port: number | undefined; - - /** - * The hostname the server is listening on. Does not include the port. - * - * This will be `undefined` when the server is listening on a unix socket. - * - * @example - * ```js - * "localhost" - * ``` - */ - readonly hostname: string | undefined; - - /** - * Is the server running in development mode? - * - * In development mode, `Bun.serve()` returns rendered error messages with - * stack traces instead of a generic 500 error. This makes debugging easier, - * but development mode shouldn't be used in production or you will risk - * leaking sensitive information. - */ - readonly development: boolean; - - /** - * An identifier of the server instance - * - * When bun is started with the `--hot` flag, this ID is used to hot reload the server without interrupting pending requests or websockets. - * - * When bun is not started with the `--hot` flag, this ID is currently unused. - */ - readonly id: string; - } - - /** - * The type of options that can be passed to {@link serve} - */ - type Serve = - | ServeOptions - | TLSServeOptions - | UnixServeOptions - | UnixTLSServeOptions - | WebSocketServeOptions - | TLSWebSocketServeOptions - | UnixWebSocketServeOptions - | UnixTLSWebSocketServeOptions; - - /** - * The type of options that can be passed to {@link serve}, with support for `routes` and a safer requirement for `fetch` - */ - type ServeFunctionOptions> }> = - | (__internal.DistributedOmit, WebSocketServeOptions>, "fetch"> & { - routes: R; - fetch?: (this: Server, request: Request, server: Server) => Response | Promise; - }) - | (__internal.DistributedOmit, WebSocketServeOptions>, "routes"> & { - routes?: never; - fetch: (this: Server, request: Request, server: Server) => Response | Promise; - }) - | (Omit, "fetch"> & { - routes: { - [K in keyof R]: RouterTypes.RouteValueWithWebSocketUpgrade>; - }; - fetch?: ( - this: Server, - request: Request, - server: Server, - ) => Response | Promise | void | undefined; - }) - | (Omit, "fetch"> & { - routes?: never; - fetch: ( - this: Server, - request: Request, - server: Server, - ) => Response | Promise | void | undefined; - }); - - /** - * Bun.serve provides a high-performance HTTP server with built-in routing support. - * It enables both function-based and object-based route handlers with type-safe - * parameters and method-specific handling. - * - * @param options - Server configuration options - * - * @category HTTP & Networking - * - * @example Basic Usage - * ```ts - * Bun.serve({ - * port: 3000, - * fetch(req) { - * return new Response("Hello World"); - * } - * }); - * ``` - * - * @example Route-based Handlers - * ```ts - * Bun.serve({ - * routes: { - * // Static responses - * "/": new Response("Home page"), - * - * // Function handlers with type-safe parameters - * "/users/:id": (req) => { - * // req.params.id is typed as string - * return new Response(`User ${req.params.id}`); - * }, - * - * // Method-specific handlers - * "/api/posts": { - * GET: () => new Response("Get posts"), - * POST: async (req) => { - * const body = await req.json(); - * return new Response("Created post"); - * }, - * DELETE: (req) => new Response("Deleted post") - * }, - * - * // Wildcard routes - * "/static/*": (req) => { - * // Handle any path under /static/ - * return new Response("Static file"); - * }, - * - * // Disable route (fall through to fetch handler) - * "/api/legacy": false - * }, - * - * // Fallback handler for unmatched routes - * fetch(req) { - * return new Response("Not Found", { status: 404 }); - * } - * }); - * ``` - * - * @example Path Parameters - * ```ts - * Bun.serve({ - * routes: { - * // Single parameter - * "/users/:id": (req: BunRequest<"/users/:id">) => { - * return new Response(`User ID: ${req.params.id}`); - * }, - * - * // Multiple parameters - * "/posts/:postId/comments/:commentId": ( - * req: BunRequest<"/posts/:postId/comments/:commentId"> - * ) => { - * return new Response(JSON.stringify(req.params)); - * // Output: {"postId": "123", "commentId": "456"} - * } - * } - * }); - * ``` - * - * @example Route Precedence - * ```ts - * // Routes are matched in the following order: - * // 1. Exact static routes ("/about") - * // 2. Parameter routes ("/users/:id") - * // 3. Wildcard routes ("/api/*") - * - * Bun.serve({ - * routes: { - * "/api/users": () => new Response("Users list"), - * "/api/users/:id": (req) => new Response(`User ${req.params.id}`), - * "/api/*": () => new Response("API catchall"), - * "/*": () => new Response("Root catchall") - * } - * }); - * ``` - * - * @example Error Handling - * ```ts - * Bun.serve({ - * routes: { - * "/error": () => { - * throw new Error("Something went wrong"); - * } - * }, - * error(error) { - * // Custom error handler - * console.error(error); - * return new Response(`Error: ${error.message}`, { - * status: 500 - * }); - * } - * }); - * ``` - * - * @example Server Lifecycle - * ```ts - * const server = Bun.serve({ - * // Server config... - * }); - * - * // Update routes at runtime - * server.reload({ - * routes: { - * "/": () => new Response("Updated route") - * } - * }); - * - * // Stop the server - * server.stop(); - * ``` - * - * @example Development Mode - * ```ts - * Bun.serve({ - * development: true, // Enable hot reloading - * routes: { - * // Routes will auto-reload on changes - * } - * }); - * ``` - * - * @example Type-Safe Request Handling - * ```ts - * type Post = { - * id: string; - * title: string; - * }; - * - * Bun.serve({ - * routes: { - * "/api/posts/:id": async ( - * req: BunRequest<"/api/posts/:id"> - * ) => { - * if (req.method === "POST") { - * const body: Post = await req.json(); - * return Response.json(body); - * } - * return new Response("Method not allowed", { - * status: 405 - * }); - * } - * } - * }); - * ``` - */ - function serve }>( - options: ServeFunctionOptions & { - /** - * @deprecated Use `routes` instead in new code. This will continue to work for a while though. - */ - static?: R; - }, - ): Server; - /** * [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob) powered by the fastest system calls available for operating on files. * diff --git a/packages/bun-types/bun.ns.d.ts b/packages/bun-types/bun.ns.d.ts index d55c3f5352..a6648b65c3 100644 --- a/packages/bun-types/bun.ns.d.ts +++ b/packages/bun-types/bun.ns.d.ts @@ -3,5 +3,3 @@ import * as BunModule from "bun"; declare global { export import Bun = BunModule; } - -export {}; diff --git a/packages/bun-types/deprecated.d.ts b/packages/bun-types/deprecated.d.ts index 543661c473..2d64a2a015 100644 --- a/packages/bun-types/deprecated.d.ts +++ b/packages/bun-types/deprecated.d.ts @@ -98,6 +98,11 @@ declare module "bun" { ): void; } + /** + * @deprecated Use {@link Serve.Options Bun.Serve.Options} instead + */ + type ServeOptions = Serve.Options; + /** @deprecated Use {@link SQL.Query Bun.SQL.Query} */ type SQLQuery = SQL.Query; diff --git a/packages/bun-types/index.d.ts b/packages/bun-types/index.d.ts index b8575a0b2e..319b75dbd4 100644 --- a/packages/bun-types/index.d.ts +++ b/packages/bun-types/index.d.ts @@ -21,6 +21,7 @@ /// /// /// +/// /// /// diff --git a/packages/bun-types/serve.d.ts b/packages/bun-types/serve.d.ts new file mode 100644 index 0000000000..ee45723bcd --- /dev/null +++ b/packages/bun-types/serve.d.ts @@ -0,0 +1,1272 @@ +declare module "bun" { + /** + * A status that represents the outcome of a sent message. + * + * - if **0**, the message was **dropped**. + * - if **-1**, there is **backpressure** of messages. + * - if **>0**, it represents the **number of bytes sent**. + * + * @example + * ```js + * const status = ws.send("Hello!"); + * if (status === 0) { + * console.log("Message was dropped"); + * } else if (status === -1) { + * console.log("Backpressure was applied"); + * } else { + * console.log(`Success! Sent ${status} bytes`); + * } + * ``` + */ + type ServerWebSocketSendStatus = number; + + /** + * A state that represents if a WebSocket is connected. + * + * - `WebSocket.CONNECTING` is `0`, the connection is pending. + * - `WebSocket.OPEN` is `1`, the connection is established and `send()` is possible. + * - `WebSocket.CLOSING` is `2`, the connection is closing. + * - `WebSocket.CLOSED` is `3`, the connection is closed or couldn't be opened. + * + * @link https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState + */ + type WebSocketReadyState = 0 | 1 | 2 | 3; + + /** + * A fast WebSocket designed for servers. + * + * Features: + * - **Message compression** - Messages can be compressed + * - **Backpressure** - If the client is not ready to receive data, the server will tell you. + * - **Dropped messages** - If the client cannot receive data, the server will tell you. + * - **Topics** - Messages can be {@link ServerWebSocket.publish}ed to a specific topic and the client can {@link ServerWebSocket.subscribe} to topics + * + * This is slightly different than the browser {@link WebSocket} which Bun supports for clients. + * + * Powered by [uWebSockets](https://github.com/uNetworking/uWebSockets). + * + * @example + * ```ts + * Bun.serve({ + * websocket: { + * open(ws) { + * console.log("Connected", ws.remoteAddress); + * }, + * message(ws, data) { + * console.log("Received", data); + * ws.send(data); + * }, + * close(ws, code, reason) { + * console.log("Disconnected", code, reason); + * }, + * } + * }); + * ``` + */ + interface ServerWebSocket { + /** + * Sends a message to the client. + * + * @param data The data to send. + * @param compress Should the data be compressed? If the client does not support compression, this is ignored. + * @example + * ws.send("Hello!"); + * ws.send("Compress this.", true); + * ws.send(new Uint8Array([1, 2, 3, 4])); + */ + send(data: string | BufferSource, compress?: boolean): ServerWebSocketSendStatus; + + /** + * Sends a text message to the client. + * + * @param data The data to send. + * @param compress Should the data be compressed? If the client does not support compression, this is ignored. + * @example + * ws.send("Hello!"); + * ws.send("Compress this.", true); + */ + sendText(data: string, compress?: boolean): ServerWebSocketSendStatus; + + /** + * Sends a binary message to the client. + * + * @param data The data to send. + * @param compress Should the data be compressed? If the client does not support compression, this is ignored. + * @example + * ws.send(new TextEncoder().encode("Hello!")); + * ws.send(new Uint8Array([1, 2, 3, 4]), true); + */ + sendBinary(data: BufferSource, compress?: boolean): ServerWebSocketSendStatus; + + /** + * Closes the connection. + * + * Here is a list of close codes: + * - `1000` means "normal closure" **(default)** + * - `1009` means a message was too big and was rejected + * - `1011` means the server encountered an error + * - `1012` means the server is restarting + * - `1013` means the server is too busy or the client is rate-limited + * - `4000` through `4999` are reserved for applications (you can use it!) + * + * To close the connection abruptly, use `terminate()`. + * + * @param code The close code to send + * @param reason The close reason to send + */ + close(code?: number, reason?: string): void; + + /** + * Abruptly close the connection. + * + * To gracefully close the connection, use `close()`. + */ + terminate(): void; + + /** + * Sends a ping. + * + * @param data The data to send + */ + ping(data?: string | BufferSource): ServerWebSocketSendStatus; + + /** + * Sends a pong. + * + * @param data The data to send + */ + pong(data?: string | BufferSource): ServerWebSocketSendStatus; + + /** + * Sends a message to subscribers of the topic. + * + * @param topic The topic name. + * @param data The data to send. + * @param compress Should the data be compressed? If the client does not support compression, this is ignored. + * @example + * ws.publish("chat", "Hello!"); + * ws.publish("chat", "Compress this.", true); + * ws.publish("chat", new Uint8Array([1, 2, 3, 4])); + */ + publish(topic: string, data: string | BufferSource, compress?: boolean): ServerWebSocketSendStatus; + + /** + * Sends a text message to subscribers of the topic. + * + * @param topic The topic name. + * @param data The data to send. + * @param compress Should the data be compressed? If the client does not support compression, this is ignored. + * @example + * ws.publish("chat", "Hello!"); + * ws.publish("chat", "Compress this.", true); + */ + publishText(topic: string, data: string, compress?: boolean): ServerWebSocketSendStatus; + + /** + * Sends a binary message to subscribers of the topic. + * + * @param topic The topic name. + * @param data The data to send. + * @param compress Should the data be compressed? If the client does not support compression, this is ignored. + * @example + * ws.publish("chat", new TextEncoder().encode("Hello!")); + * ws.publish("chat", new Uint8Array([1, 2, 3, 4]), true); + */ + publishBinary(topic: string, data: BufferSource, compress?: boolean): ServerWebSocketSendStatus; + + /** + * Subscribes a client to the topic. + * + * @param topic The topic name. + * @example + * ws.subscribe("chat"); + */ + subscribe(topic: string): void; + + /** + * Unsubscribes a client to the topic. + * + * @param topic The topic name. + * @example + * ws.unsubscribe("chat"); + */ + unsubscribe(topic: string): void; + + /** + * Is the client subscribed to a topic? + * + * @param topic The topic name. + * @example + * ws.subscribe("chat"); + * console.log(ws.isSubscribed("chat")); // true + */ + isSubscribed(topic: string): boolean; + + /** + * Batches `send()` and `publish()` operations, which makes it faster to send data. + * + * The `message`, `open`, and `drain` callbacks are automatically corked, so + * you only need to call this if you are sending messages outside of those + * callbacks or in async functions. + * + * @param callback The callback to run. + * @example + * ws.cork((ctx) => { + * ctx.send("These messages"); + * ctx.sendText("are sent"); + * ctx.sendBinary(new TextEncoder().encode("together!")); + * }); + */ + cork(callback: (ws: ServerWebSocket) => T): T; + + /** + * The IP address of the client. + * + * @example + * console.log(socket.remoteAddress); // "127.0.0.1" + */ + readonly remoteAddress: string; + + /** + * The ready state of the client. + * + * - if `0`, the client is connecting. + * - if `1`, the client is connected. + * - if `2`, the client is closing. + * - if `3`, the client is closed. + * + * @example + * console.log(socket.readyState); // 1 + */ + readonly readyState: WebSocketReadyState; + + /** + * Sets how binary data is returned in events. + * + * - if `nodebuffer`, binary data is returned as `Buffer` objects. **(default)** + * - if `arraybuffer`, binary data is returned as `ArrayBuffer` objects. + * - if `uint8array`, binary data is returned as `Uint8Array` objects. + * + * @example + * let ws: WebSocket; + * ws.binaryType = "uint8array"; + * ws.addEventListener("message", ({ data }) => { + * console.log(data instanceof Uint8Array); // true + * }); + */ + binaryType?: "nodebuffer" | "arraybuffer" | "uint8array"; + + /** + * Custom data that you can assign to a client, can be read and written at any time. + * + * @example + * import { serve } from "bun"; + * + * serve({ + * fetch(request, server) { + * const data = { + * accessToken: request.headers.get("Authorization"), + * }; + * if (server.upgrade(request, { data })) { + * return; + * } + * return new Response(); + * }, + * websocket: { + * open(ws) { + * console.log(ws.data.accessToken); + * } + * } + * }); + */ + data: T; + + getBufferedAmount(): number; + } + + /** + * Compression options for WebSocket messages. + */ + type WebSocketCompressor = + | "disable" + | "shared" + | "dedicated" + | "3KB" + | "4KB" + | "8KB" + | "16KB" + | "32KB" + | "64KB" + | "128KB" + | "256KB"; + + /** + * Create a server-side {@link ServerWebSocket} handler for use with {@link Bun.serve} + * + * @example + * ```ts + * import { websocket, serve } from "bun"; + * + * serve<{name: string}>({ + * port: 3000, + * websocket: { + * open: (ws) => { + * console.log("Client connected"); + * }, + * message: (ws, message) => { + * console.log(`${ws.data.name}: ${message}`); + * }, + * close: (ws) => { + * console.log("Client disconnected"); + * }, + * }, + * + * fetch(req, server) { + * const url = new URL(req.url); + * if (url.pathname === "/chat") { + * const upgraded = server.upgrade(req, { + * data: { + * name: new URL(req.url).searchParams.get("name"), + * }, + * }); + * if (!upgraded) { + * return new Response("Upgrade failed", { status: 400 }); + * } + * return; + * } + * return new Response("Hello World"); + * }, + * }); + * ``` + */ + interface WebSocketHandler { + /** + * Specify the type for the {@link ServerWebSocket.data} property on + * connecting websocket clients. You can pass this value when you make a + * call to {@link Server.upgrade}. + * + * This pattern exists in Bun due to a [TypeScript limitation (#26242)](https://github.com/microsoft/TypeScript/issues/26242) + * + * @example + * ```ts + * Bun.serve({ + * websocket: { + * data: {} as { name: string }, // ← Specify the type of `ws.data` like this + * message: (ws, message) => console.log(ws.data.name, 'says:', message); + * }, + * // ... + * }); + * ``` + */ + data?: T; + + /** + * Called when the server receives an incoming message. + * + * If the message is not a `string`, its type is based on the value of `binaryType`. + * - if `nodebuffer`, then the message is a `Buffer`. + * - if `arraybuffer`, then the message is an `ArrayBuffer`. + * - if `uint8array`, then the message is a `Uint8Array`. + * + * @param ws The websocket that sent the message + * @param message The message received + */ + message(ws: ServerWebSocket, message: string | Buffer): void | Promise; + + /** + * Called when a connection is opened. + * + * @param ws The websocket that was opened + */ + open?(ws: ServerWebSocket): void | Promise; + + /** + * Called when a connection was previously under backpressure, + * meaning it had too many queued messages, but is now ready to receive more data. + * + * @param ws The websocket that is ready for more data + */ + drain?(ws: ServerWebSocket): void | Promise; + + /** + * Called when a connection is closed. + * + * @param ws The websocket that was closed + * @param code The close code + * @param reason The close reason + */ + close?(ws: ServerWebSocket, code: number, reason: string): void | Promise; + + /** + * Called when a ping is sent. + * + * @param ws The websocket that received the ping + * @param data The data sent with the ping + */ + ping?(ws: ServerWebSocket, data: Buffer): void | Promise; + + /** + * Called when a pong is received. + * + * @param ws The websocket that received the ping + * @param data The data sent with the ping + */ + pong?(ws: ServerWebSocket, data: Buffer): void | Promise; + + /** + * Sets the maximum size of messages in bytes. + * + * Default is 16 MB, or `1024 * 1024 * 16` in bytes. + */ + maxPayloadLength?: number; + + /** + * Sets the maximum number of bytes that can be buffered on a single connection. + * + * Default is 16 MB, or `1024 * 1024 * 16` in bytes. + */ + backpressureLimit?: number; + + /** + * Sets if the connection should be closed if `backpressureLimit` is reached. + * + * @default false + */ + closeOnBackpressureLimit?: boolean; + + /** + * Sets the the number of seconds to wait before timing out a connection + * due to no messages or pings. + * + * @default 120 + */ + idleTimeout?: number; + + /** + * Should `ws.publish()` also send a message to `ws` (itself), if it is subscribed? + * + * @default false + */ + publishToSelf?: boolean; + + /** + * Should the server automatically send and respond to pings to clients? + * + * @default true + */ + sendPings?: boolean; + + /** + * Sets the compression level for messages, for clients that supports it. By default, compression is disabled. + * + * @default false + */ + perMessageDeflate?: + | boolean + | { + /** + * Sets the compression level. + */ + compress?: WebSocketCompressor | boolean; + /** + * Sets the decompression level. + */ + decompress?: WebSocketCompressor | boolean; + }; + } + + namespace Serve { + type ExtractRouteParams = T extends `${string}:${infer Param}/${infer Rest}` + ? { [K in Param]: string } & ExtractRouteParams + : T extends `${string}:${infer Param}` + ? { [K in Param]: string } + : T extends `${string}*` + ? {} + : {}; + + /** + * Development configuration for {@link Bun.serve} + */ + type Development = + | boolean + | { + /** + * Enable Hot Module Replacement for routes (including React Fast Refresh, if React is in use) + * + * @default true if process.env.NODE_ENV !== 'production' + * + */ + hmr?: boolean; + + /** + * Enable console log streaming from browser to server + * @default false + */ + console?: boolean; + + /** + * Enable automatic workspace folders for Chrome DevTools + * + * This lets you persistently edit files in the browser. It works by adding the following route to the server: + * `/.well-known/appspecific/com.chrome.devtools.json` + * + * The response is a JSON object with the following shape: + * ```json + * { + * "workspace": { + * "root": "", + * "uuid": "" + * } + * } + * ``` + * + * The `root` field is the current working directory of the server. + * The `"uuid"` field is a hash of the file that started the server and a hash of the current working directory. + * + * For security reasons, if the remote socket address is not from localhost, 127.0.0.1, or ::1, the request is ignored. + * @default true + */ + chromeDevToolsAutomaticWorkspaceFolders?: boolean; + }; + + type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "HEAD" | "OPTIONS"; + + type Handler = (request: Req, server: S) => MaybePromise; + + type BaseRouteValue = Response | false | HTMLBundle | BunFile; + + type Routes = { + [Path in R]: + | BaseRouteValue + | Handler, Server, Response> + | Partial, Server, Response>>>; + }; + + type RoutesWithUpgrade = { + [Path in R]: + | BaseRouteValue + | Handler, Server, Response | undefined | void> + | Partial, Server, Response | undefined | void>>>; + }; + + type FetchOrRoutes = + | { + /** + * Handle HTTP requests + * + * Respond to {@link Request} objects with a {@link Response} object. + */ + fetch?(this: Server, req: Request, server: Server): MaybePromise; + + routes: Routes; + } + | { + /** + * Handle HTTP requests + * + * Respond to {@link Request} objects with a {@link Response} object. + */ + fetch(this: Server, req: Request, server: Server): MaybePromise; + + routes?: Routes; + }; + + type FetchOrRoutesWithWebSocket = { + /** + * Enable websockets with {@link Bun.serve} + * + * Upgrade a {@link Request} to a {@link ServerWebSocket} via {@link Server.upgrade} + * + * Pass `data` in {@link Server.upgrade} to attach data to the {@link ServerWebSocket.data} property + * + * @example + * ```js + * const server: Bun.Server = Bun.serve({ + * websocket: { + * open: (ws) => { + * console.log("Client connected"); + * }, + * message: (ws, message) => { + * console.log("Client sent message", message); + * }, + * close: (ws) => { + * console.log("Client disconnected"); + * }, + * }, + * fetch(req, server) { + * const url = new URL(req.url); + * if (url.pathname === "/chat") { + * const upgraded = server.upgrade(req); + * if (!upgraded) { + * return new Response("Upgrade failed", { status: 400 }); + * } + * } + * return new Response("Hello World"); + * }, + * }); + * ``` + */ + websocket: WebSocketHandler; + } & ( + | { + /** + * Handle HTTP requests, or call {@link Server.upgrade} and return early + * + * Respond to {@link Request} objects with a {@link Response} object. + */ + fetch?( + this: Server, + req: Request, + server: Server, + ): MaybePromise; + + routes: RoutesWithUpgrade; + } + | { + /** + * Handle HTTP requests, or call {@link Server.upgrade} and return early + * + * Respond to {@link Request} objects with a {@link Response} object. + */ + fetch( + this: Server, + req: Request, + server: Server, + ): MaybePromise; + + routes?: RoutesWithUpgrade; + } + ); + + interface BaseServeOptions { + /** + * Set options for using TLS with this server + * + * @example + * ```ts + * const server = Bun.serve({ + * fetch: request => new Response("Welcome to Bun!"), + * tls: { + * cert: Bun.file("cert.pem"), + * key: Bun.file("key.pem"), + * ca: [Bun.file("ca1.pem"), Bun.file("ca2.pem")], + * }, + * }); + * ``` + */ + tls?: TLSOptions | TLSOptions[]; + + /** + * What is the maximum size of a request body? (in bytes) + * @default 1024 * 1024 * 128 // 128MB + */ + maxRequestBodySize?: number; + + /** + * Render contextual errors? This enables bun's error page + * @default process.env.NODE_ENV !== 'production' + */ + development?: Development; + + /** + * Callback called when an error is thrown during request handling + * @param error The error that was thrown + * @returns A response to send to the client + * + * @example + * ```ts + * error: (error) => { + * return new Response("Internal Server Error", { status: 500 }); + * } + * ``` + */ + error?: (this: Server, error: ErrorLike) => Response | Promise | void | Promise; + + /** + * Uniquely identify a server instance with an ID + * + * --- + * + * **When bun is started with the `--hot` flag**: + * + * This string will be used to hot reload the server without interrupting + * pending requests or websockets. If not provided, a value will be + * generated. To disable hot reloading, set this value to `null`. + * + * **When bun is not started with the `--hot` flag**: + * + * This string will currently do nothing. But in the future it could be useful for logs or metrics. + */ + id?: string | null; + } + + interface HostnamePortServeOptions extends BaseServeOptions { + /** + * What hostname should the server listen on? + * + * @default + * ```js + * "0.0.0.0" // listen on all interfaces + * ``` + * @example + * ```js + * "127.0.0.1" // Only listen locally + * ``` + * @example + * ```js + * "remix.run" // Only listen on remix.run + * ```` + * + * note: hostname should not include a {@link port} + */ + hostname?: "0.0.0.0" | "127.0.0.1" | "localhost" | (string & {}); + + /** + * What port should the server listen on? + * @default process.env.PORT || "3000" + */ + port?: string | number; + + /** + * Whether the `SO_REUSEPORT` flag should be set. + * + * This allows multiple processes to bind to the same port, which is useful for load balancing. + * + * @default false + */ + reusePort?: boolean; + + /** + * Whether the `IPV6_V6ONLY` flag should be set. + * @default false + */ + ipv6Only?: boolean; + + /** + * Sets the the number of seconds to wait before timing out a connection + * due to inactivity. + * + * @default 10 + */ + idleTimeout?: number; + } + + interface UnixServeOptions extends BaseServeOptions { + /** + * If set, the HTTP server will listen on a unix socket instead of a port. + * (Cannot be used with hostname+port) + */ + unix?: string; + } + + /** + * The type of options that can be passed to {@link serve}, with support for + * `routes` and a safer requirement for `fetch` + * + * @example + * ```ts + * export default { + * fetch: req => Response.json(req.url), + * + * websocket: { + * message(ws) { + * ws.data.name; // string + * }, + * }, + * } satisfies Bun.Serve.Options<{ name: string }>; + * ``` + */ + type Options = Bun.__internal.XOR< + HostnamePortServeOptions, + UnixServeOptions + > & + Bun.__internal.XOR, FetchOrRoutesWithWebSocket>; + } + + interface BunRequest extends Request { + readonly params: { + [Key in keyof Serve.ExtractRouteParams]: Serve.ExtractRouteParams[Key]; + } & {}; + readonly cookies: CookieMap; + clone(): BunRequest; + } + + /** + * HTTP & HTTPS Server + * + * To start the server, see {@link serve} + * + * For performance, Bun pre-allocates most of the data for 2048 concurrent requests. + * That means starting a new server allocates about 500 KB of memory. Try to + * avoid starting and stopping the server often (unless it's a new instance of bun). + * + * Powered by a fork of [uWebSockets](https://github.com/uNetworking/uWebSockets). Thank you \@alexhultman. + */ + interface Server extends Disposable { + /** + * Stop listening to prevent new connections from being accepted. + * + * By default, it does not cancel in-flight requests or websockets. That means it may take some time before all network activity stops. + * + * @param closeActiveConnections Immediately terminate in-flight requests, websockets, and stop accepting new connections. + * @default false + */ + stop(closeActiveConnections?: boolean): Promise; + + /** + * Update the `fetch` and `error` handlers without restarting the server. + * + * This is useful if you want to change the behavior of your server without + * restarting it or for hot reloading. + * + * @example + * + * ```js + * // create the server + * const server = Bun.serve({ + * fetch(request) { + * return new Response("Hello World v1") + * } + * }); + * + * // Update the server to return a different response + * server.reload({ + * fetch(request) { + * return new Response("Hello World v2") + * } + * }); + * ``` + * + * Passing other options such as `port` or `hostname` won't do anything. + */ + reload(options: Serve.Options): Server; + + /** + * Mock the fetch handler for a running server. + * + * This feature is not fully implemented yet. It doesn't normalize URLs + * consistently in all cases and it doesn't yet call the `error` handler + * consistently. This needs to be fixed + */ + fetch(request: Request | string): Response | Promise; + + /** + * Upgrade a {@link Request} to a {@link ServerWebSocket} + * + * @param request The {@link Request} to upgrade + * @param options Pass headers or attach data to the {@link ServerWebSocket} + * + * @returns `true` if the upgrade was successful and `false` if it failed + * + * @example + * ```js + * import { serve } from "bun"; + * const server: Bun.Server<{ user: string }> = serve({ + * websocket: { + * open: (ws) => { + * console.log("Client connected"); + * }, + * message: (ws, message) => { + * console.log("Client sent message", message); + * }, + * close: (ws) => { + * console.log("Client disconnected"); + * }, + * }, + * fetch(req, server) { + * const url = new URL(req.url); + * if (url.pathname === "/chat") { + * const upgraded = server.upgrade(req, { + * data: {user: "John Doe"} + * }); + * if (!upgraded) { + * return new Response("Upgrade failed", { status: 400 }); + * } + * } + * return new Response("Hello World"); + * }, + * }); + * ``` + * + * What you pass to `data` is available on the {@link ServerWebSocket.data} property + */ + upgrade( + request: Request, + ...options: [WebSocketData] extends [undefined] + ? [ + options?: { + /** + */ + headers?: HeadersInit; + + /** + * Data to store on the WebSocket instance + * + * --- + * + * **Surprised this line is erroring?** + * + * Tell TypeScript about the WebSocket data by using `Bun.Server` + * + * ```ts + * const server: Bun.Server = Bun.serve({ + * fetch: (req, server) => { + * const didUpgrade = server.upgrade(req, { + * data: { ... }, // Works now! + * }); + * }, + * }); + * ``` + */ + data?: undefined; + }, + ] + : [ + options: { + /** + * Send any additional headers while upgrading, like cookies + */ + headers?: HeadersInit; + + /** + * Data to store on the WebSocket instance + */ + data: WebSocketData; + }, + ] + ): boolean; + + /** + * Send a message to all connected {@link ServerWebSocket} subscribed to a topic + * + * @param topic The topic to publish to + * @param data The data to send + * @param compress Should the data be compressed? Ignored if the client does not support compression. + * + * @returns 0 if the message was dropped, -1 if backpressure was applied, or the number of bytes sent. + * + * @example + * + * ```js + * server.publish("chat", "Hello World"); + * ``` + * + * @example + * ```js + * server.publish("chat", new Uint8Array([1, 2, 3, 4])); + * ``` + * + * @example + * ```js + * server.publish("chat", new ArrayBuffer(4), true); + * ``` + * + * @example + * ```js + * server.publish("chat", new DataView(new ArrayBuffer(4))); + * ``` + */ + publish( + topic: string, + data: string | ArrayBufferView | ArrayBuffer | SharedArrayBuffer, + compress?: boolean, + ): ServerWebSocketSendStatus; + + /** + * A count of connections subscribed to a given topic + * + * This operation will loop through each topic internally to get the count. + * + * @param topic the websocket topic to check how many subscribers are connected to + * @returns the number of subscribers + */ + subscriberCount(topic: string): number; + + /** + * Returns the client IP address and port of the given Request. If the request was closed or is a unix socket, returns null. + * + * @example + * ```js + * export default { + * async fetch(request, server) { + * return new Response(server.requestIP(request)); + * } + * } + * ``` + */ + requestIP(request: Request): SocketAddress | null; + + /** + * Reset the idleTimeout of the given Request to the number in seconds. 0 means no timeout. + * + * @example + * ```js + * export default { + * async fetch(request, server) { + * server.timeout(request, 60); + * await Bun.sleep(30000); + * return new Response("30 seconds have passed"); + * } + * } + * ``` + */ + timeout(request: Request, seconds: number): void; + + /** + * Undo a call to {@link Server.unref} + * + * If the Server has already been stopped, this does nothing. + * + * If {@link Server.ref} is called multiple times, this does nothing. Think of it as a boolean toggle. + */ + ref(): void; + + /** + * Don't keep the process alive if this server is the only thing left. + * Active connections may continue to keep the process alive. + * + * By default, the server is ref'd. + * + * To prevent new connections from being accepted, use {@link Server.stop} + */ + unref(): void; + + /** + * How many requests are in-flight right now? + */ + readonly pendingRequests: number; + + /** + * How many {@link ServerWebSocket}s are in-flight right now? + */ + readonly pendingWebSockets: number; + + readonly url: URL; + + /** + * The port the server is listening on. + * + * This will be undefined when the server is listening on a unix socket. + * + * @example + * ```js + * 3000 + * ``` + */ + readonly port: number | undefined; + + /** + * The hostname the server is listening on. Does not include the port. + * + * This will be `undefined` when the server is listening on a unix socket. + * + * @example + * ```js + * "localhost" + * ``` + */ + readonly hostname: string | undefined; + + /** + * Is the server running in development mode? + * + * In development mode, `Bun.serve()` returns rendered error messages with + * stack traces instead of a generic 500 error. This makes debugging easier, + * but development mode shouldn't be used in production or you will risk + * leaking sensitive information. + */ + readonly development: boolean; + + /** + * An identifier of the server instance + * + * When bun is started with the `--hot` flag, this ID is used to hot reload the server without interrupting pending requests or websockets. + * + * When bun is not started with the `--hot` flag, this ID is currently unused. + */ + readonly id: string; + } + + /** + * Bun.serve provides a high-performance HTTP server with built-in routing support. + * It enables both function-based and object-based route handlers with type-safe + * parameters and method-specific handling. + * + * @param options Server configuration options + * + * @example + * **Basic Usage** + * + * ```ts + * Bun.serve({ + * port: 3000, + * fetch(req) { + * return new Response("Hello World"); + * } + * }); + * ``` + * + * @example + * **Route-based Handlers** + * + * ```ts + * Bun.serve({ + * routes: { + * // Static responses + * "/": new Response("Home page"), + * + * // Function handlers with type-safe parameters + * "/users/:id": (req) => { + * // req.params.id is typed as string + * return new Response(`User ${req.params.id}`); + * }, + * + * // Method-specific handlers + * "/api/posts": { + * GET: () => new Response("Get posts"), + * POST: async (req) => { + * const body = await req.json(); + * return new Response("Created post"); + * }, + * DELETE: (req) => new Response("Deleted post") + * }, + * + * // Wildcard routes + * "/static/*": (req) => { + * // Handle any path under /static/ + * return new Response("Static file"); + * }, + * + * // Disable route (fall through to fetch handler) + * "/api/legacy": false + * }, + * + * // Fallback handler for unmatched routes + * fetch(req) { + * return new Response("Not Found", { status: 404 }); + * } + * }); + * ``` + * + * @example + * **Path Parameters** + * + * ```ts + * Bun.serve({ + * routes: { + * // Single parameter + * "/users/:id": (req: BunRequest<"/users/:id">) => { + * return new Response(`User ID: ${req.params.id}`); + * }, + * + * // Multiple parameters + * "/posts/:postId/comments/:commentId": ( + * req: BunRequest<"/posts/:postId/comments/:commentId"> + * ) => { + * return new Response(JSON.stringify(req.params)); + * // Output: {"postId": "123", "commentId": "456"} + * } + * } + * }); + * ``` + * + * @example + * **Route Precedence** + * + * ```ts + * // Routes are matched in the following order: + * // 1. Exact static routes ("/about") + * // 2. Parameter routes ("/users/:id") + * // 3. Wildcard routes ("/api/*") + * + * Bun.serve({ + * routes: { + * "/api/users": () => new Response("Users list"), + * "/api/users/:id": (req) => new Response(`User ${req.params.id}`), + * "/api/*": () => new Response("API catchall"), + * "/*": () => new Response("Root catchall") + * } + * }); + * ``` + * + * @example + * **Error Handling** + * + * ```ts + * Bun.serve({ + * routes: { + * "/error": () => { + * throw new Error("Something went wrong"); + * } + * }, + * error(error) { + * // Custom error handler + * console.error(error); + * return new Response(`Error: ${error.message}`, { + * status: 500 + * }); + * } + * }); + * ``` + * + * @example + * **Server Lifecycle** + * + * ```ts + * const server = Bun.serve({ + * // Server config... + * }); + * + * // Update routes at runtime + * server.reload({ + * routes: { + * "/": () => new Response("Updated route") + * } + * }); + * + * // Stop the server + * server.stop(); + * ``` + * + * @example + * **Development Mode** + * + * ```ts + * Bun.serve({ + * development: true, // Enable hot reloading + * routes: { + * // Routes will auto-reload on changes + * } + * }); + * ``` + * + * @example + * **Type-Safe Request Handling** + * + * ```ts + * type Post = { + * id: string; + * title: string; + * }; + * + * Bun.serve({ + * routes: { + * "/api/posts/:id": async ( + * req: BunRequest<"/api/posts/:id"> + * ) => { + * if (req.method === "POST") { + * const body: Post = await req.json(); + * return Response.json(body); + * } + * return new Response("Method not allowed", { + * status: 405 + * }); + * } + * } + * }); + * ``` + */ + function serve( + options: Serve.Options, + ): Server; +} diff --git a/src/bake/bake.d.ts b/src/bake/bake.d.ts index 06189b6bd3..91c04d7fed 100644 --- a/src/bake/bake.d.ts +++ b/src/bake/bake.d.ts @@ -5,8 +5,6 @@ // /// declare module "bun" { - type Awaitable = T | Promise; - declare namespace Bake { interface Options { /** @@ -369,7 +367,7 @@ declare module "bun" { * A common pattern would be to enforce the object is * `{ default: ReactComponent }` */ - render: (request: Request, routeMetadata: RouteMetadata) => Awaitable; + render: (request: Request, routeMetadata: RouteMetadata) => MaybePromise; /** * Prerendering does not use a request, and is allowed to generate * multiple responses. This is used for static site generation, but not @@ -379,7 +377,7 @@ declare module "bun" { * Note that `import.meta.env.STATIC` will be inlined to true during * a static build. */ - prerender?: (routeMetadata: RouteMetadata) => Awaitable; + prerender?: (routeMetadata: RouteMetadata) => MaybePromise; // TODO: prerenderWithoutProps (for partial prerendering) /** * For prerendering routes with dynamic parameters, such as `/blog/:slug`, @@ -409,7 +407,7 @@ declare module "bun" { * return { exhaustive: false }; * } */ - getParams?: (paramsMetadata: ParamsMetadata) => Awaitable; + getParams?: (paramsMetadata: ParamsMetadata) => MaybePromise; /** * When a dynamic build uses static assets, Bun can map content types in the * user's `Accept` header to the different static files. @@ -448,7 +446,7 @@ declare module "bun" { } interface DevServerHookEntryPoint { - default: (dev: DevServerHookAPI) => Awaitable; + default: (dev: DevServerHookAPI) => MaybePromise; } interface DevServerHookAPI { @@ -505,7 +503,7 @@ declare module "bun" { } } - declare interface GenericServeOptions { + declare interface BaseServeOptions { /** Add a fullstack web app to this server using Bun Bake */ app?: Bake.Options | undefined; } diff --git a/test/harness.ts b/test/harness.ts index 8da328605f..f2504723c7 100644 --- a/test/harness.ts +++ b/test/harness.ts @@ -15,8 +15,6 @@ import os from "node:os"; import { dirname, isAbsolute, join } from "path"; import * as numeric from "_util/numeric.ts"; -type Awaitable = T | Promise; - export const BREAKING_CHANGES_BUN_1_2 = false; export const isMacOS = process.platform === "darwin"; @@ -184,7 +182,7 @@ export type DirectoryTree = { | string | Buffer | DirectoryTree - | ((opts: { root: string }) => Awaitable); + | ((opts: { root: string }) => Bun.MaybePromise); }; export async function makeTree(base: string, tree: DirectoryTree) { diff --git a/test/integration/bun-types/bun-types.test.ts b/test/integration/bun-types/bun-types.test.ts index 590843de25..b237faf909 100644 --- a/test/integration/bun-types/bun-types.test.ts +++ b/test/integration/bun-types/bun-types.test.ts @@ -605,7 +605,7 @@ describe("@types/bun integration test", () => { }, { code: 2345, - line: "index.ts:326:29", + line: "index.ts:322:29", message: "Argument of type '{ headers: { \"x-bun\": string; }; }' is not assignable to parameter of type 'number'.", }, diff --git a/test/integration/bun-types/fixture/index.ts b/test/integration/bun-types/fixture/index.ts index 70ab520945..2618c9a96a 100644 --- a/test/integration/bun-types/fixture/index.ts +++ b/test/integration/bun-types/fixture/index.ts @@ -270,10 +270,6 @@ Bun.serve({ port: 3000, fetch: () => new Response("ok"), - // don't do this, use the `tls: {}` options instead - key: Bun.file(""), // dont do it! - cert: Bun.file(""), // dont do it! - tls: { key: Bun.file(""), // do this! cert: Bun.file(""), // do this! diff --git a/test/integration/bun-types/fixture/serve-types.test.ts b/test/integration/bun-types/fixture/serve-types.test.ts index 408081cb31..1f71f0434d 100644 --- a/test/integration/bun-types/fixture/serve-types.test.ts +++ b/test/integration/bun-types/fixture/serve-types.test.ts @@ -13,27 +13,35 @@ function tmpdirSync(pattern: string = "bun.test."): string { return fs.mkdtempSync(join(fs.realpathSync.native(os.tmpdir()), pattern)); } +export default { + fetch: req => Response.json(req.url), + websocket: { + message(ws) { + expectType(ws.data).is<{ name: string }>(); + }, + }, +} satisfies Bun.ServeOptions<{ name: string }>; + function expectInstanceOf(value: unknown, constructor: new (...args: any[]) => T): asserts value is T { expect(value).toBeInstanceOf(constructor); } -function test }>( +function test( name: string, - serveConfig: Bun.ServeFunctionOptions, + options: Bun.Serve.Options, { onConstructorFailure, overrideExpectBehavior, + skip: skipOptions, }: { onConstructorFailure?: (error: Error) => void | Promise; - overrideExpectBehavior?: (server: Bun.Server) => void | Promise; + overrideExpectBehavior?: (server: NoInfer>) => void | Promise; + skip?: boolean; } = {}, ) { - if ("unix" in serveConfig && typeof serveConfig.unix === "string" && process.platform === "win32") { - // Skip unix socket tests on Windows - return; - } + const skip = skipOptions || ("unix" in options && typeof options.unix === "string" && process.platform === "win32"); - async function testServer(server: Bun.Server) { + async function testServer(server: Bun.Server) { if (overrideExpectBehavior) { await overrideExpectBehavior(server); } else { @@ -45,9 +53,9 @@ function test { + it.skipIf(skip)(name, async () => { try { - using server = Bun.serve(serveConfig); + using server = Bun.serve(options); try { await testServer(server); } finally { @@ -107,18 +115,21 @@ test( test("basic + websocket + upgrade", { websocket: { message(ws, message) { - expectType().is>(); + expectType().is>(); ws.send(message); + expectType(message).is>(); }, }, fetch(req, server) { + expectType(req).is(); + // Upgrade to a ServerWebSocket if we can // This automatically checks for the `Sec-WebSocket-Key` header // meaning you don't have to check headers, you can just call `upgrade()` if (server.upgrade(req)) { // When upgrading, we return undefined since we don't want to send a Response - return; + // return; } return new Response("Regular HTTP response"); @@ -127,6 +138,16 @@ test("basic + websocket + upgrade", { test("basic + websocket + upgrade + all handlers", { fetch(req, server) { + expectType(server.upgrade).is< + ( + req: Request, + options: { + data?: { name: string }; + headers?: Bun.HeadersInit; + }, + ) => boolean + >; + const url = new URL(req.url); if (url.pathname === "/chat") { if ( @@ -147,20 +168,26 @@ test("basic + websocket + upgrade + all handlers", { }, websocket: { - open(ws: Bun.ServerWebSocket<{ name: string }>) { + data: {} as { name: string }, + + open(ws) { console.log("WebSocket opened"); ws.subscribe("the-group-chat"); }, message(ws, message) { + expectType(message).is>(); ws.publish("the-group-chat", `${ws.data.name}: ${message.toString()}`); }, close(ws, code, reason) { + expectType(code).is(); + expectType(reason).is(); ws.publish("the-group-chat", `${ws.data.name} left the chat`); }, drain(ws) { + expectType(ws.data.name).is(); console.log("Please send me data. I am ready to receive it."); }, @@ -201,7 +228,7 @@ test("port 0 + websocket + upgrade", { }, websocket: { message(ws) { - expectType(ws).is>(); + expectType(ws).is>(); }, }, }); @@ -269,9 +296,13 @@ test( { unix: `${tmpdirSync()}/bun.sock`, fetch(req, server) { - server.upgrade(req); + if (server.upgrade(req)) { + return; + } + return new Response(); }, + websocket: { message() {} }, }, { overrideExpectBehavior: server => { @@ -504,11 +535,10 @@ test("basic websocket upgrade and ws publish/subscribe to topics", { test( "port with unix socket (is a type error)", - // This prettier-ignore exists because between TypeScript 5.8 and 5.9, the location of the error message changed, so - // to satisfy both we can just keep what would have been the two erroring lines on the same line - // prettier-ignore - // @ts-expect-error - { unix: `${tmpdirSync()}/bun.sock`, port: 0, + // @ts-expect-error Cannot pass unix and port + { + unix: `${tmpdirSync()}/bun.sock`, + port: 0, fetch() { return new Response(); }, @@ -524,10 +554,10 @@ test( test( "port with unix socket with websocket + upgrade (is a type error)", - // Prettier ignore exists for same reason as above - // prettier-ignore - // @ts-expect-error - { unix: `${tmpdirSync()}/bun.sock`, port: 0, + // @ts-expect-error cannot pass unix and port at same time + { + unix: `${tmpdirSync()}/bun.sock`, + port: 0, fetch(req, server) { server.upgrade(req); if (Math.random() > 0.5) return undefined; @@ -543,3 +573,246 @@ test( }, }, ); + +test("hostname: 0.0.0.0 (default - listen on all interfaces)", { + hostname: "0.0.0.0", + fetch() { + return new Response("listening on all interfaces"); + }, +}); + +test("hostname: 127.0.0.1 (localhost only)", { + hostname: "127.0.0.1", + fetch() { + return new Response("listening on localhost only"); + }, +}); + +test("hostname: localhost", { + hostname: "localhost", + fetch() { + return new Response("listening on localhost"); + }, +}); + +test( + "hostname: custom IPv4 address", + { + hostname: "192.168.1.100", + fetch() { + return new Response("custom hostname"); + }, + }, + { + onConstructorFailure: error => { + expect(error.message).toContain("Failed to start server"); + }, + }, +); + +test("port: number type", { + port: 3000, + fetch() { + return new Response("port as number"); + }, +}); + +test("port: string type", { + port: "3001", + fetch() { + return new Response("port as string"); + }, +}); + +test("port: 0 (random port assignment)", { + port: 0, + fetch() { + return new Response("random port"); + }, +}); + +test( + "port: from environment variable", + { + port: process.env.PORT || "3002", + fetch() { + return new Response("port from env"); + }, + }, + { + overrideExpectBehavior: server => { + expect(server.port).toBeGreaterThan(0); + expect(server.url).toBeDefined(); + }, + }, +); + +test("reusePort: false (default)", { + reusePort: false, + port: 0, + fetch() { + return new Response("reusePort false"); + }, +}); + +test("reusePort: true", { + reusePort: true, + port: 0, + fetch() { + return new Response("reusePort true"); + }, +}); + +test("ipv6Only: false (default)", { + ipv6Only: false, + port: 0, + fetch() { + return new Response("ipv6Only false"); + }, +}); + +test("idleTimeout: default (10 seconds)", { + port: 0, + fetch() { + return new Response("default idleTimeout"); + }, +}); + +test("idleTimeout: custom value (30 seconds)", { + idleTimeout: 30, + port: 0, + fetch() { + return new Response("custom idleTimeout"); + }, +}); + +test("idleTimeout: 0 (no timeout)", { + idleTimeout: 0, + port: 0, + fetch() { + return new Response("no idleTimeout"); + }, +}); + +test("maxRequestBodySize: default (128MB)", { + port: 0, + fetch() { + return new Response("default maxRequestBodySize"); + }, +}); + +test("maxRequestBodySize: custom small value", { + maxRequestBodySize: 1024 * 1024, // 1MB + port: 0, + fetch() { + return new Response("small maxRequestBodySize"); + }, +}); + +test("maxRequestBodySize: custom large value", { + maxRequestBodySize: 1024 * 1024 * 1024, // 1GB + port: 0, + fetch() { + return new Response("large maxRequestBodySize"); + }, +}); + +test("development: true", { + development: true, + port: 0, + fetch() { + return new Response("development mode on"); + }, +}); + +test("development: false", { + development: false, + port: 0, + fetch() { + return new Response("development mode off"); + }, +}); + +test("development: defaults to process.env.NODE_ENV !== 'production'", { + development: process.env.NODE_ENV !== "production", + port: 0, + fetch() { + return new Response("development from env"); + }, +}); + +test( + "error callback handles errors", + { + port: 0, + fetch() { + throw new Error("Test error"); + }, + error(error) { + return new Response(`Error handled: ${error.message}`, { status: 500 }); + }, + }, + { + overrideExpectBehavior: async server => { + const res = await fetch(server.url); + expect(res.status).toBe(500); + expect(await res.text()).toBe("Error handled: Test error"); + }, + }, +); + +test( + "error callback with async handler", + { + port: 0, + fetch() { + throw new Error("Async test error"); + }, + async error(error) { + await new Promise(resolve => setTimeout(resolve, 10)); + return new Response(`Async error handled: ${error.message}`, { status: 503 }); + }, + }, + { + overrideExpectBehavior: async server => { + const res = await fetch(server.url); + expect(res.status).toBe(503); + expect(await res.text()).toBe("Async error handled: Async test error"); + }, + }, +); + +test("id: custom server identifier", { + id: "my-custom-server-id", + port: 0, + fetch() { + return new Response("server with custom id"); + }, +}); + +test("id: null (no identifier)", { + id: null, + port: 0, + fetch() { + return new Response("server with null id"); + }, +}); + +test("multiple properties combined", { + hostname: "127.0.0.1", + port: 0, + reusePort: true, + idleTimeout: 20, + maxRequestBodySize: 1024 * 1024 * 10, // 10MB + development: true, + id: "combined-test-server", + fetch(req) { + return Response.json({ + url: req.url, + method: req.method, + }); + }, + error(error) { + return new Response(`Combined server error: ${error.message}`, { status: 500 }); + }, +}); diff --git a/test/integration/bun-types/fixture/serve.ts b/test/integration/bun-types/fixture/serve.ts new file mode 100644 index 0000000000..6443fcd55c --- /dev/null +++ b/test/integration/bun-types/fixture/serve.ts @@ -0,0 +1,86 @@ +// This file is merely types only, you (probably) want to put the tests in ./serve-types.test.ts instead + +import { expectType } from "./utilities"; + +Bun.serve({ + routes: { + "/:id/:test": req => { + expectType(req.params).is<{ id: string; test: string }>(); + }, + }, + fetch: () => new Response("hello"), + websocket: { + message(ws, message) { + expectType(ws.data).is(); + expectType(message).is>(); + }, + }, +}); + +const s1 = Bun.serve({ + routes: { + "/ws/:name": req => { + expectType(req.params.name).is(); + + s1.upgrade(req, { + data: { name: req.params.name }, + }); + }, + }, + websocket: { + data: {} as { name: string }, + + message(ws) { + ws.send(JSON.stringify(ws.data)); + }, + }, +}); + +const s2 = Bun.serve({ + routes: { + "/ws/:name": req => { + expectType(req.params.name).is(); + + // @ts-expect-error - Should error because data was not passed + s2.upgrade(req, {}); + }, + }, + websocket: { + data: {} as { name: string }, + message(ws) { + expectType(ws.data).is<{ name: string }>(); + }, + }, +}); + +const s3 = Bun.serve({ + routes: { + "/ws/:name": req => { + expectType(req.params.name).is(); + + // @ts-expect-error - Should error because data and object was not passed + s3.upgrade(req); + }, + }, + websocket: { + data: {} as { name: string }, + message(ws) { + expectType(ws.data).is<{ name: string }>(); + }, + }, +}); + +const s4 = Bun.serve({ + routes: { + "/ws/:name": req => { + expectType(req.params.name).is(); + + s4.upgrade(req); + }, + }, + websocket: { + message(ws) { + expectType(ws.data).is(); + }, + }, +});