@opennextjs/aws 4.1.4 → 4.1.5

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,13 +1,34 @@
1
1
  import { isStale, writeTags } from "../utils/cache";
2
2
  import { fromReadableStream, toReadableStream } from "../utils/stream";
3
3
  import { debug } from "./logger";
4
- const pendingWritePromiseMap = new Map();
4
+ /**
5
+ * In-flight writes are tracked per request, not per module.
6
+ *
7
+ * A module-scope map is per isolate. On a runtime that serves one request per
8
+ * sandbox that is the same as per request, but on a runtime that serves many
9
+ * concurrent requests from one isolate (e.g. Cloudflare Workers) a pending
10
+ * write promise created in request A would be handed to request B. If A's
11
+ * context is torn down before the write settles, B awaits a promise that never
12
+ * resolves, `.finally()` never runs, and the key stays poisoned for the life of
13
+ * the isolate.
14
+ *
15
+ * `RequestCache` is the per-request store the tag caches already use for this.
16
+ * Scoping the map to it keeps the read-coherence a `get` needs for a write in
17
+ * flight from the same render while making a cross-request share impossible.
18
+ * With no ALS store (e.g. at build time) the dedupe is skipped and `get` falls
19
+ * through to the incremental cache.
20
+ */
21
+ const PENDING_WRITES_CACHE_KEY = "composable-cache:pending-writes";
22
+ const getPendingWritePromiseMap = () => globalThis.__openNextAls
23
+ ?.getStore()
24
+ ?.requestCache?.getOrCreate(PENDING_WRITES_CACHE_KEY);
5
25
  export default {
6
26
  async get(cacheKey) {
7
27
  try {
8
28
  // We first check if we have a pending write for this cache key
9
29
  // If we do, we return the pending promise instead of fetching the cache
10
- if (pendingWritePromiseMap.has(cacheKey)) {
30
+ const pendingWritePromiseMap = getPendingWritePromiseMap();
31
+ if (pendingWritePromiseMap?.has(cacheKey)) {
11
32
  const stored = pendingWritePromiseMap.get(cacheKey);
12
33
  if (stored) {
13
34
  return stored.then((entry) => ({
@@ -69,9 +90,10 @@ export default {
69
90
  ...entry,
70
91
  value: await fromReadableStream(entry.value),
71
92
  }));
72
- pendingWritePromiseMap.set(cacheKey, promiseEntry);
93
+ const pendingWritePromiseMap = getPendingWritePromiseMap();
94
+ pendingWritePromiseMap?.set(cacheKey, promiseEntry);
73
95
  const entry = await promiseEntry.finally(() => {
74
- pendingWritePromiseMap.delete(cacheKey);
96
+ pendingWritePromiseMap?.delete(cacheKey);
75
97
  });
76
98
  await globalThis.incrementalCache.set(cacheKey, {
77
99
  ...entry,
@@ -15,5 +15,10 @@ export function buildNextjsApp(options) {
15
15
  cp.execSync(command, {
16
16
  stdio: "inherit",
17
17
  cwd: path.dirname(options.appPackageJsonPath),
18
+ // `setStandaloneBuildMode` mutates `process.env` right before this call.
19
+ // Node forwards those mutations to the child by default, but Bun resolves
20
+ // the child environment from a snapshot taken at startup unless `env` is
21
+ // passed explicitly, so `NEXT_PRIVATE_STANDALONE` never reaches the build.
22
+ env: process.env,
18
23
  });
19
24
  }
@@ -38,7 +38,9 @@ async function computeCacheControl(path, body, host, revalidate, lastModified, i
38
38
  // calculate age
39
39
  const age = Math.round((Date.now() - (lastModified ?? 0)) / 1000);
40
40
  const hash = (str) => createHash("md5").update(str).digest("hex");
41
- const etag = hash(body);
41
+ // RFC 9110 entity-tag is a quoted opaque-tag. CloudFront drops unquoted ETags
42
+ // when serving compressed objects.
43
+ const etag = `"${hash(body)}"`;
42
44
  if (revalidate === 0) {
43
45
  // This one should never happen
44
46
  return {
@@ -1,4 +1,5 @@
1
1
  import { FunctionsConfigManifest, MiddlewareManifest, NextConfig, PrerenderManifest, } from "../../adapters/config/index.js";
2
+ import { parseSetCookieHeader } from "../../http/util.js";
2
3
  import { ISR_HEADER, PRERENDER_REVALIDATE_HEADER } from "../../utils/cacheHeaders.js";
3
4
  import { emptyReadableStream } from "../../utils/stream.js";
4
5
  import { getQueryFromSearchParams } from "../../overrides/converters/utils.js";
@@ -112,7 +113,12 @@ export async function handleMiddleware(internalEvent, initialSearch, middlewareL
112
113
  }
113
114
  }
114
115
  });
115
- const setCookies = responseHeaders.getSetCookie();
116
+ // Next folds the cookies set through `cookies()` in the middleware into a single
117
+ // comma-joined `set-cookie` header, so each entry here may be compound.
118
+ // Next itself splits them back in `runMiddleware` (`next-server.ts`).
119
+ const setCookies = responseHeaders
120
+ .getSetCookie()
121
+ .flatMap((maybeCompoundCookie) => parseSetCookieHeader(maybeCompoundCookie));
116
122
  if (setCookies.length > 0) {
117
123
  resHeaders["set-cookie"] = setCookies;
118
124
  }
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "access": "public"
4
4
  },
5
5
  "name": "@opennextjs/aws",
6
- "version": "4.1.4",
6
+ "version": "4.1.5",
7
7
  "bin": {
8
8
  "open-next": "./dist/index.js"
9
9
  },