Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 3 additions & 8 deletions examples/chat/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,14 +110,9 @@ again by itself, and it sends one only when its HTML has changed. The thread
is cut along the line that matters: the finished messages are one piece, the
reply being written is another. A token re-sends the reply and nothing else.

Two things to know when writing markup the server re-sends:

- **Test locals, not properties, in a condition.** `{message.usage && …}`
renders once and then never updates; `const { usage } = message` and
`{usage && …}` works. The compiler wraps a condition it cannot see through
in a memo, and a read the server re-runs cannot create one.
- **One source per render, read at the top.** A watcher created per message,
inside the list, never settles: each retry of the render makes a new one.
One thing to know when writing markup the server re-sends: **one source per
render, read at the top.** A watcher created per message, inside the list,
never settles: each retry of the render makes a new one.

## What the browser owns

Expand Down
18 changes: 14 additions & 4 deletions examples/chat/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,14 @@ function encoder(req, res) {
// Static assets compress once at a higher quality — they're cached, not streamed.
const staticBrotli = new Map();

function webRequest(req) {
function webRequest(req, signal) {
const url = new URL(req.url || "/", `http://${req.headers.host || `localhost:${PORT}`}`);
const method = req.method || "GET";
const body = method === "GET" || method === "HEAD" ? undefined : Readable.toWeb(req);
return new Request(url, {
method,
headers: req.headers,
signal,
body,
...(body ? { duplex: "half" } : {})
});
Expand Down Expand Up @@ -87,8 +88,11 @@ createServer(async (req, res) => {
}
}

// A tab that closes must end its render, so the thread's watcher stops.
const gone = new AbortController();
res.on("close", () => gone.abort());
try {
const response = await handleRequest(webRequest(req));
const response = await handleRequest(webRequest(req, gone.signal));
const cookies = response.headers.getSetCookie?.();
response.headers.forEach((value, key) => {
if (key !== "set-cookie") res.setHeader(key, value);
Expand All @@ -97,13 +101,19 @@ createServer(async (req, res) => {
res.statusCode = response.status;
const out = encoder(req, res) ?? res;
if (response.body) {
for await (const chunk of response.body) {
out.write(chunk);
const reader = response.body.getReader();
gone.signal.addEventListener("abort", () => reader.cancel().catch(() => {}), { once: true });
for (;;) {
const { done, value } = await reader.read();
if (done) break;
out.write(value);
if (out !== res) out.flush();
}
}
out.end();
} catch (e) {
// The tab went away mid-response: nothing to report.
if (gone.signal.aborted) return res.destroy();
console.error(e);
res.statusCode = 500;
res.end(e.message);
Expand Down
16 changes: 6 additions & 10 deletions examples/chat/src/routes/thread.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -145,12 +145,8 @@ function messagesMarkup(messages: Message[], writing: boolean, block: { onCopy:
// One message as markup. A plain function, not a component: the server
// re-runs the read that called it and sends the result, and there is nothing
// here to keep between runs.
//
// Every condition below tests a local, not a property. The compiler wraps a
// condition it cannot see through (`message.usage && …`) in a memo, and a
// read the server re-runs cannot create one.
function messageMarkup(message: Message, block: { onCopy: CopyHandler }) {
const { id, text, reasoning, error, usage } = message;
const { id, text } = message;
if (message.role === "user") {
return (
<li class="message user" data-id={id}>
Expand All @@ -167,10 +163,10 @@ function messageMarkup(message: Message, block: { onCopy: CopyHandler }) {
const again = !generating && (failed ? "Retry" : "Regenerate");
return (
<li class={`message assistant ${message.status}`} data-id={id}>
{reasoning && (
{message.reasoning && (
<details class="reasoning">
<summary>Reasoning</summary>
<p>{reasoning}</p>
<p>{message.reasoning}</p>
</details>
)}
<div class="md">
Expand All @@ -197,10 +193,10 @@ function messageMarkup(message: Message, block: { onCopy: CopyHandler }) {
</form>
)}
{stopped && <span class="muted">Stopped</span>}
{failed && <span class="error">{error}</span>}
{usage && (
{failed && <span class="error">{message.error}</span>}
{message.usage && (
<span class="muted">
{usage.tokens} tokens · {usage.seconds}s
{message.usage.tokens} tokens · {message.usage.seconds}s
</span>
)}
{again && (
Expand Down
2 changes: 2 additions & 0 deletions examples/server-dbmon/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ createServer(async (req, res) => {
}
out.end();
} catch (e) {
// The tab went away mid-response: nothing to report.
if (gone.signal.aborted) return res.destroy();
console.error(e);
res.statusCode = 500;
res.end(e.message);
Expand Down
Loading