From 5a9b38bf7315a00d3dfb716223cc67179034edb1 Mon Sep 17 00:00:00 2001 From: topboy Date: Tue, 22 Nov 2022 08:55:03 -0600 Subject: [PATCH 1/2] Browser compat fix. --- src/FormDataEncoder.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/FormDataEncoder.ts b/src/FormDataEncoder.ts index 5c035a9..33dc39e 100644 --- a/src/FormDataEncoder.ts +++ b/src/FormDataEncoder.ts @@ -201,11 +201,12 @@ export class FormDataEncoder { const size = isFile(value) ? value.size : value.byteLength if ( this.#options.enableAdditionalHeaders === true - && size != null - && !isNaN(size) + && size != null + && !isNaN(size) ) { header += `${this.#CRLF}Content-Length: ${ - isFile(value) ? value.size : value.byteLength + isFile(value) + ? value.size : value.byteLength }` } @@ -336,7 +337,15 @@ export class FormDataEncoder { async* encode(): AsyncGenerator { for (const part of this.values()) { if (isFile(part)) { - yield* part.stream() + const stream: any = part.stream() + if (stream.getReader instanceof Function) { + const reader: any = stream.getReader() + let result + do { + result = await reader.read() + if (result.value) yield result.value + } while (!result.done) + } else yield* stream } else { yield part } From 9a09de6a5792095a9b9106074354a0dd4c21acfc Mon Sep 17 00:00:00 2001 From: Nick K Date: Thu, 24 Nov 2022 04:48:09 +0300 Subject: [PATCH 2/2] Improve browser compatibility patch. --- package.json | 5 +- pnpm-lock.yaml | 101 +++++++++++++++++++++++++++++ src/FormDataEncoder.ts | 18 ++--- src/util/getStreamIterator.test.ts | 51 +++++++++++++++ src/util/getStreamIterator.ts | 42 ++++++++++++ 5 files changed, 203 insertions(+), 14 deletions(-) create mode 100644 src/util/getStreamIterator.test.ts create mode 100644 src/util/getStreamIterator.ts diff --git a/package.json b/package.json index c0a0b93..3e826ac 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "@octetstream/eslint-config": "6.2.2", "@types/mime-types": "2.1.1", "@types/node": "18.7.23", + "@types/sinon": "^10.0.13", "@typescript-eslint/eslint-plugin": "5.38.1", "@typescript-eslint/parser": "5.38.1", "ava": "4.3.3", @@ -60,8 +61,10 @@ "husky": "8.0.1", "lint-staged": "13.0.3", "pinst": "3.0.0", + "sinon": "^14.0.2", "ts-node": "10.9.1", "ttypescript": "1.5.13", - "typescript": "4.8.4" + "typescript": "4.8.4", + "web-streams-polyfill": "4.0.0-beta.3" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 950ffba..4583c59 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,7 @@ specifiers: '@octetstream/eslint-config': 6.2.2 '@types/mime-types': 2.1.1 '@types/node': 18.7.23 + '@types/sinon': ^10.0.13 '@typescript-eslint/eslint-plugin': 5.38.1 '@typescript-eslint/parser': 5.38.1 ava: 4.3.3 @@ -20,14 +21,17 @@ specifiers: husky: 8.0.1 lint-staged: 13.0.3 pinst: 3.0.0 + sinon: ^14.0.2 ts-node: 10.9.1 ttypescript: 1.5.13 typescript: 4.8.4 + web-streams-polyfill: 4.0.0-beta.3 devDependencies: '@octetstream/eslint-config': 6.2.2_l6v6yegbejckv52s6s6ejhijsy '@types/mime-types': 2.1.1 '@types/node': 18.7.23 + '@types/sinon': 10.0.13 '@typescript-eslint/eslint-plugin': 5.38.1_c7qepppml3d4ahu5cnfwqe6ltq '@typescript-eslint/parser': 5.38.1_ypn2ylkkyfa5i233caldtndbqa ava: 4.3.3 @@ -44,9 +48,11 @@ devDependencies: husky: 8.0.1 lint-staged: 13.0.3 pinst: 3.0.0 + sinon: 14.0.2 ts-node: 10.9.1_gbhfbbeqrol7fxixnzbkbuanxe ttypescript: 1.5.13_mwhvu7sfp6vq5ryuwb6hlbjfka typescript: 4.8.4 + web-streams-polyfill: 4.0.0-beta.3 packages: @@ -223,6 +229,42 @@ packages: tslib: 2.4.0 dev: true + /@sinonjs/commons/1.8.5: + resolution: {integrity: sha512-rTpCA0wG1wUxglBSFdMMY0oTrKYvgf4fNgv/sXbfCVAdf+FnPBdKJR/7XbpTCwbCrvCbdPYnlWaUUYz4V2fPDA==} + dependencies: + type-detect: 4.0.8 + dev: true + + /@sinonjs/commons/2.0.0: + resolution: {integrity: sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==} + dependencies: + type-detect: 4.0.8 + dev: true + + /@sinonjs/fake-timers/7.1.2: + resolution: {integrity: sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg==} + dependencies: + '@sinonjs/commons': 1.8.5 + dev: true + + /@sinonjs/fake-timers/9.1.2: + resolution: {integrity: sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==} + dependencies: + '@sinonjs/commons': 1.8.5 + dev: true + + /@sinonjs/samsam/7.0.1: + resolution: {integrity: sha512-zsAk2Jkiq89mhZovB2LLOdTCxJF4hqqTToGP0ASWlhp4I1hqOjcfmZGafXntCN7MDC6yySH0mFHrYtHceOeLmw==} + dependencies: + '@sinonjs/commons': 2.0.0 + lodash.get: 4.4.2 + type-detect: 4.0.8 + dev: true + + /@sinonjs/text-encoding/0.7.2: + resolution: {integrity: sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==} + dev: true + /@tsconfig/node10/1.0.9: resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==} dev: true @@ -267,6 +309,16 @@ packages: resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==} dev: true + /@types/sinon/10.0.13: + resolution: {integrity: sha512-UVjDqJblVNQYvVNUsj0PuYYw0ELRmgt1Nt5Vk0pT5f16ROGfcKJY8o1HVuMOJOpD727RrGB9EGvoaTQE5tgxZQ==} + dependencies: + '@types/sinonjs__fake-timers': 8.1.2 + dev: true + + /@types/sinonjs__fake-timers/8.1.2: + resolution: {integrity: sha512-9GcLXF0/v3t80caGs5p2rRfkB+a8VBGLJZVih6CNFkx8IZ994wiKKLSRs9nuFwk1HevWs/1mnUmkApGrSGsShA==} + dev: true + /@typescript-eslint/eslint-plugin/5.38.1_c7qepppml3d4ahu5cnfwqe6ltq: resolution: {integrity: sha512-ky7EFzPhqz3XlhS7vPOoMDaQnQMn+9o5ICR9CPr/6bw8HrFkzhMSxuA3gRfiJVvs7geYrSeawGJjZoZQKCOglQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -1058,6 +1110,11 @@ packages: engines: {node: '>=0.3.1'} dev: true + /diff/5.1.0: + resolution: {integrity: sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==} + engines: {node: '>=0.3.1'} + dev: true + /dir-glob/3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -2139,6 +2196,10 @@ packages: is-docker: 2.2.1 dev: true + /isarray/0.0.1: + resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==} + dev: true + /isexe/2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} dev: true @@ -2220,6 +2281,10 @@ packages: object.assign: 4.1.4 dev: true + /just-extend/4.2.1: + resolution: {integrity: sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==} + dev: true + /kind-of/6.0.3: resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} engines: {node: '>=0.10.0'} @@ -2321,6 +2386,10 @@ packages: p-locate: 6.0.0 dev: true + /lodash.get/4.4.2: + resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} + dev: true + /lodash.merge/4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} dev: true @@ -2496,6 +2565,16 @@ packages: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} dev: true + /nise/5.1.2: + resolution: {integrity: sha512-+gQjFi8v+tkfCuSCxfURHLhRhniE/+IaYbIphxAN2JRR9SHKhY8hgXpaXiYfHdw+gcGe4buxgbprBQFab9FkhA==} + dependencies: + '@sinonjs/commons': 2.0.0 + '@sinonjs/fake-timers': 7.1.2 + '@sinonjs/text-encoding': 0.7.2 + just-extend: 4.2.1 + path-to-regexp: 1.8.0 + dev: true + /node-domexception/1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} @@ -2771,6 +2850,12 @@ packages: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} dev: true + /path-to-regexp/1.8.0: + resolution: {integrity: sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==} + dependencies: + isarray: 0.0.1 + dev: true + /path-type/4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -3041,6 +3126,17 @@ packages: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} dev: true + /sinon/14.0.2: + resolution: {integrity: sha512-PDpV0ZI3ZCS3pEqx0vpNp6kzPhHrLx72wA0G+ZLaaJjLIYeE0n8INlgaohKuGy7hP0as5tbUd23QWu5U233t+w==} + dependencies: + '@sinonjs/commons': 2.0.0 + '@sinonjs/fake-timers': 9.1.2 + '@sinonjs/samsam': 7.0.1 + diff: 5.1.0 + nise: 5.1.2 + supports-color: 7.2.0 + dev: true + /slash/3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -3363,6 +3459,11 @@ packages: prelude-ls: 1.2.1 dev: true + /type-detect/4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + dev: true + /type-fest/0.13.1: resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} engines: {node: '>=10'} diff --git a/src/FormDataEncoder.ts b/src/FormDataEncoder.ts index 33dc39e..77a0f6d 100644 --- a/src/FormDataEncoder.ts +++ b/src/FormDataEncoder.ts @@ -1,5 +1,6 @@ /* eslint-disable no-restricted-globals */ import type {RawHeaders, FormDataEncoderHeaders} from "./util/Headers.js" +import {getStreamIterator} from "./util/getStreamIterator.js" import {createBoundary} from "./util/createBoundary.js" import {normalizeValue} from "./util/normalizeValue.js" import {isPlainObject} from "./util/isPlainObject.js" @@ -201,12 +202,11 @@ export class FormDataEncoder { const size = isFile(value) ? value.size : value.byteLength if ( this.#options.enableAdditionalHeaders === true - && size != null - && !isNaN(size) + && size != null + && !isNaN(size) ) { header += `${this.#CRLF}Content-Length: ${ - isFile(value) - ? value.size : value.byteLength + isFile(value) ? value.size : value.byteLength }` } @@ -337,15 +337,7 @@ export class FormDataEncoder { async* encode(): AsyncGenerator { for (const part of this.values()) { if (isFile(part)) { - const stream: any = part.stream() - if (stream.getReader instanceof Function) { - const reader: any = stream.getReader() - let result - do { - result = await reader.read() - if (result.value) yield result.value - } while (!result.done) - } else yield* stream + yield* getStreamIterator(part.stream()) } else { yield part } diff --git a/src/util/getStreamIterator.test.ts b/src/util/getStreamIterator.test.ts new file mode 100644 index 0000000..c5789e9 --- /dev/null +++ b/src/util/getStreamIterator.test.ts @@ -0,0 +1,51 @@ +import test from "ava" + +import {ReadableStream} from "web-streams-polyfill" +import {stub} from "sinon" + +import {getStreamIterator} from "./getStreamIterator.js" + +test( + "Returns readable stream as is, if it implements Symbol.asyncIterator", + + t => { + const stream = new ReadableStream() + + t.is(getStreamIterator(stream), stream) + } +) + +test( + "Returns fallback when given stream does not implement Symbol.asyncIterator", + + t => { + const stream = new ReadableStream() + + stub(stream, Symbol.asyncIterator).get(() => undefined) + + t.false(getStreamIterator(stream) instanceof ReadableStream) + } +) + +test("Reads from the stream using fallback", async t => { + const expected = "Some text" + + const stream = new ReadableStream({ + pull(controller) { + controller.enqueue(new TextEncoder().encode(expected)) + controller.close() + } + }) + + stub(stream, Symbol.asyncIterator).get(() => undefined) + + let actual = "" + const decoder = new TextDecoder() + for await (const chunk of getStreamIterator(stream)) { + actual += decoder.decode(chunk, {stream: true}) + } + + actual += decoder.decode() + + t.is(actual, expected) +}) diff --git a/src/util/getStreamIterator.ts b/src/util/getStreamIterator.ts new file mode 100644 index 0000000..5623b08 --- /dev/null +++ b/src/util/getStreamIterator.ts @@ -0,0 +1,42 @@ +import {isFunction} from "./isFunction.js" + +/** + * Checks if the value is async iterable + */ +const isAsyncIterable = ( + value: unknown +): value is AsyncIterable => ( + isFunction((value as AsyncIterable)[Symbol.asyncIterator]) +) + +/** + * Reads from given ReadableStream + * + * @param readable A ReadableStream to read from + */ +async function* readStream( + readable: ReadableStream +): AsyncGenerator { + const reader = readable.getReader() + + while (true) { + const {done, value} = await reader.read() + + if (done) { + break + } + + yield value + } +} + +/** + * Turns ReadableStream into async iterable when the `Symbol.asyncIterable` is not implemented on given stream. + * + * @param source A ReadableStream to create async iterator for + */ +export const getStreamIterator = ( + source: ReadableStream | AsyncIterable +): AsyncIterable => ( + isAsyncIterable(source) ? source : readStream(source) +)