@opennextjs/aws 3.9.16 → 3.10.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.
- package/dist/adapters/cache.d.ts +3 -1
- package/dist/adapters/cache.js +75 -17
- package/dist/adapters/composable-cache.d.ts +12 -0
- package/dist/adapters/composable-cache.js +82 -2
- package/dist/adapters/dynamo-provider.js +2 -0
- package/dist/adapters/revalidate.js +2 -1
- package/dist/build/compileCache.js +2 -3
- package/dist/build/copyTracedFiles.d.ts +1 -0
- package/dist/build/copyTracedFiles.js +20 -0
- package/dist/build/createAssets.js +4 -1
- package/dist/build/helper.d.ts +2 -10
- package/dist/build/helper.js +1 -55
- package/dist/build/patch/patches/patchNextServer.d.ts +1 -0
- package/dist/build/patch/patches/patchNextServer.js +19 -0
- package/dist/core/routing/cacheInterceptor.js +2 -1
- package/dist/core/routing/i18n/index.js +4 -2
- package/dist/overrides/proxyExternalRequest/fetch.js +10 -1
- package/dist/overrides/tagCache/dummy.js +3 -0
- package/dist/overrides/tagCache/dynamodb-lite.js +94 -8
- package/dist/overrides/tagCache/dynamodb-nextMode.d.ts +2 -1
- package/dist/overrides/tagCache/dynamodb-nextMode.js +112 -27
- package/dist/overrides/tagCache/dynamodb.js +104 -22
- package/dist/overrides/tagCache/fs-dev-nextMode.d.ts +2 -1
- package/dist/overrides/tagCache/fs-dev-nextMode.js +37 -6
- package/dist/overrides/tagCache/fs-dev.js +40 -8
- package/dist/types/cache.d.ts +20 -0
- package/dist/types/global.d.ts +5 -2
- package/dist/types/next-types.d.ts +1 -0
- package/dist/types/overrides.d.ts +42 -1
- package/dist/utils/cache.d.ts +22 -2
- package/dist/utils/cache.js +44 -4
- package/dist/utils/promise.js +2 -0
- package/dist/utils/requestCache.d.ts +20 -0
- package/dist/utils/requestCache.js +27 -0
- package/dist/utils/semver.d.ts +10 -0
- package/dist/utils/semver.js +55 -0
- package/package.json +2 -2
|
@@ -39,12 +39,15 @@ function buildDynamoKey(key) {
|
|
|
39
39
|
// this could transform some fetch cache key into a valid path
|
|
40
40
|
return path.posix.join(NEXT_BUILD_ID ?? "", key);
|
|
41
41
|
}
|
|
42
|
-
function buildDynamoObject(path, tags, revalidatedAt) {
|
|
43
|
-
|
|
42
|
+
function buildDynamoObject(path, tags, revalidatedAt, stale, expire) {
|
|
43
|
+
const obj = {
|
|
44
44
|
path: { S: buildDynamoKey(path) },
|
|
45
45
|
tag: { S: buildDynamoKey(tags) },
|
|
46
46
|
revalidatedAt: { N: `${revalidatedAt ?? Date.now()}` },
|
|
47
|
+
...(stale !== undefined ? { stale: { N: `${stale}` } } : {}),
|
|
48
|
+
...(expire !== undefined ? { expire: { N: `${expire}` } } : {}),
|
|
47
49
|
};
|
|
50
|
+
return obj;
|
|
48
51
|
}
|
|
49
52
|
const tagCache = {
|
|
50
53
|
mode: "original",
|
|
@@ -54,6 +57,11 @@ const tagCache = {
|
|
|
54
57
|
return [];
|
|
55
58
|
}
|
|
56
59
|
const { CACHE_DYNAMO_TABLE, NEXT_BUILD_ID } = process.env;
|
|
60
|
+
const store = globalThis.__openNextAls.getStore();
|
|
61
|
+
const cache = store?.requestCache.getOrCreate("dynamoDb:getByPath");
|
|
62
|
+
if (cache?.has(path)) {
|
|
63
|
+
return cache.get(path);
|
|
64
|
+
}
|
|
57
65
|
const result = await awsFetch(JSON.stringify({
|
|
58
66
|
TableName: CACHE_DYNAMO_TABLE,
|
|
59
67
|
IndexName: "revalidate",
|
|
@@ -72,7 +80,9 @@ const tagCache = {
|
|
|
72
80
|
const tags = Items?.map((item) => item.tag.S ?? "") ?? [];
|
|
73
81
|
debug("tags for path", path, tags);
|
|
74
82
|
// We need to remove the buildId from the path
|
|
75
|
-
|
|
83
|
+
const resultTags = tags.map((tag) => tag.replace(`${NEXT_BUILD_ID}/`, ""));
|
|
84
|
+
cache?.set(path, resultTags);
|
|
85
|
+
return resultTags;
|
|
76
86
|
}
|
|
77
87
|
catch (e) {
|
|
78
88
|
error("Failed to get tags by path", e);
|
|
@@ -85,6 +95,11 @@ const tagCache = {
|
|
|
85
95
|
return [];
|
|
86
96
|
}
|
|
87
97
|
const { CACHE_DYNAMO_TABLE, NEXT_BUILD_ID } = process.env;
|
|
98
|
+
const store = globalThis.__openNextAls.getStore();
|
|
99
|
+
const cache = store?.requestCache.getOrCreate("dynamoDb:getByTag");
|
|
100
|
+
if (cache?.has(tag)) {
|
|
101
|
+
return cache.get(tag);
|
|
102
|
+
}
|
|
88
103
|
const result = await awsFetch(JSON.stringify({
|
|
89
104
|
TableName: CACHE_DYNAMO_TABLE,
|
|
90
105
|
KeyConditionExpression: "#tag = :tag",
|
|
@@ -99,9 +114,10 @@ const tagCache = {
|
|
|
99
114
|
throw new RecoverableError(`Failed to get by tag: ${result.status}`);
|
|
100
115
|
}
|
|
101
116
|
const { Items } = (await result.json());
|
|
102
|
-
return (
|
|
103
117
|
// We need to remove the buildId from the path
|
|
104
|
-
Items?.map(({ path: { S: key } }) => key?.replace(`${NEXT_BUILD_ID}/`, "") ?? "") ?? []
|
|
118
|
+
const paths = Items?.map(({ path: { S: key } }) => key?.replace(`${NEXT_BUILD_ID}/`, "") ?? "") ?? [];
|
|
119
|
+
cache?.set(tag, paths);
|
|
120
|
+
return paths;
|
|
105
121
|
}
|
|
106
122
|
catch (e) {
|
|
107
123
|
error("Failed to get by tag", e);
|
|
@@ -114,6 +130,12 @@ const tagCache = {
|
|
|
114
130
|
return lastModified ?? Date.now();
|
|
115
131
|
}
|
|
116
132
|
const { CACHE_DYNAMO_TABLE } = process.env;
|
|
133
|
+
const store = globalThis.__openNextAls.getStore();
|
|
134
|
+
const cache = store?.requestCache.getOrCreate("dynamoDb:getLastModified");
|
|
135
|
+
const cacheKey = `${key}:${lastModified ?? 0}`;
|
|
136
|
+
if (cache?.has(cacheKey)) {
|
|
137
|
+
return cache.get(cacheKey);
|
|
138
|
+
}
|
|
117
139
|
const result = await awsFetch(JSON.stringify({
|
|
118
140
|
TableName: CACHE_DYNAMO_TABLE,
|
|
119
141
|
IndexName: "revalidate",
|
|
@@ -132,14 +154,78 @@ const tagCache = {
|
|
|
132
154
|
}
|
|
133
155
|
const revalidatedTags = (await result.json()).Items ?? [];
|
|
134
156
|
debug("revalidatedTags", revalidatedTags);
|
|
135
|
-
//
|
|
136
|
-
|
|
157
|
+
// Check if any tag has expired
|
|
158
|
+
const now = Date.now();
|
|
159
|
+
const hasExpiredTag = revalidatedTags.some((item) => {
|
|
160
|
+
if (item.expire?.N) {
|
|
161
|
+
const expiry = Number.parseInt(item.expire.N);
|
|
162
|
+
return expiry <= now && expiry > (lastModified ?? 0);
|
|
163
|
+
}
|
|
164
|
+
return false;
|
|
165
|
+
});
|
|
166
|
+
// Exclude expired tags from the revalidated count — they are handled
|
|
167
|
+
// separately via hasExpiredTag above.
|
|
168
|
+
const nonExpiredRevalidatedTags = revalidatedTags.filter((item) => {
|
|
169
|
+
if (item.expire?.N) {
|
|
170
|
+
return (Number.parseInt(item.expire.N) ===
|
|
171
|
+
Number.parseInt(item.revalidatedAt.N));
|
|
172
|
+
}
|
|
173
|
+
return true;
|
|
174
|
+
});
|
|
175
|
+
// If we have revalidated tags or expired tags we return -1 to force revalidation
|
|
176
|
+
const resultValue = nonExpiredRevalidatedTags.length > 0 || hasExpiredTag
|
|
177
|
+
? -1
|
|
178
|
+
: (lastModified ?? Date.now());
|
|
179
|
+
cache?.set(cacheKey, resultValue);
|
|
180
|
+
return resultValue;
|
|
137
181
|
}
|
|
138
182
|
catch (e) {
|
|
139
183
|
error("Failed to get revalidated tags", e);
|
|
140
184
|
return lastModified ?? Date.now();
|
|
141
185
|
}
|
|
142
186
|
},
|
|
187
|
+
async isStale(key, lastModified) {
|
|
188
|
+
try {
|
|
189
|
+
if (globalThis.openNextConfig.dangerous?.disableTagCache) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
const { CACHE_DYNAMO_TABLE } = process.env;
|
|
193
|
+
const store = globalThis.__openNextAls.getStore();
|
|
194
|
+
const itemsCache = store?.requestCache.getOrCreate("dynamoDb:revalidateQueryItems");
|
|
195
|
+
const cacheKey = `${key}:${lastModified ?? 0}`;
|
|
196
|
+
let items;
|
|
197
|
+
if (itemsCache?.has(cacheKey)) {
|
|
198
|
+
items = itemsCache.get(cacheKey);
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
// We can reuse the same query as getLastModified since it already checks for revalidatedAt > lastModified as revalidatedAt and stale have the same value
|
|
202
|
+
const result = await awsFetch(JSON.stringify({
|
|
203
|
+
TableName: CACHE_DYNAMO_TABLE,
|
|
204
|
+
IndexName: "revalidate",
|
|
205
|
+
KeyConditionExpression: "#key = :key AND #revalidatedAt > :lastModified",
|
|
206
|
+
ExpressionAttributeNames: {
|
|
207
|
+
"#key": "path",
|
|
208
|
+
"#revalidatedAt": "revalidatedAt",
|
|
209
|
+
},
|
|
210
|
+
ExpressionAttributeValues: {
|
|
211
|
+
":key": { S: buildDynamoKey(key) },
|
|
212
|
+
":lastModified": { N: String(lastModified ?? 0) },
|
|
213
|
+
},
|
|
214
|
+
}));
|
|
215
|
+
if (result.status !== 200) {
|
|
216
|
+
throw new RecoverableError(`Failed to check stale tags: ${result.status}`);
|
|
217
|
+
}
|
|
218
|
+
items = (await result.json()).Items ?? [];
|
|
219
|
+
itemsCache?.set(cacheKey, items);
|
|
220
|
+
}
|
|
221
|
+
debug("isStale items", key, items);
|
|
222
|
+
return items.length > 0;
|
|
223
|
+
}
|
|
224
|
+
catch (e) {
|
|
225
|
+
error("Failed to check stale tags", e);
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
},
|
|
143
229
|
async writeTags(tags) {
|
|
144
230
|
try {
|
|
145
231
|
const { CACHE_DYNAMO_TABLE } = process.env;
|
|
@@ -151,7 +237,7 @@ const tagCache = {
|
|
|
151
237
|
[CACHE_DYNAMO_TABLE ?? ""]: Items.map((Item) => ({
|
|
152
238
|
PutRequest: {
|
|
153
239
|
Item: {
|
|
154
|
-
...buildDynamoObject(Item.path, Item.tag, Item.revalidatedAt),
|
|
240
|
+
...buildDynamoObject(Item.path, Item.tag, Item.revalidatedAt, Item.stale, Item.expire),
|
|
155
241
|
},
|
|
156
242
|
},
|
|
157
243
|
})),
|
|
@@ -3,6 +3,7 @@ declare const _default: {
|
|
|
3
3
|
mode: "nextMode";
|
|
4
4
|
getLastRevalidated: (tags: string[]) => Promise<number>;
|
|
5
5
|
hasBeenRevalidated: (tags: string[], lastModified?: number) => Promise<boolean>;
|
|
6
|
-
|
|
6
|
+
isStale: (tags: string[], lastModified?: number) => Promise<boolean>;
|
|
7
|
+
writeTags: (tags: import("../../types/overrides").NextModeTagCacheWriteInput[]) => Promise<void>;
|
|
7
8
|
};
|
|
8
9
|
export default _default;
|
|
@@ -41,14 +41,70 @@ function buildDynamoKey(key) {
|
|
|
41
41
|
// We use the same key for both path and tag
|
|
42
42
|
// That's mostly for compatibility reason so that it's easier to use this with existing infra
|
|
43
43
|
// FIXME: Allow a simpler object without an unnecessary path key
|
|
44
|
-
function buildDynamoObject(tag, revalidatedAt) {
|
|
45
|
-
|
|
44
|
+
function buildDynamoObject(tag, revalidatedAt, stale, expire) {
|
|
45
|
+
const obj = {
|
|
46
46
|
path: { S: buildDynamoKey(tag) },
|
|
47
47
|
tag: { S: buildDynamoKey(tag) },
|
|
48
48
|
revalidatedAt: { N: `${revalidatedAt ?? Date.now()}` },
|
|
49
|
+
...(stale !== undefined ? { stale: { N: `${stale}` } } : {}),
|
|
50
|
+
...(expire !== undefined ? { expire: { N: `${expire}` } } : {}),
|
|
49
51
|
};
|
|
52
|
+
return obj;
|
|
50
53
|
}
|
|
51
54
|
// This implementation does not support automatic invalidation of paths by the cdn
|
|
55
|
+
/**
|
|
56
|
+
* Checks the items cache for each tag. Returns tags not yet cached and whether
|
|
57
|
+
* a positive result was already found among the cached ones.
|
|
58
|
+
*/
|
|
59
|
+
function checkItemsCache(tags, itemsCache, compute) {
|
|
60
|
+
const uncachedTags = [];
|
|
61
|
+
let hasMatch = false;
|
|
62
|
+
for (const tag of tags) {
|
|
63
|
+
if (itemsCache?.has(tag)) {
|
|
64
|
+
if (compute(itemsCache.get(tag)))
|
|
65
|
+
hasMatch = true;
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
uncachedTags.push(tag);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return { uncachedTags, hasMatch };
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Fetches uncached tags from DynamoDB via BatchGetItem, populates the items
|
|
75
|
+
* cache (storing null for absent tags), and returns whether any tag matched.
|
|
76
|
+
*/
|
|
77
|
+
async function fetchAndCacheItems(uncachedTags, itemsCache, compute) {
|
|
78
|
+
const { CACHE_DYNAMO_TABLE } = process.env;
|
|
79
|
+
const response = await awsFetch(JSON.stringify({
|
|
80
|
+
RequestItems: {
|
|
81
|
+
[CACHE_DYNAMO_TABLE ?? ""]: {
|
|
82
|
+
Keys: uncachedTags.map((tag) => ({
|
|
83
|
+
path: { S: buildDynamoKey(tag) },
|
|
84
|
+
tag: { S: buildDynamoKey(tag) },
|
|
85
|
+
})),
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
}), "query");
|
|
89
|
+
if (response.status !== 200) {
|
|
90
|
+
throw new RecoverableError(`Failed to query dynamo item: ${response.status}`);
|
|
91
|
+
}
|
|
92
|
+
const { Responses } = await response.json();
|
|
93
|
+
const responseItems = Responses?.[CACHE_DYNAMO_TABLE ?? ""] ?? [];
|
|
94
|
+
// Build a lookup map: DynamoDB key → item
|
|
95
|
+
const responseByKey = new Map();
|
|
96
|
+
for (const item of responseItems) {
|
|
97
|
+
responseByKey.set(item.tag.S, item);
|
|
98
|
+
}
|
|
99
|
+
let hasMatch = false;
|
|
100
|
+
for (const tag of uncachedTags) {
|
|
101
|
+
const item = responseByKey.get(buildDynamoKey(tag)) ?? null;
|
|
102
|
+
itemsCache?.set(tag, item);
|
|
103
|
+
if (compute(item))
|
|
104
|
+
hasMatch = true;
|
|
105
|
+
}
|
|
106
|
+
return hasMatch;
|
|
107
|
+
}
|
|
52
108
|
export default {
|
|
53
109
|
name: "ddb-nextMode",
|
|
54
110
|
mode: "nextMode",
|
|
@@ -63,30 +119,54 @@ export default {
|
|
|
63
119
|
if (tags.length > 100) {
|
|
64
120
|
throw new RecoverableError("Cannot query more than 100 tags at once. You should not be using this tagCache implementation for this amount of tags");
|
|
65
121
|
}
|
|
66
|
-
const
|
|
122
|
+
const store = globalThis.__openNextAls.getStore();
|
|
123
|
+
const itemsCache = store?.requestCache.getOrCreate("ddb-nextMode:tagItems");
|
|
124
|
+
const now = Date.now();
|
|
125
|
+
const compute = (item) => {
|
|
126
|
+
if (!item)
|
|
127
|
+
return false;
|
|
128
|
+
if (item.expire?.N) {
|
|
129
|
+
const expiry = Number.parseInt(item.expire.N);
|
|
130
|
+
if (expiry <= now && expiry > (lastModified ?? 0))
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
return Number.parseInt(item.revalidatedAt.N) > (lastModified ?? 0);
|
|
134
|
+
};
|
|
135
|
+
const { uncachedTags, hasMatch } = checkItemsCache(tags, itemsCache, compute);
|
|
136
|
+
if (hasMatch)
|
|
137
|
+
return true;
|
|
138
|
+
if (uncachedTags.length === 0)
|
|
139
|
+
return false;
|
|
67
140
|
// It's unlikely that we will have more than 100 items to query
|
|
68
141
|
// If that's the case, you should not use this tagCache implementation
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
},
|
|
77
|
-
},
|
|
78
|
-
}), "query");
|
|
79
|
-
if (response.status !== 200) {
|
|
80
|
-
throw new RecoverableError(`Failed to query dynamo item: ${response.status}`);
|
|
142
|
+
const result = await fetchAndCacheItems(uncachedTags, itemsCache, compute);
|
|
143
|
+
debug("retrieved tags for hasBeenRevalidated", tags);
|
|
144
|
+
return result;
|
|
145
|
+
},
|
|
146
|
+
isStale: async (tags, lastModified) => {
|
|
147
|
+
if (globalThis.openNextConfig.dangerous?.disableTagCache) {
|
|
148
|
+
return false;
|
|
81
149
|
}
|
|
82
|
-
|
|
83
|
-
const { Responses } = await response.json();
|
|
84
|
-
if (!Responses) {
|
|
150
|
+
if (tags.length === 0)
|
|
85
151
|
return false;
|
|
152
|
+
if (tags.length > 100) {
|
|
153
|
+
throw new RecoverableError("Cannot query more than 100 tags at once. You should not be using this tagCache implementation for this amount of tags");
|
|
86
154
|
}
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
155
|
+
const store = globalThis.__openNextAls.getStore();
|
|
156
|
+
const itemsCache = store?.requestCache.getOrCreate("ddb-nextMode:tagItems");
|
|
157
|
+
const compute = (item) => {
|
|
158
|
+
if (!item?.stale?.N)
|
|
159
|
+
return false;
|
|
160
|
+
return Number.parseInt(item.stale.N) > (lastModified ?? 0);
|
|
161
|
+
};
|
|
162
|
+
const { uncachedTags, hasMatch } = checkItemsCache(tags, itemsCache, compute);
|
|
163
|
+
if (hasMatch)
|
|
164
|
+
return true;
|
|
165
|
+
if (uncachedTags.length === 0)
|
|
166
|
+
return false;
|
|
167
|
+
const result = await fetchAndCacheItems(uncachedTags, itemsCache, compute);
|
|
168
|
+
debug("isStale result:", result);
|
|
169
|
+
return result;
|
|
90
170
|
},
|
|
91
171
|
writeTags: async (tags) => {
|
|
92
172
|
try {
|
|
@@ -96,13 +176,18 @@ export default {
|
|
|
96
176
|
}
|
|
97
177
|
const dataChunks = chunk(tags, MAX_DYNAMO_BATCH_WRITE_ITEM_COUNT).map((Items) => ({
|
|
98
178
|
RequestItems: {
|
|
99
|
-
[CACHE_DYNAMO_TABLE ?? ""]: Items.map((tag) =>
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
179
|
+
[CACHE_DYNAMO_TABLE ?? ""]: Items.map((tag) => {
|
|
180
|
+
const tagStr = typeof tag === "string" ? tag : tag.tag;
|
|
181
|
+
const stale = typeof tag === "string" ? undefined : tag.stale;
|
|
182
|
+
const expiry = typeof tag === "string" ? undefined : tag.expire;
|
|
183
|
+
return {
|
|
184
|
+
PutRequest: {
|
|
185
|
+
Item: {
|
|
186
|
+
...buildDynamoObject(tagStr, undefined, stale, expiry),
|
|
187
|
+
},
|
|
103
188
|
},
|
|
104
|
-
}
|
|
105
|
-
})
|
|
189
|
+
};
|
|
190
|
+
}),
|
|
106
191
|
},
|
|
107
192
|
}));
|
|
108
193
|
const toInsert = chunk(dataChunks, getDynamoBatchWriteCommandConcurrency());
|
|
@@ -17,12 +17,15 @@ function buildDynamoKey(key) {
|
|
|
17
17
|
// this could transform some fetch cache key into a valid path
|
|
18
18
|
return path.posix.join(NEXT_BUILD_ID ?? "", key);
|
|
19
19
|
}
|
|
20
|
-
function buildDynamoObject(path, tags, revalidatedAt) {
|
|
21
|
-
|
|
20
|
+
function buildDynamoObject(path, tags, revalidatedAt, stale, expire) {
|
|
21
|
+
const obj = {
|
|
22
22
|
path: { S: buildDynamoKey(path) },
|
|
23
23
|
tag: { S: buildDynamoKey(tags) },
|
|
24
24
|
revalidatedAt: { N: `${revalidatedAt ?? Date.now()}` },
|
|
25
|
+
...(stale !== undefined ? { stale: { N: `${stale}` } } : {}),
|
|
26
|
+
...(expire !== undefined ? { expire: { N: `${expire}` } } : {}),
|
|
25
27
|
};
|
|
28
|
+
return obj;
|
|
26
29
|
}
|
|
27
30
|
const tagCache = {
|
|
28
31
|
mode: "original",
|
|
@@ -31,6 +34,11 @@ const tagCache = {
|
|
|
31
34
|
if (globalThis.openNextConfig.dangerous?.disableTagCache) {
|
|
32
35
|
return [];
|
|
33
36
|
}
|
|
37
|
+
const store = globalThis.__openNextAls.getStore();
|
|
38
|
+
const cache = store?.requestCache.getOrCreate("dynamoDb:getByPath");
|
|
39
|
+
if (cache?.has(path)) {
|
|
40
|
+
return cache.get(path);
|
|
41
|
+
}
|
|
34
42
|
const result = await dynamoClient.send(new QueryCommand({
|
|
35
43
|
TableName: CACHE_DYNAMO_TABLE,
|
|
36
44
|
IndexName: "revalidate",
|
|
@@ -45,7 +53,9 @@ const tagCache = {
|
|
|
45
53
|
const tags = result.Items?.map((item) => item.tag.S ?? "") ?? [];
|
|
46
54
|
debug("tags for path", path, tags);
|
|
47
55
|
// We need to remove the buildId from the path
|
|
48
|
-
|
|
56
|
+
const resultTags = tags.map((tag) => tag.replace(`${NEXT_BUILD_ID}/`, ""));
|
|
57
|
+
cache?.set(path, resultTags);
|
|
58
|
+
return resultTags;
|
|
49
59
|
}
|
|
50
60
|
catch (e) {
|
|
51
61
|
error("Failed to get tags by path", e);
|
|
@@ -57,6 +67,11 @@ const tagCache = {
|
|
|
57
67
|
if (globalThis.openNextConfig.dangerous?.disableTagCache) {
|
|
58
68
|
return [];
|
|
59
69
|
}
|
|
70
|
+
const store = globalThis.__openNextAls.getStore();
|
|
71
|
+
const cache = store?.requestCache.getOrCreate("dynamoDb:getByTag");
|
|
72
|
+
if (cache?.has(tag)) {
|
|
73
|
+
return cache.get(tag);
|
|
74
|
+
}
|
|
60
75
|
const { Items } = await dynamoClient.send(new QueryCommand({
|
|
61
76
|
TableName: CACHE_DYNAMO_TABLE,
|
|
62
77
|
KeyConditionExpression: "#tag = :tag",
|
|
@@ -67,9 +82,10 @@ const tagCache = {
|
|
|
67
82
|
":tag": { S: buildDynamoKey(tag) },
|
|
68
83
|
},
|
|
69
84
|
}));
|
|
70
|
-
return (
|
|
71
85
|
// We need to remove the buildId from the path
|
|
72
|
-
Items?.map(({ path: { S: key } }) => key?.replace(`${NEXT_BUILD_ID}/`, "") ?? "") ?? []
|
|
86
|
+
const paths = Items?.map(({ path: { S: key } }) => key?.replace(`${NEXT_BUILD_ID}/`, "") ?? "") ?? [];
|
|
87
|
+
cache?.set(tag, paths);
|
|
88
|
+
return paths;
|
|
73
89
|
}
|
|
74
90
|
catch (e) {
|
|
75
91
|
error("Failed to get by tag", e);
|
|
@@ -81,29 +97,95 @@ const tagCache = {
|
|
|
81
97
|
if (globalThis.openNextConfig.dangerous?.disableTagCache) {
|
|
82
98
|
return lastModified ?? Date.now();
|
|
83
99
|
}
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
100
|
+
const store = globalThis.__openNextAls.getStore();
|
|
101
|
+
const itemsCache = store?.requestCache.getOrCreate("dynamoDb:revalidateQueryItems");
|
|
102
|
+
const cacheKey = `${key}:${lastModified ?? 0}`;
|
|
103
|
+
let revalidatedTags;
|
|
104
|
+
if (itemsCache?.has(cacheKey)) {
|
|
105
|
+
revalidatedTags = itemsCache.get(cacheKey);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
const result = await dynamoClient.send(new QueryCommand({
|
|
109
|
+
TableName: CACHE_DYNAMO_TABLE,
|
|
110
|
+
IndexName: "revalidate",
|
|
111
|
+
KeyConditionExpression: "#key = :key AND #revalidatedAt > :lastModified",
|
|
112
|
+
ExpressionAttributeNames: {
|
|
113
|
+
"#key": "path",
|
|
114
|
+
"#revalidatedAt": "revalidatedAt",
|
|
115
|
+
},
|
|
116
|
+
ExpressionAttributeValues: {
|
|
117
|
+
":key": { S: buildDynamoKey(key) },
|
|
118
|
+
":lastModified": { N: String(lastModified ?? 0) },
|
|
119
|
+
},
|
|
120
|
+
}));
|
|
121
|
+
revalidatedTags = result.Items ?? [];
|
|
122
|
+
itemsCache?.set(cacheKey, revalidatedTags);
|
|
123
|
+
}
|
|
98
124
|
debug("revalidatedTags", revalidatedTags);
|
|
99
|
-
//
|
|
100
|
-
|
|
125
|
+
// Check if any tag has expired
|
|
126
|
+
const now = Date.now();
|
|
127
|
+
const hasExpiredTag = revalidatedTags.some((item) => {
|
|
128
|
+
if (item.expire?.N) {
|
|
129
|
+
const expiry = Number.parseInt(item.expire.N);
|
|
130
|
+
return expiry <= now && expiry > (lastModified ?? 0);
|
|
131
|
+
}
|
|
132
|
+
return false;
|
|
133
|
+
});
|
|
134
|
+
// Exclude expired tags from the revalidated count — they are handled
|
|
135
|
+
// separately via hasExpiredTag above.
|
|
136
|
+
const nonExpiredRevalidatedTags = revalidatedTags.filter((item) => {
|
|
137
|
+
if (item.expire?.N) {
|
|
138
|
+
return Number.parseInt(item.expire.N) > now;
|
|
139
|
+
}
|
|
140
|
+
return true;
|
|
141
|
+
});
|
|
142
|
+
// If we have revalidated tags or expired tags we return -1 to force revalidation
|
|
143
|
+
return nonExpiredRevalidatedTags.length > 0 || hasExpiredTag
|
|
144
|
+
? -1
|
|
145
|
+
: (lastModified ?? Date.now());
|
|
101
146
|
}
|
|
102
147
|
catch (e) {
|
|
103
148
|
error("Failed to get revalidated tags", e);
|
|
104
149
|
return lastModified ?? Date.now();
|
|
105
150
|
}
|
|
106
151
|
},
|
|
152
|
+
async isStale(key, lastModified) {
|
|
153
|
+
try {
|
|
154
|
+
if (globalThis.openNextConfig.dangerous?.disableTagCache) {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
const store = globalThis.__openNextAls.getStore();
|
|
158
|
+
const itemsCache = store?.requestCache.getOrCreate("dynamoDb:revalidateQueryItems");
|
|
159
|
+
const cacheKey = `${key}:${lastModified ?? 0}`;
|
|
160
|
+
let items;
|
|
161
|
+
if (itemsCache?.has(cacheKey)) {
|
|
162
|
+
items = itemsCache.get(cacheKey);
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
const result = await dynamoClient.send(new QueryCommand({
|
|
166
|
+
TableName: CACHE_DYNAMO_TABLE,
|
|
167
|
+
IndexName: "revalidate",
|
|
168
|
+
KeyConditionExpression: "#key = :key AND #revalidatedAt > :lastModified",
|
|
169
|
+
ExpressionAttributeNames: {
|
|
170
|
+
"#key": "path",
|
|
171
|
+
"#revalidatedAt": "revalidatedAt",
|
|
172
|
+
},
|
|
173
|
+
ExpressionAttributeValues: {
|
|
174
|
+
":key": { S: buildDynamoKey(key) },
|
|
175
|
+
":lastModified": { N: String(lastModified ?? 0) },
|
|
176
|
+
},
|
|
177
|
+
}));
|
|
178
|
+
items = result.Items ?? [];
|
|
179
|
+
itemsCache?.set(cacheKey, items);
|
|
180
|
+
}
|
|
181
|
+
debug("isStale items", key, items);
|
|
182
|
+
return items.length > 0;
|
|
183
|
+
}
|
|
184
|
+
catch (e) {
|
|
185
|
+
error("Failed to check stale tags", e);
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
},
|
|
107
189
|
async writeTags(tags) {
|
|
108
190
|
try {
|
|
109
191
|
if (globalThis.openNextConfig.dangerous?.disableTagCache) {
|
|
@@ -114,7 +196,7 @@ const tagCache = {
|
|
|
114
196
|
[CACHE_DYNAMO_TABLE ?? ""]: Items.map((Item) => ({
|
|
115
197
|
PutRequest: {
|
|
116
198
|
Item: {
|
|
117
|
-
...buildDynamoObject(Item.path, Item.tag, Item.revalidatedAt),
|
|
199
|
+
...buildDynamoObject(Item.path, Item.tag, Item.revalidatedAt, Item.stale, Item.expire),
|
|
118
200
|
},
|
|
119
201
|
},
|
|
120
202
|
})),
|
|
@@ -3,6 +3,7 @@ declare const _default: {
|
|
|
3
3
|
mode: "nextMode";
|
|
4
4
|
getLastRevalidated: (tags: string[]) => Promise<number>;
|
|
5
5
|
hasBeenRevalidated: (tags: string[], lastModified?: number) => Promise<boolean>;
|
|
6
|
-
|
|
6
|
+
isStale: (tags: string[], lastModified?: number) => Promise<boolean>;
|
|
7
|
+
writeTags: (tags: import("../../types/overrides").NextModeTagCacheWriteInput[]) => Promise<void>;
|
|
7
8
|
};
|
|
8
9
|
export default _default;
|
|
@@ -9,9 +9,9 @@ export default {
|
|
|
9
9
|
}
|
|
10
10
|
let lastRevalidated = 0;
|
|
11
11
|
tags.forEach((tag) => {
|
|
12
|
-
const
|
|
13
|
-
if (
|
|
14
|
-
lastRevalidated =
|
|
12
|
+
const tagData = tagsMap.get(tag);
|
|
13
|
+
if (tagData && tagData.revalidatedAt > lastRevalidated) {
|
|
14
|
+
lastRevalidated = tagData.revalidatedAt;
|
|
15
15
|
}
|
|
16
16
|
});
|
|
17
17
|
debug("getLastRevalidated result:", lastRevalidated);
|
|
@@ -21,13 +21,37 @@ export default {
|
|
|
21
21
|
if (globalThis.openNextConfig.dangerous?.disableTagCache) {
|
|
22
22
|
return false;
|
|
23
23
|
}
|
|
24
|
+
const now = Date.now();
|
|
24
25
|
const hasRevalidatedTag = tags.some((tag) => {
|
|
25
|
-
const
|
|
26
|
-
|
|
26
|
+
const tagData = tagsMap.get(tag);
|
|
27
|
+
if (!tagData) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
// Check if tag has expired
|
|
31
|
+
if (typeof tagData.expire === "number") {
|
|
32
|
+
const isExpired = tagData.expire <= now && tagData.expire > (lastModified ?? 0);
|
|
33
|
+
return isExpired;
|
|
34
|
+
}
|
|
35
|
+
// Check if tag has been revalidated
|
|
36
|
+
return tagData.revalidatedAt > (lastModified ?? 0);
|
|
27
37
|
});
|
|
28
38
|
debug("hasBeenRevalidated result:", hasRevalidatedTag);
|
|
29
39
|
return hasRevalidatedTag;
|
|
30
40
|
},
|
|
41
|
+
isStale: async (tags, lastModified) => {
|
|
42
|
+
if (globalThis.openNextConfig.dangerous?.disableTagCache) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
const hasStaleTag = tags.some((tag) => {
|
|
46
|
+
const tagData = tagsMap.get(tag);
|
|
47
|
+
if (!tagData || typeof tagData.stale !== "number") {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
return tagData.stale > (lastModified ?? 0);
|
|
51
|
+
});
|
|
52
|
+
debug("isStale result:", hasStaleTag);
|
|
53
|
+
return hasStaleTag;
|
|
54
|
+
},
|
|
31
55
|
writeTags: async (tags) => {
|
|
32
56
|
if (globalThis.openNextConfig.dangerous?.disableTagCache ||
|
|
33
57
|
tags.length === 0) {
|
|
@@ -35,7 +59,14 @@ export default {
|
|
|
35
59
|
}
|
|
36
60
|
debug("writeTags", { tags: tags });
|
|
37
61
|
tags.forEach((tag) => {
|
|
38
|
-
|
|
62
|
+
const tagStr = typeof tag === "string" ? tag : tag.tag;
|
|
63
|
+
const stale = typeof tag === "string" ? undefined : tag.stale;
|
|
64
|
+
const expire = typeof tag === "string" ? undefined : tag.expire;
|
|
65
|
+
tagsMap.set(tagStr, {
|
|
66
|
+
revalidatedAt: Date.now(),
|
|
67
|
+
stale,
|
|
68
|
+
expire,
|
|
69
|
+
});
|
|
39
70
|
});
|
|
40
71
|
debug("writeTags completed, written", tags.length, "tags");
|
|
41
72
|
},
|