Skip to content

Commit

Permalink
[node-bridge] Support streaming response for Serverless Function (ver…
Browse files Browse the repository at this point in the history
…cel#8795)

Adds streaming response support for React Server Components with Next 13.
  • Loading branch information
TooTallNate authored Nov 2, 2022
1 parent 11d0091 commit 301bcf5
Show file tree
Hide file tree
Showing 17 changed files with 588 additions and 183 deletions.
4 changes: 4 additions & 0 deletions packages/build-utils/src/lambda.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export interface LambdaOptionsBase {
regions?: string[];
supportsMultiPayloads?: boolean;
supportsWrapper?: boolean;
experimentalResponseStreaming?: boolean;
}

export interface LambdaOptionsWithFiles extends LambdaOptionsBase {
Expand Down Expand Up @@ -60,6 +61,7 @@ export class Lambda {
zipBuffer?: Buffer;
supportsMultiPayloads?: boolean;
supportsWrapper?: boolean;
experimentalResponseStreaming?: boolean;

constructor(opts: LambdaOptions) {
const {
Expand All @@ -72,6 +74,7 @@ export class Lambda {
regions,
supportsMultiPayloads,
supportsWrapper,
experimentalResponseStreaming,
} = opts;
if ('files' in opts) {
assert(typeof opts.files === 'object', '"files" must be an object');
Expand Down Expand Up @@ -132,6 +135,7 @@ export class Lambda {
this.zipBuffer = 'zipBuffer' in opts ? opts.zipBuffer : undefined;
this.supportsMultiPayloads = supportsMultiPayloads;
this.supportsWrapper = supportsWrapper;
this.experimentalResponseStreaming = experimentalResponseStreaming;
}

async createZip(): Promise<Buffer> {
Expand Down
44 changes: 42 additions & 2 deletions packages/next/src/server-build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,7 @@ export async function serverBuild({

const apiPages: string[] = [];
const nonApiPages: string[] = [];
const streamingPages: string[] = [];

lambdaPageKeys.forEach(page => {
if (
Expand All @@ -359,6 +360,8 @@ export async function serverBuild({

if (pageMatchesApi(page)) {
apiPages.push(page);
} else if (appDir && lambdaAppPaths[page]) {
streamingPages.push(page);
} else {
nonApiPages.push(page);
}
Expand Down Expand Up @@ -546,7 +549,12 @@ export async function serverBuild({
const compressedPages: {
[page: string]: PseudoFile;
} = {};
const mergedPageKeys = [...nonApiPages, ...apiPages, ...internalPages];
const mergedPageKeys = [
...nonApiPages,
...streamingPages,
...apiPages,
...internalPages,
];
const traceCache = {};

const getOriginalPagePath = (page: string) => {
Expand Down Expand Up @@ -704,6 +712,27 @@ export async function serverBuild({
pageExtensions,
});

const streamingPageLambdaGroups = await getPageLambdaGroups({
entryPath: requiredServerFilesManifest.appDir || entryPath,
config,
pages: streamingPages,
prerenderRoutes,
pageTraces,
compressedPages,
tracedPseudoLayer: tracedPseudoLayer.pseudoLayer,
initialPseudoLayer,
lambdaCompressedByteLimit,
initialPseudoLayerUncompressed: uncompressedInitialSize,
internalPages,
pageExtensions,
});

for (const group of streamingPageLambdaGroups) {
if (!group.isPrerenders) {
group.isStreaming = true;
}
}

const apiLambdaGroups = await getPageLambdaGroups({
entryPath: requiredServerFilesManifest.appDir || entryPath,
config,
Expand Down Expand Up @@ -733,13 +762,23 @@ export async function serverBuild({
pseudoLayerBytes: group.pseudoLayerBytes,
uncompressedLayerBytes: group.pseudoLayerUncompressedBytes,
})),
streamingPageLambdaGroups: streamingPageLambdaGroups.map(group => ({
pages: group.pages,
isPrerender: group.isPrerenders,
pseudoLayerBytes: group.pseudoLayerBytes,
uncompressedLayerBytes: group.pseudoLayerUncompressedBytes,
})),
nextServerLayerSize: initialPseudoLayer.pseudoLayerBytes,
},
null,
2
)
);
const combinedGroups = [...pageLambdaGroups, ...apiLambdaGroups];
const combinedGroups = [
...pageLambdaGroups,
...streamingPageLambdaGroups,
...apiLambdaGroups,
];

await detectLambdaLimitExceeding(
combinedGroups,
Expand Down Expand Up @@ -832,6 +871,7 @@ export async function serverBuild({
memory: group.memory,
runtime: nodeVersion.runtime,
maxDuration: group.maxDuration,
isStreaming: group.isStreaming,
});

for (const page of group.pages) {
Expand Down
8 changes: 8 additions & 0 deletions packages/next/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -748,6 +748,7 @@ export async function createPseudoLayer(files: {

interface CreateLambdaFromPseudoLayersOptions extends LambdaOptionsWithFiles {
layers: PseudoLayer[];
isStreaming?: boolean;
}

// measured with 1, 2, 5, 10, and `os.cpus().length || 5`
Expand All @@ -757,6 +758,7 @@ const createLambdaSema = new Sema(1);
export async function createLambdaFromPseudoLayers({
files: baseFiles,
layers,
isStreaming,
...lambdaOptions
}: CreateLambdaFromPseudoLayersOptions) {
await createLambdaSema.acquire();
Expand Down Expand Up @@ -791,6 +793,11 @@ export async function createLambdaFromPseudoLayers({

return new NodejsLambda({
...lambdaOptions,
...(isStreaming
? {
experimentalResponseStreaming: true,
}
: {}),
files,
shouldAddHelpers: false,
shouldAddSourcemapSupport: false,
Expand Down Expand Up @@ -1273,6 +1280,7 @@ export type LambdaGroup = {
pages: string[];
memory?: number;
maxDuration?: number;
isStreaming?: boolean;
isPrerenders?: boolean;
pseudoLayer: PseudoLayer;
pseudoLayerBytes: number;
Expand Down

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

10 changes: 0 additions & 10 deletions packages/next/test/fixtures/00-app-dir/app/dashboard/index/page.js

This file was deleted.

15 changes: 0 additions & 15 deletions packages/next/test/fixtures/00-app-dir/app/dashboard/index/test.js

This file was deleted.

119 changes: 61 additions & 58 deletions packages/next/test/integration/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,77 +7,80 @@ const runBuildLambda = require('../../../../test/lib/run-build-lambda');

jest.setTimeout(360000);

it('should build with app-dir correctly', async () => {
const { buildResult } = await runBuildLambda(
path.join(__dirname, '../fixtures/00-app-dir')
);
// experimental appDir currently requires Node.js >= 16
if (parseInt(process.versions.node.split('.')[0], 10) >= 16) {
it('should build with app-dir correctly', async () => {
const { buildResult } = await runBuildLambda(
path.join(__dirname, '../fixtures/00-app-dir')
);

const lambdas = new Set();
const lambdas = new Set();

for (const key of Object.keys(buildResult.output)) {
if (buildResult.output[key].type === 'Lambda') {
lambdas.add(buildResult.output[key]);
for (const key of Object.keys(buildResult.output)) {
if (buildResult.output[key].type === 'Lambda') {
lambdas.add(buildResult.output[key]);
}
}
}

expect(lambdas.size).toBe(2);
expect(buildResult.output['dashboard']).toBeDefined();
expect(buildResult.output['dashboard/another']).toBeDefined();
expect(buildResult.output['dashboard/changelog']).toBeDefined();
expect(buildResult.output['dashboard/deployments/[id]']).toBeDefined();
expect(lambdas.size).toBe(2);
expect(buildResult.output['dashboard']).toBeDefined();
expect(buildResult.output['dashboard/another']).toBeDefined();
expect(buildResult.output['dashboard/changelog']).toBeDefined();
expect(buildResult.output['dashboard/deployments/[id]']).toBeDefined();

// prefixed static generation output with `/app` under dist server files
expect(buildResult.output['dashboard'].type).toBe('Prerender');
expect(buildResult.output['dashboard'].fallback.fsPath).toMatch(
/server\/app\/dashboard\.html$/
);
expect(buildResult.output['dashboard.rsc'].type).toBe('Prerender');
expect(buildResult.output['dashboard.rsc'].fallback.fsPath).toMatch(
/server\/app\/dashboard\.rsc$/
);
expect(buildResult.output['dashboard/index/index'].type).toBe('Prerender');
expect(buildResult.output['dashboard/index/index'].fallback.fsPath).toMatch(
/server\/app\/dashboard\/index\.html$/
);
expect(buildResult.output['dashboard/index.rsc'].type).toBe('Prerender');
expect(buildResult.output['dashboard/index.rsc'].fallback.fsPath).toMatch(
/server\/app\/dashboard\/index\.rsc$/
);
});
// prefixed static generation output with `/app` under dist server files
expect(buildResult.output['dashboard'].type).toBe('Prerender');
expect(buildResult.output['dashboard'].fallback.fsPath).toMatch(
/server\/app\/dashboard\.html$/
);
expect(buildResult.output['dashboard.rsc'].type).toBe('Prerender');
expect(buildResult.output['dashboard.rsc'].fallback.fsPath).toMatch(
/server\/app\/dashboard\.rsc$/
);
expect(buildResult.output['dashboard/index/index'].type).toBe('Prerender');
expect(buildResult.output['dashboard/index/index'].fallback.fsPath).toMatch(
/server\/app\/dashboard\/index\.html$/
);
expect(buildResult.output['dashboard/index.rsc'].type).toBe('Prerender');
expect(buildResult.output['dashboard/index.rsc'].fallback.fsPath).toMatch(
/server\/app\/dashboard\/index\.rsc$/
);
});

it('should build with app-dir in edge runtime correctly', async () => {
const { buildResult } = await runBuildLambda(
path.join(__dirname, '../fixtures/00-app-dir-edge')
);
it('should build with app-dir in edge runtime correctly', async () => {
const { buildResult } = await runBuildLambda(
path.join(__dirname, '../fixtures/00-app-dir-edge')
);

const edgeFunctions = new Set();
const edgeFunctions = new Set();

for (const key of Object.keys(buildResult.output)) {
if (buildResult.output[key].type === 'EdgeFunction') {
edgeFunctions.add(buildResult.output[key]);
for (const key of Object.keys(buildResult.output)) {
if (buildResult.output[key].type === 'EdgeFunction') {
edgeFunctions.add(buildResult.output[key]);
}
}
}

expect(edgeFunctions.size).toBe(3);
expect(buildResult.output['edge']).toBeDefined();
expect(buildResult.output['index']).toBeDefined();
expect(buildResult.output['index/index']).toBeDefined();
});
expect(edgeFunctions.size).toBe(3);
expect(buildResult.output['edge']).toBeDefined();
expect(buildResult.output['index']).toBeDefined();
expect(buildResult.output['index/index']).toBeDefined();
});

it('should show error from basePath with legacy monorepo build', async () => {
let error;
it('should show error from basePath with legacy monorepo build', async () => {
let error;

try {
await runBuildLambda(path.join(__dirname, 'legacy-monorepo-basepath'));
} catch (err) {
error = err;
}
console.error(error);
try {
await runBuildLambda(path.join(__dirname, 'legacy-monorepo-basepath'));
} catch (err) {
error = err;
}
console.error(error);

expect(error.message).toBe(
'basePath can not be used with `builds` in vercel.json, use Project Settings to configure your monorepo instead'
);
});
expect(error.message).toBe(
'basePath can not be used with `builds` in vercel.json, use Project Settings to configure your monorepo instead'
);
});
}

it('should build using server build', async () => {
const origLog = console.log;
Expand Down
Loading

0 comments on commit 301bcf5

Please sign in to comment.