@flexireact/core 2.2.0 → 2.4.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/README.md +203 -15
- package/core/edge/cache.ts +344 -0
- package/core/edge/fetch-polyfill.ts +247 -0
- package/core/edge/handler.ts +248 -0
- package/core/edge/index.ts +81 -0
- package/core/edge/ppr.ts +264 -0
- package/core/edge/runtime.ts +161 -0
- package/core/font/index.ts +306 -0
- package/core/image/index.ts +413 -0
- package/core/index.ts +92 -1
- package/core/metadata/index.ts +622 -0
- package/core/server/index.ts +12 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,8 +20,21 @@
|
|
|
20
20
|
<a href="#"><img src="https://img.shields.io/badge/Tailwind-v4-38B2AC.svg" alt="Tailwind CSS v4" /></a>
|
|
21
21
|
</p>
|
|
22
22
|
|
|
23
|
-
## 🆕 What's New in v2
|
|
23
|
+
## 🆕 What's New in v2.2
|
|
24
24
|
|
|
25
|
+
### v2.2.0 (Latest)
|
|
26
|
+
- **🌊 Streaming SSR** — Progressive HTML rendering with React 18 `renderToPipeableStream`
|
|
27
|
+
- **⚡ Server Actions** — Call server functions directly from client components
|
|
28
|
+
- **🔗 Link Prefetching** — Automatic prefetch on hover/viewport visibility
|
|
29
|
+
|
|
30
|
+
### v2.1.0
|
|
31
|
+
- **🛠️ Server Helpers** — `redirect()`, `notFound()`, `json()`, `cookies`, `headers`
|
|
32
|
+
- **🚧 Error/Loading Boundaries** — Per-segment `error.tsx` and `loading.tsx`
|
|
33
|
+
- **🔐 Route Middleware** — `_middleware.ts` for per-route logic
|
|
34
|
+
- **📊 Bundle Analyzer** — `flexi build --analyze`
|
|
35
|
+
- **🔄 CI/CD** — GitHub Actions workflow
|
|
36
|
+
|
|
37
|
+
### v2.0.0
|
|
25
38
|
- **TypeScript Native** — Core rewritten in TypeScript for better DX
|
|
26
39
|
- **Tailwind CSS v4** — New `@import "tailwindcss"` and `@theme` syntax
|
|
27
40
|
- **Routes Directory** — New `routes/` directory with route groups, dynamic segments
|
|
@@ -401,21 +414,117 @@ export function post(req, res) {
|
|
|
401
414
|
}
|
|
402
415
|
```
|
|
403
416
|
|
|
417
|
+
## ⚡ Server Actions (v2.2+)
|
|
418
|
+
|
|
419
|
+
Call server functions directly from client components:
|
|
420
|
+
|
|
421
|
+
```tsx
|
|
422
|
+
// actions.ts
|
|
423
|
+
'use server';
|
|
424
|
+
import { serverAction, redirect, cookies } from '@flexireact/core';
|
|
425
|
+
|
|
426
|
+
export const createUser = serverAction(async (formData: FormData) => {
|
|
427
|
+
const name = formData.get('name') as string;
|
|
428
|
+
const user = await db.users.create({ name });
|
|
429
|
+
|
|
430
|
+
// Set a cookie
|
|
431
|
+
cookies.set('userId', user.id);
|
|
432
|
+
|
|
433
|
+
// Redirect after action
|
|
434
|
+
redirect('/users');
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
// Form.tsx
|
|
438
|
+
'use client';
|
|
439
|
+
import { createUser } from './actions';
|
|
440
|
+
|
|
441
|
+
export function CreateUserForm() {
|
|
442
|
+
return (
|
|
443
|
+
<form action={createUser}>
|
|
444
|
+
<input name="name" placeholder="Name" required />
|
|
445
|
+
<button type="submit">Create User</button>
|
|
446
|
+
</form>
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
## 🔗 Link with Prefetching (v2.1+)
|
|
452
|
+
|
|
453
|
+
Enhanced Link component with automatic prefetching:
|
|
454
|
+
|
|
455
|
+
```tsx
|
|
456
|
+
import { Link } from '@flexireact/core/client';
|
|
457
|
+
|
|
458
|
+
// Prefetch on hover (default)
|
|
459
|
+
<Link href="/about">About</Link>
|
|
460
|
+
|
|
461
|
+
// Prefetch when visible in viewport
|
|
462
|
+
<Link href="/products" prefetch="viewport">Products</Link>
|
|
463
|
+
|
|
464
|
+
// Replace history instead of push
|
|
465
|
+
<Link href="/login" replace>Login</Link>
|
|
466
|
+
|
|
467
|
+
// Programmatic navigation
|
|
468
|
+
import { useRouter } from '@flexireact/core/client';
|
|
469
|
+
|
|
470
|
+
function MyComponent() {
|
|
471
|
+
const router = useRouter();
|
|
472
|
+
|
|
473
|
+
return (
|
|
474
|
+
<button onClick={() => router.push('/dashboard')}>
|
|
475
|
+
Go to Dashboard
|
|
476
|
+
</button>
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
## 🛠️ Server Helpers (v2.1+)
|
|
482
|
+
|
|
483
|
+
Utility functions for server-side operations:
|
|
484
|
+
|
|
485
|
+
```tsx
|
|
486
|
+
import { redirect, notFound, json, cookies, headers } from '@flexireact/core';
|
|
487
|
+
|
|
488
|
+
// Redirect
|
|
489
|
+
redirect('/dashboard');
|
|
490
|
+
redirect('/login', 'permanent'); // 308 redirect
|
|
491
|
+
|
|
492
|
+
// Not Found
|
|
493
|
+
notFound(); // Throws 404
|
|
494
|
+
|
|
495
|
+
// JSON Response (in API routes)
|
|
496
|
+
return json({ data: 'hello' }, { status: 200 });
|
|
497
|
+
|
|
498
|
+
// Cookies
|
|
499
|
+
const token = cookies.get(request, 'token');
|
|
500
|
+
const setCookie = cookies.set('session', 'abc123', {
|
|
501
|
+
httpOnly: true,
|
|
502
|
+
maxAge: 86400
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
// Headers
|
|
506
|
+
const auth = headers.bearerToken(request);
|
|
507
|
+
const corsHeaders = headers.cors({ origin: '*' });
|
|
508
|
+
const securityHeaders = headers.security();
|
|
509
|
+
```
|
|
510
|
+
|
|
404
511
|
## 🛡️ Middleware
|
|
405
512
|
|
|
406
|
-
|
|
513
|
+
### Global Middleware
|
|
514
|
+
|
|
515
|
+
Create `middleware.ts` in your project root:
|
|
516
|
+
|
|
517
|
+
```ts
|
|
518
|
+
import { redirect, cookies } from '@flexireact/core';
|
|
407
519
|
|
|
408
|
-
```js
|
|
409
520
|
export default function middleware(request) {
|
|
410
521
|
// Protect routes
|
|
411
522
|
if (request.pathname.startsWith('/admin')) {
|
|
412
|
-
|
|
413
|
-
|
|
523
|
+
const token = cookies.get(request, 'token');
|
|
524
|
+
if (!token) {
|
|
525
|
+
redirect('/login');
|
|
414
526
|
}
|
|
415
527
|
}
|
|
416
|
-
|
|
417
|
-
// Continue
|
|
418
|
-
return MiddlewareResponse.next();
|
|
419
528
|
}
|
|
420
529
|
|
|
421
530
|
export const config = {
|
|
@@ -423,6 +532,32 @@ export const config = {
|
|
|
423
532
|
};
|
|
424
533
|
```
|
|
425
534
|
|
|
535
|
+
### Route Middleware (v2.1+)
|
|
536
|
+
|
|
537
|
+
Create `_middleware.ts` in any route directory:
|
|
538
|
+
|
|
539
|
+
```
|
|
540
|
+
routes/
|
|
541
|
+
admin/
|
|
542
|
+
_middleware.ts # Runs for all /admin/* routes
|
|
543
|
+
dashboard.tsx
|
|
544
|
+
settings.tsx
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
```ts
|
|
548
|
+
// routes/admin/_middleware.ts
|
|
549
|
+
export default async function middleware(req, res, { route, params }) {
|
|
550
|
+
const user = await getUser(req);
|
|
551
|
+
|
|
552
|
+
if (!user?.isAdmin) {
|
|
553
|
+
return { redirect: '/login' };
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Continue to route
|
|
557
|
+
return { user };
|
|
558
|
+
}
|
|
559
|
+
```
|
|
560
|
+
|
|
426
561
|
## 🔧 Configuration
|
|
427
562
|
|
|
428
563
|
Create `flexireact.config.js`:
|
|
@@ -496,15 +631,68 @@ export default {
|
|
|
496
631
|
## 🖥️ CLI Commands
|
|
497
632
|
|
|
498
633
|
```bash
|
|
499
|
-
flexi create <name>
|
|
500
|
-
flexi dev
|
|
501
|
-
flexi build
|
|
502
|
-
flexi
|
|
503
|
-
flexi
|
|
504
|
-
flexi
|
|
505
|
-
flexi
|
|
634
|
+
flexi create <name> # Create new project
|
|
635
|
+
flexi dev # Start dev server
|
|
636
|
+
flexi build # Build for production
|
|
637
|
+
flexi build --analyze # Build with bundle analysis
|
|
638
|
+
flexi start # Start production server
|
|
639
|
+
flexi doctor # Check project health
|
|
640
|
+
flexi --version # Show version
|
|
641
|
+
flexi help # Show help
|
|
506
642
|
```
|
|
507
643
|
|
|
644
|
+
### Bundle Analysis (v2.1+)
|
|
645
|
+
|
|
646
|
+
```bash
|
|
647
|
+
flexi build --analyze
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
Output:
|
|
651
|
+
```
|
|
652
|
+
📊 Bundle Analysis:
|
|
653
|
+
|
|
654
|
+
─────────────────────────────────────────────────
|
|
655
|
+
File Size
|
|
656
|
+
─────────────────────────────────────────────────
|
|
657
|
+
client/main.js 45.2 KB (13.56 KB gzip)
|
|
658
|
+
client/vendor.js 120.5 KB (38.2 KB gzip)
|
|
659
|
+
server/pages.js 12.3 KB
|
|
660
|
+
─────────────────────────────────────────────────
|
|
661
|
+
Total: 178 KB
|
|
662
|
+
Gzipped: 51.76 KB
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
## 🌊 Streaming SSR (v2.2+)
|
|
666
|
+
|
|
667
|
+
Progressive HTML rendering with React 18:
|
|
668
|
+
|
|
669
|
+
```tsx
|
|
670
|
+
import { renderPageStream, streamToResponse } from '@flexireact/core';
|
|
671
|
+
|
|
672
|
+
// In your server handler
|
|
673
|
+
const { stream, shellReady } = await renderPageStream({
|
|
674
|
+
Component: MyPage,
|
|
675
|
+
props: { data },
|
|
676
|
+
loading: LoadingSpinner,
|
|
677
|
+
error: ErrorBoundary,
|
|
678
|
+
title: 'My Page',
|
|
679
|
+
styles: ['/styles.css']
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
// Wait for shell (initial HTML) to be ready
|
|
683
|
+
await shellReady;
|
|
684
|
+
|
|
685
|
+
// Stream to response
|
|
686
|
+
res.setHeader('Content-Type', 'text/html');
|
|
687
|
+
streamToResponse(res, stream);
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
Benefits:
|
|
691
|
+
- **Faster Time to First Byte (TTFB)** — Send HTML as it's ready
|
|
692
|
+
- **Progressive Loading** — Users see content immediately
|
|
693
|
+
- **Suspense Support** — Loading states stream in as data resolves
|
|
694
|
+
- **Better UX** — No blank screen while waiting for data
|
|
695
|
+
|
|
508
696
|
## 📚 Concepts Explained
|
|
509
697
|
|
|
510
698
|
### React Server Components (RSC)
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FlexiReact Universal Cache System
|
|
3
|
+
*
|
|
4
|
+
* Smart caching that works on:
|
|
5
|
+
* - Cloudflare Workers (Cache API + KV)
|
|
6
|
+
* - Vercel Edge (Edge Config + KV)
|
|
7
|
+
* - Deno (Deno KV)
|
|
8
|
+
* - Node.js/Bun (In-memory + File cache)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { detectRuntime } from './runtime.js';
|
|
12
|
+
|
|
13
|
+
// Cache entry
|
|
14
|
+
export interface CacheEntry<T = any> {
|
|
15
|
+
value: T;
|
|
16
|
+
expires: number; // timestamp
|
|
17
|
+
stale?: number; // stale-while-revalidate timestamp
|
|
18
|
+
tags?: string[];
|
|
19
|
+
etag?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Cache options
|
|
23
|
+
export interface CacheOptions {
|
|
24
|
+
ttl?: number; // seconds
|
|
25
|
+
staleWhileRevalidate?: number; // seconds
|
|
26
|
+
tags?: string[];
|
|
27
|
+
key?: string;
|
|
28
|
+
revalidate?: number | false; // ISR-style revalidation
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Cache storage interface
|
|
32
|
+
export interface CacheStorage {
|
|
33
|
+
get<T>(key: string): Promise<CacheEntry<T> | null>;
|
|
34
|
+
set<T>(key: string, entry: CacheEntry<T>): Promise<void>;
|
|
35
|
+
delete(key: string): Promise<void>;
|
|
36
|
+
deleteByTag(tag: string): Promise<void>;
|
|
37
|
+
clear(): Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// In-memory cache (fallback)
|
|
41
|
+
class MemoryCache implements CacheStorage {
|
|
42
|
+
private store = new Map<string, CacheEntry>();
|
|
43
|
+
private tagIndex = new Map<string, Set<string>>();
|
|
44
|
+
|
|
45
|
+
async get<T>(key: string): Promise<CacheEntry<T> | null> {
|
|
46
|
+
const entry = this.store.get(key);
|
|
47
|
+
if (!entry) return null;
|
|
48
|
+
|
|
49
|
+
// Check expiration
|
|
50
|
+
if (entry.expires && entry.expires < Date.now()) {
|
|
51
|
+
// Check stale-while-revalidate
|
|
52
|
+
if (entry.stale && entry.stale > Date.now()) {
|
|
53
|
+
return { ...entry, value: entry.value as T };
|
|
54
|
+
}
|
|
55
|
+
this.store.delete(key);
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return { ...entry, value: entry.value as T };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async set<T>(key: string, entry: CacheEntry<T>): Promise<void> {
|
|
63
|
+
this.store.set(key, entry);
|
|
64
|
+
|
|
65
|
+
// Index by tags
|
|
66
|
+
if (entry.tags) {
|
|
67
|
+
entry.tags.forEach(tag => {
|
|
68
|
+
if (!this.tagIndex.has(tag)) {
|
|
69
|
+
this.tagIndex.set(tag, new Set());
|
|
70
|
+
}
|
|
71
|
+
this.tagIndex.get(tag)!.add(key);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async delete(key: string): Promise<void> {
|
|
77
|
+
this.store.delete(key);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async deleteByTag(tag: string): Promise<void> {
|
|
81
|
+
const keys = this.tagIndex.get(tag);
|
|
82
|
+
if (keys) {
|
|
83
|
+
keys.forEach(key => this.store.delete(key));
|
|
84
|
+
this.tagIndex.delete(tag);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async clear(): Promise<void> {
|
|
89
|
+
this.store.clear();
|
|
90
|
+
this.tagIndex.clear();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Cloudflare KV cache
|
|
95
|
+
class CloudflareCache implements CacheStorage {
|
|
96
|
+
private kv: any;
|
|
97
|
+
|
|
98
|
+
constructor(kv: any) {
|
|
99
|
+
this.kv = kv;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async get<T>(key: string): Promise<CacheEntry<T> | null> {
|
|
103
|
+
try {
|
|
104
|
+
const data = await this.kv.get(key, 'json');
|
|
105
|
+
if (!data) return null;
|
|
106
|
+
|
|
107
|
+
const entry = data as CacheEntry<T>;
|
|
108
|
+
if (entry.expires && entry.expires < Date.now()) {
|
|
109
|
+
if (entry.stale && entry.stale > Date.now()) {
|
|
110
|
+
return entry;
|
|
111
|
+
}
|
|
112
|
+
await this.kv.delete(key);
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return entry;
|
|
117
|
+
} catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async set<T>(key: string, entry: CacheEntry<T>): Promise<void> {
|
|
123
|
+
const ttl = entry.expires ? Math.ceil((entry.expires - Date.now()) / 1000) : undefined;
|
|
124
|
+
await this.kv.put(key, JSON.stringify(entry), { expirationTtl: ttl });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async delete(key: string): Promise<void> {
|
|
128
|
+
await this.kv.delete(key);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async deleteByTag(tag: string): Promise<void> {
|
|
132
|
+
// KV doesn't support tag-based deletion natively
|
|
133
|
+
// Would need to maintain a tag index
|
|
134
|
+
console.warn('Tag-based deletion not fully supported in Cloudflare KV');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async clear(): Promise<void> {
|
|
138
|
+
// KV doesn't support clear
|
|
139
|
+
console.warn('Clear not supported in Cloudflare KV');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Deno KV cache
|
|
144
|
+
class DenoKVCache implements CacheStorage {
|
|
145
|
+
private kv: any;
|
|
146
|
+
|
|
147
|
+
constructor() {
|
|
148
|
+
// @ts-ignore - Deno global
|
|
149
|
+
this.kv = Deno.openKv();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async get<T>(key: string): Promise<CacheEntry<T> | null> {
|
|
153
|
+
try {
|
|
154
|
+
const kv = await this.kv;
|
|
155
|
+
const result = await kv.get(['cache', key]);
|
|
156
|
+
if (!result.value) return null;
|
|
157
|
+
|
|
158
|
+
const entry = result.value as CacheEntry<T>;
|
|
159
|
+
if (entry.expires && entry.expires < Date.now()) {
|
|
160
|
+
if (entry.stale && entry.stale > Date.now()) {
|
|
161
|
+
return entry;
|
|
162
|
+
}
|
|
163
|
+
await kv.delete(['cache', key]);
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return entry;
|
|
168
|
+
} catch {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async set<T>(key: string, entry: CacheEntry<T>): Promise<void> {
|
|
174
|
+
const kv = await this.kv;
|
|
175
|
+
await kv.set(['cache', key], entry);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async delete(key: string): Promise<void> {
|
|
179
|
+
const kv = await this.kv;
|
|
180
|
+
await kv.delete(['cache', key]);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async deleteByTag(tag: string): Promise<void> {
|
|
184
|
+
// Would need tag index
|
|
185
|
+
console.warn('Tag-based deletion requires tag index in Deno KV');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async clear(): Promise<void> {
|
|
189
|
+
// Would need to iterate all keys
|
|
190
|
+
console.warn('Clear requires iteration in Deno KV');
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Create cache based on runtime
|
|
195
|
+
function createCacheStorage(options?: { kv?: any }): CacheStorage {
|
|
196
|
+
const runtime = detectRuntime();
|
|
197
|
+
|
|
198
|
+
switch (runtime) {
|
|
199
|
+
case 'cloudflare':
|
|
200
|
+
if (options?.kv) {
|
|
201
|
+
return new CloudflareCache(options.kv);
|
|
202
|
+
}
|
|
203
|
+
return new MemoryCache();
|
|
204
|
+
|
|
205
|
+
case 'deno':
|
|
206
|
+
return new DenoKVCache();
|
|
207
|
+
|
|
208
|
+
case 'node':
|
|
209
|
+
case 'bun':
|
|
210
|
+
default:
|
|
211
|
+
return new MemoryCache();
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Main cache instance
|
|
216
|
+
let cacheStorage: CacheStorage = new MemoryCache();
|
|
217
|
+
|
|
218
|
+
// Initialize cache with platform-specific storage
|
|
219
|
+
export function initCache(options?: { kv?: any }): void {
|
|
220
|
+
cacheStorage = createCacheStorage(options);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Cache function wrapper (like React cache)
|
|
224
|
+
export function cacheFunction<T extends (...args: any[]) => Promise<any>>(
|
|
225
|
+
fn: T,
|
|
226
|
+
options: CacheOptions = {}
|
|
227
|
+
): T {
|
|
228
|
+
const { ttl = 60, staleWhileRevalidate = 0, tags = [] } = options;
|
|
229
|
+
|
|
230
|
+
return (async (...args: any[]) => {
|
|
231
|
+
const key = options.key || `fn:${fn.name}:${JSON.stringify(args)}`;
|
|
232
|
+
|
|
233
|
+
// Try cache first
|
|
234
|
+
const cached = await cacheStorage.get(key);
|
|
235
|
+
if (cached) {
|
|
236
|
+
// Check if stale and needs revalidation
|
|
237
|
+
if (cached.expires < Date.now() && cached.stale && cached.stale > Date.now()) {
|
|
238
|
+
// Return stale data, revalidate in background
|
|
239
|
+
queueMicrotask(async () => {
|
|
240
|
+
try {
|
|
241
|
+
const fresh = await fn(...args);
|
|
242
|
+
await cacheStorage.set(key, {
|
|
243
|
+
value: fresh,
|
|
244
|
+
expires: Date.now() + ttl * 1000,
|
|
245
|
+
stale: Date.now() + (ttl + staleWhileRevalidate) * 1000,
|
|
246
|
+
tags
|
|
247
|
+
});
|
|
248
|
+
} catch (e) {
|
|
249
|
+
console.error('Background revalidation failed:', e);
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
return cached.value;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Execute function
|
|
257
|
+
const result = await fn(...args);
|
|
258
|
+
|
|
259
|
+
// Cache result
|
|
260
|
+
await cacheStorage.set(key, {
|
|
261
|
+
value: result,
|
|
262
|
+
expires: Date.now() + ttl * 1000,
|
|
263
|
+
stale: staleWhileRevalidate ? Date.now() + (ttl + staleWhileRevalidate) * 1000 : undefined,
|
|
264
|
+
tags
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
return result;
|
|
268
|
+
}) as T;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Unstable cache (Next.js compatible API)
|
|
272
|
+
export function unstable_cache<T extends (...args: any[]) => Promise<any>>(
|
|
273
|
+
fn: T,
|
|
274
|
+
keyParts?: string[],
|
|
275
|
+
options?: { revalidate?: number | false; tags?: string[] }
|
|
276
|
+
): T {
|
|
277
|
+
return cacheFunction(fn, {
|
|
278
|
+
key: keyParts?.join(':'),
|
|
279
|
+
ttl: typeof options?.revalidate === 'number' ? options.revalidate : 3600,
|
|
280
|
+
tags: options?.tags
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Revalidate by tag
|
|
285
|
+
export async function revalidateTag(tag: string): Promise<void> {
|
|
286
|
+
await cacheStorage.deleteByTag(tag);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Revalidate by path
|
|
290
|
+
export async function revalidatePath(path: string): Promise<void> {
|
|
291
|
+
await cacheStorage.delete(`page:${path}`);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Cache object for direct access
|
|
295
|
+
export const cache = {
|
|
296
|
+
get: <T>(key: string) => cacheStorage.get<T>(key),
|
|
297
|
+
set: <T>(key: string, value: T, options: CacheOptions = {}) => {
|
|
298
|
+
const { ttl = 60, staleWhileRevalidate = 0, tags = [] } = options;
|
|
299
|
+
return cacheStorage.set(key, {
|
|
300
|
+
value,
|
|
301
|
+
expires: Date.now() + ttl * 1000,
|
|
302
|
+
stale: staleWhileRevalidate ? Date.now() + (ttl + staleWhileRevalidate) * 1000 : undefined,
|
|
303
|
+
tags
|
|
304
|
+
});
|
|
305
|
+
},
|
|
306
|
+
delete: (key: string) => cacheStorage.delete(key),
|
|
307
|
+
deleteByTag: (tag: string) => cacheStorage.deleteByTag(tag),
|
|
308
|
+
clear: () => cacheStorage.clear(),
|
|
309
|
+
|
|
310
|
+
// Wrap function with caching
|
|
311
|
+
wrap: cacheFunction,
|
|
312
|
+
|
|
313
|
+
// Next.js compatible
|
|
314
|
+
unstable_cache,
|
|
315
|
+
revalidateTag,
|
|
316
|
+
revalidatePath
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
// Request-level cache (per-request deduplication)
|
|
320
|
+
const requestCache = new WeakMap<Request, Map<string, any>>();
|
|
321
|
+
|
|
322
|
+
export function getRequestCache(request: Request): Map<string, any> {
|
|
323
|
+
if (!requestCache.has(request)) {
|
|
324
|
+
requestCache.set(request, new Map());
|
|
325
|
+
}
|
|
326
|
+
return requestCache.get(request)!;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// React-style cache for request deduplication
|
|
330
|
+
export function reactCache<T extends (...args: any[]) => any>(fn: T): T {
|
|
331
|
+
const cache = new Map<string, any>();
|
|
332
|
+
|
|
333
|
+
return ((...args: any[]) => {
|
|
334
|
+
const key = JSON.stringify(args);
|
|
335
|
+
if (cache.has(key)) {
|
|
336
|
+
return cache.get(key);
|
|
337
|
+
}
|
|
338
|
+
const result = fn(...args);
|
|
339
|
+
cache.set(key, result);
|
|
340
|
+
return result;
|
|
341
|
+
}) as T;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export default cache;
|