@opennextjs/aws 3.3.0 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/cache.d.ts +1 -2
- package/dist/adapters/cache.js +20 -11
- package/dist/adapters/config/index.d.ts +2 -0
- package/dist/adapters/config/index.js +4 -3
- package/dist/adapters/config/util.d.ts +2 -0
- package/dist/adapters/config/util.js +12 -2
- package/dist/adapters/middleware.js +13 -2
- package/dist/adapters/util.js +5 -1
- package/dist/build/createAssets.js +22 -23
- package/dist/build/createImageOptimizationBundle.js +2 -1
- package/dist/build/createServerBundle.js +4 -4
- package/dist/build/edge/createEdgeBundle.d.ts +4 -3
- package/dist/build/edge/createEdgeBundle.js +14 -22
- package/dist/build/validateConfig.js +2 -1
- package/dist/core/createMainHandler.js +2 -1
- package/dist/core/requestHandler.js +24 -6
- package/dist/core/resolve.d.ts +4 -0
- package/dist/core/resolve.js +10 -0
- package/dist/core/routing/middleware.js +3 -1
- package/dist/core/routing/routeMatcher.d.ts +10 -0
- package/dist/core/routing/routeMatcher.js +45 -0
- package/dist/core/routing/util.d.ts +4 -3
- package/dist/core/routing/util.js +24 -5
- package/dist/core/routingHandler.d.ts +3 -0
- package/dist/core/routingHandler.js +30 -29
- package/dist/overrides/cdnInvalidation/cloudfront.d.ts +9 -0
- package/dist/overrides/cdnInvalidation/cloudfront.js +26 -0
- package/dist/overrides/cdnInvalidation/dummy.d.ts +9 -0
- package/dist/overrides/cdnInvalidation/dummy.js +6 -0
- package/dist/overrides/imageLoader/fs-dev.d.ts +10 -0
- package/dist/overrides/imageLoader/fs-dev.js +14 -0
- package/dist/overrides/incrementalCache/fs-dev.d.ts +3 -0
- package/dist/overrides/incrementalCache/fs-dev.js +30 -0
- package/dist/overrides/incrementalCache/multi-tier-ddb-s3.d.ts +10 -0
- package/dist/overrides/incrementalCache/multi-tier-ddb-s3.js +120 -0
- package/dist/overrides/incrementalCache/s3-lite.d.ts +2 -0
- package/dist/overrides/incrementalCache/s3-lite.js +1 -1
- package/dist/overrides/proxyExternalRequest/node.js +6 -5
- package/dist/overrides/queue/direct.d.ts +3 -0
- package/dist/overrides/queue/direct.js +18 -0
- package/dist/overrides/tagCache/fs-dev.d.ts +3 -0
- package/dist/overrides/tagCache/fs-dev.js +32 -0
- package/dist/overrides/wrappers/cloudflare-edge.js +11 -4
- package/dist/overrides/wrappers/cloudflare-node.js +4 -1
- package/dist/overrides/wrappers/dummy.d.ts +1 -1
- package/dist/overrides/wrappers/dummy.js +6 -2
- package/dist/overrides/wrappers/express-dev.d.ts +7 -0
- package/dist/overrides/wrappers/express-dev.js +43 -0
- package/dist/plugins/edge.js +10 -4
- package/dist/plugins/replacement.d.ts +1 -1
- package/dist/plugins/replacement.js +1 -1
- package/dist/plugins/resolve.d.ts +1 -0
- package/dist/plugins/resolve.js +4 -1
- package/dist/types/global.d.ts +7 -1
- package/dist/types/open-next.d.ts +19 -6
- package/dist/types/overrides.d.ts +13 -4
- package/dist/utils/lru.d.ts +8 -0
- package/dist/utils/lru.js +29 -0
- package/dist/utils/regex.d.ts +18 -0
- package/dist/utils/regex.js +16 -0
- package/package.json +4 -1
|
@@ -5,6 +5,7 @@ import { OpenNextNodeResponse } from "../../http/openNextResponse.js";
|
|
|
5
5
|
import { parseHeaders } from "../../http/util.js";
|
|
6
6
|
import { debug, error } from "../../adapters/logger.js";
|
|
7
7
|
import { isBinaryContentType } from "../../utils/binary.js";
|
|
8
|
+
import { localizePath } from "./i18n/index.js";
|
|
8
9
|
/**
|
|
9
10
|
*
|
|
10
11
|
* @__PURE__
|
|
@@ -169,9 +170,9 @@ var CommonHeaders;
|
|
|
169
170
|
*
|
|
170
171
|
* @__PURE__
|
|
171
172
|
*/
|
|
172
|
-
export function fixCacheHeaderForHtmlPages(
|
|
173
|
+
export function fixCacheHeaderForHtmlPages(internalEvent, headers) {
|
|
173
174
|
// We don't want to cache error pages
|
|
174
|
-
if (rawPath === "/404" || rawPath === "/500") {
|
|
175
|
+
if (internalEvent.rawPath === "/404" || internalEvent.rawPath === "/500") {
|
|
175
176
|
if (process.env.OPEN_NEXT_DANGEROUSLY_SET_ERROR_HEADERS === "true") {
|
|
176
177
|
return;
|
|
177
178
|
}
|
|
@@ -179,9 +180,10 @@ export function fixCacheHeaderForHtmlPages(rawPath, headers) {
|
|
|
179
180
|
"private, no-cache, no-store, max-age=0, must-revalidate";
|
|
180
181
|
return;
|
|
181
182
|
}
|
|
183
|
+
const localizedPath = localizePath(internalEvent);
|
|
182
184
|
// WORKAROUND: `NextServer` does not set cache headers for HTML pages
|
|
183
185
|
// https://opennext.js.org/aws/v2/advanced/workaround#workaround-nextserver-does-not-set-cache-headers-for-html-pages
|
|
184
|
-
if (HtmlPages.includes(
|
|
186
|
+
if (HtmlPages.includes(localizedPath)) {
|
|
185
187
|
headers[CommonHeaders.CACHE_CONTROL] =
|
|
186
188
|
"public, max-age=0, s-maxage=31536000, must-revalidate";
|
|
187
189
|
}
|
|
@@ -350,13 +352,30 @@ export function fixISRHeaders(headers) {
|
|
|
350
352
|
* @returns
|
|
351
353
|
* @__PURE__
|
|
352
354
|
*/
|
|
353
|
-
export function createServerResponse(
|
|
355
|
+
export function createServerResponse(routingResult, headers, responseStream) {
|
|
356
|
+
const internalEvent = routingResult.internalEvent;
|
|
354
357
|
return new OpenNextNodeResponse((_headers) => {
|
|
355
|
-
fixCacheHeaderForHtmlPages(internalEvent
|
|
358
|
+
fixCacheHeaderForHtmlPages(internalEvent, _headers);
|
|
356
359
|
fixSWRCacheHeader(_headers);
|
|
357
360
|
addOpenNextHeader(_headers);
|
|
358
361
|
fixISRHeaders(_headers);
|
|
359
362
|
}, async (_headers) => {
|
|
360
363
|
await revalidateIfRequired(internalEvent.headers.host, internalEvent.rawPath, _headers);
|
|
364
|
+
await invalidateCDNOnRequest(routingResult, _headers);
|
|
361
365
|
}, responseStream, headers);
|
|
362
366
|
}
|
|
367
|
+
// This function is used only for `res.revalidate()`
|
|
368
|
+
export async function invalidateCDNOnRequest(params, headers) {
|
|
369
|
+
const { internalEvent, initialPath, resolvedRoutes } = params;
|
|
370
|
+
const isIsrRevalidation = internalEvent.headers["x-isr"] === "1";
|
|
371
|
+
if (!isIsrRevalidation &&
|
|
372
|
+
headers[CommonHeaders.NEXT_CACHE] === "REVALIDATED") {
|
|
373
|
+
await globalThis.cdnInvalidationHandler.invalidatePaths([
|
|
374
|
+
{
|
|
375
|
+
initialPath,
|
|
376
|
+
rawPath: internalEvent.rawPath,
|
|
377
|
+
resolvedRoutes,
|
|
378
|
+
},
|
|
379
|
+
]);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import type { InternalEvent, InternalResult, RoutingResult } from "../types/open-next";
|
|
2
2
|
export declare const MIDDLEWARE_HEADER_PREFIX = "x-middleware-response-";
|
|
3
3
|
export declare const MIDDLEWARE_HEADER_PREFIX_LEN: number;
|
|
4
|
+
export declare const INTERNAL_HEADER_PREFIX = "x-opennext-";
|
|
5
|
+
export declare const INTERNAL_HEADER_INITIAL_PATH = "x-opennext-initial-path";
|
|
6
|
+
export declare const INTERNAL_HEADER_RESOLVED_ROUTES = "x-opennext-resolved-routes";
|
|
4
7
|
export default function routingHandler(event: InternalEvent): Promise<InternalResult | RoutingResult>;
|
|
@@ -3,26 +3,12 @@ import { debug } from "../adapters/logger";
|
|
|
3
3
|
import { cacheInterceptor } from "./routing/cacheInterceptor";
|
|
4
4
|
import { fixDataPage, getNextConfigHeaders, handleFallbackFalse, handleRedirects, handleRewrites, } from "./routing/matcher";
|
|
5
5
|
import { handleMiddleware } from "./routing/middleware";
|
|
6
|
+
import { apiPrefix, dynamicRouteMatcher, staticRouteMatcher, } from "./routing/routeMatcher";
|
|
6
7
|
export const MIDDLEWARE_HEADER_PREFIX = "x-middleware-response-";
|
|
7
8
|
export const MIDDLEWARE_HEADER_PREFIX_LEN = MIDDLEWARE_HEADER_PREFIX.length;
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
: "^/";
|
|
12
|
-
// Add the basepath prefix to the regex so we correctly match the rawPath
|
|
13
|
-
const optionalBasepathPrefixRegex = RoutesManifest.basePath
|
|
14
|
-
? `^${RoutesManifest.basePath}/?`
|
|
15
|
-
: "^/";
|
|
16
|
-
// Add the basePath prefix to the api routes
|
|
17
|
-
const apiPrefix = RoutesManifest.basePath
|
|
18
|
-
? `${RoutesManifest.basePath}/api`
|
|
19
|
-
: "/api";
|
|
20
|
-
const staticRegexp = RoutesManifest.routes.static.map((route) => new RegExp(route.regex
|
|
21
|
-
.replace("^/", optionalLocalePrefixRegex)
|
|
22
|
-
.replace("^/", optionalBasepathPrefixRegex)));
|
|
23
|
-
const dynamicRegexp = RoutesManifest.routes.dynamic.map((route) => new RegExp(route.regex
|
|
24
|
-
.replace("^/", optionalLocalePrefixRegex)
|
|
25
|
-
.replace("^/", optionalBasepathPrefixRegex)));
|
|
9
|
+
export const INTERNAL_HEADER_PREFIX = "x-opennext-";
|
|
10
|
+
export const INTERNAL_HEADER_INITIAL_PATH = `${INTERNAL_HEADER_PREFIX}initial-path`;
|
|
11
|
+
export const INTERNAL_HEADER_RESOLVED_ROUTES = `${INTERNAL_HEADER_PREFIX}resolved-routes`;
|
|
26
12
|
// Geolocation headers starting from Nextjs 15
|
|
27
13
|
// See https://github.com/vercel/vercel/blob/7714b1c/packages/functions/src/headers.ts
|
|
28
14
|
const geoHeaderToNextHeader = {
|
|
@@ -50,6 +36,14 @@ export default async function routingHandler(event) {
|
|
|
50
36
|
event.headers[nextGeoName] = value;
|
|
51
37
|
}
|
|
52
38
|
}
|
|
39
|
+
// First we remove internal headers
|
|
40
|
+
// We don't want to allow users to set these headers
|
|
41
|
+
for (const key of Object.keys(event.headers)) {
|
|
42
|
+
if (key.startsWith(INTERNAL_HEADER_PREFIX) ||
|
|
43
|
+
key.startsWith(MIDDLEWARE_HEADER_PREFIX)) {
|
|
44
|
+
delete event.headers[key];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
53
47
|
const nextHeaders = getNextConfigHeaders(event, ConfigHeaders);
|
|
54
48
|
let internalEvent = fixDataPage(event, BuildId);
|
|
55
49
|
if ("statusCode" in internalEvent) {
|
|
@@ -75,9 +69,9 @@ export default async function routingHandler(event) {
|
|
|
75
69
|
internalEvent = beforeRewrites.internalEvent;
|
|
76
70
|
isExternalRewrite = beforeRewrites.isExternalRewrite;
|
|
77
71
|
}
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
if (!isStaticRoute
|
|
72
|
+
const foundStaticRoute = staticRouteMatcher(internalEvent.rawPath);
|
|
73
|
+
const isStaticRoute = !isExternalRewrite && foundStaticRoute.length > 0;
|
|
74
|
+
if (!(isStaticRoute || isExternalRewrite)) {
|
|
81
75
|
// Second rewrite to be applied
|
|
82
76
|
const afterRewrites = handleRewrites(internalEvent, RoutesManifest.rewrites.afterFiles);
|
|
83
77
|
internalEvent = afterRewrites.internalEvent;
|
|
@@ -86,9 +80,9 @@ export default async function routingHandler(event) {
|
|
|
86
80
|
// We want to run this just before the dynamic route check
|
|
87
81
|
const { event: fallbackEvent, isISR } = handleFallbackFalse(internalEvent, PrerenderManifest);
|
|
88
82
|
internalEvent = fallbackEvent;
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
if (!isDynamicRoute
|
|
83
|
+
const foundDynamicRoute = dynamicRouteMatcher(internalEvent.rawPath);
|
|
84
|
+
const isDynamicRoute = !isExternalRewrite && foundDynamicRoute.length > 0;
|
|
85
|
+
if (!(isDynamicRoute || isStaticRoute || isExternalRewrite)) {
|
|
92
86
|
// Fallback rewrite to be applied
|
|
93
87
|
const fallbackRewrites = handleRewrites(internalEvent, RoutesManifest.rewrites.fallback);
|
|
94
88
|
internalEvent = fallbackRewrites.internalEvent;
|
|
@@ -103,12 +97,12 @@ export default async function routingHandler(event) {
|
|
|
103
97
|
const isRouteFoundBeforeAllRewrites = isStaticRoute || isDynamicRoute || isExternalRewrite;
|
|
104
98
|
// If we still haven't found a route, we show the 404 page
|
|
105
99
|
// We need to ensure that rewrites are applied before showing the 404 page
|
|
106
|
-
if (!isRouteFoundBeforeAllRewrites
|
|
107
|
-
|
|
108
|
-
|
|
100
|
+
if (!(isRouteFoundBeforeAllRewrites ||
|
|
101
|
+
isApiRoute ||
|
|
102
|
+
isNextImageRoute ||
|
|
109
103
|
// We need to check again once all rewrites have been applied
|
|
110
|
-
|
|
111
|
-
|
|
104
|
+
staticRouteMatcher(internalEvent.rawPath).length > 0 ||
|
|
105
|
+
dynamicRouteMatcher(internalEvent.rawPath).length > 0)) {
|
|
112
106
|
internalEvent = {
|
|
113
107
|
...internalEvent,
|
|
114
108
|
rawPath: "/404",
|
|
@@ -136,10 +130,17 @@ export default async function routingHandler(event) {
|
|
|
136
130
|
...middlewareResponseHeaders,
|
|
137
131
|
...nextHeaders,
|
|
138
132
|
});
|
|
133
|
+
const resolvedRoutes = [
|
|
134
|
+
...foundStaticRoute,
|
|
135
|
+
...foundDynamicRoute,
|
|
136
|
+
];
|
|
137
|
+
debug("resolvedRoutes", resolvedRoutes);
|
|
139
138
|
return {
|
|
140
139
|
internalEvent,
|
|
141
140
|
isExternalRewrite,
|
|
142
141
|
origin: false,
|
|
143
142
|
isISR,
|
|
143
|
+
initialPath: event.rawPath,
|
|
144
|
+
resolvedRoutes,
|
|
144
145
|
};
|
|
145
146
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { CloudFrontClient, CreateInvalidationCommand, } from "@aws-sdk/client-cloudfront";
|
|
2
|
+
const cloudfront = new CloudFrontClient({});
|
|
3
|
+
export default {
|
|
4
|
+
name: "cloudfront",
|
|
5
|
+
invalidatePaths: async (paths) => {
|
|
6
|
+
const constructedPaths = paths.flatMap(({ initialPath, resolvedRoutes }) => {
|
|
7
|
+
const isAppRouter = resolvedRoutes.some((route) => route.type === "app");
|
|
8
|
+
// revalidateTag doesn't have any leading slash, remove it just to be sure
|
|
9
|
+
const path = initialPath.replace(/^\//, "");
|
|
10
|
+
return isAppRouter
|
|
11
|
+
? [`/${path}`, `/${path}?_rsc=*`]
|
|
12
|
+
: [
|
|
13
|
+
`/${path}`,
|
|
14
|
+
`/_next/data/${process.env.NEXT_BUILD_ID}${path === "/" ? "/index" : `/${path}`}.json*`,
|
|
15
|
+
];
|
|
16
|
+
});
|
|
17
|
+
await cloudfront.send(new CreateInvalidationCommand({
|
|
18
|
+
DistributionId: process.env.CLOUDFRONT_DISTRIBUTION_ID,
|
|
19
|
+
InvalidationBatch: {
|
|
20
|
+
// Do we need to limit the number of paths? Or batch them into multiple commands?
|
|
21
|
+
Paths: { Quantity: constructedPaths.length, Items: constructedPaths },
|
|
22
|
+
CallerReference: `${Date.now()}`,
|
|
23
|
+
},
|
|
24
|
+
}));
|
|
25
|
+
},
|
|
26
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
export default {
|
|
3
|
+
name: "fs-dev",
|
|
4
|
+
load: async (url) => {
|
|
5
|
+
const basePath = "../../assets";
|
|
6
|
+
const body = fs.createReadStream(`${basePath}/${url}`);
|
|
7
|
+
const contentType = url.endsWith(".png") ? "image/png" : "image/jpeg";
|
|
8
|
+
return {
|
|
9
|
+
body,
|
|
10
|
+
contentType,
|
|
11
|
+
cacheControl: "public, max-age=31536000, immutable",
|
|
12
|
+
};
|
|
13
|
+
},
|
|
14
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const buildId = process.env.NEXT_BUILD_ID;
|
|
4
|
+
const basePath = path.resolve(process.cwd(), `../../cache/${buildId}`);
|
|
5
|
+
const getCacheKey = (key) => {
|
|
6
|
+
return path.join(basePath, `${key}.cache`);
|
|
7
|
+
};
|
|
8
|
+
const cache = {
|
|
9
|
+
name: "fs-dev",
|
|
10
|
+
get: async (key) => {
|
|
11
|
+
const fileData = await fs.readFile(getCacheKey(key), "utf-8");
|
|
12
|
+
const data = JSON.parse(fileData);
|
|
13
|
+
const { mtime } = await fs.stat(getCacheKey(key));
|
|
14
|
+
return {
|
|
15
|
+
value: data,
|
|
16
|
+
lastModified: mtime.getTime(),
|
|
17
|
+
};
|
|
18
|
+
},
|
|
19
|
+
set: async (key, value, isFetch) => {
|
|
20
|
+
const data = JSON.stringify(value);
|
|
21
|
+
const cacheKey = getCacheKey(key);
|
|
22
|
+
// We need to create the directory before writing the file
|
|
23
|
+
await fs.mkdir(path.dirname(cacheKey), { recursive: true });
|
|
24
|
+
await fs.writeFile(cacheKey, data);
|
|
25
|
+
},
|
|
26
|
+
delete: async (key) => {
|
|
27
|
+
await fs.rm(getCacheKey(key));
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
export default cache;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { IncrementalCache } from "../../types/overrides";
|
|
2
|
+
/**
|
|
3
|
+
* This cache implementation uses a multi-tier cache with a local cache, a DynamoDB metadata cache and an S3 cache.
|
|
4
|
+
* It uses the same DynamoDB table as the default tag cache and the same S3 bucket as the default incremental cache.
|
|
5
|
+
* It will first check the local cache.
|
|
6
|
+
* If the local cache is expired, it will check the DynamoDB metadata cache to see if the local cache is still valid.
|
|
7
|
+
* Lastly it will check the S3 cache.
|
|
8
|
+
*/
|
|
9
|
+
declare const multiTierCache: IncrementalCache;
|
|
10
|
+
export default multiTierCache;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { customFetchClient } from "../../utils/fetch";
|
|
2
|
+
import { LRUCache } from "../../utils/lru";
|
|
3
|
+
import { debug } from "../../adapters/logger";
|
|
4
|
+
import S3Cache, { getAwsClient } from "./s3-lite";
|
|
5
|
+
// TTL for the local cache in milliseconds
|
|
6
|
+
const localCacheTTL = process.env.OPEN_NEXT_LOCAL_CACHE_TTL_MS
|
|
7
|
+
? Number.parseInt(process.env.OPEN_NEXT_LOCAL_CACHE_TTL_MS, 10)
|
|
8
|
+
: 0;
|
|
9
|
+
// Maximum size of the local cache in nb of entries
|
|
10
|
+
const maxCacheSize = process.env.OPEN_NEXT_LOCAL_CACHE_SIZE
|
|
11
|
+
? Number.parseInt(process.env.OPEN_NEXT_LOCAL_CACHE_SIZE, 10)
|
|
12
|
+
: 1000;
|
|
13
|
+
const localCache = new LRUCache(maxCacheSize);
|
|
14
|
+
const awsFetch = (body, type = "get") => {
|
|
15
|
+
const { CACHE_BUCKET_REGION } = process.env;
|
|
16
|
+
const client = getAwsClient();
|
|
17
|
+
return customFetchClient(client)(`https://dynamodb.${CACHE_BUCKET_REGION}.amazonaws.com`, {
|
|
18
|
+
method: "POST",
|
|
19
|
+
headers: {
|
|
20
|
+
"Content-Type": "application/x-amz-json-1.0",
|
|
21
|
+
"X-Amz-Target": `DynamoDB_20120810.${type === "get" ? "GetItem" : "PutItem"}`,
|
|
22
|
+
},
|
|
23
|
+
body,
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
const buildDynamoKey = (key) => {
|
|
27
|
+
const { NEXT_BUILD_ID } = process.env;
|
|
28
|
+
return `__meta_${NEXT_BUILD_ID}_${key}`;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* This cache implementation uses a multi-tier cache with a local cache, a DynamoDB metadata cache and an S3 cache.
|
|
32
|
+
* It uses the same DynamoDB table as the default tag cache and the same S3 bucket as the default incremental cache.
|
|
33
|
+
* It will first check the local cache.
|
|
34
|
+
* If the local cache is expired, it will check the DynamoDB metadata cache to see if the local cache is still valid.
|
|
35
|
+
* Lastly it will check the S3 cache.
|
|
36
|
+
*/
|
|
37
|
+
const multiTierCache = {
|
|
38
|
+
name: "multi-tier-ddb-s3",
|
|
39
|
+
async get(key, isFetch) {
|
|
40
|
+
// First we check the local cache
|
|
41
|
+
const localCacheEntry = localCache.get(key);
|
|
42
|
+
if (localCacheEntry) {
|
|
43
|
+
if (Date.now() - localCacheEntry.lastModified < localCacheTTL) {
|
|
44
|
+
debug("Using local cache without checking ddb");
|
|
45
|
+
return localCacheEntry;
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
// Here we'll check ddb metadata to see if the local cache is still valid
|
|
49
|
+
const { CACHE_DYNAMO_TABLE } = process.env;
|
|
50
|
+
const result = await awsFetch(JSON.stringify({
|
|
51
|
+
TableName: CACHE_DYNAMO_TABLE,
|
|
52
|
+
Key: {
|
|
53
|
+
path: { S: buildDynamoKey(key) },
|
|
54
|
+
tag: { S: buildDynamoKey(key) },
|
|
55
|
+
},
|
|
56
|
+
}));
|
|
57
|
+
if (result.status === 200) {
|
|
58
|
+
const data = await result.json();
|
|
59
|
+
const hasBeenDeleted = data.Item?.deleted?.BOOL;
|
|
60
|
+
if (hasBeenDeleted) {
|
|
61
|
+
localCache.delete(key);
|
|
62
|
+
return { value: undefined, lastModified: 0 };
|
|
63
|
+
}
|
|
64
|
+
// If the metadata is older than the local cache, we can use the local cache
|
|
65
|
+
// If it's not found we assume that no write has been done yet and we can use the local cache
|
|
66
|
+
const lastModified = data.Item?.revalidatedAt?.N
|
|
67
|
+
? Number.parseInt(data.Item.revalidatedAt.N, 10)
|
|
68
|
+
: 0;
|
|
69
|
+
if (lastModified <= localCacheEntry.lastModified) {
|
|
70
|
+
debug("Using local cache after checking ddb");
|
|
71
|
+
return localCacheEntry;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch (e) {
|
|
76
|
+
debug("Failed to get metadata from ddb", e);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const result = await S3Cache.get(key, isFetch);
|
|
80
|
+
if (result.value) {
|
|
81
|
+
localCache.set(key, {
|
|
82
|
+
value: result.value,
|
|
83
|
+
lastModified: result.lastModified ?? Date.now(),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return result;
|
|
87
|
+
},
|
|
88
|
+
// Both for set and delete we choose to do the write to S3 first and then to DynamoDB
|
|
89
|
+
// Which means that if it fails in DynamoDB, instance that don't have local cache will work as expected.
|
|
90
|
+
// But instance that have local cache will have a stale cache until the next working set or delete.
|
|
91
|
+
async set(key, value, isFetch) {
|
|
92
|
+
const revalidatedAt = Date.now();
|
|
93
|
+
await S3Cache.set(key, value, isFetch);
|
|
94
|
+
await awsFetch(JSON.stringify({
|
|
95
|
+
TableName: process.env.CACHE_DYNAMO_TABLE,
|
|
96
|
+
Item: {
|
|
97
|
+
tag: { S: buildDynamoKey(key) },
|
|
98
|
+
path: { S: buildDynamoKey(key) },
|
|
99
|
+
revalidatedAt: { N: String(revalidatedAt) },
|
|
100
|
+
},
|
|
101
|
+
}), "set");
|
|
102
|
+
localCache.set(key, {
|
|
103
|
+
value,
|
|
104
|
+
lastModified: revalidatedAt,
|
|
105
|
+
});
|
|
106
|
+
},
|
|
107
|
+
async delete(key) {
|
|
108
|
+
await S3Cache.delete(key);
|
|
109
|
+
await awsFetch(JSON.stringify({
|
|
110
|
+
TableName: process.env.CACHE_DYNAMO_TABLE,
|
|
111
|
+
Item: {
|
|
112
|
+
tag: { S: buildDynamoKey(key) },
|
|
113
|
+
path: { S: buildDynamoKey(key) },
|
|
114
|
+
deleted: { BOOL: true },
|
|
115
|
+
},
|
|
116
|
+
}), "set");
|
|
117
|
+
localCache.delete(key);
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
export default multiTierCache;
|
|
@@ -5,7 +5,7 @@ import { IgnorableError, RecoverableError } from "../../utils/error";
|
|
|
5
5
|
import { customFetchClient } from "../../utils/fetch";
|
|
6
6
|
import { parseNumberFromEnv } from "../../adapters/util";
|
|
7
7
|
let awsClient = null;
|
|
8
|
-
const getAwsClient = () => {
|
|
8
|
+
export const getAwsClient = () => {
|
|
9
9
|
const { CACHE_BUCKET_REGION } = process.env;
|
|
10
10
|
if (awsClient) {
|
|
11
11
|
return awsClient;
|
|
@@ -36,16 +36,17 @@ const nodeProxy = {
|
|
|
36
36
|
method,
|
|
37
37
|
rejectUnauthorized: false,
|
|
38
38
|
}, (_res) => {
|
|
39
|
-
const
|
|
39
|
+
const resHeaders = _res.headers;
|
|
40
|
+
const nodeReadableStream = resHeaders["content-encoding"] === "br"
|
|
40
41
|
? _res.pipe(require("node:zlib").createBrotliDecompress())
|
|
41
|
-
:
|
|
42
|
+
: resHeaders["content-encoding"] === "gzip"
|
|
42
43
|
? _res.pipe(require("node:zlib").createGunzip())
|
|
43
44
|
: _res;
|
|
44
|
-
const isBase64Encoded = isBinaryContentType(
|
|
45
|
-
!!
|
|
45
|
+
const isBase64Encoded = isBinaryContentType(resHeaders["content-type"]) ||
|
|
46
|
+
!!resHeaders["content-encoding"];
|
|
46
47
|
const result = {
|
|
47
48
|
type: "core",
|
|
48
|
-
headers: filterHeadersForProxy(
|
|
49
|
+
headers: filterHeadersForProxy(resHeaders),
|
|
49
50
|
statusCode: _res.statusCode ?? 200,
|
|
50
51
|
// TODO: check base64 encoding
|
|
51
52
|
isBase64Encoded,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const queue = {
|
|
2
|
+
name: "dev-queue",
|
|
3
|
+
send: async (message) => {
|
|
4
|
+
const prerenderManifest = (await import("../../adapters/config/index.js"))
|
|
5
|
+
.PrerenderManifest;
|
|
6
|
+
const { host, url } = message.MessageBody;
|
|
7
|
+
const protocol = host.includes("localhost") ? "http" : "https";
|
|
8
|
+
const revalidateId = prerenderManifest.preview.previewModeId;
|
|
9
|
+
await globalThis.internalFetch(`${protocol}://${host}${url}`, {
|
|
10
|
+
method: "HEAD",
|
|
11
|
+
headers: {
|
|
12
|
+
"x-prerender-revalidate": revalidateId,
|
|
13
|
+
"x-isr": "1",
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
export default queue;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
const tagFile = "../../dynamodb-provider/dynamodb-cache.json";
|
|
3
|
+
const tagContent = fs.readFileSync(tagFile, "utf-8");
|
|
4
|
+
let tags = JSON.parse(tagContent);
|
|
5
|
+
const tagCache = {
|
|
6
|
+
name: "fs-dev",
|
|
7
|
+
getByPath: async (path) => {
|
|
8
|
+
return tags
|
|
9
|
+
.filter((tagPathMapping) => tagPathMapping.path.S === path)
|
|
10
|
+
.map((tag) => tag.tag.S);
|
|
11
|
+
},
|
|
12
|
+
getByTag: async (tag) => {
|
|
13
|
+
return tags
|
|
14
|
+
.filter((tagPathMapping) => tagPathMapping.tag.S === tag)
|
|
15
|
+
.map((tag) => tag.path.S);
|
|
16
|
+
},
|
|
17
|
+
getLastModified: async (path, lastModified) => {
|
|
18
|
+
const revalidatedTags = tags.filter((tagPathMapping) => tagPathMapping.path.S === path &&
|
|
19
|
+
Number.parseInt(tagPathMapping.revalidatedAt.N) > (lastModified ?? 0));
|
|
20
|
+
return revalidatedTags.length > 0 ? -1 : (lastModified ?? Date.now());
|
|
21
|
+
},
|
|
22
|
+
writeTags: async (newTags) => {
|
|
23
|
+
const newTagsSet = new Set(newTags.map(({ tag, path }) => `${tag}-${path}`));
|
|
24
|
+
const unchangedTags = tags.filter(({ tag, path }) => !newTagsSet.has(`${tag.S}-${path.S}`));
|
|
25
|
+
tags = unchangedTags.concat(newTags.map((tag) => ({
|
|
26
|
+
tag: { S: tag.tag },
|
|
27
|
+
path: { S: tag.path },
|
|
28
|
+
revalidatedAt: { N: String(tag.revalidatedAt) },
|
|
29
|
+
})));
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
export default tagCache;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
const
|
|
2
|
-
city
|
|
1
|
+
const cfPropNameMapping = {
|
|
2
|
+
// The city name is percent-encoded.
|
|
3
|
+
// See https://github.com/vercel/vercel/blob/4cb6143/packages/functions/src/headers.ts#L94C19-L94C37
|
|
4
|
+
city: [encodeURIComponent, "x-open-next-city"],
|
|
3
5
|
country: "x-open-next-country",
|
|
4
6
|
regionCode: "x-open-next-region",
|
|
5
7
|
latitude: "x-open-next-latitude",
|
|
@@ -20,10 +22,15 @@ const handler = async (handler, converter) => async (request, env, ctx) => {
|
|
|
20
22
|
// See https://developers.cloudflare.com/workers/runtime-apis/request
|
|
21
23
|
// Note: This code could be moved to a cloudflare specific converter when one is created.
|
|
22
24
|
const cfProperties = request.cf;
|
|
23
|
-
for (const [propName,
|
|
25
|
+
for (const [propName, mapping] of Object.entries(cfPropNameMapping)) {
|
|
24
26
|
const propValue = cfProperties?.[propName];
|
|
25
27
|
if (propValue != null) {
|
|
26
|
-
|
|
28
|
+
const [encode, headerName] = Array.isArray(mapping)
|
|
29
|
+
? mapping
|
|
30
|
+
: [null, mapping];
|
|
31
|
+
internalEvent.headers[headerName] = encode
|
|
32
|
+
? encode(propValue)
|
|
33
|
+
: propValue;
|
|
27
34
|
}
|
|
28
35
|
}
|
|
29
36
|
const response = await handler(internalEvent);
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { Writable } from "node:stream";
|
|
2
|
+
// Response with null body status (101, 204, 205, or 304) cannot have a body.
|
|
3
|
+
const NULL_BODY_STATUSES = new Set([101, 204, 205, 304]);
|
|
2
4
|
const handler = async (handler, converter) => async (request, env, ctx) => {
|
|
3
5
|
globalThis.process = process;
|
|
4
6
|
globalThis.openNextWaitUntil = ctx.waitUntil.bind(ctx);
|
|
@@ -29,7 +31,8 @@ const handler = async (handler, converter) => async (request, env, ctx) => {
|
|
|
29
31
|
controller.enqueue(Uint8Array.from(chunk.chunk ?? chunk));
|
|
30
32
|
},
|
|
31
33
|
});
|
|
32
|
-
const
|
|
34
|
+
const body = NULL_BODY_STATUSES.has(statusCode) ? null : readable;
|
|
35
|
+
const response = new Response(body, {
|
|
33
36
|
status: statusCode,
|
|
34
37
|
headers: responseHeaders,
|
|
35
38
|
});
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
const dummyWrapper = async (
|
|
1
|
+
const dummyWrapper = async (handler, converter) => {
|
|
2
|
+
return async (event, responseStream) => {
|
|
3
|
+
return await handler(event, responseStream);
|
|
4
|
+
};
|
|
5
|
+
};
|
|
2
6
|
export default {
|
|
3
7
|
name: "dummy",
|
|
4
8
|
wrapper: dummyWrapper,
|
|
5
|
-
supportStreaming:
|
|
9
|
+
supportStreaming: true,
|
|
6
10
|
};
|