שמירה במטמון ואימות מחדש
הגדרות מקטע נתיב, unstable_cache, revalidateTag / revalidatePath ו-stale-while-revalidate.
שמירת תגובות של מטפלי נתיב ונתונים במטמון, ולאחר מכן אימות מחדש לפי תזמון או לפי דרישה. רשומות מיושנות מוגשות מיד בזמן שרענון יחיד פועל ברקע.
מטפלי נתיב
מטפלי נתיב מסוג GET הם דינמיים כברירת מחדל. כדי להפעיל שמירה במטמון, יש להשתמש בהגדרות מקטע הנתיב:
export const revalidate = 60;export async function GET() { const posts = await db.posts.latest(); return Response.json(posts);}| ייצוא | התנהגות |
|---|---|
| (ללא) | הפעלה בכל בקשה |
dynamic = "force-static" | שמירה במטמון עד לאימות מחדש |
revalidate = N | שמירה במטמון; התיישנות לאחר N שניות |
revalidate = 0 או dynamic = "force-dynamic" | הפעלה בכל בקשה |
ערכים אחרים של dynamic נכשלים עם TX031. זה כולל את "error" של Next.js, מכיוון ש-Text.js לא יכול לזהות מתי מטפל קורא את הבקשה.
מטפלים שנשמרים במטמון מקבלים רשומה אחת לכל שם נתיב, וה-request שלהם נראה כפי שהיה נראה בזמן הבנייה: ללא מחרוזת שאילתה, כותרות, קובצי cookie או גוף בקשה. יש לקרוא אותם במטפל דינמי במקום זאת. רק GET ו-HEAD נשמרים במטמון. תגובות עם קוד מצב 5xx, כותרת Set-Cookie או Cache-Control: private / no-store מוחזרות אך לעולם אינן נשמרות.
כותרות
תגובות שנשמרו במטמון כוללות:
| כותרת | ערך |
|---|---|
x-textjs-cache | MISS, HIT או STALE |
Cache-Control | s-maxage=N, stale-while-revalidate=<1 year − N>, או s-maxage=31536000 עבור force-static |
כאשר middleware מגדיר קובצי cookie, משכתב את הבקשה או דורס את כותרות הבקשה, הכותרת שנוצרת הופכת ל-private, no-cache, no-store, max-age=0, must-revalidate, כך שמטמונים משותפים לעולם אינם משתמשים שוב בהחלטה שהתקבלה עבור מבקר מסוים. כותרת Cache-Control שהמטפל הגדיר נשמרת.
ב-text dev, הגדרות המקטע חלות אך דבר אינו נשמר, ולכן כל בקשה מפעילה את המטפל.
מטמון נתונים
כדי לשמור פונקציה אסינכרונית כלשהי במטמון, יש להשתמש ב-unstable_cache מתוך @textjs/app/cache:
import { unstable_cache } from "@textjs/app/cache";export const getPosts = unstable_cache( async (author: string) => db.posts.byAuthor(author), ["posts"], { tags: ["posts"], revalidate: 300 },);המפתח מורכב מחלקי המפתח ומהארגומנטים, המקודדים לפי ערך (ארגומנטים מסוג undefined ו-null, Map, Set, Date ו-URL נשארים מובחנים). ללא חלקי מפתח, קוד המקור של הפונקציה והמקום שבו נוצרה מזהים אותה; יש להעביר חלקי מפתח בכל פעם שהפונקציה לוכדת ערכים מההיקף החיצוני, למשל כשהיא נוצרת לכל בקשה או בתוך לולאה. התוצאות עוברות סריאליזציה ל-JSON, כך שכל קריאה מחזירה עותק חדש (ואובייקטים מסוג Date חוזרים כמחרוזות); תוצאה שאי אפשר לבצע לה סריאליזציה גורמת לזריקת חריגה. cache(fn, { key, tags, revalidate }) מקבל את אותן ההגדרות כאובייקט אפשרויות.
בתוך מטפל נתיב שנשמר במטמון, רשומות הנתונים מעבירות לנתיב את התגיות שלהן ואת ערך ה-revalidate הקצר ביותר שלהן: אימות מחדש של "posts" מרענן גם את /api/posts. קריאת נתונים עם revalidate: 0 הופכת את הנתיב לדינמי.
הגשת ערך מיושן בזמן אימות מחדש
לאחר שגיל הרשומה חורג מ-revalidate:
- הבקשה הבאה מקבלת מיד את הערך המיושן (
STALE). - רענון יחיד פועל ברקע; בקשות מקבילות חולקות אותו.
- אם הרענון זורק חריגה, הערך המיושן ממשיך להיות מוגש והשגיאה נרשמת ביומן.
ב-Vercel וב-Cloudflare, רענונים ברקע נרשמים באמצעות waitUntil(). רשומות שגילן עולה על שנה תמיד מחושבות מחדש בחזית.
אימות מחדש לפי דרישה
import { revalidatePath, revalidateTag } from "@textjs/app/cache";export async function POST(request: Request) { // Anyone can POST here: require a secret shared with your CMS or webhook. if (request.headers.get("authorization") !== `Bearer ${process.env.REVALIDATE_SECRET}`) { return new Response("Unauthorized", { status: 401 }); } await revalidateTag("posts"); await revalidatePath("/api/feed"); return Response.json({ revalidated: true });}| קריאה | ביטול תוקף |
|---|---|
revalidateTag("posts") | כל מה שמתויג ב-posts; הבקשה הבאה מחשבת מחדש |
revalidateTag("posts", "max") | סימון כמיושן; הבקשה הבאה מקבלת את הערך המיושן בזמן הרענון |
revalidatePath("/api/feed") | הנתיב הזה |
revalidatePath("/api/posts/[id]") | כל נתיב של נתיב דינמי |
revalidatePath("/api", "layout") | כל מה שתחת /api |
שתיהן מחזירות promise. במאגר שבזיכרון ביטול התוקף מיידי; יש להמתין לו באמצעות await כאשר מאגר מותאם אישית הוא אסינכרוני.
מאגרי מטמון מותאמים אישית
מאגר ברירת המחדל הוא LRU בזיכרון (1,000 רשומות, כ-50 MB) לכל מופע שרת. כדי לשתף מטמון בין מופעים, יש להפנות את cacheHandler למודול שמייצא CacheStore כייצוא ברירת מחדל:
export default defineConfig({ cacheHandler: "./cache-handler.ts" });import type { CacheStore } from "@textjs/app/cache";const store: CacheStore = { async get(key) { return redis.get(key).then((v) => (v ? JSON.parse(v) : undefined)); }, async set(key, entry) { await redis.set(key, JSON.stringify(entry)); }, async delete(key) { await redis.del(key); }, async markTags(tags, mode, at) { for (const t of tags) await redis.hset(`tag:${t}`, mode, at); }, async getTagState(tags) { /* max expiredAt / staleAt across tags */ return { expiredAt: 0, staleAt: 0 }; },};export default store;הרשומות הן JSON רגיל. setCacheStore(store) מתקין במקום זאת מאגר בזמן הריצה.
הערות לגבי פלטפורמות
- לכל מופע serverless (פונקציות Vercel, סביבות Cloudflare isolates) יש מטמון משלו בזיכרון.
Cache-Control: s-maxageמאפשר גם ל-CDN של הפלטפורמה לשמור תגובות במטמון; אימות מחדש לפי דרישה אינו מנקה את מטמון ה-CDN. יש להשתמש ב-cacheHandlerמשותף כאשרrevalidateTagחייב להגיע לכל מופע. - Cloudflare דורש את הדגל
nodejs_compat(המטמון משתמש ב-AsyncLocalStorage). ראו פריסה.