diff options
-rw-r--r-- | .prettierrc.cjs | 2 | ||||
-rw-r--r-- | docs/api/file-io.md | 57 | ||||
-rw-r--r-- | docs/api/websockets.md | 113 | ||||
-rw-r--r-- | docs/cli/test.md | 2 | ||||
-rw-r--r-- | docs/nav.ts | 3 | ||||
-rw-r--r-- | docs/test/dom.md | 75 |
6 files changed, 203 insertions, 49 deletions
diff --git a/.prettierrc.cjs b/.prettierrc.cjs index e330988a4..44f2bd933 100644 --- a/.prettierrc.cjs +++ b/.prettierrc.cjs @@ -6,7 +6,7 @@ module.exports = { quoteProps: "preserve", overrides: [ { - files: "README.md", + files: ["*.md"], options: { printWidth: 80, }, diff --git a/docs/api/file-io.md b/docs/api/file-io.md index 07336d071..effc57580 100644 --- a/docs/api/file-io.md +++ b/docs/api/file-io.md @@ -202,6 +202,53 @@ const response = await fetch("https://bun.sh"); await Bun.write("index.html", response); ``` +## Incremental writing with `FileSink` + +Bun provides a native incremental file writing API called `FileSink`. To retrieve a `FileSink` instance from a `BunFile`: + +```ts +const file = Bun.file("output.txt"); +const writer = file.writer(); +``` + +To incrementally write to the file, call `.write()`. + +```ts +const file = Bun.file("output.txt"); +const writer = file.writer(); + +writer.write("it was the best of times\n"); +writer.write("it was the worst of times\n"); +``` + +These chunks will be buffered internally. To flush the buffer to disk, use `.flush()`. This returns the number of flushed bytes. + +```ts +writer.flush(); // write buffer to disk +``` + +The buffer will also auto-flush when the `FileSink`'s _high water mark_ is reached; that is, when its internal buffer is full. This value can be configured. + +```ts +const file = Bun.file("output.txt"); +const writer = file.writer({ highWaterMark: 1024 * 1024 }); // 1MB +``` + +To flush the buffer and close the file: + +```ts +writer.end(); +``` + +Note that, by default, the `bun` process will stay alive until this `FileSink` is explicitly closed with `.end()`. To opt out of this behavior, you can "unref" the instance. + +```ts +writer.unref(); + +// to "re-ref" it later +writer.ref(); +``` + ## Benchmarks The following is a 3-line implementation of the Linux `cat` command. @@ -250,5 +297,15 @@ interface BunFile { stream(): Promise<ReadableStream>; arrayBuffer(): Promise<ArrayBuffer>; json(): Promise<any>; + writer(params: { highWaterMark?: number }): FileSink; +} + +export interface FileSink { + write(chunk: string | ArrayBufferView | ArrayBuffer | SharedArrayBuffer): number; + flush(): number | Promise<number>; + end(error?: Error): number | Promise<number>; + start(options?: { highWaterMark?: number }): void; + ref(): void; + unref(): void; } ``` diff --git a/docs/api/websockets.md b/docs/api/websockets.md index d1e1d3831..b8b7f7a8e 100644 --- a/docs/api/websockets.md +++ b/docs/api/websockets.md @@ -12,41 +12,7 @@ Internally Bun's WebSocket implementation is built on [uWebSockets](https://github.com/uNetworking/uWebSockets). {% /callout %} -## Connect to a WebSocket server - -To connect to an external socket server, create an instance of `WebSocket` with the constructor. - -```ts -const socket = new WebSocket("ws://localhost:3000"); -``` - -Bun supports setting custom headers. This is a Bun-specific extension of the `WebSocket` standard. _This will not work in browsers._ - -```ts -const socket = new WebSocket("ws://localhost:3000", { - headers: { - // custom headers - }, -}); -``` - -To add event listeners to the socket: - -```ts -// message is received -socket.addEventListener("message", event => {}); - -// socket opened -socket.addEventListener("open", event => {}); - -// socket closed -socket.addEventListener("close", event => {}); - -// error handler -socket.addEventListener("error", event => {}); -``` - -## Create a WebSocket server +## Start a WebSocket server Below is a simple WebSocket server built with `Bun.serve`, in which all incoming requests are [upgraded](https://developer.mozilla.org/en-US/docs/Web/HTTP/Protocol_upgrade_mechanism) to WebSocket connections in the `fetch` handler. The socket handlers are declared in the `websocket` parameter. @@ -109,7 +75,7 @@ Bun.serve({ }); ``` -## Sending messages +### Sending messages Each `ServerWebSocket` instance has a `.send()` method for sending messages to the client. It supports a range of input types. @@ -119,7 +85,7 @@ ws.send(response.arrayBuffer()); // ArrayBuffer ws.send(new Uint8Array([1, 2, 3])); // TypedArray | DataView ``` -## Headers +### Headers Once the upgrade succeeds, Bun will send a `101 Switching Protocols` response per the [spec](https://developer.mozilla.org/en-US/docs/Web/HTTP/Protocol_upgrade_mechanism). Additional `headers` can be attched to this `Response` in the call to `server.upgrade()`. @@ -137,7 +103,7 @@ Bun.serve({ }); ``` -## Contextual data +### Contextual data Contextual `data` can be attached to a new WebSocket in the `.upgrade()` call. This data is made available on the `ws.data` property inside the WebSocket handlers. @@ -145,14 +111,16 @@ Contextual `data` can be attached to a new WebSocket in the `.upgrade()` call. T type WebSocketData = { createdAt: number; channelId: string; + authToken: string; }; // TypeScript: specify the type of `data` Bun.serve<WebSocketData>({ fetch(req, server) { + // use a library to parse cookies const cookies = parseCookies(req.headers.get("Cookie")); server.upgrade(req, { - // TS: this object must conform to WebSocketData + // this object must conform to WebSocketData data: { createdAt: Date.now(), channelId: new URL(req.url).searchParams.get("channelId"), @@ -165,10 +133,12 @@ Bun.serve<WebSocketData>({ websocket: { // handler called when a message is received async message(ws, message) { - ws.data; // WebSocketData + const user = getUserFromToken(ws.data.authToken); + await saveMessageToDatabase({ channel: ws.data.channelId, message: String(message), + userId: user.id, }); }, }, @@ -185,9 +155,11 @@ socket.addEventListener("message", event => { }) ``` -The cookies that are currently set on the page will be sent with the WebSocket upgrade request and available on `req.headers` in the `fetch` handler. Parse these cookies to determine the identity of the connecting user and set the value of `data` accordingly. +{% callout %} +**Identifying users** — The cookies that are currently set on the page will be sent with the WebSocket upgrade request and available on `req.headers` in the `fetch` handler. Parse these cookies to determine the identity of the connecting user and set the value of `data` accordingly. +{% /callout %} -## Pub/Sub +### Pub/Sub 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. This topic-based broadcast API is similar to [MQTT](https://en.wikipedia.org/wiki/MQTT) and [Redis Pub/Sub](https://redis.io/topics/pubsub). @@ -199,7 +171,9 @@ const server = Bun.serve<{ username: string }>({ console.log(`upgrade!`); const username = getUsernameFromReq(req); const success = server.upgrade(req, { data: { username } }); - return success ? undefined : new Response("WebSocket upgrade error", { status: 400 }); + return success + ? undefined + : new Response("WebSocket upgrade error", { status: 400 }); } return new Response("Hello world"); @@ -226,9 +200,9 @@ const server = Bun.serve<{ username: string }>({ console.log(`Listening on ${server.hostname}:${server.port}`); ``` -Calling `.publish(data)` will send the message to all subscribers of a topic (excluding the socket that called `.publish()`). +Calling `.publish(data)` will send the message to all subscribers of a topic _except_ the socket that called `.publish()`. -## Compression +### Compression Per-message [compression](https://websockets.readthedocs.io/en/stable/topics/compression.html) can be enabled with the `perMessageDeflate` parameter. @@ -250,7 +224,7 @@ ws.send("Hello world", true); For fine-grained control over compression characteristics, refer to the [Reference](#reference). -## Backpressure +### Backpressure The `.send(message)` method of `ServerWebSocket` returns a `number` indicating the result of the operation. @@ -260,6 +234,42 @@ The `.send(message)` method of `ServerWebSocket` returns a `number` indicating t This gives you better control over backpressure in your server. +## Connect to a `Websocket` server + +To connect to an external socket server, either from a browser or from Bun, create an instance of `WebSocket` with the constructor. + +```ts +const socket = new WebSocket("ws://localhost:3000"); +``` + +In browsers, the cookies that are currently set on the page will be sent with the WebSocket upgrade request. This is a standard feature of the `WebSocket` API. + +For convenience, Bun lets you setting custom headers directly in the constructor. This is a Bun-specific extension of the `WebSocket` standard. _This will not work in browsers._ + +```ts +const socket = new WebSocket("ws://localhost:3000", { + headers: { + // custom headers + }, +}); +``` + +To add event listeners to the socket: + +```ts +// message is received +socket.addEventListener("message", event => {}); + +// socket opened +socket.addEventListener("open", event => {}); + +// socket closed +socket.addEventListener("close", event => {}); + +// error handler +socket.addEventListener("error", event => {}); +``` + ## Reference ```ts @@ -267,7 +277,10 @@ namespace Bun { export function serve(params: { fetch: (req: Request, server: Server) => Response | Promise<Response>; websocket?: { - message: (ws: ServerWebSocket, message: string | ArrayBuffer | Uint8Array) => void; + message: ( + ws: ServerWebSocket, + message: string | ArrayBuffer | Uint8Array, + ) => void; open?: (ws: ServerWebSocket) => void; close?: (ws: ServerWebSocket) => void; error?: (ws: ServerWebSocket, error: Error) => void; @@ -297,7 +310,11 @@ type Compressor = interface Server { pendingWebsockets: number; - publish(topic: string, data: string | ArrayBufferView | ArrayBuffer, compress?: boolean): number; + publish( + topic: string, + data: string | ArrayBufferView | ArrayBuffer, + compress?: boolean, + ): number; upgrade( req: Request, options?: { diff --git a/docs/cli/test.md b/docs/cli/test.md index 74a241ef2..d19a45a12 100644 --- a/docs/cli/test.md +++ b/docs/cli/test.md @@ -77,6 +77,8 @@ Bun is compatible with popular UI testing libraries: - [DOM Testing Library](https://testing-library.com/docs/dom-testing-library/intro/) - [React Testing Library](https://testing-library.com/docs/react-testing-library/intro) +See [Test > DOM Testing](/docs/test/dom) for complete documentation. + ## Performance Bun's test runner is fast. diff --git a/docs/nav.ts b/docs/nav.ts index 94e58230a..12333e6da 100644 --- a/docs/nav.ts +++ b/docs/nav.ts @@ -190,6 +190,9 @@ export default { page("test/snapshots", "Snapshots", { description: "Add lifecycle hooks to your tests that run before/after each test or test run", }), + page("test/dom", "DOM testing", { + description: "Write headless tests for UI and React/Vue/Svelte/Lit components with happy-dom", + }), page("test/hot", "Watch mode", { description: "Reload your tests automatically on change.", }), diff --git a/docs/test/dom.md b/docs/test/dom.md new file mode 100644 index 000000000..3eb897745 --- /dev/null +++ b/docs/test/dom.md @@ -0,0 +1,75 @@ +Bun's test runner plays well with existing component and DOM testing libraries, including React Testing Library and [`happy-dom`](https://github.com/capricorn86/happy-dom). + +## `happy-dom` + +For writing headless tests for your frontend code and components, we recommend [`happy-dom`](https://github.com/capricorn86/happy-dom). Happy DOM implements a complete set of HTML and DOM APIs in plain JavaScript, making it possible to simulate a browser environment with high fidelity. + +To get started install the `@happy-dom/global-registrator` package as a dev dependency. + +```bash +$ bun add -d @happy-dom/global-registrator +``` + +We'll be using Bun's _preload_ functionality to register the `happy-dom` globals before running our tests. This step will make browser APIs like `document` available in the global scope. Create a file called `happydom.ts` in the root of your project and add the following code: + +```ts +import { GlobalRegistrator } from "@happy-dom/global-registrator"; + +GlobalRegistrator.register(); +``` + +To preload this file before `bun test`, open or create a `bunfig.toml` file and add the following lines. + +```toml +[test] +preload = "./happydom.ts" +``` + +This will execute `happydom.ts` when you run `bun test`. Now you can write tests that use browser APIs like `document` and `window`. + +```ts#dom.test.ts +import {test, expect} from 'bun:test'; + +test('dom test', () => { + document.body.innerHTML = `<button>My button</button>`; + const button = document.querySelector('button'); + expect(button?.innerText).toEqual('My button'); +}); +``` + +Depending on your `tsconfig.json` setup, you may see a `"Cannot find name 'document'"` type error in the code above. To "inject" the types for `document` and other browser APIs, add the following [triple-slash directive](https://www.typescriptlang.org/docs/handbook/triple-slash-directives.html) to the top of any test file. + +```ts-diff#dom.test.ts ++ /// <reference lib="dom" /> + + import {test, expect} from 'bun:test'; + + test('dom test', () => { + document.body.innerHTML = `<button>My button</button>`; + const button = document.querySelector('button'); + expect(button?.innerText).toEqual('My button'); + }); +``` + +Let's run this test with `bun test`: + +```bash +$ bun test +bun test v0.x.y + +dom.test.ts: +✓ dom test [0.82ms] + + 1 pass + 0 fail + 1 expect() calls +Ran 1 tests across 1 files. 1 total [125.00ms] +``` + +<!-- ## React Testing Library + +Once you've set up `happy-dom` as described above, you can use it with React Testing Library. To get started, install the `@testing-library/react` package as a dev dependency. + +```bash +$ bun add -d @testing-library/react +``` --> |