Skip to content

Commit

Permalink
Fix browser support (#122)
Browse files Browse the repository at this point in the history
  • Loading branch information
ehmicky committed Mar 15, 2024
1 parent a51d085 commit 4d233d3
Show file tree
Hide file tree
Showing 8 changed files with 118 additions and 5 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"concat"
],
"dependencies": {
"@sec-ant/readable-stream": "^0.3.2",
"is-stream": "^4.0.1"
},
"devDependencies": {
Expand Down
2 changes: 2 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ const {body: readableStream} = await fetch('https://example.com');
console.log(await getStream(readableStream));
```

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.

### Async iterables

```js
Expand Down
14 changes: 11 additions & 3 deletions source/stream.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
import {isReadableStream} from 'is-stream';
import {ponyfill} from './web-stream.js';

export const getAsyncIterable = stream => {
if (isReadableStream(stream, {checkOpen: false})) {
return getStreamIterable(stream);
}

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

return stream;
// `ReadableStream[Symbol.asyncIterator]` support is missing in multiple browsers, so we ponyfill it
if (toString.call(stream) === '[object ReadableStream]') {
return ponyfill.asyncIterator.call(stream);
}

throw new TypeError('The first argument must be a Readable, a ReadableStream, or an async iterable.');
};

const {toString} = Object.prototype;

// The default iterable for Node.js streams does not allow for multiple readers at once, so we re-implement it
const getStreamIterable = async function * (stream) {
if (nodeImports === undefined) {
Expand Down
13 changes: 13 additions & 0 deletions source/web-stream.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export const ponyfill = {};

const {prototype} = ReadableStream;

// Use this library as a ponyfill instead of a polyfill.
// I.e. avoid modifying global variables.
// We can remove this once https://github.com/Sec-ant/readable-stream/issues/2 is fixed
if (prototype[Symbol.asyncIterator] === undefined && prototype.values === undefined) {
await import('@sec-ant/readable-stream');
ponyfill.asyncIterator = prototype[Symbol.asyncIterator];
delete prototype[Symbol.asyncIterator];
delete prototype.values;
}
14 changes: 14 additions & 0 deletions test/helpers/index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,23 @@
import {Duplex, Readable} from 'node:stream';
import {finished} from 'node:stream/promises';

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

// @todo Use ReadableStream.from() after dropping support for Node 18
export const readableStreamFrom = chunks => new ReadableStream({
start(controller) {
for (const chunk of chunks) {
controller.enqueue(chunk);
}

controller.close();
},
});

// Tests related to big buffers/strings can be slow. We run them serially and
// with a higher timeout to ensure they do not randomly fail.
export const BIG_TEST_DURATION = '2m';

export const onFinishedStream = stream => finished(stream, {cleanup: true});
3 changes: 1 addition & 2 deletions test/stream.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import {once} from 'node:events';
import {version} from 'node:process';
import {Readable, Duplex} from 'node:stream';
import {finished} from 'node:stream/promises';
import {scheduler, setTimeout as pSetTimeout} from 'node:timers/promises';
import test from 'ava';
import onetime from 'onetime';
import getStream, {getStreamAsArray, MaxBufferError} from '../source/index.js';
import {fixtureString, fixtureMultiString, prematureClose} from './fixtures/index.js';
import {onFinishedStream} from './helpers/index.js';

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

// eslint-disable-next-line max-params
Expand Down
13 changes: 13 additions & 0 deletions test/web-stream-ponyfill.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import test from 'ava';

// Emulate browsers that do not support those methods
delete ReadableStream.prototype.values;
delete ReadableStream.prototype[Symbol.asyncIterator];

// Run those tests, but emulating browsers
await import('./web-stream.js');

test('Should not polyfill ReadableStream', t => {
t.is(ReadableStream.prototype.values, undefined);
t.is(ReadableStream.prototype[Symbol.asyncIterator], undefined);
});
63 changes: 63 additions & 0 deletions test/web-stream.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import test from 'ava';
import getStream from '../source/index.js';
import {fixtureString, fixtureMultiString} from './fixtures/index.js';
import {readableStreamFrom, onFinishedStream} from './helpers/index.js';

test('Can use ReadableStream', async t => {
const stream = readableStreamFrom(fixtureMultiString);
t.is(await getStream(stream), fixtureString);
await onFinishedStream(stream);
});

test('Can use already ended ReadableStream', async t => {
const stream = readableStreamFrom(fixtureMultiString);
t.is(await getStream(stream), fixtureString);
t.is(await getStream(stream), '');
await onFinishedStream(stream);
});

test('Can use already canceled ReadableStream', async t => {
let canceledValue;
const stream = new ReadableStream({
cancel(canceledError) {
canceledValue = canceledError;
},
});
const error = new Error('test');
await stream.cancel(error);
t.is(canceledValue, error);
t.is(await getStream(stream), '');
await onFinishedStream(stream);
});

test('Can use already errored ReadableStream', async t => {
const error = new Error('test');
const stream = new ReadableStream({
start(controller) {
controller.error(error);
},
});
t.is(await t.throwsAsync(getStream(stream)), error);
t.is(await t.throwsAsync(onFinishedStream(stream)), error);
});

test('Cancel ReadableStream when maxBuffer is hit', async t => {
let canceled = false;
const stream = new ReadableStream({
start(controller) {
controller.enqueue(fixtureString);
controller.enqueue(fixtureString);
controller.close();
},
cancel() {
canceled = true;
},
});
const error = await t.throwsAsync(
getStream(stream, {maxBuffer: 1}),
{message: /maxBuffer exceeded/},
);
t.deepEqual(error.bufferedData, fixtureString[0]);
await onFinishedStream(stream);
t.true(canceled);
});

0 comments on commit 4d233d3

Please sign in to comment.