Skip to content

Commit c358330

Browse files
conico974khuezy
andauthored
feat: support for streaming (#213)
* feat: support for streaming * fix streaming not working after a while * fix page static props and streaming not always starting * fix ISR not working reliably * better handling of errors * fix for node runtime v12 * cleanup * fix headers and middleware response headers being overwritten * add padding to guarantee flush * wip * Add nextTick to flush buffer * change version to experimental streaming * enable brotli compression * wip * fix set-cookie being overwritten by multiple set-cookie * fix end not draining before ending stream * remove extra write in end * return writableNeedDrain; support for warmer * add drain to internal write * override removeHeader to remove set-cookie * revert removal of drain in internal write * fixed OPEN_NEXT_URL * reset version * fix lint * revert fixDataPage * revert fixISRHeaders change * fix 404 on fallback:false * refactor move config to a single location * fix cookies set in middleware --------- Co-authored-by: Khue Nguyen <khuezy.nguyen@gmail.com>
1 parent 2512093 commit c358330

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

68 files changed

+2597
-945
lines changed

examples/app-pages-router/app/isr/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@ async function getTime() {
55
export const revalidate = 10;
66
export default async function ISR() {
77
const time = getTime();
8-
return <div>ISR: {time}</div>;
8+
return <div>Time: {time}</div>;
99
}

examples/app-pages-router/app/layout.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ export default function RootLayout({
1717
}) {
1818
return (
1919
<html lang="en">
20-
<body className={inter.className}>{children}</body>
20+
<body className={inter.className}>
21+
<header>Header</header>
22+
{children}
23+
</body>
2124
</html>
2225
);
2326
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { PropsWithChildren } from "react";
2+
3+
export default function Layout({ children }: PropsWithChildren) {
4+
return (
5+
<div>
6+
<h1>SSR</h1>
7+
{children}
8+
</div>
9+
);
10+
}
Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,23 @@
1-
import { wait } from "@open-next/utils";
1+
import React from "react";
2+
3+
import { headers } from "next/headers";
4+
5+
async function getTime() {
6+
const res = await new Promise<string>((resolve) => {
7+
setTimeout(() => {
8+
resolve(new Date().toISOString());
9+
}, 1500);
10+
});
11+
return res;
12+
}
213

3-
export const revalidate = 0;
414
export default async function SSR() {
5-
await wait(2000);
6-
const time = new Date().toISOString();
15+
const time = await getTime();
16+
const headerList = headers();
717
return (
818
<div>
9-
<h1>SSR {time}</h1>
19+
<h1>Time: {time}</h1>
20+
<div> {headerList.get("host")}</div>
1021
</div>
1122
);
1223
}

examples/app-pages-router/next.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/** @type {import('next').NextConfig} */
22
const nextConfig = {
33
poweredByHeader: false,
4+
cleanDistDir: true,
45
transpilePackages: ["@example/shared"],
56
output: "standalone",
67
outputFileTracing: "../sst",

examples/app-pages-router/pages/pages_isr/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,5 @@ export async function getStaticProps() {
1212
export default function Page({
1313
time,
1414
}: InferGetStaticPropsType<typeof getStaticProps>) {
15-
return <div className="flex">ISR: {time}</div>;
15+
return <div className="flex">Time: {time}</div>;
1616
}

examples/app-pages-router/pages/pages_ssr/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,5 @@ export async function getServerSideProps() {
1111
export default function Page({
1212
time,
1313
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
14-
return <div className="flex">SSR: {time}</div>;
14+
return <div className="flex">Time: {time}</div>;
1515
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { wait } from "@open-next/utils";
2+
import { NextRequest } from "next/server";
3+
4+
export const dynamic = "force-dynamic";
5+
6+
export async function GET(request: NextRequest) {
7+
const resStream = new TransformStream();
8+
const writer = resStream.writable.getWriter();
9+
10+
const res = new Response(resStream.readable, {
11+
headers: {
12+
"Content-Type": "text/event-stream",
13+
Connection: "keep-alive",
14+
"Cache-Control": "no-cache, no-transform",
15+
},
16+
});
17+
18+
setTimeout(async () => {
19+
writer.write(
20+
`data: ${JSON.stringify({
21+
message: "open",
22+
time: new Date().toISOString(),
23+
})}\n\n`,
24+
);
25+
for (let i = 1; i <= 4; i++) {
26+
await wait(2000);
27+
writer.write(
28+
`data: ${JSON.stringify({
29+
message: "hello:" + i,
30+
time: new Date().toISOString(),
31+
})}\n\n`,
32+
);
33+
}
34+
35+
await wait(2000); // Wait for 4 seconds
36+
writer.write(
37+
`data: ${JSON.stringify({
38+
message: "close",
39+
time: new Date().toISOString(),
40+
})}\n\n`,
41+
);
42+
await wait(5000);
43+
await writer.close();
44+
}, 100);
45+
46+
return res;
47+
}

examples/app-router/app/isr/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@ async function getTime() {
55
export const revalidate = 10;
66
export default async function ISR() {
77
const time = getTime();
8-
return <div>ISR: {time}</div>;
8+
return <div>Time: {time}</div>;
99
}

examples/app-router/app/page.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ export default function Home() {
2626
new timestamp
2727
</Nav>
2828
<Nav href={"/ssr"} title="SSR">
29-
Server Side Render should generate a new timestamp on each load
29+
Server Side Render should generate a new timestamp on each load.
30+
Streaming support for loading...
3031
</Nav>
3132
<Nav href={"/api"} title="API">
3233
Calls an API endpoint defined in app/api/hello/route and middleware
@@ -40,6 +41,9 @@ export default function Home() {
4041
<Nav href={"/search-query"} title="Search Query">
4142
Search Query Params should be available in middleware
4243
</Nav>
44+
<Nav href={"/sse"} title="Server Sent Events">
45+
Server Sent Events via Streaming
46+
</Nav>
4347
</main>
4448
</>
4549
);

examples/app-router/app/sse/page.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"use client";
2+
3+
import { useEffect, useState } from "react";
4+
5+
export default function SSE() {
6+
const [events, setEvents] = useState<any[]>([]);
7+
8+
useEffect(() => {
9+
const e = new EventSource("/api/sse");
10+
11+
e.onmessage = (msg) => {
12+
console.log(msg);
13+
try {
14+
const data = JSON.parse(msg.data);
15+
if (data.message === "close") {
16+
e.close();
17+
console.log("closing");
18+
}
19+
setEvents((prev) => prev.concat(data));
20+
} catch (err) {
21+
console.log("failed to parse: ", err, msg);
22+
}
23+
};
24+
}, []);
25+
26+
return (
27+
<>
28+
<h1>Server Sent Event</h1>
29+
{events.map((e, i) => (
30+
<div key={i}>
31+
Message {i}: {JSON.stringify(e)}
32+
</div>
33+
))}
34+
</>
35+
);
36+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { PropsWithChildren } from "react";
2+
3+
export default function Layout({ children }: PropsWithChildren) {
4+
return (
5+
<div>
6+
<h1>SSR</h1>
7+
{children}
8+
</div>
9+
);
10+
}

examples/app-router/app/ssr/page.tsx

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,23 @@
1-
import { wait } from "@open-next/utils";
1+
import React from "react";
2+
3+
import { headers } from "next/headers";
4+
5+
async function getTime() {
6+
const res = await new Promise<string>((resolve) => {
7+
setTimeout(() => {
8+
resolve(new Date().toISOString());
9+
}, 1500);
10+
});
11+
return res;
12+
}
213

3-
export const revalidate = 0;
414
export default async function SSR() {
5-
await wait(2000);
6-
const time = new Date().toISOString();
15+
const time = await getTime();
16+
const headerList = headers();
717
return (
818
<div>
9-
<h1>SSR {time}</h1>
19+
<h1>Time: {time}</h1>
20+
<div> {headerList.get("host")}</div>
1021
</div>
1122
);
1223
}

examples/app-router/middleware.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,19 @@ export function middleware(request: NextRequest) {
2121
}
2222

2323
const requestHeaders = new Headers();
24+
// Setting the Request Headers, this should be available in RSC
2425
requestHeaders.set("request-header", "request-header");
2526
requestHeaders.set(
2627
"search-params",
2728
`mw/${request.nextUrl.searchParams.get("searchParams") || ""}`,
2829
);
2930
const responseHeaders = new Headers();
31+
// Response headers should show up in the client's response headers
3032
responseHeaders.set("response-header", "response-header");
3133

3234
// Set the cache control header with custom swr
3335
// For: isr.test.ts
34-
if (path === "/isr") {
36+
if (path === "/isr" && !request.headers.get("x-prerender-revalidate")) {
3537
responseHeaders.set(
3638
"cache-control",
3739
"max-age=10, stale-while-revalidate=999",
@@ -47,8 +49,12 @@ export function middleware(request: NextRequest) {
4749

4850
// Set cookies in middleware
4951
// For: middleware.cookies.test.ts
50-
r.cookies.set("from", "middleware");
51-
r.cookies.set("with", "love");
52+
r.cookies.set("from", "middleware", {
53+
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365),
54+
});
55+
r.cookies.set("with", "love", {
56+
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365),
57+
});
5258

5359
return r;
5460
}

examples/app-router/next.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/** @type {import('next').NextConfig} */
22
const nextConfig = {
33
poweredByHeader: false,
4+
cleanDistDir: true,
45
transpilePackages: ["@example/shared"],
56
output: "standalone",
67
outputFileTracing: "../sst",

examples/app-router/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"version": "0.1.0",
44
"private": true,
55
"scripts": {
6-
"openbuild": "node ../../packages/open-next/dist/index.js build --build-command \"npx turbo build\"",
6+
"openbuild": "node ../../packages/open-next/dist/index.js build --streaming --build-command \"npx turbo build\"",
77
"dev": "next dev --port 3001",
88
"build": "next build",
99
"start": "next start --port 3001",

examples/pages-router/next.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/** @type {import('next').NextConfig} */
22
const nextConfig = {
33
transpilePackages: ["@example/shared"],
4+
cleanDistDir: true,
45
reactStrictMode: true,
56
output: "standalone",
67
outputFileTracing: "../sst",

examples/pages-router/src/pages/isr/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,5 @@ export async function getStaticProps() {
1212
export default function Page({
1313
time,
1414
}: InferGetStaticPropsType<typeof getStaticProps>) {
15-
return <div className="flex">ISR: {time}</div>;
15+
return <div className="flex">Time: {time}</div>;
1616
}

examples/pages-router/src/pages/ssr/index.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,10 @@ export async function getServerSideProps() {
1111
export default function Page({
1212
time,
1313
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
14-
return <div className="flex">SSR: {time}</div>;
14+
return (
15+
<>
16+
<h1>SSR</h1>
17+
<div className="flex">Time: {time}</div>
18+
</>
19+
);
1520
}

examples/sst/README.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
11
# sst
22

3-
This example contains the AppRouter, PagesRouter, and AppPagesRouter Stacks for the end to end tests to hit.
3+
This example contains the AppRouter, PagesRouter, and AppPagesRouter Stacks for the end to end tests to hit.
4+
5+
## AppRouter
6+
7+
This app uses the app router exclusively and it uses the streaming feature
8+
9+
## PagesRouter
10+
11+
This app uses the pages router exclusively
12+
13+
## AppPagesRouter
14+
15+
This app uses a mix of app and pages router and *DOES NOT* use the streaming feature

examples/sst/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,6 @@
1111
"devDependencies": {
1212
"aws-cdk-lib": "2.91.0",
1313
"constructs": "10.2.69",
14-
"sst": "2.24.20"
14+
"sst": "2.24.24"
1515
}
1616
}

examples/sst/stacks/AppPagesRouter.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { NextjsSite } from "sst/constructs";
22

3+
// NOTE: App Pages Router doesn't do streaming
34
export function AppPagesRouter({ stack }) {
45
const site = new NextjsSite(stack, "apppagesrouter", {
56
path: "../app-pages-router",

examples/sst/stacks/AppRouter.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import { NextjsSite } from "sst/constructs";
1+
import { NextjsSite } from "./NextjsSite";
22

33
export function AppRouter({ stack }) {
44
const site = new NextjsSite(stack, "approuter", {
55
path: "../app-router",
66
buildCommand: "npm run openbuild",
77
bind: [],
88
environment: {},
9+
timeout: "20 seconds",
910
});
1011

1112
stack.addOutputs({

examples/sst/stacks/NextjsSite.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { NextjsSite as SSTNextSite } from "sst/constructs";
2+
// NOTE: Temporary class to add streaming
3+
export class NextjsSite extends SSTNextSite {
4+
protected supportsStreaming(): boolean {
5+
return true;
6+
}
7+
}

packages/open-next/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
"@node-minify/core": "^8.0.6",
4242
"@node-minify/terser": "^8.0.6",
4343
"@tsconfig/node18": "^1.0.1",
44-
"esbuild": "^0.18.18",
44+
"esbuild": "0.19.2",
4545
"@esbuild-plugins/node-resolve": "0.2.2",
4646
"path-to-regexp": "^6.2.1",
4747
"promise.series": "^0.2.0"

packages/open-next/src/adapters/cache.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ import {
1616
// import { getDerivedTags } from "next/dist/server/lib/incremental-cache/utils";
1717
import path from "path";
1818

19+
import { loadBuildId } from "./config/util.js";
1920
import { awsLogger, debug, error } from "./logger.js";
20-
import { loadBuildId } from "./util.js";
2121

2222
// TODO: Remove this, temporary only to run some tests
2323
const getDerivedTags = (tags: string[]) => tags;

0 commit comments

Comments
 (0)