diff --git a/bun.lock b/bun.lock index 2f08f1d900..5e7baad5b7 100644 --- a/bun.lock +++ b/bun.lock @@ -27,10 +27,8 @@ }, "packages/bun-types": { "name": "bun-types", - "version": "1.2.5", "dependencies": { "@types/node": "*", - "@types/ws": "*", }, "devDependencies": { "@biomejs/biome": "^1.5.3", @@ -166,8 +164,6 @@ "@types/semver": ["@types/semver@7.5.8", "", {}, "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ=="], - "@types/ws": ["@types/ws@8.5.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-4+q7P5h3SpJxaBft0Dzpbr6lmMaqh0Jr2tbhJZ/luAwvD7ohSCniYkwz/pLxuT2h0EOa6QADgJj1Ko+TzRfZ+w=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@7.16.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "7.16.1", "@typescript-eslint/type-utils": "7.16.1", "@typescript-eslint/utils": "7.16.1", "@typescript-eslint/visitor-keys": "7.16.1", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^1.3.0" }, "peerDependencies": { "@typescript-eslint/parser": "^7.0.0", "eslint": "^8.56.0" } }, "sha512-SxdPak/5bO0EnGktV05+Hq8oatjAYVY3Zh2bye9pGZy6+jwyR3LG3YKkV4YatlsgqXP28BTeVm9pqwJM96vf2A=="], "@typescript-eslint/parser": ["@typescript-eslint/parser@7.16.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "7.16.1", "@typescript-eslint/types": "7.16.1", "@typescript-eslint/typescript-estree": "7.16.1", "@typescript-eslint/visitor-keys": "7.16.1", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.56.0" } }, "sha512-u+1Qx86jfGQ5i4JjK33/FnawZRpsLxRnKzGE6EABZ40KxVT/vWsiZFEBBHjFOljmmV3MBYOHEKi0Jm9hbAOClA=="], @@ -916,8 +912,6 @@ "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], - "@types/ws/@types/node": ["@types/node@20.12.14", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-scnD59RpYD91xngrQQLGkE+6UrHUPzeKZWhhjBSa3HSkwjbQc38+q3RoIVEwxQGRw3M+j5hpNAM+lgV3cVormg=="], - "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], @@ -1008,8 +1002,6 @@ "@definitelytyped/utils/which/isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="], - "@types/ws/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], - "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], "are-we-there-yet/readable-stream/isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index 98a0fc6567..73196283ec 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -162,11 +162,6 @@ declare module "bun" { open: Event; } - interface EventListenerOptions { - /** Not directly used by Node.js. Added for API completeness. Default: `false`. */ - capture?: boolean; - } - interface AddEventListenerOptions extends EventListenerOptions { /** When `true`, the listener is automatically removed when it is first invoked. Default: `false`. */ once?: boolean; @@ -4264,6 +4259,211 @@ declare module "bun" { compact?: boolean; } + type WebSocketOptionsProtocolsOrProtocol = + | { + /** + * Protocols to use for the WebSocket connection + */ + protocols?: string | string[]; + } + | { + /** + * Protocol to use for the WebSocket connection + */ + protocol?: string; + }; + + type WebSocketOptionsTLS = { + /** + * Options for the TLS connection + */ + tls?: { + /** + * Whether to reject the connection if the certificate is not valid + * + * @default true + */ + rejectUnauthorized?: boolean; + }; + }; + + type WebSocketOptionsHeaders = { + /** + * Headers to send to the server + */ + headers?: import("node:http").OutgoingHttpHeaders; + }; + + /** + * Constructor options for the `Bun.WebSocket` client + */ + type WebSocketOptions = WebSocketOptionsProtocolsOrProtocol & WebSocketOptionsTLS & WebSocketOptionsHeaders; + + interface WebSocketEventMap { + close: CloseEvent; + error: Event; + message: MessageEvent; + open: Event; + } + + /** + * A WebSocket client implementation + * + * @example + * ```ts + * const ws = new WebSocket("ws://localhost:8080", { + * headers: { + * "x-custom-header": "hello", + * }, + * }); + * + * ws.addEventListener("open", () => { + * console.log("Connected to server"); + * }); + * + * ws.addEventListener("message", (event) => { + * console.log("Received message:", event.data); + * }); + * + * ws.send("Hello, server!"); + * ws.terminate(); + * ``` + */ + interface WebSocket extends EventTarget { + /** + * The URL of the WebSocket connection + */ + readonly url: string; + + /** + * Legacy URL property (same as url) + * @deprecated Use url instead + */ + readonly URL: string; + + /** + * The current state of the connection + */ + readonly readyState: + | typeof WebSocket.CONNECTING + | typeof WebSocket.OPEN + | typeof WebSocket.CLOSING + | typeof WebSocket.CLOSED; + + /** + * The number of bytes of data that have been queued using send() but not yet transmitted to the network + */ + readonly bufferedAmount: number; + + /** + * The protocol selected by the server + */ + readonly protocol: string; + + /** + * The extensions selected by the server + */ + readonly extensions: string; + + /** + * The type of binary data being received. + */ + binaryType: "arraybuffer" | "nodebuffer"; + + /** + * Event handler for open event + */ + onopen: ((this: WebSocket, ev: Event) => any) | null; + + /** + * Event handler for message event + */ + onmessage: ((this: WebSocket, ev: MessageEvent) => any) | null; + + /** + * Event handler for error event + */ + onerror: ((this: WebSocket, ev: Event) => any) | null; + + /** + * Event handler for close event + */ + onclose: ((this: WebSocket, ev: CloseEvent) => any) | null; + + /** + * Transmits data to the server + * @param data The data to send to the server + */ + send(data: string | ArrayBufferLike | ArrayBufferView): void; + + /** + * Closes the WebSocket connection + * @param code A numeric value indicating the status code + * @param reason A human-readable string explaining why the connection is closing + */ + close(code?: number, reason?: string): void; + + /** + * Sends a ping frame to the server + * @param data Optional data to include in the ping frame + */ + ping(data?: string | ArrayBufferLike | ArrayBufferView): void; + + /** + * Sends a pong frame to the server + * @param data Optional data to include in the pong frame + */ + pong(data?: string | ArrayBufferLike | ArrayBufferView): void; + + /** + * Immediately terminates the connection + */ + terminate(): void; + + /** + * Registers an event handler of a specific event type on the WebSocket. + * @param type A case-sensitive string representing the event type to listen for + * @param listener The function to be called when the event occurs + * @param options An options object that specifies characteristics about the event listener + */ + addEventListener( + type: K, + listener: (this: WebSocket, ev: WebSocketEventMap[K]) => any, + options?: boolean | AddEventListenerOptions, + ): void; + addEventListener( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | AddEventListenerOptions, + ): void; + + /** + * Removes an event listener previously registered with addEventListener() + * @param type A case-sensitive string representing the event type to remove + * @param listener The function to remove from the event target + * @param options An options object that specifies characteristics about the event listener + */ + removeEventListener( + type: K, + listener: (this: WebSocket, ev: WebSocketEventMap[K]) => any, + options?: boolean | EventListenerOptions, + ): void; + removeEventListener( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | EventListenerOptions, + ): void; + + /** @deprecated Use instance property instead */ + readonly CONNECTING: 0; + /** @deprecated Use instance property instead */ + readonly OPEN: 1; + /** @deprecated Use instance property instead */ + readonly CLOSING: 2; + /** @deprecated Use instance property instead */ + readonly CLOSED: 3; + } + /** * Pretty-print an object the same as {@link console.log} to a `string` * diff --git a/packages/bun-types/globals.d.ts b/packages/bun-types/globals.d.ts index 9bde105386..1920f24f39 100644 --- a/packages/bun-types/globals.d.ts +++ b/packages/bun-types/globals.d.ts @@ -11,7 +11,7 @@ declare module "bun" { type NodeCryptoWebcryptoSubtleCrypto = import("crypto").webcrypto.SubtleCrypto; type NodeCryptoWebcryptoCryptoKey = import("crypto").webcrypto.CryptoKey; - type LibEmptyOrWSWebSocket = LibDomIsLoaded extends true ? {} : import("ws").WebSocket; + type LibEmptyOrBunWebSocket = LibDomIsLoaded extends true ? {} : Bun.WebSocket; type LibEmptyOrNodeUtilTextEncoder = LibDomIsLoaded extends true ? {} : import("node:util").TextEncoder; @@ -63,15 +63,71 @@ declare var Worker: Bun.__internal.UseLibDomIfAvailable< } >; -interface WebSocket extends Bun.__internal.LibEmptyOrWSWebSocket {} +/** + * A WebSocket client implementation. + */ +interface WebSocket extends Bun.__internal.LibEmptyOrBunWebSocket {} /** * A WebSocket client implementation - * - * If `DOM` is included in tsconfig `lib`, this falls back to the default DOM global `WebSocket`. - * Otherwise (when outside of a browser environment), this will be the `WebSocket` - * implementation from the `ws` package, which Bun implements. */ -declare var WebSocket: Bun.__internal.UseLibDomIfAvailable<"WebSocket", typeof import("ws").WebSocket>; +declare var WebSocket: Bun.__internal.UseLibDomIfAvailable< + "WebSocket", + { + prototype: WebSocket; + + /** + * Creates a new WebSocket instance with the given URL and options. + * + * @param url The URL to connect to. + * @param options The options to use for the connection. + * + * @example + * ```ts + * const ws = new WebSocket("wss://dev.local", { + * protocols: ["proto1", "proto2"], + * headers: { + * "Cookie": "session=123456", + * }, + * }); + * ``` + */ + new (url: string | URL, options?: Bun.WebSocketOptions): WebSocket; + + /** + * Creates a new WebSocket instance with the given URL and protocols. + * + * @param url The URL to connect to. + * @param protocols The protocols to use for the connection. + * + * @example + * ```ts + * const ws = new WebSocket("wss://dev.local"); + * const ws = new WebSocket("wss://dev.local", ["proto1", "proto2"]); + * ``` + */ + new (url: string | URL, protocols?: string | string[]): WebSocket; + + /** + * The connection is not yet open + */ + readonly CONNECTING: 0; + + /** + * The connection is open and ready to communicate + */ + readonly OPEN: 1; + + /** + * The connection is in the process of closing + */ + readonly CLOSING: 2; + + /** + * The connection is closed or couldn't be opened + */ + readonly CLOSED: 3; + } +>; interface Crypto { readonly subtle: SubtleCrypto; diff --git a/packages/bun-types/package.json b/packages/bun-types/package.json index e95f395985..6804292f4d 100644 --- a/packages/bun-types/package.json +++ b/packages/bun-types/package.json @@ -15,8 +15,7 @@ ], "homepage": "https://bun.sh", "dependencies": { - "@types/node": "*", - "@types/ws": "*" + "@types/node": "*" }, "devDependencies": { "@biomejs/biome": "^1.5.3", diff --git a/test/integration/bun-types/bun-types.test.ts b/test/integration/bun-types/bun-types.test.ts index 6719147e09..7befae33c2 100644 --- a/test/integration/bun-types/bun-types.test.ts +++ b/test/integration/bun-types/bun-types.test.ts @@ -122,7 +122,22 @@ describe("@types/bun integration test", () => { "error TS2339: Property 'write' does not exist on type 'ReadableByteStreamController'.", "websocket.ts", - "error TS2353: Object literal may only specify known properties, and 'headers' does not exist in type 'string[]'.", + `error TS2353: Object literal may only specify known properties, and 'protocols' does not exist in type 'string[]'.`, + `error TS2353: Object literal may only specify known properties, and 'protocol' does not exist in type 'string[]'.`, + `error TS2353: Object literal may only specify known properties, and 'protocol' does not exist in type 'string[]'.`, + `error TS2353: Object literal may only specify known properties, and 'headers' does not exist in type 'string[]'.`, + `error TS2353: Object literal may only specify known properties, and 'protocols' does not exist in type 'string[]'.`, + `error TS2554: Expected 2 arguments, but got 0.`, + `error TS2551: Property 'URL' does not exist on type 'WebSocket'. Did you mean 'url'?`, + `error TS2339: Property 'ping' does not exist on type 'WebSocket'.`, + `error TS2339: Property 'ping' does not exist on type 'WebSocket'.`, + `error TS2339: Property 'ping' does not exist on type 'WebSocket'.`, + `error TS2339: Property 'ping' does not exist on type 'WebSocket'.`, + `error TS2339: Property 'pong' does not exist on type 'WebSocket'.`, + `error TS2339: Property 'pong' does not exist on type 'WebSocket'.`, + `error TS2339: Property 'pong' does not exist on type 'WebSocket'.`, + `error TS2339: Property 'pong' does not exist on type 'WebSocket'.`, + `error TS2339: Property 'terminate' does not exist on type 'WebSocket'.`, "worker.ts", "error TS2339: Property 'ref' does not exist on type 'Worker'.", diff --git a/test/integration/bun-types/fixture/websocket.ts b/test/integration/bun-types/fixture/websocket.ts index a1b11aeb09..0c90c8e578 100644 --- a/test/integration/bun-types/fixture/websocket.ts +++ b/test/integration/bun-types/fixture/websocket.ts @@ -1,15 +1,271 @@ -export class TestWebSocketClient { - #ws: WebSocket; +import { expectType } from "./utilities"; - constructor() { - this.#ws = new WebSocket("wss://dev.local", { - headers: { - cookie: "test=test", - }, - }); - } +// WebSocket constructor tests +{ + // Constructor with string URL only + new WebSocket("wss://dev.local"); - close() { - if (this.#ws != null) this.#ws.close(); - } + // Constructor with string URL and protocols array + new WebSocket("wss://dev.local", ["proto1", "proto2"]); + + // Constructor with string URL and single protocol string + new WebSocket("wss://dev.local", "proto1"); + + // Constructor with URL object only + new WebSocket(new URL("wss://dev.local")); + + // Constructor with URL object and protocols array + new WebSocket(new URL("wss://dev.local"), ["proto1", "proto2"]); + + // Constructor with URL object and single protocol string + new WebSocket(new URL("wss://dev.local"), "proto1"); + + // Constructor with string URL and options object with protocols + new WebSocket("wss://dev.local", { + protocols: ["proto1", "proto2"], + }); + + // Constructor with string URL and options object with protocol + new WebSocket("wss://dev.local", { + protocol: "proto1", + }); + + // Constructor with URL object and options with TLS settings + new WebSocket(new URL("wss://dev.local"), { + protocol: "proto1", + tls: { + rejectUnauthorized: false, + }, + }); + + // Constructor with headers + new WebSocket("wss://dev.local", { + headers: { + "Cookie": "session=123456", + "User-Agent": "BunWebSocketTest", + }, + }); + + // Constructor with full options object + new WebSocket("wss://dev.local", { + protocols: ["proto1", "proto2"], + headers: { + "Cookie": "session=123456", + }, + tls: { + rejectUnauthorized: true, + }, + }); +} + +// Assignability test +{ + function toAny(value: T): any { + return value; + } + + const AnySocket = toAny(WebSocket); + + const ws: WebSocket = new AnySocket("wss://dev.local"); + + ws.close(); + ws.addEventListener("open", e => expectType(e).is()); + ws.addEventListener("message", e => expectType(e).is()); + ws.addEventListener("message", (e: MessageEvent) => expectType(e).is>()); + ws.addEventListener("message", (e: MessageEvent) => expectType(e.data).is()); +} + +// WebSocket static properties test +{ + expectType(WebSocket.CONNECTING).is<0>(); + expectType(WebSocket.OPEN).is<1>(); + expectType(WebSocket.CLOSING).is<2>(); + expectType(WebSocket.CLOSED).is<3>(); + + const instance: WebSocket = null as never; + expectType(instance.CONNECTING).is<0>(); + expectType(instance.OPEN).is<1>(); + expectType(instance.CLOSING).is<2>(); + expectType(instance.CLOSED).is<3>(); +} + +// WebSocket event handlers test +{ + const ws = new WebSocket("wss://dev.local"); + + // Using event handler properties + ws.onopen = (event: Event) => { + expectType(event).is(); + }; + + ws.onmessage = (event: MessageEvent) => { + expectType(event.data).is(); + }; + + ws.onerror = (event: Event) => { + expectType(event).is(); + }; + + ws.onclose = (event: CloseEvent) => { + expectType(event).is(); + expectType(event.code).is(); + expectType(event.reason).is(); + expectType(event.wasClean).is(); + }; + + // Using event handler properties without typing the agument + ws.onopen = event => { + expectType(event).is(); + }; + + ws.onmessage = event => { + expectType(event.data).is(); + + if (typeof event.data === "string") { + expectType(event.data).is(); + } else if (event.data instanceof ArrayBuffer) { + expectType(event.data).is(); + } + }; + + ws.onerror = event => { + expectType(event).is(); + }; + + ws.onclose = event => { + expectType(event).is(); + expectType(event.code).is(); + expectType(event.reason).is(); + expectType(event.wasClean).is(); + }; +} + +// WebSocket addEventListener test +{ + const ws = new WebSocket("wss://dev.local"); + + // Event handler functions + const handleOpen = (event: Event) => { + expectType(event).is(); + }; + + const handleMessage = (event: MessageEvent) => { + expectType(event.data).is(); + }; + + const handleError = (event: Event) => { + expectType(event).is(); + }; + + const handleClose = (event: CloseEvent) => { + expectType(event).is(); + expectType(event.code).is(); + expectType(event.reason).is(); + expectType(event.wasClean).is(); + }; + + // Add event listeners + ws.addEventListener("open", handleOpen); + ws.addEventListener("message", handleMessage); + ws.addEventListener("error", handleError); + ws.addEventListener("close", handleClose); + + // Remove event listeners + ws.removeEventListener("open", handleOpen); + ws.removeEventListener("message", handleMessage); + ws.removeEventListener("error", handleError); + ws.removeEventListener("close", handleClose); +} + +// WebSocket property access test +{ + const ws = new WebSocket("wss://dev.local"); + + // Read various properties + expectType(ws.readyState).is<0 | 2 | 1 | 3>(); + expectType(ws.bufferedAmount).is(); + expectType(ws.url).is(); + expectType(ws.protocol).is(); + expectType(ws.extensions).is(); + + // Legacy URL property (deprecated but exists) + expectType(ws.URL).is(); + + // Set binary type + ws.binaryType = "arraybuffer"; + ws.binaryType = "blob"; +} + +// WebSocket send method test +{ + const ws = new WebSocket("wss://dev.local"); + + // Send string data + ws.send("Hello, server!"); + + // Send ArrayBuffer + const buffer = new ArrayBuffer(10); + ws.send(buffer); + + // Send ArrayBufferView (Uint8Array) + const uint8Array = new Uint8Array(buffer); + ws.send(uint8Array); + + // --------------------------------------- // + // `.send(blob)` is not supported yet + // --------------------------------------- // + // // Send Blob + // const blob = new Blob(["Hello, server!"]); + // ws.send(blob); + // --------------------------------------- // +} + +// WebSocket close method test +{ + const ws = new WebSocket("wss://dev.local"); + + // Close without parameters + ws.close(); + + // Close with code + ws.close(1000); + + // Close with code and reason + ws.close(1001, "Going away"); +} + +// Bun-specific WebSocket extensions test +{ + const ws = new WebSocket("wss://dev.local"); + + // Send ping frame with no data + ws.ping(); + + // Send ping frame with string data + ws.ping("ping data"); + + // Send ping frame with ArrayBuffer + const pingBuffer = new ArrayBuffer(4); + ws.ping(pingBuffer); + + // Send ping frame with ArrayBufferView + const pingView = new Uint8Array(pingBuffer); + ws.ping(pingView); + + // Send pong frame with no data + ws.pong(); + + // Send pong frame with string data + ws.pong("pong data"); + + // Send pong frame with ArrayBuffer + const pongBuffer = new ArrayBuffer(4); + ws.pong(pongBuffer); + + // Send pong frame with ArrayBufferView + const pongView = new Uint8Array(pongBuffer); + ws.pong(pongView); + + // Terminate the connection immediately + ws.terminate(); } diff --git a/test/integration/bun-types/fixture/ws.ts b/test/integration/bun-types/fixture/ws.ts deleted file mode 100644 index fdc4072af2..0000000000 --- a/test/integration/bun-types/fixture/ws.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { WebSocket, WebSocketServer } from "ws"; - -const ws = new WebSocket("ws://www.host.com/path"); - -ws.send("asdf"); - -const wss = new WebSocketServer({ - port: 8080, - perMessageDeflate: false, -}); -wss;