├── stream.png ├── server.js ├── package.json ├── crypto.js ├── .gitignore ├── Transform.js ├── Readable.js ├── Writable.js ├── LICENSE ├── index.js └── README.md /stream.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perguth/node-streams/HEAD/stream.png -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var http = require('http') 2 | var crypto = require('./crypto') 3 | 4 | var server = http.createServer(function (req, res) { 5 | // process.stdout.write(req.url.substr(1)) 6 | process.stdout.write(crypto.decrypt(req.url.substr(1))) 7 | 8 | res.writeHead(200, {'Access-Control-Allow-Origin': '*'}) 9 | res.end() 10 | }) 11 | 12 | server.listen(9090) 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-streams", 3 | "version": "1.0.0", 4 | "description": "Summary and examples regarding NodeJS Streams.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Per Guth ", 10 | "license": "MIT", 11 | "dependencies": { 12 | "budo": "^8.2.1", 13 | "crypto": "0.0.3", 14 | "filestream": "^4.1.3", 15 | "http": "0.0.0", 16 | "inherits": "^2.0.1", 17 | "request": "^2.72.0", 18 | "stream": "0.0.2" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /crypto.js: -------------------------------------------------------------------------------- 1 | var crypto = require('crypto') 2 | var algorithm = 'aes192' 3 | 4 | module.exports = { 5 | encrypt, 6 | decrypt 7 | } 8 | 9 | function decrypt (ciphertext) { 10 | var decipher = crypto.createDecipher(algorithm, 'pw') 11 | var plaintext = decipher.update(ciphertext, 'hex', 'utf8') 12 | plaintext += decipher.final('utf8') 13 | return plaintext 14 | } 15 | 16 | function encrypt (plaintext) { 17 | var encipher = crypto.createCipher(algorithm, 'pw') 18 | var ciphertext = encipher.update(plaintext, 'utf8', 'hex') 19 | ciphertext += encipher.final('hex') 20 | return ciphertext 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | 35 | # Live coding files 36 | *.live.js 37 | -------------------------------------------------------------------------------- /Transform.js: -------------------------------------------------------------------------------- 1 | var Transform = require('stream').Transform 2 | 3 | // -- Long version -- 4 | var inherits = require('inherits') 5 | 6 | inherits(MyTransform, Transform) 7 | 8 | function MyTransform () { 9 | Transform.call(this) 10 | 11 | this._transform = function (buffer, encoding, next) { 12 | // do something with buffer 13 | this.push(buffer) 14 | next() 15 | } 16 | } 17 | 18 | var transformStream = new MyTransform() 19 | 20 | // -- Short version -- 21 | var transformStream2 = new Transform({ 22 | transform: (buffer, encoding, next) => { 23 | // do something with buffer 24 | this.push(buffer) 25 | next() 26 | } 27 | }) 28 | 29 | // -- Methods -- 30 | 31 | // ._transform(chunk, encoding, callback) 32 | // ._flush(callback) 33 | 34 | // -- Events -- 35 | 36 | // finish 37 | // end 38 | -------------------------------------------------------------------------------- /Readable.js: -------------------------------------------------------------------------------- 1 | var Readable = require('stream').Readable 2 | 3 | // -- Long version -- 4 | var inherits = require('inherits') 5 | 6 | inherits(MyReadable, Readable) 7 | 8 | function MyReadable () { 9 | Readable.call(this) 10 | 11 | this._read = function () { 12 | var data = 'payload' 13 | this.push(data) 14 | this.push(null) 15 | } 16 | } 17 | var readStream = new MyReadable() 18 | 19 | // -- Short version -- 20 | var readStream2 = new Readable({ 21 | read: function () { 22 | var data = 'payload' 23 | this.push(data) 24 | this.push(null) 25 | } 26 | }) 27 | 28 | // -- Methods -- 29 | 30 | // .read([size]) 31 | // .setEncoding(encoding) 32 | // .pause() 33 | // .isPaused() 34 | // .resume() 35 | // .readable.unshift(chunk) 36 | // .pipe(destination[, options]) 37 | // .unpipe([destination]) 38 | 39 | // -- Events -- 40 | 41 | readStream.on('data', (buff) => { 42 | console.log(buff.toString()) 43 | }) 44 | 45 | // data 46 | // end 47 | // close 48 | // error 49 | // readable 50 | -------------------------------------------------------------------------------- /Writable.js: -------------------------------------------------------------------------------- 1 | var Writable = require('stream').Writable 2 | 3 | // -- Long version -- 4 | var inherits = require('inherits') 5 | 6 | inherits(MyWritable, Writable) 7 | 8 | function MyWritable () { 9 | Writable.call(this) 10 | 11 | this._write = (buffer, encoding, next) => { 12 | console.log(buffer) 13 | next() 14 | } 15 | } 16 | var writeStream = new MyWritable() 17 | 18 | // -- Short version -- 19 | var writeStream2 = new Writable({ 20 | write: (buffer, encoding, next) => { 21 | console.log(buffer) 22 | next() 23 | } 24 | }) 25 | 26 | // -- Methods -- 27 | 28 | var data = 'payload' 29 | writeStream.write(data) 30 | 31 | // .write(chunk[, encoding][, callback]) -> 32 | // .setDefaultEncoding(encoding) 33 | // .end([chunk][, encoding][, callback]) 34 | // .cork() 35 | // .uncork() 36 | 37 | // -- Events -- 38 | 39 | writeStream.on('drain', () => { 40 | writeStream.write(data) 41 | }) 42 | 43 | // drain 44 | // finish 45 | // error 46 | // finish 47 | // pipe 48 | // unpipe 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Per 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // budo index.js --live --host localhost 2 | 3 | function firstExamples () { 4 | var Reader = require('filestream').read 5 | 6 | var uploadBtn = document.createElement('input') 7 | uploadBtn.setAttribute('type', 'file') 8 | uploadBtn.setAttribute('onchange', 'window.sendFile(event)') 9 | var a = document.body.appendChild(uploadBtn) 10 | 11 | window.sendFile = (event) => { 12 | var readStream = new Reader(event.target.files[0]) 13 | 14 | function missOutOnChunks () { 15 | readStream.on('end', () => console.log('You missed out, bro!')) 16 | 17 | readStream.resume() 18 | setTimeout(() => { 19 | readStream.on('end', () => console.log('end')) 20 | readStream.on('data', (buff) => console.log(buff)) 21 | }, 300) 22 | } 23 | // missOutOnChunks() 24 | 25 | function readWithoutReadable () { 26 | console.log(readStream.read()) 27 | setTimeout(() => console.log(readStream.read()), 300) 28 | } 29 | // readWithoutReadable() 30 | 31 | function fastestMethod () { 32 | readStream.on('data', (chunk) => console.log(chunk)) 33 | } 34 | // fastestMethod() 35 | 36 | function explicitlyPaused () { 37 | readStream.pause() 38 | readStream.on('data', (chunk) => console.log(chunk)) 39 | } 40 | // explicitlyPaused() 41 | 42 | function readOnReadable () { 43 | readStream.on('end', () => console.log('end')) 44 | readStream.on('readable', () => console.log('readable!', readStream.read())) 45 | } 46 | // readOnReadable() 47 | 48 | function dataEventOnRead () { 49 | readStream.pause() 50 | readStream.on('data', (chunk) => console.log('data event', chunk)) 51 | 52 | var once = false 53 | readStream.on('readable', () => { 54 | if (once--) return 55 | console.log('.read()', readStream.read()) 56 | }) 57 | } 58 | // dataEventOnRead() 59 | } 60 | } 61 | // firstExamples() 62 | 63 | function basicReadWriteStreams () { 64 | var inherits = require('inherits') 65 | var Writable = require('stream').Writable 66 | var Readable = require('stream').Readable 67 | var PassThrough = require('stream').PassThrough 68 | 69 | inherits(MyReadable, Readable) 70 | function MyReadable (opts) { 71 | Readable.call(this, opts) 72 | 73 | this._read = (size) => { 74 | this.push('Hello') 75 | this.push('Hanqing!') 76 | this.push(null) 77 | } 78 | } 79 | 80 | inherits(MyWritable, Writable) 81 | function MyWritable (opts) { 82 | Writable.call(this, opts) 83 | 84 | this._write = (chunk, _, next) => { 85 | console.log( 86 | chunk// .toString() 87 | ) 88 | next() 89 | } 90 | } 91 | 92 | inherits(MyPassThrough, PassThrough) 93 | function MyPassThrough (opts) { 94 | PassThrough.call(this, opts) 95 | 96 | this.on('data', (buff) => console.log('peek:', 97 | buff// .toString() 98 | )) 99 | } 100 | 101 | var rs = new MyReadable( 102 | // {encoding: 'utf8'} 103 | ) 104 | var ws = new MyWritable( 105 | // {decodeStrings: false} 106 | ) 107 | var ps = new MyPassThrough( 108 | {encoding: 'utf8'} 109 | ) 110 | 111 | rs 112 | .pipe(ps) 113 | .pipe(ws) 114 | } 115 | // basicReadWriteStreams() 116 | 117 | function simpleReadWriteStreams () { 118 | var Writable = require('stream').Writable 119 | var Readable = require('stream').Readable 120 | var PassThrough = require('stream').PassThrough 121 | 122 | var rs = new Readable({ 123 | read: function (size) { 124 | this.push('Hello') 125 | this.push('Hanqing!') 126 | this.push(null) 127 | } 128 | }) 129 | var ws = new Writable({ 130 | write: (buff, _, next) => { 131 | console.log( 132 | buff// .toString() 133 | ) 134 | next() 135 | } 136 | // decodeStrings: false 137 | }) 138 | var ps = new PassThrough({ 139 | encoding: 'utf8' 140 | }) 141 | ps.on('data', (buff) => { 142 | console.log('peek:', 143 | buff// .toString() 144 | ) 145 | }) 146 | 147 | rs 148 | .pipe(ps) 149 | .pipe(ws) 150 | } 151 | // simpleReadWriteStreams() 152 | 153 | function referat () { 154 | // -- Readable -- 155 | var Readable = require('stream').Readable 156 | 157 | var rs = new Readable({ 158 | read: function (size) { 159 | this.push('test') 160 | this.push(null) 161 | } 162 | }) 163 | 164 | // -- Writable -- 165 | var Writable = require('stream').Writable 166 | var request = require('request') 167 | 168 | var ws = new Writable({ 169 | write: (buffer, _, next) => { 170 | request('http://localhost:9090/' + buffer.toString()) 171 | next() 172 | } 173 | }) 174 | 175 | // rs.pipe(ws) 176 | 177 | // -- HTML -- 178 | var button = document.createElement('input') 179 | button.setAttribute('type', 'file') 180 | button.setAttribute('onchange', 'window.sendFile(event)') 181 | document.body.appendChild(button) 182 | 183 | // -- Readable: File Stream -- 184 | var Reader = require('filestream').read 185 | 186 | window.sendFile = (event) => { 187 | var fs = new Reader(event.target.files[0], {chunkSize: 10}) 188 | 189 | fs.pipe(ps).pipe(ts).pipe(ws) 190 | } 191 | 192 | // -- Transform: Crypto -- 193 | var Transform = require('stream').Transform 194 | let crypto = require('./crypto') 195 | 196 | var ts = new Transform({ 197 | transform: function (buffer, _, next) { 198 | var ciphertext = crypto.encrypt(buffer.toString()) 199 | this.push(ciphertext) 200 | setTimeout(next, 300) 201 | }, 202 | highWaterMark: 10 203 | }) 204 | 205 | // -- Passthrough -- 206 | var PassThrough = require('stream').PassThrough 207 | 208 | var ps = new PassThrough() 209 | ps.on('data', (buff) => console.log('peek:', buff.toString())) 210 | } 211 | referat() 212 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Stream Design Illustration](stream.png) 2 | -- 3 | 4 | ##### Attached JS files 5 | 6 | - Main example: [index.js](./index.js), [server.js](./server.js) 7 | - Stream overview files, showing the API and provided events: 8 | - [Readables.js](./Readable.js) 9 | - [Writables.js](./Writable.js) 10 | - [Transform.js](./Transform.js) 11 | - A crypto wrapper: [crypto.js](./crypto.js) 12 | 13 | ##### Running examples 14 | 15 | ```sh 16 | git clone https://github.com/pguth/node-streams.git 17 | cd node-streams 18 | npm install 19 | npm install -g budo 20 | budo --live --host localhost index.js 21 | # in a second terminal: 22 | node server.js 23 | # Now go to http://localhost:9966 and select one of the text files with the file picker. 24 | # Within index.js uncomment the function calls that are of interest to you. 25 | ``` 26 | 27 | ## Cheatsheet 28 | 99% of this text stems from [https://nodejs.org/api/stream.html]() 29 | 30 | > Almost all Node.js programs, no matter how simple, use Streams in some way. 31 | 32 | All streams are instances of EventEmitter. You can load the Stream base classes by doing require('stream'). It is not necessary to implement Stream interfaces in order to consume streams in your programs. 33 | 34 | #### Three major concepts 35 | See: [Node.js streams demystified](https://gist.github.com/joyrexus/10026630) 36 | 37 | 1. **Source** - Where the data comes from. 38 | 2. **Pipeline** - Where you filter or transform your data as it passes through, 39 | 3. **Sink** - Where your data ultimately goes. 40 | 41 | #### Five classes of streams 42 | See: [Node.js streams demystified](https://gist.github.com/joyrexus/10026630) 43 | 44 | 1. **Readable** - sources 45 | 2. **Writable** - sinks 46 | 3. **Duplex** - both source and sink 47 | 4. **Transform** - in-flight stream operations 48 | 5. **Passthrough** - stream spy 49 | 50 | # Consume streams 51 | 52 | ## Readables 53 | 54 | Streams start out in paused mode. The fastest way to get data out of a readable is to subscribe to the `data` event. 55 | 56 | To inititate **flowing mode**: 57 | 58 | - Add a data event handle 59 | - Call `.resume()` 60 | - Call `.pipe()` to send data to a writable 61 | - If the stream has not been explicitly paused: attach a `data` event listener. 62 | 63 | Calling `.read()` does not switch the stream into flowing mode. 64 | 65 | To inititate **paused mode**: 66 | 67 | - Call `.pause()` 68 | If pipe desinations are set calling `.pause()` does not guarantee that the stream stays paused since they could ask for more data. 69 | - Remove any `data` event handlers *and* remove all pipe destinations by caling `.unpipe()`. 70 | 71 | **Strategies** for reading a readable: 72 | 73 | - In flowing mode: 74 | - Attach a `.pipe()` destination. 75 | - Subscribe to the `data` event (and unpause if explicitly paused). 76 | - In paused mode: 77 | - Subscribe to the `readable` event and call `.read()` on every event (until the `end` event fires). 78 | 79 | ### Events 80 | - `data` 81 | - `end` 82 | - `close` 83 | - `error` 84 | - `readable` 85 | 86 | ### Methods 87 | - `.pipe(destination[, options])` → \ destination stream 88 | Multiple destinations can be piped to safely. 89 | - options \ Pipe options 90 | - **end** \ End the writer when the reader ends. Default = true 91 | - `.unpipe([destination])` 92 | - `.read([size])` → \ | \ | \ 93 | This method should only be called in paused mode. In flowing mode, this method is called automatically until the internal buffer is drained. 94 | Causes a `data` event. 95 | The read() method pulls some data out of the internal buffer and returns it. If there is no data available, then it will return null. 96 | - `.isPaused()` → \ 97 | - `.pause()` → this 98 | - `.resume()` → this 99 | - `.setEncoding(encoding)` → this 100 | - `.readable.unshift(chunk)` 101 | Unlike stream.push(chunk), stream.unshift(chunk) will not end the reading process by resetting the internal reading state of the stream. Following the call to unshift() with an immediate stream.push('') will reset the reading state appropriately, however it is best to simply avoid calling unshift() while in the process of performing a read. 102 | - `.wrap(stream)` 103 | 104 | ## Writables 105 | 106 | ### Events 107 | 108 | - `drain` 109 | If `.write()` returned false, `drain` will indicate when to write again. 110 | - `error` 111 | - `finish` 112 | When `.end()` has been called and all data is flushed. 113 | - `pipe` → \ source stream 114 | When `.pipe()` is called 115 | - `unpipe` → \ source stream 116 | 117 | ### Methods 118 | 119 | - `.cork()` 120 | Forces buffering of all writes. Continues on `.uncork()` or `.end()`. 121 | - `.end([chunk][, encoding][, callback])` 122 | Callback for when the stream is finished. 123 | - `.setDefaultEncoding(encoding)` 124 | - `.uncork()` 125 | - `.write(chunk[, encoding][, callback])` → bool 126 | Callback for when this chunk of data is flushed. 127 | The return value indicates if you should continue writing right now. This return value is strictly advisory. Wait for the 'drain' event before writing more data. 128 | 129 | 130 | # Implement streams 131 | 132 | 1. **Extend** the appropriate parent class in your own subclass. 133 | 2. **Call** the appropriate parent class constructor in your constructor, to be sure that the internal mechanisms are set up properly. 134 | 3. **Implement** one or more specific methods, as detailed below. 135 | 136 | | Use-case | Class | Method(s) to implement 137 | | ------------------------- | --------- | ------------------------- 138 | | Reading only | Readable | \_read! 139 | | Writing only | Writable | \_write!, \_writev? 140 | | Reading and writing | Duplex | \_read!, \_write!, \_writev? 141 | | Operate on written data, 142 | then read the result | Transform | \_transform!, \_flush? 143 | 144 | **In your implementation code, it is very important to never call the methods described in API for Stream Consumers.** Otherwise, you can potentially cause adverse side effects in programs that consume your streaming interfaces. 145 | 146 | **Note:** Method prefixed with an underscore are internal to the class that defines it, and should not be called directly by user programs. However, you are expected to override this method in your own extension classes. 147 | 148 | 149 | ## Readable 150 | 151 | `stream.Readable.call(this, [options])` 152 | 153 | - options \ 154 | - **highWaterMark** \ The maximum number of bytes to store in the internal buffer before ceasing to read from the underlying resource. Default = 16384 (16kb), or 16 for objectMode streams 155 | - **encoding** \ If specified, then buffers will be decoded to strings using the specified encoding. Default = null 156 | - **objectMode** \ Whether this stream should behave as a stream of objects. Meaning that stream.read(n) returns a single value instead of a Buffer of size n. Default = false 157 | - **read** \ Implementation for the stream._read() method. 158 | - **Methods** 159 | - `._read([size])` 160 | When _read() is called, if data is available from the resource, the _read() implementation should start pushing that data into the read queue by calling this.push(dataChunk). _read() should continue reading from the resource and pushing data until push returns false, at which point it should stop reading from the resource. Only when _read() is called again after it has stopped should it start reading more data from the resource and pushing that data onto the queue. 161 | The size argument is advisory. 162 | - `.push(chunk[, encoding])` → \ Whether or not more pushes should be performed 163 | If a value other than null is passed, The push() method adds a chunk of data into the queue for subsequent stream processors to consume. If null is passed, it signals the end of the stream (EOF), after which no more data can be written. 164 | 165 | 166 | ## Writable 167 | 168 | `stream.Writable.call(this, [options])` 169 | 170 | - options \ 171 | - **highWaterMark** \ Buffer level when stream.write() starts returning false. Default = 16384 (16kb), or 16 for objectMode streams. 172 | - **decodeStrings** \ Whether or not to decode strings into Buffers before passing them to stream._write(). Default = true 173 | - **objectMode** \ Whether or not the stream.write(anyObj) is a valid operation. If set you can write arbitrary data instead of only Buffer / String data. Default = false 174 | - **write** \ Implementation for the stream._write() method. 175 | - **writev** \ Implementation for the stream._writev() method. 176 | - **Methods** 177 | - `._write(chunk, encoding, callback)` 178 | "chunk" Will always be a buffer unless the decodeStrings option was set to false and an encoding is given. 179 | Call the often "next" named callback function (optionally with an error argument) when you are done processing the supplied chunk. 180 | 181 | 182 | ## Duplex 183 | 184 | `stream.Duplex.call(this, [options])` 185 | 186 | - options \ 187 | - **allowHalfOpen** \ Default = true. If set to false, then the stream will automatically end the readable side when the writable side ends and vice versa. 188 | - **readableObjectMode** \ Default = false. Sets objectMode for readable side of the stream. Has no effect if objectMode is true. 189 | - **writableObjectMode** \ Default = false. Sets objectMode for writable side of the stream. Has no effect if objectMode is true. 190 | 191 | 192 | ## Transform 193 | 194 | `stream.Transform.call(this, [options])` 195 | 196 | - options \ 197 | - **transform** \ Implementation for the stream._transform() method. 198 | - **flush** \ Implementation for the stream._flush() method. 199 | - **Events** 200 | - `finish`, `end` 201 | The 'finish' and 'end' events are from the parent Writable and Readable classes respectively. The 'finish' event is fired after stream.end() is called and all chunks have been processed by stream.\_transform(), 'end' is fired after all data has been output which is after the callback in stream.\_flush() has been called. 202 | - **Methods** 203 | - `._transform(chunk, encoding, callback)` 204 | "chunk" will always be a buffer unless the decodeStrings option was set to false and an encoding is given. 205 | Call the callback function (optionally with an error argument and data) when you are done processing the supplied chunk. 206 | If you supply a second argument to the callback it will be passed to the push method. 207 | - `._flush(callback)` 208 | This will be called at the very end, after all the written data is consumed, but before emitting 'end' to signal the end of the readable side. Call the callback function (optionally with an error argument) when you are done flushing any remaining data. 209 | 210 | 211 | ## Simplified Constructor API 212 | 213 | In simple cases there is now the added benefit of being able to construct a stream without inheritance by passing the appropriate methods without the "_" as constructor options: 214 | 215 | ```js 216 | var duplex = new stream.Duplex({ 217 | read: function(n) { 218 | // sets this._read under the hood 219 | 220 | // push data onto the read queue, passing null 221 | // will signal the end of the stream (EOF) 222 | this.push(chunk); 223 | }, 224 | write: function(chunk, encoding, next) { 225 | // sets this._write under the hood 226 | 227 | // An optional error can be passed as the first argument 228 | next() 229 | } 230 | }); 231 | ``` 232 | 233 | 234 | ## Acknowledgements :fireworks: 235 | 236 | This material was produced as a preparation of myself for a presentation in a row of presentations held together with these cool guys: 237 | 238 | - [Streams Introduction](http://slides.com/queicherius/reactive-pattern#/) by [David](https://github.com/queicherius). 239 | - RxJS introduction by [Paul](https://github.com/paulsonnentag). 240 | --------------------------------------------------------------------------------