Skip to content

Commit 4d233d3

Browse files
authored
Fix browser support (#122)
1 parent a51d085 commit 4d233d3

File tree

8 files changed

+118
-5
lines changed

8 files changed

+118
-5
lines changed

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"concat"
4444
],
4545
"dependencies": {
46+
"@sec-ant/readable-stream": "^0.3.2",
4647
"is-stream": "^4.0.1"
4748
},
4849
"devDependencies": {

readme.md

+2
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ const {body: readableStream} = await fetch('https://example.com');
6060
console.log(await getStream(readableStream));
6161
```
6262

63+
This works in any browser, even [the ones](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream#browser_compatibility) not supporting `ReadableStream.values()` yet.
64+
6365
### Async iterables
6466

6567
```js

source/stream.js

+11-3
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,25 @@
11
import {isReadableStream} from 'is-stream';
2+
import {ponyfill} from './web-stream.js';
23

34
export const getAsyncIterable = stream => {
45
if (isReadableStream(stream, {checkOpen: false})) {
56
return getStreamIterable(stream);
67
}
78

8-
if (typeof stream?.[Symbol.asyncIterator] !== 'function') {
9-
throw new TypeError('The first argument must be a Readable, a ReadableStream, or an async iterable.');
9+
if (typeof stream?.[Symbol.asyncIterator] === 'function') {
10+
return stream;
1011
}
1112

12-
return stream;
13+
// `ReadableStream[Symbol.asyncIterator]` support is missing in multiple browsers, so we ponyfill it
14+
if (toString.call(stream) === '[object ReadableStream]') {
15+
return ponyfill.asyncIterator.call(stream);
16+
}
17+
18+
throw new TypeError('The first argument must be a Readable, a ReadableStream, or an async iterable.');
1319
};
1420

21+
const {toString} = Object.prototype;
22+
1523
// The default iterable for Node.js streams does not allow for multiple readers at once, so we re-implement it
1624
const getStreamIterable = async function * (stream) {
1725
if (nodeImports === undefined) {

source/web-stream.js

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export const ponyfill = {};
2+
3+
const {prototype} = ReadableStream;
4+
5+
// Use this library as a ponyfill instead of a polyfill.
6+
// I.e. avoid modifying global variables.
7+
// We can remove this once https://github.com/Sec-ant/readable-stream/issues/2 is fixed
8+
if (prototype[Symbol.asyncIterator] === undefined && prototype.values === undefined) {
9+
await import('@sec-ant/readable-stream');
10+
ponyfill.asyncIterator = prototype[Symbol.asyncIterator];
11+
delete prototype[Symbol.asyncIterator];
12+
delete prototype.values;
13+
}

test/helpers/index.js

+14
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,23 @@
11
import {Duplex, Readable} from 'node:stream';
2+
import {finished} from 'node:stream/promises';
23

34
export const createStream = streamDef => typeof streamDef === 'function'
45
? Duplex.from(streamDef)
56
: Readable.from(streamDef);
67

8+
// @todo Use ReadableStream.from() after dropping support for Node 18
9+
export const readableStreamFrom = chunks => new ReadableStream({
10+
start(controller) {
11+
for (const chunk of chunks) {
12+
controller.enqueue(chunk);
13+
}
14+
15+
controller.close();
16+
},
17+
});
18+
719
// Tests related to big buffers/strings can be slow. We run them serially and
820
// with a higher timeout to ensure they do not randomly fail.
921
export const BIG_TEST_DURATION = '2m';
22+
23+
export const onFinishedStream = stream => finished(stream, {cleanup: true});

test/stream.js

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import {once} from 'node:events';
22
import {version} from 'node:process';
33
import {Readable, Duplex} from 'node:stream';
4-
import {finished} from 'node:stream/promises';
54
import {scheduler, setTimeout as pSetTimeout} from 'node:timers/promises';
65
import test from 'ava';
76
import onetime from 'onetime';
87
import getStream, {getStreamAsArray, MaxBufferError} from '../source/index.js';
98
import {fixtureString, fixtureMultiString, prematureClose} from './fixtures/index.js';
9+
import {onFinishedStream} from './helpers/index.js';
1010

11-
const onFinishedStream = stream => finished(stream, {cleanup: true});
1211
const noopMethods = {read() {}, write() {}};
1312

1413
// eslint-disable-next-line max-params

test/web-stream-ponyfill.js

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import test from 'ava';
2+
3+
// Emulate browsers that do not support those methods
4+
delete ReadableStream.prototype.values;
5+
delete ReadableStream.prototype[Symbol.asyncIterator];
6+
7+
// Run those tests, but emulating browsers
8+
await import('./web-stream.js');
9+
10+
test('Should not polyfill ReadableStream', t => {
11+
t.is(ReadableStream.prototype.values, undefined);
12+
t.is(ReadableStream.prototype[Symbol.asyncIterator], undefined);
13+
});

test/web-stream.js

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import test from 'ava';
2+
import getStream from '../source/index.js';
3+
import {fixtureString, fixtureMultiString} from './fixtures/index.js';
4+
import {readableStreamFrom, onFinishedStream} from './helpers/index.js';
5+
6+
test('Can use ReadableStream', async t => {
7+
const stream = readableStreamFrom(fixtureMultiString);
8+
t.is(await getStream(stream), fixtureString);
9+
await onFinishedStream(stream);
10+
});
11+
12+
test('Can use already ended ReadableStream', async t => {
13+
const stream = readableStreamFrom(fixtureMultiString);
14+
t.is(await getStream(stream), fixtureString);
15+
t.is(await getStream(stream), '');
16+
await onFinishedStream(stream);
17+
});
18+
19+
test('Can use already canceled ReadableStream', async t => {
20+
let canceledValue;
21+
const stream = new ReadableStream({
22+
cancel(canceledError) {
23+
canceledValue = canceledError;
24+
},
25+
});
26+
const error = new Error('test');
27+
await stream.cancel(error);
28+
t.is(canceledValue, error);
29+
t.is(await getStream(stream), '');
30+
await onFinishedStream(stream);
31+
});
32+
33+
test('Can use already errored ReadableStream', async t => {
34+
const error = new Error('test');
35+
const stream = new ReadableStream({
36+
start(controller) {
37+
controller.error(error);
38+
},
39+
});
40+
t.is(await t.throwsAsync(getStream(stream)), error);
41+
t.is(await t.throwsAsync(onFinishedStream(stream)), error);
42+
});
43+
44+
test('Cancel ReadableStream when maxBuffer is hit', async t => {
45+
let canceled = false;
46+
const stream = new ReadableStream({
47+
start(controller) {
48+
controller.enqueue(fixtureString);
49+
controller.enqueue(fixtureString);
50+
controller.close();
51+
},
52+
cancel() {
53+
canceled = true;
54+
},
55+
});
56+
const error = await t.throwsAsync(
57+
getStream(stream, {maxBuffer: 1}),
58+
{message: /maxBuffer exceeded/},
59+
);
60+
t.deepEqual(error.bufferedData, fixtureString[0]);
61+
await onFinishedStream(stream);
62+
t.true(canceled);
63+
});

0 commit comments

Comments
 (0)