این کتاب خلاصهٔ کامل و آموزشیِ «الگوهای طراحی Node.js» نوشتهٔ ماریو کاشارو و لوچیانو مامینو (ویرایش سوم) است؛ کتاب مرجع مهندسی پلتفرم Node.js. همهٔ مفاهیم از صفر و با تشبیههای ساده توضیح داده شدهاند: از معماری رویدادمحور و حلقهٔ رویداد تا سیستم ماژولها، کالبک و پرومیس، استریمها، الگوهای طراحی GoF، مقیاسپذیری و الگوهای پیامرسانی. هدف نهایی: اینکه مهارتهای عمیق Node.js را با تفکر Agentic Coding ترکیب کنی و زیرساخت ایجنتهای هوش مصنوعی بسازی.
فرض کن هیچچیز از Node.js نمیدانی. سادهترین تعریف: Node.js محیط اجرای جاوااسکریپت بیرون از مرورگر است. تا سال ۲۰۰۹ جاوااسکریپت فقط داخل مرورگر زندگی میکرد و کارش قشنگکردن صفحات وب بود. Ryan Dahl در ۲۰۰۹ موتور جاوااسکریپت سریع گوگل (V8) را برداشت، یک لایهٔ ورودی/خروجی فوقالعاده به آن چسباند (libuv) و به دنیا داد: حالا همان زبانی که فرانتاندکارها بلد بودند، میتوانست سرور بسازد، فایل بخواند، به دیتابیس وصل شود. تشبیه: اگر جاوااسکریپت یک زبان است، Node.js مثل «گذاشتن آن زبان روی چرخهای سنگین صنعتی» است — حالا میتواند کارهای واقعی دنیا (فایل، شبکه، پردازش) را انجام دهد.
اما چرا این کتاب مهم است و چرا «الگوی طراحی»؟ الگوی طراحی (Design Pattern) یعنی راهحل قابل استفادهٔ مجدد برای مسئلهٔ تکرارشونده — مثل دستور آشپزی که بارها امتحانش کردهای. Node.js یک ویژگی عجیب دارد که همهٔ معماریاش را شکل میدهد: تکرشتهای بودن (Single Thread). در زبانهای کلاسیک مثل Java، هر درخواست کاربر روی یک رشتهٔ جدا اجرا میشود؛ در Node.js فقط یک رشته هست و همه باید نوبت بگیرند. این محدودیت، دنیایی از الگوها ساخته: کالبک، پرومیس، async/await، استریم، صف، ... . اگر این الگوها را بلد نباشی، کدت یا قفل میکند یا زیر بار له میشود؛ اگر بلد باشی، Node.js یکی از کارآمدترین ابزارهای دنیاست — به همین دلیل تقریباً همهٔ ابزارهای دنیای ایجنتهای هوش مصنوعی (سرورهای MCP، دروازهها، CLIها) با آن ساخته شدهاند.
این کتاب (ویرایش سوم، ۶۶۱ صفحه، ۱۳ فصل) را میشود در سه پرده فهمید: پردهٔ اول — بنیانها (فصلهای ۱ تا ۶): چطور Node.js کار میکند، ماژولها، کالبک و رویدادها، کنترل جریان ناهمگام و استریمها. پردهٔ دوم — الگوها (فصلهای ۷ تا ۹): الگوهای ایجادی، ساختاری و رفتاری که هم GoF کلاسیک را به زبان JavaScript بازنویسی میکنند هم الگوهای بومی Node مثل Middleware را اضافه میکنند. پردهٔ سوم — مقیاس (فصلهای ۱۰ تا ۱۳): جاوااسکریپت یونیورسال، دستورالعملهای پیشرفته، مقیاسپذیری و پیامرسانی. در این خلاصه هر فصل را از صفر توضیح دادهام، با کد نمونهٔ واقعی کتاب و توضیح فارسی خطبهخط، و در انتها فصل ویژهٔ «اتصال به دنیای Agentic Coding» و واژهنامه را اضافه کردهام.
اگر مبتدی هستی: به ترتیب بخوان؛ فصلها روی هم سوارند. اگر Node.js بلدی و فقط الگوها را میخواهی: از فصل ۶ به بعد. اگر میخواهی سریع وارد دنیای ایجنتسازی شوی: بعد از فصلهای ۱ تا ۵، مستقیم برو سراغ فصل ویژهٔ پایانی، بعد به سند جفت این کتاب («هرمس ایجنت») برگرد. برای هر مفهوم سعی کردهام اول یک تشبیه دنیای واقعی بدهم، بعد جزئیات فنی — این الگوی آموزش را در کل کتاب حفظ کن.
1.1فلسفهٔ Node («راه Node»)
قبل از کد، کتاب چهار اصل فلسفی را معرفی میکند که همهٔ اکوسیستم با آنها نفس میکشد. ۱) هستهٔ کوچک (Small Core): هستهٔ Node عمداً حداقلی است — فقط اجزای I/O؛ بقیهچیز به «سرزمین کاربران» (userland یعنی دنیای بستههای npm) واگذار شده. چرا؟ چون هستهٔ کوچک آسان نگهداری میشود و اکوسیستم آزادانه آزمایش میکند. ۲) ماژولهای کوچک (Small Modules): نه فقط کوچک از نظر حجم، بلکه کوچک از نظر دامنه (Scope) — هر بسته فقط «یک کار» را خوب بکند (اصل یونیکسی «هر برنامه فقط یک کار را خوب انجام بده»). مدیریت بسته (npm و yarn) این را ممکن کرده: پروژهای با صدها وابستگی ریز عادی است. این همان اصل DRY (تکرار نکن) در سطح جدیدی است. ۳) سطح API کوچک (Small Surface Area): هر ماژول فقط یک ورودی روشن بدهد و جزئیات درونیاش را لو ندهد؛ ماژولها «برای استفاده» ساخته میشوند نه «برای گسترش». ۴) سادگی و عملگرایی (KISS و worse is better): طراحی ساده که امروز کار کند، از طراحی «کامل» که هیچوقت آماده نمیشود بهتر است.
1.2چرا Node اصلاً اینطوری طراحی شد؟ داستان I/O
قلب معماری Node پاسخ به یک مسئله است: I/O کند است. حافظهٔ RAM در حد نانوثانیه جواب میدهد، اما دیسک و شبکه در حد میلیثانیهاند — یعنی ده هزار تا صد هزار برابر کندتر. حالا دو راهحل تصور کن. I/O مسدودکننده (Blocking I/O): برنامه تا رسیدن جوابِ دیسک/شبکه، خوابیدن میرود؛ رشته میایستد و هیچکار دیگری نمیکند. سرورهای کلاسیک برای هر اتصال یک رشتهٔ جدا میسازند که بیشتر عمرش را در خواب است — مثل رستورانی که بهازای هر مشتری یک گارسون استخدام میکند که فقط کنار میز آن مشتری بایستد!
// مسدودکننده: توقف ترد تا رسیدن داده data = socket.read() // خط تا اینجا نمیرود جلو! print(data)
I/O غیرمسدودکننده (Non-Blocking I/O): در سیستمعامل، منابع را میشود «غیرمسدودکننده» علامتگذاری کرد؛ خواندن فوراً برمیگردد و اگر دادهای آماده نباشد جواب «الان ندارم» (EAGAIN) میدهد. سادهترین استفاده: حلقهای که مدام همهٔ منابع را چک میکند — به آن busy-waiting میگویند: با یک رشته چند منابع را مدیریت میکنی اما CPU را میسوزانی، مثل مرغابیای که مدام همهٔ میزها را چک کند.
resources = [socketA, socketB, fileA]
while (!resources.isEmpty()) {
for (resource of resources) {
data = resource.read()
if (data === NO_DATA_AVAILABLE) continue // هنوز آماده نیست
if (data === RESOURCE_CLOSED) resources.remove(i)
else consumeData(data)
}
}
راهحل هوشمند — دمولتیپلکسر رویداد (Event Demultiplexer): سیستمعاملها APIهایی دارند (در لینوکس epoll، در macOS kqueue، در ویندوز IOCP) که میگویند: «لیستی از منابع را بده، فقط وقتی حداقل یکی آماده شد بیدارت کن و فقط آنهای آماده را برگردان». اسمش از مخابرات آمده (دمولتیپلکس = جدا کردن سیگنالهای ترکیبشده). حالا گارسونِ رستوران فقط یک نفر است که تا صدای زنگ یکی از میزها صبر میکند!
watchedList.add(socketA, FOR_READ) // (۱) ثبت منابع
watchedList.add(fileB, FOR_READ)
while (events = demultiplexer.watch(watchedList)) { // (۲) بیدارشدن فقط با رویداد
for (event of events) { // (۳) فقط منابعِ آماده
data = event.resource.read() // این read هرگز مسدود نمیشود
if (data === RESOURCE_CLOSED) demultiplexer.unwatch(event.resource)
else consumeData(data)
}
}
(۱) هر منبع را با نوع عملیات موردعلاقه ثبت میکنیم. (۲) watch() تا آمدن رویداد «میخوابد» — یک فراخوان مسدودکننده بهجای هزاران پرسوجو. (۳) چون فقط منابع آماده برگشتهاند، خواندن تضمینی بدون توقف است. وقتی رویدادها تمام شد، کنترل دوباره در watch() میخوابد. همین چرخه «حلقهٔ رویداد» (Event Loop) است — و مجموعهٔ این منطق (ثبت درخواست، صف رویداد، فراخوانی هندلر) یک الگوی نامدار است: الگوی راکتور (Reactor Pattern) — «تا رسیدن رویداد از منابعِ تحتنظارت بلاک کن، بعد هر رویداد را به هندلرش بفرست».
| جزء | نقش |
|---|---|
| libuv | موتور I/O: پیادهسازی راکتور، حلقهٔ رویداد، صف رویداد؛ و انتزاع دمولتیپلکسرهای سیستمعاملهای مختلف (همین که کدت هم در لینوکس هم ویندوز کار کند) |
| Bindings | پلهای C/C++ که libuv را برای جاوااسکریپت قابل صدا زدن میکنند |
| V8 | موتور جاوااسکریپت گوگل که کد را کامپایل و اجرا میکند |
| Core JS Library | کتابخانهٔ سطحبالا: fs، http، crypto و... |
نکتهٔ ظریف: فایلهای معمولی یونیکس اصلاً حالت non-blocking ندارند؛ libuv با رشتههای پشتیبان این را شبیهسازی میکند. و یک نکتهٔ کاربردی که خیلیها نمیدانند: برنامهٔ Node وقتی خارج میشود که دمولتیپلکسر عملیات معلقی نداشته باشد و صف خالی باشد — این توضیح میدهد چرا اسکریپتت «آویزان میماند» (مثلاً یک سرور را روشن کردهای که هیچوقت کارش تمام نمیشود) و چرا کدی که همهٔ کارهایش async است «ناگهان خاتمه پیدا میکند». در Node برخلاف مرورگر: window و DOM وجود ندارند اما process داری (process.env برای متغیرهای محیطی و process.argv برای آرگومانهای خط فرمان)، و به همهٔ سرویسهای سیستمعامل از طریق ماژولهای هسته دسترسی داری. برای کارهای سنگین CPU هم دو درِ فرار هست: N-API برای کد بومی C/C++ و WebAssembly.
2.1چرا ماژول لازم داریم و CommonJS چطور کار میکند
ماژول (Module) واحد سازماندهی نرمافزار است: تقسیم کد به فایلها، مخفیسازی اطلاعات (Encapsulation)، استفادهٔ مجدد و مدیریت وابستگیها. جاوااسکریپت تا ES2015 اصلاً سیستم ماژول رسمی نداشت! (در مرورگر تگ <script> و بعد کتابخانههایی مثل RequireJS). Node از روز اول با CommonJS شروع کرد و بعدها ESM (ECMAScript Modules) استاندارد رسمی زبان اضافه شد. نکتهٔ جذاب فصل: نویسندهها یک require خانگی میسازند تا ثابت کنند هیچ جادویی در کار نیست:
function loadModule (filename, module, require) {
const wrappedSrc =
`(function (module, exports, require) {
${fs.readFileSync(filename, 'utf8')}
})(module, module.exports, require)`
eval(wrappedSrc) // فقط برای آموزش — هرگز در کد واقعی!
}
function require (moduleName) {
const id = require.resolve(moduleName) // (۱) اسم ← مسیر یکتا
if (require.cache[id]) { // (۲) کش؟ همان خروجی قبلی
return require.cache[id].exports
}
const module = { exports: {}, id } // (۳) ماژول تازه
require.cache[id] = module // (۴) قبل از لود، در کش بنشین!
loadModule(id, module, require) // (۵) بخوان، پیچ کن، اجرا کن
return module.exports // (۶) خروجی را بده
}
require.cache = {}
سه راز CommonJS همینجا لو میرود: (۱) هر فایل در واقع درون یک تابع پیچیده میشود که module، exports و require را تزریق میکند — به همین دلیل متغیرهای هر فایل خصوصیاند. (۲) require همگام (sync) است — به همین دلیل هسته، نسخههای همگام API هم میدهد و نمیتوانی module.exports را داخل setTimeout تنظیم کنی! (۳) هر ماژول فقط یکبار ارزیابی میشود و بعد از کش سرو میشود — «Singleton». الگوریتم resolve: مسیر مطلق/نسبی ← ماژول هسته ← بستهها در نزدیکترین node_modules با بالا رفتن تا ریشه (امتحانِ name.js، name/index.js و main در package.json). همین تو در تو بودن node_modules است که «جهنم وابستگی» (تعارض نسخهها) را حل میکند: هر بسته میتواند نسخهٔ خصوصی خودش را داشته باشد.
exports فقط «مستعارِ ارجاعی» از module.exports است در شروع. اضافهکردن ویژگی روی exports کار میکند؛ اما بازنویسی exports هیچ کاری نمیکند — فقط متغیر محلی را عوض کردهای!
exports.hello = () => console.log('Hello') // درست: روی شیء مشترک مینشیند
exports = () => console.log('Hello') // غلط! فقط متغیر محلی عوض شد
module.exports = () => console.log('Hello') // درست: خروجی واقعی را عوض کن
یک پیشتاریخ جالب هم هست: قبل از ماژولها، الگوی Revealing Module با IIFE حیلهٔ خصوصیسازی را انجام میداد — یک تابع فوراً-اجرا-شونده scope خصوصی میسازد و فقط API عمومی را برمیگرداند؛ پدر معنوی ماژولها. الگوهای تعریف ماژول در CommonJS: خروجیهای نامدار (اتصال به exports)، خروجی یک تابع (الگوی مشهور «substack»: ماژول = یک تابع اصلی + فضای نام اختیاری؛ مصداق تکوظیفگی)، خروجی کلاس، خروجی نمونهٔ از پیش ساخته (singleton مانند — اما نه تضمینی! اگر دو نسخه از همان بسته نصب شود، دو نمونه داری) و monkey patching (دستکاری ماژولهای دیگر در زمان اجرا — غیرقابل پیشبینی، جز در تست مثل nock).
2.2ESM: ماژول استاندارد زبان
ESM ایستاست (Static): دستورهای import باید در سطح بالا باشند، خارج از if و حلقه، با شناسهٔ رشتهای ثابت. چرا این سختگیری؟ چون موتور قبل از اجرای حتی یک خط کد، کل گراف وابستگی را میشناسد ← تحلیل ایستا، tree shaking (حذف کد مرده) و خطای زودهنگام. سه فاز لود کتاب با تشبیه قشنگی میگوید: «فاز ۱ همهٔ نقطهها را پیدا میکند؛ فاز ۲ نقطهها را به هم وصل میکند تا مسیر ساخته شود؛ فاز ۳ مسیرها را به ترتیب درست قدم میزند» — یعنی ساخت (Construction) ← نصب (Instantiation) ← ارزیابی (Evaluation) که از برگها به سمت نقطهٔ ورود اجرا میشود. برای ورود به ESM: پسوند .mjs یا "type": "module" در package.json. خروجیها: نامدار (export function...) — توصیهشده چون IDE و tree-shaking دوستش دارند؛ پیشفرض (export default) — فقط برای «یک قابلیت اصلی»؛ و ترکیبی. و import() پویا که در زمان اجرا با مسیر ساختهشده کار میکند و Promise برمیگرداند (برای لود تنبل و ماژولهای وابسته به زبان/پلتفرم).
مهمترین تفاوت فنی با CommonJS: اتصالهای زندهٔ فقط-خواندنی (Read-only Live Bindings):
// counter.js
export let count = 0
export function increment () { count++ }
// main.js
import { count, increment } from './counter.js'
console.log(count) // 0
increment()
console.log(count) // 1 ← تغییر ماژول مبدأ دیده میشود!
count++ // TypeError: تغییر از سمت مصرفکننده ممنوع!
در CommonJS یک کپی سطحی میگرفتی که در لحظهٔ require فریز میشد؛ در ESM مصرفکننده «لینک زنده» به متغیر مبدأ دارد ولی نمیتواند بنویسد. همین مکانیزم + سه فاز استاتیک، باعث میشود وابستگیهای چرخهای (Circular Dependencies) در ESM تمیز حل شوند: ماژولهای متقابلاً وابسته تصویر کامل و بهروز از هم دارند؛ در CommonJS هرکس زودتر require کند، تصویر ناقص و کهنه از دیگری میگیرد (کد a.js و b.js کتاب را ببین: داخل b، ماژول a بهصورت { "loaded": false } ظاهر میشود!). دیگر نکتههای ESM: درون ESM همیشه strict mode هست؛ require، __filename و __dirname وجود ندارند (بهجایشان import.meta.url + fileURLToPath و createRequire)؛ پسوندها اجباریاند؛ و قانون بینعملیاتی: ESM میتواند فقط default خروجی CJS را ایمپورت کند، اما CJS اصلاً نمیتواند ESM را ایمپورت کند.
3.1الگوی کالبک و سبک CPS
کالبک (Callback) تابعی است که به تابع دیگری میدهی تا «وقتی کار تمام شد» با نتیجه صدا زده شود. چرا در JavaScript طبیعی است؟ چون توابع «شهروند درجهیک»ند (مثل هر مقدار قابل پاسدادن) و کلوژر (Closure) زمینهٔ ساخت کالبک را حفظ میکند. حالا اصطلاح دقیقتر: CPS (Continuation-Passig Style) یعنی بهجای return کردن، نتیجه را به «ادامه» (همان تابعی که دادهای) تحویل بده. دقت کن CPS لزوماً ناهمگام نیست:
// CPS همگام: کالبک قبل از تمامشدن تابع صدا زده میشود
function addCps (a, b, callback) { callback(a + b) }
console.log('before')
addCps(1, 2, result => console.log(`Result: ${result}`))
console.log('after')
// خروجی: before → Result: 3 → after
// CPS ناهمگام: تابع فوراً برمیگردد؛ کالبک بعداً از حلقهٔ رویداد اجرا میشود
function additionAsync (a, b, callback) {
setTimeout(() => callback(a + b), 100)
}
console.log('before')
additionAsync(1, 2, result => console.log(`Result: ${result}`))
console.log('after')
// خروجی: before → after → Result: 3
تفاوت فقط «ترتیب چاپ» نیست؛ در حالت ناهمگام، کنترل فوراً به حلقهٔ رویداد برمیگردد و اجرای کالبک بعداً «با پشتهٔ تازه» از حلقه ادامه پیدا میکند. این یعنی: خطای پرتابشده در یک کالبک ناهمگام هرگز توسط try/catch فراخواننده گرفته نمیشود (پشتهٔ متفاوت!) — یکی از مهمترین درسهای کل کتاب. یادت باشد هر کالبکی یعنی CPS نیست: کالبکِ Array.map فقط «مهمان هر عنصر» است، نه ادامهٔ نتیجه.
3.2آزادسازی Zalgo: تلهٔ مرگبار همگام/ناهمگام
معروفترین درس فصل. فرض کن تابعی مینویسی که گاهی همگام رفتار میکند و گاهی ناهمگام — مثلاً اگر جواب در کش باشد همانجا کالبک را صدا میزند، وگرنه ناهمگام میخواند:
import { readFile } from 'fs'
const cache = new Map()
function inconsistentRead (filename, cb) {
if (cache.has(filename)) {
cb(cache.get(filename)) // ← کشخورده: همگام!
} else {
readFile(filename, 'utf8', (err, data) => {
cache.set(filename, data)
cb(data) // ← کشنخورده: ناهمگام
})
}
}
مشکل کجاست؟ مصرفکنندهای که «بعد از فراخوان» شنونده ثبت میکند، در حالت کشخورده برای همیشه رویداد را از دست میدهد — چون کالبک قبل از ثبتش اجرا شده. همان کد بسته به وضعیت کش، بیصدا درست یا غلط کار میکند؛ چنین باگی در تولید تقریباً بازتولیدناپذیر است. سازندهٔ npm، Isaac Schlueter، اسم این وضعیت را گذاشت آزادسازی Zalgo (موجود آشوب در افسانههای اینترنتی): «یک API هرگز نمیتواند همگام و ناهمگام باشد».
// درمان: تضمین ناهمگام بودن همیشگی با process.nextTick
function consistentReadAsync (filename, callback) {
if (cache.has(filename)) {
process.nextTick(() => callback(cache.get(filename))) // ← به دورهٔ بعد
} else {
readFile(filename, 'utf8', (err, data) => {
cache.set(filename, data)
callback(data)
})
}
}
سه ابزار بهتعویق را از هم جدا کن: process.nextTick() یک میکروتسک است — «درست بعد از همین عملیات، قبل از هر I/O معلق» (برای تضمین ناهمگامی عالی؛ اما فراخوانی بازگشتیاش باعث گرسنگی I/O میشود)؛ setImmediate() بعد از رویدادهای I/O اجرا میشود (ایمن برای ریهکردن حلقه)؛ setTimeout(cb, 0) حداقل ۱ میلیثانیه صبر میکند و معمولاً بعد از setImmediate میرسد.
3.3قراردادهای کالبک Node و مدیریت خطا
قراردادهایی که کل اکوسیستم رویشان بنا شده: (۱) کالبک همیشه آخرین پارامتر است. (۲) خطا همیشه اولین پارامتر کالبک است: (err, data...) — در موفقیت err باید null/undefined باشد و همیشه نمونهٔ واقعی Error (هرگز رشته یا عدد!). (۳) خطا را همیشه چک کن. (۴) خطا را «منتقل» کن، نه پرتاب: در CPS بهجای throw، خطا به کالبک بعدی میرود:
import { readFile } from 'fs'
function readJSON (filename, callback) {
readFile(filename, 'utf8', (err, data) => {
let parsed
if (err) {
return callback(err) // (۱) خطای I/O را منتقل کن و خارج شو
}
try {
parsed = JSON.parse(data) // (۲) کد همگام: این یکی throw میکند
} catch (err) {
return callback(err) // (۳) پس بگیر و به کالبک تبدیلش کن
}
callback(null, parsed) // (۴) موفقیت: خطا = null اول
})
}
(۱) الگوی return callback(err) دو کار میکند: انتقال خطا + خروج فوری (فراموشکردن return یعنی ادامهٔ اجرا و ممکن بودن دوبار صدا زدن کالبک — منبع کلاسیک باگ). (۲) و (۳) کد همگام درون کالبک هنوز میتواند throw کند، پس try/catch لازم است و خطایش باید به CPS ترجمه شود. (۴) صدا زدن کالبک «بیرون» از try است تا خطای خودش بلعیده نشود. و اگر خطایی واقعاً از یک کالبک ناهمگام فرار کند؟ به حلقهٔ رویداد میرسد، فرایند با کد غیرصفر میمیرد و رویداد uncaughtException فقط برای «تمیزکاری نهایی» است چون وضعیت ناسازگار شده — فلسفهٔ توصیهشده fail-fast: تمیز کن، process.exit(1)، و ناظر (supervisor) دوباره بالا بیاورد.
3.4الگوی Observer و EventEmitter
کالبک برای «یک نتیجه، یکبار» است؛ اما برای «مطلعشدن از اتفاقهای ممکنِ مکرر» به الگوی Observer میرسیم: یک «سوژه» فهرستی از «ناظرها» دارد و هر تغییری را به همه اطلاع میدهد. پیادهسازی بومی Node: EventEmitter از ماژول events با متدهای on، once (خودش بعد از اولین بار حذف میشود)، emit و removeListener — همه زنجیرهپذیر. مثال کتاب:
import { EventEmitter } from 'events'
import { readFile } from 'fs'
function findRegex (files, regex) {
const emitter = new EventEmitter()
for (const file of files) {
readFile(file, 'utf8', (err, content) => {
if (err) return emitter.emit('error', err) // قرارداد رویداد خطا
emitter.emit('fileread', file)
const match = content.match(regex)
if (match) match.forEach(elem => emitter.emit('found', file, elem))
})
}
return emitter
}
findRegex(['fileA.txt', 'fileB.json'], /hello \w+/g)
.on('fileread', file => console.log(`${file} was read`))
.on('found', (file, match) => console.log(`Matched "${match}" in ${file}`))
.on('error', err => console.error(`Error emitted ${err.message}`))
تابع بهجای «پردازش کامل و برگرداندن آرایه»، فوراً یک emitter برمیگرداند و حین کار رویداد میپخش: fileread برای هر فایل، found برای هر تطبیق (صفر تا N بار!) و error. دو قاعدهٔ حیاتی: رویداد ویژهٔ error — هرگز نمیتوانی از emitter خطا پرتاب کنی؛ قرارداد این است رویداد 'error' با نمونهٔ Error منتشر شود، و اگر این رویداد بدون شنونده منتشر شود، EventEmitter همان را throw میکند و برنامه میمیرد — همیشه شنوندهٔ error ثبت کن. و نشتی حافظه از شنوندهها: شنوندهای که حذف نشود، از طریق کلوژرش متغیرها را زنده نگه میدارد — «منبع اصلی نشتی حافظه در Node.js». هشدار عبور از ۱۰ شنونده برای یک رویداد فعال میشود (setMaxListeners). قاعدهٔ انتخاب هم جذاب است: کالبک یعنی «نتیجهای که حتماً یکبار میآید»؛ رویداد یعنی «اتفاقی که ممکن است صفر بار، یک بار یا بارها بیفتد» — و مثل glob میشود هر دو را ترکیب کرد (کالبک برای نتیجهٔ نهایی + emitter برای پیشرفت زنده). و همان قانون Zalgo اینجا هم برقرار است: انتشار همگام و ناهمگامِ یک نوع رویداد را هرگز قاطی نکن.
4.1جهنم کالبک و سه قانون انضباط
نویسندهها کل فصل را با یک مثال واحد پیش میبرند: عنکبوت وب (Web Spider) — برنامهای که یک URL را دانلود و ذخیره میکند و بعد بهصورت بازگشتی لینکهایش را هم دانلود میکند. نسخهٔ اول با کالبکهای تو در تو نوشته میشود و شکل معروف هرم مصائب (Pyramid of Doom) پیدا میکند:
asyncFoo(err => {
asyncBar(err => {
asyncFooBar(err => {
//... هر لایه یک تورفتگی جدید
})
})
})
مسئله فقط زشتی نیست: خوانایی و نگهداری مرده، اسمهای err/err1/error قاطی، و هر کلوژر حافظه نگه میدارد. جالب اینکه کتاب ادعا میکند مشکل عمدتاً «انضباطی» است نه ساختاری، و سه قانون میدهد: (۱) زود خارج شو — اصل بازگشت زودهنگام: if (err) return cb(err) بهجای تو در توی if/else؛ (۲) کالبکها را به توابع نامدار تبدیل کن و نتیجههای میانی را بهعنوان پارامتر پاس بده (پشتهٔ خطاها هم بهتر میشود)؛ (۳) ماژولار کن — توابع کوچکِ قابل استفادهٔ مجدد. با همین سه قانون، بدون هیچ کتابخانهای نسخهٔ تمیز spider ساخته میشود:
export function spider (url, cb) {
const filename = urlToFilename(url)
fs.access(filename, err => {
if (!err || err.code !== 'ENOENT') { // (۱) شرط وارونه: قبلاً دانلود شده
return cb(null, filename, false)
}
download(url, filename, err => { // (۲) مسیر موفق در سطح بالا
if (err) return cb(err)
cb(null, filename, true)
})
})
}
4.2سه الگوی اجرا: ترتیبی، موازی، موازیمحدود
اجرای ترتیبی (Sequential) وقتی لازم است که نتیجهٔ هر گام، گام بعد را عوض کند — در CPS «دلیل اصلی جهنم کالبک» همین است. برای مجموعهٔ معلوم از وظایف: هر تابع، بعدی را در کالبکش صدا میزند. برای پیمایش یک مجموعه، الگوی عمومی Sequential Iterator:
function spiderLinks (currentUrl, body, nesting, cb) {
if (nesting === 0) return process.nextTick(cb) // همیشه ناهمگام (یادت هست؟)
const links = getPageLinks(currentUrl, body)
if (links.length === 0) return process.nextTick(cb)
function iterate (index) { // (۱) تکرارکنندهٔ محلی
if (index === links.length) return cb() // (۲) پایان
spider(links[index], nesting - 1, function (err) {
if (err) return cb(err)
iterate(index + 1) // (۳) بعدی، در کالبک قبلی
})
}
iterate(0) // (۴) شروع
}
اجرای موازی (Unlimited Parallel) برای وقتی که وظایف مستقلاند: همه را بزن و منتظر همه بمان. چون Node تکرشتهای است، اسم دقیقش همزمانی (Concurrency) است — وظایف با I/O غیرمسدودکننده درهمتنیده میشوند. چون اینجا Promise.all نداریم، با شمارنده جمع میبندیم:
let completed = 0
let hasErrors = false
function done (err) {
if (err) { hasErrors = true; return cb(err) }
if (++completed === links.length && !hasErrors) return cb()
}
links.forEach(link => spider(link, nesting - 1, done))
پرچم hasErrors تضمین میکند کالبک نهایی هرگز دوبار صدا زده نشود. اما موازیسازی بیحد دو مشکل میسازد: شرایط رقابت (Race Condition) — دو وظیفه همزمان چک میکنند «فایل هست؟»، هر دو «نه» میبینند و هر دو همان صفحه را دانلود میکنند (در Node تکرشتهای هم رقابت واقعی است چون خطر در «فاصلهٔ شروع عملیات ناهمگام تا رسیدن نتیجه» است!) — درمان ساده: انحصار متقابل با یک Set از URLهای در جریان؛ و اتلاف منابع — هزاران دانلود همزمان یعنی تمامشدن دیسکریپتورها، رد اتصال (ECONNREFUSED) و حتی ظرفیت حملهٔ DoS. راهحل: اجرای موازی محدود و برای «محدودیت سراسری» (نه بهازای هر صفحه!) یک صف مرکزی:
import { EventEmitter } from 'events'
export class TaskQueue extends EventEmitter {
constructor (concurrency) {
super()
this.concurrency = concurrency
this.running = 0
this.queue = []
}
pushTask (task) {
this.queue.push(task)
process.nextTick(this.next.bind(this))
return this
}
next () {
while (this.running < this.concurrency && this.queue.length) {
const task = this.queue.shift()
task(() => {
this.running--
process.nextTick(this.next.bind(this))
})
this.running++
}
}
}
این TaskQueue قلب فصل است: هر وقت ظرفیت آزاد شد، وظیفهٔ بعدی را میکشد؛ کتاب بعداً از EventEmitter ارث میبرد تا 'error' (شکست وظیفه — صف میایستد نه میمیرد) و 'empty' (صف خالی شد) منتشر کند. درس فرهنگی: سیستمهای صفمحور انتظار خطا را دارند و راهبردشان «تلاش مجدد/بازیابی» است نه مرگ. در دنیای واقعی هم کتابخانهٔ معروف async همین الگوها را بستهبندی کرده (series/parallel/waterfall/queue).
5.1پرومیس چیست و چرا پادزهر کالبک است
Promise (پرومیس) شیئی است که «وضعیت و نتیجهٔ نهایی» یک عملیات ناهمگام را حمل میکند — مثل رسید پستی که دستت است و موقع باز کردنش محتوا را میبینی. سه حالت دارد: pending (در جریان)، fulfilled (موفق) و rejected (خطا)؛ بعد از نشست (settled) دیگر تغییر نمیکند و فقط یکبار مینشیند. سه ویژگی جادویی دارد: (۱) هندلرهای then «تضمینی ناهمگام و حداکثر یکبار» اجرا میشوند — حتی اگر پرومیس همگام resolve شده باشد؛ یعنی پرومیس پادزهر ساختاری Zalgo است. (۲) هر then یک پرومیس جدید برمیگرداند ← زنجیره: اگر هندلر مقدار برگرداند پرومیس بعدی با همان مقدار پُر میشود؛ اگر پرومیس برگرداند، سرنوشتش را میپذیرد؛ اگر throw کند، پرومیس بعدی reject میشود — «بالاخره throw قابل استفاده شد». (۳) خطاها به پایینترین catch سرریز میشوند بدون هیچ boilerplate انتقال. هر شیء دارای then «thenable» است و با تایپگذاریِ اردکی (Duck Typing) با پرومیسهای هر کتابخانهای ترکیب میشود (استاندارد Promises/A+).
// تبدیل API کالبکی به پرومیس (promisify) — نسخهٔ آموزشی
function promisify (callbackBasedApi) {
return function promisified (...args) {
return new Promise((resolve, reject) => { // (۱)
const newArgs = [
...args,
function (err, result) { // (۲) کالبک اضافهٔ خودمان
if (err) return reject(err)
resolve(result)
}
]
callbackBasedApi(...newArgs) // (۳) فراخوانی اصلی
})
}
}
(۱) پرومیس میسازیم و برمیگردانیم؛ (۲) یک کالبک Node-سبک آخر آرگومانها میچسبانیم که خطا را به reject و نتیجه را به resolve ترجمه میکند؛ (۳) تابع اصلی را صدا میزنیم. در دنیای واقعی از util.promisify() یا fs.promises استفاده کن. پرومیسهای استاتیک هم بلد باش: Promise.all (همه موفق ← آرایه؛ اولین شکست ← fail-fast)، Promise.allSettled (همیشه منتظر همه، نتیجهٔ تکتک را میدهد)، Promise.race (اولین نشسته برنده است)، catch و finally.
5.2همان سه الگوی فصل ۴، اینبار با پرومیس
// ترتیبی: زنجیره را در حلقه بساز
function spiderLinks (currentUrl, content, nesting) {
let promise = Promise.resolve() // دانهٔ خالی
if (nesting === 0) return promise
const links = getPageLinks(currentUrl, content)
for (const link of links) {
promise = promise.then(() => spider(link, nesting - 1))
}
return promise // آخرین حلقه همه را کامل میکند
}
// موازی: همه را شروع کن، با all جمع کن
function spiderLinksParallel (currentUrl, content, nesting) {
if (nesting === 0) return Promise.resolve()
const links = getPageLinks(currentUrl, content)
const promises = links.map(link => spider(link, nesting - 1))
return Promise.all(promises)
}
تفاوت ترتیبی و موازی فقط در «منتظر ماندن قبل از شروع بعدی» است. حالا نسخهٔ نهایی و توصیهشدهٔ زبان — async/await: تابع async اجازهٔ await میدهد («مکث» تا پرومیس بنشیند و ارزشش را بدهد)، همیشه پرومیس برمیگرداند و try/catch بالاخره یکدست کار میکند — هم throw همگام و هم reject پرومیس را میگیرد:
async function download (url, filename) {
console.log(`Downloading ${url}`)
const { text: content } = await superagent.get(url) // دانلود
await mkdirpPromises(dirname(filename)) // ساخت پوشه
await fsPromises.writeFile(filename, content) // ذخیره
console.log(`Downloaded and saved: ${url}`)
return content
}
async function spiderLinks (currentUrl, content, nesting) {
if (nesting === 0) return
const links = getPageLinks(currentUrl, content)
for (const link of links) {
await spider(link, nesting - 1) // ترتیبی: فقط حلقهٔ ساده با await
}
}
نسخهٔ ۱۹ خطی کالبکیِ download اینجا ۷ خط صاف شد — «واقعاً الگویی برای یادگیری نمانده». اما چند تلهٔ مهم: تلهٔ return در برابر return await: داخل try، اگر بنویسی return promise، پرومیس مستقیم به فراخواننده میرود و catch محلی هرگز اجرا نمیشود؛ بنویس return await promise تا خطا در همین تابع هندل شود. ضدالگوی forEach: links.forEach(async link => await ...) نه ترتیبی است نه انتظار — پرومیسهای برگشتی نادیده گرفته میشوند؛ ترتیبی واقعی یعنی for...of با await. نشتی حافظهٔ زنجیرهٔ بیپایان: تابع بازگشتی که پرومیسِ فراخوانی بعدیاش را return میکند (return leakingLoop() داخل then خودش) زنجیرهای از پرومیسهای هرگز-نمینشینند میسازد که هر حلقه حافظه اضافه میکند — این نقصِ خودِ مشخصهٔ Promises/A+ است! درمانها: return را حذف کن (اما خطای عمق را گم میکنی)، بازگشت را با سازندهٔ Promise و reject داخلی بپیچ، یا تبدیل به async function با حلقهٔ while(true) کن. کتاب یک الگوی پیشرفته هم دارد: Producer–Consumer (TaskQueuePC) — بهجای شمارنده، تعداد ثابت «مصرفکننده» با await getNextTask() حلقهٔ بیپایان میزنند؛ صف خالی؟ resolve آنها «پارک» میشود (مصرفکننده میخوابد) و وظیفهٔ جدید بیدارش میکند — تعداد مصرفکنندهها = سقف همزمانی. در تولید از p-limit یا p-map استفاده کن.
6.1چرا استریم؟ بهینهسازی فضا، زمان و ترکیبپذیری
دو رویکرد برای کار با دادههای بزرگ: بافرینگ (Buffering) — کل داده را در حافظه جمع کن، بعد یکجا پردازش کن؛ استریمینگ (Streaming) — هر تکه (chunk) به محض رسیدن پردازش شود. Dominic Tarr استریم را «بهترین و بدفهمیدهشدهترین ایدهٔ Node» نامیده و شعار جامعه این است: «stream all the things!». سه برتری دارد: ۱) بهینگی فضایی — حافظه ثابت میماند حتی برای فایلهای چند گیگابایتی (سقف بافر V8 حدود ۲ گیگابایت است؛ نسخهٔ بافرینگ کتاب روی فایل ۸ گیگابایتی با ERR_FS_FILE_TOO_LARGE میمیرد). ۲) بهینگی زمانی — در مثال ۶ مرحلهای کتاب (فشردهسازی ← ارسال ← دریافت ← بازگشایی ← ذخیره)، نسخهٔ بافرینگ مراحل را «یکی پس از دیگری» کامل میکند؛ استریم هر تکه را مثل خط تولید (Assembly Line) میفرستد: مرحلهٔ n روی تکهٔ فعلی کار میکند در حالی که مرحلهٔ قبلی مشغول تکهٔ بعدی است. ۳) ترکیبپذیری (Composability) — چون همهٔ استریمها رابط یکسان دارند، اتصالشان مثل لگو است: میشود وسط خط لولهٔ gzip، یک لایهٔ رمزنگاری Transform گذاشت و هیچ چیز دیگری عوض نشود.
// کل فلسفهٔ استریم در ۵ خط: فشردهسازی فایل با حافظهٔ ثابت
import { createReadStream, createWriteStream } from 'fs'
import { createGzip } from 'zlib'
const filename = process.argv[2]
createReadStream(filename) // منبع: خواندن تکهتکه
.pipe(createGzip()) // تبدیل: فشردهسازی هر تکه
.pipe(createWriteStream(`${filename}.gz`)) // مقصد: نوشتن
.on('finish', () => console.log('File successfully compressed'))
6.2چهار نوع استریم و حالتهای خواندن/نوشتن
چهار کلاس پایه: Readable (منبع داده)، Writable (مقصد)، Duplex (هر دو، مستقل — مثل سوکت TCP) و Transform (Duplexی که خروجیاش از ورودی محاسبه میشود — مثل gzip). نوع پنجم PassThrough («بیاثر») است که زیرestimateشدهترین است: برای اتصالِ بعداً (late piping)، مشاهدهگری و استریم تنبل. همهٔ استریمها EventEmitter هم هستند (رویدادهای end، finish، error). استریمها دو مود دارند: binary (بافر/رشته) و object (objectMode: true — هر مقدار JS؛ همین است که استریم را از I/O به «پارادایم عمومی پردازش داده» تبدیل میکند). برای مصرف Readable دو راه: حالت غیرجاری (paused) پیشفرض — رویداد readable و حلقهٔ کشیدن با read() تا null؛ حالت جاری (flowing) — شنوندهٔ data بگذار تا تکهها «به سمتت پرتاب شوند» (pause/resume کنترلش میکند). و راه مدرن: iterator ناهمگام — for await (const chunk of process.stdin).
// پیادهسازی Readable سفارشی: فقط _read را پیاده کن
import { Readable } from 'stream'
import Chance from 'chance'
const chance = new Chance()
export class RandomStream extends Readable {
constructor (options) { super(options); this.emittedBytes = 0 }
_read (size) {
const chunk = chance.string({ length: size }) // (۱) تولید داده
this.push(chunk, 'utf8') // (۲) به بافر داخلی
this.emittedBytes += chunk.length
if (chance.bool({ likelihood: 5 })) {
this.push(null) // (۳) null = پایان استریم
}
}
}
قرارداد نامگذاری را همیشه رعایت کن: متدهای read/write برای «مصرفکننده» و متدهای زیرخطدار _read/_write/_transform برای «پیادهکننده» — هرگز زیرخطدارها را مستقیم صدا نزن. و شکل سادهشدهٔ سازنده: new Readable({ read () {...} }) بدون تعریف کلاس؛ و Readable.from(iterable) هر آرایه/generator را به استریم object-mode تبدیل میکند.
6.3فشار برگشتی: مهمترین مفهوم فصل
Backpressure (فشار برگشتی) یعنی سازنده سریعتر از مصرفکننده تولید میکند و باید «پدال گاز را رها کند». مکانیزم: بافر داخلی Writable سقفی دارد به نام highWaterMark (پیشفرض ۱۶KB)؛ write() وقتی بافر پرتر از سقف شود false برمیگرداند؛ وقتی بافر خالی شد، رویداد drain میآید و میتوانی ادامه بدهی. نکتهٔ خطرناک: این سازوکار «توصیهای» است — اگر false را نادیده بگیری، هیچچیز جلویت را نمیگیرد جز رشد بیپایان حافظه:
function generateMore () { // (۱)
while (chance.bool({ likelihood: 95 })) {
const randomChunk = chance.string({ length: (16 * 1024) - 1 })
const shouldContinue = res.write(`${randomChunk}\n`) // (۲)
if (!shouldContinue) {
console.log('back-pressure') // (۳) بافر پر شد
return res.once('drain', generateMore) // (۴) صبر تا خالیشدن
}
}
res.end('\n\n') // (۵) پایان
}
این همان الگویی است که pipe() و pipeline() خودکار برایت انجام میدهند. اما مراقب تلهٔ معروف باش: pipe خطاها را منتقل نمیکند! a.pipe(b).on('error', ...) فقط خطای b را میگیرد؛ خطای a استریمها را «unpipe» میکند بدون destroy — یعنی نشتی دیسکریپتور و حافظه. راهحل رسمی: pipeline() که به همهٔ استریمها هندلر خطا میدهد، در شکست همه را destroy میکند و در کالبک نهایی خطا را میدهد:
import { createGzip, createGunzip } from 'zlib'
import { Transform, pipeline } from 'stream'
const uppercasify = new Transform({
transform (chunk, enc, cb) {
this.push(chunk.toString().toUpperCase())
cb() // تمام شد؛ تکهٔ بعدی
}
})
pipeline(
process.stdin,
createGunzip(),
uppercasify,
createGzip(),
process.stdout,
(err) => { // خطای هر استریمی اینجا
if (err) { console.error(err); process.exit(1) }
}
)
دو جواهر دیگر فصل: استریم بهعنوان کنترل جریان — چون _transform تا فراخوانی callback بعدی صدا زده نمیشود، استریم طبیعتاً Sequential Iterator است؛ با ParallelStream (شمارندهٔ وظایف در جریان) unordered-parallel میشود، با LimitedParallelStream محدود، و با پکیج parallel-transform موازیِ مرتب. و توپولوژیهای لولهکشی: ترکیب (پوشاندن کل خط لوله در یک Duplex سیاهجعبه با pumpify)، انشعاب/forking (یک منبع به چند مقصد، مثل Unix tee)، ادغام/merging (چند منبع به یک مقصد — حتماً {end:false} و شمارش پایانها، وگرنه مقصد با اولین منبع میبندد!) و مالتیپلکس/دمولتیپلکس (چند کانال منطقی روی یک استریم فیزیکی با هدرِ شناسهٔ کانال — همان کاری که مخابرات با IP/TCP میکند). هشدارهای امنیتی/عملی فصل هم ارزشمندند: همیشه basename() روی نام فایل دریافتی (وگرنه حملهٔ مسیر!)، setEncoding('utf8') برای متن تا کاراکتر چندبایتی وسط تکه نصف نشود، و برای ساخت انبوه استریمها، تنبل باش تا EMFILE too many open files نگیری.
الگوهای GoF (گنگ چهارنفره: Gamma, Helm, Johnson, Vlissides) محصول زبانهای کلاسیک OO هستند؛ JavaScript با پروتوتایپ و توابع درجهیک، اجرای کلاسیکشان را ناممکن یا بیمعنا میکند — چیزی که میماند «ایده و مسئله» است. این فصل: چگونه میسازیم؟
7.1Factory: آفرینش را از پیادهسازی جدا کن
Factory یعنی بهجای new مستقیم، یک تابع بسازد. چرا؟ سه دلیل: new کدت را برای همیشه به یک کلاس گره میزند؛ Factory سطح API کوچکتری دارد (هیچچیز برای گسترش/هک لو نمیدهد) و میتواند کلاسها را کاملاً خصوصی نگه دارد:
function createImage (name) {
return new Image(name)
}
// دو سال بعد، بدون شکستن هیچ فراخوانندهای:
function createImage (name) {
if (name.match(/\.jpe?g$/)) return new ImageJpeg(name)
if (name.match(/\.gif$/)) return new ImageGif(name)
if (name.match(/\.png$/)) return new ImagePng(name)
throw new Error('Unsupported format')
}
Factory حتی با کلوژر میتواند وضعیت «واقعاً خصوصی» بسازد (شیء privateProperties بیرون از دسترس دنیا) و با duck typing هر چیزی با رابطِ درست برگرداند — مثال کتاب: در محیط production یک noopProfiler (شیء بیاثر با همان رابط) برگرداندن بهجای پروفایلر واقعی، فقط با چک process.env.NODE_ENV.
7.2Builder و Revealing Constructor
Builder برای سازندههای هیولایی است — مثل کلاس قایق کتاب با ۱۰ پارامتر بولی/رشتهای که هیچکس نمیداند کدام کدام است. Builder یک رابط روان (Fluent Interface) میسازد: هر متد ستکننده this برمیگرداند و build() تضمین میکند خروجی همیشه «کامل و سازگار» است:
const myBoat = new BoatBuilder()
.withMotors(2, 'Best Motor Co.', 'OM123') // پارامترهای مرتبط با هم
.withSails(1, 'fabric', 'white')
.withCabin()
.hullColor('blue')
.build()
Revealing Constructor الگوی بومی جامعهٔ JS است (نامگذاری Domenic Denicola) — و اگر new Promise((resolve, reject) => ...) را بلدی، بلدش هستی! سازنده یک «executor» میگیرد، در لحظهٔ ساخت با بخشی از اعماق شیء صدا زده میشود و بعد از آن آن اعماق برای همیشه نامرئیاند. کتاب با ImmutableBuffer نشانش میدهد: متدهای تغییردهنده (write*/fill*/swap*) فقط به executor داده میشوند؛ متدهای خواندنی روی instance بایند میشوند؛ بعد از سازنده، نوشتن غیرممکن است — «قابلیتِ تغییر فقط یکبار در تاریخِ جهانِ آن شیء وجود داشت: لحظهٔ ساخت».
7.3Singleton و جایگزینش: Dependency Injection
در Node چون «هر ماژول فقط یکبار ارزیابی میشود»، Singleton گاهی «تقریباً ضدالگو» است: فقط کافی است نمونه را از ماژول export کنی و کش کار را تمام کند. اما حاشیهٔ مهم: کلید کش «مسیر کامل ماژول» است، پس یکتایی فقط در محدودهٔ یک بسته تضمین میشود — اگر نسخههای ناسازگار یک وابستگی بهصورت تو در تو نصب شوند، دو نمونه داری! گرهزدن ماژولها با «import یک singleton» ساده و خواناست، اما تستپذیری و تعویض پیادهسازی را میکُشد. راهحل: Dependency Injection (تزریق وابستگی) — کامپوننت وابستگی را «دریافت» کند نه import:
// blog.js — وابستگی دریافت میشود، نه ایمپورت
export class Blog {
constructor (db) { // ← تزریق در سازنده
this.db = db
this.dbRun = promisify(db.run.bind(db))
this.dbAll = promisify(db.all.bind(db))
}
initialize () { /* CREATE TABLE ... */ }
createPost (id, title, content, createdAt) { /* INSERT ... */ }
getAllPosts () { /* SELECT ... */ }
}
// index.js — «تزریقکننده» گراف را میبندد
async function main () {
const db = createDb(join(__dirname, 'data.sqlite')) // factory
const blog = new Blog(db) // تزریق
await blog.initialize()
}
main().catch(console.error)
فقط دو چیز عوض شد: blog.js دیگر db.js را import نمیکند و سازندهٔ Blog تزریق میگیرد. حالا هر شیء با متدهای سازگار run/all کار میکند — از جمله mock در تست (باز هم duck typing!). هزینهٔ DI: رابطهها در زمان کدنویسی کمتر «پیدا»اند و گراف وابستگی را باید دستی و بهترتیب بسازی؛ برای اپهای بزرگ IoC container مثل inversify یا awilix هست، هرچند کتاب میگوید این «کمی از راه Node فاصله میگیرد».
8.1Proxy: کنترل دسترسی با رابط یکسان
Proxy شیئی است که دسترسی به «سوژه» (subject) را کنترل میکند و رابط دقیقاً یکسان با او دارد (اسم دیگرش surrogate) — کاربردها: اعتبارسنجی داده، امنیت، کش، مقداردهی تنبل، لاگگیری، اشیای راهدور. سه تکنیک پیادهسازی وجود دارد و درس اصلی فصل همینهاست: (۱) Object Composition — شیء جدید با همان رابط که سوژه را نگه میدارد و متدهای موردنظر را رهگیری، بقیه را یکییکی delegate میکند (امن اما کسلکننده؛ برای lazy init اجباری)؛ (۲) Object Augmentation / Monkey Patching — متدِ خود سوژه را مستقیم عوض کن (کوتاه اما شیء مشترک را جهش میدهد — خطرناک!)؛ (۳) کلاس بومی ES2015 Proxy — گزینندهها: new Proxy(target, handler) با تلههای get/set/has/apply/delete/construct که روی هر عملیات متناظر خودکار آتش میشوند؛ بدون جهش سوژه، delegate پیشفرض، و instanceof هم کار میکند چون پروتوتایپ target به ارث میرسد:
const safeCalculatorHandler = {
get: (target, property) => {
if (property === 'divide') { // فقط این متد رهگیری میشود
return function () {
const divisor = target.peekValue()
if (divisor === 0) throw Error('Division by 0')
return target.divide() // delegate به سوژه
}
}
return target[property] // بقیه: عبور شفاف
}
}
const calculator = new StackCalculator()
const safeCalculator = new Proxy(calculator, safeCalculatorHandler)
همین تکنیک با تلهٔ set به الگوی Change Observer میرسد — پایهٔ برنامهنویسی واکنشی (Reactive Programming): هر انتسابِ واقعاً جدید، ناظر را با {prop, prev, curr} خبر میکند؛ همان ایدهای که Vue 3 و MobX رویش بنا شدهاند. هشدارها: monkey patching روی اشیای مشترک یعنی «همهٔ دارندگان همان نمونه» رفتار جدید میبینند؛ و کلاس Proxy را نمیتوان کامل transpile کرد (تلهها به پشتیبانی runtime نیاز دارند).
8.2Decorator و Adapter: قیافهشناسی سه خواهر
Decorator شیء موجود را «بهصورت پویا و بهازای هر نمونه» با قابلیت تازه غنی میکند — پیادهسازیاش دقیقاً همان سه تکنیک Proxy است؛ تفاوت در قصد است: decorator «افزایش» میدهد، proxy «کنترل دسترسی با رابط بدون تغییر». مثال کتاب پلاگین LevelUP است:
export function levelSubscribe (db) {
db.subscribe = (pattern, listener) => { // (۱) قابلیت جدید!
db.on('put', (key, val) => { // (۲) هر نوشتن
const match = Object.keys(pattern).every(
k => (pattern[k] === val[k]) // (۳) تطبیق الگو
)
if (match) listener(key, val) // (۴)
})
}
return db
}
Adapter اما رابط را «عوض» میکند — مثل مبدل USB-C: کلاینت انتظار رابط A را دارد، تو رابط B داری؛ آداپتور ترجمه میکند (حتی ترکیب چند فراخوان). کتاب یک آداپتور کامل fs روی LevelUP میسازد: readFile/writeFile به db.get/put ترجمه میشوند و حتی خطای NotFoundError به شکل ENOENT فایلسیستم بازسازی میشود تا کد کلاینت هیچ تفاوتی نبیند. جملهٔ جمعبندی کتاب از دید مصرفکننده: پروکسی رابط یکسان میدهد، دکوراتور رابط غنیشده، آداپتور رابط متفاوت — در JavaScript روی این نامها وسواس نداشته باش؛ نوع مسئله را ببین. (LevelDB/LevelUP را هم بشناس: دیتابیس key-value مینیمال گوگل که «Node.js دنیای دیتابیسها» لقب گرفته — هستهٔ کوچک، اکوسیستم پلاگین بزرگ.)
9.1Strategy، State و Template
Strategy بخش متغیر الگوریتم را به «استراتژیهای قابل تعویض» جدا میکند و زمینه (context) منطق مشترک را نگه میدارد — پادزهر شاخههای بیابانی if/else. تشبیه کتاب: لاستیک ماشین — برای برف لاستیک زمستانی، برای اتوبان اسپرت؛ ماشین (context) را عوض نمیکنی. مثال Config با فرمتهای مختلف:
export const iniStrategy = {
deserialize: data => ini.parse(data),
serialize: data => ini.stringify(data)
}
export const jsonStrategy = {
deserialize: data => JSON.parse(data),
serialize: data => JSON.stringify(data, null, ' ')
}
// new Config(iniStrategy) یا new Config(jsonStrategy) — یک کلاس، دو رفتار
State تخصص Strategy است جایی که استراتژیِ فعال «در طول عمر شیء» با وضعیت داخلی عوض میشود — مثال کتاب FailsafeSocket: سوکتی که آفلاین دادهها را صف میکند و با اتصال میریزد؛ هر وضعیت (offline/online) کلاس کوچکی با رفتار کامل خودش است و changeState() بینشان سوییچ میکند — هیچ if (offline) در کد نیست؛ بهترین تفکیک وقتی است که «خودِ وضعیتها» انتقال را تحریک کنند. Template برعکس: اسکلت الگوریتم در کلاس پایه ثابت است و زیرکلاس «جای خالیها» را پر میکند — در JS متدهای انتزاعی را با stubهای پرتابکنندهٔ خطا شبیهسازی میکنند (_serialize() { throw new Error('must be implemented') }). مثال دنیای واقعی که خودت دیدی: استریمهای Node — فقط _read/_write/_transform را پر میکنی. مقایسهٔ دقیق: Strategy تغییر در زمان اجرا؛ Template تثبیت زمان تعریف زیرکلاس (Strategy ایستا).
9.2Iterator و Generators: پیمایش بدون شناخت درون
پروتکل iterator: هر شیء با متد next() که {done, value} برمیگرداند یک iterator است؛ پروتکل iterable: هر شیء با Symbol.iterator که iterator تحویل میدهد — همین دو قرارداد باریک، کل زبان را با تو هماهنگ کرده: for...of، اسپرد، destructure، Array.from، Promise.all و حتی Readable.from همه فقط این قرارداد را میشناسند. اما دستنویسکردن iterator مزخرف است؛ راه راحت:
function * fruitGenerator () {
yield 'peach' // { value: 'peach', done: false }
yield 'watermelon' // { value: 'watermelon', done: false }
return 'summer' // { value: 'summer', done: true } — در for...of نمیآید!
}
for (const fruit of fruitGenerator()) console.log(fruit) // peach, watermelon
generator (تابع با function*) «شبهکوروتین» است: در yield معلق میشود و در next() از همانجا ادامه میدهد — وضعیت محلی بین ورودها حفظ میشود. دوطرفه هم هست: next(arg) مقدار به درون میفرستد، throw() در نقطهٔ معلق خطا میپراند. نسخهٔ ناهمگام: async generator (async function*) با for await...of — و اتصال طلایی: استریمهای خواندنی Node خودشان async iterable هستند و Readable.from() برعکس هر iterable را استریم میکند (نمونهٔ Adapter!). مثال CheckUrls کتاب:
export class CheckUrls {
constructor (urls) { this.urls = urls }
async * [Symbol.asyncIterator] () {
for (const url of this.urls) {
try {
const checkResult = await superagent.head(url).redirects(2)
yield `${url} is up, status: ${checkResult.status}`
} catch (err) {
yield `${url} is down, error: ${err.message}`
}
}
}
}
// مصرف: for await (const status of new CheckUrls([...])) console.log(status)
9.3Middleware و Command: دو ستون معماری Node
Middleware الگویی است که Express جهانیاش کرد: خط لولهای از واحدهای پردازش (req, res, next) که توسط یک «Middleware Manager» بهترتیب اجرا میشوند؛ هر واحد میتواند کارش را تمام کند و next() صدا بزند، یا با ندادنِ next جریان را متوقف کند. مفهومخویشاوندانش Intercepting Filter و Chain of Responsibility هستند. کتاب با ZeroMQ نشانش میدهد تا از Express گره باز شود:
export class ZmqMiddlewareManager {
constructor (socket) {
this.socket = socket
this.inboundMiddleware = []
this.outboundMiddleware = []
this.handleIncomingMessages().catch(console.error)
}
async handleIncomingMessages () { // پیامهای ورودی
for await (const [message] of this.socket) {
await this.executeMiddleware(this.inboundMiddleware, message)
.catch(err => console.error('Error while processing', err))
}
}
async send (message) { // پیامهای خروجی
const finalMessage = await this.executeMiddleware(this.outboundMiddleware, message)
return this.socket.send(finalMessage)
}
async executeMiddleware (middlewares, initialMessage) {
let message = initialMessage
for await (const middlewareFunc of middlewares) {
message = await middlewareFunc.call(this, message) // خروجی هر واحد، ورودی بعدی
}
return message
}
}
و هر middleware فقط یک شیء با inbound/outbound است (مثل jsonMiddleware که Buffer→JSON و برعکس). نکتهٔ ترتیب: inbound با push و outbound با unshift ثبت میشوند تا جفتهای مکمل (فشردهسازی/بازگشایی) بهترتیب معکوس درست اجرا شوند. تصمیم طراحی مهم: انتشار داده بین واحدها یا با «افزایش شیء مشترک» (راحت، گرهزدگی پنهان) یا «کپی ایدهآل» (ایمن، گرانتر). نسخههای همخانواده: Koa (با async/await) و Middy (برای Lambda).
Command آخرین الگو: بهجای فراخوانی مستقیم تابع، «همهچیزِ لازم برای اجرای آیندهٔ یک عمل» را در شیء مادی کن — مزایا: زمانبندی، سریالسازی روی شبکه (توزیع کار، RPC)، تاریخچه، undo، تراکنش اتمی و تحلیل تبدیلی (پشت ویرایشگرهای همکاری). نمونهٔ کتاب:
export function createPostStatusCmd (service, status) {
let postId = null
return {
run () { postId = service.postUpdate(status) },
undo () {
if (postId) { service.destroyUpdate(postId); postId = null }
},
serialize () { return { type: 'status', action: 'post', status: status } }
}
}
export class Invoker {
constructor () { this.history = [] }
run (cmd) { this.history.push(cmd); cmd.run() }
undo () { this.history.pop().undo() }
delay (cmd, ms) { setTimeout(cmd.run.bind(cmd), ms) }
runRemotely (cmd) { /* ارسال serialize شده با HTTP POST */ }
}
همان یک شیء حالا قابل اجرا، لغو، زمانبندی و ارسال راهدور است. نسخهٔ مینیمال هم «الگوی Task» است: () => target(...args) — برای فقط زمانبندی کافی است؛ Command کامل را فقط برای undo/تاریخچه/توزیع نگه دار.
10.1Module Bundler: کامپایلر مرورگر
مرورگر نمیتواند مثل سرور از فایلسیستم ماژول بخواند و دانلود صدها فایل جدا هم گران است؛ راهحل: Module Bundler (بستهبند ماژول) — «کامپایلری برای مرورگر» که نقطهٔ ورود + کل گراف وابستگی را میگیرد و چند فایل بهینه میسازد. دو فاز داخلیاش را بشناس (این را بفهمی، خروجی webpack را دیگر جادو نمیبینی): ۱) حل وابستگی (Dependency Resolution) — از ورود بهصورت بازگشتی importها را میکاود و گراف وابستگی میسازد؛ خروجی یک modules map است: نقشهٔ شناسه→تابعِ factory. ۲) بستهبندی (Packing) — نقشه در یک IIFE پیچیده میشود که یک require() محلی تعریف میکند:
((modulesMap) => {
const require = (name) => {
const module = { exports: {} } // هر ماژول، module خودش
modulesMap[name](module, require) // اجرای factory
return module.exports
}
require('app.js') // بالا آوردن نقطهٔ ورود
})({
'app.js': (module, require) => {/*...*/},
'calculator.js': (module, require) => {/*...*/},
'display.js': (module, require) => {/*...*/},
})
همان سیستم ماژول فصل ۲، اینبار «در یک فایل برای مرورگر»! bundler امروزی همچنین transpile میکند (Babel برای مرورگرهای قدیمی)، tree shaking میکند (حذف exportهای استفادهنشده) و برای cache busting نام فایل را با hash محتوا میسازد (main.1234567890.js) تا CDN هرگز نسخهٔ کهنه ندهد. نمونهها: webpack، Parcel (بیپیکربندی)، Rollup (اولین ESM کامل) و مدرنها مثل ESBuild.
10.2سه الگوی تفکیک پلتفرم و SSR
وقتی کد مشترک میخواهی، سه سطح الگو داری: (۱) شاخهزنی زمان اجرا — typeof window !== 'undefined'؛ سریع اما هر دو شاخه به مرورگر ارسال میشود (حتی ممکن است راز/کلید لو برود!) و خوانایی میمیرد. (۲) شاخهزنی زمان ساخت — DefinePlugin جای __BROWSER__ مقدار میگذارد و Terser کد مرده را فیزیکی حذف میکند؛ سبکتر اما ifهای پراکنده. (۳) تعویض ماژول (Module Swapping) — تمیزترین: دو پیادهسازی جدا (say-hello.js و say-hello-browser.js) و در زمان ساخت، NormalModuleReplacementPlugin مسیر را با regex عوض میکند؛ هر پیادهسازی کتابخانهٔ مخصوص پلتفرم خودش را دارد و بستهٔ مقابل آلوده نمیشود.
SSR (رندر سمت سرور) در برابر CSR: در اپ تکصفحهایِ خالص (CSR) سرور HTML تقریباً خالی میفرستد (curl را امتحان کن: هیچ!) — اولین رنگآمیزی کند و SEO صفر. با SSR سرور با renderToString() اپ React را به HTML واقعی تبدیل و ارسال میکند؛ روتر سمت سرور StaticRouter است (URL از درخواست) و سمت کلاینت BrowserRouter. دو مشکل کلاسیک که کتاب با کد حل میکند: صفحهٔ 404 که status 200 میدهد — راهحل: staticContext (شیء پل بین React و سرور؛ صفحهٔ 404 داخلش statusCode = 404 مینویسد و سرور بعد از رندر میخواند)؛ و مسئلهٔ دادهٔ ناهمگام — componentDidMount فقط در مرورگر اجرا میشود و renderToString همگام است، پس SSR «Loading...» بیفایده میفرستد. دو الگوی حل: رندر دومرحلهای (پرومیسهای داده به staticContext وصل میشوند، سرور منتظرشان میماند و دوباره رندر میکند — منعطف اما گران؛ هر renderToString ارزان نیست) و صفحات ناهمگام (Async Pages) — ساختار مقیدتر اما سریعتر: مسیرها در آرایهٔ مشترک routes.js؛ سرور با matchPath() قبل از رندر میفهمد کدام صفحه رندر میشود؛ اگر متد استاتیک preloadAsyncData() داشت، اول داده را میگیرد، در window.__STATIC_CONTEXT__ تزریق میکند، یکبار رندر میکند (کتاب میگوید Next.js همین تکنیک را واقعیتر اجرا میکند). گام آخر: Hydration — چون سرور و کلاینت markup یکسان تولید میکنند، کلاینت بهجای رندر از صفر با hydrate() فقط شنوندههای رویداد را به DOM آماده میچسباند و لود اول سریع میشود.
11.1صف پیشآمادهسازی: وقتی کامپوننت هنوز آماده نیست
درایورهای DB و کلاینتهای صف پیام «ناهمگامآغاز» هستند: تا اتصال کامل نشود هر فراخوانی میشکند. سه راهحل ساده مشکل دارند (چک محلی در هر مصرفکننده = boilerplate پراکنده؛ بهتعویق انداختن کل اپ = شکننده و کند). راهحل شیک: Pre-initialization Queue — کامپوننت خودش فراخوانیهای «زودهنگام» را بهصورت Command صف کند و بعد از اتصال بازپخش کند؛ مصرفکننده اصلاً نمیفهمد:
class DB extends EventEmitter {
connected = false
commandsQueue = []
async query (queryString) {
if (!this.connected) {
console.log(`Request queued: ${queryString}`)
return new Promise((resolve, reject) => { // (۱) پرومیس «نگهدارنده»
const command = () => {
this.query(queryString).then(resolve, reject) // (۲) بازپخش بعدی
}
this.commandsQueue.push(command) // (۳) صفِ کارها
})
}
console.log(`Query executed: ${queryString}`)
}
connect () {
setTimeout(() => {
this.connected = true
this.emit('connected')
this.commandsQueue.forEach(command => command()) // (۴) پخش همه
this.commandsQueue = []
}, 500)
}
}
export const db = new DB()
همین الگو عمومیاش با State است (QueuingState ← InitializedState) و در دنیای واقعی: Mongoose دقیقاً همینطور عملیات را صف میکند و pg هر کوئری را فارغ از وضعیت — برای تجربهٔ توسعهدهندهٔ خوب. اگر کامپوننت را نمیتوانی عوض کنی، با Proxy همان را دورش بپیچ.
11.2تجمیع و کش درخواستها: جادوی پرومیسها
دو ایده که با پرومیس تقریباً مجانی میشوند: Batching (تجمیع) — اگر درخواست یکسانی در جریان است، «سوار همان» شو؛ Caching (کش) — نتیجه را با TTL نگه دار. رمز فنی: یک پرومیس «خودش کشِ مقدار resolve شده است» — چند شنونده میتوانند به یک پرومیس وصل شوند (batching) و then همیشه ناهمگام و فقط یکبار میآید (ضد Zalgo):
import { totalSales as totalSalesRaw } from './totalSales.js'
const CACHE_TTL = 30 * 1000 // ۳۰ ثانیه
const cache = new Map()
export function totalSales (product) {
if (cache.has(product)) return cache.get(product) // کشخورده
const resultPromise = totalSalesRaw(product)
cache.set(product, resultPromise)
resultPromise.then(() => { // فقط موفق: TTL بگذار
setTimeout(() => cache.delete(product), CACHE_TTL)
}, err => {
cache.delete(product) // شکست: فوراً پاک کن!
throw err
})
return resultPromise
}
قانون طلایی: هرگز پرومیسِ شکستخورده را کش نکن — وگرنه خطای گذرا تا پایان TTL برای همه بازپخش میشود. در تست بار کتاب، همین ۱۲ خط سرور را حدود ۴ برابر سریعتر کرد. برای مقیاس واقعی: LRU/FIFO برای محدودکردن حافظه، Redis/Memcached برای اشتراک بین نمونهها، و جملهٔ معروف: «دو چیز سخت در علوم کامپیوتر: نامگذاری چیزها و ابطال کش».
11.3لغو عملیات ناهمگام و کارهای CPU-محور
لغو (Cancellation): Node رشتهای نمیکشد و Promises/A+ هم API لغو ندارد. نسخهٔ نهایی الگو: سرپرست مبتنی بر generator — جریان را با yield بنویس (بهجای await) و یک سرپرست با next()/throw() آن را میراند؛ لحظهٔ فراخوان cancel()، سرپرست در نزدیکترین مرز گام با CancelError reject میکند — لغو شفاف بدون لاجیک پراکنده. در تولید از پکیج caf استفاده کن (و بدان تفاوت: لغوِ «واکنش» پرومیس ≠ لغوِ «عملیات زیرین»).
کارهای CPU-محور (CPU-bound): وظیفهای که تا تمام شدن، هرگز به حلقهٔ رویداد برنمیگردد — در Node تکرشتهای یعنی «کل سرور فریز» (کتاب نشان میدهد سرور حتی I'm alive! جواب نمیدهد!). مثال نمایشی مسئلهٔ subset sum است (جستجوی فراگیر O(2ⁿ)). سه راهحل بهترتیب بلوغ: (۱) درهمتنیدگی با setImmediate — الگوریتم را به گامهای ریز بشکن و هر گام را با setImmediate به دورهٔ بعد بفرست تا حلقهٔ رویداد نفس بکشد؛ ظریف: هرگز process.nextTick نه! (قبل از I/O اجرا میشود و گرسنگی I/O میسازد)؛ هزینه: سربار هر گام × تعداد گامها — فقط برای کارهای کوتاه و پراکنده. (۲) پروسهٔ بیرونی با child_process.fork() — کل محاسبه به فرزند برود؛ سرعت کامل، هستهٔ جدا، حتی زبان دیگر («همیشه بهترین ابزار برای هر کار»)؛ fork کانال پیام خودکار میسازد (send/on('message')) و ProcessPool (فرچید/آزادساز + صف انتظار) پروسهها را گرم نگه میدارد و سقف همزمانی میگذارد (ضد DoS). (۳) Worker Threads (worker_threads، از Node 10.5) — رشتههای واقعی درون همان پروسه: حافظهٔ کمتر، شروع سریعتر، هر worker با V8 و event loop مستقل و «هیچچیز مشترک بهطور پیشفرض» — همین ایزولهسازی است که معناشناسی تکرشتهای JS را حفظ میکند («جاوااسکریپت با ترد، دیگر جاوااسکریپت نیست»)؛ اشتراک داده فقط با پیام، ArrayBuffer انتقالی، یا SharedArrayBuffer + Atomics. مهاجرت از ProcessPool به ThreadPool سه خط است: new Worker(file) و رویداد 'online'. توصیهٔ تولیدی: بهجای pool دستساز از workerpool یا piscina (timeout، مدیریت خطا، بازیابی کرش) و اگر یک ماشین کافی نبود — فصل ۱۲!
12.1مکعب مقیاس و ماژول cluster
جملهٔ کلیدی فصل: «Node برای توزیعشدن به دنیا آمد» — تکرشتهایاش یعنی مقیاسدهی زودتر از پلتفرمهای چندرشتهای سراغت میآید، و این مزیت است نه نقص: همان تکنیکها دسترسپذیری بالا، تحمل خطا و ریاستارت بدون قطعی را هم میخرند. مدل استاندارد «مکعب مقیاس»: محور X = کلونکردن (n نسخهٔ یکسان، هرکدام ۱/n بار)، محور Y = تفکیک بر اساس سرویس (میکروسرویسها)، محور Z = پارتیشن داده (هر نمونه یک برش از داده). اول X و Y را بسوزان؛ Z گران است (هر عملیات lookup میخواهد). ابزار محور X در یک ماشین: ماژول cluster:
import { createServer } from 'http'
import { cpus } from 'os'
import cluster from 'cluster'
if (cluster.isMaster) { // (۱) پروسهٔ مادر
const availableCpus = cpus()
console.log(`Clustering to ${availableCpus.length} processes`)
availableCpus.forEach(() => cluster.fork()) // بهازای هر هسته
} else { // (۲) حالت کارگر (همین فایل دوباره!)
const { pid } = process
const server = createServer((req, res) => {
res.end(`Hello from ${pid}\n`)
})
server.listen(8080)
}
الگوی همیشگی: if (isMaster) fork() else doWork() — مادر فقط fork میکند؛ کارگر همان فایل دوباره اجرا شده و سرور واقعی است. تقسیم اتصالات round-robin است و بنچمارک کتاب روی ۴ هسته ~۳.۳ برابر شد (۳۰۰ ← ۱۰۰۰ تراکنش/ثانیه). چون کارگرها پروسههای مستقلاند، تابآوری مجانی است: کارگرِ کرششده را دوباره fork کن (چک !worker.exitedAfterDisconnect تا خروج عمدی را جایگزین نکنی) — تست کرش کتاب: سروری که هر ۱-۳ ثانیه میمیرد، ~۹۲٪ دسترسپذیری نگه داشت! و ریاستارت بدون قطعی: روی سیگنال SIGUSR2 کارگرها را یکییکی disconnect() کن، جایگزین fork کن، تا رویداد 'listening' صبر کن، بعدی — همیشه حداقل یک کارگر زنده. (سیگنالهای Unix ویندوز ندارند؛ pm2 همهٔ اینها را بستهبندی کرده.)
12.2وضعیت، پروکسی معکوس و کانتینرها
اولین غافلگیری مقیاس: وضعیت (state). لاگینِ «جان» در حافظهٔ نمونهٔ ۱ ذخیره شده؛ درخواست بعدی به نمونهٔ ۲ میرود که او را نمیشناسد! دو درمان: مخزن مشترک (Redis/Memcached یا هر DB) — بهتر؛ یا Sticky Session (بالانسر همهٔ درخواستهای یک جلسه را به یک نمونه قفل کند) — سریع اما مزیت افزونگی را باطل میکند (نمونهای که میمیرد، جلسههایش را با خود میبرد) و برای WebSocket لازم میشود. اصل معماری: «نمونهها را stateless نگه دار». گام بعدی مقیاس بین ماشینها: Reverse Proxy (پروکسی معکوس) — سرور جلویی که درخواستها را به بکاندها میفرستد؛ علاوه بر بالانس: SSL termination، کش، بازنویسی URL، فایلهای استاتیک و محافظت DoS؛ نمونهٔ کتاب Nginx:
http {
upstream my-load-balanced-app {
server 127.0.0.1:8081;
server 127.0.0.1:8082;
server 127.0.0.1:8083;
server 127.0.0.1:8084;
}
server {
listen 8080;
location / { proxy_pass http://my-load-balanced-app; }
}
}
برای زیرساخت الاستیک (سرورها با ترافیک بالا/پایین میشوند) Service Registry لازم است: مخزن مرکزی که نمونهها هنگام بالا آمدن خودشان را ثبت و هنگام خروج حذف میکنند (نمونه: Consul با health check که سرویس مرده را خودش بیرون میکشد)؛ بالانسر پویا با http-proxy در هر درخواست از رجیستری میپرسد و round-robin میکند (در تولید: کش کن! هر درخواست پرسوجوی رجیستری گران است). و لایهٔ صنعتی: Container (Docker) — «واحد استاندارد نرمافزار که کد و همهٔ وابستگیهایش را بستهبندی میکند» — یک پروسهٔ Linux ایزوله با سربار نزدیک بومی (پرش بزرگ از VM)؛ Dockerfile ← ایمیج ← اجرا. و Kubernetes («سکاندار» یونانی) برای چندکانتینر/چندسرور: نودها را کلاستر میکند، کانتینر مرده را ریاستارت، دیسکاوری و بالانس و rollout/rollback دارد؛ فلسفهاش اعلانی (Declarative) است: وضعیت مطلوب را تعریف کن («records of intent»)، خودش نگهش میدارد؛ مقیاس یعنی kubectl scale --replicas=5. هشدار: پیچیدگی K8s برای اپ کوچک نمیارزد؛ در web-scale توجیه دارد.
12.3از مونولیت به میکروسرویس و سه الگوی یکپارچهسازی
معماری مونولیتیک: همهٔ قابلیتها یک کدبیس و یک پروسه — حتی اگر درونش ماژولار باشی، جفتشدگی بهراحتی رشد میکند («خیلی راحت است که Checkout مستقیم شیء Product را آپدیت کند») و هر کامپوننت «یک بلوک چوبی در برج جنگا» است. میکروسرویس: مهمترین الگوی فصل با خلاصهٔ طلا: «از نوشتن اپلیکیشنهای بزرگ دوری کن». سرویسهای کوچک و خودبسنده که هر کدام دیتابیس خودشان را دارند (مالکیت داده = ضد جفتشدگی داده)؛ ملاک «کوچکی» نیست — جفتشدگی کم، انسجام بالا، پیچیدگی یکپارچهسازی. مزایا: سرویسهای از دور خارجشدنی (کرش پخش نمیشود؛ بازنویسی یکی به هر زبان/DB)، استفادهٔ مجدد، مخفیسازی اطلاعات، مقیاس مستقل. معایب: یکپارچهسازی، استقرار و مانیتورینگِ گرههای زیاد. سه الگوی یکپارچهسازی از ساده به پیچیده: ۱) API Proxy (گیتوی) — نقطهٔ ورود واحد جلوی چند API + بالانس/کش/احراز هویت/rate limit؛ یکپارچهسازی «ساختاری»: سرویسها را جابهجا/ادغام کن بدون اثر بر بالادست. ۲) API Orchestration — لایهای که سرویسها را به «عملیات مخصوص اپ» میچیند: completeCheckout() = پرداخت Checkout + حذف Cart + آپدیت Products؛ یکپارچهسازی «معنایی» که API جدید میسازد؛ خطر: تبدیلشدن به God Object. ۳) Message Broker — میانافزاری که فرستنده از گیرنده جدا میکند؛ Checkout رویداد purchased منتشر میکند و Cart و Products مشترک میشوند و واکنش میدهند — «هیچ سرویس خدایی نیست که بخواهد دندههای کل سیستم را بچرخاند»؛ پل ورود به فصل ۱۳.
13.1چهار تصمیم بنیادین سیستم پیامرسانی
«اگر مقیاس یعنی تقسیم سیستمها، یکپارچهسازی یعنی وصلکردنشان». چارچوب کتاب چهار تصمیم است: (۱) جهت: یکطرفه (push و تمام) یا Request/Reply (هر درخواست جواب دارد — در یک کانال ساده، بین گرهها پیچیده). (۲) نوع پیام: Command Message (به گیرنده میگوید «انجام بده» — پایهٔ RPC؛ فعلهای REST هم فرماناند)، Event Message («اتفاقی افتاد» — همه را همصفحه نگه میدارد)، Document Message (دادهٔ خالص بدون دستور — معمولاً جواب فرمان). (۳) زمانبندی: همگام = تماس تلفنی (هر دو طرف آنلاین و زنده)؛ ناهمگام = پیامک (گیرنده میتواند آفلاین باشد؛ جواب دیر/جابهجا/هیچ — پارالیسم بهتر با منابع کمتر؛ و سیستم ناهمگام میتواند پیام را «ذخیره» کند — کلید قابلیت اطمینان). (۴) تحویل: مستقیم peer-to-peer (آدرس/پروتکل/فرمت را باید بدانی؛ بدون نقطهٔ شکست واحد؛ تأخیر کم) یا با Broker (جداکنندهٔ فرستنده/گیرنده، صف پایدار، مسیریابی، پل پروتکل — RabbitMQ با AMQP/MQTT/STOMP حرف میزند؛ هزینه: یک سیستم دیگر برای اجرا). دو ساختار میانی هم مهماند: Message Queue (پیامها تا امکان تحویل ذخیره میشوند؛ کندی/مرگ مصرفکننده ← انباشت و بعد تحویل) و Stream (لاگ) — ساختار فقط-افزودنی و ماندگار که «رکوردها با مصرف حذف نمیشوند»: هر مصرفکننده زنده میخواند یا تاریخچه را replay میکند؛ «شبیه data store است نه message broker» (Kafka/Kinesis در مقیاس، Redis Streams برای حالت ساده). و معناشناسی تحویل: at-most-once (fire-and-forget؛ ممکن است گم شود)، at-least-once (پایدار+retry؛ دوبل ممکن)، exactly-once (قابلاعتمادترین، کندترین). و Durable Subscriber — مشترکی که به لطف صف، «همهٔ» پیامها حتی زمان قطعی را میگیرد.
13.2Pub/Sub: از چت Redis تا RabbitMQ پایدار
Publish/Subscribe شناختهشدهترین الگوی یکطرفه است — Observer توزیعشده: مشترکها علاقهمندی ثبت میکنند؛ ناشر گیرندهها را نمیشناسد؛ دو طرف «شل» به هم وصلاند. نسخهٔ Redis کتاب، چت چندنمونهای است: هر نمونه هم ناشر است هم مشترک روی کانال chat_messages:
import Redis from 'ioredis'
const redisSub = new Redis() // دو اتصال!
const redisPub = new Redis()
wss.on('connection', client => {
client.on('message', msg => redisPub.publish('chat_messages', msg))
})
redisSub.subscribe('chat_messages')
redisSub.on('message', (channel, msg) => {
for (const client of wss.clients) {
if (client.readyState === ws.OPEN) client.send(msg)
}
})
نکتهٔ عملی: دو اتصال لازم است چون اتصال Redis در حالت مشترک فقط فرمانهای اشتراک میفهمد. نسخهٔ peer-to-peer با ZeroMQ: هر نود PUB را bind میکند و به PUBهای همتایان SUB connect میکند با فیلتر پیشوند — و صادقانه بدان: ZeroMQ بیسروصدا بخشنده است؛ اتصال ناموفق را بیخطا retry میکند و PUB بدون مشترک پیامها را «بیاعتراض دور میریزد».
وقتی پیام نباید گم شود: AMQP/RabbitMQ با مدل غنی — Queue (durable = بعد از ریاستارت broker بازساخته میشود اما فقط پیامهای persistent زنده میمانند!؛ exclusive = متصل به یک اتصال؛ auto-delete = با آخرین مشترک حذف)، Exchange (direct = کلید دقیق؛ topic = الگوی glob؛ fanout = پخش به همه) و Binding (پیوند exchange↔queue با کلید). الگوی Durable Subscriber یعنی صفِ غیر-exclusive و غیر-auto-delete. نمونهٔ سرویس تاریخچهٔ چت:
const connection = await amqp.connect('amqp://localhost')
const channel = await connection.createChannel()
await channel.assertExchange('chat', 'fanout') // مطمئن شو هست
const { queue } = channel.assertQueue('chat_history') // صف durable
await channel.bindQueue(queue, 'chat') // fanout: بدون کلید
channel.consume(queue, async msg => {
const content = msg.content.toString()
await db.put(timestamp(), content) // اول ذخیره...
channel.ack(msg) // ...بعد تأیید
})
قاعدهٔ حیاتی: ack فقط بعد از پردازش موفق — کرش وسط کار یعنی پیام در صف میماند و دوباره تحویل میشود. (noAck: true فقط وقتی گمشدن قابلتحمل است.) جالبتر: Redis Streams — با xadd افزودنیاند (ID مونوتونیک خودکار)، با xrange کل تاریخچه خوانده میشود و با xread BLOCK میتوانی برای رکورد جدید «پارک» کنی — یعنی هم پیام زنده، هم replay تاریخچه، بدون هیچ سرویس تاریخچهی جدا!
13.3توزیع وظایف و Request/Reply روی کانال یکطرفه
وقتی هدف «توزیع بار» است نه اعلان: الگوی Competing Consumers (مصرفکنندههای رقیب) — هر وظیفه به دقیقاً یک کارگر برسد. زیباییاش: مصرفکنندهها «connect» میشوند، پس افزودن کارگر بدون دستزدن به تولیدکننده یا رجیستری! سه پیادهسازی: ZeroMQ PUSH/PULL — سوکتهای یکطرفه (PUSH بفرستد، PULL بگیرد؛ هرکدام bind یا connect)؛ یک PUSH به چند PULL = بالانس، چند PUSH به یک PULL = صف منصفانه؛ سازهٔ کلاسیک «بادبانکش» (ventilator وظایف را پخش میکند ← کارگران ← sink جمع میکند)؛ اما بدون ack: کرش کارگر = گمشدن وظایف در جریان. AMQP — با sendToQueue() مستقیم به صف (بدون exchange) و چند مصرفکننده روی همان صف = توزیع بار رسمی. Redis Consumer Groups — گروه نامدار از مصرفکنندههای نامدار روی یک استریم (xreadgroup)؛ رکوردها باید xack شوند وگرنه «pending» میمانند و هر مصرفکننده فقط pending های خودش را میبیند (بازیابی پس از کرش)؛ چند گروه مستقل میتوانند همزمان یک استریم را پردازش کنند.
و وارونگی زیبا: Request/Reply روی کانال یکطرفه — چون کانالهای یکطرفه سریعتریناند، دو الگو به آن معنای دوسویه میدهند: Correlation Identifier — هر درخواست ID یکتا بگیرد؛ گیرنده همان ID را در جواب (inReplyTo) برگرداند؛ فرستنده با Map از ID به resolver، جوابهای بیترتیب را به درخواست درست میدوزد (با timeout). Return Address — وقتی چند کانال/درخواستکننده هست: درخواست، آدرس صف جوابش را حمل کند (در AMQP ویژگی replyTo با صف خصوصی exclusive با نام تصادفی که با اتصال نابود میشود). ترکیب این دو + صف durable درخواستها = سرور جوابدهِ قابلمقیاس: چند replier خودکار رقیب میشوند و هیچ جوابی گم نمیشود. کتاب که میبندد، جملهاش این است: زمانی که بین ZeroMQ (سرعت، بدون نقطهٔ شکست) و RabbitMQ (پایداری، مسیریابی) شک داری، «تأخیر کم ممکن است در برابر کاهش عظیم پیچیدگی معنی نداشته باشد» — کل فصل را با این ترازو بخوان.
این فصل به درخواست ما اضافه شده: چطور الگوهای این کتاب را با تفکر Agentic Coding ترکیب کنی؟ (سند جفت این کتاب، «هرمس ایجنت ۲.۰»، سمت دیگر همین پل است.) واقعیت بازار ۲۰۲۶ این است: تقریباً همهٔ زیرساخت دنیای ایجنتها — سرورهای MCP، دروازهها (Gateway)، CLIها، ابزارها — با Node.js نوشته شدهاند، چون I/O محور است، اکوسیستمش غول است و همزمانی سبکی دارد. کسی که فقط «پرامپتنویسی» بلد است، کاربر ایجنتهاست؛ کسی که این کتاب را بلد است، سازندهٔ ایجنتهاست. این نگاشت را ببین:
| الگوی کتاب | در دنیای Agentic Coding |
|---|---|
| حلقهٔ رویداد و Reactor (فصل ۱) | قلب هر دروازهٔ ایجنت: حلقهٔ «مدل ← ابزار ← نتیجه» همان چرخهٔ رویداد است با رویدادهای آلمانیالگوی مدل |
| ماژول و SKILL.md (فصل ۲ + هرمس) | مهارت ایجنت = ماژول دانش؛ همان اصول رابط کوچک و مخفیسازی؛ استاندارد باز SKILL.md یعنی «پورتپذیری ماژول» بین ایجنتها |
| EventEmitter و Observer (فصل ۳) | Observer Hooks ایجنتها (تلهمتری فقط-خواندنی) و رویدادهای اتمام/خطای ابزارها — دقیقاً emitter با قرارداد error |
| صف با همزمانی محدود (فصلهای ۴-۵) | صف وظایف ایجنتها: اجرای موازیِ محدود ابزارها (p-limit) تا سهمیه API نسوزد؛ الگوی Producer–Consumer همان است |
| استریم و backpressure (فصل ۶) | پخش جواب استریمی مدل به کاربر، پردازش لاگهای حجمی، خط لولهٔ RAG؛ backpressure وقتی مدل سریعتر از DB توست! |
| Factory و تزریق وابستگی (فصل ۷) | سوئیچ provider مدل (OpenAI/Anthropic/محلی) با factory + duck typing؛ تست ایجنت با mock کردن ابزارها |
| Proxy (فصل ۸) | لایهٔ rate-limit/caching/لاگ دور کلاینت مدل؛ Change Observer برای ردیابی وضعیت جلسه — همان تکنیک تلهٔ Proxy |
| Middleware (فصل ۹) | معماری سرور MCP: احراز هویت، اعتبارسنجی و لاگ بهصورت خط لولهٔ middleware دور هر فراخوان ابزار |
| Command (فصل ۹) | هر «فراخوان ابزار» یک Command است: سریالپذیر، قابل حسابرسی، قابل undo — پایهٔ لاگ مسیر (trajectory) |
| batching/caching (فصل ۱۱) | تجمیع فراخوانهای تکراری مدل/ابزار و کش جوابها با پرومیس — همان ۱۲ خط طلایی، برای کاهش هزینهٔ توکن |
| worker_threads و Pool (فصل ۱۱) | اجرای ابزارهای CPU-محور (embeddings، پردازش سند) بیرون از حلقهٔ اصلی سرور |
| cluster و صف پیام (فصل ۱۲-۱۳) | مقیاسدادن Gateway ایجنت، صف وظایف چندایجنته، دوبارهکاری ایمن با at-least-once و ack بعد از پردازش |
نقشهٔ راه سهماههٔ پیشنهادی: ماه اول — فصلهای ۱ تا ۶ این کتاب را عمیق تمرین کن (هر الگو را با یک اسکریپت واقعی بنویس؛ مخصوصاً TaskQueue و pipeline). ماه دوم — فصلهای ۷ تا ۱۳ را با یک پروژهٔ واقعی ترکیب کن: مثلاً یک «صف وظایف چندایجنته» با SQLite + صف + worker (ترکیب فصلهای ۴، ۹ و ۱۱). ماه سوم — سند هرمس را بخوان، هرمس را روی VPS بالا بیاور و یک سرور MCP با Node.js بنویس که ابزارهای خودت را به آن عرضه کند (سرور MCP از نظر معماری = همان خط لولهٔ middleware + الگوی Command). از این نقطه به بعد، تو دیگر نه «کاربر AI» هستی نه فقط «برنامهنویس» — بلکه کسی هستی که هم زبان ایجنتها (پرامپت، حافظه، حلقهٔ یادگیری) را میفهمد و هم زبان زیرساختشان (Node.js) را.
همهٔ اصطلاحات کلیدی خلاصه یکجا؛ برای وقتی که منابع انگلیسی یا سورسکد را میخوانی.
| Term (انگلیسی) | توضیح فارسی |
|---|---|
| Reactor Pattern | الگوی راکتور؛ بلاکشدن تا رسیدن رویداد از منابع و توزیع هر رویداد به هندلرش — قلب معماری Node |
| Event Loop | حلقهٔ رویداد؛ چرخهٔ «بلاک روی دمولتیپلکسر ← پردازش رویدادهای آماده ← تکرار» |
| Event Demultiplexer | دمولتیپلکسر رویداد؛ API سیستمعامل (epoll/kqueue/IOCP) که فقط منابع آماده را برمیگرداند |
| Blocking / Non-blocking I/O | ورودی-خروجی مسدودکننده (ترد میخوابد) / غیرمسدودکننده (فوراً برمیگردد با «الان ندارم») |
| Busy-Waiting | انتظار فعال؛ پرسوجوی مداوم منابع در حلقه — هدردهی CPU |
| libuv | موتور I/O متنباز Node: پیادهسازی راکتور، حلقهٔ رویداد و انتزاع سیستمعاملها |
| V8 | موتور جاوااسکریپت گوگل؛ قلب اجرای کد در Node |
| Userland | سرزمین کاربران؛ دنیای بستههای npm بیرون از هستهٔ Node |
| CommonJS (CJS) | سیستم ماژول اصلی Node: require و module.exports؛ همگام و پویا |
| ESM | ماژولهای ECMAScript؛ سیستم استاندارد زبان: import/export ایستا با سه فاز لود |
| Read-only Live Binding | اتصال زندهٔ فقط-خواندنی؛ تغییر مبدأ برای مصرفکننده دیده میشود اما نوشتن از سمت او ممنوع |
| Tree Shaking | حذف کد مرده؛ کنارگذاشتن exportهای استفادهنشده به لطف import ایستا |
| Circular Dependency | وابستگی چرخهای؛ دو ماژول که به هم require/import میکنند (CJS: تصویر ناقص؛ ESM: تمیز) |
| CPS (Continuation-Passing Style) | سبک انتقال ادامه؛ تحویل نتیجه به «تابع ادامه» بهجای return |
| Releasing Zalgo | آزادسازی زالگو؛ ضدالگوی ترکیب رفتار همگام/ناهمگام در یک API — منشأ باگهای بازتولیدناپذیر |
| process.nextTick() | زمانبندی میکروتسک بلافاصله بعد از عملیات فعلی، قبل از I/O (خطر گرسنگی I/O در بازگشت) |
| setImmediate() | زمانبندی بعد از رویدادهای I/O؛ ابزار ایمن برای شکستن کارهای سنگین |
| Error-First Callback | قرارداد کالبک Node: (err, result...) با err=null در موفقیت و همیشه نمونهٔ Error |
| Fail-Fast | سریعشکستخوردن؛ خروج فوری بعد از استثنای ناپیگرفته و واگذاری ریاستارت به ناظر |
| EventEmitter | پیادهسازی بومی Observer در Node: on/once/emit/removeListener |
| Observer Pattern | الگوی ناظر؛ سوژه به همهٔ ناظرها اطلاع میدهد — برای رویدادهای مکرر |
| Sequential Iterator | تکرارکنندهٔ ترتیبی؛ الگوی iterate(index) برای پیمایش ناهمگام یک مجموعه |
| Concurrency | همزمانی؛ وظایف در جریانِ همزمان با درهمتنیدگی روی یک ترد (نه پارالیسم واقعی) |
| Race Condition | شرط رقابت؛ نتیجهٔ وابسته به ترتیب زمانی عملیات ناهمگام روی منبع مشترک |
| TaskQueue | صف وظایف؛ صف مرکزی با سقف همزمانی برای اجرای موازی محدود |
| Producer–Consumer | تولیدکننده-مصرفکننده؛ همزمانی با مصرفکنندههای ثابت که وقتی صف خالی است «میخوابند» |
| Promise | پرومیس؛ حامل وضعیت و نتیجهٔ آیندهٔ عملیات ناهمگام: pending/fulfilled/rejected |
| Thenable | هر شیء دارای then()؛ سازگاری پرومیسها با duck typing |
| Promisification | پرومیسیسازی؛ پیچاندن API کالبکی در پوشش پرومیسبرگشتان (util.promisify) |
| async/await | ساختار نحوی روی پرومیسها؛ مکث و ادامه با معناشناسی سینکنما و try/catch یکدست |
| return await | تضاد کلاسیک: return await promise → catch محلی کار میکند؛ return promise → نمیشود |
| Stream | استریم؛ پردازش تکهبهتکهٔ داده با حافظهٔ ثابت: Readable/Writable/Duplex/Transform |
| Backpressure | فشار برگشتی؛ سازوکار write=false و رویداد drain برای کندکردن سازندهٔ سریع |
| highWaterMark | سقف بافر داخلی استریم (پیشفرض ۱۶KB) که ماشهٔ backpressure است |
| pipe() / pipeline() | اتصال استریمها؛ pipeline رسمی و امن است (انتقال خطا + destroy همه در شکست) |
| Object Mode | حالت شیء؛ استریمِ مقادیر دلخواه JS بهجای بافر — استریم را پارادایم عمومی پردازش میکند |
| Transform | استریم تبدیلگر؛ خروجی از ورودی محاسبه میشود: _transform و _flush |
| Multiplexing (mux/demux) | مالتیپلکس؛ چند کانال منطقی روی یک استریم فیزیکی با هدر شناسهٔ کانال |
| Factory | الگوی کارخانه؛ تابع ساخت اشیا برای جداکردن آفرینش از پیادهسازی و سطح API کوچک |
| Builder | الگوی سازنده؛ رابط روانِ گامبهگام برای سازندههای پیچیده + تضمین سازگاری خروجی |
| Revealing Constructor | سازندهٔ افشاگر؛ executor فقط در لحظهٔ ساخت به اعماق شیء دست دارد (مثل Promise) |
| Singleton | تکنمونه؛ در Node با کش ماژول تقریباً مجانی — اما فقط در محدودهٔ یک بسته |
| Dependency Injection (DI) | تزریق وابستگی؛ دریافت وابستگیها از بیرون بهجای import مستقیم — برای تست و تعویض پیادهسازی |
| Proxy | الگوی پروکسی؛ کنترل دسترسی با رابط یکسان: composition/monkey patching/کلاس Proxy با تلهها |
| Decorator | الگوی دکوراتور؛ افزودن قابلیت بهازای هر نمونه — همان تکنیکها با قصد «غنیسازی» |
| Adapter | الگوی آداپتور؛ ترجمهٔ رابط انتظار کلاینت به رابط موجود (مبدل USB) |
| Change Observer | ناظر تغییر؛ اطلاعرسانی تغییر ویژگیها با تلهٔ set — پایهٔ برنامهنویسی واکنشی |
| Strategy / State | استراتژی (تعویض بخش متغیر الگوریتم) / وضعیت (تخصص استراتژی با تعویض در طول عمر شیء) |
| Template | الگوی قالب؛ اسکلت ثابت + جای خالی برای زیرکلاسها (مثل _read در استریمها) |
| Iterator / Generator | پروتکل پیمایش next()→{done,value} / تابع function* با yield معلق — و نسخههای ناهمگام |
| Middleware | میانافزار؛ خط لولهٔ واحدهای پردازش (req, res, next) — معماری Express و سرورهای MCP |
| Command | الگوی فرمان؛ مادیسازی یک فراخوان به شیء قابل زمانبندی/سریال/undo + الگوی مینیمال Task |
| Module Bundler | بستهبند ماژول؛ «کامپایلر مرورگر»: گراف وابستگی + require محلی در یک فایل (webpack و...) |
| SSR / CSR | رندر سمت سرور (HTML واقعی؛ SEO) / رندر سمت کلاینت (SPA خالی) |
| Hydration | آبرسانی؛ چسباندن رفتار کلاینت به DOM ساختهشدهٔ سرور بدون رندر مجدد |
| Pre-initialization Queue | صف پیشآمادهسازی؛ صفکردن فراخوانیهای زودهنگام و بازپخش پس از اتصال (روش Mongoose) |
| Request Batching / Caching | تجمیع و کش درخواستها با پرومیس (سوارشدن بر درخواستِ در جریان؛ هرگز پرومیس شکسته را کش نکن) |
| CPU-bound | کار CPU-محور؛ فریزکنندهٔ حلقهٔ رویداد — با setImmediate، fork یا worker_threads حل شود |
| Worker Threads | رشتههای کاری Node (≥10.5) با V8 و event loop مستقل و بدون اشتراک پیشفرض |
| Scale Cube | مکعب مقیاس؛ X=کلون، Y=تفکیک سرویس، Z=پارتیشن داده |
| cluster Module | ماژول کلاستر؛ مادر + کارگرها (بهازای هر هسته) با توزیع اتصال round-robin |
| Zero-Downtime Restart | ریاستارت بدون قطعی؛ تعویض کارگرها یکییکی تا همیشه حداقل یکی زنده باشد |
| Sticky Session | جلسهٔ چسبان؛ قفلکردن درخواستهای یک جلسه به یک نمونه — به بهای باطلشدن افزونگی |
| Reverse Proxy | پروکسی معکوس؛ دروازهٔ جلویی + بالانس (Nginx) با SSL termination و کش |
| Service Registry | رجیستری سرویس؛ مخزن مرکزی ثبت/حذف خودکار نمونهها (Consul) برای مقیاس پویا |
| Container / Kubernetes | کانتینر (بستهٔ کد+وابستگی، Docker) / ارکستراتور اعلانی کانتینرها (سکاندار) |
| Monolith / Microservice | مونولیت (یک کدبیس/پروسه، برج جنگا) / میکروسرویس (خودبسنده با دیتابیس اختصاصی؛ جفتشدگی کم) |
| API Proxy / Orchestration | گیتوی ساختاری جلوی چند API / لایهٔ معنایی ترکیب سرویسها به عملیات جدید (خطر: God Object) |
| Message Broker | میانافزار پیام؛ جداکنندهٔ فرستنده/گیرنده با صف، مسیریابی و پل پروتکل (RabbitMQ) |
| Command / Event / Document Message | سه نوع پیام: «انجام بده» / «اتفاق افتاد» / «فقط داده» |
| Message Queue vs Stream | صف (ذخیره تا تحویل) / لاگ فقط-افزودنیِ replayپذیر و چندمشترک (Redis Streams، Kafka) |
| At-most/At-least/Exactly-once | سه معناشناسی تحویل: ممکن است گم شود / ممکن است دوبل شود / تکی اما کند |
| Durable Subscriber | مشترک پایدار؛ با صف durable همهٔ پیامها حتی زمان قطعی را میگیرد |
| AMQP (Exchange/Binding/Queue) | پروتکل پیام RabbitMQ: مسیریاب direct/topic/fanout، پیوند کلیددار، صف با ack |
| ack | تأیید پردازش پیام توسط مصرفکننده؛ فقط بعد از پردازش موفق — وگرنه پیام pending میماند |
| Competing Consumers | مصرفکنندههای رقیب؛ چند کارگر روی یک صف، هر وظیفه به دقیقاً یکی — توزیع بار |
| Consumer Group | گروه مصرفکننده Redis Streams؛ round-robin با ack و لیست pending برای بازیابی |
| PUSH/PULL (ZeroMQ) | سوکتهای یکطرفه برای خط لوله: ventilator ← کارگران ← sink؛ بدون ack |
| Correlation Identifier | شناسهٔ ارتباط؛ تطبیق جواب با درخواست روی کانال یکطرفه با ID یکتا + Map |
| Return Address | آدرس بازگشت؛ صف جواب خصوصی که در ویژگی replyTo درخواست حمل میشود |
مسئلهها را بشناس.
الگو خودش میآید.