Skip to content

Commit c257049

Browse files
richardddricharddavisonkuhe
authored
feat(fetch-http-handler): improve performance, replace FileReader with Blob.arrayBuffer() (#1179)
* Update stream-collector.ts * Update stream collector * Add changeset * remove FileReader where possible --------- Co-authored-by: Richard Davison <[email protected]> Co-authored-by: George Fu <[email protected]>
1 parent f4e0bd9 commit c257049

File tree

6 files changed

+48
-89
lines changed

6 files changed

+48
-89
lines changed

.changeset/breezy-cobras-repair.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@smithy/chunked-blob-reader": major
3+
"@smithy/fetch-http-handler": major
4+
"@smithy/chunked-blob-reader-native": patch
5+
---
6+
7+
replace FileReader with Blob.arrayBuffer() where possible

packages/chunked-blob-reader-native/src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ export function blobReader(
88
chunkSize: number = 1024 * 1024
99
): Promise<void> {
1010
return new Promise((resolve, reject) => {
11+
/**
12+
* TODO(react-native): https://github.com/facebook/react-native/issues/34402
13+
* To drop FileReader in react-native, we need the Blob.arrayBuffer() method to work.
14+
*/
1115
const fileReader = new FileReader();
1216

1317
fileReader.onerror = reject;

packages/chunked-blob-reader/src/index.spec.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1+
import { Blob as BlobPolyfill } from "buffer";
2+
13
import { blobReader } from "./index";
24

5+
// jsdom inaccurate Blob https://github.com/jsdom/jsdom/issues/2555.
6+
global.Blob = BlobPolyfill as any;
7+
38
describe("blobReader", () => {
49
it("reads an entire blob", async () => {
510
const longMessage: number[] = [];
Lines changed: 9 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,18 @@
11
/**
22
* @internal
3+
* Reads the blob data into the onChunk consumer.
34
*/
4-
export function blobReader(
5+
export async function blobReader(
56
blob: Blob,
67
onChunk: (chunk: Uint8Array) => void,
78
chunkSize: number = 1024 * 1024
89
): Promise<void> {
9-
return new Promise((resolve, reject) => {
10-
const fileReader = new FileReader();
10+
const size = blob.size;
11+
let totalBytesRead = 0;
1112

12-
fileReader.addEventListener("error", reject);
13-
fileReader.addEventListener("abort", reject);
14-
15-
const size = blob.size;
16-
let totalBytesRead = 0;
17-
18-
function read() {
19-
if (totalBytesRead >= size) {
20-
resolve();
21-
return;
22-
}
23-
fileReader.readAsArrayBuffer(blob.slice(totalBytesRead, Math.min(size, totalBytesRead + chunkSize)));
24-
}
25-
26-
fileReader.addEventListener("load", (event) => {
27-
const result = <ArrayBuffer>(event.target as any).result;
28-
onChunk(new Uint8Array(result));
29-
totalBytesRead += result.byteLength;
30-
// read the next block
31-
read();
32-
});
33-
34-
// kick off the read
35-
read();
36-
});
13+
while (totalBytesRead < size) {
14+
const slice: Blob = blob.slice(totalBytesRead, Math.min(size, totalBytesRead + chunkSize));
15+
onChunk(new Uint8Array(await slice.arrayBuffer()));
16+
totalBytesRead += slice.size;
17+
}
3718
}
Lines changed: 21 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,29 @@
1+
import { Blob as BlobPolyfill } from "buffer";
2+
13
import { streamCollector } from "./stream-collector";
24

3-
/**
4-
* Have to mock the FileReader behavior in IE, where
5-
* reader.result is null if reads an empty blob.
6-
*/
5+
// jsdom inaccurate Blob https://github.com/jsdom/jsdom/issues/2555.
6+
global.Blob = BlobPolyfill as any;
7+
78
describe("streamCollector", () => {
8-
let originalFileReader = (global as any).FileReader;
9-
let originalBlob = (global as any).Blob;
10-
beforeAll(() => {
11-
originalFileReader = (global as any).FileReader;
12-
originalBlob = (global as any).Blob;
13-
});
14-
afterAll(() => {
15-
(global as any).FileReader = originalFileReader;
16-
(global as any).Blob = originalBlob;
9+
const blobAvailable = typeof Blob === "function";
10+
const readableStreamAvailable = typeof ReadableStream === "function";
11+
12+
(blobAvailable ? it : it.skip)("collects Blob into bytearray", async () => {
13+
const blobby = new Blob([new Uint8Array([1, 2]), new Uint8Array([3, 4])]);
14+
const collected = await streamCollector(blobby);
15+
expect(collected).toEqual(new Uint8Array([1, 2, 3, 4]));
1716
});
1817

19-
it("returns a Uint8Array when blob is empty and when FileReader data is null(in IE)", (done) => {
20-
(global as any).FileReader = function FileReader() {
21-
this.result = null; //In IE, FileReader.result is null after reading empty blob
22-
this.readAsDataURL = jest.fn().mockImplementation(() => {
23-
if (this.onloadend) {
24-
this.readyState = 2;
25-
this.onloadend();
26-
}
27-
});
28-
};
29-
(global as any).Blob = function Blob() {};
30-
const dataPromise = streamCollector(new Blob());
31-
dataPromise.then((data: any) => {
32-
expect(data).toEqual(Uint8Array.from([]));
33-
done();
18+
(readableStreamAvailable ? it : it.skip)("collects ReadableStream into bytearray", async () => {
19+
const stream = new ReadableStream({
20+
start(controller) {
21+
controller.enqueue(new Uint8Array([1, 2]));
22+
controller.enqueue(new Uint8Array([3, 4]));
23+
controller.close();
24+
},
3425
});
26+
const collected = await streamCollector(stream);
27+
expect(collected).toEqual(new Uint8Array([1, 2, 3, 4]));
3528
});
3629
});
Lines changed: 2 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,13 @@
11
import { StreamCollector } from "@smithy/types";
2-
import { fromBase64 } from "@smithy/util-base64";
32

4-
//reference: https://snack.expo.io/r1JCSWRGU
5-
export const streamCollector: StreamCollector = (stream: Blob | ReadableStream): Promise<Uint8Array> => {
3+
export const streamCollector: StreamCollector = async (stream: Blob | ReadableStream): Promise<Uint8Array> => {
64
if (typeof Blob === "function" && stream instanceof Blob) {
7-
return collectBlob(stream);
5+
return new Uint8Array(await stream.arrayBuffer());
86
}
97

108
return collectStream(stream as ReadableStream);
119
};
1210

13-
async function collectBlob(blob: Blob): Promise<Uint8Array> {
14-
const base64 = await readToBase64(blob);
15-
const arrayBuffer = fromBase64(base64);
16-
return new Uint8Array(arrayBuffer);
17-
}
18-
1911
async function collectStream(stream: ReadableStream): Promise<Uint8Array> {
2012
const chunks = [];
2113
const reader = stream.getReader();
@@ -40,26 +32,3 @@ async function collectStream(stream: ReadableStream): Promise<Uint8Array> {
4032

4133
return collected;
4234
}
43-
44-
function readToBase64(blob: Blob): Promise<string> {
45-
return new Promise((resolve, reject) => {
46-
const reader = new FileReader();
47-
reader.onloadend = () => {
48-
// reference: https://developer.mozilla.org/en-US/docs/Web/API/FileReader/readAsDataURL
49-
// response from readAsDataURL is always prepended with "data:*/*;base64,"
50-
if (reader.readyState !== 2) {
51-
return reject(new Error("Reader aborted too early"));
52-
}
53-
const result = (reader.result ?? "") as string;
54-
// Response can include only 'data:' for empty blob, return empty string in this case.
55-
// Otherwise, return the string after ','
56-
const commaIndex = result.indexOf(",");
57-
const dataOffset = commaIndex > -1 ? commaIndex + 1 : result.length;
58-
resolve(result.substring(dataOffset));
59-
};
60-
reader.onabort = () => reject(new Error("Read aborted"));
61-
reader.onerror = () => reject(reader.error);
62-
// reader.readAsArrayBuffer is not always available
63-
reader.readAsDataURL(blob);
64-
});
65-
}

0 commit comments

Comments
 (0)