@revealui/cache 0.1.0
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/LICENSE +22 -0
- package/LICENSE.commercial +111 -0
- package/dist/index.d.ts +366 -0
- package/dist/index.js +760 -0
- package/dist/index.js.map +1 -0
- package/package.json +52 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,760 @@
|
|
|
1
|
+
// src/cdn-config.ts
|
|
2
|
+
var DEFAULT_CDN_CONFIG = {
|
|
3
|
+
provider: "vercel",
|
|
4
|
+
ttl: 31536e3,
|
|
5
|
+
// 1 year for static assets
|
|
6
|
+
staleWhileRevalidate: 86400,
|
|
7
|
+
// 1 day
|
|
8
|
+
staleIfError: 604800,
|
|
9
|
+
// 1 week
|
|
10
|
+
bypassCache: false,
|
|
11
|
+
cacheKey: ["url", "headers.accept", "headers.accept-encoding"],
|
|
12
|
+
varyHeaders: ["Accept", "Accept-Encoding"]
|
|
13
|
+
};
|
|
14
|
+
function generateCacheControl(config) {
|
|
15
|
+
const directives = [];
|
|
16
|
+
if (config.noStore) {
|
|
17
|
+
directives.push("no-store");
|
|
18
|
+
return directives.join(", ");
|
|
19
|
+
}
|
|
20
|
+
if (config.noCache) {
|
|
21
|
+
directives.push("no-cache");
|
|
22
|
+
return directives.join(", ");
|
|
23
|
+
}
|
|
24
|
+
if (config.public) {
|
|
25
|
+
directives.push("public");
|
|
26
|
+
} else if (config.private) {
|
|
27
|
+
directives.push("private");
|
|
28
|
+
}
|
|
29
|
+
if (config.maxAge !== void 0) {
|
|
30
|
+
directives.push(`max-age=${config.maxAge}`);
|
|
31
|
+
}
|
|
32
|
+
if (config.sMaxAge !== void 0) {
|
|
33
|
+
directives.push(`s-maxage=${config.sMaxAge}`);
|
|
34
|
+
}
|
|
35
|
+
if (config.staleWhileRevalidate !== void 0) {
|
|
36
|
+
directives.push(`stale-while-revalidate=${config.staleWhileRevalidate}`);
|
|
37
|
+
}
|
|
38
|
+
if (config.staleIfError !== void 0) {
|
|
39
|
+
directives.push(`stale-if-error=${config.staleIfError}`);
|
|
40
|
+
}
|
|
41
|
+
if (config.immutable) {
|
|
42
|
+
directives.push("immutable");
|
|
43
|
+
}
|
|
44
|
+
return directives.join(", ");
|
|
45
|
+
}
|
|
46
|
+
var CDN_CACHE_PRESETS = {
|
|
47
|
+
// Static assets with hashed filenames (immutable)
|
|
48
|
+
immutable: {
|
|
49
|
+
maxAge: 31536e3,
|
|
50
|
+
// 1 year
|
|
51
|
+
sMaxAge: 31536e3,
|
|
52
|
+
public: true,
|
|
53
|
+
immutable: true
|
|
54
|
+
},
|
|
55
|
+
// Static assets (images, fonts)
|
|
56
|
+
static: {
|
|
57
|
+
maxAge: 2592e3,
|
|
58
|
+
// 30 days
|
|
59
|
+
sMaxAge: 31536e3,
|
|
60
|
+
// 1 year on CDN
|
|
61
|
+
staleWhileRevalidate: 86400,
|
|
62
|
+
// 1 day
|
|
63
|
+
public: true
|
|
64
|
+
},
|
|
65
|
+
// API responses (short-lived)
|
|
66
|
+
api: {
|
|
67
|
+
maxAge: 0,
|
|
68
|
+
sMaxAge: 60,
|
|
69
|
+
// 1 minute on CDN
|
|
70
|
+
staleWhileRevalidate: 30,
|
|
71
|
+
public: true
|
|
72
|
+
},
|
|
73
|
+
// HTML pages (dynamic)
|
|
74
|
+
page: {
|
|
75
|
+
maxAge: 0,
|
|
76
|
+
sMaxAge: 300,
|
|
77
|
+
// 5 minutes on CDN
|
|
78
|
+
staleWhileRevalidate: 60,
|
|
79
|
+
public: true
|
|
80
|
+
},
|
|
81
|
+
// User-specific data
|
|
82
|
+
private: {
|
|
83
|
+
maxAge: 300,
|
|
84
|
+
// 5 minutes
|
|
85
|
+
private: true,
|
|
86
|
+
staleWhileRevalidate: 60
|
|
87
|
+
},
|
|
88
|
+
// No caching
|
|
89
|
+
noCache: {
|
|
90
|
+
noStore: true
|
|
91
|
+
},
|
|
92
|
+
// Revalidate every request
|
|
93
|
+
revalidate: {
|
|
94
|
+
maxAge: 0,
|
|
95
|
+
sMaxAge: 0,
|
|
96
|
+
noCache: true
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
async function purgeCDNCache(urls, config) {
|
|
100
|
+
const { provider } = config;
|
|
101
|
+
switch (provider) {
|
|
102
|
+
case "cloudflare":
|
|
103
|
+
return purgeCloudflare(urls, config);
|
|
104
|
+
case "vercel":
|
|
105
|
+
return purgeVercel(urls, config);
|
|
106
|
+
case "fastly":
|
|
107
|
+
return purgeFastly(urls, config);
|
|
108
|
+
default:
|
|
109
|
+
throw new Error(`Unsupported CDN provider: ${provider}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async function purgeCloudflare(urls, config) {
|
|
113
|
+
const { apiKey, zoneId } = config;
|
|
114
|
+
if (!(apiKey && zoneId)) {
|
|
115
|
+
throw new Error("Cloudflare API key and zone ID required");
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
const response = await fetch(
|
|
119
|
+
`https://api.cloudflare.com/client/v4/zones/${zoneId}/purge_cache`,
|
|
120
|
+
{
|
|
121
|
+
method: "POST",
|
|
122
|
+
headers: {
|
|
123
|
+
Authorization: `Bearer ${apiKey}`,
|
|
124
|
+
"Content-Type": "application/json"
|
|
125
|
+
},
|
|
126
|
+
body: JSON.stringify({ files: urls })
|
|
127
|
+
}
|
|
128
|
+
);
|
|
129
|
+
const data = await response.json();
|
|
130
|
+
return {
|
|
131
|
+
success: data.success,
|
|
132
|
+
purged: urls.length,
|
|
133
|
+
errors: data.errors
|
|
134
|
+
};
|
|
135
|
+
} catch (error) {
|
|
136
|
+
return {
|
|
137
|
+
success: false,
|
|
138
|
+
purged: 0,
|
|
139
|
+
errors: [error instanceof Error ? error.message : "Unknown error"]
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async function purgeVercel(urls, config) {
|
|
144
|
+
const { apiKey } = config;
|
|
145
|
+
if (!apiKey) {
|
|
146
|
+
throw new Error("Vercel API token required");
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
const response = await fetch("https://api.vercel.com/v1/purge", {
|
|
150
|
+
method: "POST",
|
|
151
|
+
headers: {
|
|
152
|
+
Authorization: `Bearer ${apiKey}`,
|
|
153
|
+
"Content-Type": "application/json"
|
|
154
|
+
},
|
|
155
|
+
body: JSON.stringify({ urls })
|
|
156
|
+
});
|
|
157
|
+
const data = await response.json();
|
|
158
|
+
return {
|
|
159
|
+
success: response.ok,
|
|
160
|
+
purged: urls.length,
|
|
161
|
+
errors: data.error ? [data.error.message] : void 0
|
|
162
|
+
};
|
|
163
|
+
} catch (error) {
|
|
164
|
+
return {
|
|
165
|
+
success: false,
|
|
166
|
+
purged: 0,
|
|
167
|
+
errors: [error instanceof Error ? error.message : "Unknown error"]
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
async function purgeFastly(urls, config) {
|
|
172
|
+
const { apiKey } = config;
|
|
173
|
+
if (!apiKey) {
|
|
174
|
+
throw new Error("Fastly API key required");
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
const results = await Promise.all(
|
|
178
|
+
urls.map(async (url) => {
|
|
179
|
+
const response = await fetch(url, {
|
|
180
|
+
method: "PURGE",
|
|
181
|
+
headers: {
|
|
182
|
+
"Fastly-Key": apiKey
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
return response.ok;
|
|
186
|
+
})
|
|
187
|
+
);
|
|
188
|
+
const purged = results.filter(Boolean).length;
|
|
189
|
+
return {
|
|
190
|
+
success: purged === urls.length,
|
|
191
|
+
purged
|
|
192
|
+
};
|
|
193
|
+
} catch (error) {
|
|
194
|
+
return {
|
|
195
|
+
success: false,
|
|
196
|
+
purged: 0,
|
|
197
|
+
errors: [error instanceof Error ? error.message : "Unknown error"]
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
async function purgeCacheByTag(tags, config) {
|
|
202
|
+
const { provider, apiKey, zoneId } = config;
|
|
203
|
+
if (provider === "cloudflare") {
|
|
204
|
+
if (!(apiKey && zoneId)) {
|
|
205
|
+
throw new Error("Cloudflare API key and zone ID required");
|
|
206
|
+
}
|
|
207
|
+
try {
|
|
208
|
+
const response = await fetch(
|
|
209
|
+
`https://api.cloudflare.com/client/v4/zones/${zoneId}/purge_cache`,
|
|
210
|
+
{
|
|
211
|
+
method: "POST",
|
|
212
|
+
headers: {
|
|
213
|
+
Authorization: `Bearer ${apiKey}`,
|
|
214
|
+
"Content-Type": "application/json"
|
|
215
|
+
},
|
|
216
|
+
body: JSON.stringify({ tags })
|
|
217
|
+
}
|
|
218
|
+
);
|
|
219
|
+
const data = await response.json();
|
|
220
|
+
return {
|
|
221
|
+
success: data.success,
|
|
222
|
+
purged: tags.length,
|
|
223
|
+
errors: data.errors
|
|
224
|
+
};
|
|
225
|
+
} catch (error) {
|
|
226
|
+
return {
|
|
227
|
+
success: false,
|
|
228
|
+
purged: 0,
|
|
229
|
+
errors: [error instanceof Error ? error.message : "Unknown error"]
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
throw new Error(`Cache tag purging not supported for ${provider}`);
|
|
234
|
+
}
|
|
235
|
+
async function purgeAllCache(config) {
|
|
236
|
+
const { provider, apiKey, zoneId } = config;
|
|
237
|
+
if (provider === "cloudflare") {
|
|
238
|
+
if (!(apiKey && zoneId)) {
|
|
239
|
+
throw new Error("Cloudflare API key and zone ID required");
|
|
240
|
+
}
|
|
241
|
+
try {
|
|
242
|
+
const response = await fetch(
|
|
243
|
+
`https://api.cloudflare.com/client/v4/zones/${zoneId}/purge_cache`,
|
|
244
|
+
{
|
|
245
|
+
method: "POST",
|
|
246
|
+
headers: {
|
|
247
|
+
Authorization: `Bearer ${apiKey}`,
|
|
248
|
+
"Content-Type": "application/json"
|
|
249
|
+
},
|
|
250
|
+
body: JSON.stringify({ purge_everything: true })
|
|
251
|
+
}
|
|
252
|
+
);
|
|
253
|
+
const data = await response.json();
|
|
254
|
+
return {
|
|
255
|
+
success: data.success,
|
|
256
|
+
errors: data.errors
|
|
257
|
+
};
|
|
258
|
+
} catch (error) {
|
|
259
|
+
return {
|
|
260
|
+
success: false,
|
|
261
|
+
errors: [error instanceof Error ? error.message : "Unknown error"]
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
throw new Error(`Purge all not supported for ${provider}`);
|
|
266
|
+
}
|
|
267
|
+
async function warmCDNCache(urls, options = {}) {
|
|
268
|
+
const { concurrency = 5, headers = {} } = options;
|
|
269
|
+
const results = [];
|
|
270
|
+
const chunks = [];
|
|
271
|
+
for (let i = 0; i < urls.length; i += concurrency) {
|
|
272
|
+
chunks.push(urls.slice(i, i + concurrency));
|
|
273
|
+
}
|
|
274
|
+
for (const chunk of chunks) {
|
|
275
|
+
const chunkResults = await Promise.all(
|
|
276
|
+
chunk.map(async (url) => {
|
|
277
|
+
try {
|
|
278
|
+
const response = await fetch(url, { headers });
|
|
279
|
+
return {
|
|
280
|
+
success: response.ok,
|
|
281
|
+
error: response.ok ? void 0 : `${response.status} ${response.statusText}`
|
|
282
|
+
};
|
|
283
|
+
} catch (error) {
|
|
284
|
+
return {
|
|
285
|
+
success: false,
|
|
286
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
})
|
|
290
|
+
);
|
|
291
|
+
results.push(...chunkResults);
|
|
292
|
+
}
|
|
293
|
+
const warmed = results.filter((r) => r.success).length;
|
|
294
|
+
const failed = results.filter((r) => !r.success).length;
|
|
295
|
+
const errors = results.flatMap((r) => r.error ? [r.error] : []);
|
|
296
|
+
return { warmed, failed, errors };
|
|
297
|
+
}
|
|
298
|
+
function generateCacheTags(resource) {
|
|
299
|
+
const tags = [];
|
|
300
|
+
tags.push(resource.type);
|
|
301
|
+
if (resource.id) {
|
|
302
|
+
tags.push(`${resource.type}:${resource.id}`);
|
|
303
|
+
}
|
|
304
|
+
if (resource.related) {
|
|
305
|
+
tags.push(...resource.related);
|
|
306
|
+
}
|
|
307
|
+
return tags;
|
|
308
|
+
}
|
|
309
|
+
function generateVercelCacheConfig(preset) {
|
|
310
|
+
const config = CDN_CACHE_PRESETS[preset];
|
|
311
|
+
const cacheControl = generateCacheControl(config);
|
|
312
|
+
return {
|
|
313
|
+
headers: {
|
|
314
|
+
"Cache-Control": cacheControl,
|
|
315
|
+
"CDN-Cache-Control": cacheControl,
|
|
316
|
+
"Vercel-CDN-Cache-Control": cacheControl
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
function generateCloudflareConfig(preset, options = {}) {
|
|
321
|
+
const config = CDN_CACHE_PRESETS[preset];
|
|
322
|
+
const cacheControl = generateCacheControl(config);
|
|
323
|
+
const headers = {
|
|
324
|
+
"Cache-Control": cacheControl
|
|
325
|
+
};
|
|
326
|
+
if (options.cacheTags && options.cacheTags.length > 0) {
|
|
327
|
+
headers["Cache-Tag"] = options.cacheTags.join(",");
|
|
328
|
+
}
|
|
329
|
+
if (options.bypassOnCookie) {
|
|
330
|
+
headers["Cache-Control"] = `${cacheControl}, bypass=${options.bypassOnCookie}`;
|
|
331
|
+
}
|
|
332
|
+
return { headers };
|
|
333
|
+
}
|
|
334
|
+
function shouldCacheResponse(status, headers) {
|
|
335
|
+
if (status >= 400) {
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
const cacheControl = headers.get("cache-control") || "";
|
|
339
|
+
if (cacheControl.includes("no-store") || cacheControl.includes("no-cache") || cacheControl.includes("private")) {
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
return true;
|
|
343
|
+
}
|
|
344
|
+
function getCacheTTL(headers) {
|
|
345
|
+
const cacheControl = headers.get("cache-control") || "";
|
|
346
|
+
const sMaxAgeMatch = cacheControl.match(/s-maxage=(\d+)/);
|
|
347
|
+
if (sMaxAgeMatch?.[1]) {
|
|
348
|
+
return parseInt(sMaxAgeMatch[1], 10);
|
|
349
|
+
}
|
|
350
|
+
const maxAgeMatch = cacheControl.match(/max-age=(\d+)/);
|
|
351
|
+
if (maxAgeMatch?.[1]) {
|
|
352
|
+
return parseInt(maxAgeMatch[1], 10);
|
|
353
|
+
}
|
|
354
|
+
const expires = headers.get("expires");
|
|
355
|
+
if (expires) {
|
|
356
|
+
const expiresDate = new Date(expires);
|
|
357
|
+
const now = /* @__PURE__ */ new Date();
|
|
358
|
+
return Math.max(0, Math.floor((expiresDate.getTime() - now.getTime()) / 1e3));
|
|
359
|
+
}
|
|
360
|
+
return 0;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// src/logger.ts
|
|
364
|
+
var cacheLogger = console;
|
|
365
|
+
function configureCacheLogger(logger) {
|
|
366
|
+
cacheLogger = logger;
|
|
367
|
+
}
|
|
368
|
+
function getCacheLogger() {
|
|
369
|
+
return cacheLogger;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// src/edge-cache.ts
|
|
373
|
+
var ISR_PRESETS = {
|
|
374
|
+
// Revalidate every request
|
|
375
|
+
always: {
|
|
376
|
+
revalidate: 0
|
|
377
|
+
},
|
|
378
|
+
// Revalidate every minute
|
|
379
|
+
minute: {
|
|
380
|
+
revalidate: 60
|
|
381
|
+
},
|
|
382
|
+
// Revalidate every 5 minutes
|
|
383
|
+
fiveMinutes: {
|
|
384
|
+
revalidate: 300
|
|
385
|
+
},
|
|
386
|
+
// Revalidate every hour
|
|
387
|
+
hourly: {
|
|
388
|
+
revalidate: 3600
|
|
389
|
+
},
|
|
390
|
+
// Revalidate daily
|
|
391
|
+
daily: {
|
|
392
|
+
revalidate: 86400
|
|
393
|
+
},
|
|
394
|
+
// Never revalidate (static)
|
|
395
|
+
never: {
|
|
396
|
+
revalidate: false
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
async function generateStaticParams(fetchFn, mapFn) {
|
|
400
|
+
try {
|
|
401
|
+
const items = await fetchFn();
|
|
402
|
+
return items.map(mapFn);
|
|
403
|
+
} catch (error) {
|
|
404
|
+
getCacheLogger().error(
|
|
405
|
+
"Failed to generate static params",
|
|
406
|
+
error instanceof Error ? error : new Error(String(error))
|
|
407
|
+
);
|
|
408
|
+
return [];
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
async function revalidateTag(tag, secret) {
|
|
412
|
+
const baseUrl = process.env.NEXT_PUBLIC_URL;
|
|
413
|
+
if (!baseUrl) {
|
|
414
|
+
getCacheLogger().warn("revalidateTag skipped: NEXT_PUBLIC_URL is not configured", { tag });
|
|
415
|
+
return { revalidated: false, error: "NEXT_PUBLIC_URL is not configured" };
|
|
416
|
+
}
|
|
417
|
+
try {
|
|
418
|
+
const url = new URL("/api/revalidate", baseUrl);
|
|
419
|
+
const headers = { "Content-Type": "application/json" };
|
|
420
|
+
if (secret) {
|
|
421
|
+
headers["x-revalidate-secret"] = secret;
|
|
422
|
+
}
|
|
423
|
+
const response = await fetch(url.toString(), {
|
|
424
|
+
method: "POST",
|
|
425
|
+
headers,
|
|
426
|
+
body: JSON.stringify({ tag })
|
|
427
|
+
});
|
|
428
|
+
const data = await response.json();
|
|
429
|
+
if (!response.ok) {
|
|
430
|
+
getCacheLogger().warn("revalidateTag failed", {
|
|
431
|
+
tag,
|
|
432
|
+
status: response.status,
|
|
433
|
+
error: data.error
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
return {
|
|
437
|
+
revalidated: response.ok,
|
|
438
|
+
error: data.error
|
|
439
|
+
};
|
|
440
|
+
} catch (error) {
|
|
441
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
442
|
+
getCacheLogger().warn("revalidateTag error", { tag, error: message });
|
|
443
|
+
return {
|
|
444
|
+
revalidated: false,
|
|
445
|
+
error: message
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
async function revalidatePath(path, secret) {
|
|
450
|
+
const baseUrl = process.env.NEXT_PUBLIC_URL;
|
|
451
|
+
if (!baseUrl) {
|
|
452
|
+
getCacheLogger().warn("revalidatePath skipped: NEXT_PUBLIC_URL is not configured", { path });
|
|
453
|
+
return { revalidated: false, error: "NEXT_PUBLIC_URL is not configured" };
|
|
454
|
+
}
|
|
455
|
+
try {
|
|
456
|
+
const url = new URL("/api/revalidate", baseUrl);
|
|
457
|
+
const headers = { "Content-Type": "application/json" };
|
|
458
|
+
if (secret) {
|
|
459
|
+
headers["x-revalidate-secret"] = secret;
|
|
460
|
+
}
|
|
461
|
+
const response = await fetch(url.toString(), {
|
|
462
|
+
method: "POST",
|
|
463
|
+
headers,
|
|
464
|
+
body: JSON.stringify({ path })
|
|
465
|
+
});
|
|
466
|
+
const data = await response.json();
|
|
467
|
+
return {
|
|
468
|
+
revalidated: response.ok,
|
|
469
|
+
error: data.error
|
|
470
|
+
};
|
|
471
|
+
} catch (error) {
|
|
472
|
+
return {
|
|
473
|
+
revalidated: false,
|
|
474
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
async function revalidatePaths(paths, secret) {
|
|
479
|
+
const results = await Promise.allSettled(paths.map((path) => revalidatePath(path, secret)));
|
|
480
|
+
let revalidated = 0;
|
|
481
|
+
let failed = 0;
|
|
482
|
+
const errors = [];
|
|
483
|
+
for (let i = 0; i < results.length; i++) {
|
|
484
|
+
const result = results[i];
|
|
485
|
+
const path = paths[i];
|
|
486
|
+
if (!(result && path)) {
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
if (result.status === "fulfilled" && result.value.revalidated) {
|
|
490
|
+
revalidated++;
|
|
491
|
+
} else {
|
|
492
|
+
failed++;
|
|
493
|
+
const error = result.status === "fulfilled" ? result.value.error || "Unknown error" : String(result.reason) || "Unknown error";
|
|
494
|
+
errors.push({ path, error });
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return { revalidated, failed, errors };
|
|
498
|
+
}
|
|
499
|
+
async function revalidateTags(tags, secret) {
|
|
500
|
+
const results = await Promise.allSettled(tags.map((tag) => revalidateTag(tag, secret)));
|
|
501
|
+
let revalidated = 0;
|
|
502
|
+
let failed = 0;
|
|
503
|
+
const errors = [];
|
|
504
|
+
for (let i = 0; i < results.length; i++) {
|
|
505
|
+
const result = results[i];
|
|
506
|
+
const tag = tags[i];
|
|
507
|
+
if (!(result && tag)) {
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
if (result.status === "fulfilled" && result.value.revalidated) {
|
|
511
|
+
revalidated++;
|
|
512
|
+
} else {
|
|
513
|
+
failed++;
|
|
514
|
+
const error = result.status === "fulfilled" ? result.value.error || "Unknown error" : String(result.reason) || "Unknown error";
|
|
515
|
+
errors.push({ tag, error });
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return { revalidated, failed, errors };
|
|
519
|
+
}
|
|
520
|
+
function createEdgeCachedFetch(config = {}) {
|
|
521
|
+
return async (url, options) => {
|
|
522
|
+
const fetchOptions = {
|
|
523
|
+
...options,
|
|
524
|
+
...config,
|
|
525
|
+
next: {
|
|
526
|
+
...options?.next,
|
|
527
|
+
...config.next
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
const response = await fetch(url, fetchOptions);
|
|
531
|
+
if (!response.ok) {
|
|
532
|
+
throw new Error(`Fetch failed: ${response.statusText}`);
|
|
533
|
+
}
|
|
534
|
+
return response.json();
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
function createCachedFunction(fn, options = {}) {
|
|
538
|
+
if (options.revalidate === false) {
|
|
539
|
+
return fn;
|
|
540
|
+
}
|
|
541
|
+
const ttlMs = (options.revalidate ?? 60) * 1e3;
|
|
542
|
+
const cache = /* @__PURE__ */ new Map();
|
|
543
|
+
return async (...args) => {
|
|
544
|
+
const key = JSON.stringify(args);
|
|
545
|
+
const now = Date.now();
|
|
546
|
+
const cached = cache.get(key);
|
|
547
|
+
if (cached && now < cached.expiresAt) {
|
|
548
|
+
return cached.value;
|
|
549
|
+
}
|
|
550
|
+
const value = await fn(...args);
|
|
551
|
+
cache.set(key, { value, expiresAt: now + ttlMs });
|
|
552
|
+
return value;
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
var EdgeRateLimiter = class {
|
|
556
|
+
constructor(config) {
|
|
557
|
+
this.config = config;
|
|
558
|
+
}
|
|
559
|
+
cache = /* @__PURE__ */ new Map();
|
|
560
|
+
/**
|
|
561
|
+
* Check rate limit
|
|
562
|
+
*/
|
|
563
|
+
check(request) {
|
|
564
|
+
const key = this.config.key ? this.config.key(request) : request.headers.get("x-forwarded-for") || "unknown";
|
|
565
|
+
const now = Date.now();
|
|
566
|
+
let entry = this.cache.get(key);
|
|
567
|
+
if (!entry || now > entry.resetTime) {
|
|
568
|
+
entry = {
|
|
569
|
+
count: 0,
|
|
570
|
+
resetTime: now + this.config.window
|
|
571
|
+
};
|
|
572
|
+
this.cache.set(key, entry);
|
|
573
|
+
}
|
|
574
|
+
entry.count++;
|
|
575
|
+
const allowed = entry.count <= this.config.limit;
|
|
576
|
+
const remaining = Math.max(0, this.config.limit - entry.count);
|
|
577
|
+
return {
|
|
578
|
+
allowed,
|
|
579
|
+
limit: this.config.limit,
|
|
580
|
+
remaining,
|
|
581
|
+
reset: entry.resetTime
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Clean up expired entries
|
|
586
|
+
*/
|
|
587
|
+
cleanup() {
|
|
588
|
+
const now = Date.now();
|
|
589
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
590
|
+
if (now > entry.resetTime) {
|
|
591
|
+
this.cache.delete(key);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
function getGeoLocation(request) {
|
|
597
|
+
const country = request.headers.get("x-vercel-ip-country");
|
|
598
|
+
const region = request.headers.get("x-vercel-ip-country-region");
|
|
599
|
+
const city = request.headers.get("x-vercel-ip-city");
|
|
600
|
+
const latitude = request.headers.get("x-vercel-ip-latitude");
|
|
601
|
+
const longitude = request.headers.get("x-vercel-ip-longitude");
|
|
602
|
+
if (!country) {
|
|
603
|
+
const cfCountry = request.headers.get("cf-ipcountry");
|
|
604
|
+
if (cfCountry) {
|
|
605
|
+
return {
|
|
606
|
+
country: cfCountry
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
return null;
|
|
610
|
+
}
|
|
611
|
+
return {
|
|
612
|
+
country: country || void 0,
|
|
613
|
+
region: region || void 0,
|
|
614
|
+
city: city ? decodeURIComponent(city) : void 0,
|
|
615
|
+
latitude: latitude ? parseFloat(latitude) : void 0,
|
|
616
|
+
longitude: longitude ? parseFloat(longitude) : void 0
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
function getABTestVariant(request, testName, variants) {
|
|
620
|
+
const cookieName = `ab-test-${testName}`;
|
|
621
|
+
const cookieVariant = request.cookies.get(cookieName)?.value;
|
|
622
|
+
if (cookieVariant && variants.includes(cookieVariant)) {
|
|
623
|
+
return cookieVariant;
|
|
624
|
+
}
|
|
625
|
+
const ip = request.headers.get("x-forwarded-for") || "unknown";
|
|
626
|
+
const hash = simpleHash(ip + testName);
|
|
627
|
+
const variantIndex = hash % variants.length;
|
|
628
|
+
const variant = variants[variantIndex];
|
|
629
|
+
if (!variant) {
|
|
630
|
+
throw new Error("No variant found for A/B test");
|
|
631
|
+
}
|
|
632
|
+
return variant;
|
|
633
|
+
}
|
|
634
|
+
function simpleHash(str) {
|
|
635
|
+
let hash = 0;
|
|
636
|
+
for (let i = 0; i < str.length; i++) {
|
|
637
|
+
const char = str.charCodeAt(i);
|
|
638
|
+
hash = (hash << 5) - hash + char;
|
|
639
|
+
hash = hash & hash;
|
|
640
|
+
}
|
|
641
|
+
return Math.abs(hash);
|
|
642
|
+
}
|
|
643
|
+
function getPersonalizationConfig(request) {
|
|
644
|
+
const userAgent = request.headers.get("user-agent") || "";
|
|
645
|
+
const device = getDeviceType(userAgent);
|
|
646
|
+
const location = getGeoLocation(request);
|
|
647
|
+
return {
|
|
648
|
+
userId: request.cookies.get("user-id")?.value,
|
|
649
|
+
location: location || void 0,
|
|
650
|
+
device
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
function getDeviceType(userAgent) {
|
|
654
|
+
if (/mobile/i.test(userAgent) && !/tablet|ipad/i.test(userAgent)) {
|
|
655
|
+
return "mobile";
|
|
656
|
+
}
|
|
657
|
+
if (/tablet|ipad/i.test(userAgent)) {
|
|
658
|
+
return "tablet";
|
|
659
|
+
}
|
|
660
|
+
return "desktop";
|
|
661
|
+
}
|
|
662
|
+
function setEdgeCacheHeaders(response, config) {
|
|
663
|
+
const cacheControl = [];
|
|
664
|
+
if (config.maxAge !== void 0) {
|
|
665
|
+
cacheControl.push(`max-age=${config.maxAge}`);
|
|
666
|
+
}
|
|
667
|
+
if (config.sMaxAge !== void 0) {
|
|
668
|
+
cacheControl.push(`s-maxage=${config.sMaxAge}`);
|
|
669
|
+
}
|
|
670
|
+
if (config.staleWhileRevalidate !== void 0) {
|
|
671
|
+
cacheControl.push(`stale-while-revalidate=${config.staleWhileRevalidate}`);
|
|
672
|
+
}
|
|
673
|
+
if (cacheControl.length > 0) {
|
|
674
|
+
response.headers.set("Cache-Control", cacheControl.join(", "));
|
|
675
|
+
}
|
|
676
|
+
if (config.tags && config.tags.length > 0) {
|
|
677
|
+
response.headers.set("Cache-Tag", config.tags.join(","));
|
|
678
|
+
}
|
|
679
|
+
return response;
|
|
680
|
+
}
|
|
681
|
+
function addPreloadLinks(response, resources) {
|
|
682
|
+
const links = resources.map((resource) => {
|
|
683
|
+
const attrs = [`<${resource.href}>`, `rel="preload"`, `as="${resource.as}"`];
|
|
684
|
+
if (resource.type) {
|
|
685
|
+
attrs.push(`type="${resource.type}"`);
|
|
686
|
+
}
|
|
687
|
+
if (resource.crossorigin) {
|
|
688
|
+
attrs.push("crossorigin");
|
|
689
|
+
}
|
|
690
|
+
return attrs.join("; ");
|
|
691
|
+
});
|
|
692
|
+
if (links.length > 0) {
|
|
693
|
+
response.headers.set("Link", links.join(", "));
|
|
694
|
+
}
|
|
695
|
+
return response;
|
|
696
|
+
}
|
|
697
|
+
async function warmISRCache(paths, baseURL = process.env.NEXT_PUBLIC_URL || "http://localhost:3000") {
|
|
698
|
+
const results = await Promise.allSettled(
|
|
699
|
+
paths.map(async (path) => {
|
|
700
|
+
const url = new URL(path, baseURL);
|
|
701
|
+
const response = await fetch(url.toString());
|
|
702
|
+
if (!response.ok) {
|
|
703
|
+
throw new Error(`${response.status} ${response.statusText}`);
|
|
704
|
+
}
|
|
705
|
+
return true;
|
|
706
|
+
})
|
|
707
|
+
);
|
|
708
|
+
let warmed = 0;
|
|
709
|
+
let failed = 0;
|
|
710
|
+
const errors = [];
|
|
711
|
+
for (let i = 0; i < results.length; i++) {
|
|
712
|
+
const result = results[i];
|
|
713
|
+
const path = paths[i];
|
|
714
|
+
if (!(result && path)) {
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
717
|
+
if (result.status === "fulfilled") {
|
|
718
|
+
warmed++;
|
|
719
|
+
} else {
|
|
720
|
+
failed++;
|
|
721
|
+
errors.push({
|
|
722
|
+
path,
|
|
723
|
+
error: result.reason instanceof Error ? result.reason.message : String(result.reason) || "Unknown error"
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
return { warmed, failed, errors };
|
|
728
|
+
}
|
|
729
|
+
export {
|
|
730
|
+
CDN_CACHE_PRESETS,
|
|
731
|
+
DEFAULT_CDN_CONFIG,
|
|
732
|
+
EdgeRateLimiter,
|
|
733
|
+
ISR_PRESETS,
|
|
734
|
+
addPreloadLinks,
|
|
735
|
+
configureCacheLogger,
|
|
736
|
+
createCachedFunction,
|
|
737
|
+
createEdgeCachedFetch,
|
|
738
|
+
generateCacheControl,
|
|
739
|
+
generateCacheTags,
|
|
740
|
+
generateCloudflareConfig,
|
|
741
|
+
generateStaticParams,
|
|
742
|
+
generateVercelCacheConfig,
|
|
743
|
+
getABTestVariant,
|
|
744
|
+
getCacheLogger,
|
|
745
|
+
getCacheTTL,
|
|
746
|
+
getGeoLocation,
|
|
747
|
+
getPersonalizationConfig,
|
|
748
|
+
purgeAllCache,
|
|
749
|
+
purgeCDNCache,
|
|
750
|
+
purgeCacheByTag,
|
|
751
|
+
revalidatePath,
|
|
752
|
+
revalidatePaths,
|
|
753
|
+
revalidateTag,
|
|
754
|
+
revalidateTags,
|
|
755
|
+
setEdgeCacheHeaders,
|
|
756
|
+
shouldCacheResponse,
|
|
757
|
+
warmCDNCache,
|
|
758
|
+
warmISRCache
|
|
759
|
+
};
|
|
760
|
+
//# sourceMappingURL=index.js.map
|