diff --git a/.changeset/breezy-cobras-repair.md b/.changeset/breezy-cobras-repair.md new file mode 100644 index 00000000000..c2c371f4ef3 --- /dev/null +++ b/.changeset/breezy-cobras-repair.md @@ -0,0 +1,7 @@ +--- +"@smithy/chunked-blob-reader": major +"@smithy/fetch-http-handler": major +"@smithy/chunked-blob-reader-native": patch +--- + +replace FileReader with Blob.arrayBuffer() where possible diff --git a/.changeset/fluffy-planes-grow.md b/.changeset/fluffy-planes-grow.md deleted file mode 100644 index 9a1fcefe4ff..00000000000 --- a/.changeset/fluffy-planes-grow.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@smithy/fetch-http-handler": minor ---- - -Improve blob stream collector performance diff --git a/packages/chunked-blob-reader-native/src/index.ts b/packages/chunked-blob-reader-native/src/index.ts index d208d5994c8..abf8db9667b 100644 --- a/packages/chunked-blob-reader-native/src/index.ts +++ b/packages/chunked-blob-reader-native/src/index.ts @@ -8,6 +8,10 @@ export function blobReader( chunkSize: number = 1024 * 1024 ): Promise { return new Promise((resolve, reject) => { + /** + * TODO(react-native): https://github.com/facebook/react-native/issues/34402 + * To drop FileReader in react-native, we need the Blob.arrayBuffer() method to work. + */ const fileReader = new FileReader(); fileReader.onerror = reject; diff --git a/packages/chunked-blob-reader/src/index.spec.ts b/packages/chunked-blob-reader/src/index.spec.ts index 290696968bb..79a5aa924ea 100644 --- a/packages/chunked-blob-reader/src/index.spec.ts +++ b/packages/chunked-blob-reader/src/index.spec.ts @@ -1,5 +1,10 @@ +import { Blob as BlobPolyfill } from "buffer"; + import { blobReader } from "./index"; +// jsdom inaccurate Blob https://github.com/jsdom/jsdom/issues/2555. +global.Blob = BlobPolyfill as any; + describe("blobReader", () => { it("reads an entire blob", async () => { const longMessage: number[] = []; diff --git a/packages/chunked-blob-reader/src/index.ts b/packages/chunked-blob-reader/src/index.ts index b3fffeaec1c..ccb312fb503 100644 --- a/packages/chunked-blob-reader/src/index.ts +++ b/packages/chunked-blob-reader/src/index.ts @@ -1,37 +1,18 @@ /** * @internal + * Reads the blob data into the onChunk consumer. */ -export function blobReader( +export async function blobReader( blob: Blob, onChunk: (chunk: Uint8Array) => void, chunkSize: number = 1024 * 1024 ): Promise { - return new Promise((resolve, reject) => { - const fileReader = new FileReader(); + const size = blob.size; + let totalBytesRead = 0; - fileReader.addEventListener("error", reject); - fileReader.addEventListener("abort", reject); - - const size = blob.size; - let totalBytesRead = 0; - - function read() { - if (totalBytesRead >= size) { - resolve(); - return; - } - fileReader.readAsArrayBuffer(blob.slice(totalBytesRead, Math.min(size, totalBytesRead + chunkSize))); - } - - fileReader.addEventListener("load", (event) => { - const result = (event.target as any).result; - onChunk(new Uint8Array(result)); - totalBytesRead += result.byteLength; - // read the next block - read(); - }); - - // kick off the read - read(); - }); + while (totalBytesRead < size) { + const slice: Blob = blob.slice(totalBytesRead, Math.min(size, totalBytesRead + chunkSize)); + onChunk(new Uint8Array(await slice.arrayBuffer())); + totalBytesRead += slice.size; + } } diff --git a/packages/fetch-http-handler/src/stream-collector.spec.ts b/packages/fetch-http-handler/src/stream-collector.spec.ts index 7f7e47e9ffc..77acc30d5b6 100644 --- a/packages/fetch-http-handler/src/stream-collector.spec.ts +++ b/packages/fetch-http-handler/src/stream-collector.spec.ts @@ -1,36 +1,29 @@ +import { Blob as BlobPolyfill } from "buffer"; + import { streamCollector } from "./stream-collector"; -/** - * Have to mock the FileReader behavior in IE, where - * reader.result is null if reads an empty blob. - */ +// jsdom inaccurate Blob https://github.com/jsdom/jsdom/issues/2555. +global.Blob = BlobPolyfill as any; + describe("streamCollector", () => { - let originalFileReader = (global as any).FileReader; - let originalBlob = (global as any).Blob; - beforeAll(() => { - originalFileReader = (global as any).FileReader; - originalBlob = (global as any).Blob; - }); - afterAll(() => { - (global as any).FileReader = originalFileReader; - (global as any).Blob = originalBlob; + const blobAvailable = typeof Blob === "function"; + const readableStreamAvailable = typeof ReadableStream === "function"; + + (blobAvailable ? it : it.skip)("collects Blob into bytearray", async () => { + const blobby = new Blob([new Uint8Array([1, 2]), new Uint8Array([3, 4])]); + const collected = await streamCollector(blobby); + expect(collected).toEqual(new Uint8Array([1, 2, 3, 4])); }); - it("returns a Uint8Array when blob is empty and when FileReader data is null(in IE)", (done) => { - (global as any).FileReader = function FileReader() { - this.result = null; //In IE, FileReader.result is null after reading empty blob - this.readAsDataURL = jest.fn().mockImplementation(() => { - if (this.onloadend) { - this.readyState = 2; - this.onloadend(); - } - }); - }; - (global as any).Blob = function Blob() {}; - const dataPromise = streamCollector(new Blob()); - dataPromise.then((data: any) => { - expect(data).toEqual(Uint8Array.from([])); - done(); + (readableStreamAvailable ? it : it.skip)("collects ReadableStream into bytearray", async () => { + const stream = new ReadableStream({ + start(controller) { + controller.enqueue(new Uint8Array([1, 2])); + controller.enqueue(new Uint8Array([3, 4])); + controller.close(); + }, }); + const collected = await streamCollector(stream); + expect(collected).toEqual(new Uint8Array([1, 2, 3, 4])); }); }); diff --git a/packages/fetch-http-handler/src/stream-collector.ts b/packages/fetch-http-handler/src/stream-collector.ts index 86578b22d75..eefa5053734 100644 --- a/packages/fetch-http-handler/src/stream-collector.ts +++ b/packages/fetch-http-handler/src/stream-collector.ts @@ -1,6 +1,5 @@ import { StreamCollector } from "@smithy/types"; -//reference: https://snack.expo.io/r1JCSWRGU export const streamCollector: StreamCollector = async (stream: Blob | ReadableStream): Promise => { if (typeof Blob === "function" && stream instanceof Blob) { return new Uint8Array(await stream.arrayBuffer());