Skip to content

Commit 2255877

Browse files
feat(util-stream-browser): provide handling utilities for browser stream (#3783)
* feat(util-stream-browser): intial commit to sdk stream mixin * feat(util-stream-browser): implement browser stream utilities * feat(util-stream-browser): update sdkStreamMixin input to unknown * fix(util-stream-browser): address feedbacks Co-authored-by: Trivikram Kamat <[email protected]>
1 parent 0ef4af6 commit 2255877

File tree

4 files changed

+307
-2
lines changed

4 files changed

+307
-2
lines changed

packages/util-stream-browser/package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"build:types": "tsc -p tsconfig.types.json",
88
"build:types:downlevel": "downlevel-dts dist-types dist-types/ts3.4",
99
"clean": "rimraf ./dist-* && rimraf *.tsbuildinfo",
10-
"test": "exit 0"
10+
"test": "jest"
1111
},
1212
"main": "./dist-es/index.js",
1313
"module": "./dist-es/index.js",
@@ -18,11 +18,14 @@
1818
},
1919
"license": "Apache-2.0",
2020
"dependencies": {
21+
"@aws-sdk/fetch-http-handler": "*",
2122
"@aws-sdk/types": "*",
23+
"@aws-sdk/util-base64-browser": "*",
24+
"@aws-sdk/util-hex-encoding": "*",
25+
"@aws-sdk/util-utf8-browser": "*",
2226
"tslib": "^2.3.1"
2327
},
2428
"devDependencies": {
25-
"@types/node": "^10.0.0",
2629
"concurrently": "7.0.0",
2730
"downlevel-dts": "0.7.0",
2831
"rimraf": "3.0.2",
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from "./getAwsChunkedEncodingStream";
2+
export * from "./sdk-stream-mixin";
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
// @jest-environment jsdom
2+
import { streamCollector } from "@aws-sdk/fetch-http-handler";
3+
import { SdkStreamMixin } from "@aws-sdk/types";
4+
import { toBase64 } from "@aws-sdk/util-base64-browser";
5+
import { toHex } from "@aws-sdk/util-hex-encoding";
6+
import { toUtf8 } from "@aws-sdk/util-utf8-browser";
7+
8+
import { sdkStreamMixin } from "./sdk-stream-mixin";
9+
10+
jest.mock("@aws-sdk/fetch-http-handler");
11+
jest.mock("@aws-sdk/util-base64-browser");
12+
jest.mock("@aws-sdk/util-hex-encoding");
13+
jest.mock("@aws-sdk/util-utf8-browser");
14+
15+
const mockStreamCollectorReturn = Uint8Array.from([117, 112, 113]);
16+
(streamCollector as jest.Mock).mockReturnValue(mockStreamCollectorReturn);
17+
18+
describe(sdkStreamMixin.name, () => {
19+
const expectAllTransformsToFail = async (sdkStream: SdkStreamMixin) => {
20+
const transformMethods: Array<keyof SdkStreamMixin> = [
21+
"transformToByteArray",
22+
"transformToString",
23+
"transformToWebStream",
24+
];
25+
for (const method of transformMethods) {
26+
try {
27+
await sdkStream[method]();
28+
fail(new Error("expect subsequent tranform to fail"));
29+
} catch (error) {
30+
expect(error.message).toContain("The stream has already been transformed");
31+
}
32+
}
33+
};
34+
35+
let originalReadableStreamCtr = global.ReadableStream;
36+
const mockReadableStream = jest.fn();
37+
class ReadableStream {
38+
constructor() {
39+
mockReadableStream();
40+
}
41+
}
42+
43+
let payloadStream: ReadableStream;
44+
45+
beforeAll(() => {
46+
global.ReadableStream = ReadableStream as any;
47+
});
48+
49+
beforeEach(() => {
50+
originalReadableStreamCtr = global.ReadableStream;
51+
jest.clearAllMocks();
52+
payloadStream = new ReadableStream();
53+
});
54+
55+
afterEach(() => {
56+
global.ReadableStream = originalReadableStreamCtr;
57+
});
58+
59+
it("should throw if input stream is not a Blob or Web Stream instance", () => {
60+
const originalBlobCtr = global.Blob;
61+
global.Blob = undefined;
62+
global.ReadableStream = undefined;
63+
try {
64+
sdkStreamMixin({});
65+
fail("expect unexpected stream to fail");
66+
} catch (e) {
67+
expect(e.message).toContain("nexpected stream implementation");
68+
global.Blob = originalBlobCtr;
69+
}
70+
});
71+
72+
describe("transformToByteArray", () => {
73+
it("should transform binary stream to byte array", async () => {
74+
const sdkStream = sdkStreamMixin(payloadStream);
75+
const byteArray = await sdkStream.transformToByteArray();
76+
expect(streamCollector as jest.Mock).toBeCalledWith(payloadStream);
77+
expect(byteArray).toEqual(mockStreamCollectorReturn);
78+
});
79+
80+
it("should fail any subsequent tranform calls", async () => {
81+
const sdkStream = sdkStreamMixin(payloadStream);
82+
await sdkStream.transformToByteArray();
83+
await expectAllTransformsToFail(sdkStream);
84+
});
85+
});
86+
87+
describe("transformToString", () => {
88+
let originalTextDecoder = global.TextDecoder;
89+
const mockDecode = jest.fn();
90+
global.TextDecoder = jest.fn().mockImplementation(function () {
91+
return { decode: mockDecode };
92+
});
93+
94+
beforeEach(() => {
95+
originalTextDecoder = global.TextDecoder;
96+
jest.clearAllMocks();
97+
});
98+
99+
afterEach(() => {
100+
global.TextDecoder = originalTextDecoder;
101+
});
102+
103+
it.each([
104+
[undefined, toUtf8],
105+
["utf8", toUtf8],
106+
["utf-8", toUtf8],
107+
["base64", toBase64],
108+
["hex", toHex],
109+
])("should transform to string with %s encoding", async (encoding, encodingFn) => {
110+
const mockEncodedStringValue = `a string with ${encoding} encoding`;
111+
(encodingFn as jest.Mock).mockReturnValueOnce(mockEncodedStringValue);
112+
const sdkStream = sdkStreamMixin(payloadStream);
113+
const str = await sdkStream.transformToString(encoding);
114+
expect(streamCollector).toBeCalled();
115+
expect(encodingFn).toBeCalledWith(mockStreamCollectorReturn);
116+
expect(str).toEqual(mockEncodedStringValue);
117+
});
118+
119+
it("should use TexDecoder to handle other encodings", async () => {
120+
const utfLabel = "windows-1251";
121+
mockDecode.mockReturnValue(`a string with ${utfLabel} encoding`);
122+
const sdkStream = sdkStreamMixin(payloadStream);
123+
const str = await sdkStream.transformToString(utfLabel);
124+
expect(global.TextDecoder).toBeCalledWith(utfLabel);
125+
expect(str).toEqual(`a string with ${utfLabel} encoding`);
126+
});
127+
128+
it("should throw if TextDecoder is not available", async () => {
129+
global.TextDecoder = null;
130+
const utfLabel = "windows-1251";
131+
const sdkStream = sdkStreamMixin(payloadStream);
132+
try {
133+
await sdkStream.transformToString(utfLabel);
134+
fail("expect transformToString to throw when TextDecoder is not available");
135+
} catch (error) {
136+
expect(error.message).toContain("TextDecoder is not available");
137+
}
138+
});
139+
140+
it("should fail any subsequent tranform calls", async () => {
141+
const sdkStream = sdkStreamMixin(payloadStream);
142+
await sdkStream.transformToString();
143+
await expectAllTransformsToFail(sdkStream);
144+
});
145+
});
146+
147+
describe("transformToWebStream with ReadableStream payload", () => {
148+
it("should return the payload if it is Web Stream instance", () => {
149+
const payloadStream = new ReadableStream();
150+
const sdkStream = sdkStreamMixin(payloadStream as any);
151+
const transformed = sdkStream.transformToWebStream();
152+
expect(transformed).toBe(payloadStream);
153+
});
154+
155+
it("should fail any subsequent tranform calls", async () => {
156+
const payloadStream = new ReadableStream();
157+
const sdkStream = sdkStreamMixin(payloadStream as any);
158+
sdkStream.transformToWebStream();
159+
await expectAllTransformsToFail(sdkStream);
160+
});
161+
});
162+
163+
describe("transformToWebStream with Blob payload", () => {
164+
let originalBlobCtr = global.Blob;
165+
const mockBlob = jest.fn();
166+
const mockBlobStream = jest.fn();
167+
class Blob {
168+
constructor() {
169+
mockBlob();
170+
}
171+
172+
stream() {
173+
return mockBlobStream();
174+
}
175+
}
176+
global.Blob = Blob as any;
177+
178+
beforeEach(() => {
179+
global.ReadableStream = undefined;
180+
originalBlobCtr = global.Blob;
181+
jest.clearAllMocks();
182+
});
183+
184+
afterEach(() => {
185+
global.Blob = originalBlobCtr;
186+
});
187+
188+
it("should transform blob to web stream with Blob.stream()", () => {
189+
mockBlobStream.mockReturnValue("transformed");
190+
const payloadStream = new Blob();
191+
const sdkStream = sdkStreamMixin(payloadStream as any);
192+
const transformed = sdkStream.transformToWebStream();
193+
expect(transformed).toBe("transformed");
194+
expect(mockBlobStream).toBeCalled();
195+
});
196+
197+
it("should fail if Blob.stream() is not available", async () => {
198+
class Blob {
199+
constructor() {
200+
mockBlob();
201+
}
202+
}
203+
204+
global.Blob = Blob as any;
205+
const payloadStream = new Blob();
206+
const sdkStream = sdkStreamMixin(payloadStream as any);
207+
try {
208+
sdkStream.transformToWebStream();
209+
fail("expect to fail");
210+
} catch (e) {
211+
expect(e.message).toContain("Please make sure the Blob.stream() is polyfilled");
212+
}
213+
});
214+
215+
it("should fail any subsequent tranform calls", async () => {
216+
const payloadStream = new Blob();
217+
const sdkStream = sdkStreamMixin(payloadStream as any);
218+
sdkStream.transformToWebStream();
219+
await expectAllTransformsToFail(sdkStream);
220+
});
221+
});
222+
});
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { streamCollector } from "@aws-sdk/fetch-http-handler";
2+
import { SdkStream, SdkStreamMixin } from "@aws-sdk/types";
3+
import { toBase64 } from "@aws-sdk/util-base64-browser";
4+
import { toHex } from "@aws-sdk/util-hex-encoding";
5+
import { toUtf8 } from "@aws-sdk/util-utf8-browser";
6+
7+
const ERR_MSG_STREAM_HAS_BEEN_TRANSFORMED = "The stream has already been transformed.";
8+
9+
/**
10+
* The stream handling utility functions for browsers and React Native
11+
*
12+
* @internal
13+
*/
14+
export const sdkStreamMixin = (stream: unknown): SdkStream<ReadableStream | Blob> => {
15+
if (!isBlobInstance(stream) && !isReadableStreamInstance(stream)) {
16+
//@ts-ignore
17+
const name = stream?.__proto__?.constructor?.name || stream;
18+
throw new Error(`Unexpected stream implementation, expect Blob or ReadableStream, got ${name}`);
19+
}
20+
21+
let transformed = false;
22+
const transformToByteArray = async () => {
23+
if (transformed) {
24+
throw new Error(ERR_MSG_STREAM_HAS_BEEN_TRANSFORMED);
25+
}
26+
transformed = true;
27+
return await streamCollector(stream);
28+
};
29+
30+
const blobToWebStream = (blob: Blob) => {
31+
if (typeof blob.stream !== "function") {
32+
throw new Error(
33+
"Cannot transform payload Blob to web stream. Please make sure the Blob.stream() is polyfilled.\n" +
34+
"If you are using React Native, this API is not yet supported, see: https://react-native.canny.io/feature-requests/p/fetch-streaming-body"
35+
);
36+
}
37+
return blob.stream();
38+
};
39+
40+
return Object.assign<ReadableStream | Blob, SdkStreamMixin>(stream, {
41+
transformToByteArray: transformToByteArray,
42+
43+
transformToString: async (encoding?: string) => {
44+
const buf = await transformToByteArray();
45+
if (encoding === "base64") {
46+
return toBase64(buf);
47+
} else if (encoding === "hex") {
48+
return toHex(buf);
49+
} else if (encoding === undefined || encoding === "utf8" || encoding === "utf-8") {
50+
// toUtf8() itself will use TextDecoder and fallback to pure JS implementation.
51+
return toUtf8(buf);
52+
} else if (typeof TextDecoder === "function") {
53+
return new TextDecoder(encoding).decode(buf);
54+
} else {
55+
throw new Error("TextDecoder is not available, please make sure polyfill is provided.");
56+
}
57+
},
58+
59+
transformToWebStream: () => {
60+
if (transformed) {
61+
throw new Error(ERR_MSG_STREAM_HAS_BEEN_TRANSFORMED);
62+
}
63+
transformed = true;
64+
if (isBlobInstance(stream)) {
65+
// ReadableStream is undefined in React Native
66+
return blobToWebStream(stream);
67+
} else if (isReadableStreamInstance(stream)) {
68+
return stream;
69+
} else {
70+
throw new Error(`Cannot transform payload to web stream, got ${stream}`);
71+
}
72+
},
73+
});
74+
};
75+
76+
const isBlobInstance = (stream: unknown): stream is Blob => typeof Blob === "function" && stream instanceof Blob;
77+
78+
const isReadableStreamInstance = (stream: unknown): stream is ReadableStream =>
79+
typeof ReadableStream === "function" && stream instanceof ReadableStream;

0 commit comments

Comments
 (0)