├── .gitmodules ├── src ├── assert.ts ├── utils.ts ├── index.ts ├── tsconfig.json ├── wrappers.ts ├── stream-like.ts ├── checks.ts ├── transform-wrapper.ts ├── writable-wrapper.ts └── readable-wrapper.ts ├── test ├── vendor │ ├── ungap-promise-all-settled.d.ts │ └── wpt-runner.d.ts ├── tsconfig.json ├── wrapping-writable-stream.ts ├── wrappers.ts ├── wrapping-transform-stream.ts ├── wrapping-readable-stream.ts └── run-web-platform-tests.ts ├── .idea ├── codeStyles │ ├── codeStyleConfig.xml │ └── Project.xml ├── misc.xml ├── vcs.xml ├── jsLibraryMappings.xml ├── modules.xml └── web-streams-adapter.iml ├── rollup.config.js ├── LICENSE.md ├── package.json ├── .gitignore └── README.md /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "web-platform-tests"] 2 | path = web-platform-tests 3 | url = https://github.com/MattiasBuelens/web-platform-tests.git 4 | -------------------------------------------------------------------------------- /src/assert.ts: -------------------------------------------------------------------------------- 1 | export default function assert(test: boolean): void | never { 2 | if (!test) { 3 | throw new TypeError('Assertion failed'); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/vendor/ungap-promise-all-settled.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@ungap/promise-all-settled' { 2 | const allSettled: typeof Promise.allSettled; 3 | export default allSettled; 4 | } 5 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | export function noop() { 2 | return; 3 | } 4 | 5 | export function typeIsObject(x: any): x is object | Function { 6 | return (typeof x === 'object' && x !== null) || typeof x === 'function'; 7 | } 8 | -------------------------------------------------------------------------------- /.idea/jsLibraryMappings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../src/tsconfig.json", 3 | "compilerOptions": { 4 | "target": "es2015", 5 | "module": "commonjs", 6 | "lib": [ 7 | "es2015", 8 | "es2020.promise" 9 | ] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './stream-like'; 2 | export * from './wrappers'; 3 | 4 | export { createReadableStreamWrapper, createWrappingReadableSource } from './readable-wrapper'; 5 | export { createWritableStreamWrapper, createWrappingWritableSink } from './writable-wrapper'; 6 | export { createTransformStreamWrapper, createWrappingTransformer } from './transform-wrapper'; 7 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "es2015", 5 | "moduleResolution": "node", 6 | "newLine": "lf", 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "importHelpers": true, 10 | "lib": [ 11 | "dom", 12 | "es5", 13 | "es2015.promise", 14 | "es2015.iterable" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/vendor/wpt-runner.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'wpt-runner' { 2 | export interface Reporter { 3 | startSuite(name: string): void; 4 | 5 | pass(message: string): void; 6 | 7 | fail(message: string): void; 8 | 9 | reportStack(stack: string): void; 10 | } 11 | 12 | export interface Options { 13 | rootURL?: string; 14 | setup?: (window: any) => void; 15 | filter?: (testPath: string, url: string) => boolean | PromiseLike; 16 | reporter?: Reporter; 17 | } 18 | 19 | export default function wptRunner(testsPath: string, options?: Options): Promise; 20 | } 21 | -------------------------------------------------------------------------------- /src/wrappers.ts: -------------------------------------------------------------------------------- 1 | import { ReadableByteStreamLike, ReadableStreamLike, TransformStreamLike, WritableStreamLike } from './stream-like'; 2 | 3 | export type ReadableStreamWrapper = (readable: ReadableStreamLike, 4 | options?: { type?: undefined }) => ReadableStreamLike; 5 | 6 | export interface ReadableByteStreamWrapper { 7 | (readable: ReadableByteStreamLike, options: { type: 'bytes' }): ReadableByteStreamLike; 8 | 9 | (readable: ReadableStreamLike, options?: { type?: undefined }): ReadableStreamLike; 10 | } 11 | 12 | export type TransformStreamWrapper = (Transform: TransformStreamLike) => TransformStreamLike; 13 | 14 | export type WritableStreamWrapper = (writable: WritableStreamLike) => WritableStreamLike; 15 | -------------------------------------------------------------------------------- /.idea/web-streams-adapter.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /test/wrapping-writable-stream.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { createWrappingWritableSink } from '../'; 3 | 4 | export function createWrappingWritableStream(baseClass: typeof WritableStream): typeof WritableStream { 5 | const wrappingClass = class WrappingWritableStream extends baseClass { 6 | 7 | constructor(underlyingSink: UnderlyingSink = {}, 8 | strategy: QueuingStrategy = {}) { 9 | const wrappedWritableStream = new baseClass(underlyingSink); 10 | underlyingSink = createWrappingWritableSink(wrappedWritableStream); 11 | 12 | super(underlyingSink, strategy); 13 | } 14 | 15 | get locked() { 16 | return super.locked; 17 | } 18 | 19 | abort(reason: any) { 20 | return super.abort(reason); 21 | } 22 | 23 | getWriter(): WritableStreamDefaultWriter { 24 | return super.getWriter(); 25 | } 26 | 27 | }; 28 | 29 | Object.defineProperty(wrappingClass, 'name', { value: 'WritableStream' }); 30 | 31 | return wrappingClass; 32 | } 33 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from '@rollup/plugin-typescript'; 2 | import dts from 'rollup-plugin-dts'; 3 | import cleanup from 'rollup-plugin-cleanup'; 4 | 5 | const pkg = require('./package.json'); 6 | 7 | module.exports = [{ 8 | input: 'src/index.ts', 9 | output: [ 10 | { 11 | file: `${pkg.main}.js`, 12 | format: 'umd', 13 | freeze: false, 14 | sourcemap: true, 15 | name: 'WebStreamsAdapter' 16 | }, 17 | { 18 | file: pkg.module, 19 | format: 'es', 20 | freeze: false, 21 | sourcemap: true 22 | } 23 | ], 24 | plugins: [ 25 | typescript({ 26 | tsconfig: 'src/tsconfig.json' 27 | }), 28 | cleanup({ 29 | // tslib has CRLF line endings, so normalize before bundling 30 | comments: 'all', 31 | maxEmptyLines: -1, 32 | lineEndings: 'unix' 33 | }) 34 | ] 35 | }, { 36 | input: 'src/index.ts', 37 | output: [ 38 | { 39 | file: pkg.types, 40 | format: 'es' 41 | } 42 | ], 43 | plugins: [ 44 | dts() 45 | ] 46 | }]; 47 | -------------------------------------------------------------------------------- /test/wrappers.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { createWrappingReadableStream } from './wrapping-readable-stream'; 3 | import { createWrappingWritableStream } from './wrapping-writable-stream'; 4 | import { createWrappingTransformStream } from './wrapping-transform-stream'; 5 | 6 | export interface StreamClasses { 7 | ReadableStream: typeof ReadableStream; 8 | WritableStream: typeof WritableStream; 9 | TransformStream: typeof TransformStream; 10 | } 11 | 12 | export function createWrappingStreams({ ReadableStream, WritableStream, TransformStream }: StreamClasses): StreamClasses { 13 | const WrappingReadableStream = createWrappingReadableStream(ReadableStream); 14 | const WrappingWritableStream = createWrappingWritableStream(WritableStream); 15 | const WrappingTransformStream = createWrappingTransformStream(TransformStream, WrappingReadableStream, WrappingWritableStream); 16 | 17 | return { 18 | ReadableStream: WrappingReadableStream, 19 | WritableStream: WrappingWritableStream, 20 | TransformStream: WrappingTransformStream 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Mattias Buelens 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": "@mattiasbuelens/web-streams-adapter", 3 | "version": "0.1.0", 4 | "description": "Adapters for converting between different implementations of WHATWG Streams", 5 | "main": "dist/web-streams-adapter", 6 | "module": "dist/web-streams-adapter.mjs", 7 | "types": "dist/web-streams-adapter.d.ts", 8 | "files": [ 9 | "dist/" 10 | ], 11 | "engines": { 12 | "node": ">= 12" 13 | }, 14 | "scripts": { 15 | "build": "rollup -c", 16 | "test": "cd test && node -r ts-node/register --expose_gc run-web-platform-tests.ts" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/MattiasBuelens/web-streams-adapter.git" 21 | }, 22 | "author": "Mattias Buelens ", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/MattiasBuelens/web-streams-adapter/issues" 26 | }, 27 | "homepage": "https://github.com/MattiasBuelens/web-streams-adapter#readme", 28 | "devDependencies": { 29 | "@rollup/plugin-typescript": "^8.2.1", 30 | "@types/micromatch": "^4.0.1", 31 | "@types/node": "^14.14.44", 32 | "@types/resolve": "^1.20.0", 33 | "@ungap/promise-all-settled": "^1.1.2", 34 | "micromatch": "^4.0.4", 35 | "resolve": "^1.20.0", 36 | "rollup": "^3.29.5", 37 | "rollup-plugin-cleanup": "^3.2.1", 38 | "rollup-plugin-dts": "^3.0.1", 39 | "ts-node": "^9.1.1", 40 | "tslib": "^2.2.0", 41 | "typescript": "^4.2.4", 42 | "web-streams-polyfill": "^3.0.3", 43 | "wpt-runner": "^3.2.1" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/wrapping-transform-stream.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { createWrappingReadableSource, createWrappingTransformer, createWrappingWritableSink } from '../'; 3 | 4 | export function createWrappingTransformStream(baseClass: typeof TransformStream, 5 | readableClass: typeof ReadableStream, 6 | writableClass: typeof WritableStream): typeof TransformStream { 7 | const wrappingClass = class WrappingTransformStream extends baseClass { 8 | 9 | private readonly _wrappedReadable: ReadableStream; 10 | private readonly _wrappedWritable: WritableStream; 11 | 12 | constructor(transformer: Transformer = {}, 13 | writableStrategy: QueuingStrategy = {}, 14 | readableStrategy: QueuingStrategy = {}) { 15 | const wrappedTransformStream = new baseClass(transformer); 16 | transformer = createWrappingTransformer(wrappedTransformStream); 17 | 18 | super(transformer); 19 | 20 | const wrappedReadableSource = createWrappingReadableSource(super.readable, { type: transformer.readableType }); 21 | this._wrappedReadable = new readableClass(wrappedReadableSource as any, readableStrategy); 22 | 23 | const wrappedWritableSink = createWrappingWritableSink(super.writable); 24 | this._wrappedWritable = new writableClass(wrappedWritableSink, writableStrategy); 25 | } 26 | 27 | get readable() { 28 | void super.readable; // brand check 29 | return this._wrappedReadable; 30 | } 31 | 32 | get writable() { 33 | void super.writable; // brand check 34 | return this._wrappedWritable; 35 | } 36 | 37 | }; 38 | 39 | Object.defineProperty(wrappingClass, 'name', { value: 'TransformStream' }); 40 | 41 | return wrappingClass; 42 | } 43 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 45 | -------------------------------------------------------------------------------- /test/wrapping-readable-stream.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { 3 | createWrappingReadableSource, 4 | ReadableByteStreamLike, 5 | ReadableStreamBYOBReader, 6 | UnderlyingByteSource 7 | } from '../'; 8 | 9 | export function createWrappingReadableStream(baseClass: typeof ReadableStream): typeof ReadableStream { 10 | const wrappingClass = class WrappingReadableStream extends baseClass { 11 | 12 | constructor(underlyingSource: UnderlyingSource | UnderlyingByteSource = {}, 13 | strategy: QueuingStrategy = {}) { 14 | let wrappedReadableStream = new baseClass(underlyingSource as any, strategy); 15 | if (underlyingSource.type === 'bytes') { 16 | underlyingSource = createWrappingReadableSource(wrappedReadableStream as unknown as ReadableByteStreamLike, { type: 'bytes' }); 17 | } else { 18 | underlyingSource = createWrappingReadableSource(wrappedReadableStream); 19 | } 20 | 21 | super(underlyingSource as any); 22 | } 23 | 24 | get locked() { 25 | return super.locked; 26 | } 27 | 28 | cancel(reason: any) { 29 | return super.cancel(reason); 30 | } 31 | 32 | getReader(): ReadableStreamDefaultReader; 33 | getReader(options: { mode: 'byob' }): ReadableStreamBYOBReader; 34 | getReader(options?: any): ReadableStreamDefaultReader | ReadableStreamBYOBReader { 35 | return (super.getReader as any)(options); 36 | } 37 | 38 | pipeThrough(pair: { writable: WritableStream, readable: ReadableStream }, options?: StreamPipeOptions): ReadableStream { 39 | return super.pipeThrough(pair, options); 40 | } 41 | 42 | pipeTo(dest: WritableStream, options: StreamPipeOptions = {}) { 43 | return super.pipeTo(dest, options); 44 | } 45 | 46 | tee(): [WrappingReadableStream, WrappingReadableStream] { 47 | const [branch1, branch2] = super.tee(); 48 | 49 | const source1 = createWrappingReadableSource(branch1); 50 | const source2 = createWrappingReadableSource(branch2); 51 | const wrapped1 = new WrappingReadableStream(source1); 52 | const wrapped2 = new WrappingReadableStream(source2); 53 | return [wrapped1, wrapped2]; 54 | } 55 | }; 56 | 57 | Object.defineProperty(wrappingClass, 'name', { value: 'ReadableStream' }); 58 | 59 | return wrappingClass; 60 | } 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### JetBrains template 3 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 4 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 5 | 6 | # User-specific stuff: 7 | .idea/**/workspace.xml 8 | .idea/**/tasks.xml 9 | .idea/dictionaries 10 | 11 | # Sensitive or high-churn files: 12 | .idea/**/dataSources/ 13 | .idea/**/dataSources.ids 14 | .idea/**/dataSources.xml 15 | .idea/**/dataSources.local.xml 16 | .idea/**/sqlDataSources.xml 17 | .idea/**/dynamic.xml 18 | .idea/**/uiDesigner.xml 19 | 20 | # Gradle: 21 | .idea/**/gradle.xml 22 | .idea/**/libraries 23 | 24 | # CMake 25 | cmake-build-debug/ 26 | cmake-build-release/ 27 | 28 | # Mongo Explorer plugin: 29 | .idea/**/mongoSettings.xml 30 | 31 | ## File-based project format: 32 | *.iws 33 | 34 | ## Plugin-specific files: 35 | 36 | # IntelliJ 37 | out/ 38 | 39 | # mpeltonen/sbt-idea plugin 40 | .idea_modules/ 41 | 42 | # JIRA plugin 43 | atlassian-ide-plugin.xml 44 | 45 | # Cursive Clojure plugin 46 | .idea/replstate.xml 47 | 48 | # Crashlytics plugin (for Android Studio and IntelliJ) 49 | com_crashlytics_export_strings.xml 50 | crashlytics.properties 51 | crashlytics-build.properties 52 | fabric.properties 53 | ### Node template 54 | # Logs 55 | logs 56 | *.log 57 | npm-debug.log* 58 | yarn-debug.log* 59 | yarn-error.log* 60 | 61 | # Runtime data 62 | pids 63 | *.pid 64 | *.seed 65 | *.pid.lock 66 | 67 | # Directory for instrumented libs generated by jscoverage/JSCover 68 | lib-cov 69 | 70 | # Coverage directory used by tools like istanbul 71 | coverage 72 | 73 | # nyc test coverage 74 | .nyc_output 75 | 76 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 77 | .grunt 78 | 79 | # Bower dependency directory (https://bower.io/) 80 | bower_components 81 | 82 | # node-waf configuration 83 | .lock-wscript 84 | 85 | # Compiled binary addons (https://nodejs.org/api/addons.html) 86 | build/Release 87 | 88 | # Dependency directories 89 | node_modules/ 90 | jspm_packages/ 91 | 92 | # Typescript v1 declaration files 93 | typings/ 94 | 95 | # Optional npm cache directory 96 | .npm 97 | 98 | # Optional eslint cache 99 | .eslintcache 100 | 101 | # Optional REPL history 102 | .node_repl_history 103 | 104 | # Output of 'npm pack' 105 | *.tgz 106 | 107 | # Yarn Integrity file 108 | .yarn-integrity 109 | 110 | # dotenv environment variables file 111 | .env 112 | 113 | # next.js build output 114 | .next 115 | 116 | 117 | ## Project 118 | dist/ 119 | -------------------------------------------------------------------------------- /src/stream-like.ts: -------------------------------------------------------------------------------- 1 | export interface ReadableStreamLikeConstructor { 2 | new( 3 | underlyingSource?: UnderlyingSource, 4 | strategy?: QueuingStrategy 5 | ): ReadableStreamLike; 6 | } 7 | 8 | export interface ReadableByteStreamLikeConstructor extends ReadableStreamLikeConstructor { 9 | new( 10 | underlyingSource: UnderlyingByteSource, 11 | strategy?: { highWaterMark?: number; size?: undefined; } 12 | ): ReadableByteStreamLike; 13 | 14 | new( 15 | underlyingSource?: UnderlyingSource, 16 | strategy?: QueuingStrategy 17 | ): ReadableStreamLike; 18 | } 19 | 20 | export interface ReadableStreamLike { 21 | readonly locked: boolean; 22 | 23 | getReader(): ReadableStreamDefaultReader; 24 | } 25 | 26 | export interface ReadableByteStreamLike extends ReadableStreamLike { 27 | getReader(): ReadableStreamDefaultReader; 28 | 29 | getReader({ mode }: { mode: 'byob' }): ReadableStreamBYOBReader; 30 | } 31 | 32 | export interface UnderlyingByteSource { 33 | start?: UnderlyingByteSourceStartCallback; 34 | pull?: UnderlyingByteSourcePullCallback; 35 | cancel?: UnderlyingSourceCancelCallback; 36 | type: 'bytes'; 37 | autoAllocateChunkSize?: number; 38 | } 39 | 40 | export type UnderlyingByteSourcePullCallback = (controller: ReadableByteStreamController) => void | PromiseLike; 41 | 42 | export type UnderlyingByteSourceStartCallback = (controller: ReadableByteStreamController) => void | PromiseLike; 43 | 44 | export interface ReadableByteStreamController { 45 | readonly byobRequest: ReadableStreamBYOBRequest | null; 46 | readonly desiredSize: number | null; 47 | 48 | close(): void; 49 | 50 | enqueue(chunk: ArrayBufferView): void; 51 | 52 | error(e?: any): void; 53 | } 54 | 55 | export interface ReadableStreamBYOBRequest { 56 | readonly view: ArrayBufferView | null; 57 | 58 | respond(bytesWritten: number): void; 59 | 60 | respondWithNewView(view: ArrayBufferView): void; 61 | } 62 | 63 | export interface ReadableStreamBYOBReader { 64 | readonly closed: Promise; 65 | 66 | cancel(reason?: any): Promise; 67 | 68 | read(view: T): Promise>; 69 | 70 | releaseLock(): void; 71 | } 72 | 73 | export type ReadableStreamBYOBReadResult = { 74 | done: boolean; 75 | value: T; 76 | }; 77 | 78 | export interface WritableStreamLikeConstructor { 79 | new(underlyingSink?: UnderlyingSink, 80 | strategy?: QueuingStrategy): WritableStreamLike; 81 | } 82 | 83 | export interface WritableStreamLike { 84 | readonly locked: boolean; 85 | 86 | getWriter(): WritableStreamDefaultWriter; 87 | } 88 | 89 | export interface TransformStreamLikeConstructor { 90 | new(transformer?: Transformer, 91 | writableStrategy?: QueuingStrategy, 92 | readableStrategy?: QueuingStrategy): TransformStreamLike; 93 | } 94 | 95 | export interface TransformStreamLike { 96 | readonly writable: WritableStreamLike; 97 | readonly readable: ReadableStreamLike; 98 | } 99 | -------------------------------------------------------------------------------- /src/checks.ts: -------------------------------------------------------------------------------- 1 | import { typeIsObject } from './utils'; 2 | import { 3 | ReadableByteStreamLike, 4 | ReadableByteStreamLikeConstructor, 5 | ReadableStreamLike, 6 | ReadableStreamLikeConstructor, 7 | TransformStreamLike, 8 | TransformStreamLikeConstructor, 9 | WritableStreamLike, 10 | WritableStreamLikeConstructor 11 | } from './stream-like'; 12 | 13 | type Constructor = new(...args: any[]) => T; 14 | 15 | function isStreamConstructor(ctor: any): ctor is Constructor { 16 | if (typeof ctor !== 'function') { 17 | return false; 18 | } 19 | let startCalled = false; 20 | try { 21 | new ctor({ 22 | start() { 23 | startCalled = true; 24 | } 25 | }); 26 | } catch (e) { 27 | // ignore 28 | } 29 | return startCalled; 30 | } 31 | 32 | export function isReadableStream(readable: any): readable is ReadableStreamLike { 33 | if (!typeIsObject(readable)) { 34 | return false; 35 | } 36 | if (typeof (readable as ReadableStreamLike).getReader !== 'function') { 37 | return false; 38 | } 39 | return true; 40 | } 41 | 42 | export function isReadableStreamConstructor(ctor: any): ctor is ReadableStreamLikeConstructor { 43 | if (!isStreamConstructor(ctor)) { 44 | return false; 45 | } 46 | if (!isReadableStream(new ctor())) { 47 | return false; 48 | } 49 | return true; 50 | } 51 | 52 | export function isWritableStream(writable: any): writable is WritableStreamLike { 53 | if (!typeIsObject(writable)) { 54 | return false; 55 | } 56 | if (typeof (writable as WritableStreamLike).getWriter !== 'function') { 57 | return false; 58 | } 59 | return true; 60 | } 61 | 62 | export function isWritableStreamConstructor(ctor: any): ctor is WritableStreamLikeConstructor { 63 | if (!isStreamConstructor(ctor)) { 64 | return false; 65 | } 66 | if (!isWritableStream(new ctor())) { 67 | return false; 68 | } 69 | return true; 70 | } 71 | 72 | export function isTransformStream(transform: any): transform is TransformStreamLike { 73 | if (!typeIsObject(transform)) { 74 | return false; 75 | } 76 | if (!isReadableStream((transform as TransformStreamLike).readable)) { 77 | return false; 78 | } 79 | if (!isWritableStream((transform as TransformStreamLike).writable)) { 80 | return false; 81 | } 82 | return true; 83 | } 84 | 85 | export function isTransformStreamConstructor(ctor: any): ctor is TransformStreamLikeConstructor { 86 | if (!isStreamConstructor(ctor)) { 87 | return false; 88 | } 89 | if (!isTransformStream(new ctor())) { 90 | return false; 91 | } 92 | return true; 93 | } 94 | 95 | export function supportsByobReader(readable: ReadableStreamLike): boolean { 96 | try { 97 | const reader = (readable as unknown as ReadableByteStreamLike).getReader({ mode: 'byob' }); 98 | reader.releaseLock(); 99 | return true; 100 | } catch { 101 | return false; 102 | } 103 | } 104 | 105 | export function supportsByteSource(ctor: ReadableStreamLikeConstructor): ctor is ReadableByteStreamLikeConstructor { 106 | try { 107 | new (ctor as ReadableByteStreamLikeConstructor)({ type: 'bytes' }); 108 | return true; 109 | } catch { 110 | return false; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/transform-wrapper.ts: -------------------------------------------------------------------------------- 1 | import assert from './assert'; 2 | import { isTransformStream, isTransformStreamConstructor } from './checks'; 3 | import { TransformStreamLike, TransformStreamLikeConstructor } from './stream-like'; 4 | import { TransformStreamWrapper } from './wrappers'; 5 | import { noop } from './utils'; 6 | 7 | export function createTransformStreamWrapper(ctor: TransformStreamLikeConstructor): TransformStreamWrapper { 8 | assert(isTransformStreamConstructor(ctor)); 9 | 10 | return (transform: TransformStreamLike) => { 11 | if (transform.constructor === ctor) { 12 | return transform; 13 | } 14 | const transformer = createWrappingTransformer(transform); 15 | return new ctor(transformer); 16 | }; 17 | } 18 | 19 | export function createWrappingTransformer( 20 | transform: TransformStreamLike): Transformer { 21 | assert(isTransformStream(transform)); 22 | 23 | const { readable, writable } = transform; 24 | assert(readable.locked === false); 25 | assert(writable.locked === false); 26 | 27 | let reader: ReadableStreamDefaultReader = readable.getReader(); 28 | let writer: WritableStreamDefaultWriter; 29 | try { 30 | writer = writable.getWriter(); 31 | } catch (e) { 32 | reader.releaseLock(); // do not leak reader 33 | throw e; 34 | } 35 | 36 | return new WrappingTransformStreamTransformer(reader, writer); 37 | } 38 | 39 | class WrappingTransformStreamTransformer implements Transformer { 40 | 41 | private readonly _reader: ReadableStreamDefaultReader; 42 | private readonly _writer: WritableStreamDefaultWriter; 43 | private readonly _flushPromise: Promise; 44 | private _flushResolve!: () => void; 45 | private _flushReject!: (reason: any) => void; 46 | private _transformStreamController: TransformStreamDefaultController = undefined!; 47 | 48 | constructor(reader: ReadableStreamDefaultReader, writer: WritableStreamDefaultWriter) { 49 | this._reader = reader; 50 | this._writer = writer; 51 | this._flushPromise = new Promise((resolve, reject) => { 52 | this._flushResolve = resolve; 53 | this._flushReject = reject; 54 | }); 55 | } 56 | 57 | start(controller: TransformStreamDefaultController) { 58 | this._transformStreamController = controller; 59 | 60 | this._reader.read() 61 | .then(this._onRead) 62 | .then(this._onTerminate, this._onError); 63 | 64 | const readerClosed = this._reader.closed; 65 | if (readerClosed) { 66 | readerClosed 67 | .then(this._onTerminate, this._onError); 68 | } 69 | } 70 | 71 | transform(chunk: I) { 72 | return this._writer.write(chunk); 73 | } 74 | 75 | flush() { 76 | return this._writer.close() 77 | .then(() => this._flushPromise); 78 | } 79 | 80 | private _onRead = (result: ReadableStreamDefaultReadResult): void | Promise => { 81 | if (result.done) { 82 | return; 83 | } 84 | this._transformStreamController.enqueue(result.value); 85 | return this._reader.read().then(this._onRead); 86 | }; 87 | 88 | private _onError = (reason: any) => { 89 | this._flushReject(reason); 90 | this._transformStreamController.error(reason); 91 | 92 | this._reader.cancel(reason).catch(noop); 93 | this._writer.abort(reason).catch(noop); 94 | }; 95 | 96 | private _onTerminate = () => { 97 | this._flushResolve(); 98 | this._transformStreamController.terminate(); 99 | 100 | const error = new TypeError('TransformStream terminated'); 101 | this._writer.abort(error).catch(noop); 102 | }; 103 | 104 | } 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # web-streams-adapter 2 | Adapters for converting between different implementations of [WHATWG Streams][spec]. 3 | 4 | ## Why? 5 | When you've got a `ReadableStream` from a native web API, you might be disappointed to find out 6 | that not all browser support the latest and greatest features from the streams spec yet: 7 | ```js 8 | const response = await fetch('http://example.com/data.txt'); 9 | const readable = response.body; 10 | const writable = new WritableStream({ write(chunk) { console.log(chunk) } }); 11 | await readable.pipeTo(writable); // TypeError: Object doesn't support property or method 'pipeTo' 12 | ``` 13 | 14 | This is because although many browsers have already started implementing streams, 15 | most of them are not yet fully up-to-date with the latest specification: 16 | * Chrome 67 [supports][ts-chrome-status] `ReadableStream`, `WritableStream` and `TransformStream`. 17 | Readable byte streams are [supported][byte-stream-chrome-status] as of Chrome 89. 18 | However, async iteration is [not yet supported][async-iterator-crbug]. 19 | * Edge 89 has the same support as Chrome 89. 20 | * Firefox 65 supports `ReadableStream`, but no readable byte streams or writable streams yet. 21 | As such, methods like `pipeTo()` and `pipeThrough()` that take a `WritableStream` are not yet supported either. 22 | * Safari supports `ReadableStream`, but no readable byte streams or writable streams. 23 | 24 | For up-to-date information, check [caniuse.com][caniuse] 25 | and the browser compatibility tables on MDN for [`ReadableStream`][rs-compat] and [`WritableStream`][ws-compat]. 26 | 27 | ## What? 28 | `web-streams-adapter` provides adapter functions that take any readable/writable/transform stream 29 | and wraps it into a different readable/writable/stream with a different (more complete) implementation of your choice, 30 | for example [web-streams-polyfill]. 31 | ```js 32 | // setup 33 | import { ReadableStream as PolyfillReadableStream } from 'web-streams-polyfill'; 34 | import { createReadableStreamWrapper } from '@mattiasbuelens/web-streams-adapter'; 35 | const toPolyfillReadable = createReadableStreamWrapper(PolyfillReadableStream); 36 | 37 | // when handling a fetch response 38 | const response = await fetch('http://example.com/data.txt'); 39 | const readable = toPolyfillReadable(response.body); 40 | console.log(readable instanceof PolyfillReadableStream); // -> true 41 | await readable.pipeTo(writable); // works! 42 | ``` 43 | 44 | You can also use an adapter to convert from your polyfilled stream back to a native stream: 45 | ```js 46 | // setup 47 | const toNativeReadable = createReadableStreamWrapper(self.ReadableStream); 48 | 49 | // when starting a fetch with a streaming POST body 50 | const readable = new PolyfillReadableStream({ /* amazingness */ }); 51 | const response = await fetch(url, { 52 | method: 'POST', 53 | body: toNativeReadable(readable) // works! 54 | }); 55 | ``` 56 | 57 | ## How? 58 | For readable streams, `web-streams-adapter` creates an underlying source that pulls from the given readable stream 59 | using the primitive reader API. This source can then be used by *any* other readable stream implementation, 60 | both native and polyfilled ones. 61 | 62 | For writable and transform streams, it uses a very similar approach to create an underlying sink or transformer 63 | using primitive reader and writer APIs on the given stream. 64 | 65 | [spec]: https://streams.spec.whatwg.org/ 66 | [ts-chrome-status]: https://www.chromestatus.com/feature/5466425791610880 67 | [byte-stream-chrome-status]: https://chromestatus.com/feature/4535319661641728 68 | [async-iterator-crbug]: https://crbug.com/929585 69 | [caniuse]: https://www.caniuse.com/#feat=streams 70 | [rs-compat]: https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream#Browser_Compatibility 71 | [ws-compat]: https://developer.mozilla.org/en-US/docs/Web/API/WritableStream#Browser_Compatibility 72 | [web-streams-polyfill]: https://github.com/MattiasBuelens/web-streams-polyfill 73 | -------------------------------------------------------------------------------- /test/run-web-platform-tests.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | import path from 'path'; 6 | import fs from 'fs'; 7 | import { promisify } from 'util'; 8 | import wptRunner from 'wpt-runner'; 9 | import micromatch from 'micromatch'; 10 | import resolve from 'resolve'; 11 | import { createWrappingStreams } from './wrappers'; 12 | import allSettled from '@ungap/promise-all-settled'; 13 | 14 | const readFileAsync = promisify(fs.readFile); 15 | const queueMicrotask = global.queueMicrotask || ((fn: () => void) => Promise.resolve().then(fn)); 16 | 17 | const wptPath = path.resolve(__dirname, '../web-platform-tests'); 18 | const testsPath = path.resolve(wptPath, 'streams'); 19 | 20 | const includedTests = process.argv.length >= 3 ? process.argv.slice(2) : ['**/*.html']; 21 | const excludedTests = [ 22 | // We cannot polyfill TransferArrayBuffer yet, so disable tests for detached array buffers 23 | // See https://github.com/MattiasBuelens/web-streams-polyfill/issues/3 24 | 'readable-byte-streams/detached-buffers.any.html', 25 | // Disable tests for different size functions per realm, since they need a working