Skip to content

Commit e43fcd8

Browse files
committed
Backport fix for browsers compatibility
1 parent dbe3f91 commit e43fcd8

File tree

5 files changed

+232
-2
lines changed

5 files changed

+232
-2
lines changed

lib/FormDataEncoder.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/* eslint-disable no-restricted-globals */
22

3+
import getStreamIterator from "./util/getStreamIterator"
34
import createBoundary from "./util/createBoundary"
45
import isPlainObject from "./util/isPlainObject"
56
import normalize from "./util/normalizeValue"
@@ -319,7 +320,7 @@ export class FormDataEncoder {
319320
async* encode(): AsyncGenerator<Uint8Array, void, undefined> {
320321
for (const part of this.values()) {
321322
if (isFileLike(part)) {
322-
yield* part.stream()
323+
yield* getStreamIterator(part.stream())
323324
} else {
324325
yield part
325326
}

lib/util/getStreamIterator.test.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import test from "ava"
2+
3+
import {ReadableStream} from "web-streams-polyfill"
4+
import {stub} from "sinon"
5+
6+
import getStreamIterator from "./getStreamIterator"
7+
8+
test(
9+
"Returns readable stream as is, if it implements Symbol.asyncIterator",
10+
11+
t => {
12+
const stream = new ReadableStream()
13+
14+
t.is(getStreamIterator(stream), stream)
15+
}
16+
)
17+
18+
test(
19+
"Returns fallback when given stream does not implement Symbol.asyncIterator",
20+
21+
t => {
22+
const stream = new ReadableStream()
23+
24+
stub(stream, Symbol.asyncIterator).get(() => undefined)
25+
26+
t.false(getStreamIterator(stream) instanceof ReadableStream)
27+
}
28+
)
29+
30+
test("Reads from the stream using fallback", async t => {
31+
const expected = "Some text"
32+
33+
const stream = new ReadableStream({
34+
pull(controller) {
35+
controller.enqueue(new TextEncoder().encode(expected))
36+
controller.close()
37+
}
38+
})
39+
40+
stub(stream, Symbol.asyncIterator).get(() => undefined)
41+
42+
let actual = ""
43+
const decoder = new TextDecoder()
44+
for await (const chunk of getStreamIterator(stream)) {
45+
actual += decoder.decode(chunk, {stream: true})
46+
}
47+
48+
actual += decoder.decode()
49+
50+
t.is(actual, expected)
51+
})
52+
53+
test("Throws TypeError for unsupported data sources", t => {
54+
// @ts-expect-error
55+
const trap = () => getStreamIterator({})
56+
57+
t.throws(trap, {
58+
instanceOf: TypeError,
59+
message: "Unsupported data source: Expected either "
60+
+ "ReadableStream or async iterable."
61+
})
62+
})

lib/util/getStreamIterator.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import isFunction from "./isFunction"
2+
3+
/**
4+
* Checks if the value is async iterable
5+
*/
6+
const isAsyncIterable = (
7+
value: unknown
8+
): value is AsyncIterable<Uint8Array> => (
9+
isFunction((value as AsyncIterable<Uint8Array>)[Symbol.asyncIterator])
10+
)
11+
12+
/**
13+
* Reads from given ReadableStream
14+
*
15+
* @param readable A ReadableStream to read from
16+
*/
17+
async function* readStream(
18+
readable: ReadableStream<Uint8Array>
19+
): AsyncGenerator<Uint8Array, void, undefined> {
20+
const reader = readable.getReader()
21+
22+
while (true) {
23+
const {done, value} = await reader.read()
24+
25+
if (done) {
26+
break
27+
}
28+
29+
yield value
30+
}
31+
}
32+
33+
/**
34+
* Turns ReadableStream into async iterable when the `Symbol.asyncIterable` is not implemented on given stream.
35+
*
36+
* @param source A ReadableStream to create async iterator for
37+
*/
38+
const getStreamIterator = (
39+
source: ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>
40+
): AsyncIterable<Uint8Array> => {
41+
if (isAsyncIterable(source)) {
42+
return source
43+
}
44+
45+
if (isFunction(source.getReader)) {
46+
return readStream(source)
47+
}
48+
49+
// Throw an error otherwise (for example, in case if encountered Node.js Readable stream without Symbol.asyncIterator method)
50+
throw new TypeError(
51+
"Unsupported data source: Expected either ReadableStream or async iterable."
52+
)
53+
}
54+
55+
export default getStreamIterator

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"@octetstream/eslint-config": "5.0.0",
4242
"@types/mime-types": "2.1.1",
4343
"@types/node": "17.0.25",
44+
"@types/sinon": "^10.0.13",
4445
"@typescript-eslint/eslint-plugin": "4.33.0",
4546
"@typescript-eslint/parser": "4.33.0",
4647
"@zoltu/typescript-transformer-append-js-extension": "1.0.1",
@@ -56,8 +57,10 @@
5657
"husky": "7.0.4",
5758
"lint-staged": "12.4.0",
5859
"pinst": "2.1.6",
60+
"sinon": "14.0.2",
5961
"ts-node": "10.7.0",
6062
"ttypescript": "1.5.13",
61-
"typescript": "4.6.3"
63+
"typescript": "4.6.3",
64+
"web-streams-polyfill": "4.0.0-beta.3"
6265
}
6366
}

pnpm-lock.yaml

Lines changed: 109 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)