Skip to content

Commit 734019b

Browse files
committed
fix(storage): uploadBytes now with upstream types, tests
1 parent 3a3824b commit 734019b

4 files changed

Lines changed: 87 additions & 19 deletions

File tree

.github/scripts/compare-types/packages/storage/config.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -166,13 +166,6 @@ const config: PackageConfig = {
166166
'(the Web Streams API type). The Node.js stream type is used because the ' +
167167
'React Native environment does not have the Web Streams API.',
168168
},
169-
{
170-
name: 'uploadBytes',
171-
reason:
172-
'Returns `Promise<TaskResult>` in RN Firebase instead of `Promise<UploadResult>`. ' +
173-
'`TaskResult` is a type alias for `UploadResult`, so the runtime shape is identical; ' +
174-
'the different name is for consistency with the native task system.',
175-
},
176169
{
177170
name: 'uploadBytesResumable',
178171
reason:

packages/storage/e2e/StorageTask.e2e.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1070,6 +1070,45 @@ describe('storage() -> StorageTask', function () {
10701070
});
10711071
});
10721072

1073+
describe('uploadBytes()', function () {
1074+
it('resolves with ref and metadata whose size matches uploaded bytes', async function () {
1075+
const { getStorage, ref, uploadBytes } = storageModular;
1076+
const jsonDerulo = JSON.stringify({ foo: 'bar' });
1077+
const expectedByteLength = jsonDerulo.length;
1078+
1079+
const arrayBuffer = new ArrayBuffer(jsonDerulo.length);
1080+
const arrayBufferView = new Uint8Array(arrayBuffer);
1081+
1082+
for (let i = 0, strLen = jsonDerulo.length; i < strLen; i++) {
1083+
arrayBufferView[i] = jsonDerulo.charCodeAt(i);
1084+
}
1085+
1086+
const uploadResult = await uploadBytes(
1087+
ref(getStorage(), `${PATH}/uploadBytesModular.json`),
1088+
arrayBuffer,
1089+
{
1090+
contentType: 'application/json',
1091+
},
1092+
);
1093+
1094+
uploadResult.ref.fullPath.should.containEql('uploadBytesModular.json');
1095+
uploadResult.metadata.should.be.an.Object();
1096+
uploadResult.metadata.size.should.eql(expectedByteLength);
1097+
uploadResult.metadata.contentType.should.eql('application/json');
1098+
});
1099+
1100+
it('rejects when metadata is not an object', async function () {
1101+
const { getStorage, ref, uploadBytes } = storageModular;
1102+
try {
1103+
await uploadBytes(ref(getStorage(), `${PATH}/uploadBytesBadMeta.json`), new ArrayBuffer(), 123);
1104+
return Promise.reject(new Error('Did not error!'));
1105+
} catch (error) {
1106+
error.message.should.containEql('must be an object value');
1107+
return Promise.resolve();
1108+
}
1109+
});
1110+
});
1111+
10731112
describe('upload tasks', function () {
10741113
// before(async function () {
10751114
// // TODO we need some semi-large assets to upload and download I think?

packages/storage/lib/modular.ts

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,19 @@
1717

1818
import { getApp } from '@react-native-firebase/app';
1919
import { MODULAR_DEPRECATION_ARG } from '@react-native-firebase/app/dist/module/common';
20+
import { NativeFirebaseError } from '@react-native-firebase/app/dist/module/internal';
2021
import type { FirebaseApp } from '@react-native-firebase/app';
2122
import type {
2223
FirebaseStorage,
2324
StorageReference,
2425
FullMetadata,
2526
ListResult,
2627
ListOptions,
27-
TaskResult,
2828
Task,
2929
SettableMetadata,
3030
UploadMetadata,
3131
EmulatorMockTokenOptions,
32+
UploadResult,
3233
} from './types/storage';
3334
import { TaskEvent, TaskState } from './types/storage';
3435
import type { StorageReferenceInternal, StorageInternal } from './types/internal';
@@ -269,43 +270,67 @@ export function updateMetadata(
269270

270271
/**
271272
* Uploads data to this object's location. The upload is not resumable. If the upload is canceled,
272-
* the Promise will reject with the TaskSnapshot. If there is an error it will reject with the StorageError
273+
* the Promise rejects with a {@link NativeFirebaseError} (typically `storage/cancelled`),
274+
* matching other storage upload tasks. Other failures reject with the same error type as {@link uploadBytesResumable}.
273275
* @param storageRef - Storage `Reference` instance.
274276
* @param data - The data (Blob | Uint8Array | ArrayBuffer) to upload to the storage bucket at the reference location.
275277
* @param metadata - A Storage `UploadMetadata` instance to update. Optional.
276-
* @returns {Promise<TaskResult>}
278+
* @returns {Promise<UploadResult>}
277279
*/
278280
export async function uploadBytes(
279281
storageRef: StorageReference,
280282
data: Blob | Uint8Array | ArrayBuffer,
281283
metadata?: UploadMetadata,
282-
): Promise<TaskResult> {
284+
): Promise<UploadResult> {
283285
const task = uploadBytesResumable(storageRef, data, metadata);
284286
return new Promise((resolve, reject) => {
285-
task.on(
287+
let completed = false;
288+
const subscription: { unsubscribe?: () => void } = {};
289+
290+
const settle = (fn: () => void) => {
291+
if (completed) {
292+
return;
293+
}
294+
completed = true;
295+
subscription.unsubscribe?.();
296+
fn();
297+
};
298+
299+
subscription.unsubscribe = task.on(
286300
TaskEvent.STATE_CHANGED,
287301
taskSnapshot => {
288302
switch (taskSnapshot.state) {
289303
case TaskState.RUNNING:
290304
break;
291305
case TaskState.PAUSED:
306+
// we are wrapping the resumable version, just resume if it pauses
292307
task.resume();
293308
break;
294309
case TaskState.SUCCESS:
295-
resolve({ ref: taskSnapshot.ref, metadata: taskSnapshot.metadata });
310+
settle(() => resolve({ ref: taskSnapshot.ref, metadata: taskSnapshot.metadata }));
296311
break;
297312
case TaskState.CANCELED:
298-
// The TaskSnapshot may be useful to have if we reject due to cancel
299-
reject(taskSnapshot);
313+
settle(() =>
314+
reject(
315+
NativeFirebaseError.fromEvent(
316+
{ code: 'cancelled', message: 'User cancelled the operation.' },
317+
'storage',
318+
),
319+
),
320+
);
300321
break;
301322
case TaskState.ERROR:
302-
// this will be handled in the dedicated error listener
323+
// this is handled in the dedicated error listener below
303324
break;
304325
default:
305-
throw new Error(`Unhandled task state in uploadBytes: ${taskSnapshot.state}`);
326+
settle(() =>
327+
reject(new Error(`Unhandled task state in uploadBytes: ${taskSnapshot.state}`)),
328+
);
306329
}
307330
},
308-
error => reject(error),
331+
error => {
332+
settle(() => reject(error));
333+
},
309334
);
310335
});
311336
}

packages/storage/type-test.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import storage, {
33
// Types
44
type FirebaseStorage,
55
type FirebaseStorageTypes,
6+
type StorageReference,
67
// Modular API
78
getStorage,
89
connectStorageEmulator,
@@ -234,10 +235,20 @@ updateMetadata(modularRef1, { cacheControl: 'no-cache' }).then(
234235

235236
uploadBytes(modularRef1, new Blob(), { cacheControl: 'no-cache' }).then(
236237
(result: TaskResult) => {
237-
console.log(result);
238+
const uploadBytesRef: StorageReference = result.ref;
239+
const uploadBytesMeta: FullMetadata = result.metadata;
240+
console.log(uploadBytesRef.fullPath, uploadBytesMeta.size);
238241
},
239242
);
240243

244+
uploadBytes(modularRef1, new Uint8Array([1, 2, 3])).then((result: TaskResult) => {
245+
console.log(result.ref.name, result.metadata.size);
246+
});
247+
248+
uploadBytes(modularRef1, new ArrayBuffer(0)).then((result: TaskResult) => {
249+
console.log(result.metadata.fullPath);
250+
});
251+
241252
const modularUploadBytesResumable = uploadBytesResumable(modularRef1, new Blob(), {
242253
cacheControl: 'no-cache',
243254
});

0 commit comments

Comments
 (0)