"We should have some ways of connecting programs like garden hose--screw in another segment when it becomes necessary to massage data in another way. This is the way of IO also."
Why Streams?
Example 1
const server1 = http.createServer(function (req, res) {
let response = counter++;
console.log('S1: Received request ' + response);
fs.readFile(__dirname + '/bigFile', function (err, data) {
if (err) {
console.log(err);
} else {
console.log(`Sending response of ${response}`);
res.end(data);
}
});
});
const server2 = http.createServer((req, res) => {
let response = counter++;
console.log('S2: Received request ' + response);
const reader = fs.createReadStream(__dirname + '/bigFile');
// console.log(`Sending response of ${response}`);
reader.pipe(res);
});
Types (based on data value) of Steams
Types of Stream
Examples of Readable streams include:
Reading Modes
All Readable streams begin in paused mode but can be switched to flowing mode in one of the following ways:
const { createReadStream } = require('fs');
const stream = createReadStream('filetoread.txt');
All streams are instances of EventEmitter. They emit events that can be used to read and write data.
Readable Stream Events
readableSrc.pipe(writableDest)
a.pipe(b).pipe(c).pipe(d)
# Which is equivalent to:
a.pipe(b)
b.pipe(c)
c.pipe(d)
# Which, in Linux, is equivalent to:
$ a | b | c | d
Advantages of Node pipe method
// readable.pipe(writable)
readable.on('data', (chunk) => {
writable.write(chunk);
});
readable.on('end', () => {
writable.end();
});
The most important events on a readable stream are:
Use-case | Class | Method(s) to implement |
---|---|---|
Reading only | Readable | _read() |
Writing only | Writable | _write(), _writev(), _final() |
Reading and writing | Duplex | _read(), _write(), _writev(), _final() |
Operate on written data, then read the result | Transform | _transform(), _flush(), _final() |
Implementation
Implementing a Readable Stream
Three ways
To implement a readable stream, we require the Readable interface, and construct an object from it, and implement a read() method in the stream’s configuration parameter:
const { Readable } = require('stream');
const inStream = new Readable({
read() {}
});
inStream.push('ABCDEFGHIJKLM');
inStream.push('NOPQRSTUVWXYZ');
inStream.push(null); // No more data
inStream.pipe(process.stdout);
const inStream = new Readable({
read(size) {
this.push(String.fromCharCode(this.currentCharCode++));
if (this.currentCharCode > 90) {
this.push(null);
}
}
});
inStream.currentCharCode = 65;
inStream.pipe(process.stdout);
Writable streams are an abstraction for a destination to which data is written.
const myStream = fs.createWriteStream('file');
myStream.write('some data');
myStream.write('some more data');
myStream.end('done writing data');
Events
Writable Streams
Implementing Writable Streams
Class-based / utils
Note: the Writable constructor does not have use _ for the method names
const { Writable } = require('stream');
const dest = [];
const writer = new Writable({
write(chunk, _, cb) {
dest.push(chunk);
cb();
},
writev(chunks, cb) {
dest.push(chunks);
cb();
}
});
A Duplex stream is one that implements both Readable and Writable, such as a TCP socket connection.
Methods to implement
A Transform stream is a Duplex stream where the output is computed in some way from the input.
Examples include zlib streams or crypto streams that compress, encrypt, or decrypt data.
Implementing a Transform Stream
const capitalise = new Transform({
transform(chunk, enc, cb) {
this.push(chunk.toString().toUpperCase());
cb();
},
decodeStrings: true,
});
Readable.from
By default, objectMode is set to true
Readable.from(iterable, [options]) it’s a utility method for creating Readable Streams out of iterators, which holds the data contained in iterable.
async function * generate() {
yield 'hello';
yield 'streams';
}
const readable = Readable.from(generate());
readable.on('data', (chunk) => {
console.log(chunk);
});
A function to get notified when a stream is no longer readable, writable or has experienced an error or a premature close event.
Especially useful in error handling scenarios where a stream is destroyed prematurely (like an aborted HTTP request), and will not emit 'end' or 'finish'.
const { finished } = require('stream');
const rs = fs.createReadStream('archive.tar');
finished(rs, (err) => {
if (err) {
console.error('Stream failed.', err);
} else {
console.log('Stream is done reading.');
}
});
rs.resume(); // Drain the stream.
callback <Function> Called when the pipeline is fully done.
pipeline(
fs.createReadStream('file'),
zlib.createGzip(),
fs.createWriteStream('outfile.gz'),
(err) => {
if (err) {
console.error('Pipeline failed', err);
} else {
console.log('Pipeline succeeded');
}
}
);
Using async iterators as streams
async function* cap(stream) {
for await (const chunk of stream) {
yield chunk.toString().toUpperCase();
}
}
pipeline(
createReadStream('filetoread.txt'),
cap,
createWriteStream('pipeline.txt'),
() => {
console.log('pipeline done');
}
);