├── .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 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
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