Skip to content

Commit 12ac77b

Browse files
committed
1 parent 8abea36 commit 12ac77b

File tree

3 files changed

+164
-0
lines changed

3 files changed

+164
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { expect, test } from "@playwright/test";
2+
3+
const SADE_SMOOTH_OPERATOR_LYRIC = `Diamond life, lover boy
4+
He move in space with minimum waste and maximum joy
5+
City lights and business nights
6+
When you require streetcar desire for higher heights
7+
No place for beginners or sensitive hearts
8+
When sentiment is left to chance
9+
No place to be ending but somewhere to start
10+
No need to ask, he's a smooth operator
11+
Smooth operator, smooth operator
12+
Smooth operator`;
13+
14+
test("streaming should work in api route", async ({ page }) => {
15+
await page.goto("/sse");
16+
17+
// wait for first line to be present
18+
await page.getByTestId("line").first().waitFor();
19+
const initialLines = await page.getByTestId("line").count();
20+
// fail if all lines appear at once
21+
// this is a safeguard to ensure that the response is streamed and not buffered all at once
22+
expect(initialLines).toBe(1);
23+
24+
const seenLines: Array<{ line: string; time: number }> = [];
25+
const startTime = Date.now();
26+
27+
// we loop until we see all lines
28+
while (seenLines.length < SADE_SMOOTH_OPERATOR_LYRIC.split("\n").length) {
29+
const lines = await page.getByTestId("line").all();
30+
if (lines.length > seenLines.length) {
31+
expect(lines.length).toBe(seenLines.length + 1);
32+
const newLine = lines[lines.length - 1];
33+
seenLines.push({
34+
line: await newLine.innerText(),
35+
time: Date.now() - startTime,
36+
});
37+
}
38+
// wait for a bit before checking again
39+
await page.waitForTimeout(200);
40+
}
41+
42+
expect(seenLines.map((n) => n.line)).toEqual(SADE_SMOOTH_OPERATOR_LYRIC.split("\n"));
43+
for (let i = 1; i < seenLines.length; i++) {
44+
expect(seenLines[i].time - seenLines[i - 1].time).toBeGreaterThan(500);
45+
}
46+
47+
await expect(page.getByTestId("video")).toBeVisible();
48+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import type { NextApiRequest, NextApiResponse } from "next";
2+
3+
const SADE_SMOOTH_OPERATOR_LYRIC = `Diamond life, lover boy
4+
He move in space with minimum waste and maximum joy
5+
City lights and business nights
6+
When you require streetcar desire for higher heights
7+
No place for beginners or sensitive hearts
8+
When sentiment is left to chance
9+
No place to be ending but somewhere to start
10+
No need to ask, he's a smooth operator
11+
Smooth operator, smooth operator
12+
Smooth operator`;
13+
14+
function sleep(ms: number) {
15+
return new Promise((resolve) => {
16+
setTimeout(resolve, ms);
17+
});
18+
}
19+
20+
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
21+
if (req.method !== "GET") {
22+
return res.status(405).json({ message: "Method not allowed" });
23+
}
24+
25+
res.setHeader("Content-Type", "text/event-stream");
26+
res.setHeader("Connection", "keep-alive");
27+
res.setHeader("Cache-Control", "no-cache, no-transform");
28+
res.setHeader("Transfer-Encoding", "chunked");
29+
30+
res.write(`data: ${JSON.stringify({ type: "start", model: "ai-lyric-model" })}\n\n`);
31+
await sleep(1000);
32+
33+
const lines = SADE_SMOOTH_OPERATOR_LYRIC.split("\n");
34+
for (const line of lines) {
35+
res.write(`data: ${JSON.stringify({ type: "content", body: line })}\n\n`);
36+
await sleep(1000);
37+
}
38+
39+
res.write(`data: ${JSON.stringify({ type: "complete" })}\n\n`);
40+
41+
res.end();
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"use client";
2+
3+
import { useEffect, useState } from "react";
4+
5+
type Event = {
6+
type: "start" | "content" | "complete";
7+
model?: string;
8+
body?: string;
9+
};
10+
11+
export default function SSE() {
12+
const [events, setEvents] = useState<Event[]>([]);
13+
const [finished, setFinished] = useState(false);
14+
15+
useEffect(() => {
16+
const e = new EventSource("/api/streaming");
17+
18+
e.onmessage = (msg) => {
19+
console.log(msg);
20+
try {
21+
const data = JSON.parse(msg.data) as Event;
22+
if (data.type === "complete") {
23+
e.close();
24+
setFinished(true);
25+
}
26+
if (data.type === "content") {
27+
setEvents((prev) => prev.concat(data));
28+
}
29+
} catch (err) {
30+
console.error(err, msg);
31+
}
32+
};
33+
}, []);
34+
35+
return (
36+
<div
37+
style={{
38+
padding: "20px",
39+
marginBottom: "20px",
40+
display: "flex",
41+
flexDirection: "column",
42+
gap: "40px",
43+
}}
44+
>
45+
<h1
46+
style={{
47+
fontSize: "2rem",
48+
marginBottom: "20px",
49+
}}
50+
>
51+
Sade - Smooth Operator
52+
</h1>
53+
<div>
54+
{events.map((e, i) => (
55+
<p data-testid="line" key={i}>
56+
{e.body}
57+
</p>
58+
))}
59+
</div>
60+
{finished && (
61+
<iframe
62+
data-testid="video"
63+
width="560"
64+
height="315"
65+
src="https://www.youtube.com/embed/4TYv2PhG89A?si=e1fmpiXZZ1PBKPE5"
66+
title="YouTube video player"
67+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
68+
referrerPolicy="strict-origin-when-cross-origin"
69+
allowFullScreen
70+
></iframe>
71+
)}
72+
</div>
73+
);
74+
}

0 commit comments

Comments
 (0)