@opennextjs/aws 3.6.6 → 3.7.1

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.
@@ -1,4 +1,4 @@
1
- import { getTagsFromValue, hasBeenRevalidated } from "../utils/cache";
1
+ import { getTagsFromValue, hasBeenRevalidated, writeTags } from "../utils/cache";
2
2
  import { isBinaryContentType } from "../utils/binary";
3
3
  import { debug, error, warn } from "./logger";
4
4
  function isFetchCache(options) {
@@ -246,7 +246,7 @@ export default class Cache {
246
246
  try {
247
247
  if (globalThis.tagCache.mode === "nextMode") {
248
248
  const paths = (await globalThis.tagCache.getPathsByTags?.(_tags)) ?? [];
249
- await globalThis.tagCache.writeTags(_tags);
249
+ await writeTags(_tags);
250
250
  if (paths.length > 0) {
251
251
  // TODO: we should introduce a new method in cdnInvalidationHandler to invalidate paths by tags for cdn that supports it
252
252
  // It also means that we'll need to provide the tags used in every request to the wrapper or converter.
@@ -291,7 +291,7 @@ export default class Cache {
291
291
  }
292
292
  }
293
293
  // Update all keys with the given tag with revalidatedAt set to now
294
- await globalThis.tagCache.writeTags(toInsert);
294
+ await writeTags(toInsert);
295
295
  // We can now invalidate all paths in the CDN
296
296
  // This only applies to `revalidateTag`, not to `res.revalidate()`
297
297
  const uniquePaths = Array.from(new Set(toInsert
@@ -340,7 +340,7 @@ export default class Cache {
340
340
  const storedTags = await globalThis.tagCache.getByPath(key);
341
341
  const tagsToWrite = derivedTags.filter((tag) => !storedTags.includes(tag));
342
342
  if (tagsToWrite.length > 0) {
343
- await globalThis.tagCache.writeTags(tagsToWrite.map((tag) => ({
343
+ await writeTags(tagsToWrite.map((tag) => ({
344
344
  path: key,
345
345
  tag: tag,
346
346
  // In case the tags are not there we just need to create them
@@ -1,8 +1,15 @@
1
+ import { writeTags } from "../utils/cache";
1
2
  import { fromReadableStream, toReadableStream } from "../utils/stream";
2
3
  import { debug } from "./logger";
4
+ const pendingWritePromiseMap = new Map();
3
5
  export default {
4
6
  async get(cacheKey) {
5
7
  try {
8
+ // We first check if we have a pending write for this cache key
9
+ // If we do, we return the pending promise instead of fetching the cache
10
+ if (pendingWritePromiseMap.has(cacheKey)) {
11
+ return pendingWritePromiseMap.get(cacheKey);
12
+ }
6
13
  const result = await globalThis.incrementalCache.get(cacheKey, "composable");
7
14
  if (!result?.value?.value) {
8
15
  return undefined;
@@ -32,7 +39,10 @@ export default {
32
39
  }
33
40
  },
34
41
  async set(cacheKey, pendingEntry) {
35
- const entry = await pendingEntry;
42
+ pendingWritePromiseMap.set(cacheKey, pendingEntry);
43
+ const entry = await pendingEntry.finally(() => {
44
+ pendingWritePromiseMap.delete(cacheKey);
45
+ });
36
46
  const valueToStore = await fromReadableStream(entry.value);
37
47
  await globalThis.incrementalCache.set(cacheKey, {
38
48
  ...entry,
@@ -42,7 +52,7 @@ export default {
42
52
  const storedTags = await globalThis.tagCache.getByPath(cacheKey);
43
53
  const tagsToWrite = entry.tags.filter((tag) => !storedTags.includes(tag));
44
54
  if (tagsToWrite.length > 0) {
45
- await globalThis.tagCache.writeTags(tagsToWrite.map((tag) => ({ tag, path: cacheKey })));
55
+ await writeTags(tagsToWrite.map((tag) => ({ tag, path: cacheKey })));
46
56
  }
47
57
  }
48
58
  },
@@ -60,7 +70,7 @@ export default {
60
70
  },
61
71
  async expireTags(...tags) {
62
72
  if (globalThis.tagCache.mode === "nextMode") {
63
- return globalThis.tagCache.writeTags(tags);
73
+ return writeTags(tags);
64
74
  }
65
75
  const tagCache = globalThis.tagCache;
66
76
  const revalidatedAt = Date.now();
@@ -79,7 +89,7 @@ export default {
79
89
  for (const entry of pathsToUpdate.flat()) {
80
90
  setToWrite.add(entry);
81
91
  }
82
- await globalThis.tagCache.writeTags(Array.from(setToWrite));
92
+ await writeTags(Array.from(setToWrite));
83
93
  },
84
94
  // This one is necessary for older versions of next
85
95
  async receiveExpiredTags(...tags) {
@@ -4,11 +4,15 @@ import { emptyReadableStream } from "../utils/stream";
4
4
  import { NextConfig } from "../adapters/config";
5
5
  import { createGenericHandler } from "../core/createGenericHandler";
6
6
  import { convertBodyToReadableStream } from "../core/routing/util";
7
+ import { INTERNAL_EVENT_REQUEST_ID } from "../core/routingHandler";
7
8
  globalThis.__openNextAls = new AsyncLocalStorage();
8
9
  const defaultHandler = async (internalEvent, options) => {
9
10
  globalThis.isEdgeRuntime = true;
11
+ const requestId = globalThis.openNextConfig.middleware?.external
12
+ ? internalEvent.headers[INTERNAL_EVENT_REQUEST_ID]
13
+ : Math.random().toString(36);
10
14
  // We run everything in the async local storage context so that it is available in edge runtime functions
11
- return runWithOpenNextRequestContext({ isISRRevalidation: false, waitUntil: options?.waitUntil }, async () => {
15
+ return runWithOpenNextRequestContext({ isISRRevalidation: false, waitUntil: options?.waitUntil, requestId }, async () => {
12
16
  // @ts-expect-error - This is bundled
13
17
  const handler = await import("./middleware.mjs");
14
18
  const response = await handler.default({
@@ -1,25 +1,31 @@
1
1
  import { runWithOpenNextRequestContext } from "../utils/promise";
2
2
  import { debug, error } from "../adapters/logger";
3
3
  import { createGenericHandler } from "../core/createGenericHandler";
4
- import { resolveIncrementalCache, resolveOriginResolver, resolveProxyRequest, resolveQueue, resolveTagCache, } from "../core/resolve";
4
+ import { resolveAssetResolver, resolveIncrementalCache, resolveOriginResolver, resolveProxyRequest, resolveQueue, resolveTagCache, } from "../core/resolve";
5
5
  import { constructNextUrl } from "../core/routing/util";
6
- import routingHandler, { INTERNAL_HEADER_INITIAL_URL, INTERNAL_HEADER_RESOLVED_ROUTES, } from "../core/routingHandler";
6
+ import routingHandler, { INTERNAL_EVENT_REQUEST_ID, INTERNAL_HEADER_INITIAL_URL, INTERNAL_HEADER_RESOLVED_ROUTES, } from "../core/routingHandler";
7
7
  globalThis.internalFetch = fetch;
8
8
  globalThis.__openNextAls = new AsyncLocalStorage();
9
9
  const defaultHandler = async (internalEvent, options) => {
10
- const originResolver = await resolveOriginResolver(globalThis.openNextConfig.middleware?.originResolver);
11
- const externalRequestProxy = await resolveProxyRequest(globalThis.openNextConfig.middleware?.override?.proxyExternalRequest);
10
+ // We know that the middleware is external when this adapter is used
11
+ const middlewareConfig = globalThis.openNextConfig
12
+ .middleware;
13
+ const originResolver = await resolveOriginResolver(middlewareConfig?.originResolver);
14
+ const externalRequestProxy = await resolveProxyRequest(middlewareConfig?.override?.proxyExternalRequest);
15
+ const assetResolver = await resolveAssetResolver(middlewareConfig?.assetResolver);
12
16
  //#override includeCacheInMiddleware
13
- globalThis.tagCache = await resolveTagCache(globalThis.openNextConfig.middleware?.override?.tagCache);
14
- globalThis.queue = await resolveQueue(globalThis.openNextConfig.middleware?.override?.queue);
15
- globalThis.incrementalCache = await resolveIncrementalCache(globalThis.openNextConfig.middleware?.override?.incrementalCache);
17
+ globalThis.tagCache = await resolveTagCache(middlewareConfig?.override?.tagCache);
18
+ globalThis.queue = await resolveQueue(middlewareConfig?.override?.queue);
19
+ globalThis.incrementalCache = await resolveIncrementalCache(middlewareConfig?.override?.incrementalCache);
16
20
  //#endOverride
21
+ const requestId = Math.random().toString(36);
17
22
  // We run everything in the async local storage context so that it is available in the external middleware
18
23
  return runWithOpenNextRequestContext({
19
24
  isISRRevalidation: internalEvent.headers["x-isr"] === "1",
20
25
  waitUntil: options?.waitUntil,
26
+ requestId,
21
27
  }, async () => {
22
- const result = await routingHandler(internalEvent);
28
+ const result = await routingHandler(internalEvent, { assetResolver });
23
29
  if ("internalEvent" in result) {
24
30
  debug("Middleware intercepted event", internalEvent);
25
31
  if (!result.isExternalRewrite) {
@@ -32,6 +38,7 @@ const defaultHandler = async (internalEvent, options) => {
32
38
  ...result.internalEvent.headers,
33
39
  [INTERNAL_HEADER_INITIAL_URL]: internalEvent.url,
34
40
  [INTERNAL_HEADER_RESOLVED_ROUTES]: JSON.stringify(result.resolvedRoutes),
41
+ [INTERNAL_EVENT_REQUEST_ID]: requestId,
35
42
  },
36
43
  },
37
44
  isExternalRewrite: result.isExternalRewrite,
@@ -50,6 +57,10 @@ const defaultHandler = async (internalEvent, options) => {
50
57
  type: "middleware",
51
58
  internalEvent: {
52
59
  ...result.internalEvent,
60
+ headers: {
61
+ ...result.internalEvent.headers,
62
+ [INTERNAL_EVENT_REQUEST_ID]: requestId,
63
+ },
53
64
  rawPath: "/500",
54
65
  url: constructNextUrl(result.internalEvent.url, "/500"),
55
66
  method: "GET",
@@ -63,6 +74,7 @@ const defaultHandler = async (internalEvent, options) => {
63
74
  };
64
75
  }
65
76
  }
77
+ result.headers[INTERNAL_EVENT_REQUEST_ID] = requestId;
66
78
  debug("Middleware response", result);
67
79
  return result;
68
80
  });
@@ -139,6 +139,7 @@ async function generateBundle(name, options, fnOptions, codeCustomization) {
139
139
  const isAfter141 = buildHelper.compareSemver(options.nextVersion, ">=", "14.1");
140
140
  const isAfter142 = buildHelper.compareSemver(options.nextVersion, ">=", "14.2");
141
141
  const isAfter152 = buildHelper.compareSemver(options.nextVersion, ">=", "15.2.0");
142
+ const isAfter154 = buildHelper.compareSemver(options.nextVersion, ">=", "15.4.0");
142
143
  const disableRouting = isBefore13413 || config.middleware?.external;
143
144
  const updater = new ContentUpdater(options);
144
145
  const additionalPlugins = codeCustomization?.additionalPlugins
@@ -153,6 +154,7 @@ async function generateBundle(name, options, fnOptions, codeCustomization) {
153
154
  ...(disableRouting ? ["withRouting"] : []),
154
155
  ...(isAfter142 ? ["patchAsyncStorage"] : []),
155
156
  ...(isAfter141 ? ["appendPrefetch"] : []),
157
+ ...(isAfter154 ? [] : ["setInitialURL"]),
156
158
  ],
157
159
  }),
158
160
  openNextReplacementPlugin({
@@ -61,11 +61,13 @@ export async function generateOutput(options) {
61
61
  const edgeFunctions = {};
62
62
  const isExternalMiddleware = config.middleware?.external ?? false;
63
63
  if (isExternalMiddleware) {
64
+ const middlewareConfig = options.config
65
+ .middleware;
64
66
  edgeFunctions.middleware = {
65
67
  bundle: ".open-next/middleware",
66
68
  handler: "handler.handler",
67
- pathResolver: await extractOverrideName("pattern-env", config.middleware.originResolver),
68
- ...(await extractOverrideFn(config.middleware?.override)),
69
+ pathResolver: await extractOverrideName("pattern-env", middlewareConfig.originResolver),
70
+ ...(await extractOverrideFn(middlewareConfig.override)),
69
71
  };
70
72
  }
71
73
  // Add edge functions
@@ -72,7 +72,9 @@ export function validateConfig(config) {
72
72
  It is safe to disable if you only use page router`);
73
73
  }
74
74
  validateFunctionOptions(config.imageOptimization ?? {});
75
- validateFunctionOptions(config.middleware ?? {});
75
+ if (config.middleware?.external === true) {
76
+ validateFunctionOptions(config.middleware ?? {});
77
+ }
76
78
  //@ts-expect-error - Revalidate custom wrapper type is different
77
79
  validateFunctionOptions(config.revalidate ?? {});
78
80
  //@ts-expect-error - Warmer custom wrapper type is different
@@ -4,8 +4,10 @@ export async function createGenericHandler(handler) {
4
4
  // @ts-expect-error `./open-next.config.mjs` exists only in the build output
5
5
  const config = await import("./open-next.config.mjs").then((m) => m.default);
6
6
  globalThis.openNextConfig = config;
7
- const override = config[handler.type]
8
- ?.override;
7
+ const handlerConfig = config[handler.type];
8
+ const override = handlerConfig && "override" in handlerConfig
9
+ ? handlerConfig.override
10
+ : undefined;
9
11
  // From the config, we create the converter
10
12
  const converter = await resolveConverter(override?.converter);
11
13
  // Then we create the handler
@@ -1,7 +1,7 @@
1
1
  import { debug } from "../adapters/logger";
2
2
  import { generateUniqueId } from "../adapters/util";
3
3
  import { openNextHandler } from "./requestHandler";
4
- import { resolveCdnInvalidation, resolveConverter, resolveIncrementalCache, resolveProxyRequest, resolveQueue, resolveTagCache, resolveWrapper, } from "./resolve";
4
+ import { resolveAssetResolver, resolveCdnInvalidation, resolveConverter, resolveIncrementalCache, resolveProxyRequest, resolveQueue, resolveTagCache, resolveWrapper, } from "./resolve";
5
5
  export async function createMainHandler() {
6
6
  // @ts-expect-error `./open-next.config.mjs` exists only in the build output
7
7
  const config = await import("./open-next.config.mjs").then((m) => m.default);
@@ -16,6 +16,9 @@ export async function createMainHandler() {
16
16
  globalThis.queue = await resolveQueue(thisFunction.override?.queue);
17
17
  globalThis.incrementalCache = await resolveIncrementalCache(thisFunction.override?.incrementalCache);
18
18
  globalThis.tagCache = await resolveTagCache(thisFunction.override?.tagCache);
19
+ if (config.middleware?.external !== true) {
20
+ globalThis.assetResolver = await resolveAssetResolver(globalThis.openNextConfig.middleware?.assetResolver);
21
+ }
19
22
  globalThis.proxyExternalRequest = await resolveProxyRequest(thisFunction.override?.proxyExternalRequest);
20
23
  globalThis.cdnInvalidationHandler = await resolveCdnInvalidation(thisFunction.override?.cdnInvalidation);
21
24
  // From the config, we create the converter
@@ -4,8 +4,8 @@ import { runWithOpenNextRequestContext } from "../utils/promise";
4
4
  import { NextConfig } from "../adapters/config/index";
5
5
  import { debug, error } from "../adapters/logger";
6
6
  import { patchAsyncStorage } from "./patchAsyncStorage";
7
- import { constructNextUrl, convertRes, convertToQuery, createServerResponse, } from "./routing/util";
8
- import routingHandler, { INTERNAL_HEADER_INITIAL_URL, INTERNAL_HEADER_RESOLVED_ROUTES, MIDDLEWARE_HEADER_PREFIX, MIDDLEWARE_HEADER_PREFIX_LEN, } from "./routingHandler";
7
+ import { constructNextUrl, convertRes, convertToQuery, convertToQueryString, createServerResponse, } from "./routing/util";
8
+ import routingHandler, { INTERNAL_EVENT_REQUEST_ID, INTERNAL_HEADER_INITIAL_URL, INTERNAL_HEADER_RESOLVED_ROUTES, MIDDLEWARE_HEADER_PREFIX, MIDDLEWARE_HEADER_PREFIX_LEN, } from "./routingHandler";
9
9
  import { requestHandler, setNextjsPrebundledReact } from "./util";
10
10
  // This is used to identify requests in the cache
11
11
  globalThis.__openNextAls = new AsyncLocalStorage();
@@ -14,10 +14,17 @@ patchAsyncStorage();
14
14
  //#endOverride
15
15
  export async function openNextHandler(internalEvent, options) {
16
16
  const initialHeaders = internalEvent.headers;
17
+ // We only use the requestId header if we are using an external middleware
18
+ // This is to ensure that no one can spoof the requestId
19
+ // When using an external middleware, we always assume that headers cannot be spoofed
20
+ const requestId = globalThis.openNextConfig.middleware?.external
21
+ ? internalEvent.headers[INTERNAL_EVENT_REQUEST_ID]
22
+ : Math.random().toString(36);
17
23
  // We run everything in the async local storage context so that it is available in the middleware as well as in NextServer
18
24
  return runWithOpenNextRequestContext({
19
25
  isISRRevalidation: initialHeaders["x-isr"] === "1",
20
26
  waitUntil: options?.waitUntil,
27
+ requestId,
21
28
  }, async () => {
22
29
  await globalThis.__next_route_preloader("waitUntil");
23
30
  if (initialHeaders["x-forwarded-host"]) {
@@ -40,7 +47,9 @@ export async function openNextHandler(internalEvent, options) {
40
47
  ...internalHeaders,
41
48
  };
42
49
  //#override withRouting
43
- routingResult = await routingHandler(internalEvent);
50
+ routingResult = await routingHandler(internalEvent, {
51
+ assetResolver: globalThis.assetResolver,
52
+ });
44
53
  //#endOverride
45
54
  const headers = "type" in routingResult
46
55
  ? routingResult.headers
@@ -158,7 +167,10 @@ async function processRequest(req, res, routingResult) {
158
167
  // Here we try to apply as much request metadata as possible
159
168
  // We apply every metadata from `resolve-routes` https://github.com/vercel/next.js/blob/916f105b97211de50f8580f0b39c9e7c60de4886/packages/next/src/server/lib/router-utils/resolve-routes.ts
160
169
  // and `router-server` https://github.com/vercel/next.js/blob/916f105b97211de50f8580f0b39c9e7c60de4886/packages/next/src/server/lib/router-server.ts
161
- const initialURL = new URL(routingResult.initialURL);
170
+ const initialURL = new URL(
171
+ // We always assume that only the routing layer can set this header.
172
+ routingResult.internalEvent.headers[INTERNAL_HEADER_INITIAL_URL] ??
173
+ routingResult.initialURL);
162
174
  let invokeStatus;
163
175
  if (routingResult.internalEvent.rawPath === "/500") {
164
176
  invokeStatus = 500;
@@ -185,6 +197,13 @@ async function processRequest(req, res, routingResult) {
185
197
  setNextjsPrebundledReact(routingResult.internalEvent.rawPath);
186
198
  //#endOverride
187
199
  // Next Server
200
+ // TODO: only enable this on Next 15.4+
201
+ // We need to set the pathname to the data request path
202
+ //#override setInitialURL
203
+ req.url =
204
+ initialURL.pathname +
205
+ convertToQueryString(routingResult.internalEvent.query);
206
+ //#endOverride
188
207
  await requestHandler(requestMetadata)(req, res);
189
208
  }
190
209
  catch (e) {
@@ -1,4 +1,4 @@
1
- import type { BaseEventOrResult, DefaultOverrideOptions, InternalEvent, InternalResult, OpenNextConfig, OverrideOptions } from "../types/open-next";
1
+ import type { BaseEventOrResult, DefaultOverrideOptions, ExternalMiddlewareConfig, InternalEvent, InternalResult, OpenNextConfig, OverrideOptions } from "../types/open-next";
2
2
  import type { Converter, TagCache, Wrapper } from "../types/overrides";
3
3
  type RemoveUndefined<T> = T extends undefined ? never : T;
4
4
  export declare function resolveConverter<E extends BaseEventOrResult = InternalEvent, R extends BaseEventOrResult = InternalResult>(converter: DefaultOverrideOptions<E, R>["converter"]): Promise<Converter<E, R>>;
@@ -34,7 +34,12 @@ export declare function resolveImageLoader(imageLoader: RemoveUndefined<OpenNext
34
34
  * @returns
35
35
  * @__PURE__
36
36
  */
37
- export declare function resolveOriginResolver(originResolver: RemoveUndefined<OpenNextConfig["middleware"]>["originResolver"]): Promise<import("../types/overrides").OriginResolver>;
37
+ export declare function resolveOriginResolver(originResolver: RemoveUndefined<ExternalMiddlewareConfig>["originResolver"]): Promise<import("../types/overrides").OriginResolver>;
38
+ /**
39
+ * @returns
40
+ * @__PURE__
41
+ */
42
+ export declare function resolveAssetResolver(assetResolver: RemoveUndefined<OpenNextConfig["middleware"]>["assetResolver"]): Promise<import("../types/overrides").AssetResolver>;
38
43
  /**
39
44
  * @__PURE__
40
45
  */
@@ -78,6 +78,17 @@ export async function resolveOriginResolver(originResolver) {
78
78
  const m_1 = await import("../overrides/originResolver/pattern-env.js");
79
79
  return m_1.default;
80
80
  }
81
+ /**
82
+ * @returns
83
+ * @__PURE__
84
+ */
85
+ export async function resolveAssetResolver(assetResolver) {
86
+ if (typeof assetResolver === "function") {
87
+ return assetResolver();
88
+ }
89
+ const m_1 = await import("../overrides/assetResolver/dummy.js");
90
+ return m_1.default;
91
+ }
81
92
  /**
82
93
  * @__PURE__
83
94
  */
@@ -7,6 +7,16 @@ import { localizePath } from "./i18n";
7
7
  import { generateMessageGroupId } from "./queue";
8
8
  const CACHE_ONE_YEAR = 60 * 60 * 24 * 365;
9
9
  const CACHE_ONE_MONTH = 60 * 60 * 24 * 30;
10
+ /*
11
+ * We use this header to prevent Firefox (and possibly some CDNs) from incorrectly reusing the RSC responses during caching.
12
+ * This can especially happen when there's a redirect in the middleware as the `_rsc` query parameter is not visible there.
13
+ * So it will get dropped during the redirect, which results in the RSC response being cached instead of the actual HTML on the path `/`.
14
+ * This value can be found in the routes manifest, under `rsc.varyHeader`.
15
+ * They recompute it here in Next:
16
+ * https://github.com/vercel/next.js/blob/c5bf5bb4c8b01b1befbbfa7ad97a97476ee9d0d7/packages/next/src/server/base-server.ts#L2011
17
+ * Also see this PR: https://github.com/vercel/next.js/pull/79426
18
+ */
19
+ const VARY_HEADER = "RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Router-Segment-Prefetch, Next-Url";
10
20
  async function computeCacheControl(path, body, host, revalidate, lastModified) {
11
21
  let finalRevalidate = CACHE_ONE_YEAR;
12
22
  const existingRoute = Object.entries(PrerenderManifest.routes).find((p) => p[0] === path)?.[1];
@@ -100,6 +110,7 @@ async function generateResult(event, localizedPath, cachedValue, lastModified) {
100
110
  ...cacheControl,
101
111
  "content-type": type,
102
112
  ...cachedValue.meta?.headers,
113
+ vary: VARY_HEADER,
103
114
  },
104
115
  };
105
116
  }
@@ -1,6 +1,11 @@
1
1
  import type { Header, PrerenderManifest, RedirectDefinition, RewriteDefinition } from "../../types/next-types";
2
2
  import type { InternalEvent, InternalResult } from "../../types/open-next";
3
3
  export declare function getNextConfigHeaders(event: InternalEvent, configHeaders?: Header[] | undefined): Record<string, string | undefined>;
4
+ /**
5
+ * TODO: This method currently only check for the first match.
6
+ * It should check for all matches for `beforeFiles` and `afterFiles` rewrite
7
+ * See https://nextjs.org/docs/app/api-reference/config/next-config-js/rewrites
8
+ */
4
9
  export declare function handleRewrites<T extends RewriteDefinition>(event: InternalEvent, rewrites: T[]): {
5
10
  internalEvent: {
6
11
  query: Record<string, string | string[]>;
@@ -97,6 +97,11 @@ export function getNextConfigHeaders(event, configHeaders) {
97
97
  }
98
98
  return requestHeaders;
99
99
  }
100
+ /**
101
+ * TODO: This method currently only check for the first match.
102
+ * It should check for all matches for `beforeFiles` and `afterFiles` rewrite
103
+ * See https://nextjs.org/docs/app/api-reference/config/next-config-js/rewrites
104
+ */
100
105
  export function handleRewrites(event, rewrites) {
101
106
  const { rawPath, headers, query, cookies, url } = event;
102
107
  const localizedRawPath = localizePath(event);
@@ -233,6 +233,8 @@ export function addOpenNextHeader(headers) {
233
233
  }
234
234
  if (globalThis.openNextDebug) {
235
235
  headers["X-OpenNext-Version"] = globalThis.openNextVersion;
236
+ }
237
+ if (process.env.OPEN_NEXT_REQUEST_ID_HEADER || globalThis.openNextDebug) {
236
238
  headers["X-OpenNext-RequestId"] =
237
239
  globalThis.__openNextAls.getStore()?.requestId;
238
240
  }
@@ -1,8 +1,12 @@
1
1
  import type { InternalEvent, InternalResult, RoutingResult } from "../types/open-next";
2
+ import type { AssetResolver } from "../types/overrides";
2
3
  export declare const MIDDLEWARE_HEADER_PREFIX = "x-middleware-response-";
3
4
  export declare const MIDDLEWARE_HEADER_PREFIX_LEN: number;
4
5
  export declare const INTERNAL_HEADER_PREFIX = "x-opennext-";
5
6
  export declare const INTERNAL_HEADER_INITIAL_URL = "x-opennext-initial-url";
6
7
  export declare const INTERNAL_HEADER_LOCALE = "x-opennext-locale";
7
8
  export declare const INTERNAL_HEADER_RESOLVED_ROUTES = "x-opennext-resolved-routes";
8
- export default function routingHandler(event: InternalEvent): Promise<InternalResult | RoutingResult>;
9
+ export declare const INTERNAL_EVENT_REQUEST_ID = "x-opennext-request-id";
10
+ export default function routingHandler(event: InternalEvent, { assetResolver }: {
11
+ assetResolver?: AssetResolver;
12
+ }): Promise<InternalResult | RoutingResult>;
@@ -12,6 +12,7 @@ export const INTERNAL_HEADER_PREFIX = "x-opennext-";
12
12
  export const INTERNAL_HEADER_INITIAL_URL = `${INTERNAL_HEADER_PREFIX}initial-url`;
13
13
  export const INTERNAL_HEADER_LOCALE = `${INTERNAL_HEADER_PREFIX}locale`;
14
14
  export const INTERNAL_HEADER_RESOLVED_ROUTES = `${INTERNAL_HEADER_PREFIX}resolved-routes`;
15
+ export const INTERNAL_EVENT_REQUEST_ID = `${INTERNAL_HEADER_PREFIX}request-id`;
15
16
  // Geolocation headers starting from Nextjs 15
16
17
  // See https://github.com/vercel/vercel/blob/7714b1c/packages/functions/src/headers.ts
17
18
  const geoHeaderToNextHeader = {
@@ -21,17 +22,25 @@ const geoHeaderToNextHeader = {
21
22
  "x-open-next-latitude": "x-vercel-ip-latitude",
22
23
  "x-open-next-longitude": "x-vercel-ip-longitude",
23
24
  };
24
- function applyMiddlewareHeaders(eventHeaders, middlewareHeaders, setPrefix = true) {
25
- const keyPrefix = setPrefix ? MIDDLEWARE_HEADER_PREFIX : "";
25
+ /**
26
+ * Adds the middleware headers to an event or result.
27
+ *
28
+ * @param eventOrResult
29
+ * @param middlewareHeaders
30
+ */
31
+ function applyMiddlewareHeaders(eventOrResult, middlewareHeaders) {
32
+ // Use the `MIDDLEWARE_HEADER_PREFIX` prefix for events, they will be processed by the request handler later.
33
+ // Results do not go through the request handler and should not be prefixed.
34
+ const isResult = isInternalResult(eventOrResult);
35
+ const headers = eventOrResult.headers;
36
+ const keyPrefix = isResult ? "" : MIDDLEWARE_HEADER_PREFIX;
26
37
  Object.entries(middlewareHeaders).forEach(([key, value]) => {
27
38
  if (value) {
28
- eventHeaders[keyPrefix + key] = Array.isArray(value)
29
- ? value.join(",")
30
- : value;
39
+ headers[keyPrefix + key] = Array.isArray(value) ? value.join(",") : value;
31
40
  }
32
41
  });
33
42
  }
34
- export default async function routingHandler(event) {
43
+ export default async function routingHandler(event, { assetResolver }) {
35
44
  try {
36
45
  // Add Next geo headers
37
46
  for (const [openNextGeoName, nextGeoName] of Object.entries(geoHeaderToNextHeader)) {
@@ -48,12 +57,13 @@ export default async function routingHandler(event) {
48
57
  delete event.headers[key];
49
58
  }
50
59
  }
51
- const nextHeaders = getNextConfigHeaders(event, ConfigHeaders);
52
- let internalEvent = fixDataPage(event, BuildId);
53
- if ("statusCode" in internalEvent) {
54
- return internalEvent;
60
+ // Headers from the Next config and middleware (the later are applied further down).
61
+ let headers = getNextConfigHeaders(event, ConfigHeaders);
62
+ let eventOrResult = fixDataPage(event, BuildId);
63
+ if (isInternalResult(eventOrResult)) {
64
+ return eventOrResult;
55
65
  }
56
- const redirect = handleRedirects(internalEvent, RoutesManifest.redirects);
66
+ const redirect = handleRedirects(eventOrResult, RoutesManifest.redirects);
57
67
  if (redirect) {
58
68
  // We need to encode the value in the Location header to make sure it is valid according to RFC
59
69
  // https://stackoverflow.com/a/7654605/16587222
@@ -61,99 +71,105 @@ export default async function routingHandler(event) {
61
71
  debug("redirect", redirect);
62
72
  return redirect;
63
73
  }
64
- const eventOrResult = await handleMiddleware(internalEvent,
74
+ const middlewareEventOrResult = await handleMiddleware(eventOrResult,
65
75
  // We need to pass the initial search without any decoding
66
76
  // TODO: we'd need to refactor InternalEvent to include the initial querystring directly
67
77
  // Should be done in another PR because it is a breaking change
68
78
  new URL(event.url).search);
69
- const isResult = "statusCode" in eventOrResult;
70
- if (isResult) {
71
- return eventOrResult;
79
+ if (isInternalResult(middlewareEventOrResult)) {
80
+ return middlewareEventOrResult;
72
81
  }
73
- const middlewareResponseHeaders = eventOrResult.responseHeaders;
74
- let isExternalRewrite = eventOrResult.isExternalRewrite ?? false;
75
- // internalEvent is `InternalEvent | MiddlewareEvent`
76
- internalEvent = eventOrResult;
82
+ headers = {
83
+ ...middlewareEventOrResult.responseHeaders,
84
+ ...headers,
85
+ };
86
+ let isExternalRewrite = middlewareEventOrResult.isExternalRewrite ?? false;
87
+ eventOrResult = middlewareEventOrResult;
77
88
  if (!isExternalRewrite) {
78
89
  // First rewrite to be applied
79
- const beforeRewrites = handleRewrites(internalEvent, RoutesManifest.rewrites.beforeFiles);
80
- internalEvent = beforeRewrites.internalEvent;
81
- isExternalRewrite = beforeRewrites.isExternalRewrite;
90
+ const beforeRewrite = handleRewrites(eventOrResult, RoutesManifest.rewrites.beforeFiles);
91
+ eventOrResult = beforeRewrite.internalEvent;
92
+ isExternalRewrite = beforeRewrite.isExternalRewrite;
93
+ // Check for matching public files after `beforeFiles` rewrites
94
+ // See:
95
+ // - https://nextjs.org/docs/app/api-reference/file-conventions/middleware#execution-order
96
+ // - https://nextjs.org/docs/app/api-reference/config/next-config-js/rewrites
97
+ if (!isExternalRewrite) {
98
+ const assetResult = await assetResolver?.maybeGetAssetResult?.(eventOrResult);
99
+ if (assetResult) {
100
+ applyMiddlewareHeaders(assetResult, headers);
101
+ return assetResult;
102
+ }
103
+ }
82
104
  }
83
- const foundStaticRoute = staticRouteMatcher(internalEvent.rawPath);
105
+ const foundStaticRoute = staticRouteMatcher(eventOrResult.rawPath);
84
106
  const isStaticRoute = !isExternalRewrite && foundStaticRoute.length > 0;
85
107
  if (!(isStaticRoute || isExternalRewrite)) {
86
108
  // Second rewrite to be applied
87
- const afterRewrites = handleRewrites(internalEvent, RoutesManifest.rewrites.afterFiles);
88
- internalEvent = afterRewrites.internalEvent;
89
- isExternalRewrite = afterRewrites.isExternalRewrite;
109
+ const afterRewrite = handleRewrites(eventOrResult, RoutesManifest.rewrites.afterFiles);
110
+ eventOrResult = afterRewrite.internalEvent;
111
+ isExternalRewrite = afterRewrite.isExternalRewrite;
90
112
  }
91
113
  let isISR = false;
92
114
  // We want to run this just before the dynamic route check
93
115
  // We can skip it if its an external rewrite
94
116
  if (!isExternalRewrite) {
95
- const fallbackResult = handleFallbackFalse(internalEvent, PrerenderManifest);
96
- internalEvent = fallbackResult.event;
117
+ const fallbackResult = handleFallbackFalse(eventOrResult, PrerenderManifest);
118
+ eventOrResult = fallbackResult.event;
97
119
  isISR = fallbackResult.isISR;
98
120
  }
99
- const foundDynamicRoute = dynamicRouteMatcher(internalEvent.rawPath);
121
+ const foundDynamicRoute = dynamicRouteMatcher(eventOrResult.rawPath);
100
122
  const isDynamicRoute = !isExternalRewrite && foundDynamicRoute.length > 0;
101
123
  if (!(isDynamicRoute || isStaticRoute || isExternalRewrite)) {
102
124
  // Fallback rewrite to be applied
103
- const fallbackRewrites = handleRewrites(internalEvent, RoutesManifest.rewrites.fallback);
104
- internalEvent = fallbackRewrites.internalEvent;
125
+ const fallbackRewrites = handleRewrites(eventOrResult, RoutesManifest.rewrites.fallback);
126
+ eventOrResult = fallbackRewrites.internalEvent;
105
127
  isExternalRewrite = fallbackRewrites.isExternalRewrite;
106
128
  }
107
- const isNextImageRoute = internalEvent.rawPath.startsWith("/_next/image");
129
+ const isNextImageRoute = eventOrResult.rawPath.startsWith("/_next/image");
108
130
  const isRouteFoundBeforeAllRewrites = isStaticRoute || isDynamicRoute || isExternalRewrite;
109
131
  // If we still haven't found a route, we show the 404 page
110
132
  // We need to ensure that rewrites are applied before showing the 404 page
111
133
  if (!(isRouteFoundBeforeAllRewrites ||
112
134
  isNextImageRoute ||
113
135
  // We need to check again once all rewrites have been applied
114
- staticRouteMatcher(internalEvent.rawPath).length > 0 ||
115
- dynamicRouteMatcher(internalEvent.rawPath).length > 0)) {
116
- internalEvent = {
117
- ...internalEvent,
136
+ staticRouteMatcher(eventOrResult.rawPath).length > 0 ||
137
+ dynamicRouteMatcher(eventOrResult.rawPath).length > 0)) {
138
+ eventOrResult = {
139
+ ...eventOrResult,
118
140
  rawPath: "/404",
119
- url: constructNextUrl(internalEvent.url, "/404"),
141
+ url: constructNextUrl(eventOrResult.url, "/404"),
120
142
  headers: {
121
- ...internalEvent.headers,
143
+ ...eventOrResult.headers,
122
144
  "x-middleware-response-cache-control": "private, no-cache, no-store, max-age=0, must-revalidate",
123
145
  },
124
146
  };
125
147
  }
126
148
  if (globalThis.openNextConfig.dangerous?.enableCacheInterception &&
127
- !("statusCode" in internalEvent)) {
149
+ !isInternalResult(eventOrResult)) {
128
150
  debug("Cache interception enabled");
129
- internalEvent = await cacheInterceptor(internalEvent);
130
- if ("statusCode" in internalEvent) {
131
- applyMiddlewareHeaders(internalEvent.headers, {
132
- ...middlewareResponseHeaders,
133
- ...nextHeaders,
134
- }, false);
135
- return internalEvent;
151
+ eventOrResult = await cacheInterceptor(eventOrResult);
152
+ if (isInternalResult(eventOrResult)) {
153
+ applyMiddlewareHeaders(eventOrResult, headers);
154
+ return eventOrResult;
136
155
  }
137
156
  }
138
157
  // We apply the headers from the middleware response last
139
- applyMiddlewareHeaders(internalEvent.headers, {
140
- ...middlewareResponseHeaders,
141
- ...nextHeaders,
142
- });
158
+ applyMiddlewareHeaders(eventOrResult, headers);
143
159
  const resolvedRoutes = [
144
160
  ...foundStaticRoute,
145
161
  ...foundDynamicRoute,
146
162
  ];
147
163
  debug("resolvedRoutes", resolvedRoutes);
148
164
  return {
149
- internalEvent,
165
+ internalEvent: eventOrResult,
150
166
  isExternalRewrite,
151
167
  origin: false,
152
168
  isISR,
153
169
  resolvedRoutes,
154
170
  initialURL: event.url,
155
171
  locale: NextConfig.i18n
156
- ? detectLocale(internalEvent, NextConfig.i18n)
172
+ ? detectLocale(eventOrResult, NextConfig.i18n)
157
173
  : undefined,
158
174
  };
159
175
  }
@@ -184,3 +200,10 @@ export default async function routingHandler(event) {
184
200
  };
185
201
  }
186
202
  }
203
+ /**
204
+ * @param eventOrResult
205
+ * @returns Whether the event is an instance of `InternalResult`
206
+ */
207
+ function isInternalResult(eventOrResult) {
208
+ return eventOrResult != null && "statusCode" in eventOrResult;
209
+ }
@@ -0,0 +1,8 @@
1
+ import type { AssetResolver } from "../../types/overrides";
2
+ /**
3
+ * A dummy asset resolver.
4
+ *
5
+ * It never overrides the result with an asset.
6
+ */
7
+ declare const resolver: AssetResolver;
8
+ export default resolver;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * A dummy asset resolver.
3
+ *
4
+ * It never overrides the result with an asset.
5
+ */
6
+ const resolver = {
7
+ name: "dummy",
8
+ };
9
+ export default resolver;
@@ -1,6 +1,6 @@
1
1
  import type { AsyncLocalStorage } from "node:async_hooks";
2
2
  import type { OutgoingHttpHeaders } from "node:http";
3
- import type { CDNInvalidationHandler, IncrementalCache, ProxyExternalRequest, Queue, TagCache } from "./overrides";
3
+ import type { AssetResolver, CDNInvalidationHandler, IncrementalCache, ProxyExternalRequest, Queue, TagCache } from "./overrides";
4
4
  import type { DetachedPromiseRunner } from "../utils/promise";
5
5
  import type { OpenNextConfig, WaitUntil } from "./open-next";
6
6
  export interface RequestData {
@@ -53,6 +53,8 @@ interface OpenNextRequestContext {
53
53
  mergeHeadersPriority?: "middleware" | "handler";
54
54
  lastModified?: number;
55
55
  waitUntil?: WaitUntil;
56
+ /** We use this to deduplicate write of the tags*/
57
+ writtenTags: Set<string>;
56
58
  }
57
59
  declare global {
58
60
  /**
@@ -179,6 +181,12 @@ declare global {
179
181
  * Defined in `createMainHandler`
180
182
  */
181
183
  var cdnInvalidationHandler: CDNInvalidationHandler;
184
+ /**
185
+ * The function called to resolve assets.
186
+ * Available in main functions
187
+ * Defined in `createMainHandler` when the middleware is internal
188
+ */
189
+ var assetResolver: AssetResolver | undefined;
182
190
  /**
183
191
  * A function to preload the routes.
184
192
  * This needs to be defined on globalThis because it can be used by custom overrides.
@@ -1,7 +1,7 @@
1
1
  import type { ReadableStream } from "node:stream/web";
2
2
  import type { Writable } from "node:stream";
3
3
  import type { WarmerEvent, WarmerResponse } from "../adapters/warmer-function";
4
- import type { CDNInvalidationHandler, Converter, ImageLoader, IncrementalCache, OriginResolver, ProxyExternalRequest, Queue, TagCache, Warmer, Wrapper } from "./overrides";
4
+ import type { AssetResolver, CDNInvalidationHandler, Converter, ImageLoader, IncrementalCache, OriginResolver, ProxyExternalRequest, Queue, TagCache, Warmer, Wrapper } from "./overrides";
5
5
  export type BaseEventOrResult<T extends string = string> = {
6
6
  type: T;
7
7
  };
@@ -103,6 +103,7 @@ export type IncludedOriginResolver = "pattern-env" | "dummy";
103
103
  export type IncludedWarmer = "aws-lambda" | "dummy";
104
104
  export type IncludedProxyExternalRequest = "node" | "fetch" | "dummy";
105
105
  export type IncludedCDNInvalidationHandler = "cloudfront" | "dummy";
106
+ export type IncludedAssetResolver = "dummy";
106
107
  export interface DefaultOverrideOptions<E extends BaseEventOrResult = InternalEvent, R extends BaseEventOrResult = InternalResult> {
107
108
  /**
108
109
  * This is the main entrypoint of your app.
@@ -243,37 +244,56 @@ export interface SplittedFunctionOptions extends FunctionOptions {
243
244
  */
244
245
  patterns: string[];
245
246
  }
247
+ /**
248
+ * MiddlewareConfig that applies to both external and internal middlewares
249
+ *
250
+ * Note: this type is internal and included in both `ExternalMiddlewareConfig` and `InternalMiddlewareConfig`
251
+ */
252
+ type CommonMiddlewareConfig = {
253
+ /**
254
+ * The assetResolver is used to resolve assets in the routing layer.
255
+ *
256
+ * @default "dummy"
257
+ */
258
+ assetResolver?: IncludedAssetResolver | LazyLoadedOverride<AssetResolver>;
259
+ };
260
+ /** MiddlewareConfig that applies to external middlewares only */
261
+ export type ExternalMiddlewareConfig = DefaultFunctionOptions & CommonMiddlewareConfig & {
262
+ external: true;
263
+ /**
264
+ * The runtime used by next for the middleware.
265
+ * @default "edge"
266
+ */
267
+ runtime?: "node" | "edge";
268
+ /**
269
+ * The override options for the middleware.
270
+ * By default the lite override are used (.i.e. s3-lite, dynamodb-lite, sqs-lite)
271
+ * @default undefined
272
+ */
273
+ override?: OverrideOptions;
274
+ /**
275
+ * Origin resolver is used to resolve the origin for internal rewrite.
276
+ * By default, it uses the pattern-env origin resolver.
277
+ * Pattern env uses pattern set in split function options and an env variable OPEN_NEXT_ORIGIN
278
+ * OPEN_NEXT_ORIGIN should be a json stringified object with the key of the splitted function as key and the origin as value
279
+ * @default "pattern-env"
280
+ */
281
+ originResolver?: IncludedOriginResolver | LazyLoadedOverride<OriginResolver>;
282
+ };
283
+ /** MiddlewareConfig that applies to internal middlewares only */
284
+ export type InternalMiddlewareConfig = {
285
+ external: false;
286
+ } & CommonMiddlewareConfig;
246
287
  export interface OpenNextConfig {
247
288
  default: FunctionOptions;
248
289
  functions?: Record<string, SplittedFunctionOptions>;
249
290
  /**
250
291
  * Override the default middleware
251
- * If you set this options, the middleware need to be deployed separately.
292
+ * When `external` is true, the middleware need to be deployed separately.
252
293
  * It supports both edge and node runtime.
253
- * @default undefined
294
+ * @default undefined - Which is equivalent to `external: false`
254
295
  */
255
- middleware?: DefaultFunctionOptions & {
256
- external: true;
257
- /**
258
- * The runtime used by next for the middleware.
259
- * @default "edge"
260
- */
261
- runtime?: "node" | "edge";
262
- /**
263
- * The override options for the middleware.
264
- * By default the lite override are used (.i.e. s3-lite, dynamodb-lite, sqs-lite)
265
- * @default undefined
266
- */
267
- override?: OverrideOptions;
268
- /**
269
- * Origin resolver is used to resolve the origin for internal rewrite.
270
- * By default, it uses the pattern-env origin resolver.
271
- * Pattern env uses pattern set in split function options and an env variable OPEN_NEXT_ORIGIN
272
- * OPEN_NEXT_ORIGIN should be a json stringified object with the key of the splitted function as key and the origin as value
273
- * @default "pattern-env"
274
- */
275
- originResolver?: IncludedOriginResolver | LazyLoadedOverride<OriginResolver>;
276
- };
296
+ middleware?: ExternalMiddlewareConfig | InternalMiddlewareConfig;
277
297
  /**
278
298
  * Override the default warmer
279
299
  * By default, works for lambda only.
@@ -355,3 +375,4 @@ export interface OpenNextConfig {
355
375
  */
356
376
  edgeExternals?: string[];
357
377
  }
378
+ export {};
@@ -15,6 +15,19 @@ export interface Queue {
15
15
  send(message: QueueMessage): Promise<void>;
16
16
  name: string;
17
17
  }
18
+ /**
19
+ * Resolves assets in the routing layer.
20
+ */
21
+ export interface AssetResolver {
22
+ name: string;
23
+ /**
24
+ * Called by the routing layer to check for a matching static asset.
25
+ *
26
+ * @param event
27
+ * @returns an `InternalResult` when an asset is found a the path from the event, undefined otherwise.
28
+ */
29
+ maybeGetAssetResult?: (event: InternalEvent) => Promise<InternalResult | undefined> | undefined;
30
+ }
18
31
  export type CachedFile = {
19
32
  type: "redirect";
20
33
  props?: Object;
@@ -95,6 +108,11 @@ export type NextModeTagCache = BaseTagCache & {
95
108
  writeTags(tags: string[]): Promise<void>;
96
109
  getPathsByTags?: (tags: string[]) => Promise<string[]>;
97
110
  };
111
+ export interface OriginalTagCacheWriteInput {
112
+ tag: string;
113
+ path: string;
114
+ revalidatedAt?: number;
115
+ }
98
116
  /**
99
117
  * On get :
100
118
  We just check for the cache key in the tag cache. If it has been revalidated we just return null, otherwise we continue
@@ -119,11 +137,7 @@ export type OriginalTagCache = BaseTagCache & {
119
137
  getByTag(tag: string): Promise<string[]>;
120
138
  getByPath(path: string): Promise<string[]>;
121
139
  getLastModified(path: string, lastModified?: number): Promise<number>;
122
- writeTags(tags: {
123
- tag: string;
124
- path: string;
125
- revalidatedAt?: number;
126
- }[]): Promise<void>;
140
+ writeTags(tags: OriginalTagCacheWriteInput[]): Promise<void>;
127
141
  };
128
142
  export type TagCache = NextModeTagCache | OriginalTagCache;
129
143
  export type WrapperHandler<E extends BaseEventOrResult = InternalEvent, R extends BaseEventOrResult = InternalResult> = (handler: OpenNextHandler<E, R>, converter: Converter<E, R>) => Promise<(...args: any[]) => any>;
@@ -1,3 +1,4 @@
1
- import type { CacheValue, WithLastModified } from "../types/overrides";
1
+ import type { CacheValue, OriginalTagCacheWriteInput, WithLastModified } from "../types/overrides";
2
2
  export declare function hasBeenRevalidated(key: string, tags: string[], cacheEntry: WithLastModified<CacheValue<any>>): Promise<boolean>;
3
3
  export declare function getTagsFromValue(value?: CacheValue<"cache">): string[];
4
+ export declare function writeTags(tags: (string | OriginalTagCacheWriteInput)[]): Promise<void>;
@@ -1,3 +1,4 @@
1
+ import { debug } from "../adapters/logger";
1
2
  export async function hasBeenRevalidated(key, tags, cacheEntry) {
2
3
  if (globalThis.openNextConfig.dangerous?.disableTagCache) {
3
4
  return false;
@@ -30,3 +31,34 @@ export function getTagsFromValue(value) {
30
31
  return [];
31
32
  }
32
33
  }
34
+ function getTagKey(tag) {
35
+ if (typeof tag === "string") {
36
+ return tag;
37
+ }
38
+ return JSON.stringify({
39
+ tag: tag.tag,
40
+ path: tag.path,
41
+ });
42
+ }
43
+ export async function writeTags(tags) {
44
+ const store = globalThis.__openNextAls.getStore();
45
+ debug("Writing tags", tags, store);
46
+ if (!store || globalThis.openNextConfig.dangerous?.disableTagCache) {
47
+ return;
48
+ }
49
+ const tagsToWrite = tags.filter((t) => {
50
+ const tagKey = getTagKey(t);
51
+ const shouldWrite = !store.writtenTags.has(tagKey);
52
+ // We preemptively add the tag to the writtenTags set
53
+ // to avoid writing the same tag multiple times in the same request
54
+ if (shouldWrite) {
55
+ store.writtenTags.add(tagKey);
56
+ }
57
+ return shouldWrite;
58
+ });
59
+ if (tagsToWrite.length === 0) {
60
+ return;
61
+ }
62
+ // Here we know that we have the correct type
63
+ await globalThis.tagCache.writeTags(tagsToWrite);
64
+ }
@@ -17,7 +17,8 @@ export declare class DetachedPromiseRunner {
17
17
  add<T>(promise: Promise<T>): void;
18
18
  await(): Promise<void>;
19
19
  }
20
- export declare function runWithOpenNextRequestContext<T>({ isISRRevalidation, waitUntil, }: {
20
+ export declare function runWithOpenNextRequestContext<T>({ isISRRevalidation, waitUntil, requestId, }: {
21
21
  isISRRevalidation: boolean;
22
22
  waitUntil?: WaitUntil;
23
+ requestId?: string;
23
24
  }, fn: () => Promise<T>): Promise<T>;
@@ -77,12 +77,13 @@ function provideNextAfterProvider() {
77
77
  globalThis[VERCEL_REQUEST_CONTEXT_SYMBOL] = nextAfterContext;
78
78
  }
79
79
  }
80
- export function runWithOpenNextRequestContext({ isISRRevalidation, waitUntil, }, fn) {
80
+ export function runWithOpenNextRequestContext({ isISRRevalidation, waitUntil, requestId = Math.random().toString(36), }, fn) {
81
81
  return globalThis.__openNextAls.run({
82
- requestId: Math.random().toString(36),
82
+ requestId,
83
83
  pendingPromiseRunner: new DetachedPromiseRunner(),
84
84
  isISRRevalidation,
85
85
  waitUntil,
86
+ writtenTags: new Set(),
86
87
  }, async () => {
87
88
  provideNextAfterProvider();
88
89
  let result;
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "access": "public"
4
4
  },
5
5
  "name": "@opennextjs/aws",
6
- "version": "3.6.6",
6
+ "version": "3.7.1",
7
7
  "bin": {
8
8
  "open-next": "./dist/index.js"
9
9
  },