From 6bc17999243e980a723481e3ac632011893240ba Mon Sep 17 00:00:00 2001 From: Pawel_Makarewicz Date: Thu, 5 Mar 2026 21:10:49 +0100 Subject: [PATCH 1/6] feat: implement directory scanning and snapshot generation --- src/fs/snapshot.js | 65 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 59 insertions(+), 6 deletions(-) diff --git a/src/fs/snapshot.js b/src/fs/snapshot.js index 050103d3..36e80214 100644 --- a/src/fs/snapshot.js +++ b/src/fs/snapshot.js @@ -1,9 +1,62 @@ +import { promises as fs } from 'fs'; +import path from 'path'; + +async function scanDirectory(dirPath, relativePath = '') { + try { + const entries = []; + const items = await fs.readdir(dirPath); + items.sort(); + + for (const item of items) { + const fullPath = path.join(dirPath, item); + const relPath = path.join(relativePath, item); + const stats = await fs.stat(fullPath); + + if (stats.isDirectory()) { + entries.push({ path: relPath, type: 'directory' }); + const subEntries = await scanDirectory(fullPath, relPath); + entries.push(...subEntries); + } else if (stats.isFile()) { + const buffer = await fs.readFile(fullPath); + entries.push({ + path: relPath, + type: 'file', + size: stats.size, + content: buffer.toString('base64'), + }); + } + } + + return entries; + } catch (error) { + throw new Error(`FS operation failed: ${error.message}`); + } +} + const snapshot = async () => { - // Write your code here - // Recursively scan workspace directory - // Write snapshot.json with: - // - rootPath: absolute path to workspace - // - entries: flat array of relative paths and metadata + const workspacePath = path.join(process.cwd(), 'workspace'); + try { + await fs.access(workspacePath); + } catch (error) { + throw new Error(`FS operation failed: ${error.message}`); + } + + const entries = await scanDirectory(workspacePath); + + const result = { + rootPath: workspacePath, + entries, + }; + + const snapshotPath = path.join(path.dirname(workspacePath), 'snapshot.json'); + try { + await fs.writeFile(snapshotPath, JSON.stringify(result, null, 2)); + } catch (error) { + throw new Error(`FS operation failed: ${error.message}`); + } }; -await snapshot(); +await snapshot().catch((error) => { + console.error(error.message); + process.exit(1); +}); From 78f7316bce1448d651f6c42c719a32b0620802e8 Mon Sep 17 00:00:00 2001 From: Pawel_Makarewicz Date: Thu, 5 Mar 2026 21:35:37 +0100 Subject: [PATCH 2/6] feat: implement interactive CLI with command support --- src/cli/interactive.js | 42 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/src/cli/interactive.js b/src/cli/interactive.js index d0e3e0d9..6b9ef15e 100644 --- a/src/cli/interactive.js +++ b/src/cli/interactive.js @@ -1,8 +1,42 @@ +import readline from 'readline'; + const interactive = () => { - // Write your code here - // Use readline module for interactive CLI - // Support commands: uptime, cwd, date, exit - // Handle Ctrl+C and unknown commands + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + prompt: '> ', + }); + + rl.prompt(); + + rl.on('line', (line) => { + const command = line.trim(); + + switch (command) { + case 'uptime': + console.log(`Uptime: ${process.uptime().toFixed(2)}s`); + break; + case 'cwd': + console.log(process.cwd()); + break; + case 'date': + console.log(new Date().toISOString()); + break; + case 'exit': + console.log('Goodbye!'); + process.exit(0); + break; + default: + console.log('Unknown command'); + } + + rl.prompt(); + }); + + rl.on('close', () => { + console.log('Goodbye!'); + process.exit(0); + }); }; interactive(); From 86bde885ddaf9efe22a6f05b8043cbe32653f360 Mon Sep 17 00:00:00 2001 From: Pawel_Makarewicz Date: Fri, 6 Mar 2026 19:40:32 +0100 Subject: [PATCH 3/6] feat: implement progress bar functionality with customizable options --- src/cli/progress.js | 61 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 57 insertions(+), 4 deletions(-) diff --git a/src/cli/progress.js b/src/cli/progress.js index 3e060763..05222c54 100644 --- a/src/cli/progress.js +++ b/src/cli/progress.js @@ -1,8 +1,61 @@ +const args = process.argv.slice(2); + +const RESET = '\x1b[0m'; + +const getArg = (name) => { + const index = args.indexOf(name); + if (index === -1 || index + 1 >= args.length) return undefined; + return args[index + 1]; +}; + +const getNumericArg = (name, defaultValue) => { + const raw = getArg(name); + if (raw === undefined) return defaultValue; + const parsed = parseInt(raw, 10); + return Number.isNaN(parsed) ? defaultValue : parsed; +}; + +const parseHexColor = (hex) => { + if (!hex) return null; + const match = hex.match(/^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/); + if (!match) return null; + const r = parseInt(match[1], 16); + const g = parseInt(match[2], 16); + const b = parseInt(match[3], 16); + return `\x1b[38;2;${r};${g};${b}m`; +}; + +const renderBar = (percent, length, colorCode) => { + const filled = Math.round((percent / 100) * length); + const empty = length - filled; + + const filledBar = '█'.repeat(filled); + const emptyBar = ' '.repeat(empty); + + const coloredFilled = colorCode ? `${colorCode}${filledBar}${RESET}` : filledBar; + return `[${coloredFilled}${emptyBar}] ${percent}%`; +}; + const progress = () => { - // Write your code here - // Simulate progress bar from 0% to 100% over ~5 seconds - // Update in place using \r every 100ms - // Format: [████████████████████ ] 67% + const duration = getNumericArg('--duration', 5000); + const interval = getNumericArg('--interval', 100); + const length = getNumericArg('--length', 30); + const colorCode = parseHexColor(getArg('--color')); + + const totalSteps = Math.ceil(duration / interval); + let currentStep = 0; + + const timer = setInterval(() => { + currentStep++; + const percent = Math.min(Math.round((currentStep / totalSteps) * 100), 100); + + process.stdout.write(`\r${renderBar(percent, length, colorCode)}`); + + if (currentStep >= totalSteps) { + clearInterval(timer); + process.stdout.write('\nDone!\n'); + } + }, interval); }; progress(); From f5b63941763291b2678bfd79ea345d42395547ac Mon Sep 17 00:00:00 2001 From: Pawel_Makarewicz Date: Sun, 8 Mar 2026 16:55:07 +0100 Subject: [PATCH 4/6] feat: implement dynamic plugin loading with error handling --- src/modules/dynamic.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/modules/dynamic.js b/src/modules/dynamic.js index 008ca387..f8bf276f 100644 --- a/src/modules/dynamic.js +++ b/src/modules/dynamic.js @@ -1,9 +1,13 @@ const dynamic = async () => { - // Write your code here - // Accept plugin name as CLI argument - // Dynamically import plugin from plugins/ directory - // Call run() function and print result - // Handle missing plugin case + const args = process.argv.slice(2); + const pluginName = args.at(-1); + try { + const plugin = await import(`./plugins/${pluginName}.js`); + console.log(plugin.run()); + } catch (error) { + console.error(`Plugin not found`); + process.exit(1); + } }; await dynamic(); From d7b71507e527555aafef82111c188316068a3db0 Mon Sep 17 00:00:00 2001 From: Pawel_Makarewicz Date: Sun, 8 Mar 2026 19:45:23 +0100 Subject: [PATCH 5/6] feat: implement line numbering in stream processing --- src/streams/lineNumberer.js | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/streams/lineNumberer.js b/src/streams/lineNumberer.js index 579d662e..44d7aa74 100644 --- a/src/streams/lineNumberer.js +++ b/src/streams/lineNumberer.js @@ -1,8 +1,25 @@ +import { Transform } from 'node:stream'; + +const addLineNumbers = (chunk, counter) => + chunk + .toString() + .split('\n') + .map((line, i) => { + if (i === 0 && line === '') return ''; + return `${counter.value++} | ${line}`; + }) + .join('\n'); + +const counter = { value: 1 }; + +const transform = new Transform({ + transform(chunk, enc, cb) { + cb(null, addLineNumbers(chunk, counter)); + }, +}); + const lineNumberer = () => { - // Write your code here - // Read from process.stdin - // Use Transform Stream to prepend line numbers - // Write to process.stdout + process.stdin.pipe(transform).pipe(process.stdout); }; lineNumberer(); From 1f0d88b07df1ed46167c6da02cf77d61c1b743c7 Mon Sep 17 00:00:00 2001 From: Pawel_Makarewicz Date: Tue, 10 Mar 2026 08:26:02 +0100 Subject: [PATCH 6/6] feat: add split-to-files and transform demo implementations --- src/streams/split-to-files-no-transform.js | 146 ++++++++++++ src/streams/split-to-files.js | 256 +++++++++++++++++++++ src/streams/transform-demo.js | 98 ++++++++ 3 files changed, 500 insertions(+) create mode 100644 src/streams/split-to-files-no-transform.js create mode 100644 src/streams/split-to-files.js create mode 100644 src/streams/transform-demo.js diff --git a/src/streams/split-to-files-no-transform.js b/src/streams/split-to-files-no-transform.js new file mode 100644 index 00000000..22564e4b --- /dev/null +++ b/src/streams/split-to-files-no-transform.js @@ -0,0 +1,146 @@ +/** + * Та же задача, но БЕЗ Transform — вся логика в _write() + * + * Сравни с split-to-files.js где используется Transform (lineSplitter). + * Тут всё в одном месте — и разбиение на строки, и ротация файлов. + * + * ⚠️ Работает, но _write() делает ДВЕ работы одновременно: + * 1) Склеивает огрызки строк (потому что чанк режет по байтам) + * 2) Считает строки и ротирует файлы + */ + +import { Writable } from 'node:stream'; +import { createReadStream, createWriteStream } from 'node:fs'; +import { writeFile, unlink, mkdir, readFile } from 'node:fs/promises'; +import { join, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const WORK_DIR = join(__dirname, '_split-demo-v2'); +const INPUT_PATH = join(WORK_DIR, 'input.txt'); + +const TOTAL_LINES = 35; +const LINES_PER_FILE = 10; + +await mkdir(WORK_DIR, { recursive: true }); + +const lines = Array.from({ length: TOTAL_LINES }, (_, i) => + `Сегмент #${String(i + 1).padStart(2, '0')}: данные-${Math.random().toString(36).slice(2, 8)}` +); +await writeFile(INPUT_PATH, lines.join('\n')); + +console.log('=== БЕЗ Transform — всё в _write() ===\n'); + +// ─── Всё в одном Writable ────────────────────────────────────────── + +let remainder = ''; // огрызок строки с прошлого чанка +let lineCount = 0; +let fileIndex = 0; +let currentStream = null; +const createdFiles = []; + +function openNewFile() { + return new Promise((resolve, reject) => { + fileIndex++; + const fileName = `chunk_${String(fileIndex).padStart(3, '0')}.txt`; + const filePath = join(WORK_DIR, fileName); + createdFiles.push(filePath); + currentStream = createWriteStream(filePath, { encoding: 'utf-8' }); + console.log(`\n 📁 Создан: ${fileName}`); + currentStream.on('open', resolve); + currentStream.on('error', reject); + }); +} + +function closeCurrentFile() { + return new Promise((resolve) => { + if (!currentStream) return resolve(); + currentStream.end(() => { + console.log(` 📦 Закрыт (${lineCount} строк)`); + lineCount = 0; + resolve(); + }); + }); +} + +const splitter = new Writable({ + // НЕ objectMode! Мы получаем сырые Buffer/string чанки от Readable + decodeStrings: false, + + async write(chunk, encoding, callback) { + try { + // ─── Работа 1: собираем строки из огрызков ─── + // (это то, что делал Transform в прошлой версии) + remainder += chunk.toString(); + const parts = remainder.split('\n'); + remainder = parts.pop(); // последний — неполная строка, сохраняем + + // ─── Работа 2: пишем строки, ротируем файлы ─── + // (это то, что делал Writable в прошлой версии) + for (const line of parts) { + if (line.length === 0) continue; + + if (!currentStream || lineCount >= LINES_PER_FILE) { + await closeCurrentFile(); + await openNewFile(); + } + + lineCount++; + currentStream.write(line + '\n'); + console.log(` ✏️ ${lineCount}/${LINES_PER_FILE} → "${line.slice(0, 40)}..."`); + } + + callback(); + } catch (err) { + callback(err); + } + }, +}); + +// Обработка остатка и последнего файла +splitter._final = async function (callback) { + try { + // Остаток (последняя строка без \n) + if (remainder.length > 0) { + if (!currentStream || lineCount >= LINES_PER_FILE) { + await closeCurrentFile(); + await openNewFile(); + } + lineCount++; + currentStream.write(remainder + '\n'); + console.log(` ✏️ ${lineCount}/${LINES_PER_FILE} → "${remainder.slice(0, 40)}..."`); + } + await closeCurrentFile(); + callback(); + } catch (err) { + callback(err); + } +}; + +// ─── Цепочка: ВСЕГО 2 звена ──────────────────────────────────────── + +const readable = createReadStream(INPUT_PATH, { + highWaterMark: 64, + encoding: 'utf-8', +}); + +readable.pipe(splitter); // ← без Transform! Напрямую. + +splitter.on('finish', async () => { + console.log('\n' + '─'.repeat(60)); + console.log('\n ✅ Результат:\n'); + + for (const filePath of createdFiles) { + const content = await readFile(filePath, 'utf-8'); + const lc = content.trim().split('\n').length; + const fn = filePath.split('/').pop(); + console.log(` 📄 ${fn} (${lc} строк)`); + } + + // Уборка + for (const f of createdFiles) await unlink(f); + await unlink(INPUT_PATH); + const { rmdir } = await import('node:fs/promises'); + await rmdir(WORK_DIR); + console.log('\n 🧹 Удалено\n'); +}); diff --git a/src/streams/split-to-files.js b/src/streams/split-to-files.js new file mode 100644 index 00000000..1437e888 --- /dev/null +++ b/src/streams/split-to-files.js @@ -0,0 +1,256 @@ +/** + * Демо: Разбиение стрима на несколько файлов + * + * Задача: + * Читаем файл с N строками (сегментами). + * Каждые LINES_PER_FILE строк — создаём НОВЫЙ файл и пишем туда. + * + * Схема: + * + * [Большой файл] + * │ + * ▼ + * Readable (.pipe) + * │ + * ▼ + * Split (разбивает по \n) + * │ + * ▼ + * FileSplitter (кастомный Writable) + * │ + * ├──► chunk_001.txt (строки 1–10) + * ├──► chunk_002.txt (строки 11–20) + * ├──► chunk_003.txt (строки 21–30) + * └──► ... + * + * Ключевая идея: + * Внутри _write() ты сам решаешь КУДА писать. + * Считаешь строки → когда порог достигнут → + * 1) закрываешь текущий WriteStream (.end()) + * 2) создаёшь новый createWriteStream() + * 3) пишешь в него + * + * ⚠️ Важно: .write() у WriteStream может вернуть false (буфер полон). + * Тогда нужно подождать событие 'drain', прежде чем продолжать. + * Именно для этого callback в _write() вызывается после drain. + */ + +import { Writable, Transform } from 'node:stream'; +import { createReadStream, createWriteStream } from 'node:fs'; +import { writeFile, unlink, mkdir, readdir, readFile } from 'node:fs/promises'; +import { join, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const WORK_DIR = join(__dirname, '_split-demo'); +const INPUT_PATH = join(WORK_DIR, 'input.txt'); + +const TOTAL_LINES = 35; // сколько строк сгенерируем +const LINES_PER_FILE = 10; // каждые 10 строк → новый файл + +// ─── 1. Подготовка: создаём папку и входной файл ───────────────────── + +await mkdir(WORK_DIR, { recursive: true }); + +// Генерируем файл с 35 строками +const lines = Array.from({ length: TOTAL_LINES }, (_, i) => + `Сегмент #${String(i + 1).padStart(2, '0')}: данные-${Math.random().toString(36).slice(2, 8)}` +); +await writeFile(INPUT_PATH, lines.join('\n')); + +console.log('=== ДЕМО: Разбиение стрима на несколько файлов ===\n'); +console.log(` Входной файл: ${TOTAL_LINES} строк`); +console.log(` Порог: каждые ${LINES_PER_FILE} строк → новый файл\n`); +console.log('─'.repeat(60)); + +// ─── 2. Transform: разбиваем поток байтов на строки ────────────────── +// +// Проблема: Readable отдаёт чанки по байтам (highWaterMark), +// а не по строкам. Чанк может оборваться посреди строки! +// +// Решение: Transform-стрим, который буферизует остаток +// и отдаёт только целые строки. + +const lineSplitter = new Transform({ + readableObjectMode: true, // на выходе — объекты (строки), а не Buffer + encoding: 'utf-8', + + transform(chunk, encoding, callback) { + // _остаток хранит «хвост» предыдущего чанка + this._remainder = (this._remainder || '') + chunk.toString(); + + const parts = this._remainder.split('\n'); + + // Последний элемент — неполная строка (или ''), сохраняем + this._remainder = parts.pop(); + + // Все остальные — целые строки, отправляем дальше + for (const line of parts) { + if (line.length > 0) { + this.push(line); + } + } + callback(); + }, + + // Когда входной поток закончился — отдаём остаток + flush(callback) { + if (this._remainder && this._remainder.length > 0) { + this.push(this._remainder); + } + callback(); + }, +}); + +// ─── 3. Кастомный Writable: ротация файлов ────────────────────────── +// +// ЭТО ГЛАВНАЯ ЧАСТЬ. +// +// Внутри _write() мы: +// - считаем строки +// - когда счётчик достиг порога → закрываем старый файл, открываем новый +// - пишем строку в текущий файл +// +// Событие 'drain' — вот то самое событие, о котором ты спрашивал! +// Когда writable.write() возвращает false, буфер переполнен. +// Нужно ждать 'drain', чтобы продолжить запись. + +class FileSplitter extends Writable { + constructor(outputDir, linesPerFile) { + super({ objectMode: true }); // принимаем объекты (строки от lineSplitter) + + this.outputDir = outputDir; + this.linesPerFile = linesPerFile; + this.lineCount = 0; // счётчик строк в текущем файле + this.fileIndex = 0; // номер текущего файла + this.currentStream = null; // текущий WriteStream + this.createdFiles = []; // список созданных файлов (для уборки) + } + + /** + * Создаёт новый выходной файл и WriteStream для него. + * Возвращает Promise, который резолвится когда файл готов к записи. + */ + _openNewFile() { + return new Promise((resolve, reject) => { + this.fileIndex++; + const fileName = `chunk_${String(this.fileIndex).padStart(3, '0')}.txt`; + const filePath = join(this.outputDir, fileName); + this.createdFiles.push(filePath); + + // Создаём новый WriteStream + this.currentStream = createWriteStream(filePath, { encoding: 'utf-8' }); + + console.log(`\n 📁 Создан файл: ${fileName}`); + + // Ждём 'open' — файл реально открыт и готов + this.currentStream.on('open', () => resolve()); + this.currentStream.on('error', (err) => reject(err)); + }); + } + + /** + * Закрывает текущий WriteStream. + * .end() — "больше писать не буду", стрим допишет буфер и закроется. + * Ждём 'finish' — всё реально записано на диск. + */ + _closeCurrentFile() { + return new Promise((resolve) => { + if (!this.currentStream) return resolve(); + + this.currentStream.end(() => { + console.log(` 📦 Файл закрыт (записано ${this.lineCount} строк)`); + this.lineCount = 0; + resolve(); + }); + }); + } + + /** + * _write() — вызывается для КАЖДОГО чанка (у нас — каждой строки). + * + * ⚠️ callback нужно вызвать ТОЛЬКО когда запись реально завершена. + * Если вызвать раньше — стрим пришлёт следующий чанк, + * а мы ещё не закончили с текущим → потеря данных или гонка. + */ + async _write(line, encoding, callback) { + try { + // Первый чанк или порог достигнут → ротация файла + if (!this.currentStream || this.lineCount >= this.linesPerFile) { + await this._closeCurrentFile(); + await this._openNewFile(); + } + + this.lineCount++; + const data = line + '\n'; + + // .write() возвращает boolean: + // true → буфер НЕ полон, можно писать ещё + // false → буфер полон, НУЖНО ЖДАТЬ 'drain'! + const canContinue = this.currentStream.write(data); + + console.log(` ✏️ Строка ${this.lineCount}/${this.linesPerFile} → "${line.slice(0, 40)}..."`); + + if (!canContinue) { + // Буфер WriteStream переполнен! + // Ждём событие 'drain' — "буфер освободился, можно писать" + console.log(` ⏳ Буфер полон! Ждём 'drain'...`); + await new Promise((resolve) => this.currentStream.once('drain', resolve)); + console.log(` ✅ 'drain' получен, продолжаем`); + } + + callback(); // "я закончил с этим чанком, давай следующий" + } catch (err) { + callback(err); + } + } + + /** + * _final() — вызывается когда ВСЕ чанки обработаны. + * Тут закрываем последний файл. + */ + async _final(callback) { + try { + await this._closeCurrentFile(); + callback(); + } catch (err) { + callback(err); + } + } +} + +// ─── 4. Собираем цепочку и запускаем ──────────────────────────────── + +const readable = createReadStream(INPUT_PATH, { + highWaterMark: 64, // специально маленький, чтобы было несколько чанков + encoding: 'utf-8', +}); + +const splitter = new FileSplitter(WORK_DIR, LINES_PER_FILE); + +// readable → lineSplitter → fileSplitter +readable + .pipe(lineSplitter) + .pipe(splitter); + +splitter.on('finish', async () => { + console.log('\n' + '─'.repeat(60)); + console.log('\n ✅ Готово! Результат:\n'); + + // Показываем содержимое каждого созданного файла + for (const filePath of splitter.createdFiles) { + const content = await readFile(filePath, 'utf-8'); + const lineCount = content.trim().split('\n').length; + const fileName = filePath.split('/').pop(); + console.log(` 📄 ${fileName} (${lineCount} строк):`); + console.log(` Первая: "${content.split('\n')[0]}"`); + console.log(` Последняя: "${content.trim().split('\n').pop()}"\n`); + } + + // Уборка + for (const f of splitter.createdFiles) await unlink(f); + await unlink(INPUT_PATH); + const { rmdir } = await import('node:fs/promises'); + await rmdir(WORK_DIR); + console.log(' 🧹 Временные файлы удалены\n'); +}); diff --git a/src/streams/transform-demo.js b/src/streams/transform-demo.js new file mode 100644 index 00000000..6c5f8424 --- /dev/null +++ b/src/streams/transform-demo.js @@ -0,0 +1,98 @@ +/** + * Демо: как работает Transform стрим + * + * Схема: + * + * [Файл] → Readable → Transform → Writable → [Файл] + * + * ┌─────────────────────────┐ + * │ TRANSFORM │ + * данные ────► │ [writable буфер] │ + * │ ↓ │ + * │ _transform(chunk) │ ← тут ты меняешь данные + * │ ↓ │ + * │ [readable буфер] ────────► дальше + * └─────────────────────────┘ + * + * У Transform ДВА буфера: + * 1) Writable-сторона (вход) — принимает данные + * 2) Readable-сторона (выход) — отдаёт изменённые данные + */ + +import { Transform } from 'node:stream'; +import { createReadStream, createWriteStream } from 'node:fs'; +import { writeFile, unlink } from 'node:fs/promises'; +import { join, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const inputPath = join(__dirname, 'transform-input.txt'); +const outputPath = join(__dirname, 'transform-output.txt'); + +// Создаём тестовый файл +await writeFile(inputPath, 'hello world\ngoodbye world\nnode streams'); + +console.log('=== ДЕМО: Transform стрим ===\n'); + +// --- Создаём свой Transform --- +const toUpperCase = new Transform({ + highWaterMark: 16, + + // Вот главный метод — вызывается для КАЖДОГО чанка + transform(chunk, encoding, callback) { + const input = chunk.toString(); + const output = input.toUpperCase(); + + console.log(` 🔄 TRANSFORM получил: "${input.trim()}"`); + console.log(` Writable-буфер (вход): ${this.writableLength} байт`); + + // this.push() — кладёт результат в readable-буфер (выход) + this.push(output); + + console.log(` Readable-буфер (выход): ${this.readableLength} байт`); + console.log(` Отдал: "${output.trim()}"\n`); + + // callback() — "я закончил с этим чанком, давай следующий" + callback(); + }, +}); + +// --- Собираем цепочку --- + +const readable = createReadStream(inputPath, { + highWaterMark: 16, + encoding: 'utf-8', +}); + +const writable = createWriteStream(outputPath, { + highWaterMark: 16, +}); + +console.log(' Цепочка: Файл → Readable → Transform(toUpperCase) → Writable → Файл\n'); +console.log('─'.repeat(60)); + +// pipe соединяет всё: +// readable.pipe(transform).pipe(writable) +// +// readable → [буфер] → transform.writable-вход → [буфер] +// _transform() +// transform.readable-выход → [буфер] → writable → [буфер] → диск + +readable + .pipe(toUpperCase) + .pipe(writable); + +writable.on('finish', async () => { + console.log('─'.repeat(60)); + console.log('\n ✅ Готово!\n'); + + // Показываем результат + const { readFile } = await import('node:fs/promises'); + const result = await readFile(outputPath, 'utf-8'); + console.log(` Вход: "hello world | goodbye world | node streams"`); + console.log(` Выход: "${result.replace(/\n/g, ' | ')}"\n`); + + // Уборка + await unlink(inputPath); + await unlink(outputPath); +});