Skip to content

Commit 46da501

Browse files
committed
feat: add toStream
1 parent 3fefe57 commit 46da501

File tree

5 files changed

+65
-31
lines changed

5 files changed

+65
-31
lines changed

.changeset/yummy-yaks-attend.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"ovr": minor
3+
---
4+
5+
feat: Add `toStream` helper function to create a `ReadableStream` from a `JSX.Element`.

apps/docs/src/server/docs/02-components.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ function Component() {
123123

124124
## Running components
125125

126-
To evaluate components (for example, if you aren't using `App` or need to call them separately), you can use the `toGenerator` and `toString` functions.
126+
To evaluate components (for example, if you aren't using `App` or need to call them separately), you can use these functions.
127127

128128
### toGenerator
129129

@@ -141,6 +141,22 @@ for await (const chunk of gen) {
141141
}
142142
```
143143

144+
### toStream
145+
146+
Turn a `JSX.Element` into a `ReadableStream<Uint8Array>`, this pipes the result of `toGenerator` into a `ReadableStream`.
147+
148+
```tsx
149+
import { toStream } from "ovr";
150+
151+
const Component = () => <p>element</p>;
152+
153+
const stream = toStream(Component);
154+
155+
const response = new Response(stream, {
156+
"Content-Type": "text/html; charset=utf-8",
157+
});
158+
```
159+
144160
### toString
145161

146162
Convert any `JSX.Element` into a `string` of HTML with `toString`. This runs `toGenerator` joins the results into a single string.

packages/ovr/src/app/context.ts

Lines changed: 5 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Chunk } from "../jsx/chunk/index.js";
2-
import { type JSX, toGenerator } from "../jsx/index.js";
2+
import { type JSX, toStream } from "../jsx/index.js";
33
import type { Params, Route } from "../trie/index.js";
44
import { hash } from "../util/hash.js";
55
import {
@@ -80,9 +80,6 @@ export class Context<P extends Params = Params> {
8080
/** Check if the `Response` has been built already */
8181
#finalized = false;
8282

83-
/** Used across requests */
84-
static readonly #encoder = new TextEncoder();
85-
8683
static readonly #headClose = "</head>";
8784
static readonly #bodyClose = "</body>";
8885

@@ -242,7 +239,7 @@ export class Context<P extends Params = Params> {
242239
Page = this.#layouts[i]!({ children: Page });
243240
}
244241

245-
let gen: AsyncGenerator<Chunk, void>;
242+
let stream: ReadableStream<Uint8Array>;
246243

247244
if (this.base) {
248245
// inject into base
@@ -265,37 +262,17 @@ export class Context<P extends Params = Params> {
265262
elements[3] = bodyParts[0];
266263
elements.push(Page, Context.#bodyClose + bodyParts[1]);
267264

268-
gen = toGenerator(
265+
stream = toStream(
269266
elements.map((el) =>
270267
typeof el === "string" ? new Chunk(el, true) : el,
271268
),
272269
);
273270
} else {
274271
// HTML partial - just use the layouts + page
275-
gen = toGenerator(Page);
272+
stream = toStream(Page);
276273
}
277274

278-
this.html(
279-
new ReadableStream<Uint8Array>({
280-
async pull(c) {
281-
const result = await gen.next();
282-
283-
if (result.done) {
284-
c.close();
285-
gen.return();
286-
return;
287-
}
288-
289-
// need to encode for Node JS (ex: during prerendering)
290-
c.enqueue(Context.#encoder.encode(result.value.value));
291-
},
292-
293-
cancel() {
294-
gen.return();
295-
},
296-
}),
297-
status,
298-
);
275+
this.html(stream, status);
299276
}
300277

301278
/**

packages/ovr/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export { jsx, toGenerator, toString, type JSX } from "./jsx/index.js";
1+
export { jsx, toGenerator, toStream, toString, type JSX } from "./jsx/index.js";
22
export { Chunk } from "./jsx/chunk/index.js";
33
export { Trie, Route } from "./trie/index.js";
44
export { App, type Middleware } from "./app/index.js";

packages/ovr/src/jsx/index.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ export async function* Fragment(props: { children?: JSX.Element } = {}) {
109109

110110
/**
111111
* @param element
112-
* @yields Chunks of HTML as the `Element` resolves.
112+
* @yields `Chunk`s of HTML as the `Element` resolves.
113113
*/
114114
export async function* toGenerator(
115115
element: JSX.Element,
@@ -233,3 +233,39 @@ export async function* toGenerator(
233233
*/
234234
export const toString = async (element: JSX.Element) =>
235235
(await Array.fromAsync(toGenerator(element))).join("");
236+
237+
/** Single encoder to use across requests. */
238+
const encoder = new TextEncoder();
239+
240+
/**
241+
* `toGenerator` piped into a `ReadableStream`.
242+
* Use `toGenerator` when possible to avoid the overhead of the stream.
243+
*
244+
* @param element
245+
* @returns `ReadableStream` of HTML
246+
*/
247+
export const toStream = (element: JSX.Element) => {
248+
const gen = toGenerator(element);
249+
250+
return new ReadableStream<Uint8Array>({
251+
async pull(c) {
252+
const result = await gen.next();
253+
254+
if (result.done) {
255+
c.close();
256+
gen.return();
257+
return;
258+
}
259+
260+
c.enqueue(
261+
// need to encode for Node JS (ex: during prerendering)
262+
// faster than piping through a `TextEncoderStream`
263+
encoder.encode(result.value.value),
264+
);
265+
},
266+
267+
cancel() {
268+
gen.return();
269+
},
270+
});
271+
};

0 commit comments

Comments
 (0)