@opennextjs/aws 4.1.5 → 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/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 +27 -12
- 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;
|
|
@@ -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: {
|
|
@@ -104,18 +104,33 @@ function getBodyForAppRouter(event, cachedValue) {
|
|
|
104
104
|
if (cachedValue.type !== "app") {
|
|
105
105
|
throw new Error("getBodyForAppRouter called with non-app cache value");
|
|
106
106
|
}
|
|
107
|
-
const segmentHeader =
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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;
|
|
119
134
|
}
|
|
120
135
|
// `rsc` is absent when the build collected neither a `.rsc` nor a `.prefetch.rsc` file for
|
|
121
136
|
// this entry - fallback shells, and postponed PPR routes on Next 16.2+, see `CachedFile`.
|
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"
|