استریمهای Node.js: از مشکل حافظه تا مدلهای Push و Pull
خلاصهٔ کاملتر
بزرگترین مشکلی که استریمها حلش میکنن اینه: وقتی داده از حافظه بیشتره چی کار کنیم؟ رویکرد ساده اینه که کل فایل رو بخونیم، پردازش کنیم، بنویسیم. برای یه فایل کوچیک عالیه، ولی اگه کاربر یه ویدیو ۲ گیگابایتی آپلود کنه، برنامه باید ۲ گیگ رم اختصاص بده. ده تا کاربر همزمان؟ یعنی ۲۰ گیگ. این رویکرد مقیاسپذیر نیست.
راهحل پردازش چانکی (chunked processing) ـه: بهجای کل فایل، فقط ۶۴ کیلوبایت بخون، پردازش کن، بنویس، برو سراغ بعدی. اینطوری مصرف حافظه به اندازه چانک محدود میمونه نه اندازه فایل. یه فایل ۲ گیگ رو میشه با ۶۴ کیلوبایت حافظه پردازش کرد. ضمناً عملیاتها میتونن روی هم بیفتن: درحالیکه چانک N پردازش میشه، چانک N+1 از دیسک خونده میشه.
دو فلسفهی کاملاً متفاوت برای مدیریت جریان داده وجود داره. در مدل Push، تولیدکننده تصمیم میگیره کِی داده بفرسته و مصرفکننده فقط واکنش نشون میده. Node.js این مدل رو با EventEmitter پیاده میکنه. مثلاً یه استریم ساده میتونه اینطوری باشه:
class SimplePushStream extends EventEmitter {
_pushNext() {
if (this.index >= this.data.length) {
this.emit("end");
return;
}
const chunk = this.data[this.index++];
this.emit("data", chunk);
setImmediate(() => this._pushNext());
}
}استفاده از setImmediate اینجا مهمه: بهجای اینکه همه چانکها در یه حلقه تنگ فرستاده بشن، هر چانک توی یه تیک جداگانه از event loop ارسال میشه و کنترل به بقیه میرسه.
مشکل اصلی مدل Push backpressure ـه. اگه تولیدکننده سریعتر از مصرفکننده باشه، چانکها باید جایی بافر بشن. اگه مکانیزمی برای «کُند شو» وجود نداشته باشه، بافر بینهایت بزرگ میشه و حافظه تموم میشه. پیادهسازی backpressure در مدل خالص Push ساده نیست.
در مقابل، مدل Pull کنترل رو به مصرفکننده میده. مصرفکننده وقتی آمادهست داده میکشه. JavaScript این مدل رو با پروتکل Iterator فرمال کرده: یه شیء با متد next() که هر بار یه { value, done } برمیگردونه. Generator functionها (با function* و کلیدواژه yield) راهی مختصرتر برای پیادهسازی همین پروتکلن. چون generator ها Iterable هستن، میشه مستقیم توی حلقه for...of ازشون استفاده کرد.
برای دنیای واقعی که عملیاتها async هستن (خوندن فایل، درخواست شبکه)، JavaScript async iterator داره. یه async generator با async function* تعریف میشه و مصرفکننده با for await...of ازش میکشه. Node.js با ترکیب هر دو مدل Push و Pull، استریمهایی ساخته که هم رویدادمحور هستن هم backpressure رو بهدرستی مدیریت میکنن.
نکات کلیدی:
- استریمها داده رو چانکبهچانک پردازش میکنن تا مصرف حافظه کنترلشده بمونه
- Backpressure یعنی مصرفکننده به تولیدکننده بگه «کُند شو»؛ مدیریتش حیاتیه
- مدل Push بر پایه EventEmitter ـه: تولیدکننده داده رو emit میکنه
- مدل Pull بر پایه Iterator/Generator ـه: مصرفکننده با next() یا for...of داده میکشه
- Async iteratorها با async function* و for await...of برای منابع داده ناهمزمان استفاده میشن
- Node.js هر دو مدل رو ترکیب کرده تا بهترینهای هر دو دنیا رو داشته باشه




