├── api-style.markdown ├── index.js ├── package.json ├── readme.markdown ├── states.markdown ├── stream_spec.md └── test ├── readable.js └── writable.js /api-style.markdown: -------------------------------------------------------------------------------- 1 | # Stream API Style. 2 | 3 | I've been writing lots of modules that act interact with, through, or on streams. 4 | 5 | here are my opinions on the right was to build a stream related interface. 6 | 7 | these are my principles: 8 | 9 | 1. be idiomatic: follow an api from node core, where possible. 10 | 2. be obvious: choose a pattern that makes the behaviour clear. 11 | 3. be prime: the length of a list needs to be a non even prime, or it looks weird. 12 | 13 | ## function that creates a Stream 14 | 15 | Call a method that returns a stream. 16 | where possible, use this pattern. 17 | 18 | A stream should be created and then piped to other streams. 19 | passing a stream to a function should be avoided. 20 | 21 | ``` js 22 | var stream = net.connect(port, host) 23 | 24 | //good 25 | stream.pipe(dnode({hi: hello})).pipe(stream) 26 | 27 | //un-good 28 | myRpc({hi: hello}, stream) 29 | 30 | //double-un-good 31 | 32 | myRpc({hi: hello}, port, host) 33 | 34 | ``` 35 | 36 | using the form `createTYPEStream` is encouraged. 37 | The call must return a stream synchronously, 38 | and then buffer writes until writes can be made. 39 | 40 | Don't if you are building a highlevel api with streaming something 41 | (like RPC) try and use this pattern. How your thing works is less 42 | obvious if you pass the stream to it. (exactly what does it do to the stream? 43 | you have to read the code. better to interact in a generic way, like `Stream#pipe`) 44 | 45 | Also, do not callback a stream asynchronously 46 | 47 | ``` js 48 | 49 | createStream(function (err, stream) { 50 | //you don't need callbacks if you are using streams. 51 | //emit the error on the stream instead. 52 | }) 53 | ``` 54 | 55 | if you find your self wanting to use this pattern try [streamin](https://github.com/fent/node-streamin) 56 | 57 | ### do 58 | 59 | * [dnode](https://github.com/substack/dnode) 60 | * [mux-demux](https://github.com/dominictarr/mux-demux) 61 | * [shoe](https://github.com/substack/shoe) 62 | 63 | ### don't 64 | 65 | * [smith](https://github.com/c9/smith) this could have returned a stream and piped to it. 66 | 67 | ## Server style - EventEmitter that emits streams 68 | 69 | Like a server, multiple streams are creating at unknown times. 70 | This is a familiar pattern with Servers, 71 | but is also seen with reconnectors. 72 | 73 | ``` js 74 | net.createServer(function (stream) { 75 | //you can pass in a function, 76 | //but it's just a shorthand for assigning an event listener 77 | }).listen(port) 78 | ``` 79 | 80 | returns an event emitter what emits streams, 81 | on an event named `'connection'`. 82 | If it is a server, it should have a method named `.listen(port, host, cb)` 83 | (be idiomatic: don't pass a port option to create server) 84 | 85 | 86 | ### Another Example - reconnect 87 | 88 | [reconnect](https://github.com/dominictarr/reconnect) also follows this pattern, 89 | except that the streams emitted are always one at a time. 90 | 91 | ``` js 92 | reconnect(function (stream) { 93 | //looks just like the server pattern, 94 | //except streams are emitted one after another. 95 | 96 | }).connect() 97 | ``` 98 | 99 | ### do 100 | 101 | * [reconnect](https://github.com/dominictarr/reconnect) 102 | * [mux-demux](https://github.com/dominictarr/mux-demux) 103 | * [autonode](https://github.com/dominictarr/autonode) 104 | 105 | ## mutate a stream - pass to a function 106 | 107 | Only pass a stream to a function when you couldn't possibly have used the other pattern. 108 | 109 | You don't really know what it does to the stream, 110 | because it's hidden inside the function. 111 | 112 | You will probably need to read the code, and that is hard work. 113 | It is much better to use one of the above patterns, if possible. 114 | 115 | [duplexer](https://github.com/Raynos/duplexer) creates a single readable, writable (duplex) stream from 116 | a WritableStream and a ReadableStream. That is pretty weird, right? 117 | This couldn't have been done with out passing the streams to a function, so it's okay. 118 | 119 | you may save a few lines by using this pattern, 120 | but they will be much less obvious. 121 | 122 | ``` js 123 | var duplex = require('duplexer') 124 | var cp = require('child_process') 125 | var child = cp.spawn(cmd, args) 126 | 127 | duplex(child.stdin, child.stdout) 128 | ``` 129 | 130 | ### do 131 | 132 | * [stream-spec](https://github.com/dominictarr/stream-spec) 133 | * [emit-stream](https://github.com/substack/emit-stream) 134 | * [duplexer](https://github.com/Raynos/duplexer) 135 | 136 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var a = require('assert') 2 | var macgyver = require('macgyver') 3 | var Stream = require('stream') 4 | 5 | function merge (to, from) { 6 | to = to || {} 7 | for (var k in from) 8 | if('undefined' === typeof to[k]) 9 | to[k] = from[k] 10 | return to 11 | } 12 | 13 | 14 | module.exports = function (stream, opts) { 15 | 16 | a.ok(stream instanceof Stream) 17 | a.ok('function', typeof stream.pipe) 18 | a.ok('function', typeof stream.destroy) 19 | 20 | var mac = macgyver() 21 | var opts = merge(('string' == typeof opts ? {name: opts} : opts) || {}, {name: 'stream'}) 22 | var spec = {} 23 | function add(name, method) { 24 | spec[name] = function (_opts) { 25 | method(mac, stream, merge(_opts, opts)) 26 | return this 27 | } 28 | } 29 | 30 | add('through' , throughSpec) 31 | add('basic' , throughSpec) //legacy, remove this. 32 | add('duplex' , duplexSpec) 33 | add('readable' , readableSpec) 34 | add('writable' , writableSpec) 35 | add('pausable' , pauseSpec) 36 | add('drainable' , drainSpec) 37 | add('strictPause' , strictSpec) 38 | 39 | spec.all = function (opts) { 40 | if(stream.writable && stream.readable) 41 | return this.through(opts).pausable(opts) 42 | else if(stream.writable) 43 | return this.writable().pausable() 44 | else 45 | return this.readable() 46 | } 47 | 48 | spec.validate = function () { 49 | mac.validate() 50 | return this 51 | } 52 | 53 | spec.validateOnExit = function () { 54 | //your test framework probably has assigned a listener for on exit also, 55 | //make sure we are first. so the framework has a chance to detect a 56 | //validation error. 57 | if(process.listeners) 58 | process.listeners('exit').unshift(function () { 59 | try { 60 | mac.validate() 61 | } catch (err) { 62 | console.error(err && err.stack) 63 | throw err 64 | } 65 | }) 66 | else 67 | setTimeout(mac.validate, 10e3) 68 | return this 69 | } 70 | 71 | return spec 72 | } 73 | 74 | function writableSpec (mac, stream, opts) { 75 | 76 | a.ok('function', typeof stream.destroy, opts.name + '.end *must* be a function') 77 | a.equal(stream.writable, true, opts.name + '.writable *must* == true') 78 | function e (n) { return opts.name + '.emit(\''+n+'\')' } 79 | function n (n) { return opts.name + '.'+n+'()' } 80 | 81 | stream.end = mac(stream.end, n('end')).returns(function () { 82 | a.equal(stream.writable, false, opts.name + ' must not be writable after end()') 83 | }).once() 84 | stream.write = 85 | mac(stream.write, n('write')) 86 | .throws(function (err, threw) { 87 | // a.equal(threw, !stream.writable, 'write should throw if !writable') 88 | }) 89 | 90 | var onClose = mac(function (){ 91 | if(opts.debug) console.error(e('close')) 92 | }, e('close')).once() 93 | var onError = mac(function (err){ 94 | if(opts.debug) console.error(e('error'), err) 95 | }, e('error')).before(onClose) 96 | 97 | stream.on('close', onClose) 98 | stream.on('error', onError) 99 | 100 | if(opts.error === false) 101 | onError.never() 102 | if(opts.error === true) 103 | onError.once() 104 | } 105 | 106 | function readableSpec (mac, stream, opts) { 107 | 108 | merge(opts, {end: true}) 109 | function e (n) { return opts.name + '.emit(\''+n+'\')' } 110 | function n (n) { return opts.name + '.'+n+'()' } 111 | 112 | var onError = mac(function (err){ 113 | //'error' means the same thing as 'close'. 114 | onClose.maybeOnce() 115 | if(opts.debug) console.error(e('error'), err) 116 | }, e('error')) 117 | //.before(onClose) error does not emit close, officially, yet. 118 | 119 | var onEnd = mac(function end (){ 120 | if(opts.debug) console.error(e('end'), err) 121 | }, e('end')) 122 | 123 | .isPassed(function () { 124 | a.equal(stream.readable, false, 'stream must not be readable on "end"') 125 | }) 126 | 127 | var onClose = mac(function (){ 128 | if(opts.debug) console.error(e('close')) 129 | }, e('close')) 130 | .once() 131 | 132 | //on end must occur before onClose or onError 133 | //that is to say, end MUST NOT occur after 'close' or 'error' 134 | 135 | onEnd.before(onClose).before(onError) 136 | 137 | var onData = mac(function data (){}, e('data')).before(onEnd) 138 | 139 | stream.on('close', onClose) 140 | stream.on('end', onEnd) 141 | stream.on('data', onData) 142 | 143 | if(opts.end !== false) onEnd.once() 144 | else onEnd.never() 145 | 146 | if(opts.error === false) 147 | onError.never() 148 | if(opts.error === true) 149 | onError.once() 150 | 151 | } 152 | 153 | function throughSpec (mac, stream, opts) { 154 | writableSpec(mac, stream, opts) 155 | readableSpec(mac, stream, opts) 156 | throughPauseSpec(mac, stream, opts) 157 | } 158 | 159 | function duplexSpec (mac, stream, opts) { 160 | writableSpec(mac, stream, opts) 161 | readableSpec(mac, stream, opts) 162 | pauseSpec(mac, stream, opts) 163 | drainSpec(mac, stream, opts) 164 | } 165 | 166 | function drainSpec (mac, stream, opts) { 167 | var paused = false 168 | function e (n) { return opts.name + '.emit(\''+n+'\')' } 169 | function n (n) { return opts.name + '.'+n+'()' } 170 | 171 | function drain() { 172 | paused = false 173 | } 174 | var onDrain = mac(drain).never() 175 | 176 | stream.on('drain', onDrain) 177 | stream.write = 178 | mac(stream.write, n('write')) 179 | .returns(function (written) { 180 | 181 | if(!paused && !written) { 182 | //after write returns false, it must emit drain eventually. 183 | onDrain.again() 184 | } 185 | paused = (written === false) 186 | }) 187 | 188 | } 189 | 190 | //for through-streams 191 | 192 | function throughPauseSpec (mac, stream, opts) { 193 | var paused = false 194 | 195 | function e (n) { return opts.name + '.emit(\''+n+'\')' } 196 | function n (n) { return opts.name + '.'+n+'()' } 197 | 198 | function drain() { 199 | paused = false 200 | } 201 | var onDrain = mac(drain, e('drain')).never() 202 | 203 | a.ok(stream.pause, 'stream *must* have pause') 204 | 205 | if(!stream.readable) 206 | throw new Error('pause does not make sense for a non-readable stream') 207 | 208 | stream.pause = mac(stream.pause, n('pause')) 209 | .isPassed(function () { 210 | if(paused) return 211 | //console.log('entered pause state by pause()') 212 | paused = true 213 | onDrain.again() 214 | }) 215 | 216 | stream.on('drain', onDrain) 217 | stream.write = 218 | mac(stream.write, n('write')) 219 | .returns(function (written) { 220 | 221 | if(!paused && !written) { 222 | //after write returns false, it must emit drain eventually. 223 | //console.log('entered pause state by write() === false') 224 | onDrain.again() 225 | } 226 | paused = (written === false) 227 | }) 228 | 229 | if(opts.strict) 230 | stream.on('data', function onData(data) { 231 | //stream must not emit data when paused! 232 | a.equal(paused, false, 'a strict stream *must not* emit \'data\' when paused') 233 | }) 234 | } 235 | /* 236 | demand that the stream does not emit any data when paused 237 | */ 238 | function pauseSpec (mac, stream, opts) { 239 | var paused = false 240 | function e (n) { return opts.name + '.emit(\''+n+'\')' } 241 | function n (n) { return opts.name + '.'+n+'()' } 242 | 243 | if(!stream.readable) 244 | throw new Error('strict pause does not make sense for a non-readable stream') 245 | 246 | stream.pause = mac(stream.pause) 247 | .isPassed(function () { 248 | paused = true 249 | }) 250 | stream.resume = mac(stream.resume) 251 | .isPassed(function () { 252 | paused = false 253 | }) 254 | if(opts.strict) 255 | stream.on('data', function () { 256 | a.equal(paused, false, 'a strict pausing stream must not emit data when paused') 257 | }) 258 | } 259 | 260 | function strictSpec (mac, stream, opts) { 261 | return pauseSpec(mac, stream, merge(opts, {strict: true})) 262 | } 263 | 264 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stream-spec", 3 | "version": "0.3.6", 4 | "description": "executable specification for Stream (to make testing streams easy)", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "node test/writable.js; node test/readable.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/dominictarr/stream-spec.git" 12 | }, 13 | "keywords": [ 14 | "stream", 15 | "spec", 16 | "specification", 17 | "streams", 18 | "test" 19 | ], 20 | "dependencies": { 21 | "macgyver": "~1.10" 22 | }, 23 | "author": "Dominic Tarr", 24 | "license": "(MIT OR Apache-2.0)" 25 | } 26 | -------------------------------------------------------------------------------- /readme.markdown: -------------------------------------------------------------------------------- 1 | # StreamSpec 2 | 3 | Automatic checking of the Stream implementations. 4 | `stream-spec` instruments your stream to verify that it has correct behaviour. 5 | All you need to do to test a stream is to wrap it with `stream-spec`, and then pipe 6 | some test data through it. 7 | it's purpose it to make it easy to test user-land streams have correct behavour. 8 | 9 | correct stream behaviour [illustrated](https://github.com/dominictarr/stream-spec/blob/master/states.markdown) 10 | 11 | correct stream behaviour [explained](https://github.com/dominictarr/stream-spec/blob/master/stream_spec.md) 12 | 13 | stream api design [style](https://github.com/dominictarr/stream-spec/blob/master/api-style.markdown) 14 | 15 | ## a simple test 16 | 17 | using [stream-tester](https://github.com/dominictarr/stream-tester) 18 | 19 | ``` js 20 | var spec = require('stream-spec') 21 | var tester = require('stream-tester') 22 | 23 | spec(stream) 24 | .through({strict: true}) 25 | .validateOnExit() 26 | 27 | tester.createRandomStream(function () { 28 | return 'line ' + Math.random() + '\n' 29 | }, 1000) 30 | .pipe(stream) 31 | .pipe(tester.createPauseStream()) 32 | 33 | ``` 34 | 35 | send 1000 random lines through the stream and check that 36 | it buffers on pause. 37 | 38 | ## types of `Stream` 39 | 40 | ### Writable (but not readable) 41 | 42 | a `WritableStream` must implement `write`, `end`, `destroy` and emit `'drain'` if it pauses, 43 | and `'close'` after the stream ends, or is destroyed. 44 | 45 | If the stream is sync (does no io) it probably does not need to pause, so the `write()` should never equal `false` 46 | 47 | ``` js 48 | spec(stream) 49 | .writable() 50 | .drainable() 51 | .validateOnExit() 52 | ``` 53 | 54 | ### Readable (but not writable) 55 | 56 | a `ReadableStream` must emit `'data'`, `'end'`, and implement `destroy`, 57 | and `'close'` after the stream ends, or is destroyed. 58 | is strongly recommended to implement `pause` and `resume` 59 | 60 | If the option `{strict: true}` is passed, it means the stream is not allowed to emit 61 | `'data'` or `'end'` when the stream is paused. 62 | 63 | If the option `{end: false}` is passed, then end may not be emitted. 64 | 65 | ``` js 66 | spec(stream) 67 | .readable() 68 | .pausable({strict: true})) //strict is optional. 69 | .validateOnExit() 70 | ``` 71 | 72 | ### Through (sync writable and readable, aka: 'filter') 73 | 74 | A `Stream` that is both readable and writable, and where the input is processed and then emitted as output, more or less directly. 75 | Example, [zlib](http://nodejs.org/api/zlib.html). contrast this with duplex stream. 76 | 77 | when you call `pause()` on a `ThroughStream`, it should change it into a paused state on the writable side also, 78 | and `write()===false`. Calling `resume()` should cause `'drain'` to be emitted eventually. 79 | 80 | If the option `{strict: true}` is passed, it means the stream is not allowed to emit 81 | `'data'` or `'end'` when the stream is paused. 82 | 83 | ``` js 84 | spec(stream) 85 | .through({strict: true}) //strict is optional. 86 | .validateOnExit() 87 | ``` 88 | 89 | ### Duplex 90 | 91 | A `Stream` that is both readable and writable, but the streams go off to some other place or thing, 92 | and are not coupled directly. The readable and writable side of a `DuplexStream` each have their own pause state. 93 | 94 | If the option `{strict: true}` is passed, it means the stream is not allowed to emit 95 | `'data'` or `'end'` when the stream is paused. 96 | 97 | ``` js 98 | spec(stream) 99 | .duplex({strict: true}) 100 | .validateOnExit() 101 | ``` 102 | 103 | ### other options 104 | 105 | ``` js 106 | spec(stream, name) //use name as the name of the stream in error messages. 107 | 108 | spec(stream, { 109 | name: name, //same as above. 110 | strict: true, //'data' nor 'end' may be emitted when paused. 111 | error: true, //this stream *must* error. 112 | error: false, //this stream *must not* error. 113 | //if neither error: boolean option is passed, the stream *may* error. 114 | }) 115 | 116 | 117 | 118 | ``` 119 | -------------------------------------------------------------------------------- /states.markdown: -------------------------------------------------------------------------------- 1 | # Stream state diagrams. 2 | 3 | interpretation of these diagrams: 4 | boxes are states. arrows are transitions. 5 | transitions are labled with the events which cause those 6 | transitions. if an event is mentioned on the diagram, 7 | but is not on a transition from a particular state, 8 | it means that event is forbidden in that state. 9 | 10 | if an event is not mentioned in a diagram, then 11 | that event is not forbidden, just irrelevant. 12 | 13 | where events are listed next to a transition, it 14 | means that any of those events may cause that transition. 15 | 16 | combining diagrams: I have represented stream behaviour 17 | across multiple diagrams for simplicity. 18 | Stream implementations must satisify all relevant diagrams. 19 | 20 | ## write / pause 21 | 22 | ``` 23 | write()!==false write()===false 24 | .--. .--. 25 | | | | | 26 | | v | v 27 | .---------. write()===false .--------. 28 | | |----------------->| | 29 | | !paused | | paused | 30 | | |<-----------------| | 31 | `---------` 'drain' `--------` 32 | 33 | ``` 34 | A `WritableStream` must emit `'drain'` to leave the paused state. 35 | A `WritableStream` may only return `false` from `write()` when paused. 36 | It is recommended that when a `WritableStream` in not paused, it should return `true`. 37 | 38 | ## writable/!writable 39 | 40 | ``` 41 | write(), 'drain' 42 | .--. 43 | | | 44 | | v 45 | .----------. .-----------. 46 | | |--------------->| | 47 | | writable | end(), | !writable | 48 | | | destroy() | | 49 | `----------` `-----------` 50 | | | 51 | | | 'error', 'close' 52 | | v 53 | | .-----------. 54 | | 'error' | | 55 | `-------------------->| closed, | 56 | | !writable | 57 | | | 58 | `-----------` 59 | 60 | ``` 61 | 62 | 63 | A `WritableStream` may not emit 'drain' or permit `write()` after `end()`, `'error'` 64 | or `destroy()` have occured. 65 | A `WritableStream` must eventually emit `'close'`, unless there is an 'error'. 66 | 67 | `write()` may throw, if called when `!writable`, `end()` should just be ignored 68 | if called when `!writable`, in 0.8 and earlier [pipe](https://github.com/joyent/node/blob/master/lib/stream.js#L66) 69 | does not check writable state before calling `end()` 70 | 71 | ## read / pause (strict) 72 | 73 | ``` 74 | 'data', 'end' 75 | .--. 76 | | | 77 | | v 78 | .---------. pause() .--------. 79 | | |------------>| | 80 | | !paused | | paused | 81 | | |<------------| | 82 | `---------` resume() `--------` 83 | 84 | ``` 85 | 86 | A strict `ReadableStream` must not emit 'data' or 'end' when in the paused state. 87 | In `0.8` node http streams are now strict. 88 | 89 | ## read / pause (loose) 90 | 91 | ``` 92 | 'data', 'end', 93 | pause(), resume() 94 | .--. 95 | | | 96 | | v 97 | .---------. 98 | | | 99 | | paused, | 100 | | !paused | 101 | `---------` 102 | 103 | ``` 104 | 105 | A unstrict `ReadableStream` should _try_ not to emit 'data' or 'end' when paused. 106 | most streams are unstrict. 107 | 108 | ## readable / !readable 109 | 110 | ``` 111 | 'data' 112 | .--. 113 | | | 114 | | v 115 | .----------. .-----------. 116 | | |--------------->| | 117 | | readable | 'end', | !readable | 118 | | | destroy() | | 119 | `----------` `-----------` 120 | | | 121 | | | 'error', 'close' 122 | | v 123 | | .-----------. 124 | | 'error' | | 125 | `-------------------->| closed, | 126 | | !readable | 127 | | | 128 | `-----------` 129 | 130 | ``` 131 | 132 | A `ReadableStream` must not emit 'data' after 'end', 'error', or destroy(). 133 | A `ReadableStream` must then eventually emit `'close'`, unless there was an 'error'. 134 | 135 | -------------------------------------------------------------------------------- /stream_spec.md: -------------------------------------------------------------------------------- 1 | # a `Stream` spec 2 | 3 | This document defines the behaviour that a `Stream` must implement in order to be compatible with `Stream#pipe`. 4 | This is not an official document, but is intended as a guide to produce correct behaviour in user-land streams. 5 | 6 | ## Stream 7 | 8 | All streams *must* emit `'error'` if writing to or reading from becomes physically impossible. 9 | `'error'` implys that the stream has ended, do not emit `'end'` after error, `'close'` may be emitted. 10 | 11 | All streams *should* emit `'close'`. 12 | `'close'` means that any underlying resources have been disposed of. 13 | `'close'` must be emitted either after end, or instead of `'end'`. 14 | 15 | Emitting `'close'` without `'end'` indicates a broken pipe - `Stream#pipe` will call `dest.destroy()` 16 | 17 | If a `ReadableStream` has ended normally, it *must not* emit `'close'` before `'end'`. 18 | 19 | A `Stream` *must not* emit `'error'` if the error is recoverable. 20 | (that is not in the stream spec) 21 | 22 | All streams *should* implement `destroy` but a `WritableStream` *must* implement `destroy`. 23 | 24 | ### emit('error') 25 | 26 | All streams *must* emit `'error'` when an error that is not recoverable has occurred. 27 | If it has become physically impossible to write to or read from the `Stream`, then emit `'error'`. 28 | 29 | A `WriteableStream` *may* throw an error if `write` has been called after `end`. 30 | (which should never happen, in correct usage) 31 | 32 | otherwise, a stream *must never* throw an error. (always emit) 33 | 34 | ## WritableStream 35 | 36 | A `WritableStream` *must* implement methods `write`, `end`, and `destroy`, 37 | and `writable` *must* be set to `true`, and *must* inherit `Stream#pipe` 38 | 39 | ### write(data) 40 | 41 | `write` must return either `true` or `false`. 42 | (if `false` then the writer *should* pause) 43 | If `write` is called after end, an error *may* be thrown. 44 | 45 | If `write` returns `false`,it *must* eventually emit `'drain'`. 46 | `write` returning `false` means the stream is paused. 47 | paused means (or downstream) is at capacity, 48 | and the writer/upstream *should attempt* to slow down or stop. 49 | It does not mean all data must be buffered, although that is something a stream may reasonably do. 50 | 51 | ### end() 52 | 53 | Calling `end` *must* set `writable` to `false`. 54 | If the `Stream` is also readable, it *must* eventually emit `'end'`, and then `'close'`. 55 | If the `Stream` in not also readable, it *must* eventually emit `'close'` but not emit `'end'`. 56 | 57 | ### destroy() 58 | 59 | Used to dispose of a `Stream`. 60 | 61 | Calling `destroy` *must* dispose of any underlying resources. 62 | Calling `destroy` *must* emit `'close'` eventually, 63 | once any underlying resources are disposed of. 64 | 65 | 66 | ### emit ('drain') 67 | 68 | After pausing, a `Stream` must eventually emit `'drain'`. 69 | For example, when if a call to `write() === false`, 70 | `Stream#pipe` will call `pause` on the source and 71 | then call `source.resume()`, when the dest emits `'drain'`. 72 | 73 | If drain is not emitted correctly, it's possible for `'data'` events to stop coming 74 | (depending on the source's behaviour when paused). 75 | 76 | ## ReadableStream 77 | 78 | A `ReadableStream` *must* inherit `pipe` from `Stream`, 79 | and set `readable` to `true`, and *must* emit zero or more 'data' events, 80 | followed by a single `end` event. A `ReadableStream` *may* implement `pause` and `resume` methods. 81 | 82 | * I will not bother to specify the behaviour of `pipe` because I am attempting to document what must be done in order for your `Stream` to be compatible with `pipe`. 83 | 84 | ### emit('data', data) 85 | 86 | A `ReadableStream` *may* emit one or more `'data'` events. 87 | A `ReadableStream` *must not* emit emit a `'data'` event after it has emitted `'end'` 88 | 89 | ### emit('end') 90 | 91 | A `ReadableStream` *should* emit an `'end'` event when it is not going to emit any more `'data'` events. 92 | `'end'` *must not* be emitted more than once. 93 | A `ReadableStream` may set `readable` to `false` after it has emitted the `'end'` event. 94 | 95 | Also, a `Stream` should internally call `destroy` after it has emitted `'end'`. 96 | 97 | ### emit ('close') 98 | 99 | A `ReadableStream` *must* emit a `'close'` event after the `'end'` event. `'close'` *must* only be emitted once. if `destroy` is called, `'close'` must be emitted, unless the stream has already ended normally. If `'close'` is emitted before `'end'` that signifies a broken stream, this *should* only happen if `destroy` was called. 100 | 101 | Emitting close will cause `pipe` to call `destroy` on the down stream pipe, if it is emitted before `end`. 102 | 103 | ### pause() 104 | 105 | A readable `Stream` *may* implement the `pause` method. 106 | When `pause` is called, the stream should attempt to emit `'data'` less often. 107 | (possibly stopping altogether until `resume` is called) 108 | 109 | ### resume() 110 | 111 | A `ReadableStream` *may* implement the `resume` method. 112 | If the `Stream` has been paused, it may now emit `'data'` more often, 113 | or commence emitting `data` if it has stopped all together. 114 | 115 | If a stream is also writable, and has returned `false` on `write` it *must* now eventually emit `drain` 116 | 117 | ### destroy() 118 | 119 | A `ReadableStream` *should* implement `destroy`. 120 | 121 | Calling `destroy` *must* dispose of any underlying resources. 122 | Calling `destroy` *must* emit `'close'` eventually, 123 | once any underlying resources are disposed of. 124 | 125 | -------------------------------------------------------------------------------- /test/readable.js: -------------------------------------------------------------------------------- 1 | 2 | var Stream = require('stream') 3 | var spec = require('..') 4 | 5 | var tests = 0 6 | function pass(message) { 7 | console.log('ok ' + ++tests, message ? ' -- ' + message : '') 8 | } 9 | function fail(message) { 10 | console.log('not ok ' + ++tests, message ? ' -- ' + message : '') 11 | } 12 | 13 | function checkValid(contract, expectFail) { 14 | return function (create, test) { 15 | var stream = create() 16 | try { 17 | var validate = contract(stream, spec).validate 18 | test(stream) 19 | validate() 20 | } catch (err) { 21 | if(!expectFail) { 22 | fail(err.message); throw err 23 | } 24 | return pass(err.message) 25 | } 26 | if(expectFail) { 27 | throw new Error('expected error') 28 | } 29 | pass('valid') 30 | } 31 | } 32 | 33 | var valid = checkValid(function (stream, spec) { 34 | return spec(stream).readable() 35 | }, false) 36 | 37 | var invalid = checkValid(function (stream, spec) { 38 | return spec(stream).readable() 39 | }, true) 40 | 41 | //does not emit 'end' 42 | invalid(function () { 43 | var s = new Stream() 44 | return s 45 | }, function (s) { 46 | }) 47 | 48 | //does not set readable = false, on 'end' 49 | invalid(function () { 50 | var s = new Stream() 51 | s.destroy = function () {} 52 | s._end = function () { 53 | this.emit('end') 54 | } 55 | return s 56 | }, function (s) { 57 | s._end() 58 | }) 59 | 60 | //does not set emit 'close' 61 | invalid(function () { 62 | var s = new Stream() 63 | s.destroy = function () {} 64 | s._end = function () { 65 | this.readable = false 66 | this.emit('end') 67 | } 68 | return s 69 | }, function (s) { 70 | s._end() 71 | }) 72 | 73 | valid(function () { 74 | var s = new Stream() 75 | s.destroy = function () {} 76 | s._end = function () { 77 | this.readable = false 78 | this.emit('end') 79 | this.emit('close') 80 | } 81 | return s 82 | }, function (s) { 83 | s._end() 84 | }) 85 | 86 | 87 | -------------------------------------------------------------------------------- /test/writable.js: -------------------------------------------------------------------------------- 1 | 2 | var Stream = require('stream') 3 | var spec = require('..') 4 | 5 | var tests = 0 6 | function pass(message) { 7 | console.log('ok ' + ++tests, message ? ' -- ' + message : '') 8 | } 9 | function fail(message) { 10 | console.log('not ok ' + ++tests, message ? ' -- ' + message : '') 11 | } 12 | 13 | function checkValid(contract, expectFail) { 14 | return function (create, test) { 15 | var stream = create() 16 | try { 17 | var validate = contract(stream, spec).validate 18 | test(stream) 19 | validate() 20 | } catch (err) { 21 | if(!expectFail) { 22 | fail(err.message); throw err 23 | } 24 | return pass(err.message) 25 | } 26 | if(expectFail) { 27 | throw new Error('expected error') 28 | } 29 | } 30 | } 31 | 32 | var valid = checkValid(function (stream, spec) { 33 | return spec(stream).writable() 34 | }, false) 35 | 36 | var invalid = checkValid(function (stream, spec) { 37 | return spec(stream).writable() 38 | }, true) 39 | 40 | var wrong1 = function () { 41 | var s = new Stream() 42 | s.write = function () {} 43 | s.destroy = function () {} 44 | //s.end = function () {} 45 | //s.writable = true 46 | return s 47 | } 48 | var wrong2 = //fails because end isn't defined 49 | invalid(function () { 50 | var s = new Stream() 51 | s.write = function (){ } 52 | s.destroy = function () {} 53 | //s.end = function () {} 54 | s.writable = true 55 | return s 56 | }, function (s) {}) 57 | 58 | //fails because end isn't defined 59 | invalid(function () { 60 | var s = new Stream() 61 | s.write = function (){ } 62 | s.destroy = function () {} 63 | s.end = function () {} 64 | //s.writable = true 65 | return s 66 | }, function (s) {}) 67 | 68 | 69 | //fails because end isn't called 70 | invalid(function () { 71 | var s = new Stream() 72 | s.write = function (){ } 73 | s.destroy = function () {} 74 | s.end = function () {} 75 | s.writable = true 76 | return s 77 | } 78 | , function (s) { 79 | s.write('hello') 80 | }) 81 | 82 | //fails because end doesn't set writable = false 83 | invalid(function () { 84 | var s = new Stream() 85 | s.write = function (){ } 86 | s.destroy = function () {} 87 | s.end = function () {} 88 | s.writable = true 89 | return s 90 | }, function (s) { 91 | s.write('hello') 92 | s.end() 93 | }) 94 | //fails because does not emit 'close' 95 | invalid(function () { 96 | var s = new Stream() 97 | s.write = function (){ } 98 | s.destroy = function () {} 99 | s.end = function () {this.writable = false} 100 | s.writable = true 101 | return s 102 | }, function (s) { 103 | s.write('hello') 104 | s.end() 105 | }) 106 | 107 | //passes 108 | valid(function () { 109 | var s = new Stream() 110 | s.write = function (){ } 111 | s.destroy = function () {} 112 | s.end = function () { 113 | this.writable = false 114 | this.emit('close') 115 | } 116 | s.writable = true 117 | return s 118 | }, function (s) { 119 | s.write('hello') 120 | s.end() 121 | }) 122 | 123 | --------------------------------------------------------------------------------