عالِج ملفات بحجم عدة غيغابايت بذاكرة ثابتة بفضل التدفّقات، وpipeline، والضغط العكسي.
افتح هذا الدرس في Kodokonتُحمّل readFile ملفًا بأكمله في الذاكرة: فمع سجلّ حجمه 4 غيغابايت، تنهار عمليتك. أما التدفّقات فتعالج البيانات على شكل أجزاء (chunks) ببصمة ذاكرة ثابتة، محدودة بـ highWaterMark (64 كيبيبايت افتراضيًا لتدفّق ملف). وتوجد أربعة أنواع: Readable، وWritable، وDuplex، وTransform. والمشكلة الجوهرية التي تحلّها تُسمى الضغط العكسي (backpressure): ماذا تفعل عندما يكون المُنتِج أسرع من المُستهلِك؟
import { createWriteStream } from "node:fs";
const out = createWriteStream("big.txt");
let i = 0;
function writeChunks() {
let ok = true;
while (i < 1e6 && ok) {
ok = out.write(`line ${i}\n`);
i += 1;
}
if (i < 1e6) out.once("drain", writeChunks);
else out.end();
}
writeChunks();عندما تُرجِع write() القيمة false، يكون المخزن المؤقّت الداخلي قد تجاوز highWaterMark: البيانات غير مفقودة، لكن الاستمرار في الكتابة يُراكم كل شيء في الذاكرة - وهو تحديدًا ما وُجدت التدفّقات لتجنّبه. والعقد هو: توقّف عن الكتابة وانتظر الحدث drain. وهذا ما تفعله pipe() نيابةً عنك، وما تفعله pipeline() على نحو أفضل: فبالإضافة إلى الضغط العكسي، تنشر الأخطاء وتُتلِف كل التدفّقات بنظافة، في الاتجاهين معًا.
import { createReadStream, createWriteStream }
from "node:fs";
import { Transform } from "node:stream";
import { pipeline } from "node:stream/promises";
const upper = new Transform({
transform(chunk, encoding, callback) {
callback(null, chunk.toString().toUpperCase());
},
});
await pipeline(
createReadStream("big.txt"),
upper,
createWriteStream("big-upper.txt"),
);
console.log("done");إن تدفّقات Readable قابلة للتكرار بشكل غير متزامن: إذ تستهلك for await الأجزاء مع احترام الضغط العكسي. لكن انتبه، فالجزء (chunk) شريحة اعتباطية من البايتات: لا شيء يضمن أنه ينتهي عند حدّ سطر. وللمعالجة سطرًا سطرًا، تتولّى node:readline إعادة تجميع القطع، بما في ذلك \r\n مقسومًا على جزأين بفضل crlfDelay: Infinity.
import { createReadStream } from "node:fs";
import { createInterface } from "node:readline";
const rl = createInterface({
input: createReadStream("big.txt"),
crlfDelay: Infinity,
});
let count = 0;
for await (const line of rl) {
if (line.includes("42")) count += 1;
}
console.log(count, "matching lines");