الـ Memory والـ GC والـ Leaks: الـ process اللي بيكبر لحد ما يقع

جارٍ التحميل…

01 / ملخص الدرس

الـ Memory والـ GC والـ Leaks: الـ process اللي بيكبر لحد ما يقع

cache من غير حد و listener جديد مع كل request، فالـ heap بيكبر مع كل request ومش بيرجع. بنفهم rss و heapUsed، والـ young والـ old generation والـ GC pauses، و max-old-space-size في الـ containers، وأشهر أنواع الـ leaks، و heap snapshots وإزاي تقارن اتنين وتدوّر على الحاجة اللي زادت بعدد الـ requests، و LRU و TTL. والتحدي: mem.js قصاد تسعتاشر حالة.

02 / شرح الدرس مكتوبًا

نص شرح المعلم والكود الذي كتبه ونتيجة تشغيله، بنفس ترتيب الدرس.

أهلًا بيك. أصعب bug في production: الـ app شغّال تمام، وبعد يومين يقع بـ out of memory، ويقوم تاني، وبعد يومين يقع تاني. ومحدش عارف ليه، لأن مفيش error، ومفيش request معين بيوقّعه. ده memory leak. server dot js فيه route اسمه report: بيعمل Report ويحطه في cache، بـ key فيه اسم الـ user والوقت. وبيضيف listener على event اسمه flush علشان يطبع آخر url. وفيه اتنين routes للمعمل: debug memory بيعمل GC الأول ويرجّع حجم الـ heap، و debug snapshot بيكتب heap snapshot في ملف. علشان GC بإيدينا، بنشغّل node بـ expose-gc. و leak dot js: خمسين user، ألفين request، وكل خمسمية يطبع الـ memory، وبياخد snapshot في الأول وفي الآخر. نشغّل.

leak.js
// leak.js: خمسين user، كل واحد بيطلب الـ report بتاعه، وكل مرة الـ t جديد
const B = "http://localhost:3000";
const get = async (p) => (await fetch(B + p)).json();
const name = process.argv[2] ?? "run";
const show = async (i) => { const m = await get("/debug/memory"); console.log(`${String(i).padStart(5)} request ← heap ${m.heapUsedMB}MB | cache ${m.cache} | listeners ${m.listeners}`); };

await get(`/debug/snapshot?name=${name}-before`);
await show(0);
for (let i = 1; i <= 2000; i++) {
  await get(`/report?user=u${i % 50}&t=${i}`);
  if (i % 500 === 0) await show(i);
}
await get(`/debug/snapshot?name=${name}-after`);
console.log(`snapshots: snaps/${name}-before و snaps/${name}-after`);

المخرجات

$ mkdir -p snaps; node --expose-gc server.js & S=$!; sleep 1; node leak.js naive; kill $S
on 3000
    0 request ← heap 6.6MB | cache 0 | listeners 0
warning: MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 flush listeners added t
  500 request ← heap 10.5MB | cache 500 | listeners 500
 1000 request ← heap 13.8MB | cache 1000 | listeners 1000
 1500 request ← heap 17.1MB | cache 1500 | listeners 1500
 2000 request ← heap 20.3MB | cache 2000 | listeners 2000
snapshots: snaps/naive-before و snaps/naive-after

الـ heap بيكبر مع كل دفعة، بنفس المقدار تقريبًا، ومش بيرجع، رغم إننا بنعمل GC قبل كل قياس. يعني الحاجات دي لسه حد ماسكها. الـ cache فيه ألفين، والـ listeners ألفين. وفوق، الـ server طبع warning اسمه MaxListenersExceededWarning: أكتر من عشرة listeners على نفس الـ event. ده مش error، والـ app كمّل عادي، بس ده من أهم الـ warnings اللي لازم ما تتجاهلهاش. وعلشان كده الـ server بيسمع على event اسمه warning ويبعته للـ logs، بدل ما يضيع في الـ stderr. ودلوقتي اتخيل ده بـ مِيّة request في التانية. كام ساعة والـ process يقع.

الـ memory في Node فيها كام رقم: rss: كل اللي الـ process ماسكه من الـ RAM. ده اللي الـ container بيقيسه وبيقتلك لو عدّى الـ limit. heapUsed: الـ JavaScript objects بتاعتك. و external و arrayBuffers: الـ Buffers والحاجات اللي بره الـ heap. والـ GC في V8 بيقسّم الـ heap لجيلين: young generation: صغير، وكل object جديد بيتعمل فيه. أغلب الـ objects بتموت بسرعة، فـ GC بتاعه اسمه scavenge، سريع وبيحصل كتير. old generation: اللي عاش أكتر من GC، بيتنقل هنا. والـ GC بتاعه mark sweep compact، أتقل، وممكن يعمل pause يبان في الـ event loop delay بتاع الدرس اللي فات. والـ GC بيمسح بس اللي محدش ماسكه. لو فيه Map أو array أو listener لسه بيشاور على object، الـ object ده عايش، حتى لو إنت مش محتاجه تاني. ده هو الـ leak: حاجة reachable ومش محتاجها. وفي containers: الـ old generation ليها حد. اعمل max-old-space-size حوالي خمسة وسبعين في المِيّة من الـ container limit، علشان يبقى فيه مكان للـ Buffers والـ stack. كده V8 يحاول بجد قبل ما يوصل للحد، وتاخد error واضح بدل ما الـ kernel يقتل الـ process من غير ما يقول حاجة.

أشهر أنواع الـ leaks: cache من غير حد: Map أو object بيكبر مع كل key جديد. وخصوصًا لو الـ key فيه حاجة بتتغيّر زي الوقت أو الـ request id. الحل: LRU بحد أقصى و TTL. وفيه مكتبة اسمها lru-cache بتعمل ده. listeners: كل request بيضيف listener ومحدش بيشيله. والـ listener ده closure، يعني ماسك كل اللي حواليه. الحل: listener واحد في الـ startup، أو once، أو off لما الـ request يخلص. timers: setInterval جُوّه request من غير clearInterval. closures: function صغيرة ماسكة req، و req ماسك res، و res ماسك الـ socket، فـ request واحد كامل فضل عايش. global arrays للـ logs أو الـ metrics. و promises عمرها ما بتخلص، ومستنيين عليها حاجات. ولو عايز تربط data بـ object من غير ما تمسكه: WeakMap. لما الـ object يموت، الـ entry بتموت معاه.

طيب في production، تعرف منين؟ وتلاقيه إزاي؟ الـ graph بتاع heapUsed: الطبيعي شكله زي سنان المنشار، يطلع والـ GC ينزّله لنفس المستوى. الـ leak شكله زي السلم: كل مرة الـ GC بينزّله لمستوى أعلى من اللي قبله. الأداة: heap snapshot. صورة لكل object في الـ heap، مين ماسك مين. تاخدها بـ v8 writeHeapSnapshot، أو بـ heapsnapshot-signal وتبعت signal للـ process، أو heapsnapshot-near-heap-limit علشان ياخدها لوحده قبل ما يقع. الطريقة: snapshot، وبعدين load، وبعدين snapshot تاني، وقارن. وفي Chrome DevTools فيه Comparison view بيعمل كده. والـ trick: لو بعت ألفين request، دوّر على الحاجات اللي زادت ألفين بالضبط. دول اللي بيتعملوا مرة لكل request ومش بيموتوا. وبعدها بص على الـ retainers: مين ماسك الـ object ده. وخلي بالك: الـ snapshot بيوقّف الـ process لحد ما يخلص، ومحتاج memory قد الـ heap تقريبًا. فخده من instance واحدة بعد ما تشيلها من الـ load balancer.

server dot js الجديد: الـ cache بقى createLRU بحد خمسمية و TTL دقيقة، والـ listener بقى واحد بس في الـ startup، والـ route بيغيّر متغيّر اسمه lastUrl. و diff dot js بيقرا الـ snapshots ويطبع اللي زاد. التحدي: اكتب mem dot js. test dot js فيه تسعتاشر حالة.

تمرين

mem.js: createLRU بـ Map وحد أقصى بيشيل الأقدم و TTL وقت الـ get و now ينفع يتغيّر، و heapCounts بيقرا الـ nodes بالـ node_fields ويعدّ بالاسم، و diffHeaps بيطلّع اللي زاد مترتب بالـ size.

الحل. createLRU: الـ Map بيفتكر ترتيب الإضافة، فلما نلمس key نمسحه ونحطه تاني، فيروح الآخر. والأقدم دايمًا أول واحد. والـ TTL بيتشيك وقت الـ get بس، من غير timers. heapCounts: بنمشي على الـ nodes بخطوة قد عدد الـ fields، وكل field بالـ index بتاعه من الـ meta. diffHeaps: الفرق لكل اسم، واللي زاد بس. نقارن الـ snapshots بتاعة الـ server القديم، اللي اتسجّلوا في أول الدرس.

mem.js
// mem.js: cache بحد، وقراية heap snapshots ومقارنتهم

// LRU: Map بيحفظ ترتيب الإضافة، فأول key فيه هو أقدم واحد اتلمس
export function createLRU({ max = 1000, ttlMs = Infinity, now = Date.now } = {}) {
  const m = new Map();
  const fresh = (e) => now() - e.at < ttlMs;
  return {
    get(k) {
      const e = m.get(k);
      if (!e) return undefined;
      if (!fresh(e)) { m.delete(k); return undefined; } // انتهى
      m.delete(k); m.set(k, e); // اتلمس ← يروح الآخر
      return e.v;
    },
    set(k, v) {
      m.delete(k);
      m.set(k, { v, at: now() });
      while (m.size > max) m.delete(m.keys().next().value); // شيل الأقدم
      return this;
    },
    has(k) { return this.get(k) !== undefined; },
    get size() { return m.size; },
  };
}

// heap snapshot: nodes array واحدة طويلة، كل node عدد fields بالترتيب اللي في meta.node_fields
const KINDS = new Set(["object", "closure", "array", "string", "regexp"]);
export function heapCounts(snap) {
  const meta = snap.snapshot.meta, F = meta.node_fields, types = meta.node_types[0];
  const step = F.length, iType = F.indexOf("type"), iName = F.indexOf("name"), iSize = F.indexOf("self_size");
  const out = new Map();
  for (let i = 0; i < snap.nodes.length; i += step) {
    const type = types[snap.nodes[i + iType]];
    if (!KINDS.has(type)) continue;
    // الـ object اسمه الـ constructor، والـ closure اسمه اسم الـ function، والباقي بنوعه
    const name = type === "object" || type === "closure" ? `${snap.strings[snap.nodes[i + iName]] || "(anonymous)"}${type === "closure" ? "()" : ""}` : `(${type})`;
    const r = out.get(name) ?? { count: 0, size: 0 };
    r.count++; r.size += snap.nodes[i + iSize];
    out.set(name, r);
  }
  return out;
}

export function diffHeaps(before, after, { n = 5 } = {}) {
  const a = heapCounts(before), b = heapCounts(after);
  return [...b].map(([name, r]) => {
    const old = a.get(name) ?? { count: 0, size: 0 };
    return { name, countDelta: r.count - old.count, sizeDelta: r.size - old.size };
  }).filter((r) => r.countDelta > 0).sort((x, y) => y.sizeDelta - x.sizeDelta).slice(0, n);
}

المخرجات

$ node diff.js naive
أكتر حاجات زادت في naive:
  + 82076 نسخة  +  2925KB  (string)
  + 28015 نسخة  +  2681KB  (array)
  + 54022 نسخة  +  2407KB  Object
  + 26010 نسخة  +   813KB  Array
  +  2000 نسخة  +   781KB  ServerResponse
  +  8002 نسخة  +   641KB  system / Context

زادوا 2000 بالضبط، يعني واحد لكل request: ServerResponse، IncomingMessage، Url، next()، trimPrefix()، onFlush()، Report

فوق: strings و arrays و objects. دول دايمًا فوق، ومش بيقولوا حاجة كتير. تحت، اللي زادوا ألفين بالضبط: Report، ده الـ cache. و onFlush، ده الـ listener. وكمان ServerResponse و IncomingMessage، يعني الـ req والـ res بتوع كل request لسه عايشين. ليه؟ لأن onFlush closure بيستخدم req dot url، فماسك req، و req ماسك res. فالـ listener الصغير ده خلّى كل request كامل يفضل في الـ memory. نشغّل الـ server الجديد، ونقارن الـ snapshots بتوعه.

mem.js

المخرجات

$ mkdir -p snaps; node --expose-gc server.js & S=$!; sleep 1; node leak.js fixed; kill $S; node diff.js fixed
on 3000
    0 request ← heap 6.6MB | cache 0 | listeners 1
  500 request ← heap 8.4MB | cache 500 | listeners 1
 1000 request ← heap 8.5MB | cache 500 | listeners 1
 1500 request ← heap 8.6MB | cache 500 | listeners 1
 2000 request ← heap 8.5MB | cache 500 | listeners 1
snapshots: snaps/fixed-before و snaps/fixed-after
أكتر حاجات زادت في fixed:
  + 10523 نسخة  +   489KB  Object
  + 11551 نسخة  +   342KB  (string)
  +   514 نسخة  +   114KB  (array)
  +   500 نسخة  +    23KB  Report
  +   509 نسخة  +    16KB  Array
  +     1 نسخة  +     0KB  Socket

زادوا 2000 بالضبط، يعني واحد لكل request: ولا حاجة

الـ heap طلع شوية في الأول، وبعدين واقف مكانه. ده الـ cache بيتملي لحد خمسمية ويقف. listener واحد، ومفيش warning. والـ diff: خمسمية Report بس، ده حجم الـ cache بالضبط. ومفيش حاجة زادت ألفين. وخلي بالك: الـ key لسه فيه الوقت، يعني الـ cache عمره ما هيلاقي حاجة. ده bug تاني. بس حتى لو الـ key صح، أي cache لازم يبقى ليه حد. ونشغّل test dot js.

mem.js

المخرجات

$ mkdir -p snaps; node --expose-gc server.js & S=$!; sleep 1; node test.js; kill $S
on 3000
✅ LRU: get و size 
✅ LRU: الرابع بيشيل الأقدم (b، لأن a اتلمس) 
✅ LRU: set لـ key موجود بيحدّثه ويرجّعه الآخر 
✅ LRU: key مش موجود ← undefined 
✅ TTL: قبل ما يخلص ← موجود 
✅ TTL: بعد ما يخلص ← undefined ومتشال 
✅ LRU: عشرة آلاف set ← مية بس 
✅ heapCounts: User اتنين بتمانين byte 
✅ heapCounts: الـ closure باسم الـ function و () 
✅ heapCounts: string ← (string)، و object من غير اسم ← (anonymous) 
✅ heapCounts: code و hidden مش محسوبين 
✅ diffHeaps: onClick زاد اتنين، User زاد واحد 
✅ diffHeaps: مترتب بالـ size، واللي ما زادش مش موجود 
✅ diffHeaps: n 
✅ snapshot حقيقي: Leaky زادت تلات آلاف 3000
✅ server: الـ cache مش بيعدّي خمسمية 500
✅ server: listener واحد بس على flush 1
✅ server: الـ heap ما زادش بعد ألفين request كمان 9MB ← 8.9MB
✅ server: الـ report لسه شغّال 

19/19 نجحوا

كله نجح. الدرس الجاي: مشكلة أداء مش في Node خالص، في الـ database. الـ N plus one: صفحة واحدة بتعمل مِيّة query من غير ما تاخد بالك، وإزاي DataLoader يجمعهم في query واحدة.

server.js
// server.js: نفس الـ app، بس الـ cache ليه حد، والـ listener واحد بس
import express from "express";
import { EventEmitter } from "node:events";
import { writeHeapSnapshot } from "node:v8";
import { createLRU } from "./mem.js";

class Report {
  constructor(user, t) {
    this.user = user;
    this.t = t;
    this.rows = Array.from({ length: 20 }, (_, i) => ({ i, v: i * 3.5, label: `row-${user}-${i}` }));
  }
}

const app = express();
// warnings بتاعة Node بتروح للـ logs زي أي حاجة تانية، علشان ما تضيعش
process.on("warning", (w) => console.log(`warning: ${w.name}: ${w.message.slice(0, 70)}`));
const bus = new EventEmitter();
const cache = createLRU({ max: 500, ttlMs: 60_000 }); // أي cache لازم يبقى ليه حد
let lastUrl = null;
bus.on("flush", () => console.log("flush", lastUrl)); // listener واحد، مرة واحدة في الـ startup

app.get("/report", (req, res) => {
  const key = `${req.query.user}:${req.query.t}`;
  if (!cache.has(key)) cache.set(key, new Report(req.query.user, req.query.t));
  lastUrl = req.url;
  res.json({ rows: cache.get(key).rows.length });
});
// للمعمل بس: في production الـ routes دي ورا auth، أو على port داخلي
app.get("/debug/memory", (req, res) => {
  globalThis.gc?.(); // محتاج node --expose-gc: نقيس اللي فاضل بعد الـ GC بس
  const m = process.memoryUsage();
  const mb = (x) => Math.round((x / 1048576) * 10) / 10;
  res.json({ heapUsedMB: mb(m.heapUsed), rssMB: mb(m.rss), cache: cache.size, listeners: bus.listenerCount("flush") });
});
app.get("/debug/snapshot", (req, res) => {
  globalThis.gc?.();
  res.json({ file: writeHeapSnapshot(`snaps/${req.query.name}.heapsnapshot`) });
});
app.listen(3000, () => console.log("on 3000"));
diff.js
// diff.js: إيه اللي زاد بين الـ snapshot الأولاني والأخراني؟
import { readFileSync } from "node:fs";
import { diffHeaps } from "./mem.js";

const name = process.argv[2] ?? "naive";
const load = (f) => JSON.parse(readFileSync(`snaps/${name}-${f}.heapsnapshot`, "utf8"));
console.log(`أكتر حاجات زادت في ${name}:`);
for (const r of diffHeaps(load("before"), load("after"), { n: 6 }))
  console.log(`  +${String(r.countDelta).padStart(6)} نسخة  +${(r.sizeDelta / 1024).toFixed(0).padStart(6)}KB  ${r.name}`);

// الـ trick: دوّر على الحاجات اللي زادت بعدد الـ requests بالضبط
const per = Number(process.argv[3] ?? 2000);
const exact = diffHeaps(load("before"), load("after"), { n: 1000 }).filter((r) => r.countDelta === per);
console.log(`\nزادوا ${per} بالضبط، يعني واحد لكل request: ${exact.map((r) => r.name).join("، ") || "ولا حاجة"}`);
test.js
import { writeHeapSnapshot } from "node:v8";
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { createLRU, heapCounts, diffHeaps } from "./mem.js";

let ok = 0, all = 0;
const check = (name, cond, extra = "") => { all++; if (cond) ok++; console.log(cond ? "✅" : "❌", name, extra); };
const B = "http://localhost:3000";

const c = createLRU({ max: 3 });
c.set("a", 1).set("b", 2).set("c", 3);
check("LRU: get و size", c.get("a") === 1 && c.size === 3);
c.set("d", 4);
check("LRU: الرابع بيشيل الأقدم (b، لأن a اتلمس)", !c.has("b") && c.has("a") && c.has("c") && c.has("d") && c.size === 3);
c.set("c", 30);
c.set("e", 5);
check("LRU: set لـ key موجود بيحدّثه ويرجّعه الآخر", c.get("c") === 30 && !c.has("a"));
check("LRU: key مش موجود ← undefined", c.get("zzz") === undefined);
let t = 0;
const tl = createLRU({ max: 10, ttlMs: 100, now: () => t });
tl.set("x", 1);
t = 99;
check("TTL: قبل ما يخلص ← موجود", tl.get("x") === 1);
t = 100;
check("TTL: بعد ما يخلص ← undefined ومتشال", tl.get("x") === undefined && tl.size === 0);
const big = createLRU({ max: 100 });
for (let i = 0; i < 10000; i++) big.set(i, i);
check("LRU: عشرة آلاف set ← مية بس", big.size === 100 && big.has(9999) && !big.has(0));

const fake = (nodes) => ({
  snapshot: { meta: { node_fields: ["type", "name", "id", "self_size", "edge_count"], node_types: [["hidden", "array", "string", "object", "code", "closure"]] } },
  strings: ["", "User", "onClick", "x"],
  nodes: nodes.flatMap(([type, name, size]) => [type, name, 0, size, 0]),
});
const s1 = fake([[3, 1, 40], [3, 1, 40], [5, 2, 32], [2, 3, 16], [4, 3, 100], [0, 3, 8], [3, 0, 24]]);
const hc = heapCounts(s1);
check("heapCounts: User اتنين بتمانين byte", hc.get("User")?.count === 2 && hc.get("User")?.size === 80);
check("heapCounts: الـ closure باسم الـ function و ()", hc.get("onClick()")?.count === 1);
check("heapCounts: string ← (string)، و object من غير اسم ← (anonymous)", hc.get("(string)")?.count === 1 && hc.get("(anonymous)")?.count === 1);
check("heapCounts: code و hidden مش محسوبين", hc.size === 4);
const s2 = fake([[3, 1, 40], [3, 1, 40], [3, 1, 40], [5, 2, 32], [5, 2, 32], [5, 2, 32], [2, 3, 16]]);
const d = diffHeaps(s1, s2);
check("diffHeaps: onClick زاد اتنين، User زاد واحد", d.find((r) => r.name === "onClick()")?.countDelta === 2 && d.find((r) => r.name === "User")?.countDelta === 1);
check("diffHeaps: مترتب بالـ size، واللي ما زادش مش موجود", d[0].name === "onClick()" && !d.some((r) => r.name === "(string)" || r.name === "(anonymous)"));
check("diffHeaps: n", diffHeaps(s1, s2, { n: 1 }).length === 1);

class Leaky { constructor(i) { this.i = i; } }
const dir = mkdtempSync(`${tmpdir()}/heap-`);
const snap = (n) => JSON.parse(readFileSync(writeHeapSnapshot(`${dir}/${n}.heapsnapshot`), "utf8"));
const before = snap("a");
globalThis.keep = Array.from({ length: 3000 }, (_, i) => new Leaky(i));
const after = snap("b");
rmSync(dir, { recursive: true });
const leaky = diffHeaps(before, 
…
03 / دروس المسار (122)
  1. 01أهلًا بيك: يعني إيه backend، والكورس ده ماشي إزاي
  2. 02الملفات والفولدرات والـ terminal: تتحرك على جهازك زي المبرمجين
  3. 03Node وأول برنامج: تشغّل وتقرا رسالة الخطأ
  4. 04القيم والمتغيرات والأنواع: const و let و typeof
  5. 05القرارات والتكرار: if و === والـ truthy والـ loops
  6. 06الـ functions: parameters و return والـ arrow functions والـ scope
  7. 07الـ objects والـ arrays: destructuring و spread و map و filter و reduce
  8. 08JSON والنصوص والتواريخ: اللغة اللي الـ backend بيتكلم بيها
  9. 09الـ callbacks والـ closures وأول لمسة async: setTimeout
  10. 10الـ classes والـ errors: throw و try/catch و stack trace
  11. 11git: تحفَظ كل خطوة وترجعلها، وما تسرّبش أسرارك
  12. 12ESM و CommonJS و package exports: Node بيلاقي الـ modules إزاي
  13. 13الـ Promises و async/await: sequential و parallel والأخطاء والإلغاء
  14. 14الـ event loop والـ microtasks: Node بيعمل كل حاجة بـ thread واحد إزاي
  15. 15الـ Streams والـ Backpressure: ملفات أكبر من الـ memory
  16. 16الـ File System والـ paths: قراية وكتابة صح، ومن غير path traversal
  17. 17الـ Buffers والبيانات الـ binary: الـ bytes من جُوّه
  18. 18Crypto APIs والأسرار: random و hash و HMAC و AES-GCM
  19. 19الـ Worker Threads: شغل CPU تقيل من غير ما توقف الـ server
  20. 20الـ Errors على مستوى الـ process: operational و programmer و crash-only
  21. 21الـ Node test runner والـ debugging: tests من غير مكتبات
  22. 22HTTP من جُوّه: الـ methods والـ status codes والـ headers
  23. 23Express والـ Routing: الـ params والـ query والترتيب والـ Router
  24. 24Express 5: إيه اللي اتغيّر، وإزاي تنقل مشروع قديم
  25. 25الـ Middleware ودورة حياة الـ request جُوّه Express
  26. 26Validation للمدخلات بـ Zod: ما تصدّقش أي حاجة جاية من برة
  27. 27تصنيف الأخطاء و Problem Details: شكل واحد لكل الـ errors
  28. 28الـ CORS والـ Cookies: المتصفح بيحمي المستخدم إزاي
  29. 29رفع الملفات وحدود الحجم: multer من غير ثغرات
  30. 30الـ Realtime: Server-Sent Events و WebSockets
  31. 31OpenAPI والـ API Versioning: عقد مكتوب، وتغيير من غير ما تكسر حد
  32. 32PostgreSQL: الـ DDL وأنواع البيانات الصح لكل عمود
  33. 33الـ Constraints: PK و FK و UNIQUE و CHECK — الـ database بتحمي نفسها
  34. 34الـ Normalization والـ ERD: تقسّم البيانات صح من أول يوم
  35. 35الـ JOINs والتجميع: INNER و LEFT و GROUP BY و HAVING و window functions
  36. 36JSONB في PostgreSQL: إمتى تستخدمه، وإزاي تسأله وتعمله index
  37. 37الـ Indexes من جُوّه: B-tree و GIN و BRIN
  38. 38Partial و Covering indexes: indexes أصغر وأسرع
  39. 39الـ Pagination بالـ Keyset: ليه OFFSET بيبطّأ وبيكرر
  40. 40Full Text Search في Postgres: بحث بالكلمات، بالعربي والإنجليزي
  41. 41EXPLAIN ANALYZE والـ Planner: اقرا الـ plan وصلّح الـ query
  42. 42الـ Transactions بـ node-postgres: كله أو ولا حاجة
  43. 43الـ MVCC ومستويات الـ Isolation: transactions كتير في نفس الوقت
  44. 44الـ Locks والـ Deadlocks: مين مستني مين
  45. 45SKIP LOCKED و Job Leasing: Postgres كـ job queue
  46. 46Row Level Security والـ Multi-tenancy: كل عميل يشوف بياناته بس
  47. 47Migrations من غير downtime: expand و backfill و contract
  48. 48الـ Replication والـ Read Replicas: نسخ حية من الـ database
  49. 49الـ Connection Pooling و PgBouncer: connections أقل، requests أكتر
  50. 50الـ Partitioning والـ VACUUM والمراقبة: Postgres على المدى الطويل
  51. 51Redis وأنواع البيانات: strings و hashes و lists و sets و sorted sets
  52. 52Cache-aside والـ TTL: Redis قدام الـ database
  53. 53الـ Cache invalidation والـ Stampede: لما الـ key الشهير يموت
  54. 54Lua والـ Atomicity في Redis: كام أمر كأنهم أمر واحد
  55. 55الـ Rate Limiting في Redis: fixed window و sliding window و token bucket
  56. 56الـ Sessions في Redis: تسجيل دخول تقدر تلغيه في أي وقت
  57. 57Redis Streams والـ Consumer Groups: log بيتوزع على workers
  58. 58Pub/Sub و BullMQ: إشارات بين الـ instances، وطوابير شغل محترمة
  59. 59Redis لما يقع: الـ persistence (RDB و AOF) والـ replication
  60. 60Redis في الإنتاج: الـ ACLs والـ Cluster والمراقبة
  61. 61تخزين الـ Passwords بـ Argon2id: صح من أول مرة
  62. 62Session Fixation والـ Rotation: id جديد مع كل صلاحية جديدة
  63. 63الـ Cookie Flags والـ CSRF: المتصفح بيبعت الـ cookie، فلازم تتأكد مين اللي طلب
  64. 64الـ XSS والـ Content Security Policy: ما تخليش النص يتحوّل لـ code
  65. 65الـ OAuth و OpenID Connect و PKCE: login بـ Google من غير ما تشوف الـ password
  66. 66التأكد من الـ JWT صح: التوقيع والـ claims والـ scopes
  67. 67الـ BOLA و BFLA: الـ token السليم مش معناه إن الطلب مسموح
  68. 68الـ RBAC و ABAC والملكية: policy واحدة بدل ifs متفرقة
  69. 69الـ MFA والـ recovery: التاني factor اللي بجد
  70. 70الـ Passkeys و WebAuthn: login من غير password وضد الـ phishing
  71. 71الـ Threat Modeling بـ STRIDE: فكّر زي المهاجم قبل ما تكتب code
  72. 72الـ SQL Injection والـ Parameterization: الـ data ما تبقاش code
  73. 73الـ SSRF والـ Egress: ما تخليش الـ server بتاعك يزور الشبكة الداخلية
  74. 74الـ Security Headers والـ TLS: كل response يقول للـ browser يحمي المستخدم إزاي
  75. 75توقيع الـ Webhooks والـ Replay: الـ webhook ده جاي فعلًا من Stripe؟
  76. 76تغيير الأسرار والـ HMAC: إزاي تغيّر مفتاح من غير ما كل حاجة تقع
  77. 77سلسلة التوريد والـ SBOM: إنت واثق في ألف package ما تعرفهمش؟
  78. 78الـ Audit والـ Privacy Logs: سجّل مين عمل إيه، من غير ما تسجّل الأسرار
  79. 79اختبار الأمان و OWASP ASVS: الأمن جُوّه الـ CI، مش audit آخر السنة
  80. 80الـ Prototype Pollution والـ Resource DoS: request واحد يخلّي الكل admin أو يوقّع الـ server
  81. 81الـ SLI والـ SLO والـ Error Budget: إمتى تصحّي الـ on-call وإمتى تسيبه نايم
  82. 82الـ Event Loop Delay والـ ELU: لما Node نفسه يتخنق
  83. 83الـ Worker Threads والـ CPU Profiling: الشغل التقيل بره الـ event loop، والوقت راح فين
  84. 84الـ Memory والـ GC والـ Leaks: الـ process اللي بيكبر لحد ما يقعانت هنا
  85. 85الـ N+1 Queries والـ DataLoader: صفحة واحدة بمِيّتين query
  86. 86الـ HTTP Caching والـ Compression: أسرع request هو اللي ما بيوصلش
  87. 87الـ Load Testing والـ Soak Testing: الـ server بيستحمل قد إيه بجد
  88. 88الـ Concurrency Limits والـ Bulkheads: dependency بطيئة ما توقعش الـ app كله
  89. 89الـ Timeouts والـ Retries والـ Circuit Breakers: إزاي تتعامل مع dependency بطيئة أو واقعة
  90. 90OpenTelemetry والـ Distributed Tracing: الـ request راح فين وضاع وقته فين
  91. 91Docker Layers والـ Build Context: ليه الـ build بياخد دقيقة وفيه الـ .env بتاعك
  92. 92Multi-stage Builds و Non-root: image صغيرة ومن غير root
  93. 93Docker Compose والـ Networks: الـ services بتلاقي بعض إزاي
  94. 94Docker Volumes: الـ data بتتحفَظ فين
  95. 95Healthcheck و Restart و Resource Limits: running مش معناها شغّال
  96. 96BuildKit والـ Build Secrets: الـ token ما يدخلش الـ image
  97. 97Reverse Proxy و TLS: اللي واقف قدام الـ app
  98. 98Multi-arch Images والـ Native Modules: اشتغلت عندي
  99. 99CI/CD و Rollback و Canary: الـ deploy يبقى ممل
  100. 100Kubernetes من الأول: Pods و Deployments و Services و Probes
  101. 101Modular Monolith و DDD: حدود واضحة من غير network
  102. 102Transactional Outbox و Inbox: الـ event ما يضيعش وما يتنفذش مرتين
  103. 103Sagas: عملية واحدة على تلات services من غير transaction واحدة
  104. 104CQRS و Event Sourcing: الـ events هي الحقيقة
  105. 105الـ Queues والـ Brokers: BullMQ و RabbitMQ و Kafka و SQS
  106. 106Realtime على أكتر من instance: Redis pub/sub و presence و resume
  107. 107الـ Object Storage والـ Media: presigned URLs و uploads مباشرة
  108. 108التكامل مع الـ Email والـ Search: retries و webhooks و indexes
  109. 109GraphQL و gRPC في الإنتاج: contracts و DataLoader و limits و deadlines
  110. 110Kafka على أكتر من zone و region: الـ replication والـ ISR والـ acks والـ MirrorMaker والـ failover
  111. 111TypeScript و runtime boundaries: الـ types بتختفي، والـ schemas لأ
  112. 112ORM ولا SQL: query builders و repositories و migrations و transactions
  113. 113الـ Serverless من جُوّه: cold starts و freeze و timeouts و connections
  114. 114الـ Testing Pyramid: unit و integration و e2e على Postgres حقيقية
  115. 115Contract Testing و Property-Based Testing بـ fast-check
  116. 116الـ Payments: idempotency و webhooks و refunds و reconciliation
  117. 117الـ Timezones والـ Internationalization: UTC و DST والـ locales والـ plurals والفلوس
  118. 118الـ Privacy والـ Data Retention: consent و export و erasure و crypto shredding
  119. 119الـ ADRs والـ Feature Flags: القرارات المكتوبة والـ rollout من غير deploy
  120. 120Multi-region active-active: حل الـ conflicts بـ HLC و CRDTs
  121. 121Readiness و Liveness و Startup Probes: الـ pod جاهز ولا عايش؟
  122. 122Incident Runbooks: من الـ alert للـ postmortem