التخزين المؤقت وإعادة التحقق
إعداد مقاطع المسار، و 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 الخاص بها كما لو كان وقت البناء: بلا سلسلة استعلام أو ترويسات أو ملفات تعريف ارتباط أو متن. اقرأ هذه العناصر في معالج ديناميكي بدلًا من ذلك. لا يُخزّن مؤقتًا سوى 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 ملفات تعريف الارتباط أو تعيد الكتابة أو تتجاوز ترويسات الطلب، تصبح الترويسة المولّدة 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 |
يعيد كلاهما وعدًا. مع المخزن الموجود في الذاكرة، يكون إبطال الصلاحية فوريًا؛ انتظره باستخدام 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) مخزنًا في وقت التشغيل بدلًا من ذلك.
ملاحظات المنصات
- لكل نسخة بلا خادم (دوال Vercel، وبيئات Cloudflare المعزولة) ذاكرة تخزين مؤقت خاصة بها في الذاكرة. تتيح
Cache-Control: s-maxageلشبكة CDN الخاصة بالمنصة تخزين الاستجابات مؤقتًا أيضًا؛ ولا تؤدي إعادة التحقق عند الطلب إلى تفريغ شبكة CDN. استخدمcacheHandlerمشتركًا عندما يجب أن تصلrevalidateTagإلى كل نسخة. - تتطلب Cloudflare العلامة
nodejs_compat(تستخدم ذاكرة التخزين المؤقتAsyncLocalStorage). راجع النشر.