Skip to content
This repository has been archived by the owner on Jan 28, 2025. It is now read-only.

feat(lambda-at-edge): use new aws s3 client for faster require time #583

Merged
merged 7 commits into from
Sep 4, 2020
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
5 changes: 4 additions & 1 deletion packages/libs/lambda-at-edge/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@
"homepage": "https://github.com/danielcondemarin/serverless-next.js#readme",
"devDependencies": {
"@rollup/plugin-commonjs": "^15.0.0",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^9.0.0",
"@rollup/plugin-typescript": "^5.0.2",
"@types/aws-lambda": "^8.10.57",
"@types/cookie": "^0.4.0",
"@types/execa": "^2.0.0",
Expand All @@ -44,14 +44,17 @@
"path-to-regexp": "^6.1.0",
"rollup": "^2.26.6",
"rollup-plugin-node-externals": "^2.2.0",
"rollup-plugin-typescript2": "^0.27.2",
"ts-loader": "^7.0.5",
"typescript": "^3.9.6"
},
"dependencies": {
"@aws-sdk/client-s3": "1.0.0-gamma.8",
"@zeit/node-file-trace": "^0.6.5",
"cookie": "^0.4.1",
"execa": "^4.0.2",
"fs-extra": "^9.0.1",
"get-stream": "^6.0.0",
"jsonwebtoken": "^8.5.1"
}
}
7 changes: 5 additions & 2 deletions packages/libs/lambda-at-edge/rollup.config.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import commonjs from "@rollup/plugin-commonjs";
import typescript from "@rollup/plugin-typescript";
import typescript from "rollup-plugin-typescript2";
import { nodeResolve } from "@rollup/plugin-node-resolve";
import externals from "rollup-plugin-node-externals";
import json from "@rollup/plugin-json";

const LOCAL_EXTERNALS = [
"./manifest.json",
Expand All @@ -17,6 +18,7 @@ const generateConfig = (filename) => ({
format: "cjs"
},
plugins: [
json(),
commonjs(),
externals({
exclude: "@sls-next/next-aws-cloudfront"
Expand All @@ -26,7 +28,8 @@ const generateConfig = (filename) => ({
tsconfig: "tsconfig.bundle.json"
})
],
external: [...NPM_EXTERNALS, ...LOCAL_EXTERNALS]
external: [...NPM_EXTERNALS, ...LOCAL_EXTERNALS],
inlineDynamicImports: true
});

export default ["default-handler", "api-handler"].map(generateConfig);
26 changes: 19 additions & 7 deletions packages/libs/lambda-at-edge/src/default-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ import {
OriginResponseEvent,
PerfLogger
} from "../types";
import S3 from "aws-sdk/clients/s3";
import { performance } from "perf_hooks";
import { ServerResponse } from "http";
import jsonwebtoken from "jsonwebtoken";
import type { Readable } from "stream";

const NEXT_PREVIEW_DATA_COOKIE = "__next_preview_data";
const NEXT_PRERENDER_BYPASS_COOKIE = "__prerender_bypass";
Expand Down Expand Up @@ -372,8 +372,10 @@ const handleOriginResponse = async ({
const uri = normaliseUri(request.uri);
const { domainName, region } = request.origin!.s3!;
const bucketName = domainName.replace(`.s3.${region}.amazonaws.com`, "");
// It's usually better to do this outside the handler, but we need to know the bucket region
const s3 = new S3({ region: request.origin?.s3?.region });

// Lazily import only S3Client to reduce init times until actually needed
const { S3Client } = await import("@aws-sdk/client-s3/S3Client");
const s3 = new S3Client({ region: request.origin?.s3?.region });
let pagePath;
if (
isDataRequest(uri) &&
Expand Down Expand Up @@ -406,9 +408,12 @@ const handleOriginResponse = async ({
ContentType: "text/html",
CacheControl: "public, max-age=0, s-maxage=2678400, must-revalidate"
};
const { PutObjectCommand } = await import(
"@aws-sdk/client-s3/commands/PutObjectCommand"
);
await Promise.all([
s3.putObject(s3JsonParams).promise(),
s3.putObject(s3HtmlParams).promise()
s3.send(new PutObjectCommand(s3JsonParams)),
s3.send(new PutObjectCommand(s3HtmlParams))
]);
}
res.writeHead(200, response.headers as any);
Expand All @@ -422,7 +427,14 @@ const handleOriginResponse = async ({
Bucket: bucketName,
Key: `static-pages${hasFallback.fallback}`
};
const { Body } = await s3.getObject(s3Params).promise();
const { GetObjectCommand } = await import(
"@aws-sdk/client-s3/commands/GetObjectCommand"
);
const { Body } = await s3.send(new GetObjectCommand(s3Params));

// Body is stream per: https://github.com/aws/aws-sdk-js-v3/issues/1096
const getStream = await import("get-stream");
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am using get-stream package: https://bundlephobia.com/[email protected] which is small

const bodyString = await getStream.default(Body as Readable);
return {
status: "200",
statusDescription: "OK",
Expand All @@ -435,7 +447,7 @@ const handleOriginResponse = async ({
}
]
},
body: Body?.toString("utf-8")
body: bodyString
};
}
};
Expand Down
24 changes: 0 additions & 24 deletions packages/libs/lambda-at-edge/tests/aws-sdk-s3.mock.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,19 @@ import {
CloudFrontHeaders,
CloudFrontResponse
} from "aws-lambda";
import S3 from "aws-sdk/clients/s3";
import { S3Client } from "@aws-sdk/client-s3/S3Client";

jest.mock("aws-sdk/clients/s3", () => require("../aws-sdk-s3.mock"));
jest.mock("@aws-sdk/client-s3/S3Client", () =>
require("../mocks/s3/aws-sdk-s3-client.mock")
);

jest.mock("@aws-sdk/client-s3/commands/GetObjectCommand", () =>
require("../mocks/s3/aws-sdk-s3-client-get-object-command.mock")
);

jest.mock("@aws-sdk/client-s3/commands/PutObjectCommand", () =>
require("../mocks/s3/aws-sdk-s3-client-put-object-command.mock")
);

jest.mock(
"../../src/manifest.json",
Expand Down Expand Up @@ -44,11 +54,9 @@ const mockPageRequire = (mockPagePath: string): void => {
};

describe("Lambda@Edge origin response", () => {
let s3Client: S3;
let s3Client: S3Client;
beforeEach(() => {
s3Client = new S3();
(s3Client.getObject as jest.Mock).mockClear();
(s3Client.putObject as jest.Mock).mockClear();
s3Client = new S3Client({});
});
describe("Fallback pages", () => {
it("serves fallback page from S3", async () => {
Expand All @@ -64,26 +72,25 @@ describe("Lambda@Edge origin response", () => {
const result = await handler(event);
const response = result as CloudFrontResponse;

expect(s3Client.getObject).toHaveBeenCalledWith(
expect.objectContaining({
Key: "static-pages/tests/prerender-manifest-fallback/[fallback].html"
})
);

expect(response).toEqual(
expect.objectContaining({
status: "200",
statusDescription: "OK",
headers: {
"content-type": [
{
key: "Content-Type",
value: "text/html"
}
]
}
})
);
expect(s3Client.send).toHaveBeenCalledWith({
Command: "GetObjectCommand",
Bucket: "my-bucket.s3.amazonaws.com",
Key: "static-pages/tests/prerender-manifest-fallback/[fallback].html"
});

expect(response).toEqual({
status: "200",
statusDescription: "OK",
headers: {
"content-type": [
{
key: "Content-Type",
value: "text/html"
}
]
},
body: "S3Body"
});
});
it("renders and uploads HTML and JSON for fallback SSG data requests", async () => {
const event = createCloudFrontEvent({
Expand Down Expand Up @@ -113,25 +120,23 @@ describe("Lambda@Edge origin response", () => {
});
expect(cfResponse.status).toEqual(200);

expect(s3Client.putObject).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
Key: "_next/data/build-id/fallback/not-yet-built.json",
Body: JSON.stringify({
page: "pages/fallback/[slug].js"
}),
ContentType: "application/json"
})
);
expect(s3Client.putObject).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
Key: "static-pages/fallback/not-yet-built.html",
Body: "<div>Rendered Page</div>",
ContentType: "text/html",
CacheControl: "public, max-age=0, s-maxage=2678400, must-revalidate"
})
);
expect(s3Client.send).toHaveBeenNthCalledWith(1, {
Command: "PutObjectCommand",
Bucket: "my-bucket.s3.amazonaws.com",
Key: "_next/data/build-id/fallback/not-yet-built.json",
Body: JSON.stringify({
page: "pages/fallback/[slug].js"
}),
ContentType: "application/json"
});
expect(s3Client.send).toHaveBeenNthCalledWith(2, {
Command: "PutObjectCommand",
Bucket: "my-bucket.s3.amazonaws.com",
Key: "static-pages/fallback/not-yet-built.html",
Body: "<div>Rendered Page</div>",
ContentType: "text/html",
CacheControl: "public, max-age=0, s-maxage=2678400, must-revalidate"
});
});
});

Expand Down Expand Up @@ -163,7 +168,7 @@ describe("Lambda@Edge origin response", () => {
page: "pages/customers/[customer].js"
});
expect(cfResponse.status).toEqual(200);
expect(s3Client.putObject).not.toHaveBeenCalled();
expect(s3Client.send).not.toHaveBeenCalled();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// This mock makes it easier to unit test by returning params with the command name
const MockGetObjectCommand = jest.fn((params: object) => {
return {
...{
Command: "GetObjectCommand"
},
...params
};
});

export { MockGetObjectCommand as GetObjectCommand };
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// This mock makes it easier to unit test by returning params with the command name
const MockPutObjectCommand = jest.fn((params: object) => {
return {
...{
Command: "PutObjectCommand"
},
...params
};
});

export { MockPutObjectCommand as PutObjectCommand };
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Readable } from "stream";

export const mockSend = jest.fn((input) => {
if (input.Command === "GetObjectCommand") {
return {
Body: Readable.from(["S3Body"])
};
} else {
return {};
}
});

const MockS3Client = jest.fn(() => ({
constructor: () => {},
send: mockSend
}));

export { MockS3Client as S3Client };
3 changes: 2 additions & 1 deletion packages/libs/lambda-at-edge/tsconfig.bundle.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"noImplicitAny": true,
"sourceMap": false,
"strict": true,
"allowJs": true
"allowJs": true,
"resolveJsonModule": true
},
"include": ["./src/default-handler.ts", "./src/api-handler.ts"]
}
Loading