@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.
Files changed (61) hide show
  1. package/dist/adapters/cache.d.ts +1 -2
  2. package/dist/adapters/cache.js +20 -11
  3. package/dist/adapters/config/index.d.ts +2 -0
  4. package/dist/adapters/config/index.js +4 -3
  5. package/dist/adapters/config/util.d.ts +2 -0
  6. package/dist/adapters/config/util.js +12 -2
  7. package/dist/adapters/middleware.js +13 -2
  8. package/dist/adapters/util.js +5 -1
  9. package/dist/build/createAssets.js +22 -23
  10. package/dist/build/createImageOptimizationBundle.js +2 -1
  11. package/dist/build/createServerBundle.js +4 -4
  12. package/dist/build/edge/createEdgeBundle.d.ts +4 -3
  13. package/dist/build/edge/createEdgeBundle.js +14 -22
  14. package/dist/build/validateConfig.js +2 -1
  15. package/dist/core/createMainHandler.js +2 -1
  16. package/dist/core/requestHandler.js +24 -6
  17. package/dist/core/resolve.d.ts +4 -0
  18. package/dist/core/resolve.js +10 -0
  19. package/dist/core/routing/middleware.js +3 -1
  20. package/dist/core/routing/routeMatcher.d.ts +10 -0
  21. package/dist/core/routing/routeMatcher.js +45 -0
  22. package/dist/core/routing/util.d.ts +4 -3
  23. package/dist/core/routing/util.js +24 -5
  24. package/dist/core/routingHandler.d.ts +3 -0
  25. package/dist/core/routingHandler.js +30 -29
  26. package/dist/overrides/cdnInvalidation/cloudfront.d.ts +9 -0
  27. package/dist/overrides/cdnInvalidation/cloudfront.js +26 -0
  28. package/dist/overrides/cdnInvalidation/dummy.d.ts +9 -0
  29. package/dist/overrides/cdnInvalidation/dummy.js +6 -0
  30. package/dist/overrides/imageLoader/fs-dev.d.ts +10 -0
  31. package/dist/overrides/imageLoader/fs-dev.js +14 -0
  32. package/dist/overrides/incrementalCache/fs-dev.d.ts +3 -0
  33. package/dist/overrides/incrementalCache/fs-dev.js +30 -0
  34. package/dist/overrides/incrementalCache/multi-tier-ddb-s3.d.ts +10 -0
  35. package/dist/overrides/incrementalCache/multi-tier-ddb-s3.js +120 -0
  36. package/dist/overrides/incrementalCache/s3-lite.d.ts +2 -0
  37. package/dist/overrides/incrementalCache/s3-lite.js +1 -1
  38. package/dist/overrides/proxyExternalRequest/node.js +6 -5
  39. package/dist/overrides/queue/direct.d.ts +3 -0
  40. package/dist/overrides/queue/direct.js +18 -0
  41. package/dist/overrides/tagCache/fs-dev.d.ts +3 -0
  42. package/dist/overrides/tagCache/fs-dev.js +32 -0
  43. package/dist/overrides/wrappers/cloudflare-edge.js +11 -4
  44. package/dist/overrides/wrappers/cloudflare-node.js +4 -1
  45. package/dist/overrides/wrappers/dummy.d.ts +1 -1
  46. package/dist/overrides/wrappers/dummy.js +6 -2
  47. package/dist/overrides/wrappers/express-dev.d.ts +7 -0
  48. package/dist/overrides/wrappers/express-dev.js +43 -0
  49. package/dist/plugins/edge.js +10 -4
  50. package/dist/plugins/replacement.d.ts +1 -1
  51. package/dist/plugins/replacement.js +1 -1
  52. package/dist/plugins/resolve.d.ts +1 -0
  53. package/dist/plugins/resolve.js +4 -1
  54. package/dist/types/global.d.ts +7 -1
  55. package/dist/types/open-next.d.ts +19 -6
  56. package/dist/types/overrides.d.ts +13 -4
  57. package/dist/utils/lru.d.ts +8 -0
  58. package/dist/utils/lru.js +29 -0
  59. package/dist/utils/regex.d.ts +18 -0
  60. package/dist/utils/regex.js +16 -0
  61. 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(rawPath, headers) {
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(rawPath)) {
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(internalEvent, headers, responseStream) {
355
+ export function createServerResponse(routingResult, headers, responseStream) {
356
+ const internalEvent = routingResult.internalEvent;
354
357
  return new OpenNextNodeResponse((_headers) => {
355
- fixCacheHeaderForHtmlPages(internalEvent.rawPath, _headers);
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
- // Add the locale prefix to the regex so we correctly match the rawPath
9
- const optionalLocalePrefixRegex = RoutesManifest.locales.length
10
- ? `^/(?:${RoutesManifest.locales.map((locale) => `${locale}/?`).join("|")})?`
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 isStaticRoute = !isExternalRewrite &&
79
- staticRegexp.some((route) => route.test(internalEvent.rawPath));
80
- if (!isStaticRoute && !isExternalRewrite) {
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 isDynamicRoute = !isExternalRewrite &&
90
- dynamicRegexp.some((route) => route.test(internalEvent.rawPath));
91
- if (!isDynamicRoute && !isStaticRoute && !isExternalRewrite) {
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
- !isApiRoute &&
108
- !isNextImageRoute &&
100
+ if (!(isRouteFoundBeforeAllRewrites ||
101
+ isApiRoute ||
102
+ isNextImageRoute ||
109
103
  // We need to check again once all rewrites have been applied
110
- !staticRegexp.some((route) => route.test(internalEvent.rawPath)) &&
111
- !dynamicRegexp.some((route) => route.test(internalEvent.rawPath))) {
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,9 @@
1
+ declare const _default: {
2
+ name: string;
3
+ invalidatePaths: (paths: {
4
+ initialPath: string;
5
+ rawPath: string;
6
+ resolvedRoutes: import("../../types/open-next").ResolvedRoute[];
7
+ }[]) => Promise<void>;
8
+ };
9
+ export default _default;
@@ -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,9 @@
1
+ declare const _default: {
2
+ name: string;
3
+ invalidatePaths: (_: {
4
+ initialPath: string;
5
+ rawPath: string;
6
+ resolvedRoutes: import("../../types/open-next").ResolvedRoute[];
7
+ }[]) => Promise<void>;
8
+ };
9
+ export default _default;
@@ -0,0 +1,6 @@
1
+ export default {
2
+ name: "dummy",
3
+ invalidatePaths: (_) => {
4
+ return Promise.resolve();
5
+ },
6
+ };
@@ -0,0 +1,10 @@
1
+ import fs from "node:fs";
2
+ declare const _default: {
3
+ name: string;
4
+ load: (url: string) => Promise<{
5
+ body: fs.ReadStream;
6
+ contentType: string;
7
+ cacheControl: string;
8
+ }>;
9
+ };
10
+ export default _default;
@@ -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,3 @@
1
+ import type { IncrementalCache } from "../../types/overrides.js";
2
+ declare const cache: IncrementalCache;
3
+ export default cache;
@@ -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;
@@ -1,3 +1,5 @@
1
+ import { AwsClient } from "aws4fetch";
1
2
  import type { IncrementalCache } from "../../types/overrides";
3
+ export declare const getAwsClient: () => AwsClient;
2
4
  declare const incrementalCache: IncrementalCache;
3
5
  export default incrementalCache;
@@ -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 nodeReadableStream = _res.headers["content-encoding"] === "br"
39
+ const resHeaders = _res.headers;
40
+ const nodeReadableStream = resHeaders["content-encoding"] === "br"
40
41
  ? _res.pipe(require("node:zlib").createBrotliDecompress())
41
- : _res.headers["content-encoding"] === "gzip"
42
+ : resHeaders["content-encoding"] === "gzip"
42
43
  ? _res.pipe(require("node:zlib").createGunzip())
43
44
  : _res;
44
- const isBase64Encoded = isBinaryContentType(headers["content-type"]) ||
45
- !!headers["content-encoding"];
45
+ const isBase64Encoded = isBinaryContentType(resHeaders["content-type"]) ||
46
+ !!resHeaders["content-encoding"];
46
47
  const result = {
47
48
  type: "core",
48
- headers: filterHeadersForProxy(_res.headers),
49
+ headers: filterHeadersForProxy(resHeaders),
49
50
  statusCode: _res.statusCode ?? 200,
50
51
  // TODO: check base64 encoding
51
52
  isBase64Encoded,
@@ -0,0 +1,3 @@
1
+ import type { Queue } from "../../types/overrides.js";
2
+ declare const queue: Queue;
3
+ export default queue;
@@ -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,3 @@
1
+ import type { TagCache } from "../../types/overrides";
2
+ declare const tagCache: TagCache;
3
+ export default tagCache;
@@ -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 cfPropNameToHeaderName = {
2
- city: "x-open-next-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, headerName] of Object.entries(cfPropNameToHeaderName)) {
25
+ for (const [propName, mapping] of Object.entries(cfPropNameMapping)) {
24
26
  const propValue = cfProperties?.[propName];
25
27
  if (propValue != null) {
26
- internalEvent.headers[headerName] = propValue;
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 response = new Response(readable, {
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
  });
@@ -2,6 +2,6 @@ import type { WrapperHandler } from "../../types/overrides";
2
2
  declare const _default: {
3
3
  name: string;
4
4
  wrapper: WrapperHandler;
5
- supportStreaming: false;
5
+ supportStreaming: true;
6
6
  };
7
7
  export default _default;
@@ -1,6 +1,10 @@
1
- const dummyWrapper = async () => async () => undefined;
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: false,
9
+ supportStreaming: true,
6
10
  };
@@ -0,0 +1,7 @@
1
+ import type { WrapperHandler } from "../../types/overrides.js";
2
+ declare const _default: {
3
+ wrapper: WrapperHandler;
4
+ name: string;
5
+ supportStreaming: boolean;
6
+ };
7
+ export default _default;