├── .gitignore ├── LICENSE ├── package.json ├── .travis.yml ├── README.md ├── index.js ├── karma.conf.js └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | node_modules 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Austin Middleton 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "readable-stream-node-to-web", 3 | "version": "1.0.1", 4 | "description": "Converts a node Readable stream to a web ReadableStream", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "standard && karma start" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/xuset/readable-stream-node-to-web.git" 12 | }, 13 | "keywords": [ 14 | "readable", 15 | "stream", 16 | "readablestream", 17 | "node", 18 | "web", 19 | "whatwg" 20 | ], 21 | "author": "xuset", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/xuset/readable-stream-node-to-web/issues" 25 | }, 26 | "homepage": "https://github.com/xuset/readable-stream-node-to-web#readme", 27 | "devDependencies": { 28 | "browserify": "^14.4.0", 29 | "karma": "^1.7.0", 30 | "karma-browserify": "^5.1.1", 31 | "karma-chrome-launcher": "^2.2.0", 32 | "karma-firefox-launcher": "^1.0.1", 33 | "karma-sauce-launcher": "^1.1.0", 34 | "karma-tap": "^3.1.1", 35 | "standard": "^10.0.2", 36 | "tape": "^4.7.0", 37 | "watchify": "^3.9.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - node 5 | env: 6 | global: 7 | - CI=true 8 | - secure: KIwheRFsv5dMv+68/haNfWu6+2/q40CFlGp7RySOf0t156q9+2bimfHFlSmmxObKYE14Q7GVVf/lvAfUA74vBXu4nf15Z8GxNOK4QL3/DETzLJHu13KMBp8NESa6yQMuu85SBNYJD29mUKtYNKCoqsWyx76mg+lo9pn9/yVTGTkzr1y52XKGPvdNqYFVAwmOXafZpute/Tfp+1JeCEQVvE50iTaNFvfTWE+2/Vra8TLMHK7ww8g8GlOE8wAG+REYgCdVLuGE6Ha3YZGzVhAFXrbYCB3sYJGrwI21AxUgMlL03lBMQLuca2HcgjPFVJj+DKL7Virz6Sc9WAMB1ldwV108Qdx9uwy6aVg7dTPX6yFIkuxKyO8vWOY1mCOhK3yrEaLXcDC5r8WsK3VAwJcFbq/3Y4fdP0fj5zr6nVG/fy9AOO2qD+TrHek6sPFDRo/Px52BOqc69Pob7i1waU84wqrMpn5binei4+GXnbMTQdjJEDenOeeOp+UxsC2r+uj/ix8DR6cdSIY7nXQoHXI+d8u003/ju/fOKXGhbI/ul18KqgtLFV99mKXAXRvKXaubbPNfdv1zNTm79lc4nUlv3jlroWJze6IQ6O/SE6XeT1iPk1J++PYQvZHKqcew7wcfzO5R2P9Ha5nq41Zg2RGJ2+ZkxIh3IdRXbUGFyykN72g= 9 | - secure: nubV+YxqofhRymecT0NQ9e/oJh+mAmFlnMOszPbTTXfuAcgsfJqJs91nue7UiKpyQWdB5v3K/88K6i2Juvlry2LTnriFNqHylNWdsIRlg25HQAzMjpM7H9qiOo5jaA+XkW5j7X3wgfODMQ3F1JN8hJW6dCv1tRK3efMIQB1YVSRpMUQqjYFz8+dmeW1UV9/9iNPg6/1/3jZqMT6vaBDa+PuYVohLSWvaPG3FeS5J4OLPVKm7vsCDL4SsxLLM803dwror87aq89N4T4vhDGaFA0fUIqdo+xqPU5XiQaEPhwqow5kBMx/udFlZbpDlXNMt6yf33XHh4KNzmp7yLQfGtRXrHUWK4m44M7pr+rX27OfIM6BwWF/n921zMi0PjfU5ighYuDtci43hY/I+7Wt79brpoCui8mg/j53yyZg8N1keYLac2pP21b4IcuuJyPUzgHegDhjFH6MNuEqYAbJjbqwoeocFXgVCbzGDNtVTZYmtDL05zkejrr31OCyAWwtUagNv2SFc8NV50U39ZEGWH6REiUb7SA7q30yy8cweRfbgzSPvj5XTidvgcZF1UrTKyVtbSegJCMGD7YIFGxo5TAunhWuj8Wqh8KoxQI093GXaFoe5qhywfjHAG8SSGKnjP0lm9aetAnaMflftcFS+CzpE4CWPYQSl8GmTFYlMwHY= 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # readable-stream-node-to-web [![Build Status](https://travis-ci.org/xuset/readable-stream-node-to-web.svg?branch=master)](https://travis-ci.org/xuset/readable-stream-node-to-web) [![npm](https://img.shields.io/npm/v/readable-stream-node-to-web.svg)](https://npmjs.org/package/readable-stream-node-to-web) 2 | 3 | #### Convert a nodejs Readable stream to a web ReadableStream 4 | 5 | [![Sauce Test Status](https://saucelabs.com/browser-matrix/xuset-readable-stream-node-to-web.svg)](https://saucelabs.com/u/xuset-readable-stream-node-to-web) 6 | 7 | 8 | ```js 9 | var nodeToWebStream = require('readable-stream-node-to-web'); 10 | 11 | var nodeStream = // Obtain a nodejs Readable stream 12 | var webStream = nodeToWebStream(nodeStream); 13 | ``` 14 | 15 | `nodeStream` is a [NodeJS Readable stream](https://nodejs.org/api/stream.html#stream_readable_streams) 16 | 17 | `webStream` is a [WhatWG web ReadableStream](https://streams.spec.whatwg.org/#rs-class) 18 | 19 | This method takes a readable node stream and returns a web stream to reads from given the node stream. This allows the plethora of nodejs modules involving Readable streams to be used in a web context like using a node stream with [FetchEvent.respondWith()](https://developer.mozilla.org/en-US/docs/Web/API/FetchEvent/respondWith) 20 | 21 | This module can be used with bundlers like browserify or webpack. This module is also written in es5 so there is no need for transpilation. 22 | 23 | ## License 24 | 25 | MIT. Copyright (c) Austin Middleton. 26 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | /* global ReadableStream */ 3 | 4 | module.exports = nodeToWeb 5 | module.exports.WEBSTREAM_SUPPORT = typeof ReadableStream !== 'undefined' 6 | 7 | function nodeToWeb (nodeStream) { 8 | // Assumes the nodeStream has not ended/closed 9 | if (!module.exports.WEBSTREAM_SUPPORT) throw new Error('No web ReadableStream support') 10 | 11 | var destroyed = false 12 | var listeners = {} 13 | 14 | function start (controller) { 15 | listeners['data'] = onData 16 | listeners['end'] = onData 17 | listeners['end'] = onDestroy 18 | listeners['close'] = onDestroy 19 | listeners['error'] = onDestroy 20 | for (var name in listeners) nodeStream.on(name, listeners[name]) 21 | 22 | nodeStream.pause() 23 | 24 | function onData (chunk) { 25 | if (destroyed) return 26 | controller.enqueue(chunk) 27 | nodeStream.pause() 28 | } 29 | 30 | function onDestroy (err) { 31 | if (destroyed) return 32 | destroyed = true 33 | 34 | for (var name in listeners) nodeStream.removeListener(name, listeners[name]) 35 | 36 | if (err) controller.error(err) 37 | else controller.close() 38 | } 39 | } 40 | 41 | function pull () { 42 | if (destroyed) return 43 | nodeStream.resume() 44 | } 45 | 46 | function cancel () { 47 | destroyed = true 48 | 49 | for (var name in listeners) nodeStream.removeListener(name, listeners[name]) 50 | 51 | nodeStream.push(null) 52 | nodeStream.pause() 53 | if (nodeStream.destroy) nodeStream.destroy() 54 | else if (nodeStream.close) nodeStream.close() 55 | } 56 | 57 | return new ReadableStream({start: start, pull: pull, cancel: cancel}) 58 | } 59 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config) { 2 | config.set({ 3 | // base path that will be used to resolve all patterns (eg. files, exclude) 4 | basePath: '', 5 | 6 | // frameworks to use 7 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 8 | frameworks: ['tap', 'browserify'], 9 | 10 | // list of files / patterns to load in the browser 11 | files: [ 12 | 'test.js' 13 | ], 14 | 15 | // list of files to exclude 16 | exclude: [ 17 | ], 18 | 19 | // preprocess matching files before serving them to the browser 20 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 21 | preprocessors: { 22 | 'test.js': ['browserify'] 23 | }, 24 | 25 | // test results reporter to use 26 | // possible values: 'dots', 'progress' 27 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 28 | reporters: process.env.CI ? ['dots', 'saucelabs'] : ['dots'], 29 | 30 | // web server port 31 | port: 9876, 32 | 33 | // enable / disable colors in the output (reporters and logs) 34 | colors: true, 35 | 36 | // level of logging 37 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 38 | logLevel: config.LOG_INFO, 39 | 40 | // enable / disable watching file and executing tests whenever any file changes 41 | autoWatch: true, 42 | 43 | browserNoActivityTimeout: process.env.CI ? 300 * 1000 : 10 * 1000, 44 | 45 | // start these browsers 46 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 47 | browsers: process.env.CI ? ['SL_Chrome'] : ['Chrome'], 48 | 49 | // Continuous Integration mode 50 | // if true, Karma captures browsers, runs the tests and exits 51 | singleRun: !!process.env.CI, 52 | 53 | // Concurrency level 54 | // how many browser should be started simultaneous 55 | concurrency: Infinity, 56 | 57 | sauceLabs: { 58 | testName: 'readable-stream-node-to-web' 59 | }, 60 | customLaunchers: { 61 | SL_Chrome: { 62 | base: 'SauceLabs', 63 | browserName: 'chrome', 64 | version: 'latest' 65 | } 66 | } 67 | }) 68 | } 69 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | 2 | var test = require('tape') 3 | var PassThrough = require('stream').PassThrough 4 | var ReadableStream = require('stream').Readable 5 | var nodeToWebStream = require('.') 6 | 7 | test('sanity check', function (t) { 8 | var str = 'foobar' 9 | streamToString(nodeToWebStream(stringToStream(str))) 10 | .then(output => { 11 | t.equal(str, output) 12 | t.end() 13 | }) 14 | .catch(err => t.end(err)) 15 | }) 16 | 17 | test('cancel() ensures cleanup', function (t) { 18 | t.timeoutAfter(3000) 19 | var nodeStream = new ReadableStream() 20 | nodeStream._destroy = function () { 21 | t.end() 22 | } 23 | var webStream = nodeToWebStream(nodeStream) 24 | 25 | nodeStream.on('end', function () { 26 | }) 27 | 28 | webStream.cancel() 29 | }) 30 | 31 | test('cancel() ensures _destroy()', function (t) { 32 | t.timeoutAfter(3000) 33 | var nodeStream = new ReadableStream() 34 | var webStream = nodeToWebStream(nodeStream) 35 | 36 | nodeStream._destroy = function () { 37 | t.end() 38 | } 39 | 40 | webStream.cancel() 41 | }) 42 | 43 | test('errored node stream', function (t) { 44 | t.timeoutAfter(3000) 45 | var nodeStream = new ReadableStream({ read: function () {} }) 46 | var webStream = nodeToWebStream(nodeStream) 47 | 48 | nodeStream.emit('error', new Error('foobar')) 49 | 50 | webStream.getReader().read().catch(function (err) { 51 | t.equals(err.message, 'foobar') 52 | t.end() 53 | }) 54 | }) 55 | 56 | test('node stream closed early', function (t) { 57 | t.timeoutAfter(3000) 58 | var nodeStream = new ReadableStream({ read: function () {} }) 59 | var webStream = nodeToWebStream(nodeStream) 60 | 61 | nodeStream.push(null) 62 | 63 | webStream.getReader().read().then(function (result) { 64 | t.equals(result.done, true) 65 | t.end() 66 | }) 67 | }) 68 | 69 | function stringToStream (str) { 70 | var s = new PassThrough() 71 | s.end(Buffer.from(str)) 72 | return s 73 | } 74 | 75 | function streamToString (stream) { 76 | return new Promise(function (resolve, reject) { 77 | let reader = stream.getReader() 78 | let buffer = '' 79 | reader.read().then(onRead) 80 | 81 | function onRead (result) { 82 | if (result.done) return resolve(buffer) 83 | 84 | buffer += result.value.toString() 85 | reader.read().then(onRead) 86 | } 87 | }) 88 | } 89 | --------------------------------------------------------------------------------