Skip to content

Commit

Permalink
revalidation relies on the expires header
Browse files Browse the repository at this point in the history
  • Loading branch information
kirkness committed Apr 29, 2021
1 parent fc8bd38 commit 8a0d5ff
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 71 deletions.
39 changes: 12 additions & 27 deletions packages/libs/lambda-at-edge/src/default-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import { removeBlacklistedHeaders } from "./headers/removeBlacklistedHeaders";
import { getStaticRegenerationResponse } from "./lib/getStaticRegenerationResponse";
import { s3BucketNameFromEventRequest } from "./s3/s3BucketNameFromEventRequest";
import { triggerStaticRegeneration } from "./lib/triggerStaticRegeneration";
import { s3StorePage } from "./s3/s3StorePage";

const basePath = RoutesManifestJson.basePath;

Expand Down Expand Up @@ -617,6 +618,7 @@ const handleOriginResponse = async ({
const staticRegenerationResponse = getStaticRegenerationResponse({
requestedOriginUri: uri,
expiresHeader: response.headers.expires?.[0]?.value || "",
lastModifiedHeader: response.headers["last-modified"]?.[0]?.value || "",
manifest
});

Expand Down Expand Up @@ -694,33 +696,16 @@ const handleOriginResponse = async ({
"passthrough"
);
if (isSSG) {
const baseKey = uri
.replace(/^\//, "")
.replace(/\.(json|html)$/, "")
.replace(/^_next\/data\/[^\/]*\//, "");
const jsonKey = `_next/data/${manifest.buildId}/${baseKey}.json`;
const htmlKey = `static-pages/${manifest.buildId}/${baseKey}.html`;
const s3JsonParams = {
Bucket: bucketName,
Key: `${s3BasePath}${jsonKey}`,
Body: JSON.stringify(renderOpts.pageData),
ContentType: "application/json",
CacheControl: "public, max-age=0, s-maxage=2678400, must-revalidate"
};
const s3HtmlParams = {
Bucket: bucketName,
Key: `${s3BasePath}${htmlKey}`,
Body: html,
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.send(new PutObjectCommand(s3JsonParams)),
s3.send(new PutObjectCommand(s3HtmlParams))
]);
await s3StorePage({
html,
uri,
basePath,
bucketName: bucketName || "",
buildId: manifest.buildId,
pageData: renderOpts.pageData,
region: request.origin?.s3?.region || "",
revalidate: renderOpts.revalidate
});
}
const outHeaders: OutgoingHttpHeaders = {};
Object.entries(response.headers).map(([name, headers]) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ interface StaticRegenerationResponseOptions {
requestedOriginUri: string;
// Header as set on the origin object
expiresHeader: string;
lastModifiedHeader: string;
manifest: OriginRequestDefaultHandlerManifest;
}

Expand All @@ -14,6 +15,15 @@ interface StaticRegenerationResponseValue {
secondsRemainingUntilRevalidation: number;
}

const firstRegenerateExpiryDate = (
lastModifiedHeader: string,
initialRevalidateSeconds: number
) => {
return new Date(
new Date(lastModifiedHeader).getTime() + initialRevalidateSeconds * 1000
);
};

/**
* Function called within an origin response as part of the Incremental Static
* Regeneration logic. Returns required headers for the response, or false if
Expand All @@ -27,13 +37,26 @@ const getStaticRegenerationResponse = (
options.requestedOriginUri.replace(".html", "")
]?.initialRevalidateSeconds;

// If this page did not write a revalidate value at build time it is not an
// ISR page
if (typeof initialRevalidateSeconds !== "number") {
// ISR pages that were either previously regenerated or generated
// post-initial-build, will have an `Expires` header set. However ISR pages
// that have not been regenerated but at build-time resolved a revalidate
// property will not have an `Expires` header and therefore we check using the
// manifest.
if (
!options.expiresHeader &&
!(
options.lastModifiedHeader && typeof initialRevalidateSeconds === "number"
)
) {
return false;
}

const expiresAt = new Date(options.expiresHeader);
const expiresAt = options.expiresHeader
? new Date(options.expiresHeader)
: firstRegenerateExpiryDate(
options.lastModifiedHeader,
initialRevalidateSeconds as number
);

// isNaN will resolve true on initial load of this page (as the expiresHeader
// won't be set), in which case we trigger a regeneration now
Expand Down
51 changes: 11 additions & 40 deletions packages/libs/lambda-at-edge/src/regeneration-handler.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import lambdaAtEdgeCompat from "@sls-next/next-aws-cloudfront";
import { OriginRequestDefaultHandlerManifest } from "./types";
import { S3Client } from "@aws-sdk/client-s3";
import { buildS3RetryStrategy } from "./s3/s3RetryStrategy";
import { s3StorePage } from "./s3/s3StorePage";

export const handler: AWSLambda.SQSHandler = async (event) => {
await Promise.all(
Expand Down Expand Up @@ -29,12 +28,6 @@ export const handler: AWSLambda.SQSHandler = async (event) => {
manifestString
);

const s3 = new S3Client({
region: cloudFrontEventRequest.origin?.s3?.region,
maxAttempts: 3,
retryStrategy: await buildS3RetryStrategy()
});

const { req, res } = lambdaAtEdgeCompat(
{ request: cloudFrontEventRequest },
{ enableHTTPCompression: manifest.enableHTTPCompression }
Expand All @@ -50,44 +43,22 @@ export const handler: AWSLambda.SQSHandler = async (event) => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const page = require(`./pages${srcPath}`);

const jsonKey = `_next/data/${manifest.buildId}${baseKey}.json`;
const htmlKey = `static-pages/${manifest.buildId}${baseKey}.html`;

const { renderOpts, html } = await page.renderReqToHTML(
req,
res,
"passthrough"
);

const revalidate =
renderOpts.revalidate ?? ssgRoute.initialRevalidateSeconds;
const expires = new Date(Date.now() + revalidate * 1000);
const s3BasePath = basePath ? `${basePath.replace(/^\//, "")}/` : "";
const s3JsonParams = {
Bucket: bucketName,
Key: `${s3BasePath}${jsonKey}`,
Body: JSON.stringify(renderOpts.pageData),
ContentType: "application/json",
Expires: expires,
CacheControl: "public, max-age=0, s-maxage=2678400, must-revalidate"
};

const s3HtmlParams = {
Bucket: bucketName,
Key: `${s3BasePath}${htmlKey}`,
Body: html,
ContentType: "text/html",
Expires: expires,
CacheControl: "public, max-age=0, s-maxage=2678400, must-revalidate"
};

const { PutObjectCommand } = await import(
"@aws-sdk/client-s3/commands/PutObjectCommand"
);
await Promise.all([
s3.send(new PutObjectCommand(s3JsonParams)),
s3.send(new PutObjectCommand(s3HtmlParams))
]);
await s3StorePage({
html,
uri: cloudFrontEventRequest.uri,
basePath,
bucketName: bucketName || "",
buildId: manifest.buildId,
pageData: renderOpts.pageData,
region: cloudFrontEventRequest.origin?.s3?.region || "",
revalidate: renderOpts.revalidate
});
})
);
};
73 changes: 73 additions & 0 deletions packages/libs/lambda-at-edge/src/s3/s3StorePage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { buildS3RetryStrategy } from "./s3RetryStrategy";

interface S3StorePageOptions {
basePath: string | undefined;
uri: string;
revalidate?: number | undefined;
bucketName: string;
html: string;
buildId: string;
region: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
pageData: Record<string, any>;
}

/**
* There are multiple occasions where a static/SSG page will be generated after
* the initial build. This function accepts a generated page, stores it and
* applies the appropriate headers (e.g. setting an 'Expires' header for
* regeneration).
*/
export const s3StorePage = async (
options: S3StorePageOptions
): Promise<void> => {
const { S3Client } = await import("@aws-sdk/client-s3/S3Client");

const s3 = new S3Client({
region: options.region,
maxAttempts: 3,
retryStrategy: await buildS3RetryStrategy()
});

const s3BasePath = options.basePath
? `${options.basePath.replace(/^\//, "")}/`
: "";
const baseKey = options.uri
.replace(/^\//, "")
.replace(/\.(json|html)$/, "")
.replace(/^_next\/data\/[^\/]*\//, "");
const jsonKey = `_next/data/${options.buildId}/${baseKey}.json`;
const htmlKey = `static-pages/${options.buildId}/${baseKey}.html`;
const cacheControl = options.revalidate
? undefined
: "public, max-age=0, s-maxage=2678400, must-revalidate";
const expires = options.revalidate
? new Date(new Date().getTime() + 1000 * options.revalidate)
: undefined;

const s3JsonParams = {
Bucket: options.bucketName,
Key: `${s3BasePath}${jsonKey}`,
Body: JSON.stringify(options.pageData),
ContentType: "application/json",
CacheControl: cacheControl,
Expires: expires
};

const s3HtmlParams = {
Bucket: options.bucketName,
Key: `${s3BasePath}${htmlKey}`,
Body: options.html,
ContentType: "text/html",
CacheControl: cacheControl,
Expires: expires
};

const { PutObjectCommand } = await import(
"@aws-sdk/client-s3/commands/PutObjectCommand"
);
await Promise.all([
s3.send(new PutObjectCommand(s3JsonParams)),
s3.send(new PutObjectCommand(s3HtmlParams))
]);
};

0 comments on commit 8a0d5ff

Please sign in to comment.