@netlify/plugin-nextjs 5.13.5 → 5.14.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/build/content/prerendered.js +1 -1
- package/dist/build/content/server.js +1 -1
- package/dist/build/plugin-context.js +5 -1
- package/dist/build/skew-protection.js +106 -0
- package/dist/build/verification.js +1 -1
- package/dist/esm-chunks/{chunk-TLQCAGE2.js → chunk-TVEBGDAB.js} +23 -1
- package/dist/index.js +3 -1
- package/dist/run/handlers/cache.cjs +18 -8
- package/dist/run/handlers/tags-handler.cjs +74 -32
- package/dist/run/handlers/use-cache-handler.js +32 -10
- package/dist/run/storage/regional-blob-store.cjs +87 -43
- package/dist/shared/blob-types.cjs +1 -1
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
8
|
require_semver
|
|
9
|
-
} from "../esm-chunks/chunk-
|
|
9
|
+
} from "../esm-chunks/chunk-TVEBGDAB.js";
|
|
10
10
|
import {
|
|
11
11
|
__toESM
|
|
12
12
|
} from "../esm-chunks/chunk-6BT4RYQJ.js";
|
|
@@ -150,6 +150,10 @@ var PluginContext = class {
|
|
|
150
150
|
get edgeHandlerDir() {
|
|
151
151
|
return join(this.edgeFunctionsDir, EDGE_HANDLER_NAME);
|
|
152
152
|
}
|
|
153
|
+
/** Absolute path to the skew protection config */
|
|
154
|
+
get skewProtectionConfigPath() {
|
|
155
|
+
return this.resolveFromPackagePath(".netlify/v1/skew-protection.json");
|
|
156
|
+
}
|
|
153
157
|
constructor(options) {
|
|
154
158
|
this.constants = options.constants;
|
|
155
159
|
this.featureFlags = options.featureFlags;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
|
|
2
|
+
var require = await (async () => {
|
|
3
|
+
var { createRequire } = await import("node:module");
|
|
4
|
+
return createRequire(import.meta.url);
|
|
5
|
+
})();
|
|
6
|
+
|
|
7
|
+
import "../esm-chunks/chunk-6BT4RYQJ.js";
|
|
8
|
+
|
|
9
|
+
// src/build/skew-protection.ts
|
|
10
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
11
|
+
import { dirname } from "node:path";
|
|
12
|
+
var EnabledOrDisabledReason = /* @__PURE__ */ ((EnabledOrDisabledReason2) => {
|
|
13
|
+
EnabledOrDisabledReason2["OPT_OUT_DEFAULT"] = "off-default";
|
|
14
|
+
EnabledOrDisabledReason2["OPT_OUT_NO_VALID_DEPLOY_ID"] = "off-no-valid-deploy-id";
|
|
15
|
+
EnabledOrDisabledReason2["OPT_OUT_NO_VALID_DEPLOY_ID_ENV_VAR"] = "off-no-valid-deploy-id-env-var";
|
|
16
|
+
EnabledOrDisabledReason2["OPT_IN_FF"] = "on-ff";
|
|
17
|
+
EnabledOrDisabledReason2["OPT_IN_ENV_VAR"] = "on-env-var";
|
|
18
|
+
EnabledOrDisabledReason2["OPT_OUT_ENV_VAR"] = "off-env-var";
|
|
19
|
+
return EnabledOrDisabledReason2;
|
|
20
|
+
})(EnabledOrDisabledReason || {});
|
|
21
|
+
var optInOptions = /* @__PURE__ */ new Set([
|
|
22
|
+
"on-ff" /* OPT_IN_FF */,
|
|
23
|
+
"on-env-var" /* OPT_IN_ENV_VAR */
|
|
24
|
+
]);
|
|
25
|
+
var skewProtectionConfig = {
|
|
26
|
+
patterns: [".*"],
|
|
27
|
+
sources: [
|
|
28
|
+
{
|
|
29
|
+
type: "cookie",
|
|
30
|
+
name: "__vdpl"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
type: "header",
|
|
34
|
+
name: "X-Deployment-Id"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
type: "query",
|
|
38
|
+
name: "dpl"
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
};
|
|
42
|
+
function shouldEnableSkewProtection(ctx) {
|
|
43
|
+
let enabledOrDisabledReason = "off-default" /* OPT_OUT_DEFAULT */;
|
|
44
|
+
if (process.env.NETLIFY_NEXT_SKEW_PROTECTION === "true" || process.env.NETLIFY_NEXT_SKEW_PROTECTION === "1") {
|
|
45
|
+
enabledOrDisabledReason = "on-env-var" /* OPT_IN_ENV_VAR */;
|
|
46
|
+
} else if (process.env.NETLIFY_NEXT_SKEW_PROTECTION === "false" || process.env.NETLIFY_NEXT_SKEW_PROTECTION === "0") {
|
|
47
|
+
return {
|
|
48
|
+
enabled: false,
|
|
49
|
+
enabledOrDisabledReason: "off-env-var" /* OPT_OUT_ENV_VAR */
|
|
50
|
+
};
|
|
51
|
+
} else if (ctx.featureFlags?.["next-runtime-skew-protection"]) {
|
|
52
|
+
enabledOrDisabledReason = "on-ff" /* OPT_IN_FF */;
|
|
53
|
+
} else {
|
|
54
|
+
return {
|
|
55
|
+
enabled: false,
|
|
56
|
+
enabledOrDisabledReason: "off-default" /* OPT_OUT_DEFAULT */
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
if ((!process.env.DEPLOY_ID || process.env.DEPLOY_ID === "0") && optInOptions.has(enabledOrDisabledReason)) {
|
|
60
|
+
return {
|
|
61
|
+
enabled: false,
|
|
62
|
+
enabledOrDisabledReason: enabledOrDisabledReason === "on-env-var" /* OPT_IN_ENV_VAR */ && ctx.constants.IS_LOCAL ? (
|
|
63
|
+
// this case is singled out to provide visible feedback to users that env var has no effect
|
|
64
|
+
"off-no-valid-deploy-id-env-var" /* OPT_OUT_NO_VALID_DEPLOY_ID_ENV_VAR */
|
|
65
|
+
) : (
|
|
66
|
+
// this is silent disablement to avoid spam logs for users opted in via feature flag
|
|
67
|
+
// that don't explicitly opt in via env var
|
|
68
|
+
"off-no-valid-deploy-id" /* OPT_OUT_NO_VALID_DEPLOY_ID */
|
|
69
|
+
)
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
enabled: optInOptions.has(enabledOrDisabledReason),
|
|
74
|
+
enabledOrDisabledReason
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
var setSkewProtection = async (ctx, span) => {
|
|
78
|
+
const { enabled, enabledOrDisabledReason } = shouldEnableSkewProtection(ctx);
|
|
79
|
+
span.setAttribute("skewProtection", enabledOrDisabledReason);
|
|
80
|
+
if (!enabled) {
|
|
81
|
+
if (enabledOrDisabledReason === "off-no-valid-deploy-id-env-var" /* OPT_OUT_NO_VALID_DEPLOY_ID_ENV_VAR */) {
|
|
82
|
+
console.warn(
|
|
83
|
+
`NETLIFY_NEXT_SKEW_PROTECTION environment variable is set to ${process.env.NETLIFY_NEXT_SKEW_PROTECTION}, but skew protection is currently unavailable for CLI deploys. Skew protection will not be enabled.`
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (enabledOrDisabledReason === "on-env-var" /* OPT_IN_ENV_VAR */) {
|
|
89
|
+
console.log(
|
|
90
|
+
`Setting up Next.js Skew Protection due to NETLIFY_NEXT_SKEW_PROTECTION=${process.env.NETLIFY_NEXT_SKEW_PROTECTION} environment variable.`
|
|
91
|
+
);
|
|
92
|
+
} else {
|
|
93
|
+
console.log("Setting up Next.js Skew Protection.");
|
|
94
|
+
}
|
|
95
|
+
process.env.NEXT_DEPLOYMENT_ID = process.env.DEPLOY_ID;
|
|
96
|
+
await mkdir(dirname(ctx.skewProtectionConfigPath), {
|
|
97
|
+
recursive: true
|
|
98
|
+
});
|
|
99
|
+
await writeFile(ctx.skewProtectionConfigPath, JSON.stringify(skewProtectionConfig));
|
|
100
|
+
};
|
|
101
|
+
export {
|
|
102
|
+
EnabledOrDisabledReason,
|
|
103
|
+
setSkewProtection,
|
|
104
|
+
shouldEnableSkewProtection,
|
|
105
|
+
skewProtectionConfig
|
|
106
|
+
};
|
|
@@ -163,6 +163,9 @@ var require_identifiers = __commonJS({
|
|
|
163
163
|
"use strict";
|
|
164
164
|
var numeric = /^[0-9]+$/;
|
|
165
165
|
var compareIdentifiers = (a, b) => {
|
|
166
|
+
if (typeof a === "number" && typeof b === "number") {
|
|
167
|
+
return a === b ? 0 : a < b ? -1 : 1;
|
|
168
|
+
}
|
|
166
169
|
const anum = numeric.test(a);
|
|
167
170
|
const bnum = numeric.test(b);
|
|
168
171
|
if (anum && bnum) {
|
|
@@ -269,7 +272,25 @@ var require_semver = __commonJS({
|
|
|
269
272
|
if (!(other instanceof _SemVer)) {
|
|
270
273
|
other = new _SemVer(other, this.options);
|
|
271
274
|
}
|
|
272
|
-
|
|
275
|
+
if (this.major < other.major) {
|
|
276
|
+
return -1;
|
|
277
|
+
}
|
|
278
|
+
if (this.major > other.major) {
|
|
279
|
+
return 1;
|
|
280
|
+
}
|
|
281
|
+
if (this.minor < other.minor) {
|
|
282
|
+
return -1;
|
|
283
|
+
}
|
|
284
|
+
if (this.minor > other.minor) {
|
|
285
|
+
return 1;
|
|
286
|
+
}
|
|
287
|
+
if (this.patch < other.patch) {
|
|
288
|
+
return -1;
|
|
289
|
+
}
|
|
290
|
+
if (this.patch > other.patch) {
|
|
291
|
+
return 1;
|
|
292
|
+
}
|
|
293
|
+
return 0;
|
|
273
294
|
}
|
|
274
295
|
comparePre(other) {
|
|
275
296
|
if (!(other instanceof _SemVer)) {
|
|
@@ -1030,6 +1051,7 @@ var require_range = __commonJS({
|
|
|
1030
1051
|
return result;
|
|
1031
1052
|
};
|
|
1032
1053
|
var parseComparator = (comp, options) => {
|
|
1054
|
+
comp = comp.replace(re[t.BUILD], "");
|
|
1033
1055
|
debug("comp", comp, options);
|
|
1034
1056
|
comp = replaceCarets(comp, options);
|
|
1035
1057
|
debug("caret", comp);
|
package/dist/index.js
CHANGED
|
@@ -26,6 +26,7 @@ import { clearStaleEdgeHandlers, createEdgeHandlers } from "./build/functions/ed
|
|
|
26
26
|
import { clearStaleServerHandlers, createServerHandler } from "./build/functions/server.js";
|
|
27
27
|
import { setImageConfig } from "./build/image-cdn.js";
|
|
28
28
|
import { PluginContext } from "./build/plugin-context.js";
|
|
29
|
+
import { setSkewProtection } from "./build/skew-protection.js";
|
|
29
30
|
import {
|
|
30
31
|
verifyAdvancedAPIRoutes,
|
|
31
32
|
verifyNetlifyFormsWorkaround,
|
|
@@ -49,7 +50,7 @@ var onPreBuild = async (options) => {
|
|
|
49
50
|
console.warn(skipText);
|
|
50
51
|
return;
|
|
51
52
|
}
|
|
52
|
-
await tracer.withActiveSpan("onPreBuild", async () => {
|
|
53
|
+
await tracer.withActiveSpan("onPreBuild", async (span) => {
|
|
53
54
|
process.env.NEXT_PRIVATE_STANDALONE = "true";
|
|
54
55
|
const ctx = new PluginContext(options);
|
|
55
56
|
if (options.constants.IS_LOCAL) {
|
|
@@ -58,6 +59,7 @@ var onPreBuild = async (options) => {
|
|
|
58
59
|
} else {
|
|
59
60
|
await restoreBuildCache(ctx);
|
|
60
61
|
}
|
|
62
|
+
await setSkewProtection(ctx, span);
|
|
61
63
|
});
|
|
62
64
|
};
|
|
63
65
|
var onBuild = async (options) => {
|
|
@@ -204,16 +204,20 @@ var NetlifyCacheHandler = class {
|
|
|
204
204
|
);
|
|
205
205
|
return null;
|
|
206
206
|
}
|
|
207
|
-
const staleByTags = await this.checkCacheEntryStaleByTags(
|
|
207
|
+
const { stale: staleByTags, expired: expiredByTags } = await this.checkCacheEntryStaleByTags(
|
|
208
208
|
blob,
|
|
209
209
|
context.tags,
|
|
210
210
|
context.softTags
|
|
211
211
|
);
|
|
212
|
-
if (
|
|
213
|
-
span.addEvent("
|
|
212
|
+
if (expiredByTags) {
|
|
213
|
+
span.addEvent("Expired", { expiredByTags, key, ttl });
|
|
214
214
|
return null;
|
|
215
215
|
}
|
|
216
216
|
this.captureResponseCacheLastModified(blob, key, span);
|
|
217
|
+
if (staleByTags) {
|
|
218
|
+
span.addEvent("Stale", { staleByTags, key, ttl });
|
|
219
|
+
blob.lastModified = -1;
|
|
220
|
+
}
|
|
217
221
|
const isDataRequest = Boolean(context.fetchUrl);
|
|
218
222
|
if (!isDataRequest) {
|
|
219
223
|
this.captureCacheTags(blob.value, key);
|
|
@@ -350,8 +354,8 @@ var NetlifyCacheHandler = class {
|
|
|
350
354
|
}
|
|
351
355
|
});
|
|
352
356
|
}
|
|
353
|
-
async revalidateTag(tagOrTags) {
|
|
354
|
-
return (0, import_tags_handler.markTagsAsStaleAndPurgeEdgeCache)(tagOrTags);
|
|
357
|
+
async revalidateTag(tagOrTags, durations) {
|
|
358
|
+
return (0, import_tags_handler.markTagsAsStaleAndPurgeEdgeCache)(tagOrTags, durations);
|
|
355
359
|
}
|
|
356
360
|
resetRequestCache() {
|
|
357
361
|
}
|
|
@@ -365,16 +369,22 @@ var NetlifyCacheHandler = class {
|
|
|
365
369
|
} else if (cacheEntry.value?.kind === "PAGE" || cacheEntry.value?.kind === "PAGES" || cacheEntry.value?.kind === "APP_PAGE" || cacheEntry.value?.kind === "ROUTE" || cacheEntry.value?.kind === "APP_ROUTE") {
|
|
366
370
|
cacheTags = cacheEntry.value.headers?.[import_constants.NEXT_CACHE_TAGS_HEADER]?.split(/,|%2c/gi) || [];
|
|
367
371
|
} else {
|
|
368
|
-
return
|
|
372
|
+
return {
|
|
373
|
+
stale: false,
|
|
374
|
+
expired: false
|
|
375
|
+
};
|
|
369
376
|
}
|
|
370
377
|
if (this.revalidatedTags && this.revalidatedTags.length !== 0) {
|
|
371
378
|
for (const tag of this.revalidatedTags) {
|
|
372
379
|
if (cacheTags.includes(tag)) {
|
|
373
|
-
return
|
|
380
|
+
return {
|
|
381
|
+
stale: true,
|
|
382
|
+
expired: true
|
|
383
|
+
};
|
|
374
384
|
}
|
|
375
385
|
}
|
|
376
386
|
}
|
|
377
|
-
return (0, import_tags_handler.
|
|
387
|
+
return (0, import_tags_handler.isAnyTagStaleOrExpired)(cacheTags, cacheEntry.lastModified);
|
|
378
388
|
}
|
|
379
389
|
};
|
|
380
390
|
var cache_default = NetlifyCacheHandler;
|
|
@@ -20,8 +20,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/run/handlers/tags-handler.cts
|
|
21
21
|
var tags_handler_exports = {};
|
|
22
22
|
__export(tags_handler_exports, {
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
getMostRecentTagExpirationTimestamp: () => getMostRecentTagExpirationTimestamp,
|
|
24
|
+
isAnyTagStaleOrExpired: () => isAnyTagStaleOrExpired,
|
|
25
25
|
markTagsAsStaleAndPurgeEdgeCache: () => markTagsAsStaleAndPurgeEdgeCache,
|
|
26
26
|
purgeEdgeCache: () => purgeEdgeCache
|
|
27
27
|
});
|
|
@@ -86,58 +86,86 @@ var pipeline = (0, import_util.promisify)(import_stream.pipeline);
|
|
|
86
86
|
|
|
87
87
|
// package.json
|
|
88
88
|
var name = "@netlify/plugin-nextjs";
|
|
89
|
-
var version = "5.
|
|
89
|
+
var version = "5.14.1";
|
|
90
90
|
|
|
91
91
|
// src/run/handlers/tags-handler.cts
|
|
92
92
|
var import_storage = require("../storage/storage.cjs");
|
|
93
93
|
var import_request_context = require("./request-context.cjs");
|
|
94
94
|
var purgeCacheUserAgent = `${name}@${version}`;
|
|
95
|
-
async function
|
|
95
|
+
async function getTagManifest(tag, cacheStore) {
|
|
96
96
|
const tagManifest = await cacheStore.get(tag, "tagManifest.get");
|
|
97
97
|
if (!tagManifest) {
|
|
98
98
|
return null;
|
|
99
99
|
}
|
|
100
|
-
return tagManifest
|
|
100
|
+
return tagManifest;
|
|
101
101
|
}
|
|
102
|
-
async function
|
|
102
|
+
async function getMostRecentTagExpirationTimestamp(tags) {
|
|
103
103
|
if (tags.length === 0) {
|
|
104
104
|
return 0;
|
|
105
105
|
}
|
|
106
106
|
const cacheStore = (0, import_storage.getMemoizedKeyValueStoreBackedByRegionalBlobStore)({ consistency: "strong" });
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
)
|
|
110
|
-
const timestamps = timestampsOrNulls.filter((timestamp) => timestamp !== null);
|
|
111
|
-
if (timestamps.length === 0) {
|
|
107
|
+
const manifestsOrNulls = await Promise.all(tags.map((tag) => getTagManifest(tag, cacheStore)));
|
|
108
|
+
const expirationTimestamps = manifestsOrNulls.filter((manifest) => manifest !== null).map((manifest) => manifest.expireAt);
|
|
109
|
+
if (expirationTimestamps.length === 0) {
|
|
112
110
|
return 0;
|
|
113
111
|
}
|
|
114
|
-
return Math.max(...
|
|
112
|
+
return Math.max(...expirationTimestamps);
|
|
115
113
|
}
|
|
116
|
-
function
|
|
114
|
+
function isAnyTagStaleOrExpired(tags, timestamp) {
|
|
117
115
|
if (tags.length === 0 || !timestamp) {
|
|
118
|
-
return Promise.resolve(false);
|
|
116
|
+
return Promise.resolve({ stale: false, expired: false });
|
|
119
117
|
}
|
|
120
118
|
const cacheStore = (0, import_storage.getMemoizedKeyValueStoreBackedByRegionalBlobStore)({ consistency: "strong" });
|
|
121
119
|
return new Promise((resolve, reject) => {
|
|
122
120
|
const tagManifestPromises = [];
|
|
123
121
|
for (const tag of tags) {
|
|
124
|
-
const
|
|
122
|
+
const tagManifestPromise = getTagManifest(tag, cacheStore);
|
|
125
123
|
tagManifestPromises.push(
|
|
126
|
-
|
|
127
|
-
if (!
|
|
128
|
-
return false;
|
|
124
|
+
tagManifestPromise.then((tagManifest) => {
|
|
125
|
+
if (!tagManifest) {
|
|
126
|
+
return { stale: false, expired: false };
|
|
127
|
+
}
|
|
128
|
+
const stale = tagManifest.staleAt >= timestamp;
|
|
129
|
+
const expired = tagManifest.expireAt >= timestamp && tagManifest.expireAt <= Date.now();
|
|
130
|
+
if (expired && stale) {
|
|
131
|
+
const expiredResult = {
|
|
132
|
+
stale,
|
|
133
|
+
expired
|
|
134
|
+
};
|
|
135
|
+
resolve(expiredResult);
|
|
136
|
+
return expiredResult;
|
|
129
137
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
138
|
+
if (stale) {
|
|
139
|
+
const staleResult = {
|
|
140
|
+
stale,
|
|
141
|
+
expired,
|
|
142
|
+
expireAt: tagManifest.expireAt
|
|
143
|
+
};
|
|
144
|
+
return staleResult;
|
|
134
145
|
}
|
|
135
|
-
return false;
|
|
146
|
+
return { stale: false, expired: false };
|
|
136
147
|
})
|
|
137
148
|
);
|
|
138
149
|
}
|
|
139
|
-
Promise.all(tagManifestPromises).then((
|
|
140
|
-
|
|
150
|
+
Promise.all(tagManifestPromises).then((tagManifestsAreStaleOrExpired) => {
|
|
151
|
+
let result = { stale: false, expired: false };
|
|
152
|
+
for (const tagResult of tagManifestsAreStaleOrExpired) {
|
|
153
|
+
if (tagResult.expired) {
|
|
154
|
+
result = tagResult;
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
if (tagResult.stale) {
|
|
158
|
+
result = {
|
|
159
|
+
stale: true,
|
|
160
|
+
expired: false,
|
|
161
|
+
expireAt: (
|
|
162
|
+
// make sure to use expireAt that is lowest of all tags
|
|
163
|
+
result.stale && !result.expired && typeof result.expireAt === "number" ? Math.min(result.expireAt, tagResult.expireAt) : tagResult.expireAt
|
|
164
|
+
)
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
resolve(result);
|
|
141
169
|
}).catch(reject);
|
|
142
170
|
});
|
|
143
171
|
}
|
|
@@ -154,13 +182,15 @@ function purgeEdgeCache(tagOrTags) {
|
|
|
154
182
|
(0, import_request_context.getLogger)().withError(error).error(`[NextRuntime] Purging the cache for tags [${tags.join(",")}] failed`);
|
|
155
183
|
});
|
|
156
184
|
}
|
|
157
|
-
async function doRevalidateTagAndPurgeEdgeCache(tags) {
|
|
158
|
-
(0, import_request_context.getLogger)().withFields({ tags }).debug("doRevalidateTagAndPurgeEdgeCache");
|
|
185
|
+
async function doRevalidateTagAndPurgeEdgeCache(tags, durations) {
|
|
186
|
+
(0, import_request_context.getLogger)().withFields({ tags, durations }).debug("doRevalidateTagAndPurgeEdgeCache");
|
|
159
187
|
if (tags.length === 0) {
|
|
160
188
|
return;
|
|
161
189
|
}
|
|
190
|
+
const now = Date.now();
|
|
162
191
|
const tagManifest = {
|
|
163
|
-
|
|
192
|
+
staleAt: now,
|
|
193
|
+
expireAt: now + (durations?.expire ? durations.expire * 1e3 : 0)
|
|
164
194
|
};
|
|
165
195
|
const cacheStore = (0, import_storage.getMemoizedKeyValueStoreBackedByRegionalBlobStore)({ consistency: "strong" });
|
|
166
196
|
await Promise.all(
|
|
@@ -174,19 +204,31 @@ async function doRevalidateTagAndPurgeEdgeCache(tags) {
|
|
|
174
204
|
);
|
|
175
205
|
await purgeEdgeCache(tags);
|
|
176
206
|
}
|
|
177
|
-
function markTagsAsStaleAndPurgeEdgeCache(tagOrTags) {
|
|
207
|
+
function markTagsAsStaleAndPurgeEdgeCache(tagOrTags, durations) {
|
|
178
208
|
const tags = getCacheTagsFromTagOrTags(tagOrTags);
|
|
179
|
-
const
|
|
209
|
+
const revalidationKey = JSON.stringify({ tags, durations });
|
|
180
210
|
const requestContext = (0, import_request_context.getRequestContext)();
|
|
181
211
|
if (requestContext) {
|
|
212
|
+
const ongoingRevalidation = requestContext.ongoingRevalidations?.get(revalidationKey);
|
|
213
|
+
if (ongoingRevalidation) {
|
|
214
|
+
return ongoingRevalidation;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
const revalidateTagPromise = doRevalidateTagAndPurgeEdgeCache(tags, durations);
|
|
218
|
+
if (requestContext) {
|
|
219
|
+
requestContext.ongoingRevalidations ??= /* @__PURE__ */ new Map();
|
|
220
|
+
requestContext.ongoingRevalidations.set(revalidationKey, revalidateTagPromise);
|
|
221
|
+
process.nextTick(() => {
|
|
222
|
+
requestContext.ongoingRevalidations?.delete(revalidationKey);
|
|
223
|
+
});
|
|
182
224
|
requestContext.trackBackgroundWork(revalidateTagPromise);
|
|
183
225
|
}
|
|
184
226
|
return revalidateTagPromise;
|
|
185
227
|
}
|
|
186
228
|
// Annotate the CommonJS export names for ESM import in node:
|
|
187
229
|
0 && (module.exports = {
|
|
188
|
-
|
|
189
|
-
|
|
230
|
+
getMostRecentTagExpirationTimestamp,
|
|
231
|
+
isAnyTagStaleOrExpired,
|
|
190
232
|
markTagsAsStaleAndPurgeEdgeCache,
|
|
191
233
|
purgeEdgeCache
|
|
192
234
|
});
|
|
@@ -1381,8 +1381,8 @@ var LRUCache = class _LRUCache {
|
|
|
1381
1381
|
// src/run/handlers/use-cache-handler.ts
|
|
1382
1382
|
import { getLogger } from "./request-context.cjs";
|
|
1383
1383
|
import {
|
|
1384
|
-
|
|
1385
|
-
|
|
1384
|
+
getMostRecentTagExpirationTimestamp,
|
|
1385
|
+
isAnyTagStaleOrExpired,
|
|
1386
1386
|
markTagsAsStaleAndPurgeEdgeCache
|
|
1387
1387
|
} from "./tags-handler.cjs";
|
|
1388
1388
|
import { getTracer } from "./tracer.cjs";
|
|
@@ -1444,23 +1444,29 @@ var NetlifyDefaultUseCacheHandler = {
|
|
|
1444
1444
|
});
|
|
1445
1445
|
return void 0;
|
|
1446
1446
|
}
|
|
1447
|
-
|
|
1448
|
-
|
|
1447
|
+
const { stale, expired } = await isAnyTagStaleOrExpired(entry.tags, entry.timestamp);
|
|
1448
|
+
if (expired) {
|
|
1449
|
+
getLogger().withFields({ cacheKey, ttl, status: "EXPIRED BY TAG" }).debug(`[NetlifyDefaultUseCacheHandler] get result`);
|
|
1449
1450
|
span.setAttributes({
|
|
1450
|
-
cacheStatus: "
|
|
1451
|
+
cacheStatus: "expired tag, discarded",
|
|
1451
1452
|
ttl
|
|
1452
1453
|
});
|
|
1453
1454
|
return void 0;
|
|
1454
1455
|
}
|
|
1455
|
-
|
|
1456
|
+
let { revalidate, value } = entry;
|
|
1457
|
+
if (stale) {
|
|
1458
|
+
revalidate = -1;
|
|
1459
|
+
}
|
|
1460
|
+
const [returnStream, newSaved] = value.tee();
|
|
1456
1461
|
entry.value = newSaved;
|
|
1457
|
-
getLogger().withFields({ cacheKey, ttl, status: "HIT" }).debug(`[NetlifyDefaultUseCacheHandler] get result`);
|
|
1462
|
+
getLogger().withFields({ cacheKey, ttl, status: stale ? "STALE" : "HIT" }).debug(`[NetlifyDefaultUseCacheHandler] get result`);
|
|
1458
1463
|
span.setAttributes({
|
|
1459
|
-
cacheStatus: "hit",
|
|
1464
|
+
cacheStatus: stale ? "stale" : "hit",
|
|
1460
1465
|
ttl
|
|
1461
1466
|
});
|
|
1462
1467
|
return {
|
|
1463
1468
|
...entry,
|
|
1469
|
+
revalidate,
|
|
1464
1470
|
value: returnStream
|
|
1465
1471
|
};
|
|
1466
1472
|
}
|
|
@@ -1513,14 +1519,15 @@ var NetlifyDefaultUseCacheHandler = {
|
|
|
1513
1519
|
},
|
|
1514
1520
|
async refreshTags() {
|
|
1515
1521
|
},
|
|
1516
|
-
getExpiration: function(...
|
|
1522
|
+
getExpiration: function(...notNormalizedTags) {
|
|
1517
1523
|
return getTracer().withActiveSpan(
|
|
1518
1524
|
"DefaultUseCacheHandler.getExpiration",
|
|
1519
1525
|
async (span) => {
|
|
1526
|
+
const tags = notNormalizedTags.flat();
|
|
1520
1527
|
span.setAttributes({
|
|
1521
1528
|
tags
|
|
1522
1529
|
});
|
|
1523
|
-
const expiration = await
|
|
1530
|
+
const expiration = await getMostRecentTagExpirationTimestamp(tags);
|
|
1524
1531
|
getLogger().withFields({ tags, expiration }).debug(`[NetlifyDefaultUseCacheHandler] getExpiration`);
|
|
1525
1532
|
span.setAttributes({
|
|
1526
1533
|
expiration
|
|
@@ -1529,6 +1536,7 @@ var NetlifyDefaultUseCacheHandler = {
|
|
|
1529
1536
|
}
|
|
1530
1537
|
);
|
|
1531
1538
|
},
|
|
1539
|
+
// this is for CacheHandlerV2
|
|
1532
1540
|
expireTags(...tags) {
|
|
1533
1541
|
return getTracer().withActiveSpan(
|
|
1534
1542
|
"DefaultUseCacheHandler.expireTags",
|
|
@@ -1540,6 +1548,20 @@ var NetlifyDefaultUseCacheHandler = {
|
|
|
1540
1548
|
await markTagsAsStaleAndPurgeEdgeCache(tags);
|
|
1541
1549
|
}
|
|
1542
1550
|
);
|
|
1551
|
+
},
|
|
1552
|
+
// this is for CacheHandlerV3 / Next 16
|
|
1553
|
+
updateTags(tags, durations) {
|
|
1554
|
+
return getTracer().withActiveSpan(
|
|
1555
|
+
"DefaultUseCacheHandler.updateTags",
|
|
1556
|
+
async (span) => {
|
|
1557
|
+
getLogger().withFields({ tags, durations }).debug(`[NetlifyDefaultUseCacheHandler] updateTags`);
|
|
1558
|
+
span.setAttributes({
|
|
1559
|
+
tags,
|
|
1560
|
+
durations: JSON.stringify(durations)
|
|
1561
|
+
});
|
|
1562
|
+
await markTagsAsStaleAndPurgeEdgeCache(tags, durations);
|
|
1563
|
+
}
|
|
1564
|
+
);
|
|
1543
1565
|
}
|
|
1544
1566
|
};
|
|
1545
1567
|
function configureUseCacheHandlers() {
|
|
@@ -25,40 +25,12 @@ __export(regional_blob_store_exports, {
|
|
|
25
25
|
});
|
|
26
26
|
module.exports = __toCommonJS(regional_blob_store_exports);
|
|
27
27
|
|
|
28
|
-
// node_modules/@netlify/blobs/dist/
|
|
29
|
-
var
|
|
30
|
-
var
|
|
31
|
-
var
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
if (res.headers.has(NF_REQUEST_ID)) {
|
|
35
|
-
details += `, ID: ${res.headers.get(NF_REQUEST_ID)}`;
|
|
36
|
-
}
|
|
37
|
-
super(`Netlify Blobs has generated an internal error (${details})`);
|
|
38
|
-
this.name = "BlobsInternalError";
|
|
39
|
-
}
|
|
40
|
-
};
|
|
41
|
-
var collectIterator = async (iterator) => {
|
|
42
|
-
const result = [];
|
|
43
|
-
for await (const item of iterator) {
|
|
44
|
-
result.push(item);
|
|
45
|
-
}
|
|
46
|
-
return result;
|
|
47
|
-
};
|
|
48
|
-
var base64Decode = (input) => {
|
|
49
|
-
const { Buffer: Buffer2 } = globalThis;
|
|
50
|
-
if (Buffer2) {
|
|
51
|
-
return Buffer2.from(input, "base64").toString();
|
|
52
|
-
}
|
|
53
|
-
return atob(input);
|
|
54
|
-
};
|
|
55
|
-
var base64Encode = (input) => {
|
|
56
|
-
const { Buffer: Buffer2 } = globalThis;
|
|
57
|
-
if (Buffer2) {
|
|
58
|
-
return Buffer2.from(input).toString("base64");
|
|
59
|
-
}
|
|
60
|
-
return btoa(input);
|
|
61
|
-
};
|
|
28
|
+
// node_modules/@netlify/blobs/node_modules/@netlify/runtime-utils/dist/main.js
|
|
29
|
+
var getString = (input) => typeof input === "string" ? input : JSON.stringify(input);
|
|
30
|
+
var base64Decode = globalThis.Buffer ? (input) => Buffer.from(input, "base64").toString() : (input) => atob(input);
|
|
31
|
+
var base64Encode = globalThis.Buffer ? (input) => Buffer.from(getString(input)).toString("base64") : (input) => btoa(getString(input));
|
|
32
|
+
|
|
33
|
+
// node_modules/@netlify/blobs/dist/chunk-HN33TXZT.js
|
|
62
34
|
var getEnvironment = () => {
|
|
63
35
|
const { Deno, Netlify, process: process2 } = globalThis;
|
|
64
36
|
return Netlify?.env ?? Deno?.env ?? {
|
|
@@ -111,7 +83,7 @@ var encodeMetadata = (metadata) => {
|
|
|
111
83
|
return payload;
|
|
112
84
|
};
|
|
113
85
|
var decodeMetadata = (header) => {
|
|
114
|
-
if (!header
|
|
86
|
+
if (!header?.startsWith(BASE64_PREFIX)) {
|
|
115
87
|
return {};
|
|
116
88
|
}
|
|
117
89
|
const encodedData = header.slice(BASE64_PREFIX.length);
|
|
@@ -132,6 +104,25 @@ var getMetadataFromResponse = (response) => {
|
|
|
132
104
|
);
|
|
133
105
|
}
|
|
134
106
|
};
|
|
107
|
+
var NF_ERROR = "x-nf-error";
|
|
108
|
+
var NF_REQUEST_ID = "x-nf-request-id";
|
|
109
|
+
var BlobsInternalError = class extends Error {
|
|
110
|
+
constructor(res) {
|
|
111
|
+
let details = res.headers.get(NF_ERROR) || `${res.status} status code`;
|
|
112
|
+
if (res.headers.has(NF_REQUEST_ID)) {
|
|
113
|
+
details += `, ID: ${res.headers.get(NF_REQUEST_ID)}`;
|
|
114
|
+
}
|
|
115
|
+
super(`Netlify Blobs has generated an internal error (${details})`);
|
|
116
|
+
this.name = "BlobsInternalError";
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
var collectIterator = async (iterator) => {
|
|
120
|
+
const result = [];
|
|
121
|
+
for await (const item of iterator) {
|
|
122
|
+
result.push(item);
|
|
123
|
+
}
|
|
124
|
+
return result;
|
|
125
|
+
};
|
|
135
126
|
var BlobsConsistencyError = class extends Error {
|
|
136
127
|
constructor() {
|
|
137
128
|
super(
|
|
@@ -283,6 +274,7 @@ var Client = class {
|
|
|
283
274
|
}
|
|
284
275
|
async makeRequest({
|
|
285
276
|
body,
|
|
277
|
+
conditions = {},
|
|
286
278
|
consistency,
|
|
287
279
|
headers: extraHeaders,
|
|
288
280
|
key,
|
|
@@ -306,6 +298,11 @@ var Client = class {
|
|
|
306
298
|
if (method === "put") {
|
|
307
299
|
headers["cache-control"] = "max-age=0, stale-while-revalidate=60";
|
|
308
300
|
}
|
|
301
|
+
if ("onlyIfMatch" in conditions && conditions.onlyIfMatch) {
|
|
302
|
+
headers["if-match"] = conditions.onlyIfMatch;
|
|
303
|
+
} else if ("onlyIfNew" in conditions && conditions.onlyIfNew) {
|
|
304
|
+
headers["if-none-match"] = "*";
|
|
305
|
+
}
|
|
309
306
|
const options = {
|
|
310
307
|
body,
|
|
311
308
|
headers,
|
|
@@ -344,6 +341,8 @@ var getClientOptions = (options, contextOverride) => {
|
|
|
344
341
|
var DEPLOY_STORE_PREFIX = "deploy:";
|
|
345
342
|
var LEGACY_STORE_INTERNAL_PREFIX = "netlify-internal/legacy-namespace/";
|
|
346
343
|
var SITE_STORE_PREFIX = "site:";
|
|
344
|
+
var STATUS_OK = 200;
|
|
345
|
+
var STATUS_PRE_CONDITION_FAILED = 412;
|
|
347
346
|
var Store = class _Store {
|
|
348
347
|
constructor(options) {
|
|
349
348
|
this.client = options.client;
|
|
@@ -468,36 +467,56 @@ var Store = class _Store {
|
|
|
468
467
|
)
|
|
469
468
|
);
|
|
470
469
|
}
|
|
471
|
-
async set(key, data,
|
|
470
|
+
async set(key, data, options = {}) {
|
|
472
471
|
_Store.validateKey(key);
|
|
472
|
+
const conditions = _Store.getConditions(options);
|
|
473
473
|
const res = await this.client.makeRequest({
|
|
474
|
+
conditions,
|
|
474
475
|
body: data,
|
|
475
476
|
key,
|
|
476
|
-
metadata,
|
|
477
|
+
metadata: options.metadata,
|
|
477
478
|
method: "put",
|
|
478
479
|
storeName: this.name
|
|
479
480
|
});
|
|
480
|
-
|
|
481
|
-
|
|
481
|
+
const etag = res.headers.get("etag") ?? "";
|
|
482
|
+
if (conditions) {
|
|
483
|
+
return res.status === STATUS_PRE_CONDITION_FAILED ? { modified: false } : { etag, modified: true };
|
|
484
|
+
}
|
|
485
|
+
if (res.status === STATUS_OK) {
|
|
486
|
+
return {
|
|
487
|
+
etag,
|
|
488
|
+
modified: true
|
|
489
|
+
};
|
|
482
490
|
}
|
|
491
|
+
throw new BlobsInternalError(res);
|
|
483
492
|
}
|
|
484
|
-
async setJSON(key, data,
|
|
493
|
+
async setJSON(key, data, options = {}) {
|
|
485
494
|
_Store.validateKey(key);
|
|
495
|
+
const conditions = _Store.getConditions(options);
|
|
486
496
|
const payload = JSON.stringify(data);
|
|
487
497
|
const headers = {
|
|
488
498
|
"content-type": "application/json"
|
|
489
499
|
};
|
|
490
500
|
const res = await this.client.makeRequest({
|
|
501
|
+
...conditions,
|
|
491
502
|
body: payload,
|
|
492
503
|
headers,
|
|
493
504
|
key,
|
|
494
|
-
metadata,
|
|
505
|
+
metadata: options.metadata,
|
|
495
506
|
method: "put",
|
|
496
507
|
storeName: this.name
|
|
497
508
|
});
|
|
498
|
-
|
|
499
|
-
|
|
509
|
+
const etag = res.headers.get("etag") ?? "";
|
|
510
|
+
if (conditions) {
|
|
511
|
+
return res.status === STATUS_PRE_CONDITION_FAILED ? { modified: false } : { etag, modified: true };
|
|
500
512
|
}
|
|
513
|
+
if (res.status === STATUS_OK) {
|
|
514
|
+
return {
|
|
515
|
+
etag,
|
|
516
|
+
modified: true
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
throw new BlobsInternalError(res);
|
|
501
520
|
}
|
|
502
521
|
static formatListResultBlob(result) {
|
|
503
522
|
if (!result.key) {
|
|
@@ -508,6 +527,31 @@ var Store = class _Store {
|
|
|
508
527
|
key: result.key
|
|
509
528
|
};
|
|
510
529
|
}
|
|
530
|
+
static getConditions(options) {
|
|
531
|
+
if ("onlyIfMatch" in options && "onlyIfNew" in options) {
|
|
532
|
+
throw new Error(
|
|
533
|
+
`The 'onlyIfMatch' and 'onlyIfNew' options are mutually exclusive. Using 'onlyIfMatch' will make the write succeed only if there is an entry for the key with the given content, while 'onlyIfNew' will make the write succeed only if there is no entry for the key.`
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
if ("onlyIfMatch" in options && options.onlyIfMatch) {
|
|
537
|
+
if (typeof options.onlyIfMatch !== "string") {
|
|
538
|
+
throw new Error(`The 'onlyIfMatch' property expects a string representing an ETag.`);
|
|
539
|
+
}
|
|
540
|
+
return {
|
|
541
|
+
onlyIfMatch: options.onlyIfMatch
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
if ("onlyIfNew" in options && options.onlyIfNew) {
|
|
545
|
+
if (typeof options.onlyIfNew !== "boolean") {
|
|
546
|
+
throw new Error(
|
|
547
|
+
`The 'onlyIfNew' property expects a boolean indicating whether the write should fail if an entry for the key already exists.`
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
return {
|
|
551
|
+
onlyIfNew: true
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
}
|
|
511
555
|
static validateKey(key) {
|
|
512
556
|
if (key === "") {
|
|
513
557
|
throw new Error("Blob key must not be empty.");
|
|
@@ -25,7 +25,7 @@ __export(blob_types_exports, {
|
|
|
25
25
|
});
|
|
26
26
|
module.exports = __toCommonJS(blob_types_exports);
|
|
27
27
|
var isTagManifest = (value) => {
|
|
28
|
-
return typeof value === "object" && value !== null && "
|
|
28
|
+
return typeof value === "object" && value !== null && "staleAt" in value && typeof value.staleAt === "number" && "expiredAt" in value && typeof value.expiredAt === "number" && Object.keys(value).length === 2;
|
|
29
29
|
};
|
|
30
30
|
var isHtmlBlob = (value) => {
|
|
31
31
|
return typeof value === "object" && value !== null && "html" in value && "isFullyStaticPage" in value && typeof value.html === "string" && typeof value.isFullyStaticPage === "boolean" && Object.keys(value).length === 2;
|