@opennextjs/aws 4.1.4 → 4.1.6
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 -0
- package/dist/adapters/cache.js +12 -9
- package/dist/adapters/composable-cache.js +26 -4
- package/dist/build/buildNextApp.js +5 -0
- package/dist/build/createServerBundle.js +1 -0
- package/dist/build/patch/patches/patchBackgroundRevalidation.d.ts +1 -1
- package/dist/build/patch/patches/patchBackgroundRevalidation.js +1 -1
- package/dist/core/routing/cacheInterceptor.js +30 -13
- package/dist/core/routing/middleware.js +7 -1
- package/dist/core/util.js +13 -0
- package/dist/overrides/incrementalCache/fs-dev.js +33 -8
- package/dist/overrides/queue/sqs-lite.js +25 -13
- package/dist/overrides/queue/sqs.js +14 -2
- package/dist/overrides/wrappers/aws-apigw-streaming.js +16 -1
- package/dist/overrides/wrappers/aws-lambda-streaming.js +16 -1
- package/dist/types/next-types.d.ts +4 -1
- package/package.json +3 -3
package/dist/adapters/cache.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export default class Cache {
|
|
|
11
11
|
getFetchCache(key: string, softTags?: string[], tags?: string[]): Promise<CacheHandlerValue | null>;
|
|
12
12
|
getIncrementalCache(key: string): Promise<CacheHandlerValue | null>;
|
|
13
13
|
set(key: string, data?: IncrementalCacheValue, ctx?: IncrementalCacheContext): Promise<void>;
|
|
14
|
+
private writeCache;
|
|
14
15
|
revalidateTag(tags: string | string[], durations?: {
|
|
15
16
|
expire?: number;
|
|
16
17
|
}): Promise<void>;
|
package/dist/adapters/cache.js
CHANGED
|
@@ -195,11 +195,18 @@ export default class Cache {
|
|
|
195
195
|
if (globalThis.openNextConfig.dangerous?.disableIncrementalCache) {
|
|
196
196
|
return;
|
|
197
197
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
198
|
+
const store = globalThis.__openNextAls.getStore();
|
|
199
|
+
const writePromise = this.writeCache(key, data, ctx);
|
|
200
|
+
// Next.js ignores the value returned by `set`, so the write is registered on the request's
|
|
201
|
+
// pending promise runner instead of being awaited. `FETCH` writes are awaited: Next.js runs
|
|
202
|
+
// them in a detached chain of its own that only covers the write while `set` waits for it.
|
|
203
|
+
if (data?.kind === "FETCH" || store === undefined) {
|
|
204
|
+
await writePromise;
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
store.pendingPromiseRunner.add(writePromise);
|
|
208
|
+
}
|
|
209
|
+
async writeCache(key, data, ctx) {
|
|
203
210
|
try {
|
|
204
211
|
if (data === null || data === undefined) {
|
|
205
212
|
await globalThis.incrementalCache.delete(key);
|
|
@@ -292,10 +299,6 @@ export default class Cache {
|
|
|
292
299
|
catch (e) {
|
|
293
300
|
error("Failed to set cache", e);
|
|
294
301
|
}
|
|
295
|
-
finally {
|
|
296
|
-
// We need to resolve the promise even if there was an error
|
|
297
|
-
detachedPromise?.resolve();
|
|
298
|
-
}
|
|
299
302
|
}
|
|
300
303
|
async revalidateTag(tags, durations) {
|
|
301
304
|
const config = globalThis.openNextConfig.dangerous;
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
93
|
+
const pendingWritePromiseMap = getPendingWritePromiseMap();
|
|
94
|
+
pendingWritePromiseMap?.set(cacheKey, promiseEntry);
|
|
73
95
|
const entry = await promiseEntry.finally(() => {
|
|
74
|
-
pendingWritePromiseMap
|
|
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
|
}
|
|
@@ -168,6 +168,7 @@ async function generateBundle(name, options, fnOptions, codeCustomization) {
|
|
|
168
168
|
? ["experimentalIncrementalCacheHandler"]
|
|
169
169
|
: ["stableIncrementalCache"]),
|
|
170
170
|
...(isAfter152 ? [] : ["composableCache"]),
|
|
171
|
+
...(isAfter152 ? [] : ["topLevelComposableCache"]),
|
|
171
172
|
],
|
|
172
173
|
}),
|
|
173
174
|
openNextResolvePlugin({
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const rule = "\nrule:\n kind: binary_expression\n all:\n - has:\n kind: unary_expression\n
|
|
1
|
+
export declare const rule = "\nrule:\n kind: binary_expression\n all:\n - has:\n kind: unary_expression\n pattern: \"!$ENTRY.isStale\"\n - has:\n kind: member_expression\n regex: \"context.isPrefetch\"\nfix:\n 'true'";
|
|
2
2
|
export declare const patchBackgroundRevalidation: {
|
|
3
3
|
name: string;
|
|
4
4
|
patches: {
|
|
@@ -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
|
-
|
|
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 {
|
|
@@ -102,18 +104,33 @@ function getBodyForAppRouter(event, cachedValue) {
|
|
|
102
104
|
if (cachedValue.type !== "app") {
|
|
103
105
|
throw new Error("getBodyForAppRouter called with non-app cache value");
|
|
104
106
|
}
|
|
105
|
-
const segmentHeader =
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
107
|
+
const segmentHeader = event.headers[NEXT_SEGMENT_PREFETCH_HEADER];
|
|
108
|
+
// A request from the client Segment Cache has to be answered with the segment it asked
|
|
109
|
+
// for. The full page payload is not a different-but-valid answer: the router never
|
|
110
|
+
// records the prefetch as satisfied and re-requests it forever.
|
|
111
|
+
// This mirrors how Next serves a cached app page - see the `segmentPrefetchHeader`
|
|
112
|
+
// branch of `packages/next/src/build/templates/app-page.ts`, which responds with the
|
|
113
|
+
// matching segment or an empty 404, and never with the full page.
|
|
114
|
+
//
|
|
115
|
+
// `experimental.prefetchInlining` is deliberately not consulted. Next normalizes every
|
|
116
|
+
// truthy value - including the 16.2+ default - into `{ maxSize, maxBundleSize }`, so it
|
|
117
|
+
// must never be read as a flag. More importantly it has no say here: inlining only
|
|
118
|
+
// changes *which* segments the build emits (an emitted segment then being a bundle that
|
|
119
|
+
// already holds its inlined ancestors), and `segmentData` holds exactly the segments
|
|
120
|
+
// that were emitted, which is exactly the set the router asks for.
|
|
121
|
+
if (typeof segmentHeader === "string" && cachedValue.segmentData) {
|
|
122
|
+
if (Object.hasOwn(cachedValue.segmentData, segmentHeader)) {
|
|
123
|
+
return {
|
|
124
|
+
body: cachedValue.segmentData[segmentHeader],
|
|
125
|
+
additionalHeaders: {
|
|
126
|
+
[NEXT_PRERENDER_HEADER]: "1",
|
|
127
|
+
[NEXT_POSTPONED_HEADER]: "2",
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
// The entry holds segments, but not this one. Next answers with an empty 404 here -
|
|
132
|
+
// let the server do that rather than serving a payload of a different shape.
|
|
133
|
+
return undefined;
|
|
117
134
|
}
|
|
118
135
|
// `rsc` is absent when the build collected neither a `.rsc` nor a `.prefetch.rsc` file for
|
|
119
136
|
// this entry - fallback shells, and postponed PPR routes on Next 16.2+, see `CachedFile`.
|
|
@@ -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
|
-
|
|
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/dist/core/util.js
CHANGED
|
@@ -33,6 +33,19 @@ const nextServer = new NextServer.default({
|
|
|
33
33
|
cacheHandler: cacheHandlerPath,
|
|
34
34
|
cacheMaxMemorySize: 0, // We need to disable memory cache
|
|
35
35
|
//#endOverride
|
|
36
|
+
// Next.js 16 promoted `cacheHandlers` out of `experimental` to a top-level
|
|
37
|
+
// config key. At runtime `route-module.js#loadCustomCacheHandlers` reads
|
|
38
|
+
// `nextConfig.cacheHandlers` (top-level) — the nested `experimental` copy
|
|
39
|
+
// below is ignored on Next >= 16, so the composable ("use cache") handler
|
|
40
|
+
// must also be registered here or Next falls back to its in-memory default
|
|
41
|
+
// and composable entries never reach the incremental cache override.
|
|
42
|
+
// Older Next versions ignore this unknown top-level key and read the
|
|
43
|
+
// `experimental` copy instead; setting both is safe across versions.
|
|
44
|
+
//#override topLevelComposableCache
|
|
45
|
+
cacheHandlers: {
|
|
46
|
+
default: composableCacheHandlerPath,
|
|
47
|
+
},
|
|
48
|
+
//#endOverride
|
|
36
49
|
experimental: {
|
|
37
50
|
...NextConfig.experimental,
|
|
38
51
|
// This uses the request.headers.host as the URL
|
|
@@ -2,24 +2,49 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { getMonorepoRelativePath } from "../../utils/normalize-path";
|
|
4
4
|
const buildId = process.env.OPEN_NEXT_BUILD_ID;
|
|
5
|
-
const basePath = path.join(getMonorepoRelativePath(),
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
const basePath = path.join(getMonorepoRelativePath(), "cache");
|
|
6
|
+
/**
|
|
7
|
+
* Resolves a cache entry using the same layout as the build's cache assets.
|
|
8
|
+
*
|
|
9
|
+
* @param key The cache entry key
|
|
10
|
+
* @param cacheType The cache namespace, defaulting to the page/route cache
|
|
11
|
+
* @returns The file path for the entry
|
|
12
|
+
*/
|
|
13
|
+
const getCacheKey = (key, cacheType = "cache") => {
|
|
14
|
+
return path.join(basePath, cacheType === "fetch" ? "__fetch" : "", `${buildId}`, cacheType === "fetch" ? key : `${key}.${cacheType}`);
|
|
8
15
|
};
|
|
9
16
|
const cache = {
|
|
10
17
|
name: "fs-dev",
|
|
11
|
-
|
|
12
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Reads an entry from the selected cache namespace.
|
|
20
|
+
*
|
|
21
|
+
* @param key The cache entry key
|
|
22
|
+
* @param cacheType The cache namespace, defaulting to the page/route cache
|
|
23
|
+
* @returns The cached value and its last modification time
|
|
24
|
+
* @throws When the entry cannot be read or contains invalid JSON
|
|
25
|
+
*/
|
|
26
|
+
get: async (key, cacheType) => {
|
|
27
|
+
const cacheKey = getCacheKey(key, cacheType);
|
|
28
|
+
const fileData = await fs.readFile(cacheKey, "utf-8");
|
|
13
29
|
const data = JSON.parse(fileData);
|
|
14
|
-
const { mtime } = await fs.stat(
|
|
30
|
+
const { mtime } = await fs.stat(cacheKey);
|
|
15
31
|
return {
|
|
16
32
|
value: data,
|
|
17
33
|
lastModified: mtime.getTime(),
|
|
18
34
|
};
|
|
19
35
|
},
|
|
20
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Writes an entry to the selected cache namespace.
|
|
38
|
+
*
|
|
39
|
+
* @param key The cache entry key
|
|
40
|
+
* @param value The value to persist
|
|
41
|
+
* @param cacheType The cache namespace, defaulting to the page/route cache
|
|
42
|
+
* @returns A promise that resolves when the entry has been written
|
|
43
|
+
* @throws When serialization or a file system operation fails
|
|
44
|
+
*/
|
|
45
|
+
set: async (key, value, cacheType) => {
|
|
21
46
|
const data = JSON.stringify(value);
|
|
22
|
-
const cacheKey = getCacheKey(key);
|
|
47
|
+
const cacheKey = getCacheKey(key, cacheType);
|
|
23
48
|
// We need to create the directory before writing the file
|
|
24
49
|
await fs.mkdir(path.dirname(cacheKey), { recursive: true });
|
|
25
50
|
await fs.writeFile(cacheKey, data);
|
|
@@ -29,21 +29,33 @@ const awsFetch = (body) => {
|
|
|
29
29
|
};
|
|
30
30
|
const queue = {
|
|
31
31
|
send: async ({ MessageBody, MessageDeduplicationId, MessageGroupId }) => {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
32
|
+
const send = (async () => {
|
|
33
|
+
try {
|
|
34
|
+
const { REVALIDATION_QUEUE_URL } = process.env;
|
|
35
|
+
const result = await awsFetch(JSON.stringify({
|
|
36
|
+
QueueUrl: REVALIDATION_QUEUE_URL,
|
|
37
|
+
MessageBody: JSON.stringify(MessageBody),
|
|
38
|
+
MessageDeduplicationId,
|
|
39
|
+
MessageGroupId,
|
|
40
|
+
}));
|
|
41
|
+
if (result.status !== 200) {
|
|
42
|
+
throw new RecoverableError(`Failed to send message: ${result.status}`);
|
|
43
|
+
}
|
|
42
44
|
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
error(e);
|
|
47
|
+
}
|
|
48
|
+
})();
|
|
49
|
+
// Enqueuing revalidation should not block the response. When the wrapper
|
|
50
|
+
// provides a `waitUntil`, hand the send to it and return immediately so the
|
|
51
|
+
// SQS round trip runs after the body is flushed; otherwise (edge, or a
|
|
52
|
+
// wrapper without `waitUntil`) fall back to awaiting it inline.
|
|
53
|
+
const waitUntil = globalThis.__openNextAls.getStore()?.waitUntil;
|
|
54
|
+
if (waitUntil) {
|
|
55
|
+
waitUntil(send);
|
|
56
|
+
return;
|
|
43
57
|
}
|
|
44
|
-
|
|
45
|
-
error(e);
|
|
46
|
-
}
|
|
58
|
+
await send;
|
|
47
59
|
},
|
|
48
60
|
name: "sqs",
|
|
49
61
|
};
|
|
@@ -8,12 +8,24 @@ const sqsClient = new SQSClient({
|
|
|
8
8
|
});
|
|
9
9
|
const queue = {
|
|
10
10
|
send: async ({ MessageBody, MessageDeduplicationId, MessageGroupId }) => {
|
|
11
|
-
|
|
11
|
+
const send = sqsClient
|
|
12
|
+
.send(new SendMessageCommand({
|
|
12
13
|
QueueUrl: REVALIDATION_QUEUE_URL,
|
|
13
14
|
MessageBody: JSON.stringify(MessageBody),
|
|
14
15
|
MessageDeduplicationId,
|
|
15
16
|
MessageGroupId,
|
|
16
|
-
}))
|
|
17
|
+
}))
|
|
18
|
+
.then(() => undefined);
|
|
19
|
+
// Enqueuing revalidation should not block the response. When the wrapper
|
|
20
|
+
// provides a `waitUntil`, hand the send to it and return immediately so the
|
|
21
|
+
// SQS round trip runs after the body is flushed; otherwise (edge, or a
|
|
22
|
+
// wrapper without `waitUntil`) fall back to awaiting it inline.
|
|
23
|
+
const waitUntil = globalThis.__openNextAls.getStore()?.waitUntil;
|
|
24
|
+
if (waitUntil) {
|
|
25
|
+
waitUntil(send);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
await send;
|
|
17
29
|
},
|
|
18
30
|
name: "sqs",
|
|
19
31
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Readable } from "node:stream";
|
|
2
2
|
import { debug } from "../../adapters/logger";
|
|
3
|
+
import { DetachedPromiseRunner } from "../../utils/promise";
|
|
3
4
|
function formatWarmerResponse(event) {
|
|
4
5
|
const result = new Promise((resolve) => {
|
|
5
6
|
setTimeout(() => {
|
|
@@ -42,6 +43,15 @@ const handler = async (handler, converter) => awslambda.streamifyResponse(async
|
|
|
42
43
|
debug(err);
|
|
43
44
|
responseStream.end();
|
|
44
45
|
});
|
|
46
|
+
// AWS provides no native `waitUntil`, so back it with the shared
|
|
47
|
+
// `DetachedPromiseRunner`, which tracks the deferred promises and awaits
|
|
48
|
+
// (and logs the failures of) them before the stream closes. This keeps
|
|
49
|
+
// the Lambda alive for deferred work (background revalidation, cache
|
|
50
|
+
// write-through) that starts after the response body is sent — matching
|
|
51
|
+
// the `waitUntil` the Cloudflare wrappers pass through, and enabling the
|
|
52
|
+
// `withWaitUntil` route preloading that otherwise falls back to `none`.
|
|
53
|
+
const promiseRunner = new DetachedPromiseRunner();
|
|
54
|
+
const waitUntil = (promise) => promiseRunner.add(promise);
|
|
45
55
|
const streamCreator = {
|
|
46
56
|
writeHeaders: (_prelude) => {
|
|
47
57
|
// No content-encoding header — API Gateway streaming does not
|
|
@@ -55,7 +65,10 @@ const handler = async (handler, converter) => awslambda.streamifyResponse(async
|
|
|
55
65
|
return responseStream;
|
|
56
66
|
},
|
|
57
67
|
};
|
|
58
|
-
const response = await handler(internalEvent, {
|
|
68
|
+
const response = await handler(internalEvent, {
|
|
69
|
+
streamCreator,
|
|
70
|
+
waitUntil,
|
|
71
|
+
});
|
|
59
72
|
const isUsingEdge = globalThis.isEdgeRuntime ?? false;
|
|
60
73
|
if (isUsingEdge) {
|
|
61
74
|
debug("Headers has not been set, we must be in the edge runtime");
|
|
@@ -66,6 +79,8 @@ const handler = async (handler, converter) => awslambda.streamifyResponse(async
|
|
|
66
79
|
});
|
|
67
80
|
Readable.fromWeb(response.body).pipe(stream);
|
|
68
81
|
}
|
|
82
|
+
// Keep the invocation alive until deferred `waitUntil` work settles.
|
|
83
|
+
await promiseRunner.await();
|
|
69
84
|
});
|
|
70
85
|
export default {
|
|
71
86
|
wrapper: handler,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Readable } from "node:stream";
|
|
2
2
|
import zlib from "node:zlib";
|
|
3
3
|
import { debug, error } from "../../adapters/logger";
|
|
4
|
+
import { DetachedPromiseRunner } from "../../utils/promise";
|
|
4
5
|
function formatWarmerResponse(event) {
|
|
5
6
|
const result = new Promise((resolve) => {
|
|
6
7
|
setTimeout(() => {
|
|
@@ -64,7 +65,19 @@ const handler = async (handler, converter) => awslambda.streamifyResponse(async
|
|
|
64
65
|
return compressedStream ?? responseStream;
|
|
65
66
|
},
|
|
66
67
|
};
|
|
67
|
-
|
|
68
|
+
// AWS provides no native `waitUntil`, so back it with the shared
|
|
69
|
+
// `DetachedPromiseRunner`, which tracks the deferred promises and awaits
|
|
70
|
+
// (and logs the failures of) them before the stream closes. This keeps
|
|
71
|
+
// the Lambda alive for deferred work (background revalidation, cache
|
|
72
|
+
// write-through) that starts after the response body is sent — matching
|
|
73
|
+
// the `waitUntil` the Cloudflare wrappers pass through, and enabling the
|
|
74
|
+
// `withWaitUntil` route preloading that otherwise falls back to `none`.
|
|
75
|
+
const promiseRunner = new DetachedPromiseRunner();
|
|
76
|
+
const waitUntil = (promise) => promiseRunner.add(promise);
|
|
77
|
+
const response = await handler(internalEvent, {
|
|
78
|
+
streamCreator,
|
|
79
|
+
waitUntil,
|
|
80
|
+
});
|
|
68
81
|
const isUsingEdge = globalThis.isEdgeRuntime ?? false;
|
|
69
82
|
if (isUsingEdge) {
|
|
70
83
|
debug("Headers has not been set, we must be in the edge runtime");
|
|
@@ -75,6 +88,8 @@ const handler = async (handler, converter) => awslambda.streamifyResponse(async
|
|
|
75
88
|
});
|
|
76
89
|
Readable.fromWeb(response.body).pipe(stream);
|
|
77
90
|
}
|
|
91
|
+
// Keep the invocation alive until deferred `waitUntil` work settles.
|
|
92
|
+
await promiseRunner.await();
|
|
78
93
|
// return converter.convertTo(response);
|
|
79
94
|
});
|
|
80
95
|
export default {
|
|
@@ -73,7 +73,10 @@ export interface NextConfig {
|
|
|
73
73
|
serverActions?: boolean;
|
|
74
74
|
appDir?: boolean;
|
|
75
75
|
optimizeCss?: boolean;
|
|
76
|
-
prefetchInlining?: boolean
|
|
76
|
+
prefetchInlining?: boolean | {
|
|
77
|
+
maxSize: number;
|
|
78
|
+
maxBundleSize: number;
|
|
79
|
+
};
|
|
77
80
|
};
|
|
78
81
|
images: ImageConfig;
|
|
79
82
|
poweredByHeader?: boolean;
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"access": "public"
|
|
4
4
|
},
|
|
5
5
|
"name": "@opennextjs/aws",
|
|
6
|
-
"version": "4.1.
|
|
6
|
+
"version": "4.1.6",
|
|
7
7
|
"bin": {
|
|
8
8
|
"open-next": "./dist/index.js"
|
|
9
9
|
},
|
|
@@ -56,12 +56,12 @@
|
|
|
56
56
|
"@types/express": "5.0.0",
|
|
57
57
|
"@types/node": "20.17.6",
|
|
58
58
|
"concurrently": "^9.2.1",
|
|
59
|
-
"next": "16.3.
|
|
59
|
+
"next": "16.3.6",
|
|
60
60
|
"tsc-alias": "^1.8.16",
|
|
61
61
|
"typescript": "5.9.3"
|
|
62
62
|
},
|
|
63
63
|
"peerDependencies": {
|
|
64
|
-
"next": ">=15.5.
|
|
64
|
+
"next": ">=15.5.26 <16 || >=16.3.6"
|
|
65
65
|
},
|
|
66
66
|
"bugs": {
|
|
67
67
|
"url": "https://github.com/opennextjs/opennextjs-aws/issues"
|