Skip to content

Commit 972aa85

Browse files
authored
fix(storage): snapshotChanges should return a success snapshot (#2704)
* Works on tasks that are already running * Now appropriately emits a final success snapshot before completion * In theory should emit a canceled or error snapshot before throwing (once the JS SDK is patched) * Added tests for canceling, pausing, and resuming
1 parent 984006d commit 972aa85

File tree

2 files changed

+92
-7
lines changed

2 files changed

+92
-7
lines changed

src/storage/observable/fromTask.ts

+22-6
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,40 @@
11
import { Observable } from 'rxjs';
2-
import { shareReplay } from 'rxjs/operators';
2+
import { debounceTime } from 'rxjs/operators';
33
import { UploadTask, UploadTaskSnapshot } from '../interfaces';
44

55
// need to import, else the types become import('firebase/app').default.storage.UploadTask
66
// and it no longer works w/Firebase v7
77
import firebase from 'firebase/app';
88

9+
// Things aren't working great, I'm having to put in a lot of work-arounds for what
10+
// appear to be Firebase JS SDK bugs https://github.com/firebase/firebase-js-sdk/issues/4158
911
export function fromTask(task: UploadTask) {
1012
return new Observable<UploadTaskSnapshot>(subscriber => {
1113
const progress = (snap: UploadTaskSnapshot) => subscriber.next(snap);
1214
const error = e => subscriber.error(e);
1315
const complete = () => subscriber.complete();
14-
task.on('state_changed', progress, (e) => {
16+
// emit the current snapshot, so they don't have to wait for state_changes
17+
// to fire next... this is stale if the task is no longer running :(
18+
progress(task.snapshot);
19+
const unsub = task.on('state_changed', progress);
20+
// it turns out that neither task snapshot nor 'state_changed' fire the last
21+
// snapshot before completion, the one with status 'success" and 100% progress
22+
// so let's use the promise form of the task for that
23+
task.then(snapshot => {
24+
progress(snapshot);
25+
complete();
26+
}, e => {
27+
// TODO investigate, again this is stale, we never fire a canceled or error it seems
1528
progress(task.snapshot);
1629
error(e);
17-
}, () => {
18-
progress(task.snapshot);
19-
complete();
2030
});
31+
// on's type if Function, rather than () => void, need to wrap
32+
return function unsubscribe() {
33+
unsub();
34+
};
2135
}).pipe(
22-
shareReplay({ bufferSize: 1, refCount: false })
36+
// deal with sync emissions from first emitting `task.snapshot`, this makes sure
37+
// that if the task is already finished we don't emit the old running state
38+
debounceTime(0)
2339
);
2440
}

src/storage/storage.spec.ts

+70-1
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ import { forkJoin } from 'rxjs';
22
import { mergeMap, tap } from 'rxjs/operators';
33
import { TestBed } from '@angular/core/testing';
44
import { AngularFireModule, FIREBASE_APP_NAME, FIREBASE_OPTIONS, FirebaseApp } from '@angular/fire';
5-
import { AngularFireStorage, AngularFireStorageModule, AngularFireUploadTask, BUCKET } from '@angular/fire/storage';
5+
import { AngularFireStorage, AngularFireStorageModule, AngularFireUploadTask, BUCKET, fromTask } from '@angular/fire/storage';
66
import { COMMON_CONFIG } from '../test-config';
77
import { rando } from '../firestore/utils.spec';
88
import { ChangeDetectorRef } from '@angular/core';
99
import 'firebase/storage';
10+
import firebase from 'firebase/app';
1011

1112
if (typeof XMLHttpRequest === 'undefined') {
1213
globalThis.XMLHttpRequest = require('xhr2');
@@ -64,13 +65,19 @@ describe('AngularFireStorage', () => {
6465
const blob = blobOrBuffer(JSON.stringify(data), { type: 'application/json' });
6566
const ref = afStorage.ref(rando());
6667
const task = ref.put(blob);
68+
let emissionCount = 0;
69+
let lastSnap: firebase.storage.UploadTaskSnapshot;
6770
task.snapshotChanges()
6871
.subscribe(
6972
snap => {
73+
lastSnap = snap;
74+
emissionCount++;
7075
expect(snap).toBeDefined();
7176
},
7277
done.fail,
7378
() => {
79+
expect(lastSnap.state).toBe('success');
80+
expect(emissionCount).toBeGreaterThan(0);
7481
ref.delete().subscribe(done, done.fail);
7582
});
7683
});
@@ -104,6 +111,68 @@ describe('AngularFireStorage', () => {
104111
}).catch(done.fail);
105112
});
106113

114+
it('should cancel the task', (done) => {
115+
const data = { angular: 'promise' };
116+
const blob = blobOrBuffer(JSON.stringify(data), { type: 'application/json' });
117+
const ref = afStorage.ref(rando());
118+
const task: AngularFireUploadTask = ref.put(blob);
119+
let emissionCount = 0;
120+
let lastSnap: firebase.storage.UploadTaskSnapshot;
121+
task.snapshotChanges().subscribe(snap => {
122+
emissionCount++;
123+
lastSnap = snap;
124+
if (emissionCount === 1) {
125+
task.cancel();
126+
}
127+
}, () => {
128+
// TODO investigate, this doesn't appear to work...
129+
// https://github.com/firebase/firebase-js-sdk/issues/4158
130+
// expect(lastSnap.state).toEqual('canceled');
131+
done();
132+
}, done.fail);
133+
});
134+
135+
it('should be able to pause/resume the task', (done) => {
136+
const data = { angular: 'promise' };
137+
const blob = blobOrBuffer(JSON.stringify(data), { type: 'application/json' });
138+
const ref = afStorage.ref(rando());
139+
const task: AngularFireUploadTask = ref.put(blob);
140+
let paused = false;
141+
task.pause();
142+
task.snapshotChanges().subscribe(snap => {
143+
if (snap.state === 'paused') {
144+
paused = true;
145+
task.resume();
146+
}
147+
}, done.fail, () => {
148+
expect(paused).toBeTruthy();
149+
done();
150+
});
151+
});
152+
153+
it('should work with an already finished task', (done) => {
154+
const data = { angular: 'promise' };
155+
const blob = blobOrBuffer(JSON.stringify(data), { type: 'application/json' });
156+
const ref = afStorage.storage.ref(rando());
157+
const task = ref.put(blob);
158+
let emissionCount = 0;
159+
let lastSnap: firebase.storage.UploadTaskSnapshot;
160+
task.then(_snap => {
161+
fromTask(task).subscribe(
162+
snap => {
163+
lastSnap = snap;
164+
emissionCount++;
165+
expect(snap).toBeDefined();
166+
},
167+
done.fail,
168+
() => {
169+
expect(lastSnap.state).toBe('success');
170+
expect(emissionCount).toBe(1);
171+
ref.delete().then(done, done.fail);
172+
});
173+
});
174+
});
175+
107176
});
108177

109178
describe('reference', () => {

0 commit comments

Comments
 (0)