كل المقالات والفيديوهات

الشبكة والتخزين المؤقت في المؤسسات: من يملك البيانات القديمة؟

الشبكة والتخزين المؤقت في واجهات المؤسسات: حدّ واحد للوصول إلى البيانات، وعقود محددة الأنواع، والإلغاء وإزالة التكرار، و Cache-Control، وإعادة محاولة آمنة.

Adil Ben Moussa
08 نوفمبر 2026· 6 دقائق
الشبكة والتخزين المؤقت في المؤسسات: من يملك البيانات القديمة؟

يُجري عميل تحويلاً، ثم يعود إلى لوحة المتابعة، فيرى الرصيد القديم. التحويل نجح، لكن لوحة المتابعة قرأت من ذاكرة مؤقتة (cache) لم يخبرها أحد بعملية الكتابة. وفي البنك، يعني ذلك اتصالاً بخدمة العملاء، وأحياناً تحويلاً ثانياً.

يأتي التخزين المؤقت في هذا الجزء، إلى جانب الشبكة، لأنهما في تطبيقات المؤسسات سؤال واحد: أين تعيش هذه البيانات، ومتى تصبح خاطئة؟ فالذاكرة المؤقتة التي لا مالك لها خللٌ مؤجَّل.

حدّ واحد نحو الواجهة الخلفية

المكوّنات لا تجلب البيانات أبداً. طبقة واحدة للوصول إلى البيانات () في كل مجال تتحدث إلى الواجهة الخلفية، وكل ما عداها يقرأ منها. فيصبح لديك مكان واحد لإضافة المصادقة، ومكان واحد للإلغاء، ومكان واحد لإبطال الذاكرة المؤقتة.

وحين تحتاج الشاشات إلى شكل من البيانات لا توفره الواجهات البرمجية للمجال، ضع بينهما واجهة خلفية مخصّصة للواجهة الأمامية ()، يملكها فريق الواجهة الأمامية. افعل ذلك من أجل الشكل ومن أجل الأسرار، لا لتتجنب الحديث مع فريق الـ API.

العقد مولَّد، و required هو الحقيقة

ولّد الأنواع من وثيقة بدل كتابتها يدوياً. فمخططات OpenAPI 3.1 «مجموعة شاملة لمواصفة JSON Schema، المسودة 2020-12»، وفي Schema «الخصائص المعرَّفة بالكلمة المفتاحية properties ليست مطلوبة» ما لم تذكرها قائمة required. لذلك اقرأ مصفوفة required، لا قائمة الحقول، لتعرف ما سيكون موجوداً.

ألغِ، وأزِل التكرار، وحدّد مهلة

ثلاثة أمور يحتاج إليها كل طلب. ألغِ ما تركه المستخدم: فـ httpResource في «يلغي الطلب المعلّق قبل إصدار طلب جديد» حين تتغير مدخلاته. ومن دون ذلك، يصف توثيق النتيجة: حالة سباق () يصل فيها ردّ أقدم أخيراً، فتعرض الشاشة نتائج خاطئة. وفي fetch العادي، يتيح لك AbortController «إلغاء طلب ويب واحد أو أكثر متى شئت».

وأزِل تكرار () ما هو قيد التنفيذ أصلاً، فإذا طلبت خمسةُ عناصر واجهة (widgets) بيانات الحسابات، خرج طلب واحد فقط. وأعطِ كل استدعاء مهلة (timeout)، كي تتحول الواجهة الخلفية البطيئة إلى رسالة خطأ، لا إلى مؤشر تحميل يدور بلا نهاية.

بعد الكتابة، من يحدّث الذاكرة المؤقتة؟

إبطال مُدخل في الذاكرة المؤقتة (cache invalidation) يعني وسمه بأنه متقادم، فتذهب القراءة التالية إلى الخادم من جديد. كل ذاكرة مؤقتة تحتاج إلى مالك، والمالك هو الكود الذي ينفّذ الكتابة. بعد التحويل، إما أن يعيد مخزن الحسابات جلب البيانات، وإما أن يطبّق ردّ الخادم. أما عنصر الواجهة الذي يحتفظ بنسخته الخاصة فلن يعلم بذلك أبداً.

وفي React دون إطار، طبقة البيانات أمر تختاره أو تبنيه بنفسك. ويذكر توثيقه التخزين المؤقت، وجلب البيانات على الخادم، وشلّالات الطلبات () على أنها الأجزاء الصعبة، ويوجّه إلى جلب البيانات المدمج في الأطر بدل التأثيرات (effects). وأيّاً كانت المكتبة التي تختارها، يبقى القرار نفسه: أيّ المفاتيح تجعلها هذه الكتابة خاطئة؟

الكود الذي يكتب البيانات يملك ذاكرتها المؤقتة.

على الهاتف في المغرب، الذاكرة المؤقتة أولاً

على الهاتف في المغرب، التخزين المؤقت ليس تحسيناً إضافياً. فخارج المدن الكبرى يكون الاتصال غالباً بطيئاً أو منقطعاً، والشاشة التي تنتظر الشبكة شاشةٌ يغلقها الناس. وفي Qismi، تطبيقنا المدرسي، حللنا ذلك بتخزين مؤقت مكثّف:

  • مستويان. كل قائمة محفوظة في الذاكرة وفي تخزين الهاتف، ولكل مُدخل مدة صلاحية (): خمس دقائق لسلاسل الرسائل، وساعة للإعلانات.
  • الذاكرة المؤقتة أولاً، والتحديث في الخلفية. تعرض الشاشة النسخة المخزّنة فوراً، وتجلب نسخة جديدة في الخلفية. ويُسمّى هذا النمط ، أي عرض القديم أثناء التحقق منه.
  • الشبكة تقرّر متى. على شبكة الواي فاي تُعدّ القائمة المخزّنة متقادمة بعد 20% من مدة صلاحيتها، وعلى 3G بعد 80%، وعلى 2G بعد 95%، ودون اتصال لا تُحدَّث أبداً. فتحصل الاتصالات البطيئة على السرعة، والسريعة على البيانات الحديثة.
  • الكتابة ما زالت تملك الذاكرة المؤقتة. بعد إرسال رسالة، يمسح الكود الذي أرسلها أربعة مُدخلات: قائمة سلاسل الرسائل لدى كلٍّ من الطرفين، والمحادثة نفسها، وعدد الرسائل غير المقروءة لدى المستلم.

الصورة: tt-tech.io، مقتبسة من web.dev، Keeping things fresh with stale-while-revalidate، بترخيص .

تُظهر الصورة النمط نفسه لردّ HTTP بقيمة max-age قدرها ثانية واحدة، ونافذة stale-while-revalidate مدتها 59 ثانية.

إنها القاعدة نفسها التي سبقت، مع عدد أكبر من الذواكر المؤقتة التي تحتاج إلى مالك.

ترويسات التخزين المؤقت في HTTP

ذاكرة المتصفح المؤقتة هي الذاكرة المؤقتة الثانية، وفي البنوك هي التي تُسرّب البيانات. تعني no-store أن «أي ذاكرة مؤقتة من أي نوع (خاصة أو مشتركة) يجب ألا تخزّن هذا الرد»؛ أما private فهي «للمحتوى المخصّص للمستخدم». والمحتوى المخصّص في ذاكرة مؤقتة مشتركة «قد يسبب تسريباً غير مقصود للمعلومات». أما ملفات البناء التي تحمل بصمة (hash) في اسمها فهي الحالة المعاكسة: max-age طويلة مع immutable.

الصورة: tt-tech.io، مقتبسة من web.dev، Prevent unnecessary network requests with the HTTP Cache، بترخيص CC BY 4.0.

إعادة المحاولة وتساوي الأثر

إعادة محاولة طلب GET آمنة، أما إعادة محاولة تحويل فليست كذلك. فطريقتا POST و PATCH «ليس مضموناً أن تكونا متساويتي الأثر»، ويستطيع العميل أن يعيد بأمان محاولة طلب «يستخدم طريقة متساوية الأثر». لذلك إما أن تحمل الكتابة مفتاحاً يزيل الخادم به التكرار، وإما ألا يُعاد إرسالها تلقائياً أبداً. وحين يجيب الخادم بـ 409 Conflict، أي «تعارض الطلب مع الحالة الحالية للمورد المستهدف»، اعرض ذلك على المستخدم وأعد التحميل، ولا تُعِد المحاولة.

المعترضات () هي المكان الذي تعيش فيه الأجزاء المشتركة بين الطلبات: المصادقة، وإعادة المحاولة، والمهل، والتسجيل. اقصر إعادة المحاولة على القراءات. وأضيف هناك أيضاً معرّف ربط ()، كي يمكن مطابقة سجل التدقيق على الخادم مع ما نقر عليه المستخدم.

في Atlas Bank

لا يملك Atlas خادماً حقيقياً. FakeBackend في libs/data-access/banking هو الحدّ (يقول تعليقه: «لا شيء آخر في التطبيق يتحدث إلى "الخادم"»)، مع تأخير محاكى قدره 250 ميلي ثانية كي تكون حالات التحميل حقيقية. وتزيل AccountsStore.load() التكرار:

  /** متساوية الأثر: المستدعون المتزامنون يتشاركون طلباً واحداً قيد التنفيذ؛ والمستدعون اللاحقون يحصلون على النتيجة فوراً. */
  load(): Promise<void> {
    if (this.loadedState()) return Promise.resolve();
    this.inFlight ??= this.fetchAll();
    return this.inFlight;
  }

بعد التحويل، يضيف المخزن نفسه المعاملة التي تعيدها الواجهة الخلفية، ويطرح المبلغ من الرصيد، فتصبح كل شاشة تقرأ منه صحيحة. ويفشل الفحص الآلي () عند استدعاء fetch نحو عنوان مطلق، لأن «المتصفح لا يستدعي طرفاً ثالثاً مباشرة أبداً». أما ما لا يملكه Atlas بعد: الإلغاء، والمهل، ومفتاح عدم التكرار () في transfer(). وهذه تأتي مع طبقة HTTP حقيقية.

ما تركته جانباً، ولماذا

  • مقابل . في المؤسسات، الواجهة الخلفية هي التي تقرّر عادة.
  • جولة على و والاستطلاع الطويل (). حين تحتاج إلى الزمن الحقيقي، فهو قرار واحد بشأن تدفق واحد.
  • عمّال الخدمة ( workers) والعمل دون اتصال أولاً () على الويب. نادران في تطبيقات الويب الخاضعة للتنظيم. أما على الهاتف فالأمر مختلف، كما يُظهر Qismi.
  • التخزين المؤقت على حافة شبكة . من شأن فرق التشغيل، لا من تصميم الواجهة الأمامية.

مع بداية الأسبوع

  • ابحث في تطبيقك عن fetch( و HttpClient خارج طبقة الوصول إلى البيانات. كل نتيجة حدٌّ غائب.
  • اختر أهم عملية كتابة لديك. ودوّن البيانات المخزّنة التي تجعلها خاطئة، ومن يحدّثها.
  • افحص ترويسة Cache-Control في أكثر ردودك حساسية.

التالي: الجزء الرابع، عن العرض.

لوكيل البرمجة الخاص بك

قواعد هذا المقال، مكتوبة لوكيل. الصقها في ملف AGENTS.md لديك، أو احفظها مهارةً ()، أو وجّه وكيلك إلى الملف.

لوكيل البرمجة الخاص بك تنزيل ملف ‎.md
# Networking and caching rules for enterprise apps

Source: https://tt-tech.io/blog/enterprise-frontend-networking-caching
For Angular and React frontends. Add to AGENTS.md or save as a skill. Your project's own rules win where they differ.

## Rules

1. Components never fetch. All backend calls go through one data-access layer per domain. No `fetch(` or `HttpClient` outside it.
2. Add a backend for frontend (BFF), owned by the frontend team, only when the screens need a shape the domain APIs do not have, or to keep secrets out of the browser.
3. Generate API types from the OpenAPI document. Never write them by hand. Treat a field as always present only when the schema's `required` array names it.
4. Cancel requests the user left: `httpResource` in Angular, `AbortController` with `fetch`. An older response must never overwrite a newer one.
5. Dedupe requests already in flight, so several widgets asking for the same data cause one request.
6. Give every call a timeout, so a slow backend becomes an error message, not an endless spinner.
7. The code that performs a write owns the cache of that data. After the write, refetch or apply the server's answer in the owning store. Never keep a private copy in a widget.
8. Set `Cache-Control` on purpose: `no-store` for responses that must not be reused (an account balance), `private` for user-personalised content, never personalised content in a shared cache, and a long `max-age` plus `immutable` for hashed build assets.
9. Retry reads only. Never retry a `POST` or `PATCH` automatically unless it carries an idempotency key that the server deduplicates.
10. On `409 Conflict`, show the conflict to the user and reload. Do not retry.
11. Put cross-cutting concerns in interceptors: auth headers, timeouts, read retries, logging, and a correlation id per request so server logs can be matched to what the user clicked.
12. On mobile, where the connection is often slow or gone, cache first: show the cached copy at once and refresh it in the background (stale-while-revalidate), with a time to live per entry. The write still clears what it makes wrong.

## Before you say done

- Search the diff for `fetch(` and `HttpClient` outside the data-access layer. Move each hit behind the boundary.
- For every write in the diff, list in the pull request which cached data it makes wrong and where it is refreshed.
- Check every retry in the diff: it is on a read, or the write carries an idempotency key.
- For every new or changed sensitive response, state its `Cache-Control` header.

المصادر

  1. OpenAPI Initiative, OpenAPI Specification v3.1.1, 2024
  2. , object, التوثيق
  3. Angular, Reactive data fetching with httpResource, التوثيق
  4. React, You Might Not Need an Effect, التوثيق
  5. , AbortController
  6. MDN, Cache-Control header
  7. MDN, HTTP caching
  8. MDN, Idempotent
  9. MDN, 409 Conflict
  10. Angular, Intercepting requests and responses, التوثيق
  11. web.dev, Keeping things fresh with stale-while-revalidate
  12. web.dev, Prevent unnecessary network requests with the HTTP Cache
شارك هذا المقال