@buenojs/bueno 0.8.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.
Files changed (120) hide show
  1. package/.env.example +109 -0
  2. package/.github/workflows/ci.yml +31 -0
  3. package/LICENSE +21 -0
  4. package/README.md +892 -0
  5. package/architecture.md +652 -0
  6. package/bun.lock +70 -0
  7. package/dist/cli/index.js +3233 -0
  8. package/dist/index.js +9014 -0
  9. package/package.json +77 -0
  10. package/src/cache/index.ts +795 -0
  11. package/src/cli/ARCHITECTURE.md +837 -0
  12. package/src/cli/bin.ts +10 -0
  13. package/src/cli/commands/build.ts +425 -0
  14. package/src/cli/commands/dev.ts +248 -0
  15. package/src/cli/commands/generate.ts +541 -0
  16. package/src/cli/commands/help.ts +55 -0
  17. package/src/cli/commands/index.ts +112 -0
  18. package/src/cli/commands/migration.ts +355 -0
  19. package/src/cli/commands/new.ts +804 -0
  20. package/src/cli/commands/start.ts +208 -0
  21. package/src/cli/core/args.ts +283 -0
  22. package/src/cli/core/console.ts +349 -0
  23. package/src/cli/core/index.ts +60 -0
  24. package/src/cli/core/prompt.ts +424 -0
  25. package/src/cli/core/spinner.ts +265 -0
  26. package/src/cli/index.ts +135 -0
  27. package/src/cli/templates/deploy.ts +295 -0
  28. package/src/cli/templates/docker.ts +307 -0
  29. package/src/cli/templates/index.ts +24 -0
  30. package/src/cli/utils/fs.ts +428 -0
  31. package/src/cli/utils/index.ts +8 -0
  32. package/src/cli/utils/strings.ts +197 -0
  33. package/src/config/env.ts +408 -0
  34. package/src/config/index.ts +506 -0
  35. package/src/config/loader.ts +329 -0
  36. package/src/config/merge.ts +285 -0
  37. package/src/config/types.ts +320 -0
  38. package/src/config/validation.ts +441 -0
  39. package/src/container/forward-ref.ts +143 -0
  40. package/src/container/index.ts +386 -0
  41. package/src/context/index.ts +360 -0
  42. package/src/database/index.ts +1142 -0
  43. package/src/database/migrations/index.ts +371 -0
  44. package/src/database/schema/index.ts +619 -0
  45. package/src/frontend/api-routes.ts +640 -0
  46. package/src/frontend/bundler.ts +643 -0
  47. package/src/frontend/console-client.ts +419 -0
  48. package/src/frontend/console-stream.ts +587 -0
  49. package/src/frontend/dev-server.ts +846 -0
  50. package/src/frontend/file-router.ts +611 -0
  51. package/src/frontend/frameworks/index.ts +106 -0
  52. package/src/frontend/frameworks/react.ts +85 -0
  53. package/src/frontend/frameworks/solid.ts +104 -0
  54. package/src/frontend/frameworks/svelte.ts +110 -0
  55. package/src/frontend/frameworks/vue.ts +92 -0
  56. package/src/frontend/hmr-client.ts +663 -0
  57. package/src/frontend/hmr.ts +728 -0
  58. package/src/frontend/index.ts +342 -0
  59. package/src/frontend/islands.ts +552 -0
  60. package/src/frontend/isr.ts +555 -0
  61. package/src/frontend/layout.ts +475 -0
  62. package/src/frontend/ssr/react.ts +446 -0
  63. package/src/frontend/ssr/solid.ts +523 -0
  64. package/src/frontend/ssr/svelte.ts +546 -0
  65. package/src/frontend/ssr/vue.ts +504 -0
  66. package/src/frontend/ssr.ts +699 -0
  67. package/src/frontend/types.ts +2274 -0
  68. package/src/health/index.ts +604 -0
  69. package/src/index.ts +410 -0
  70. package/src/lock/index.ts +587 -0
  71. package/src/logger/index.ts +444 -0
  72. package/src/logger/transports/index.ts +969 -0
  73. package/src/metrics/index.ts +494 -0
  74. package/src/middleware/built-in.ts +360 -0
  75. package/src/middleware/index.ts +94 -0
  76. package/src/modules/filters.ts +458 -0
  77. package/src/modules/guards.ts +405 -0
  78. package/src/modules/index.ts +1256 -0
  79. package/src/modules/interceptors.ts +574 -0
  80. package/src/modules/lazy.ts +418 -0
  81. package/src/modules/lifecycle.ts +478 -0
  82. package/src/modules/metadata.ts +90 -0
  83. package/src/modules/pipes.ts +626 -0
  84. package/src/router/index.ts +339 -0
  85. package/src/router/linear.ts +371 -0
  86. package/src/router/regex.ts +292 -0
  87. package/src/router/tree.ts +562 -0
  88. package/src/rpc/index.ts +1263 -0
  89. package/src/security/index.ts +436 -0
  90. package/src/ssg/index.ts +631 -0
  91. package/src/storage/index.ts +456 -0
  92. package/src/telemetry/index.ts +1097 -0
  93. package/src/testing/index.ts +1586 -0
  94. package/src/types/index.ts +236 -0
  95. package/src/types/optional-deps.d.ts +219 -0
  96. package/src/validation/index.ts +276 -0
  97. package/src/websocket/index.ts +1004 -0
  98. package/tests/integration/cli.test.ts +1016 -0
  99. package/tests/integration/fullstack.test.ts +234 -0
  100. package/tests/unit/cache.test.ts +174 -0
  101. package/tests/unit/cli-commands.test.ts +892 -0
  102. package/tests/unit/cli.test.ts +1258 -0
  103. package/tests/unit/container.test.ts +279 -0
  104. package/tests/unit/context.test.ts +221 -0
  105. package/tests/unit/database.test.ts +183 -0
  106. package/tests/unit/linear-router.test.ts +280 -0
  107. package/tests/unit/lock.test.ts +336 -0
  108. package/tests/unit/middleware.test.ts +184 -0
  109. package/tests/unit/modules.test.ts +142 -0
  110. package/tests/unit/pubsub.test.ts +257 -0
  111. package/tests/unit/regex-router.test.ts +265 -0
  112. package/tests/unit/router.test.ts +373 -0
  113. package/tests/unit/rpc.test.ts +1248 -0
  114. package/tests/unit/security.test.ts +174 -0
  115. package/tests/unit/telemetry.test.ts +371 -0
  116. package/tests/unit/test-cache.test.ts +110 -0
  117. package/tests/unit/test-database.test.ts +282 -0
  118. package/tests/unit/tree-router.test.ts +325 -0
  119. package/tests/unit/validation.test.ts +794 -0
  120. package/tsconfig.json +27 -0
@@ -0,0 +1,555 @@
1
+ /**
2
+ * Incremental Static Regeneration (ISR) Implementation
3
+ *
4
+ * Provides ISR capabilities that extend SSG with:
5
+ * - Time-based revalidation
6
+ * - On-demand revalidation
7
+ * - Stale-while-revalidate strategy
8
+ * - Distributed cache support via Bun.redis
9
+ */
10
+
11
+ import { createLogger, type Logger } from "../logger/index.js";
12
+ import type {
13
+ ISRConfig,
14
+ PartialISRConfig,
15
+ ISRCacheEntry,
16
+ ISRPageConfig,
17
+ ISRRevalidationResult,
18
+ ISRStats,
19
+ SSRRenderOptions,
20
+ } from "./types.js";
21
+ import { SSRRenderer, createSSRContext } from "./ssr.js";
22
+ import type { SSRContext, RenderResult } from "./types.js";
23
+
24
+ // ============= Constants =============
25
+
26
+ const DEFAULT_CACHE_DIR = ".isr-cache";
27
+ const DEFAULT_REVALIDATE = 3600; // 1 hour
28
+ const DEFAULT_STALE_WHILE_REVALIDATE = 60; // 1 minute
29
+
30
+ // ============= ISR Manager Class =============
31
+
32
+ /**
33
+ * ISR Manager handles incremental static regeneration
34
+ *
35
+ * Features:
36
+ * - Time-based revalidation with configurable TTL
37
+ * - Stale-while-revalidate for instant responses
38
+ * - On-demand revalidation via API or webhook
39
+ * - Distributed cache support via Redis
40
+ */
41
+ export class ISRManager {
42
+ private config: ISRConfig;
43
+ private logger: Logger;
44
+ private cache: Map<string, ISRCacheEntry> = new Map();
45
+ private pendingRegenerations: Map<string, Promise<ISRRevalidationResult>> = new Map();
46
+ private ssrRenderer: SSRRenderer | null = null;
47
+ private stats = {
48
+ hits: 0,
49
+ misses: 0,
50
+ revalidations: 0,
51
+ staleHits: 0,
52
+ };
53
+
54
+ constructor(config: PartialISRConfig) {
55
+ this.config = this.normalizeConfig(config);
56
+ this.logger = createLogger({
57
+ level: "debug",
58
+ pretty: true,
59
+ context: { component: "ISRManager" },
60
+ });
61
+ }
62
+
63
+ /**
64
+ * Normalize partial config to full config with defaults
65
+ */
66
+ private normalizeConfig(config: PartialISRConfig): ISRConfig {
67
+ return {
68
+ cacheDir: config.cacheDir ?? DEFAULT_CACHE_DIR,
69
+ defaultRevalidate: config.defaultRevalidate ?? DEFAULT_REVALIDATE,
70
+ staleWhileRevalidate: config.staleWhileRevalidate ?? DEFAULT_STALE_WHILE_REVALIDATE,
71
+ maxCacheSize: config.maxCacheSize ?? 1000,
72
+ redis: config.redis,
73
+ redisKeyPrefix: config.redisKeyPrefix ?? "bueno:isr:",
74
+ enabled: config.enabled ?? true,
75
+ };
76
+ }
77
+
78
+ /**
79
+ * Set the SSR renderer
80
+ */
81
+ setSSRRenderer(renderer: SSRRenderer): void {
82
+ this.ssrRenderer = renderer;
83
+ }
84
+
85
+ /**
86
+ * Get a page from cache or render it
87
+ */
88
+ async getPage(
89
+ url: string,
90
+ request: Request,
91
+ pageConfig?: ISRPageConfig
92
+ ): Promise<RenderResult> {
93
+ if (!this.config.enabled) {
94
+ return this.renderPage(url, request);
95
+ }
96
+
97
+ const cacheKey = this.getCacheKey(url);
98
+ const entry = await this.getCacheEntry(cacheKey);
99
+
100
+ if (entry) {
101
+ const now = Date.now();
102
+ const age = (now - entry.timestamp) / 1000;
103
+ const revalidate = pageConfig?.revalidate ?? this.config.defaultRevalidate;
104
+ const staleWhileRevalidate = pageConfig?.staleWhileRevalidate ?? this.config.staleWhileRevalidate;
105
+
106
+ // Cache hit - check if stale
107
+ if (age < revalidate) {
108
+ // Fresh cache hit
109
+ this.stats.hits++;
110
+ this.logger.debug(`Cache hit (fresh): ${url}`);
111
+ return entry.result;
112
+ }
113
+
114
+ // Stale but within stale-while-revalidate window
115
+ if (age < revalidate + staleWhileRevalidate) {
116
+ this.stats.staleHits++;
117
+ this.logger.debug(`Cache hit (stale): ${url}, revalidating in background`);
118
+
119
+ // Trigger background revalidation
120
+ this.triggerBackgroundRevalidation(url, request, pageConfig);
121
+
122
+ // Return stale content
123
+ return entry.result;
124
+ }
125
+ }
126
+
127
+ // Cache miss or expired - render and cache
128
+ this.stats.misses++;
129
+ this.logger.debug(`Cache miss: ${url}`);
130
+
131
+ const result = await this.renderPage(url, request);
132
+ await this.setCacheEntry(cacheKey, result, pageConfig);
133
+
134
+ return result;
135
+ }
136
+
137
+ /**
138
+ * Render a page using SSR
139
+ */
140
+ private async renderPage(url: string, request: Request): Promise<RenderResult> {
141
+ if (!this.ssrRenderer) {
142
+ throw new Error("SSR renderer not configured");
143
+ }
144
+
145
+ const options: SSRRenderOptions = {
146
+ url,
147
+ request,
148
+ params: {},
149
+ props: {},
150
+ skipStreaming: true,
151
+ };
152
+
153
+ return this.ssrRenderer.renderWithOptions(options);
154
+ }
155
+
156
+ /**
157
+ * Trigger background revalidation
158
+ */
159
+ private triggerBackgroundRevalidation(
160
+ url: string,
161
+ request: Request,
162
+ pageConfig?: ISRPageConfig
163
+ ): void {
164
+ const cacheKey = this.getCacheKey(url);
165
+
166
+ // Don't start if already pending
167
+ if (this.pendingRegenerations.has(cacheKey)) {
168
+ return;
169
+ }
170
+
171
+ const promise = this.revalidatePage(url, request, pageConfig)
172
+ .finally(() => {
173
+ this.pendingRegenerations.delete(cacheKey);
174
+ });
175
+
176
+ this.pendingRegenerations.set(cacheKey, promise);
177
+ }
178
+
179
+ /**
180
+ * Revalidate a page
181
+ */
182
+ async revalidatePage(
183
+ url: string,
184
+ request: Request,
185
+ pageConfig?: ISRPageConfig
186
+ ): Promise<ISRRevalidationResult> {
187
+ const cacheKey = this.getCacheKey(url);
188
+ const startTime = Date.now();
189
+
190
+ try {
191
+ this.logger.info(`Revalidating: ${url}`);
192
+ this.stats.revalidations++;
193
+
194
+ const result = await this.renderPage(url, request);
195
+ await this.setCacheEntry(cacheKey, result, pageConfig);
196
+
197
+ const duration = Date.now() - startTime;
198
+ this.logger.info(`Revalidated: ${url} in ${duration}ms`);
199
+
200
+ return {
201
+ success: true,
202
+ url,
203
+ duration,
204
+ timestamp: Date.now(),
205
+ };
206
+ } catch (error) {
207
+ const duration = Date.now() - startTime;
208
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
209
+ this.logger.error(`Revalidation failed: ${url}`, error);
210
+
211
+ return {
212
+ success: false,
213
+ url,
214
+ duration,
215
+ timestamp: Date.now(),
216
+ error: errorMessage,
217
+ };
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Invalidate a specific page
223
+ */
224
+ async invalidatePage(url: string): Promise<boolean> {
225
+ const cacheKey = this.getCacheKey(url);
226
+
227
+ if (this.config.redis) {
228
+ try {
229
+ await this.config.redis.del(`${this.config.redisKeyPrefix}${cacheKey}`);
230
+ } catch (error) {
231
+ this.logger.error(`Failed to invalidate Redis cache: ${url}`, error);
232
+ }
233
+ }
234
+
235
+ const deleted = this.cache.delete(cacheKey);
236
+ if (deleted) {
237
+ this.logger.info(`Invalidated: ${url}`);
238
+ }
239
+
240
+ return deleted;
241
+ }
242
+
243
+ /**
244
+ * Invalidate multiple pages by pattern
245
+ */
246
+ async invalidatePattern(pattern: string | RegExp): Promise<number> {
247
+ let count = 0;
248
+ const regex = typeof pattern === "string" ? new RegExp(pattern) : pattern;
249
+
250
+ // Invalidate local cache
251
+ for (const key of this.cache.keys()) {
252
+ if (regex.test(key)) {
253
+ this.cache.delete(key);
254
+ count++;
255
+ }
256
+ }
257
+
258
+ // Invalidate Redis cache if available
259
+ if (this.config.redis) {
260
+ try {
261
+ const keys = await this.config.redis.keys(`${this.config.redisKeyPrefix}*`);
262
+ for (const key of keys) {
263
+ const cacheKey = key.replace(this.config.redisKeyPrefix, "");
264
+ if (regex.test(cacheKey)) {
265
+ await this.config.redis.del(key);
266
+ count++;
267
+ }
268
+ }
269
+ } catch (error) {
270
+ this.logger.error("Failed to invalidate Redis cache by pattern", error);
271
+ }
272
+ }
273
+
274
+ this.logger.info(`Invalidated ${count} pages matching pattern: ${pattern}`);
275
+ return count;
276
+ }
277
+
278
+ /**
279
+ * Invalidate all pages
280
+ */
281
+ async invalidateAll(): Promise<void> {
282
+ this.cache.clear();
283
+ this.stats = { hits: 0, misses: 0, revalidations: 0, staleHits: 0 };
284
+
285
+ if (this.config.redis) {
286
+ try {
287
+ const keys = await this.config.redis.keys(`${this.config.redisKeyPrefix}*`);
288
+ if (keys.length > 0) {
289
+ await this.config.redis.del(...keys);
290
+ }
291
+ } catch (error) {
292
+ this.logger.error("Failed to clear Redis cache", error);
293
+ }
294
+ }
295
+
296
+ this.logger.info("All ISR cache invalidated");
297
+ }
298
+
299
+ /**
300
+ * Get cache entry
301
+ */
302
+ private async getCacheEntry(key: string): Promise<ISRCacheEntry | null> {
303
+ // Check local cache first
304
+ const localEntry = this.cache.get(key);
305
+ if (localEntry) {
306
+ return localEntry;
307
+ }
308
+
309
+ // Check Redis if available
310
+ if (this.config.redis) {
311
+ try {
312
+ const data = await this.config.redis.get(`${this.config.redisKeyPrefix}${key}`);
313
+ if (data) {
314
+ const entry = JSON.parse(data) as ISRCacheEntry;
315
+ // Cache locally for faster access
316
+ this.cache.set(key, entry);
317
+ return entry;
318
+ }
319
+ } catch (error) {
320
+ this.logger.error("Failed to get Redis cache entry", error);
321
+ }
322
+ }
323
+
324
+ return null;
325
+ }
326
+
327
+ /**
328
+ * Set cache entry
329
+ */
330
+ private async setCacheEntry(
331
+ key: string,
332
+ result: RenderResult,
333
+ pageConfig?: ISRPageConfig
334
+ ): Promise<void> {
335
+ const entry: ISRCacheEntry = {
336
+ result,
337
+ timestamp: Date.now(),
338
+ revalidate: pageConfig?.revalidate ?? this.config.defaultRevalidate,
339
+ tags: pageConfig?.tags ?? [],
340
+ };
341
+
342
+ // Set local cache
343
+ this.cache.set(key, entry);
344
+
345
+ // Enforce max cache size
346
+ if (this.cache.size > this.config.maxCacheSize) {
347
+ this.evictOldestEntries();
348
+ }
349
+
350
+ // Set Redis cache if available
351
+ if (this.config.redis) {
352
+ try {
353
+ const ttl = entry.revalidate + this.config.staleWhileRevalidate;
354
+ await this.config.redis.set(
355
+ `${this.config.redisKeyPrefix}${key}`,
356
+ JSON.stringify(entry),
357
+ { EX: ttl }
358
+ );
359
+ } catch (error) {
360
+ this.logger.error("Failed to set Redis cache entry", error);
361
+ }
362
+ }
363
+ }
364
+
365
+ /**
366
+ * Evict oldest entries when cache is full
367
+ */
368
+ private evictOldestEntries(): void {
369
+ const entries = Array.from(this.cache.entries())
370
+ .sort((a, b) => a[1].timestamp - b[1].timestamp);
371
+
372
+ const toEvict = entries.slice(0, Math.floor(this.config.maxCacheSize * 0.1));
373
+ for (const [key] of toEvict) {
374
+ this.cache.delete(key);
375
+ }
376
+
377
+ this.logger.debug(`Evicted ${toEvict.length} cache entries`);
378
+ }
379
+
380
+ /**
381
+ * Generate cache key from URL
382
+ */
383
+ private getCacheKey(url: string): string {
384
+ try {
385
+ const parsed = new URL(url, "http://localhost");
386
+ // Normalize URL for caching
387
+ return `${parsed.pathname}${parsed.search}`;
388
+ } catch {
389
+ return url;
390
+ }
391
+ }
392
+
393
+ /**
394
+ * Get ISR statistics
395
+ */
396
+ getStats(): ISRStats {
397
+ return {
398
+ ...this.stats,
399
+ cacheSize: this.cache.size,
400
+ pendingRevalidations: this.pendingRegenerations.size,
401
+ hitRate: this.stats.hits + this.stats.misses > 0
402
+ ? this.stats.hits / (this.stats.hits + this.stats.misses)
403
+ : 0,
404
+ };
405
+ }
406
+
407
+ /**
408
+ * Get all cached URLs
409
+ */
410
+ getCachedUrls(): string[] {
411
+ return Array.from(this.cache.keys());
412
+ }
413
+
414
+ /**
415
+ * Check if a page is cached
416
+ */
417
+ isCached(url: string): boolean {
418
+ return this.cache.has(this.getCacheKey(url));
419
+ }
420
+
421
+ /**
422
+ * Get cache entry info
423
+ */
424
+ async getCacheInfo(url: string): Promise<{
425
+ cached: boolean;
426
+ timestamp?: number;
427
+ age?: number;
428
+ revalidate?: number;
429
+ tags?: string[];
430
+ } | null> {
431
+ const entry = await this.getCacheEntry(this.getCacheKey(url));
432
+ if (!entry) {
433
+ return { cached: false };
434
+ }
435
+
436
+ return {
437
+ cached: true,
438
+ timestamp: entry.timestamp,
439
+ age: (Date.now() - entry.timestamp) / 1000,
440
+ revalidate: entry.revalidate,
441
+ tags: entry.tags,
442
+ };
443
+ }
444
+
445
+ /**
446
+ * Revalidate pages by tag
447
+ */
448
+ async revalidateTag(tag: string): Promise<number> {
449
+ let count = 0;
450
+
451
+ for (const [key, entry] of this.cache.entries()) {
452
+ if (entry.tags.includes(tag)) {
453
+ this.cache.delete(key);
454
+ count++;
455
+ }
456
+ }
457
+
458
+ this.logger.info(`Revalidated ${count} pages with tag: ${tag}`);
459
+ return count;
460
+ }
461
+
462
+ /**
463
+ * Prune expired entries
464
+ */
465
+ pruneExpired(): number {
466
+ const now = Date.now();
467
+ let count = 0;
468
+
469
+ for (const [key, entry] of this.cache.entries()) {
470
+ const age = (now - entry.timestamp) / 1000;
471
+ if (age > entry.revalidate + this.config.staleWhileRevalidate) {
472
+ this.cache.delete(key);
473
+ count++;
474
+ }
475
+ }
476
+
477
+ if (count > 0) {
478
+ this.logger.debug(`Pruned ${count} expired cache entries`);
479
+ }
480
+
481
+ return count;
482
+ }
483
+
484
+ /**
485
+ * Get configuration
486
+ */
487
+ getConfig(): ISRConfig {
488
+ return { ...this.config };
489
+ }
490
+
491
+ /**
492
+ * Check if ISR is enabled
493
+ */
494
+ isEnabled(): boolean {
495
+ return this.config.enabled;
496
+ }
497
+ }
498
+
499
+ // ============= Factory Function =============
500
+
501
+ /**
502
+ * Create an ISR manager
503
+ */
504
+ export function createISRManager(config: PartialISRConfig): ISRManager {
505
+ return new ISRManager(config);
506
+ }
507
+
508
+ // ============= Utility Functions =============
509
+
510
+ /**
511
+ * Parse revalidation header
512
+ * Supports formats like:
513
+ * - "60" (seconds)
514
+ * - "60, stale-while-revalidate=30"
515
+ */
516
+ export function parseRevalidateHeader(header: string): {
517
+ revalidate: number;
518
+ staleWhileRevalidate: number;
519
+ } {
520
+ const parts = header.split(",").map(p => p.trim());
521
+ let revalidate = DEFAULT_REVALIDATE;
522
+ let staleWhileRevalidate = DEFAULT_STALE_WHILE_REVALIDATE;
523
+
524
+ for (const part of parts) {
525
+ if (part.includes("stale-while-revalidate=")) {
526
+ staleWhileRevalidate = parseInt(part.split("=")[1], 10);
527
+ } else {
528
+ revalidate = parseInt(part, 10);
529
+ }
530
+ }
531
+
532
+ return { revalidate, staleWhileRevalidate };
533
+ }
534
+
535
+ /**
536
+ * Generate Cache-Control header for ISR
537
+ */
538
+ export function generateCacheControlHeader(
539
+ revalidate: number,
540
+ staleWhileRevalidate: number
541
+ ): string {
542
+ return `public, max-age=0, s-maxage=${revalidate}, stale-while-revalidate=${staleWhileRevalidate}`;
543
+ }
544
+
545
+ /**
546
+ * Check if a page should be regenerated
547
+ */
548
+ export function shouldRegenerate(
549
+ entry: ISRCacheEntry,
550
+ revalidate: number,
551
+ staleWhileRevalidate: number
552
+ ): boolean {
553
+ const age = (Date.now() - entry.timestamp) / 1000;
554
+ return age > revalidate && age <= revalidate + staleWhileRevalidate;
555
+ }