Задание: https://github.com/GossJS/stream
   Это концепция реализована в Node, а не в чистом JS (2017).
Близкая концепция - буфер. см. также https://kodaktor.ru/g/get 

Поток подразумевает чтение данных, которые «пока что приходят» - и нет реалистичного способа сказать, когда поступление данных прекратится. Поэтому считываем фрагментами-порциями-чанками-секторами по несколько байт, пока при очередном считывании не окажется, что считывать больше нечего. Это близко понятию итератора по структуре, в которой нет возможности обращаться к отдельным порциями по индексу - мы движемся до тех пор, пока есть куда двигаться. Или можно сказать, что это близко понятию списка-цепочки из звеньев, в котором каждое звено указывает на ещё одно звено - и вот по этим указателям мы можем перемещаться, пока есть следующий указатель. Лента новостей в Твиттере или другой соцсети: новые посты появляются по мере загрузки, но сколько их всего, сказать, скорее всего, невозможно.

Буфер: fs.readFile
Поток: fs.createReadStream 

При чтении из потока буфер постепенно заполняется данными почанково.

Связь с асинхронностью. В JS асинхронно то, что не задерживает выполнение текущей инструкции, а создаёт своего рода ветвь кода. Чтение файла можно сделать синхронным (readFileSync), но это нарушает общую идею не-блокирования. Поэтому «по природе» асинхронно и чтение в один целый большой буфер, и чтение чанками из потока, но в последнем случае организация кода более сложная, потому что нужно два уровня обратного вызова или обработчиков - для поступления чанков и для окончания процесса в целом. 

Связь с сокетами, слушателями/обзерверами и реактивностью: происходит генерация событий и изменения состояния чего-либо (слушателя, обзервера) мгновенно в связи с событием. Тут пересечение двух абстракций: события и течение данных.


import util from 'util';
import events from 'events';

const pr = util.promisify(setTimeout);
const em = new events.EventEmitter();
em.on('myevent', data => console.log(data));

//оттягивание завершения приложения
(async()=>{
  console.time('sec20');
  await pr(20000);
  console.timeEnd('sec20')
})();

//параллельный запуск таймера на 10 сек, который вызовет событие
(async()=>{
  await pr(10000);
  em.emit('myevent', 'hello, 10 secs passed!');
})();
//параллельный запуск таймера на 2 сек, который вызовет событие
(async()=>{
  await pr(2000);
  em.emit('myevent', 'hellooo, 2 secs passed!');
})();

https://kodaktor.ru/g/get - второй пример с http.get О буфере говорят как о потоке октетов (т.к. байт - более широкое понятие, это может, но не обязательно, быть октет). В этом смысле поток - это просто последовательность. Поток Node представлен абстрактным интерфейсом Stream, и это значит, что мы не создаём потоки напрямую. Этот интерфейс реализуют, например: запросы HTTP, потоки для чтения или записи модуля File System, объекты сжатия Zlib process.stdout. Примером дуплексного потока является интернет-сокет (два потока-канала-в-одном). Определение IETF: «поток» — это независимая, двунаправленная последовательность фреймов, передаваемых между клиентом и сервером в рамках соединения HTTP/2. Одна из его основных характеристик заключается в том, что одно HTTP/2-соединение может содержать несколько одновременно открытых потоков, причём, любая конечная точка может обрабатывать чередующиеся фреймы из нескольких потоков. Вот пример чтения из потока почанково, каждый раз очередной чанк добавляется в Buffer

import http from 'http';
void http
 .get('http://kodaktor.ru/j/users', (rdStr, buf = '') => {
    rdStr.on('data', d => buf+=d);
    rdStr.on('end', () => console.log(JSON.parse(buf)));
});

    

А вот пример решения той же задачи перенаправлением из одного потока в другой:


import http from 'http';
void http
 .get('http://kodaktor.ru/j/users', rdStr => rdStr.pipe(process.stdout)); 
    
Соответственно, в рамках веб-сервера мы можем перенаправить в response:



.get('/users', r=>
    http
     .get('http://kodaktor.ru/j/users', rdStr => rdStr.pipe(r.res)) 
)


И даже так:



const pget=url=>new Promise(res=>http.get(url,rd=>res(rd)));
//...
.get('/users', async r=> 
   (await pget('http://kodaktor.ru/j/users')).pipe(r.res)
)

.get('/news', async r=> ( await pget('http://www.newsru.com/') ).pipe(r.res)) 


И 
в REPL:

const pget=url=>new Promise(res=>http.get(url,rd=>res(rd)));void(async url=>(await pget(url)).pipe(process.stdout))('http://kodaktor.ru/j/users');
В этих двух примерах выше мы просто делаем более удобным обращение с потоком, убирая коллбэк, в аргумент которого он помещается, под промис.

Что касается чтения файла, то если мы будем делать это с помощью потока и чанков, то:

const { createReadStream: r } = require('fs'); 

const rdStr = r(`./1.txt`, { highWaterMark: 1 });
let buf = '';
rdStr.on('data', d => buf += d);
rdStr.on('end', () => console.log(String(buf)));
// обратите внимание на  highWaterMark – настройку для побайтового считывания    
   
Чтобы перенаправить чтение файла:

require('fs')
  .createReadStream('./1.txt')
  .pipe(process.stdout);
   
См. также мой ответ на вопрос о чтении файла с использованием промисов – речь идёт о новом объекте promises (Node v. >= 10.2.0)

(async () => console.log(String(await require('fs').promises.readFile('./1.txt'))))();