Skip to content

Commit 6b0ca77

Browse files
authored
Migrate from the Node to the Web ReadableStream (#8410)
1 parent cfca9c6 commit 6b0ca77

File tree

11 files changed

+66
-47
lines changed

11 files changed

+66
-47
lines changed

.changeset/itchy-boxes-try.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'firebase': minor
3+
'@firebase/storage': minor
4+
---
5+
6+
Migrate from the Node to Web ReadableStream interface

common/api-review/storage.api.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ export function getMetadata(ref: StorageReference): Promise<FullMetadata>;
132132
export function getStorage(app?: FirebaseApp, bucketUrl?: string): FirebaseStorage;
133133

134134
// @public
135-
export function getStream(ref: StorageReference, maxDownloadSizeBytes?: number): NodeJS.ReadableStream;
135+
export function getStream(ref: StorageReference, maxDownloadSizeBytes?: number): ReadableStream;
136136

137137
// @internal (undocumented)
138138
export function _invalidArgument(message: string): StorageError;

docs-devsite/storage.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ This API is only available in Node.
279279
<b>Signature:</b>
280280

281281
```typescript
282-
export declare function getStream(ref: StorageReference, maxDownloadSizeBytes?: number): NodeJS.ReadableStream;
282+
export declare function getStream(ref: StorageReference, maxDownloadSizeBytes?: number): ReadableStream;
283283
```
284284

285285
#### Parameters
@@ -291,7 +291,7 @@ export declare function getStream(ref: StorageReference, maxDownloadSizeBytes?:
291291

292292
<b>Returns:</b>
293293

294-
NodeJS.ReadableStream
294+
ReadableStream
295295

296296
A stream with the object's data as bytes
297297

packages/storage/src/api.browser.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,6 @@ export function getBlob(
5858
export function getStream(
5959
ref: StorageReference,
6060
maxDownloadSizeBytes?: number
61-
): NodeJS.ReadableStream {
61+
): ReadableStream {
6262
throw new Error('getStream() is only supported by NodeJS builds');
6363
}

packages/storage/src/api.node.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export function getBlob(
5858
export function getStream(
5959
ref: StorageReference,
6060
maxDownloadSizeBytes?: number
61-
): NodeJS.ReadableStream {
61+
): ReadableStream {
6262
ref = getModularInstance(ref);
6363
return getStreamInternal(ref as Reference, maxDownloadSizeBytes);
6464
}

packages/storage/src/implementation/connection.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
* See the License for the specific language governing permissions and
1515
* limitations under the License.
1616
*/
17-
1817
/** Network headers */
1918
export type Headers = Record<string, string>;
2019

@@ -23,7 +22,7 @@ export type ConnectionType =
2322
| string
2423
| ArrayBuffer
2524
| Blob
26-
| NodeJS.ReadableStream;
25+
| ReadableStream<Uint8Array>;
2726

2827
/**
2928
* A lightweight wrapper around XMLHttpRequest with a

packages/storage/src/platform/browser/connection.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ export function newBlobConnection(): Connection<Blob> {
171171
return new XhrBlobConnection();
172172
}
173173

174-
export function newStreamConnection(): Connection<NodeJS.ReadableStream> {
174+
export function newStreamConnection(): Connection<ReadableStream> {
175175
throw new Error('Streams are only supported on Node');
176176
}
177177

packages/storage/src/platform/connection.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export function newBlobConnection(): Connection<Blob> {
4545
return nodeNewBlobConnection();
4646
}
4747

48-
export function newStreamConnection(): Connection<NodeJS.ReadableStream> {
48+
export function newStreamConnection(): Connection<ReadableStream<Uint8Array>> {
4949
// This file is only used in Node.js tests using ts-node.
5050
return nodeNewStreamConnection();
5151
}

packages/storage/src/platform/node/connection.ts

+11-9
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ abstract class FetchConnection<T extends ConnectionType>
5050
async send(
5151
url: string,
5252
method: string,
53-
body?: ArrayBufferView | Blob | string,
53+
body?: NodeJS.ArrayBufferView | Blob | string,
5454
headers?: Record<string, string>
5555
): Promise<void> {
5656
if (this.sent_) {
@@ -62,7 +62,7 @@ abstract class FetchConnection<T extends ConnectionType>
6262
const response = await this.fetch_(url, {
6363
method,
6464
headers: headers || {},
65-
body: body as ArrayBufferView | string
65+
body: body as NodeJS.ArrayBufferView | string
6666
});
6767
this.headers_ = response.headers;
6868
this.statusCode_ = response.status;
@@ -146,13 +146,15 @@ export function newBytesConnection(): Connection<ArrayBuffer> {
146146
return new FetchBytesConnection();
147147
}
148148

149-
export class FetchStreamConnection extends FetchConnection<NodeJS.ReadableStream> {
150-
private stream_: NodeJS.ReadableStream | null = null;
149+
export class FetchStreamConnection extends FetchConnection<
150+
ReadableStream<Uint8Array>
151+
> {
152+
private stream_: ReadableStream<Uint8Array> | null = null;
151153

152154
async send(
153155
url: string,
154156
method: string,
155-
body?: ArrayBufferView | Blob | string,
157+
body?: NodeJS.ArrayBufferView | Blob | string,
156158
headers?: Record<string, string>
157159
): Promise<void> {
158160
if (this.sent_) {
@@ -164,12 +166,12 @@ export class FetchStreamConnection extends FetchConnection<NodeJS.ReadableStream
164166
const response = await this.fetch_(url, {
165167
method,
166168
headers: headers || {},
167-
body: body as ArrayBufferView | string
169+
body: body as NodeJS.ArrayBufferView | string
168170
});
169171
this.headers_ = response.headers;
170172
this.statusCode_ = response.status;
171173
this.errorCode_ = ErrorCode.NO_ERROR;
172-
this.stream_ = response.body;
174+
this.stream_ = response.body as ReadableStream<Uint8Array>;
173175
} catch (e) {
174176
this.errorText_ = (e as Error)?.message;
175177
// emulate XHR which sets status to 0 when encountering a network error
@@ -178,15 +180,15 @@ export class FetchStreamConnection extends FetchConnection<NodeJS.ReadableStream
178180
}
179181
}
180182

181-
getResponse(): NodeJS.ReadableStream {
183+
getResponse(): ReadableStream {
182184
if (!this.stream_) {
183185
throw internalError('cannot .getResponse() before sending');
184186
}
185187
return this.stream_;
186188
}
187189
}
188190

189-
export function newStreamConnection(): Connection<NodeJS.ReadableStream> {
191+
export function newStreamConnection(): Connection<ReadableStream<Uint8Array>> {
190192
return new FetchStreamConnection();
191193
}
192194

packages/storage/src/reference.ts

+16-17
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@
1919
* @fileoverview Defines the Firebase StorageReference class.
2020
*/
2121

22-
import { PassThrough, Transform, TransformOptions } from 'stream';
23-
2422
import { FbsBlob } from './implementation/blob';
2523
import { Location } from './implementation/location';
2624
import { getMappings } from './implementation/metadata';
@@ -48,6 +46,7 @@ import {
4846
newStreamConnection,
4947
newTextConnection
5048
} from './platform/connection';
49+
import { RequestInfo } from './implementation/requestinfo';
5150

5251
/**
5352
* Provides methods to interact with a bucket in the Firebase Storage service.
@@ -203,42 +202,42 @@ export function getBlobInternal(
203202
export function getStreamInternal(
204203
ref: Reference,
205204
maxDownloadSizeBytes?: number
206-
): NodeJS.ReadableStream {
205+
): ReadableStream {
207206
ref._throwIfRoot('getStream');
208-
const requestInfo = getBytes(
207+
const requestInfo: RequestInfo<ReadableStream, ReadableStream> = getBytes(
209208
ref.storage,
210209
ref._location,
211210
maxDownloadSizeBytes
212211
);
213212

214-
/** A transformer that passes through the first n bytes. */
215-
const newMaxSizeTransform: (n: number) => TransformOptions = n => {
213+
// Transforms the stream so that only `maxDownloadSizeBytes` bytes are piped to the result
214+
const newMaxSizeTransform = (n: number): Transformer => {
216215
let missingBytes = n;
217216
return {
218-
transform(chunk, encoding, callback) {
217+
transform(chunk, controller: TransformStreamDefaultController) {
219218
// GCS may not honor the Range header for small files
220219
if (chunk.length < missingBytes) {
221-
this.push(chunk);
220+
controller.enqueue(chunk);
222221
missingBytes -= chunk.length;
223222
} else {
224-
this.push(chunk.slice(0, missingBytes));
225-
this.emit('end');
223+
controller.enqueue(chunk.slice(0, missingBytes));
224+
controller.terminate();
226225
}
227-
callback();
228226
}
229-
} as TransformOptions;
227+
};
230228
};
231229

232230
const result =
233231
maxDownloadSizeBytes !== undefined
234-
? new Transform(newMaxSizeTransform(maxDownloadSizeBytes))
235-
: new PassThrough();
232+
? new TransformStream(newMaxSizeTransform(maxDownloadSizeBytes))
233+
: new TransformStream(); // The default transformer forwards all chunks to its readable side
236234

237235
ref.storage
238236
.makeRequestWithTokens(requestInfo, newStreamConnection)
239-
.then(stream => (stream as NodeJS.ReadableStream).pipe(result))
240-
.catch(e => result.destroy(e));
241-
return result;
237+
.then(readableStream => readableStream.pipeThrough(result))
238+
.catch(err => result.writable.abort(err));
239+
240+
return result.readable;
242241
}
243242

244243
/**

packages/storage/test/node/stream.test.ts

+25-12
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,27 @@ import { FirebaseApp, deleteApp } from '@firebase/app';
2121
import { getStream, ref, uploadBytes } from '../../src/index.node';
2222
import * as types from '../../src/public-types';
2323

24-
async function readData(reader: NodeJS.ReadableStream): Promise<number[]> {
25-
return new Promise<number[]>((resolve, reject) => {
26-
const data: number[] = [];
27-
reader.on('error', e => reject(e));
28-
reader.on('data', chunk => data.push(...Array.from(chunk as Buffer)));
29-
reader.on('end', () => resolve(data));
24+
// See: https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream/getReader
25+
async function readData(readableStream: ReadableStream): Promise<Uint8Array> {
26+
return new Promise<Uint8Array>((resolve, reject) => {
27+
const reader: ReadableStreamDefaultReader = readableStream.getReader();
28+
const result: any[] = [];
29+
reader
30+
.read()
31+
.then(function processBytes({ done, value }): any {
32+
if (done) {
33+
resolve(new Uint8Array(result));
34+
return;
35+
}
36+
37+
result.push(...value);
38+
return reader.read().then(processBytes);
39+
})
40+
.catch(err => {
41+
console.error(err);
42+
reject(err);
43+
return;
44+
});
3045
});
3146
}
3247

@@ -46,19 +61,17 @@ describe('Firebase Storage > getStream', () => {
4661
it('can get stream', async () => {
4762
const reference = ref(storage, 'public/exp-bytes');
4863
await uploadBytes(reference, new Uint8Array([0, 1, 3, 128, 255]));
49-
const stream = await getStream(reference);
64+
const stream = getStream(reference);
5065
const data = await readData(stream);
51-
expect(new Uint8Array(data)).to.deep.equal(
52-
new Uint8Array([0, 1, 3, 128, 255])
53-
);
66+
expect(data).to.deep.equal(new Uint8Array([0, 1, 3, 128, 255]));
5467
});
5568

5669
it('can get first n bytes of stream', async () => {
5770
const reference = ref(storage, 'public/exp-bytes');
5871
await uploadBytes(reference, new Uint8Array([0, 1, 3]));
59-
const stream = await getStream(reference, 2);
72+
const stream = getStream(reference, 2);
6073
const data = await readData(stream);
61-
expect(new Uint8Array(data)).to.deep.equal(new Uint8Array([0, 1]));
74+
expect(data).to.deep.equal(new Uint8Array([0, 1]));
6275
});
6376

6477
it('getStream() throws for missing file', async () => {

0 commit comments

Comments
 (0)