@revealui/cache 0.0.0-canary-20260409021642

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/index.js ADDED
@@ -0,0 +1,926 @@
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
+ for (const directive of cacheControl.split(",")) {
347
+ const trimmed = directive.trim();
348
+ if (trimmed.startsWith("s-maxage=")) {
349
+ const val = trimmed.slice("s-maxage=".length);
350
+ const num = Number.parseInt(val, 10);
351
+ if (!Number.isNaN(num)) return num;
352
+ }
353
+ }
354
+ for (const directive of cacheControl.split(",")) {
355
+ const trimmed = directive.trim();
356
+ if (trimmed.startsWith("max-age=")) {
357
+ const val = trimmed.slice("max-age=".length);
358
+ const num = Number.parseInt(val, 10);
359
+ if (!Number.isNaN(num)) return num;
360
+ }
361
+ }
362
+ const expires = headers.get("expires");
363
+ if (expires) {
364
+ const expiresDate = new Date(expires);
365
+ const now = /* @__PURE__ */ new Date();
366
+ return Math.max(0, Math.floor((expiresDate.getTime() - now.getTime()) / 1e3));
367
+ }
368
+ return 0;
369
+ }
370
+
371
+ // src/logger.ts
372
+ var cacheLogger = console;
373
+ function configureCacheLogger(logger) {
374
+ cacheLogger = logger;
375
+ }
376
+ function getCacheLogger() {
377
+ return cacheLogger;
378
+ }
379
+
380
+ // src/edge-cache.ts
381
+ var ISR_PRESETS = {
382
+ // Revalidate every request
383
+ always: {
384
+ revalidate: 0
385
+ },
386
+ // Revalidate every minute
387
+ minute: {
388
+ revalidate: 60
389
+ },
390
+ // Revalidate every 5 minutes
391
+ fiveMinutes: {
392
+ revalidate: 300
393
+ },
394
+ // Revalidate every hour
395
+ hourly: {
396
+ revalidate: 3600
397
+ },
398
+ // Revalidate daily
399
+ daily: {
400
+ revalidate: 86400
401
+ },
402
+ // Never revalidate (static)
403
+ never: {
404
+ revalidate: false
405
+ }
406
+ };
407
+ async function generateStaticParams(fetchFn, mapFn) {
408
+ try {
409
+ const items = await fetchFn();
410
+ return items.map(mapFn);
411
+ } catch (error) {
412
+ getCacheLogger().error(
413
+ "Failed to generate static params",
414
+ error instanceof Error ? error : new Error(String(error))
415
+ );
416
+ return [];
417
+ }
418
+ }
419
+ async function revalidateTag(tag, secret) {
420
+ const baseUrl = process.env.NEXT_PUBLIC_URL;
421
+ if (!baseUrl) {
422
+ getCacheLogger().warn("revalidateTag skipped: NEXT_PUBLIC_URL is not configured", { tag });
423
+ return { revalidated: false, error: "NEXT_PUBLIC_URL is not configured" };
424
+ }
425
+ try {
426
+ const url = new URL("/api/revalidate", baseUrl);
427
+ const headers = { "Content-Type": "application/json" };
428
+ if (secret) {
429
+ headers["x-revalidate-secret"] = secret;
430
+ }
431
+ const response = await fetch(url.toString(), {
432
+ method: "POST",
433
+ headers,
434
+ body: JSON.stringify({ tag })
435
+ });
436
+ const data = await response.json();
437
+ if (!response.ok) {
438
+ getCacheLogger().warn("revalidateTag failed", {
439
+ tag,
440
+ status: response.status,
441
+ error: data.error
442
+ });
443
+ }
444
+ return {
445
+ revalidated: response.ok,
446
+ error: data.error
447
+ };
448
+ } catch (error) {
449
+ const message = error instanceof Error ? error.message : "Unknown error";
450
+ getCacheLogger().warn("revalidateTag error", { tag, error: message });
451
+ return {
452
+ revalidated: false,
453
+ error: message
454
+ };
455
+ }
456
+ }
457
+ async function revalidatePath(path, secret) {
458
+ const baseUrl = process.env.NEXT_PUBLIC_URL;
459
+ if (!baseUrl) {
460
+ getCacheLogger().warn("revalidatePath skipped: NEXT_PUBLIC_URL is not configured", { path });
461
+ return { revalidated: false, error: "NEXT_PUBLIC_URL is not configured" };
462
+ }
463
+ try {
464
+ const url = new URL("/api/revalidate", baseUrl);
465
+ const headers = { "Content-Type": "application/json" };
466
+ if (secret) {
467
+ headers["x-revalidate-secret"] = secret;
468
+ }
469
+ const response = await fetch(url.toString(), {
470
+ method: "POST",
471
+ headers,
472
+ body: JSON.stringify({ path })
473
+ });
474
+ const data = await response.json();
475
+ return {
476
+ revalidated: response.ok,
477
+ error: data.error
478
+ };
479
+ } catch (error) {
480
+ return {
481
+ revalidated: false,
482
+ error: error instanceof Error ? error.message : "Unknown error"
483
+ };
484
+ }
485
+ }
486
+ async function revalidatePaths(paths, secret) {
487
+ const results = await Promise.allSettled(paths.map((path) => revalidatePath(path, secret)));
488
+ let revalidated = 0;
489
+ let failed = 0;
490
+ const errors = [];
491
+ for (let i = 0; i < results.length; i++) {
492
+ const result = results[i];
493
+ const path = paths[i];
494
+ if (!(result && path)) {
495
+ continue;
496
+ }
497
+ if (result.status === "fulfilled" && result.value.revalidated) {
498
+ revalidated++;
499
+ } else {
500
+ failed++;
501
+ const error = result.status === "fulfilled" ? result.value.error || "Unknown error" : String(result.reason) || "Unknown error";
502
+ errors.push({ path, error });
503
+ }
504
+ }
505
+ return { revalidated, failed, errors };
506
+ }
507
+ async function revalidateTags(tags, secret) {
508
+ const results = await Promise.allSettled(tags.map((tag) => revalidateTag(tag, secret)));
509
+ let revalidated = 0;
510
+ let failed = 0;
511
+ const errors = [];
512
+ for (let i = 0; i < results.length; i++) {
513
+ const result = results[i];
514
+ const tag = tags[i];
515
+ if (!(result && tag)) {
516
+ continue;
517
+ }
518
+ if (result.status === "fulfilled" && result.value.revalidated) {
519
+ revalidated++;
520
+ } else {
521
+ failed++;
522
+ const error = result.status === "fulfilled" ? result.value.error || "Unknown error" : String(result.reason) || "Unknown error";
523
+ errors.push({ tag, error });
524
+ }
525
+ }
526
+ return { revalidated, failed, errors };
527
+ }
528
+ function createEdgeCachedFetch(config = {}) {
529
+ return async (url, options) => {
530
+ const fetchOptions = {
531
+ ...options,
532
+ ...config,
533
+ next: {
534
+ ...options?.next,
535
+ ...config.next
536
+ }
537
+ };
538
+ const response = await fetch(url, fetchOptions);
539
+ if (!response.ok) {
540
+ throw new Error(`Fetch failed: ${response.statusText}`);
541
+ }
542
+ return response.json();
543
+ };
544
+ }
545
+ function createCachedFunction(fn, options = {}) {
546
+ if (options.revalidate === false) {
547
+ return fn;
548
+ }
549
+ const ttlMs = (options.revalidate ?? 60) * 1e3;
550
+ const cache = /* @__PURE__ */ new Map();
551
+ return async (...args) => {
552
+ const key = JSON.stringify(args);
553
+ const now = Date.now();
554
+ const cached = cache.get(key);
555
+ if (cached && now < cached.expiresAt) {
556
+ return cached.value;
557
+ }
558
+ const value = await fn(...args);
559
+ cache.set(key, { value, expiresAt: now + ttlMs });
560
+ return value;
561
+ };
562
+ }
563
+ var EdgeRateLimiter = class {
564
+ constructor(config) {
565
+ this.config = config;
566
+ }
567
+ cache = /* @__PURE__ */ new Map();
568
+ /**
569
+ * Check rate limit
570
+ */
571
+ check(request) {
572
+ const key = this.config.key ? this.config.key(request) : request.headers.get("x-forwarded-for") || "unknown";
573
+ const now = Date.now();
574
+ let entry = this.cache.get(key);
575
+ if (!entry || now > entry.resetTime) {
576
+ entry = {
577
+ count: 0,
578
+ resetTime: now + this.config.window
579
+ };
580
+ this.cache.set(key, entry);
581
+ }
582
+ entry.count++;
583
+ const allowed = entry.count <= this.config.limit;
584
+ const remaining = Math.max(0, this.config.limit - entry.count);
585
+ return {
586
+ allowed,
587
+ limit: this.config.limit,
588
+ remaining,
589
+ reset: entry.resetTime
590
+ };
591
+ }
592
+ /**
593
+ * Clean up expired entries
594
+ */
595
+ cleanup() {
596
+ const now = Date.now();
597
+ for (const [key, entry] of this.cache.entries()) {
598
+ if (now > entry.resetTime) {
599
+ this.cache.delete(key);
600
+ }
601
+ }
602
+ }
603
+ };
604
+ function getGeoLocation(request) {
605
+ const country = request.headers.get("x-vercel-ip-country");
606
+ const region = request.headers.get("x-vercel-ip-country-region");
607
+ const city = request.headers.get("x-vercel-ip-city");
608
+ const latitude = request.headers.get("x-vercel-ip-latitude");
609
+ const longitude = request.headers.get("x-vercel-ip-longitude");
610
+ if (!country) {
611
+ const cfCountry = request.headers.get("cf-ipcountry");
612
+ if (cfCountry) {
613
+ return {
614
+ country: cfCountry
615
+ };
616
+ }
617
+ return null;
618
+ }
619
+ return {
620
+ country: country || void 0,
621
+ region: region || void 0,
622
+ city: city ? decodeURIComponent(city) : void 0,
623
+ latitude: latitude ? parseFloat(latitude) : void 0,
624
+ longitude: longitude ? parseFloat(longitude) : void 0
625
+ };
626
+ }
627
+ function getABTestVariant(request, testName, variants) {
628
+ const cookieName = `ab-test-${testName}`;
629
+ const cookieVariant = request.cookies.get(cookieName)?.value;
630
+ if (cookieVariant && variants.includes(cookieVariant)) {
631
+ return cookieVariant;
632
+ }
633
+ const ip = request.headers.get("x-forwarded-for") || "unknown";
634
+ const hash = simpleHash(ip + testName);
635
+ const variantIndex = hash % variants.length;
636
+ const variant = variants[variantIndex];
637
+ if (!variant) {
638
+ throw new Error("No variant found for A/B test");
639
+ }
640
+ return variant;
641
+ }
642
+ function simpleHash(str) {
643
+ let hash = 0;
644
+ for (let i = 0; i < str.length; i++) {
645
+ const char = str.charCodeAt(i);
646
+ hash = (hash << 5) - hash + char;
647
+ hash = hash & hash;
648
+ }
649
+ return Math.abs(hash);
650
+ }
651
+ function getPersonalizationConfig(request) {
652
+ const userAgent = request.headers.get("user-agent") || "";
653
+ const device = getDeviceType(userAgent);
654
+ const location = getGeoLocation(request);
655
+ return {
656
+ userId: request.cookies.get("user-id")?.value,
657
+ location: location || void 0,
658
+ device
659
+ };
660
+ }
661
+ function getDeviceType(userAgent) {
662
+ const ua = userAgent.toLowerCase();
663
+ const isTablet = ua.includes("tablet") || ua.includes("ipad");
664
+ if (isTablet) return "tablet";
665
+ if (ua.includes("mobile")) return "mobile";
666
+ return "desktop";
667
+ }
668
+ function setEdgeCacheHeaders(response, config) {
669
+ const cacheControl = [];
670
+ if (config.maxAge !== void 0) {
671
+ cacheControl.push(`max-age=${config.maxAge}`);
672
+ }
673
+ if (config.sMaxAge !== void 0) {
674
+ cacheControl.push(`s-maxage=${config.sMaxAge}`);
675
+ }
676
+ if (config.staleWhileRevalidate !== void 0) {
677
+ cacheControl.push(`stale-while-revalidate=${config.staleWhileRevalidate}`);
678
+ }
679
+ if (cacheControl.length > 0) {
680
+ response.headers.set("Cache-Control", cacheControl.join(", "));
681
+ }
682
+ if (config.tags && config.tags.length > 0) {
683
+ response.headers.set("Cache-Tag", config.tags.join(","));
684
+ }
685
+ return response;
686
+ }
687
+ function addPreloadLinks(response, resources) {
688
+ const links = resources.map((resource) => {
689
+ const attrs = [`<${resource.href}>`, `rel="preload"`, `as="${resource.as}"`];
690
+ if (resource.type) {
691
+ attrs.push(`type="${resource.type}"`);
692
+ }
693
+ if (resource.crossorigin) {
694
+ attrs.push("crossorigin");
695
+ }
696
+ return attrs.join("; ");
697
+ });
698
+ if (links.length > 0) {
699
+ response.headers.set("Link", links.join(", "));
700
+ }
701
+ return response;
702
+ }
703
+ async function warmISRCache(paths, baseURL = process.env.NEXT_PUBLIC_URL || "http://localhost:3000") {
704
+ const results = await Promise.allSettled(
705
+ paths.map(async (path) => {
706
+ const url = new URL(path, baseURL);
707
+ const response = await fetch(url.toString());
708
+ if (!response.ok) {
709
+ throw new Error(`${response.status} ${response.statusText}`);
710
+ }
711
+ return true;
712
+ })
713
+ );
714
+ let warmed = 0;
715
+ let failed = 0;
716
+ const errors = [];
717
+ for (let i = 0; i < results.length; i++) {
718
+ const result = results[i];
719
+ const path = paths[i];
720
+ if (!(result && path)) {
721
+ continue;
722
+ }
723
+ if (result.status === "fulfilled") {
724
+ warmed++;
725
+ } else {
726
+ failed++;
727
+ errors.push({
728
+ path,
729
+ error: result.reason instanceof Error ? result.reason.message : String(result.reason) || "Unknown error"
730
+ });
731
+ }
732
+ }
733
+ return { warmed, failed, errors };
734
+ }
735
+
736
+ // src/invalidation-channel.ts
737
+ var CREATE_EVENTS_TABLE_SQL = `
738
+ CREATE TABLE IF NOT EXISTS _cache_invalidation_events (
739
+ id TEXT PRIMARY KEY,
740
+ type TEXT NOT NULL,
741
+ keys TEXT[],
742
+ prefix TEXT,
743
+ tags TEXT[],
744
+ source_instance TEXT NOT NULL,
745
+ created_at BIGINT NOT NULL
746
+ );
747
+ CREATE INDEX IF NOT EXISTS _cache_inv_created_idx ON _cache_invalidation_events (created_at);
748
+ `;
749
+ var CacheInvalidationChannel = class {
750
+ db;
751
+ store;
752
+ instanceId;
753
+ pollIntervalMs;
754
+ eventTtlSeconds;
755
+ lastSeenTimestamp;
756
+ /** IDs processed at exactly lastSeenTimestamp (prevents re-processing on >= query). */
757
+ processedAtBoundary = /* @__PURE__ */ new Set();
758
+ pollTimer = null;
759
+ ready;
760
+ constructor(db, store, options) {
761
+ this.db = db;
762
+ this.store = store;
763
+ this.instanceId = options.instanceId;
764
+ this.pollIntervalMs = options.pollIntervalMs ?? 5e3;
765
+ this.eventTtlSeconds = options.eventTtlSeconds ?? 60;
766
+ this.lastSeenTimestamp = Date.now() - 1;
767
+ this.ready = this.init();
768
+ }
769
+ async init() {
770
+ await this.db.exec(CREATE_EVENTS_TABLE_SQL);
771
+ }
772
+ /** Start polling for invalidation events. */
773
+ async start() {
774
+ await this.ready;
775
+ if (this.pollTimer) return;
776
+ this.pollTimer = setInterval(() => {
777
+ void this.poll();
778
+ }, this.pollIntervalMs);
779
+ if (this.pollTimer.unref) this.pollTimer.unref();
780
+ }
781
+ /** Stop polling. */
782
+ stop() {
783
+ if (this.pollTimer) {
784
+ clearInterval(this.pollTimer);
785
+ this.pollTimer = null;
786
+ }
787
+ }
788
+ // ─── Publishing ─────────────────────────────────────────────────────
789
+ /** Publish a key deletion event. */
790
+ async publishDelete(...keys) {
791
+ await this.publish({ type: "delete", keys });
792
+ }
793
+ /** Publish a prefix deletion event. */
794
+ async publishDeletePrefix(prefix) {
795
+ await this.publish({ type: "delete-prefix", prefix });
796
+ }
797
+ /** Publish a tag-based deletion event. */
798
+ async publishDeleteTags(tags) {
799
+ await this.publish({ type: "delete-tags", tags });
800
+ }
801
+ /** Publish a clear-all event. */
802
+ async publishClear() {
803
+ await this.publish({ type: "clear" });
804
+ }
805
+ async publish(event) {
806
+ await this.ready;
807
+ const id = crypto.randomUUID();
808
+ const now = Date.now();
809
+ await this.db.query(
810
+ `INSERT INTO _cache_invalidation_events (id, type, keys, prefix, tags, source_instance, created_at)
811
+ VALUES ($1, $2, $3, $4, $5, $6, $7)`,
812
+ [
813
+ id,
814
+ event.type,
815
+ event.keys ?? null,
816
+ event.prefix ?? null,
817
+ event.tags ?? null,
818
+ this.instanceId,
819
+ now
820
+ ]
821
+ );
822
+ }
823
+ // ─── Polling ────────────────────────────────────────────────────────
824
+ /** Poll for new events and apply them to the local cache store. */
825
+ async poll() {
826
+ await this.ready;
827
+ const logger = getCacheLogger();
828
+ const result = await this.db.query(
829
+ `SELECT id, type, keys, prefix, tags, source_instance, created_at
830
+ FROM _cache_invalidation_events
831
+ WHERE created_at >= $1 AND source_instance != $2
832
+ ORDER BY created_at ASC`,
833
+ [this.lastSeenTimestamp, this.instanceId]
834
+ );
835
+ let applied = 0;
836
+ for (const row of result.rows) {
837
+ if (this.processedAtBoundary.has(row.id)) continue;
838
+ const createdAt = Number(row.created_at);
839
+ if (createdAt > this.lastSeenTimestamp) {
840
+ this.lastSeenTimestamp = createdAt;
841
+ this.processedAtBoundary.clear();
842
+ }
843
+ this.processedAtBoundary.add(row.id);
844
+ try {
845
+ await this.applyEvent(row.type, row);
846
+ applied++;
847
+ } catch (error) {
848
+ logger.error(
849
+ "Failed to apply invalidation event",
850
+ error instanceof Error ? error : new Error(String(error))
851
+ );
852
+ }
853
+ }
854
+ await this.prune();
855
+ return applied;
856
+ }
857
+ async applyEvent(type, row) {
858
+ switch (type) {
859
+ case "delete":
860
+ if (row.keys && row.keys.length > 0) {
861
+ await this.store.delete(...row.keys);
862
+ }
863
+ break;
864
+ case "delete-prefix":
865
+ if (row.prefix) {
866
+ await this.store.deleteByPrefix(row.prefix);
867
+ }
868
+ break;
869
+ case "delete-tags":
870
+ if (row.tags && row.tags.length > 0) {
871
+ await this.store.deleteByTags(row.tags);
872
+ }
873
+ break;
874
+ case "clear":
875
+ await this.store.clear();
876
+ break;
877
+ }
878
+ }
879
+ /** Remove events older than the TTL. */
880
+ async prune() {
881
+ const cutoff = Date.now() - this.eventTtlSeconds * 1e3;
882
+ const result = await this.db.query(
883
+ `WITH deleted AS (DELETE FROM _cache_invalidation_events WHERE created_at < $1 RETURNING 1)
884
+ SELECT count(*)::text AS count FROM deleted`,
885
+ [cutoff]
886
+ );
887
+ return Number.parseInt(result.rows[0]?.count ?? "0", 10);
888
+ }
889
+ /** Release resources. */
890
+ async close() {
891
+ this.stop();
892
+ }
893
+ };
894
+ export {
895
+ CDN_CACHE_PRESETS,
896
+ CacheInvalidationChannel,
897
+ DEFAULT_CDN_CONFIG,
898
+ EdgeRateLimiter,
899
+ ISR_PRESETS,
900
+ addPreloadLinks,
901
+ configureCacheLogger,
902
+ createCachedFunction,
903
+ createEdgeCachedFetch,
904
+ generateCacheControl,
905
+ generateCacheTags,
906
+ generateCloudflareConfig,
907
+ generateStaticParams,
908
+ generateVercelCacheConfig,
909
+ getABTestVariant,
910
+ getCacheLogger,
911
+ getCacheTTL,
912
+ getGeoLocation,
913
+ getPersonalizationConfig,
914
+ purgeAllCache,
915
+ purgeCDNCache,
916
+ purgeCacheByTag,
917
+ revalidatePath,
918
+ revalidatePaths,
919
+ revalidateTag,
920
+ revalidateTags,
921
+ setEdgeCacheHeaders,
922
+ shouldCacheResponse,
923
+ warmCDNCache,
924
+ warmISRCache
925
+ };
926
+ //# sourceMappingURL=index.js.map