├── .gitignore ├── LICENSE ├── README.md ├── example.js ├── index.js ├── package-lock.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 Mathias Buus 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # debugging-stream 2 | 3 | Stream that helps you debug streams 4 | 5 | ``` 6 | npm install debugging-stream 7 | ``` 8 | 9 | ## Usage 10 | 11 | ``` js 12 | const DebuggingStream = require('debugging-stream') 13 | 14 | const s = new DebuggingStream(anotherStream, { 15 | latency: 100, // add between 100ms read/write latency, 16 | jitter: 10, // add 0-10 ms of jitter to the latency 17 | speed: 100, // send 100 bytes per sec 18 | writeSpeed, // same as speed but only affects writes 19 | readSpeed // same as speed but only affects reads 20 | }) 21 | ``` 22 | 23 | ## License 24 | 25 | MIT 26 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | const DebuggingStream = require('./') 2 | const speedometer = require('speedometer') 3 | const { Duplex } = require('streamx') 4 | 5 | const writeSpeed = speedometer() 6 | 7 | const observe = new Duplex({ 8 | write (data, cb) { 9 | console.log(writeSpeed(data.byteLength), data) 10 | cb(null) 11 | } 12 | }) 13 | 14 | const s = new DebuggingStream(observe, { latency: 2000, writeSpeed: 3 }) 15 | 16 | for (let i = 0; i < 30; i++) { 17 | s.write(Buffer.from([i])) 18 | } 19 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const { Duplex } = require('streamx') 2 | const FIFO = require('fast-fifo') 3 | 4 | class Queue { 5 | constructor ({ latency = 0, speed = Infinity, jitter = 0, ondrain } = {}) { 6 | this.latency = latency 7 | this.speed = speed 8 | this.jitter = jitter 9 | this.pending = new FIFO() 10 | this.inflight = 0 11 | this.timeout = null 12 | this.ondrain = ondrain 13 | } 14 | 15 | add (data) { 16 | const latency = Math.round((Math.random() * this.jitter) + this.latency) 17 | 18 | if (data) this.inflight += data.byteLength 19 | this.pending.push({ arriveBy: Date.now() + latency + Math.ceil(1000 * this.inflight / this.speed), data }) 20 | 21 | if (this.timeout === null) this._drain() 22 | } 23 | 24 | _drain () { 25 | this.timeout = null 26 | 27 | const now = Date.now() 28 | 29 | while (true) { 30 | const next = this.pending.peek() 31 | if (!next || next.arriveBy > now) break 32 | 33 | if (next.data) this.inflight -= next.data.byteLength 34 | this.ondrain(next.data) 35 | this.pending.shift() 36 | } 37 | 38 | const next = this.pending.peek() 39 | if (!next) return 40 | 41 | this.timeout = setTimeout(this._drain.bind(this), Math.max(next.arriveBy - now, 0)) 42 | } 43 | 44 | destroy () { 45 | clearTimeout(this.timeout) 46 | this.timeout = null 47 | } 48 | } 49 | 50 | module.exports = class DebuggingStream extends Duplex { 51 | constructor (stream, { speed = Infinity, writeSpeed = speed, readSpeed = speed, latency = 0, jitter = 0 } = {}) { 52 | super() 53 | 54 | this.stream = stream 55 | this.latency = latency 56 | this.jitter = jitter 57 | this.bytesWritten = 0 58 | this.bytesRead = 0 59 | this.writeSpeed = writeSpeed 60 | this.readSpeed = readSpeed 61 | this.udx = true // for hypercore 62 | 63 | this._writes = new Queue({ speed: writeSpeed, latency, jitter, ondrain: this._onwrite.bind(this) }) 64 | this._reads = new Queue({ speed: readSpeed, latency, jitter, ondrain: this._onread.bind(this) }) 65 | this._finalCallback = null 66 | 67 | stream.on('data', (data) => { 68 | this.bytesRead += data.byteLength 69 | this._reads.add(data) 70 | }) 71 | 72 | stream.on('end', () => { 73 | this._reads.add(null) 74 | }) 75 | 76 | stream.on('error', (err) => { 77 | this.destroy(err) 78 | }) 79 | } 80 | 81 | get rawStream () { 82 | return this.stream.rawStream || this 83 | } 84 | 85 | get rtt () { 86 | return this.latency + Math.round(Math.random() * this.jitter) 87 | } 88 | 89 | _write (data, cb) { 90 | this.bytesWritten += data.byteLength 91 | this._writes.add(data) 92 | cb(null) 93 | } 94 | 95 | _final (cb) { 96 | this._writes.add(null) 97 | this._finalCallback = cb 98 | } 99 | 100 | _continueFinal () { 101 | const cb = this._finalCallback 102 | this._finalCallback = null 103 | if (cb) cb(null) 104 | } 105 | 106 | _predestroy () { 107 | this._continueFinal() 108 | this._writes.destroy() 109 | this._reads.destroy() 110 | } 111 | 112 | _onread (data) { 113 | this.push(data) 114 | } 115 | 116 | _onwrite (data) { 117 | if (data === null) this._continueFinal() 118 | else this.stream.write(data) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "debugging-stream", 3 | "version": "3.1.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "debugging-stream", 9 | "version": "3.1.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "fast-fifo": "^1.3.2", 13 | "streamx": "^2.20.2" 14 | }, 15 | "devDependencies": { 16 | "speedometer": "^1.1.0" 17 | } 18 | }, 19 | "node_modules/bare-events": { 20 | "version": "2.5.0", 21 | "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.0.tgz", 22 | "integrity": "sha512-/E8dDe9dsbLyh2qrZ64PEPadOQ0F4gbl1sUJOrmph7xOiIxfY8vwab/4bFLh4Y88/Hk/ujKcrQKc+ps0mv873A==", 23 | "optional": true 24 | }, 25 | "node_modules/fast-fifo": { 26 | "version": "1.3.2", 27 | "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", 28 | "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==" 29 | }, 30 | "node_modules/queue-tick": { 31 | "version": "1.0.1", 32 | "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", 33 | "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==" 34 | }, 35 | "node_modules/speedometer": { 36 | "version": "1.1.0", 37 | "resolved": "https://registry.npmjs.org/speedometer/-/speedometer-1.1.0.tgz", 38 | "integrity": "sha512-z/wAiTESw2XVPssY2XRcme4niTc4S5FkkJ4gknudtVoc33Zil8TdTxHy5torRcgqMqksJV2Yz8HQcvtbsnw0mQ==", 39 | "dev": true 40 | }, 41 | "node_modules/streamx": { 42 | "version": "2.20.2", 43 | "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.20.2.tgz", 44 | "integrity": "sha512-aDGDLU+j9tJcUdPGOaHmVF1u/hhI+CsGkT02V3OKlHDV7IukOI+nTWAGkiZEKCO35rWN1wIr4tS7YFr1f4qSvA==", 45 | "dependencies": { 46 | "fast-fifo": "^1.3.2", 47 | "queue-tick": "^1.0.1", 48 | "text-decoder": "^1.1.0" 49 | }, 50 | "optionalDependencies": { 51 | "bare-events": "^2.2.0" 52 | } 53 | }, 54 | "node_modules/text-decoder": { 55 | "version": "1.2.1", 56 | "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.1.tgz", 57 | "integrity": "sha512-x9v3H/lTKIJKQQe7RPQkLfKAnc9lUTkWDypIQgTzPJAq+5/GCDHonmshfvlsNSj58yyshbIJJDLmU15qNERrXQ==" 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "debugging-stream", 3 | "version": "3.1.0", 4 | "description": "Debug streams, by adding latency, speed etc", 5 | "main": "index.js", 6 | "files": [ 7 | "index.js" 8 | ], 9 | "dependencies": { 10 | "fast-fifo": "^1.3.2", 11 | "streamx": "^2.20.2" 12 | }, 13 | "author": "Mathias Buus", 14 | "license": "MIT", 15 | "devDependencies": { 16 | "speedometer": "^1.1.0" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/mafintosh/debugging-stream.git" 21 | }, 22 | "bugs": { 23 | "url": "https://github.com/mafintosh/debugging-stream/issues" 24 | }, 25 | "homepage": "https://github.com/mafintosh/debugging-stream#readme" 26 | } 27 | --------------------------------------------------------------------------------