সরাসরি পর্বে যান
পর্ব ৪৯ · ৫৯-এর মধ্যে · মডিউল ৭

Node.js — ফাইল, stream আর process: ব্রাউজারের বাইরে JavaScript

Node.js Fundamentals — Files, Paths, Buffers, Streams, Processes & Building a CLI

Taught in Bangla, this lesson takes JavaScript outside the browser with Node.js: the process object with argv, env, exit codes and signals, cross-platform paths, reading and writing files with sync, callback and promise APIs, Buffers and character encodings, streams and pipeline for large data, running child processes safely, and building a daily sales report CLI.

এই পর্বে যা শিখবেন

  • Node.js আসলে কী দিয়ে তৈরি, আর ব্রাউজারের JavaScript থেকে কোথায় আলাদা
  • process, path আর fs/promises দিয়ে ফাইল পড়া-লেখা, ফোল্ডার, error code সামলানো
  • Buffer, UTF-8 আর বাংলা লেখার bytes — কেন length আর byte সংখ্যা মেলে না
  • Stream, backpressure, pipeline আর readline দিয়ে বড় log ফাইল লাইন ধরে বিশ্লেষণ
  • নিরাপদে অন্য প্রোগ্রাম চালানো (execFile), SIGINT-এ ভদ্রভাবে বন্ধ হওয়া, আর নিজের CLI tool বানানো

১ · Node.js কী — ব্রাউজার ছাড়া একই JavaScript

এতদিন আমাদের JavaScript চলেছে মূলত ব্রাউজারে — বোতামে সাড়া, form যাচাই, পাতা বদলানো। কিন্তু অনলাইন দোকানের অর্ডার কোথায় জমা হয়? প্রতিদিন রাতে বিক্রির হিসাব কে বানায়? এই কাজগুলো চলে সার্ভারে বা আপনার কম্পিউটারের terminal-এ, যেখানে কোনো পাতা নেই, কোনো বোতাম নেই। সেখানেও JavaScript চালানোর ব্যবস্থাটির নাম Node.js.

একজন দক্ষ রাঁধুনির কথা ভাবুন। বাড়ির রান্নাঘরে তিনি চুলা, হাঁড়ি আর ফ্রিজ ব্যবহার করেন। একই রাঁধুনিকে একটি বিয়েবাড়ির ডেকচি-রান্নার প্যান্ডেলে নিলে তার হাত, রেসিপি, দক্ষতা সব একই থাকে — শুধু হাতের কাছের সরঞ্জাম বদলে যায়: বিশাল ডেকচি, গ্যাস সিলিন্ডার, মালপত্রের গুদাম। রাঁধুনি = JavaScript ভাষা আর V8 engine. বাড়ির রান্নাঘর = ব্রাউজার (DOM, বোতাম, localStorage). প্যান্ডেল = Node.js (ফাইল, নেটওয়ার্ক, process). রেসিপি বা ভাষা একই, সরঞ্জাম আলাদা।

পর্ব ০৫-এ দেখেছেন, V8 হলো JavaScript কোডকে machine code বানিয়ে চালানোর engine. ২০০৯ সালে Ryan Dahl V8-কে ব্রাউজার থেকে আলাদা করে নিয়ে তার সাথে তিনটি জিনিস জুড়ে দেন। আজকের Node.js মোটামুটি এই তিন স্তরে তৈরি:

⚙️ V8

আপনার JavaScript পার্স করে, JIT দিয়ে দ্রুত চালায়, মেমরি সামলায়। Chrome আর Edge-এর ভেতরেও এই একই engine.

ভাষা চালায়

🔁 libuv

libuv C-তে লেখা একটি library: event loop, নেটওয়ার্কের অপেক্ষা, আর ফাইল পড়ার মতো কাজের জন্য ছোট thread pool. পর্ব ৩১-এ এর phase-গুলো দেখেছেন।

অপেক্ষার কাজ করে

📦 Core modules

fs, path, http, stream, crypto, child_process — Node-এর সাথেই আসা core module, যা আলাদা করে ইনস্টল করতে হয় না।

সরঞ্জামের বাক্স
মূল ধারণা

Node.js-এর জোর দুটি জায়গায়। প্রথমত, non-blocking, event-driven I/O: ফাইল বা ডেটাবেসের উত্তরের জন্য thread বসে থাকে না, তাই একটি process একসাথে হাজার হাজার সংযোগ সামলাতে পারে। দ্বিতীয়ত, stream: ডেটা পুরোটা মেমরিতে না তুলে টুকরো টুকরো প্রসেস করা যায় — ১০ GB-র log ফাইলও কয়েক MB মেমরিতে পড়া যায়। এই পর্বের বাকি সবকিছু এই দুই ধারণার ওপর দাঁড়িয়ে।

ব্রাউজার বনাম Node — কোন global কোথায়

বিষয়ব্রাউজারNode.js
Global objectwindow (আর globalThis)global (আর globalThis)
পাতাdocument, DOMনেই — document লিখলে ReferenceError
চলমান প্রোগ্রামের তথ্যlocation, navigatorprocess — argv, env, cwd, exit code
BytesArrayBuffer, Uint8Arrayএগুলো তো আছেই, সাথে Buffer
নেটওয়ার্কfetchfetch-ও আছে (Node 18 থেকে global), সাথে http, net
ফাইলব্যবহারকারীর অনুমতি ছাড়া নয়fs দিয়ে পুরো ডিস্ক — যতটুকু OS অনুমতি দেয়
নিরাপত্তার দেয়ালsandbox, same-origin policyপ্রায় নেই — Node প্রোগ্রাম আপনার ব্যবহারকারীর সব ক্ষমতা পায়

শেষ সারিটি খুব গুরুত্বপূর্ণ। ব্রাউজারে অচেনা ওয়েবসাইটের কোড আপনার ফাইল ছুঁতে পারে না; কিন্তু Node-এ চালানো যেকোনো script বা npm package আপনার ফোল্ডার মুছে দিতে পারে, .env ফাইলের গোপন key পড়ে পাঠিয়ে দিতে পারে। তাই অচেনা package ইনস্টলের আগে ভাবুন — পর্ব ৪৩-এ supply-chain attack দেখেছেন, পর্ব ৪৬-এ npm-এর নিরাপদ ব্যবহার নিয়েও কথা হয়েছে।

চালানোর তিন উপায় — script, REPL আর watch mode

node --version            # কোন version আছে
node app.js               # একটি ফাইল চালানো
node                      # REPL — লাইন লিখুন, সাথে সাথে ফল (.exit বা Ctrl+D দিয়ে বের হোন)
node --watch app.js       # ফাইল বদলালেই আবার চালায় (Node 18.11 থেকে)
node --env-file=.env app.js   # .env ফাইল থেকে environment variable (Node 20.6 থেকে)

REPL (Read–Eval–Print Loop) ছোট পরীক্ষার জন্য দারুণ: Buffer.byteLength("টাকা") লিখে Enter চাপলেই উত্তর। --watch আগে nodemon নামের আলাদা package দিয়ে করতে হতো; এখন Node নিজেই পারে — watch mode Node 22-এ (আর 20.13-এ) “স্থিতিশীল” ঘোষিত হয়েছে। --env-file-ও একইভাবে dotenv package-এর কাজ অনেকটা নিজে করে দেয়; Node 20-এ এটি পরীক্ষামূলক (experimental) চিহ্নিত ছিল।

Core module আর node: prefix

Core module আনার আধুনিক রীতি হলো নামের আগে node: লেখা — require("node:fs") বা import fs from "node:fs". এতে পরিষ্কার বোঝা যায় এটি Node-এর নিজের module, npm-এর কোনো package নয়; কেউ npm-এ fs নামের নকল package ছাড়লেও বিভ্রান্তির সুযোগ থাকে না। কিছু নতুন module (যেমন node:test) শুধু prefix দিয়েই পাওয়া যায়। এই পর্বের runnable উদাহরণগুলো CommonJS (require) ধাঁচে লেখা, কারণ এই পাতার রানার ফাইলটিকে সেভাবে চালায়; import/export আর দুই ব্যবস্থার পার্থক্য পর্ব ৪৫-এ।

২ · process — প্রোগ্রামের পরিচয়পত্র: argv, env, exit code আর signal

প্রতিটি চলমান প্রোগ্রাম অপারেটিং সিস্টেমের চোখে একটি process. Node-এ সেই process-এর সব তথ্য আর নিয়ন্ত্রণ পাওয়া যায় process নামের global object-এ। সবচেয়ে বেশি লাগে এগুলো:

অংশকী দেয়উদাহরণ
process.argvকমান্ড লাইনের সব শব্দ, array হিসেবেnode report.js may.csv → [node-এর পথ, script-এর পথ, "may.csv"]; আসল argument শুরু argv[2] থেকে
process.envenvironment variable — সব মান stringprocess.env.PORT ?? "3000"
process.cwd()working directory — কমান্ড কোন ফোল্ডার থেকে দেওয়া হয়েছেscript যে ফোল্ডারেই থাকুক, relative পথ এখান থেকে ধরা হয়
process.exitCodeপ্রোগ্রাম শেষে OS-কে কোন exit code জানাবে০ = সফল, অন্য যেকোনো সংখ্যা = ব্যর্থ
stdin, stdout, stderrতিনটি standard streamইনপুট, সাধারণ আউটপুট, ভুলের বার্তা
process.on("SIGINT", fn)OS-এর পাঠানো signal শোনাCtrl+C চাপলে SIGINT

env আর .env — গোপন জিনিস কোডের বাইরে

ডেটাবেসের পাসওয়ার্ড, bKash বা কোনো পেমেন্ট সেবার API key, SMS gateway-এর token — এগুলো কোডে লিখে রাখা যায় না, কারণ কোড GitHub-এ যায়, সহকর্মীর ল্যাপটপে যায়। নিয়ম হলো: গোপন মান থাকবে environment variable-এ, আর কোড শুধু নাম ধরে পড়বে। নিজের কম্পিউটারে সুবিধার জন্য সেগুলো একটি .env ফাইলে লেখা হয়:

PORT=3000
DB_URL=postgres://shop:গোপন@localhost/shop
SMS_TOKEN=xxxxxxxxxxxxxxxx

node --env-file=.env server.js চালালে এই মানগুলো process.env-এ চলে আসে। তিনটি নিয়ম কখনো ভুলবেন না। (১) .env ফাইলটি .gitignore-এ দিন — কখনো commit নয়; বদলে .env.example রাখুন, যাতে শুধু নামগুলো থাকে। (২) একবার ভুল করে commit হয়ে গেলে শুধু ফাইল মুছলে চলবে না — git history-তে থেকে যায়; key বাতিল করে নতুন key নিন। (৩) সব মান string: process.env.PORT হলো "3000", সংখ্যা নয়; আর "false" string-টি truthy (পর্ব ০৮)। তাই Number(...) বা স্পষ্ট তুলনা দিয়ে রূপান্তর করুন।

Exit code — “সফল হলাম কি না” OS-কে জানানো

আপনার script যখন অন্য কোনো tool-এর অংশ — যেমন CI pipeline বা প্রতিদিন রাতের scheduled কাজ — তখন সেই tool আউটপুট পড়ে না, পড়ে exit code. ০ মানে সব ঠিক, অশূন্য মানে কিছু ভুল। Node নিজে থেকে uncaught exception বা handle না করা rejected promise-এ 1 দিয়ে বের হয়।

process.exit() সাবধানে: এটি সাথে সাথে process থামিয়ে দেয় — অর্ধেক লেখা ফাইল, না পাঠানো log, চলমান network request সব ফেলে। বেশিরভাগ ক্ষেত্রে ভালো উপায় হলো process.exitCode = 1 সেট করে স্বাভাবিকভাবে শেষ হতে দেওয়া: event loop-এ আর কাজ না থাকলে Node নিজেই বের হবে, সেই code নিয়ে। আর ভুলের বার্তা console.error দিয়ে stderr-এ লিখুন, stdout-এ নয় — তাহলে node report.js > out.json করলে ভুলের লেখা JSON ফাইলে মিশে যাবে না।

Signal আর graceful shutdown

Terminal-এ Ctrl+C চাপলে OS প্রোগ্রামকে SIGINT পাঠায়; Docker, Kubernetes বা সার্ভার বন্ধের সময় সাধারণত আসে SIGTERM. কেউ না শুনলে Node সাথে সাথে বেরিয়ে যায়। কিন্তু একটি সার্ভার হঠাৎ মরে গেলে চলমান অর্ডার অর্ধেক থেকে যায়। তাই সার্ভার এই signal শুনে graceful shutdown করে — নতুন কাজ নেওয়া বন্ধ, চলমান কাজ শেষ, সংযোগ বন্ধ, তারপর বিদায়। মেট্রো রেলের শেষ ট্রেনের মতো: “আর যাত্রী নেওয়া হবে না” ঘোষণা হয়, কিন্তু যারা উঠে গেছেন তাদের গন্তব্যে পৌঁছে দিয়ে তবেই ট্রেন ডিপোতে যায়।

const server = http.createServer(app).listen(3000);
let closing = false;

function shutdown(signal) {
  if (closing) return;            // দুবার Ctrl+C চাপলেও একবারই
  closing = true;
  console.log(`${signal} পেলাম — নতুন request নেওয়া বন্ধ`);
  server.close(async () => {      // চলমান request শেষ হলে এই callback
    await db.end();               // ডেটাবেস সংযোগ বন্ধ
    console.log("সব গুছিয়ে বিদায়");
  });                             // loop খালি হলে Node নিজেই বের হবে
  setTimeout(() => process.exit(1), 10_000).unref(); // ১০ সেকেন্ডের বেশি নয়
}
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);

এটি একটি পূর্ণ সার্ভারের অংশ (app আর db অন্য জায়গায় তৈরি) — পড়ে বোঝার জন্য। লক্ষ করুন, SIGINT-এর listener লাগালে Node-এর নিজের “বেরিয়ে যাও” আচরণ বন্ধ হয়ে যায়; তখন বের হওয়ার দায়িত্ব আপনার। শেষের timer হলো নিরাপত্তা জাল: কোনো সংযোগ আটকে থাকলে ১০ সেকেন্ড পর জোর করে বের হয়। .unref() বলে “শুধু এই timer-এর জন্য process বাঁচিয়ে রেখো না” — সব ঠিকঠাক বন্ধ হলে timer-এর অপেক্ষা ছাড়াই process শেষ হয়। Express সার্ভারে এটি কীভাবে বসে, পর্ব ৫০-এ দেখবেন।

Process-এর অবস্থা
ঘটনা
চলমান request
নতুন সংযোগ
Event loop-এ যা বাকি
Exit code
  1. স্বাভাবিক অবস্থা। সার্ভার চলছে, তিনজন গ্রাহকের request চলমান। খোলা server socket event loop-কে “বাঁচিয়ে” রাখে — তাই সার্ভার নিজে থেকে বন্ধ হয় না।
  2. Signal এল। কেউ Ctrl+C চাপলেন (বা deploy tool SIGTERM পাঠাল)। OS process-কে signal দিল; Node সেটি একটি event হিসেবে event loop-এ আনল।
  3. দরজা বন্ধ। shutdown() চলল, server.close() নতুন সংযোগ নেওয়া বন্ধ করল। চলমান request-গুলো কিন্তু চলছে। নিরাপত্তার timer চালু, তবে unref করা।
  4. দেরিতে আসা গ্রাহক। এই মুহূর্তে আরেকটি সংযোগ এল — এবার আর নেওয়া হলো না। Load balancer থাকলে সেটি অন্য সার্ভারে পাঠাবে।
  5. চলমান কাজ শেষ। তিনটি request-এর উত্তর গেল। কোনো অর্ডার অর্ধেক থেকে যায়নি — graceful shutdown-এর আসল উদ্দেশ্য এটাই।
  6. Close-এর callback. শেষ সংযোগ বন্ধ হতেই callback চলল, ডেটাবেস pool বন্ধ হলো। এখন event loop-এ আর কোনো খোলা handle নেই — unref করা timer গোনায় ধরে না।
  7. Loop খালি → বিদায়। Node স্বাভাবিকভাবে বের হলো, exit code 0. কোথাও আটকে গেলে ১০ সেকেন্ডের timer process.exit(1) দিয়ে শেষ করত — তখন exit code 1 দেখে deploy tool বুঝত কিছু গোলমাল ছিল।

৩ · path — Windows আর Linux-এ একই কোড

Windows-এ পথ লেখা হয় C:\shop\data\orders.csv, আর Linux বা macOS-এ /home/shop/data/orders.csv. বিভাজক চিহ্ন (path separator) আলাদা — \ বনাম /. আপনার ল্যাপটপ Windows, কিন্তু সার্ভার প্রায় সবসময় Linux. কোডে "data\\" + name লিখলে সার্ভারে গিয়ে ভাঙবে। সমাধান node:path module — যে OS-এ চলছে, সেই নিয়মে পথ বানায়:

Functionকী করেউদাহরণ (Linux-এ)
path.join(...)অংশগুলো ঠিক বিভাজক দিয়ে জোড়ে, .. আর বাড়তি / সাফ করেjoin("data", "may", "../june", "a.csv") → data/june/a.csv
path.resolve(...)cwd থেকে শুরু করে absolute path বানায়resolve("data") → /home/shop/data (cwd যদি /home/shop হয়)
path.basename(p, ext?)শেষ অংশ — ফাইলের নামbasename("data/orders.csv", ".csv") → orders
path.extname(p)শেষ বিন্দু থেকে extensionextname("photo.final.JPG") → .JPG
path.dirname(p)ফাইল বাদে ফোল্ডারের অংশdirname("data/orders.csv") → data
path.sepএই OS-এর বিভাজকLinux-এ /, Windows-এ \

একটি সূক্ষ্ম কিন্তু প্রচলিত বাগ: relative পথ ধরা হয় cwd থেকে, script-এর ফোল্ডার থেকে নয়। tools/report.js-এর ভেতরে readFile("orders.csv") লিখে প্রজেক্টের মূল ফোল্ডার থেকে node tools/report.js চালালে Node orders.csv খুঁজবে মূল ফোল্ডারে। Script-এর পাশের ফাইল চাইলে CommonJS-এ path.join(__dirname, "orders.csv"), আর ES module-এ import.meta.dirname (Node 20.11 থেকে) বা new URL("./orders.csv", import.meta.url). আর extname-এর .JPG খেয়াল করুন — extension তুলনার আগে .toLowerCase() করুন।

৪ · fs — ফাইল পড়া-লেখা: sync, callback আর promise

node:fs module-এ একই কাজের তিনটি রূপ আছে — ইতিহাসের তিনটি স্তর। একটি ফাইল পড়া তিনভাবে:

const fs = require("node:fs");
const fsp = require("node:fs/promises");

// ১. sync — ফাইল পড়া শেষ না হওয়া পর্যন্ত পুরো thread থেমে থাকে
const a = fs.readFileSync("config.json", "utf8");

// ২. callback — Node-এর পুরোনো রীতি, error-first callback
fs.readFile("config.json", "utf8", (err, b) => {
  if (err) return console.error(err.code);
  console.log(b.length);
});

// ৩. promise — আধুনিক রীতি, async/await-এর সাথে
const c = await fsp.readFile("config.json", "utf8");   // async function-এর ভেতরে

তিনটিই একই ফল দেয়; পার্থক্য অপেক্ষার সময় thread কী করে। Callback রূপটি পর্ব ৩২-এর error-first callback — প্রথম parameter-এ ভুল, পরেরটায় ফল। Promise রূপটি (fs/promises) পর্ব ৩৩–৩৪-এর Promise আর await দিয়ে লেখা যায়, try/catch দিয়ে ভুল ধরা যায় — নতুন কোডে এটিই প্রস্তাবিত। “Sync” শেষওয়ালা sync API-গুলো ভুল নয়, কিন্তু তাদের জায়গা সীমিত: প্রোগ্রাম শুরুর সময় একবার config পড়া, বা ছোট একবারের script — যেখানে একসাথে আর কেউ অপেক্ষা করছে না।

কেন সার্ভারে sync API বিপজ্জনক? নিচের টাইমলাইনে দুই জন গ্রাহক — রহিম একটি বড় রিপোর্ট চাইলেন (৪০ MB ফাইল পড়তে হবে), করিম শুধু একটি পণ্যের দাম। প্রথমে readFileSync, তারপর await readFile:

Main thread আর libuv (সময় ০ ms থেকে)
পদ্ধতি
সময়
Main thread
libuv thread pool
অপেক্ষায়
করিম উত্তর পেলেন
  1. Sync: রহিমের request. Handler readFileSync ডাকল। এই function ফাইল পড়া শেষ না হওয়া পর্যন্ত ফেরে না — main thread এখানেই আটকে গেল।
  2. করিম এলেন। ৫ ms-এ করিমের ছোট request এল। কাজটা এক মিলিসেকেন্ডের, কিন্তু thread ব্যস্ত — request লাইনে দাঁড়াল। Event loop ঘুরতেই পারছে না।
  3. ৩০০ ms পর। ডিস্ক থেকে ৪০ MB পড়া শেষ, রহিমের রিপোর্ট গেল। এই পুরো সময় সার্ভার কারও কথা শোনেনি — এক হাজার গ্রাহক থাকলে এক হাজারজনই অপেক্ষা করতেন।
  4. করিম অবশেষে। ৩০১ ms-এ করিমের উত্তর — যা ১ ms-এ হতে পারত। ব্যস্ত সময়ে এমন কয়েকটি request মিলে পুরো সার্ভার “ধীর” মনে হয়।
  5. Promise: রহিমের request. এবার handler await readFile(...) লিখল। পড়ার কাজটি গেল libuv-এর thread pool-এ, আর handler await-এ থেমে main thread ছেড়ে দিল।
  6. করিম সাথে সাথে। ৫ ms-এ করিমের request এল, thread খালি — সাথে সাথে handler চলল, ৬ ms-এ উত্তর। রহিমের ফাইল পড়া পাশাপাশি চলছে।
  7. Thread অপেক্ষায়, কিন্তু মুক্ত। এখন main thread-এর হাতে কাজ নেই, অন্য যেকোনো request এলে নিতে পারবে। ডিস্কের অপেক্ষা করছে libuv, JavaScript নয়।
  8. পড়া শেষ → বাকি অংশ। ৩০০ ms-এ পড়া শেষ হলে Promise পূর্ণ হলো, আর await-এর পরের অংশ (রিপোর্ট বানানো ও পাঠানো) main thread-এ চলল। রহিমের মোট সময় একই, কিন্তু করিমকে আর অপেক্ষা করতে হয়নি।

এবার একটি বাস্তব কাজ নিজে চালিয়ে দেখুন: একটি মুদি দোকানের আজকের অর্ডার CSV ফাইলে লেখা, আবার পড়ে মোট বিক্রি হিসাব করা, একটি JSON রিপোর্ট লেখা, তারপর ফোল্ডারে কী আছে দেখা। শেষে ইচ্ছা করে একটি না-থাকা ফাইল পড়ে দেখা হয়েছে ভুল কেমন আসে:

const fs = require("node:fs/promises");
const path = require("node:path");

async function main() {
  const dir = path.join("shop", "today");
  await fs.mkdir(dir, { recursive: true });            // আগে থাকলেও ভুল নয়
  const csv = "item,qty,price\nচাল,2,75\nডাল,1,140\nতেল,3,190\n";
  await fs.writeFile(path.join(dir, "orders.csv"), csv, "utf8");
  const text = await fs.readFile(path.join(dir, "orders.csv"), "utf8");
  const rows = text.trim().split("\n").slice(1).map((line) => {
    const [item, qty, price] = line.split(",");
    return { item, total: Number(qty) * Number(price) };
  });
  const grand = rows.reduce((sum, r) => sum + r.total, 0);
  const report = { orders: rows.length, grand, top: rows.toSorted((a, b) => b.total - a.total)[0].item };
  await fs.writeFile(path.join(dir, "report.json"), JSON.stringify(report, null, 2));
  console.log(report);
  const entries = await fs.readdir(dir, { withFileTypes: true });
  for (const e of entries.sort((a, b) => (a.name < b.name ? -1 : 1))) {
    const { size } = await fs.stat(path.join(dir, e.name));
    console.log(e.isFile() ? "ফাইল" : "ফোল্ডার", path.join(dir, e.name), size, "bytes");
  }
  await fs.readFile(path.join(dir, "missing.csv")).catch((err) => console.log("ধরা পড়ল:", err.code));
}
main();

আউটপুট: { orders: 3, grand: 860, top: 'তেল' }, তারপর ফাইল shop/today/orders.csv 62 bytes, ফাইল shop/today/report.json 55 bytes, আর শেষে ধরা পড়ল: ENOENT. কয়েকটি জিনিস খেয়াল করুন। mkdir-এ recursive: true না দিলে shop না থাকলে ভুল হতো, আর দ্বিতীয়বার চালালে EEXIST — recursive দুটোই সামলায়। CSV-তে অক্ষর ৪৪টি, অথচ আকার ৬২ bytes — প্রতিটি বাংলা অক্ষর UTF-8-এ ৩ bytes নেয় (অংশ ৫)। withFileTypes দিলে প্রতিটি নামের সাথে ফাইল না ফোল্ডার জানা যায়, আলাদা stat লাগে না; এখানে stat শুধু আকার জানতে। readdir-এর ক্রম OS-ভেদে আলাদা হতে পারে, তাই নাম ধরে সাজানো হয়েছে। আসল দোকানে CSV-তে কমা-ওয়ালা নাম বা উদ্ধৃতিচিহ্ন থাকতে পারে — তখন হাতে split(",") না করে একটি পরীক্ষিত CSV parser নিন।

আরও কয়েকটি দরকারি কাজ

  • JSON পড়া-লেখা: JSON.parse(await readFile(p, "utf8")) আর writeFile(p, JSON.stringify(obj, null, 2)). ভাঙা JSON-এ SyntaxError আসে — পর্ব ১৯ মনে করুন।
  • Log যোগ করা: appendFile("app.log", line + "\n") ফাইলের শেষে যোগ করে, আগের লেখা মোছে না। অনেক লাইন লিখতে হলে প্রতিবার ফাইল খোলার বদলে একটি write stream খোলা রাখা ভালো (অংশ ৬)।
  • নিরাপদে overwrite: গুরুত্বপূর্ণ ফাইল সরাসরি লিখতে গিয়ে মাঝপথে বিদ্যুৎ গেলে (লোডশেডিং!) ফাইল অর্ধেক থেকে যায়। প্রচলিত কৌশল: আগে report.json.tmp-এ লিখুন, তারপর rename করে আসল নাম দিন — একই ডিস্কে rename সাধারণত এক ধাপে হয়।
  • ফাইলের খবর: stat(p) দেয় size, mtime (শেষ বদলের সময়), isFile(), isDirectory(). শুধু “আছে কি না” দেখতে আগে stat করে তারপর খোলা ঠিক নয় — মাঝখানে অন্য কেউ ফাইল মুছে দিতে পারে; সরাসরি খুলুন আর ENOENT ধরুন।
  • ফাইল নজরে রাখা: fs.watch ফাইল বা ফোল্ডার বদলালে খবর দেয়, কিন্তু OS-ভেদে আচরণ আলাদা: একটি সেভে দুবার event আসতে পারে, ফাইলের নাম কখনো null আসে, ফোল্ডারের ভেতরের ফোল্ডার নজরে রাখা (recursive) Linux-এ Node 20-এর আশেপাশে এসেছে। তাই বাস্তব tool সাধারণত event গুলো কিছু ms জমিয়ে (debounce, পর্ব ৩৬) একবার কাজ করে, বা chokidar-এর মতো package ব্যবহার করে।

Error code — ভুলের নাম ধরে সিদ্ধান্ত

ফাইলের কাজে ভুল হলে Node একটি Error দেয়, যার code property-তে OS-এর দেওয়া ছোট নাম থাকে। এগুলো C আর Unix-এর ঐতিহ্য থেকে আসা errno code. বার্তা (err.message) OS বা version-ভেদে বদলাতে পারে, কিন্তু code স্থির — তাই সিদ্ধান্ত নিন code দেখে:

Codeমানেসাধারণত কী করবেন
ENOENTএমন ফাইল বা ফোল্ডার নেই (Error NO ENTry)প্রথমবার চালানো হলে ডিফল্ট মান ব্যবহার; নইলে পরিষ্কার বার্তা
EACCES / EPERMঅনুমতি নেইব্যবহারকারীকে জানান; root/admin হয়ে চালানো সমাধান নয়
EEXISTআগে থেকেই আছেইচ্ছা করে না-overwrite (flag: "wx") করলে এটাই প্রত্যাশিত
EISDIR / ENOTDIRফাইল চেয়েছিলেন, পেয়েছেন ফোল্ডার (বা উল্টো)পথটি যাচাই করুন
EMFILEএকসাথে খুব বেশি ফাইল খোলাএকসাথে কতগুলো খুলবেন তার সীমা দিন (পর্ব ৩৩-এর concurrency limit)
async function loadSettings(file) {
  try {
    return JSON.parse(await fs.readFile(file, "utf8"));
  } catch (err) {
    if (err.code === "ENOENT") return { currency: "BDT", lang: "bn" }; // প্রথমবার
    throw err;   // EACCES, ভাঙা JSON ইত্যাদি — চেপে রাখবেন না
  }
}

শুধু যে ভুলটি আপনি সত্যিই সামলাতে পারেন (ফাইল নেই → ডিফল্ট), সেটিই ধরুন; বাকিগুলো আবার throw করুন (পর্ব ২৯-এর rethrow). সব ভুল চুপচাপ ডিফল্ট দিয়ে ঢেকে দিলে একদিন ভাঙা settings ফাইল নিয়ে প্রোগ্রাম ভুল মুদ্রায় হিসাব করবে, আর কেউ টেরও পাবে না।

৫ · Buffer — bytes আর অক্ষর এক জিনিস নয়

ডিস্ক আর নেটওয়ার্ক অক্ষর চেনে না, চেনে শুধু bytes — ০ থেকে ২৫৫-এর সংখ্যা। “টাকা” লেখাটি ফাইলে কীভাবে থাকবে, তা ঠিক করে একটি character encoding; আজকের ওয়েবে প্রায় সর্বত্র UTF-8. Node-এ কাঁচা bytes ধরে রাখার জন্য আছে Buffer — আসলে Uint8Array-এরই একটি subclass, সাথে কিছু সুবিধাজনক method.

পর্ব ০৯-এ দেখেছেন, JavaScript string ভেতরে UTF-16-এ থাকে, আর length গোনে code unit. বাংলা অক্ষরগুলো Unicode-এর U+0980–U+09FF পরিসরে, তাই প্রতিটি একটি code unit — কিন্তু UTF-8-এ প্রতিটি ৩ bytes. নিচে “টাকা” string থেকে Buffer-এ যাওয়ার পথ ধাপে ধাপে দেখুন:

একই লেখা, তিন চোখে
দেখছি
মান
সংখ্যা
  1. JavaScript-এর চোখে। "টাকা".length হলো 4. মানুষের চোখে দুটি “অক্ষর” (টা, কা), কিন্তু JavaScript গোনে code unit.
  2. ভেতরের টুকরো। “টা” আসলে দুটি Unicode অক্ষর: ব্যঞ্জন ট আর আ-কার (া)। কা-ও তাই। মানুষের দেখা একটি অক্ষরকে বলে grapheme — পর্ব ০৯-এ Intl.Segmenter দিয়ে গুনেছিলেন।
  3. Code point. প্রতিটির একটি Unicode নম্বর: ট = U+099F, া = U+09BE, ক = U+0995. সবগুলো বাংলা ব্লকে, U+0800 থেকে U+FFFF পরিসরের ভেতরে।
  4. UTF-8 bytes. UTF-8-এর নিয়মে ওই পরিসরের প্রতিটি code point ৩ byte নেয়, শুরু হয় e0 দিয়ে। Buffer.from("টাকা") দেয় ১২টি byte. ইংরেজি “taka” হলে ৪ byte-ই হতো — ASCII অক্ষর UTF-8-এ ১ byte.
  5. মাঝখানে কাটলে। প্রথম ৪ byte কেটে toString() করলে প্রথম ৩ byte থেকে “ট” আসে, আর বাকি ১ byte অসম্পূর্ণ — তার জায়গায় replacement character �. Stream-এ chunk-এর সীমানায় ঠিক এটাই ঘটতে পারে।
  6. Base64. Bytes-কে শুধু ASCII অক্ষরে লেখার উপায়: buf.toString("base64"). প্রতি ৩ byte → ৪ অক্ষর, তাই ১২ byte → ১৬ অক্ষর। Email-এর attachment, JSON-এর ভেতরে ছোট ছবি, HTTP Basic auth-এ এটি চলে।
const buf = Buffer.from("টাকা", "utf8");
buf.length;                       // 12  (bytes)
"টাকা".length;                    // 4   (UTF-16 code unit)
buf.toString("hex");              // 'e0a69fe0a6bee0a695e0a6be'
Buffer.from("4Kaf4Ka+4KaV4Ka+", "base64").toString();  // 'টাকা'
Buffer.alloc(10);                 // ১০টি শূন্য byte — নিরাপদ
// new Buffer(10)  ← পুরোনো, deprecated; কখনো ব্যবহার করবেন না

এর ব্যবহারিক মানে কী? SMS gateway “১৬০ অক্ষর” বললে সেটি bytes বা encoding-এর হিসাব — বাংলা SMS-এ অনেক কম অক্ষর ধরে। ডেটাবেসের কলাম VARCHAR(50) কোথাও bytes গোনে, কোথাও অক্ষর। HTTP-এর Content-Length সবসময় bytes — body.length দিলে বাংলা উত্তর কেটে যাবে, দিতে হবে Buffer.byteLength(body). Base64 encryption নয় — যে কেউ উল্টে পড়তে পারে; গোপন কিছু লুকাতে এটি ব্যবহার করবেন না। আর new Buffer(n) পুরোনো Node-এ পরিষ্কার না করা মেমরি দিত, যাতে অন্য ডেটার টুকরো থেকে যেত — তাই এটি deprecated. ব্রাউজারেও একই কাজের জন্য TextEncoder/TextDecoder আছে, যা Node-এও চলে।

৬ · Stream — পুরোটা নয়, টুকরো টুকরো

৪ GB-র একটি access log থেকে জানতে চান কতগুলো request-এ 404 এসেছে। readFile দিয়ে পুরো ফাইল মেমরিতে তুললে সার্ভারের RAM ফুরিয়ে যেতে পারে; তা ছাড়া একটি string-এর সর্বোচ্চ দৈর্ঘ্যেরও সীমা আছে। অথচ কাজটার জন্য একসাথে একটি লাইনই যথেষ্ট। এই চিন্তা থেকেই stream: ডেটা আসে ছোট ছোট chunk-এ (fs.createReadStream-এ ডিফল্ট ৬৪ KiB), একটি প্রসেস হলে পরেরটা।

WASA-র পানির লাইন ভাবুন। পুরো শহরের পানি একটি বিশাল ট্যাংকে ভরে তারপর বাসায় পাঠানো হয় না — পাইপ দিয়ে অবিরাম আসে, আপনি যতটুকু দরকার কল খুলে নেন। মাঝে ফিল্টার বসানো যায়, পাইপ জোড়া দেওয়া যায়। আর আপনার বাসার ট্যাংক ভরে গেলে ভাল্‌ভ বন্ধ হয়, উৎস থেকে চাপ কমে — সেটিই backpressure.

📤 Readable

যেখান থেকে ডেটা আসে: fs.createReadStream, process.stdin, HTTP request-এর body. ব্রাউজারের ReadableStream-এর Node-রূপ।

উৎস

📥 Writable

যেখানে ডেটা যায়: fs.createWriteStream, process.stdout, HTTP response. একে বলে writable stream.

গন্তব্য

🔁 Duplex

দুই দিকেই — পড়াও যায়, লেখাও যায়, আর দুই দিক স্বাধীন। যেমন TCP socket. একে বলে duplex stream.

দুই মুখ

🧪 Transform

Duplex-এর বিশেষ রূপ: যা ঢোকে, বদলে বের হয়। gzip সংকোচন, encryption, লাইন ভাগ করা — transform stream.

ফিল্টার

প্রতিটি stream আসলে পর্ব ৩২-এ দেখা EventEmitter — "data" event-এ একটি chunk আসে, "end"-এ শেষ, "error"-এ ভুল। কিন্তু নিজে event ধরে stream জোড়া দেওয়া ঝামেলার, আর একটি জিনিস ভুলে গেলেই বিপদ: backpressure.

Backpressure — ধীর গন্তব্যের জন্য উৎসকে থামানো

ডিস্ক থেকে পড়া খুব দ্রুত, কিন্তু ধীর নেটওয়ার্কে মোবাইল গ্রাহকের কাছে পাঠানো ধীর। উৎস যদি না থেমে পড়তেই থাকে, পাঠাতে না পারা chunk জমতে থাকে মেমরিতে — ১ GB ফাইল হলে প্রায় ১ GB. Backpressure হলো গন্তব্যের “আমি ভরে গেছি, একটু থামো” সংকেত। প্রতিটি writable-এর একটি সীমা থাকে — highWaterMark; জমা ডেটা সেটি ছাড়ালে write() false ফেরত দেয়, উৎস থামে, আর জমা খালি হলে "drain" event-এ আবার চলে:

  1. Readableaccess.logদ্রুত ডিস্ক
  2. Transformgzipসংকোচন
  3. WritableHTTP responseধীর মোবাইল নেট
Buffer-এর অবস্থা (সীমা: প্রতিটিতে ৪টি chunk ধরা যাক)
Readable
Transform-এর জমা
Writable-এর জমা
মেমরিতে মোট
  1. পড়া শুরু। Readable ডিস্ক থেকে chunk পড়ছে — দ্রুত, কারণ ডিস্ক দ্রুত।
  2. Transform-এ ঢুকছে। প্রতিটি chunk সংকোচিত হয়ে সামনে যাচ্ছে। এখনো সব মসৃণ।
  3. গন্তব্য ধীর। মোবাইল গ্রাহকের নেটওয়ার্ক ধীর, Writable-এর জমা সীমায় পৌঁছাল। write() false ফেরত দিল — “আর না”।
  4. চাপ পেছনে যায়। Transform আর লিখতে পারছে না, তার নিজের জমাও ভরে গেল; সে-ও উৎসকে বলল থামতে। Readable ডিস্ক থেকে পড়া বন্ধ করল। মেমরি আর বাড়ছে না — এটাই backpressure-এর সুফল।
  5. গন্তব্য একটু খালি। নেটওয়ার্কে কয়েকটি chunk চলে গেল, Writable-এর জমা সীমার নিচে নামল আর "drain" event দিল।
  6. আবার চলা। সংকেত পেছনে গেল, Readable আবার পড়তে শুরু করল। পুরো প্রবাহ এখন চলছে সবচেয়ে ধীর অংশের গতিতে — যা ঠিক যেমন হওয়া উচিত।
  7. শেষ। ফাইল শেষ হলে end, সব জমা খালি হলে Writable-এর finish. ১ GB ফাইল পাঠাতে মেমরি লাগল মাত্র কয়েকটি chunk-এর সমান।

pipeline — জোড়া দিন, error সামলান, পরিষ্কার করুন

পুরোনো কোডে দেখবেন a.pipe(b).pipe(c). pipe backpressure সামলায়, কিন্তু error সামলায় না: মাঝের কোনো stream-এ error হলে বাকিগুলো বন্ধ হয় না, ফাইল খোলা থেকে যায়, আর error event কেউ না শুনলে পুরো process ক্র্যাশ করতে পারে। এর সমাধান node:stream/promises-এর pipeline: সব stream জোড়ে, যেকোনো একটিতে error হলে সবগুলো ধ্বংস (destroy) করে, আর একটি Promise ফেরত দেয় — যা শেষে fulfilled বা error-সহ rejected.

const fs = require("node:fs");
const zlib = require("node:zlib");
const { pipeline } = require("node:stream/promises");

async function compress(src) {
  await pipeline(
    fs.createReadStream(src),          // Readable
    zlib.createGzip(),                 // Transform
    fs.createWriteStream(src + ".gz")  // Writable
  );
  console.log("সংকোচন শেষ:", src + ".gz");
}
compress("access.log").catch((err) => {
  console.error("ব্যর্থ:", err.code ?? err.message);
  process.exitCode = 1;
});

তিন লাইনে একটি ফাইল যেকোনো আকারের হোক, কয়েক chunk মেমরিতে সংকোচিত হয়। access.log না থাকলে readable-এ ENOENT, আর pipeline সেটি rejected Promise হিসেবে দেয় — catch-এ ধরা পড়ে, ফাঁকা .gz ফাইলও আর খোলা থাকে না। fs.createReadStream ছাড়াও Readable.from(array) দিয়ে যেকোনো array বা async generator থেকে readable বানানো যায় — টেস্টে খুব কাজে লাগে।

বড় log ফাইল লাইন ধরে — readline

Chunk-এর সীমানা লাইনের সীমানা মানে না — একটি লাইন দুই chunk-এ ভাগ হয়ে যেতে পারে। লাইন ধরে পড়ার কাজটি node:readline করে দেয়। নিচে HTTP status code গোনার একটি log analyzer:

const fs = require("node:fs");
const readline = require("node:readline");

async function countStatus(file) {
  const rl = readline.createInterface({ input: fs.createReadStream(file), crlfDelay: Infinity });
  const counts = new Map();
  for await (const line of rl) {
    // 103.4.1.9 - - [19/Oct/2026:10:15:02 +0600] "GET /cart HTTP/1.1" 200 512
    const status = line.split(" ")[8];
    if (status) counts.set(status, (counts.get(status) ?? 0) + 1);
  }
  return counts;   // Map(3) { '200' => 9120, '404' => 311, '500' => 7 }
}

for await...of (পর্ব ২৭) প্রতিবার একটি লাইন দেয়, আর stream-এর backpressure নিজেই সামলায় — লুপের ভেতরের কাজ ধীর হলে পড়াও ধীর হয়। crlfDelay: Infinity দিলে Windows-এর \r\n লাইন-শেষও একটি লাইন-বিরতি হিসেবে ধরা হয়। ফাইল কত বড় তাতে মেমরির খরচ প্রায় বদলায় না। (মন্তব্যের সংখ্যাগুলো একটি উদাহরণ মাত্র।)

এবার নিজে চালান। এই উদাহরণে ইনপুট আসছে process.stdin থেকে (পাতার রানার কয়েকটি log লাইন stdin হিসেবে পাঠায়), একটি নিজের বানানো Transform লাইন ভাগ করে status গোনে আর সমস্যার লাইনগুলো বড় হাতের অক্ষরে সামনে পাঠায়, আর একটি Writable সেগুলো ছাপে। কোন status থেকে “সমস্যা” ধরা হবে, তা আসে argv, তারপর env, না পেলে ডিফল্ট ৪০০:

const { Transform, Writable } = require("node:stream");
const { pipeline } = require("node:stream/promises");

const min = Number(process.argv[2] ?? process.env.MIN_STATUS ?? 400);
const counts = {};
let rest = "";                     // দুই chunk-এ ভাগ হওয়া লাইনের টুকরো
function keep(line) {
  const status = line.split(" ")[2];
  if (!status) return false;
  counts[status] = (counts[status] ?? 0) + 1;
  return Number(status) >= min;
}
const problemsOnly = new Transform({
  readableObjectMode: true,
  transform(chunk, _enc, done) {
    const lines = (rest + chunk).split("\n");
    rest = lines.pop();                         // শেষ লাইন হয়তো অসম্পূর্ণ
    for (const line of lines) if (keep(line)) this.push(line.toUpperCase());
    done();
  },
  flush(done) { if (keep(rest)) this.push(rest.toUpperCase()); done(); },
});
const printer = new Writable({ objectMode: true, write(line, _enc, done) { console.log("  →", line); done(); } });
process.stdin.setEncoding("utf8");              // বাংলা অক্ষর chunk-এর সীমানায় ভাঙবে না
pipeline(process.stdin, problemsOnly, printer)
  .then(() => console.log("argv:", process.argv.slice(2), "min:", min, counts))
  .catch((err) => { console.error("pipeline ব্যর্থ:", err.message); process.exitCode = 1; });

আউটপুট: তিনটি সমস্যার লাইন — → GET /API/ORDERS/77 404, → POST /API/PAY 500, → GET /FAVICON.ICO 404 — তারপর argv: [] min: 400 { '200': 2, '201': 1, '404': 2, '500': 1 }. রানার কোনো argument দেয় না, তাই argv খালি, MIN_STATUS-ও নেই — ??-এর শিকল ডিফল্টে পৌঁছেছে। নিজের কম্পিউটারে node log-filter.js 500 < access.log চালালে শুধু 500-এর লাইন আসবে। rest variable-টি readline-এর কাজটাই হাতে করে দেখাচ্ছে: chunk-এর শেষের অসম্পূর্ণ লাইন পরের chunk-এর সাথে জোড়া লাগে, আর একেবারে শেষে flush বাকিটুকু সামলায়। setEncoding("utf8") না দিলে chunk আসে Buffer হিসেবে, আর rest + chunk করার সময় অংশ ৫-এর মতো একটি বাংলা অক্ষর দুই chunk-এ কেটে � হয়ে যেতে পারত। readableObjectMode থাকায় Transform প্রতিটি লাইন আলাদা একটি object হিসেবে সামনে পাঠায়, bytes-এর স্রোত হিসেবে নয়। আর error সামলানো আছে একটিই জায়গায় — pipeline-এর catch.

৭ · child_process আর os — অন্য প্রোগ্রাম চালানো, নিরাপদে

কখনো Node থেকে অন্য প্রোগ্রাম চালাতে হয় — git দিয়ে শেষ commit জানা, ffmpeg দিয়ে ভিডিও ছোট করা, ImageMagick দিয়ে ছবির আকার বদলানো। এর জন্য node:child_process. চারটি প্রধান function:

FunctionShell ব্যবহার করে?কখন
exec(command)হ্যাঁ — পুরো string shell-কে দেয়নিজের লেখা, স্থির কমান্ড; ছোট আউটপুট (সব একসাথে buffer হয়)
execFile(file, args)না (ডিফল্টে)নির্দিষ্ট প্রোগ্রাম, argument array হিসেবে — ব্যবহারকারীর ইনপুট থাকলে এটি
spawn(file, args)না (ডিফল্টে)লম্বা চলা প্রোগ্রাম, বড় আউটপুট — stdout একটি stream
fork(module)নাআরেকটি Node process, বার্তা আদান-প্রদানের চ্যানেলসহ

কখনো ব্যবহারকারীর ইনপুট shell-এ নয়। ধরুন একটি ছবি-রূপান্তর সেবা ফাইলের নাম নিয়ে লেখে exec(`convert ${name} out.png`). কেউ নাম পাঠাল a.jpg; rm -rf ~ — shell ;-কে দুই কমান্ডের বিভাজক ধরে, তাই দ্বিতীয়টিও চলবে। একে বলে command injection, OWASP-এর injection শ্রেণির সবচেয়ে বিপজ্জনক রূপগুলোর একটি। এড়ানোর উপায় escape করার চেষ্টা নয় — shell-কেই বাদ দেওয়া: execFile("convert", [name, "out.png"]). এখানে name যা-ই হোক, প্রোগ্রামের কাছে সেটি একটিই argument হিসেবে যায়, কোনো কমান্ড হিসেবে নয়। তার ওপর ইনপুট যাচাই করুন (যেমন শুধু অনুমোদিত extension), আর - দিয়ে শুরু হওয়া নাম যেন option হিসেবে না পড়ে, সেদিকেও খেয়াল রাখুন।

const { execFile } = require("node:child_process");
const { promisify } = require("node:util");
const run = promisify(execFile);

async function lastCommit(branch) {
  if (!/^[\w./-]+$/.test(branch)) throw new Error("অবৈধ branch নাম");
  const { stdout } = await run("git", ["log", "-1", "--format=%h %s", branch]);
  return stdout.trim();   // যেমন: 'a1b2c3d বাংলা ইনভয়েস যোগ'
}

util.promisify callback-ধাঁচের execFile-কে Promise ফেরত দেওয়া function বানায়, যাতে await লেখা যায়। Argument গেছে array হিসেবে, আর তার আগে একটি সহজ allowlist যাচাই। প্রোগ্রামটি অশূন্য exit code দিয়ে শেষ হলে Promise reject হয়, আর error-এ code, stdout, stderr থাকে। Windows-এ .bat/.cmd ফাইল shell ছাড়া চলে না — সেখানে আরও সাবধান থাকতে হয়।

os — যে যন্ত্রে চলছে তার খবর

node:os জানায় প্রোগ্রাম কোন যন্ত্রে চলছে: os.platform() ("win32", "linux", "darwin"), os.availableParallelism() (কতগুলো কাজ একসাথে চালানো উচিত — worker pool-এর আকার ঠিক করতে), os.totalmem()/os.freemem(), os.homedir(), os.tmpdir() (অস্থায়ী ফাইলের নিরাপদ জায়গা), আর os.EOL (লাইন-শেষ: Windows-এ \r\n, বাকিদের \n). সাধারণ নিয়ম: OS অনুমান করে কোড না লিখে এই module থেকে জেনে নিন।

৮ · নিজের CLI tool — দৈনিক বিক্রির রিপোর্ট

এতক্ষণের সব টুকরো জুড়ে একটি ছোট CLI (command-line interface) tool বানানো যাক: sales-report orders.csv -o report.json. একটি ভালো CLI-এর চারটি গুণ — সাহায্য দেখায় (--help), argument ভুল হলে পরিষ্কার বার্তা দেয়, ভুল stderr-এ লেখে, আর ঠিক exit code দেয়।

#!/usr/bin/env node
const { parseArgs } = require("node:util");
const fs = require("node:fs/promises");

const { values, positionals } = parseArgs({
  allowPositionals: true,
  options: {
    out:  { type: "string",  short: "o", default: "report.json" },
    help: { type: "boolean", short: "h" },
  },
});
if (values.help || positionals.length !== 1) {
  console.log("ব্যবহার: sales-report <orders.csv> [-o report.json]");
  process.exit(values.help ? 0 : 2);        // 2 = ভুল ব্যবহার (প্রচলিত রীতি)
}
buildReport(positionals[0], values.out).catch((err) => {
  console.error(`ভুল: ${err.code === "ENOENT" ? "ফাইল পাওয়া যায়নি" : err.message}`);
  process.exitCode = 1;
});

প্রথম লাইনটি shebang: Linux/macOS-এ ফাইলটিকে সরাসরি চালালে (./sales-report.js) OS এই লাইন দেখে বোঝে কোন প্রোগ্রাম দিয়ে চালাতে হবে; /usr/bin/env node PATH-এ যেখানে node আছে, সেটিই খুঁজে নেয়। util.parseArgs (Node 18.3-এ যুক্ত, Node 20-এ স্থিতিশীল) argv থেকে option আর positional argument আলাদা করে, অচেনা option পেলে নিজেই error দেয় — ছোট tool-এর জন্য আর কোনো package লাগে না; বড় tool-এ commander বা yargs জনপ্রিয়। buildReport হলো অংশ ৪-এর runnable কোডের মতো একটি function. help-এর ক্ষেত্রে process.exit ঠিক আছে, কারণ তখনো কোনো async কাজ শুরু হয়নি।

এবার tool-টিকে কমান্ড হিসেবে ইনস্টলযোগ্য করতে package.json-এ একটি "bin" ঘর যোগ করুন — { "bin": { "sales-report": "./bin/sales-report.js" } }. তারপর প্রজেক্ট ফোল্ডারে npm link (নিজের যন্ত্রে পরীক্ষা) বা npm-এ প্রকাশ করে npm install -g/npx sales-report. Windows-এ shebang কাজ করে না, কিন্তু npm ইনস্টলের সময় নিজেই একটি ছোট .cmd wrapper বানিয়ে দেয়। npm আর package.json-এর বিস্তারিত পর্ব ৪৬-এ।

আরও একটি ছোট tool: ছবির ফোল্ডারের নাম বদলানো (ধারণা)

কক্সবাজার ভ্রমণের ৩০০টি ছবি — নাম IMG_4821.JPG, IMG_4822.jpeg… চান coxs-bazar-001.jpg. পরিকল্পনা: readdir → extname(name).toLowerCase() দিয়ে শুধু .jpg/.jpeg বাছাই → নাম ধরে সাজানো → String(i + 1).padStart(3, "0") দিয়ে নম্বর → rename(path.join(dir, old), path.join(dir, neu)). দুটি পেশাদার অভ্যাস যোগ করুন: --dry-run option, যা শুধু “কী হবে” ছাপে, কিছু বদলায় না; আর নতুন নামের ফাইল আগে থেকে থাকলে থেমে যাওয়া — নইলে একটি rename আরেকটি ছবি মুছে দিতে পারে।

util — inspect আর বাকি সাহায্যকারী

console.log object ছাপার সময় ভেতরে util.inspect ব্যবহার করে — সেজন্যই তৃতীয় স্তরের পরের object [Object] দেখায়। পুরোটা দেখতে util.inspect(obj, { depth: null }) বা console.dir(obj, { depth: null }). util.promisify আগেই দেখেছেন; util.types.isPromise(x)-এর মতো type-যাচাইও এখানে।

Event loop খালি রাখুন — CPU-ভারী কাজ অন্য thread-এ

Async API ফাইল আর নেটওয়ার্কের অপেক্ষা সরিয়ে দেয়, কিন্তু হিসাব সরায় না। ৫ লাখ সারির CSV-র পরিসংখ্যান, ছবির আকার বদলানো, বড় JSON parse — এগুলো CPU-bound, চলে main thread-এই, আর চলার সময় সব request আটকে থাকে (blocking). সমাধান পর্ব ৪২-এ দেখা ধারণার Node-রূপ: worker_threads দিয়ে আলাদা thread-এ পাঠানো, বা কাজটা আলাদা process/queue-তে সরানো। নিয়মটা মনে রাখার সহজ উপায়: একটি request handler কয়েক মিলিসেকেন্ডের বেশি একটানা হিসাব করলে সেটি সন্দেহজনক।

Node-এর version, আর Deno ও Bun

Node বছরে দুটি major version প্রকাশ করে আসছে। এতদিনের নিয়মে জোড় সংখ্যার version (18, 20, 22, 24…) অক্টোবরের দিকে LTS হয় আর মোট প্রায় ৩০ মাস নিরাপত্তা আপডেট পায়; বিজোড় version (21, 23…) কয়েক মাসের “Current” থাকে, LTS হয় না। Production-এ সবসময় সমর্থন চলছে এমন একটি LTS নিন। এই পাতার রানার Node 20 ব্যবহার করে — উদাহরণ শেখার জন্য যথেষ্ট, কিন্তু Node 20-এর সমর্থন ২০২৬ সালের এপ্রিলে শেষ হয়েছে, তাই নতুন প্রজেক্টে নতুন LTS নিন। Release-এর নিয়ম মাঝে মাঝে বদলায়, তাই সিদ্ধান্তের আগে nodejs.org-এর release পাতা দেখে নিন।

Node একমাত্র server-side JavaScript runtime নয়। Deno (Node-এর স্রষ্টা Ryan Dahl-এর দ্বিতীয় প্রচেষ্টা, ১.০ এসেছিল ২০২০ সালে) ডিফল্টে ফাইল বা নেটওয়ার্কে প্রবেশাধিকার দেয় না — --allow-read-এর মতো অনুমতি চাইতে হয় — আর TypeScript সরাসরি চালায়। Bun (১.০ ২০২৩ সালে) V8-এর বদলে Safari-র JavaScriptCore engine ব্যবহার করে, Zig ভাষায় লেখা, আর runtime, package manager, bundler ও test runner এক সাথে দেয়। দুটিই এখন বেশিরভাগ Node API আর npm package চালাতে পারে। তবে ecosystem, hosting আর চাকরির বাজারে Node-ই এখনো প্রধান — তাই এখানে যা শিখলেন, তা তিন জায়গাতেই কাজে লাগবে।

C/C++/Python/Java জানলে

C-তে fopen/fread/fclose দিয়ে নিজে buffer সামলাতে হয়, আর ভুল ধরতে হয় errno দেখে — Node-এর ENOENT, EACCES সেই একই errno নাম (C-তে ফাইল I/O). Python-এর pathlib.Path আর open() পরিচিত লাগবে, তবে সেগুলো ডিফল্টে blocking — Node-এ ডিফল্ট পথ async (Python-এ ফাইল I/O). Java-র java.nio.file.Files আর InputStream/OutputStream ঠিক Node-এর stream-এর মতো টুকরো ধরে পড়ার চিন্তা (Java-র ফাইল I/O আর NIO); শুধু Java-র Stream API (list.stream()) সম্পূর্ণ আলাদা জিনিস, নাম মিললেও।

পরীক্ষা ও ইন্টারভিউতে
  • Node.js single-threaded হয়েও একসাথে অনেক request কীভাবে সামলায়? — JavaScript একটি thread-এ চলে, কিন্তু I/O-এর অপেক্ষা করে libuv (OS-এর evented I/O আর thread pool); অপেক্ষার সময় thread অন্য request নেয়।
  • readFile বনাম createReadStream? — প্রথমটি পুরো ফাইল মেমরিতে তোলে; দ্বিতীয়টি chunk ধরে দেয়, backpressure-সহ — বড় ফাইলে stream.
  • Backpressure কী? — গন্তব্য ধীর হলে write() false দিয়ে উৎসকে থামানো, drain-এ আবার চালানো — যাতে মেমরিতে ডেটা না জমে।
  • pipe বনাম pipeline? — দুটোই backpressure সামলায়; pipeline error হলে সব stream বন্ধ করে আর callback/Promise-এ error দেয়।
  • exec বনাম execFile/spawn? — exec shell চালায় (ইনপুট থাকলে command injection-এর ঝুঁকি); execFile/spawn argument array নেয়, shell ছাড়া।

৯ · AI-এর যুগে — Node কোডে AI-এর পরিচিত ভুল

“একটি Express route লিখে দাও যা ফাইল পড়ে” — AI কয়েক সেকেন্ডে কোড দেয়, আর বেশিরভাগ সময় তা চলেও। কিন্তু “চলে” আর “একশো জন একসাথে ব্যবহার করলে, Windows-এ, বড় ফাইলে, খারাপ ইনপুটে চলে” — এক কথা নয়। AI-এর লেখা Node কোডে এই ভুলগুলো বারবার দেখা যায়:

AI যা লেখেসমস্যাঠিক রূপ
Request handler-এ readFileSyncপ্রতিটি request-এ পুরো সার্ভার থামেawait fs.promises.readFile, বা শুরুতে একবার পড়ে cache
exec(`ffmpeg -i ${req.query.file} ...`)Command injection — ইনপুট shell-এexecFile("ffmpeg", ["-i", file, ...]) + allowlist যাচাই
a.pipe(b).pipe(res), কোনো error handler নেইError-এ ফাইল খোলা থাকে, process ক্র্যাশ করতে পারেawait pipeline(a, b, res) + try/catch
"uploads/" + name বা dir + "\\" + nameOS-ভেদে ভাঙে; ../ দিয়ে ফোল্ডারের বাইরে যাওয়া যায়path.join, তারপর ফল নির্দিষ্ট ফোল্ডারের ভেতরে কি না যাচাই
বড় CSV/log-এ readFile + split("\n")পুরো ফাইল মেমরিতে, বড় হলে ক্র্যাশcreateReadStream + readline
catch {} বা সব ভুলে ডিফল্ট মানEACCES, ভাঙা JSON চাপা পড়েerr.code দেখে শুধু প্রত্যাশিতটি সামলানো
কোডে সরাসরি API keyGit-এ গিয়ে ফাঁসprocess.env + .env (gitignore-এ)

AI-কে Node.js কোড লিখতে দিলে নিয়মগুলো আগেই বলে দিন — পরে খুঁজে ঠিক করার চেয়ে সহজ:

Node.js 24 LTS-এ (CommonJS / ES module — যেটি প্রযোজ্য) নিচের কাজের কোড লিখে দাও: [কাজের বর্ণনা — যেমন: uploads ফোল্ডারের ছবির তালিকা দেওয়া একটি HTTP route] নিয়ম: ১. ফাইলের কাজে শুধু node:fs/promises; request-এর পথে কোনো ...Sync function নয়। ২. সব পথ path.join/path.resolve দিয়ে; ব্যবহারকারীর দেওয়া নাম থাকলে ফল নির্দিষ্ট ফোল্ডারের ভেতরে কি না যাচাই করো (../ ঠেকাতে)। ৩. ১০ MB-এর বেশি হতে পারে এমন ফাইল stream দিয়ে, আর stream জোড়া দাও stream/promises-এর pipeline দিয়ে — .pipe() নয়। ৪. অন্য প্রোগ্রাম চালাতে হলে execFile/spawn, argument array-তে; exec বা shell: true নয়। ৫. ভুল সামলাও err.code দেখে (ENOENT, EACCES, EEXIST); অপ্রত্যাশিত ভুল চেপে রেখো না। ভুলের বার্তা stderr-এ, ব্যর্থ হলে process.exitCode = 1. ৬. গোপন মান শুধু process.env থেকে। ৭. কোডের পরে বলো: কোন লাইন Windows আর Linux-এ আলাদা আচরণ করতে পারে, আর কোথায় event loop আটকানোর ঝুঁকি আছে।

AI-এর Node কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।

প্র ০১২০০৯ সালে Ryan Dahl যখন Node.js দেখালেন, তখন সার্ভার লেখার ভাষার অভাব ছিল না। তবু কেন JavaScript, আর কেন “সব I/O non-blocking” — এত বড় একটি দাবি?

২০০৯ সালের নভেম্বরে বার্লিনে JSConf EU-তে Ryan Dahl প্রথম বড় আকারে Node.js দেখান। তার বক্তৃতার কেন্দ্রে ছিল একটি সহজ পর্যবেক্ষণ: একটি সাধারণ ওয়েব সার্ভার তার বেশিরভাগ সময় কিছুই করে না — ডেটাবেসের উত্তর, ডিস্ক বা ধীর ক্লায়েন্টের অপেক্ষায় বসে থাকে। তিনি CPU cycle-এ মাপা একটি তালিকা দেখান: cache থেকে পড়তে লাগে কয়েকটি cycle, আর ডিস্ক বা নেটওয়ার্ক থেকে কয়েক কোটি। অথচ প্রচলিত কোড — result = db.query(...) — সেই পুরো সময় thread-কে আটকে রাখে।

এর আগের দশকে Dan Kegel-এর “C10K” প্রবন্ধ (১৯৯৯) প্রশ্ন তুলেছিল: একটি সার্ভার কি দশ হাজার সংযোগ একসাথে ধরে রাখতে পারে? প্রতি সংযোগে একটি thread হলে উত্তর ছিল “কঠিন” — প্রতিটি thread-এর stack মেমরি নেয়, আর হাজার thread-এর মধ্যে বারবার context switch-এ CPU খরচ হয়। nginx-এর মতো event-driven সার্ভার দেখিয়েছিল একটি thread আর OS-এর epoll/kqueue দিয়ে এটি সম্ভব। কিন্তু C-তে এমন কোড লেখা কঠিন, আর Python (Twisted) বা Ruby (EventMachine)-তে সমস্যা ছিল ecosystem: যেকোনো একটি library ভুল করে blocking কল করলেই পুরো loop আটকে যেত।

Dahl-এর অন্তর্দৃষ্টি ছিল: JavaScript-এর কোনো blocking I/O-এর ঐতিহ্যই নেই। ব্রাউজারে সবকিছু আগে থেকেই event আর callback — ক্লিক, timer, XMLHttpRequest. তাই একটি সম্পূর্ণ নতুন ecosystem শুরু থেকেই non-blocking হতে পারে। আর ঠিক তখন Google-এর V8 (২০০৮) JavaScript-কে আগের চেয়ে বহুগুণ দ্রুত করে দিয়েছিল। ফলে প্রথম দিকের Node-এর API-তে প্রায় সব I/O ছিল async; sync রূপগুলো রাখা হয়েছিল শুধু সুবিধার জন্য।

দামও ছিল: callback-এর পর callback (পর্ব ৩২-এর callback hell), আর CPU-ভারী কাজে কোনো সাহায্য নেই। পরে Promise, async/await, worker_threads সেই ফাঁকগুলো ভরেছে।

মূল উপলব্ধি: Node.js কোনো নতুন কৌশল আবিষ্কার করেনি — evented I/O আগেও ছিল। নতুন ছিল একটি ভাষা আর ecosystem বেছে নেওয়া, যেখানে সেই কৌশলই স্বাভাবিক পথ। প্রযুক্তি জনপ্রিয় হয় যখন সঠিক কাজটি সহজতম কাজও হয়ে যায়।

প্র ০২২০১৪ সালে Node.js ভেঙে io.js নামে আলাদা প্রকল্প হলো, আর এক বছরের মধ্যে আবার এক হলো। একটি open-source প্রকল্পের মালিকানা আর পরিচালনা (governance) কেন প্রযুক্তির মতোই গুরুত্বপূর্ণ?

Node.js-এর শুরুর বছরগুলোতে প্রকল্পটির পৃষ্ঠপোষক ছিল Joyent নামের একটি ক্লাউড কোম্পানি — তারা Ryan Dahl-কে নিয়োগ দিয়েছিল, আর “Node.js” নাম ও ট্রেডমার্ক তাদের হাতে ছিল। ২০১২ সালে Dahl নিজে প্রকল্পের নেতৃত্ব ছেড়ে দেন। এরপর কয়েক বছরে মূল অবদানকারীদের মধ্যে অসন্তোষ জমে: নতুন version আসছিল খুব ধীরে, নতুন V8 আর নতুন JavaScript বৈশিষ্ট্য (ES6) যোগ হচ্ছিল না, আর সিদ্ধান্ত নিচ্ছিল একটি কোম্পানি, সম্প্রদায় নয়।

২০১৪ সালের ডিসেম্বরে সবচেয়ে সক্রিয় কয়েকজন অবদানকারী প্রকল্পটি fork করে নাম দেন io.js — একই কোড, কিন্তু খোলা পরিচালনা: একটি কারিগরি কমিটি, নিয়মিত release, আর নতুন V8 দ্রুত গ্রহণ। ২০১৫ সালের জানুয়ারিতে io.js 1.0 আসে, এবং কয়েক মাসের মধ্যে অনেক সক্রিয় কাজ সেখানে সরে যায়। ব্যবহারকারীরা পড়েন দ্বিধায় — কোনটা ব্যবহার করব, package কোনটায় চলবে?

সমাধান এল আলোচনায়। ২০১৫ সালে Linux Foundation-এর ছাতার নিচে নিরপেক্ষ Node.js Foundation গঠিত হয়, Joyent ট্রেডমার্ক সেখানে দেয়, আর io.js-এর পরিচালনা-মডেল গ্রহণ করা হয়। সেপ্টেম্বর ২০১৫-এ Node.js 4.0 দুই ধারা মিলিয়ে প্রকাশিত হয় — io.js-এর version সংখ্যা ধরে, তাই Node 0.12 থেকে সরাসরি 4. সেখান থেকেই আজকের LTS আর নিয়মিত release-সূচির শুরু। ২০১৯ সালে Node.js Foundation আর JS Foundation মিলে OpenJS Foundation হয়।

এই ঘটনা দেখায়, fork open-source-এর একটি “নিরাপত্তা ভাল্‌ভ”: কোড যেহেতু সবার, পরিচালনা খারাপ হলে সম্প্রদায় চলে যেতে পারে — আর সেই সম্ভাবনাই মালিককে আলোচনায় আনে। আবার ভাঙনের দামও বাস্তব: দ্বিধা, দ্বিগুণ কাজ, বিভক্ত ecosystem.

মূল উপলব্ধি: একটি প্রযুক্তি বেছে নেওয়ার সময় শুধু কোড নয়, তার পেছনের মানুষ ও নিয়মও দেখুন — কে সিদ্ধান্ত নেয়, release কত নিয়মিত, একটি কোম্পানি সরে গেলে কী হবে। দীর্ঘমেয়াদি প্রজেক্টে এগুলো performance-এর চেয়ে কম গুরুত্বপূর্ণ নয়।

প্র ০৩Ryan Dahl নিজেই ২০১৮ সালে “10 Things I Regret About Node.js” নামে বক্তৃতা দিয়ে Deno ঘোষণা করেন। নিজের তৈরি জিনিসের ভুল প্রকাশ্যে স্বীকার করা থেকে আমরা কী শিখতে পারি?

২০১৮ সালের জুনে, আবার JSConf EU-তে, Dahl যে বক্তৃতা দেন তা প্রযুক্তি-জগতে বিরল: একজন স্রষ্টা তার সবচেয়ে সফল সৃষ্টির নকশাগত ভুলের তালিকা দিচ্ছেন। তালিকার কয়েকটি এই পর্বের সাথে সরাসরি যুক্ত।

Promise সরিয়ে দেওয়া: ২০০৯–১০ সালে Node-এ Promise-এর একটি আদি রূপ যোগ করে আবার সরানো হয়েছিল; ফলে পুরো API callback-এ দাঁড়াল, আর পরে fs/promises-এর মতো দ্বিতীয় API বানাতে হলো — আজও প্রতিটি module-এ দুই-তিনটি রূপ তারই চিহ্ন। নিরাপত্তা: V8 ব্রাউজারে একটি চমৎকার sandbox, কিন্তু Node যেকোনো script-কে ফাইল, নেটওয়ার্ক, process — সব দিয়ে দেয়; একটি linter package-ও আপনার SSH key পড়তে পারে। package.json আর node_modules: module খোঁজার জটিল নিয়ম, প্রতিটি প্রজেক্টে বিশাল ফোল্ডার, আর একটি কেন্দ্রীয় registry-র ওপর নির্ভরতা। এ ছাড়া require("./x")-এ extension বাদ দেওয়ার সুবিধা, আর index.js-এর বিশেষ নিয়ম — যা module খোঁজাকে অকারণে জটিল করেছে বলে তিনি মনে করেন।

এর জবাব হিসেবে তিনি Deno শুরু করেন: ডিফল্টে কোনো অনুমতি নেই (--allow-read, --allow-net দিয়ে চাইতে হয়), শুরু থেকে Promise আর ES module, TypeScript সরাসরি, আর ব্রাউজারের মতো web API (fetch). ২০২০ সালে Deno 1.0 আসে। মজার ব্যাপার, পরে Deno-ও বাস্তবতা মেনে npm package চালানোর সুবিধা যোগ করেছে, আর Node-ও Deno থেকে শিখেছে — global fetch, web stream, এমনকি একটি permission model (Node 20-এ পরীক্ষামূলক, পরের version-গুলোতে স্থিতিশীল).

ভুল স্বীকারের এই সংস্কৃতি প্রকৌশলের একটি মূল্যবান অভ্যাসের সাথে মেলে — “blameless postmortem”: কী ভুল হয়েছে, কেন সেই সময়ে সিদ্ধান্তটি যুক্তিসঙ্গত মনে হয়েছিল, আর পরের বার কী আলাদা করব।

মূল উপলব্ধি: প্রতিটি নকশা তার সময়ের সীমাবদ্ধতার ফল, আর একবার লাখো মানুষ ব্যবহার শুরু করলে ভুল ঠিক করা প্রায় অসম্ভব — পুরোনো কোড ভেঙে যায়। তাই API-র প্রথম সংস্করণ খুব ভেবেচিন্তে বানান; আর পুরোনো সিদ্ধান্তের সমালোচনা করার আগে বুঝুন সেটি কেন নেওয়া হয়েছিল।

প্র ০৪Node-এর stream-এর চিন্তা ১৯৬০–৭০-এর দশকের Unix pipe থেকে এসেছে। “ছোট প্রোগ্রাম জোড়া দিয়ে বড় কাজ” — এই দর্শন আজও কেন এত শক্তিশালী?

১৯৬৪ সালে Bell Labs-এর Doug McIlroy একটি অভ্যন্তরীণ নোটে লেখেন — মোটামুটি এমন: বাগানের পানির পাইপের মতো প্রোগ্রাম জোড়া দেওয়ার উপায় থাকা উচিত; ডেটাকে অন্যভাবে বদলাতে হলে শুধু আরেকটি টুকরো পাইপ জুড়ে দেবেন। প্রায় এক দশক পর, ১৯৭৩ সালে, Ken Thompson এক রাতে Unix-এ pipe() system call আর shell-এ | চিহ্ন যোগ করেন। হঠাৎ করেই cat access.log | grep " 404 " | wc -l-এর মতো কমান্ড সম্ভব হলো — তিনটি ছোট প্রোগ্রাম, কেউ কাউকে চেনে না, তবু একসাথে একটি নতুন কাজ।

এর থেকে জন্ম নেয় Unix দর্শন, যা McIlroy পরে এভাবে গুছিয়ে বলেন: এমন প্রোগ্রাম লিখুন যা একটি কাজ করে এবং ভালোভাবে করে; এমন প্রোগ্রাম লিখুন যা একসাথে কাজ করে; আর text stream ব্যবহার করুন, কারণ সেটিই সর্বজনীন ইন্টারফেস। Pipe-এর আরেকটি নীরব গুণ ছিল: OS-এর pipe buffer ভরে গেলে লেখক প্রোগ্রাম আপনা থেকে থেমে যায় — অর্থাৎ backpressure শুরু থেকেই ছিল, কাউকে আলাদা করে ভাবতে হয়নি।

Node-এর stream ঠিক এই চিন্তা প্রোগ্রামের ভেতরে নিয়ে এসেছে: Readable, Transform, Writable — যেকোনো উৎস, যেকোনো ফিল্টার, যেকোনো গন্তব্য একই ইন্টারফেসে জোড়া যায়। fs.createReadStream → zlib.createGzip() → HTTP response — তিনটি আলাদা দলের লেখা কোড, একটি সাধারণ চুক্তি। Node-এর প্রথম দিকের অনেক নেতা (যেমন substack নামে পরিচিত James Halliday) এই “ছোট module, stream দিয়ে জোড়া” দর্শনের জোরালো প্রচারক ছিলেন, আর npm-এর লাখো ছোট package সেই সংস্কৃতিরই ফল — ভালো-মন্দ দুই অর্থেই।

আজকের বড় সিস্টেমেও একই চিন্তা: Kafka-র মতো event stream, data pipeline, এমনকি Linux-এর প্রতিদিনের শত শত shell script.

মূল উপলব্ধি: ভালো ইন্টারফেস ভালো অ্যালগরিদমের চেয়ে বেশি দিন বাঁচে। একটি সাধারণ, সরল চুক্তি (bytes আসে, bytes যায়, ভরে গেলে থামো) এমন সব ব্যবহার সম্ভব করে, যা নকশাকারীরা কখনো কল্পনাও করেননি।

প্র ০৫“JavaScript everywhere” — ব্রাউজার, সার্ভার, মোবাইল, এমনকি ডেস্কটপে একই ভাষা। Isomorphic বা universal JavaScript-এর লাভ কী, আর কোথায় এর সীমা?

Server-side JavaScript-এর ধারণা আসলে Node-এর চেয়ে পুরোনো: ১৯৯০-এর দশকেই Netscape তাদের ওয়েব সার্ভারে (LiveWire) JavaScript চালানোর ব্যবস্থা রেখেছিল। কিন্তু তখনকার ধীর engine আর ছোট ecosystem-এ তা টেকেনি। Node আর npm সেই ছবি বদলে দেয়। ২০১১–১৩ সালের দিকে “isomorphic JavaScript” শব্দটি জনপ্রিয় হয় — Airbnb-এর প্রকৌশলীরা বিশেষ করে এ নিয়ে লেখেন: একই কোড সার্ভারে চালিয়ে পাতা তৈরি করে পাঠানো (দ্রুত প্রথম দর্শন, সার্চ ইঞ্জিনের জন্য ভালো), তারপর ব্রাউজারে সেই কোডই পাতাকে ইন্টারঅ্যাকটিভ করে। ২০১৫ সালের দিকে অনেকে আরও সরল নাম প্রস্তাব করেন: “universal JavaScript”. প্রায় একই সময়ে MEAN stack (MongoDB, Express, Angular, Node) “পুরোটাই JavaScript”-কে জনপ্রিয় করে।

লাভগুলো বাস্তব। ছোট দল — বাংলাদেশের অনেক স্টার্টআপের মতো — একটি ভাষা জেনেই front-end আর back-end দুটো সামলাতে পারে। যাচাইয়ের নিয়ম (যেমন ফোন নম্বর বৈধ কি না) একবার লিখে ব্রাউজার আর সার্ভারে দুই জায়গায় ব্যবহার করা যায় — পর্ব ৩৯-এ দেখেছেন দুই জায়গাতেই যাচাই লাগে। Type (পর্ব ৪৭-এর TypeScript) API-র দুই প্রান্তে ভাগ করা যায়। আজকের Next.js, Nuxt, SvelteKit-এর মতো framework এই চিন্তার ওপরই দাঁড়িয়ে।

কিন্তু সীমাও আছে, আর এই পর্বেই তার কয়েকটি দেখলেন। ব্রাউজার আর Node-এর global আলাদা — window, document, localStorage সার্ভারে নেই; fs, process ব্রাউজারে নেই। “একই কোড দুই জায়গায়” লিখতে গিয়ে প্রায়ই typeof window !== "undefined" জাতীয় শর্তে কোড ভরে যায়। সবচেয়ে বিপজ্জনক ভুল: সার্ভারের গোপন জিনিস (API key, ডেটাবেসের তথ্য) ভুল করে ব্রাউজারের bundle-এ চলে যাওয়া। আর “একটি ভাষা” মানে “একই দক্ষতা” নয় — DOM-এর অ্যানিমেশন আর ডেটাবেস transaction সম্পূর্ণ আলাদা জ্ঞান।

মূল উপলব্ধি: একই ভাষা ঘর্ষণ কমায়, কিন্তু পরিবেশের পার্থক্য মুছে দেয় না। কোড কোথায় চলবে — ব্রাউজার, সার্ভার, নাকি দুই জায়গায় — প্রতিটি ফাইলের জন্য সচেতনভাবে ঠিক করুন, বিশেষ করে গোপন তথ্য কোন সীমানা পার হচ্ছে।

অনুশীলন

প্রথমে নিজে ভাবুন বা চালিয়ে দেখুন, তারপর উত্তর মিলিয়ে নিন।

  1. Bytes গুনুন: চালানোর আগে অনুমান করুন — "৳ ১০০".length আর Buffer.byteLength("৳ ১০০") কত? তারপর REPL-এ মিলিয়ে দেখুন। একটি API-র নিয়ম “নাম সর্বোচ্চ ৩০ bytes” — বাংলায় মোটামুটি কয়টি অক্ষর ধরবে?

    length = ৫ (৳, space, ১, ০, ০)। Bytes = ১৩: ৳ (U+09F3) আর তিনটি বাংলা অঙ্ক প্রতিটি ৩ byte — ১২, সাথে space ১ byte. তাই ৩০ bytes-এ বাংলা অক্ষর ধরবে মাত্র ১০টির মতো (কার-চিহ্নও আলাদা অক্ষর হিসেবে গোনা হয়, তাই মানুষের চোখে আরও কম)। সীমা যাচাইয়ে name.length <= 30 লিখলে বাংলা নামে ভুল হবে; লিখতে হবে Buffer.byteLength(name) <= 30, আর কাটতে হলে bytes-এর মাঝখানে নয়, পূর্ণ অক্ষরের সীমানায়।

  2. বাগ খুঁজুন: একজন লিখেছেন app.get("/download", (req, res) => { const data = fs.readFileSync("uploads/" + req.query.name); res.send(data); });. এখানে অন্তত তিনটি সমস্যা খুঁজে বের করুন।

    (১) readFileSync request-এর পথে — বড় ফাইলে পুরো সার্ভার থামে। (২) পুরো ফাইল মেমরিতে — ১ GB ফাইল চাইলে ১ GB RAM; ঠিক উপায় pipeline(fs.createReadStream(p), res). (৩) Path traversal — name=../.env পাঠালে uploads-এর বাইরের গোপন ফাইল চলে যাবে। সমাধান: const p = path.resolve("uploads", name), তারপর p path.resolve("uploads") + path.sep দিয়ে শুরু হয় কি না যাচাই। (৪) বোনাস: কোনো error handling নেই — ফাইল না থাকলে ENOENT-এ 404 না দিয়ে সার্ভার 500 দেবে। আর "uploads/" হাতে লেখা পথ, যদিও এখানে Windows-এও / চলে যায়।

  3. Exit code পরীক্ষা: একটি ফাইলে process.exitCode = 3; console.log("শেষ"); লিখে চালান, তারপর Linux/macOS-এ echo $? (PowerShell-এ $LASTEXITCODE) দেখুন। এবার ফাইলে JSON.parse("{"); যোগ করে আবার দেখুন। কী পেলেন, কেন?

    প্রথমবার “শেষ” ছাপা হয়ে exit code 3 — Node স্বাভাবিকভাবে শেষ হয়ে সেট করা code জানিয়েছে। দ্বিতীয়বার JSON.parse একটি SyntaxError ছোড়ে যা কেউ ধরেনি — uncaught exception. Node stderr-এ stack trace ছাপে আর exit code হয় 1, আপনার সেট করা ৩ নয়। CI বা cron এই code দেখেই বোঝে কাজ ব্যর্থ। তাই নিজের CLI-তে প্রত্যাশিত ভুল নিজে ধরুন, পরিষ্কার বার্তা দিন, আর অর্থপূর্ণ code সেট করুন।

  4. নকশা করুন (কোড লিখে): অংশ ৬-এর runnable উদাহরণটি বদলে লিখুন, যাতে stdin-এর বদলে Readable.from([...]) দিয়ে একটি array থেকে লাইন আসে, আর সমস্যার লাইনগুলো fs.createWriteStream("problems.log")-এ লেখা হয়। শেষে readFile দিয়ে ফাইলটি পড়ে ছাপুন।

    মূল বদল তিনটি। উৎস: Readable.from(["GET / 200\n", "POST /pay 500\n"]). Transform-এ readableObjectMode বাদ দিন আর this.push(line + "\n") করুন, কারণ ফাইলে bytes লিখতে হবে, object নয়। গন্তব্য: fs.createWriteStream("problems.log"). তারপর await pipeline(source, problemsOnly, out); console.log(await fsp.readFile("problems.log", "utf8")); — pipeline-এর Promise পূর্ণ হওয়া মানে write stream-এর finish হয়ে গেছে, তাই ফাইল সম্পূর্ণ লেখা। pipeline-এর আগে পড়তে গেলে ফাইল খালি বা অর্ধেক পেতেন — এটি একটি প্রচলিত বাগ।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমএকটি Node.js সার্ভারের graceful shutdown-এর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 🛑 OS থেকে SIGINT বা SIGTERM signal আসে
  2. 🚪 server.close() — নতুন সংযোগ নেওয়া বন্ধ
  3. ⏳ চলমান request-গুলো শেষ হয়
  4. 🗄️ ডেটাবেস ও অন্যান্য সংযোগ বন্ধ করা হয়
  5. ✅ Event loop খালি হয়ে process exit code 0 দিয়ে শেষ হয়

Node.js-এ Buffer.from("টাকা").length-এর মান কত (UTF-8 encoding)?

  • 4
  • 12
  • 8
  • 2

একটি Node.js HTTP সার্ভারের request handler-এ fs.readFileSync দিয়ে একটি বড় ফাইল পড়া হলে মূল সমস্যা কী?

  • ফাইলের লেখা ভুলভাবে পড়া হয়
  • প্রতিটি request-এর জন্য নতুন thread তৈরি হয়ে মেমরি ফুরিয়ে যায়
  • পড়া শেষ না হওয়া পর্যন্ত main thread আটকে থাকে, ফলে অন্য সব request অপেক্ষা করে
  • Node.js-এ readFileSync আর সমর্থিত নয়, তাই error দেয়

Node.js stream-এ backpressure বলতে কী বোঝায়?

  • ধীর গন্তব্য ভরে গেলে উৎসকে থামানোর ব্যবস্থা, যাতে মেমরিতে ডেটা না জমে
  • ডেটা পাঠানোর আগে সংকোচন করা
  • Error হলে সব stream বন্ধ করার নিয়ম
  • উৎসকে দ্রুত পড়তে বাধ্য করা, যাতে গন্তব্য অপেক্ষা না করে

ব্যবহারকারীর দেওয়া ফাইলের নাম দিয়ে Node.js থেকে একটি বাইরের প্রোগ্রাম চালাতে হবে। সবচেয়ে নিরাপদ পদ্ধতি কোনটি?

  • exec দিয়ে template literal-এ নাম বসিয়ে
  • exec দিয়ে, তবে নামের ভেতরের উদ্ধৃতিচিহ্ন হাতে escape করে
  • spawn-এ shell: true দিয়ে
  • execFile দিয়ে, নামটি argument array-র একটি উপাদান হিসেবে, আর আগে যাচাই করে

Node.js-এ a.pipe(b).pipe(c)-এর তুলনায় stream/promises-এর pipeline(a, b, c)-এর প্রধান সুবিধা কী?

  • শুধু pipeline-ই backpressure সামলায়
  • যেকোনো stream-এ error হলে সবগুলো বন্ধ করে, আর Promise-এর মাধ্যমে একটি জায়গায় error দেয়
  • pipeline পুরো ডেটা আগে মেমরিতে তুলে নেয়, তাই দ্রুত
  • pipeline শুধু ফাইলের stream-এ কাজ করে

Windows আর Linux দুই জায়গায় ঠিকমতো চলবে এমনভাবে data ফোল্ডারের orders.csv-এর পথ তৈরি করার সঠিক উপায় কোনটি?

  • "data\\" + "orders.csv"
  • "data" + "/" + "orders.csv" সবসময়ই সবচেয়ে ভালো
  • path.join("data", "orders.csv")
  • path.extname("data", "orders.csv")

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. Node.js = V8 + libuv + core module. একই JavaScript, কিন্তু DOM-এর বদলে ফাইল, নেটওয়ার্ক আর process — আর ব্রাউজারের মতো sandbox নেই।
  2. process: argv (argument শুরু argv[2] থেকে), env (সব string; গোপন মান .env-এ, কখনো commit নয়), exitCode (০ = সফল), stderr-এ ভুল, আর SIGINT/SIGTERM-এ graceful shutdown.
  3. পথ জুড়তে path.join; relative পথ ধরা হয় cwd থেকে। ফাইলে fs/promises, sync শুধু শুরুতে বা ছোট script-এ; ভুলে err.code দেখে সিদ্ধান্ত।
  4. Buffer = bytes. বাংলা অক্ষর UTF-8-এ ৩ byte: "টাকা" length ৪, bytes ১২. Bytes-এর মাঝখানে কাটলে �.
  5. বড় ডেটায় stream; ধীর গন্তব্যে backpressure উৎসকে থামায়। জোড়া দিন pipeline দিয়ে, লাইন ধরে পড়ুন readline দিয়ে, বাইরের প্রোগ্রাম চালান execFile দিয়ে — shell-এ ইনপুট কখনো নয়।

পরের পর্বে: ফাইল আর process তো হাতে এল — এবার সার্ভার। Express দিয়ে একটি REST API: route, middleware, JSON request-response, status code, validation আর ভুল সামলানো — আর এই পর্বের graceful shutdown কোথায় বসে।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান