Angular: 5.1.1
Firebase: 5.0.0-rc.4
AngularFire: 5.0.0-rc.5
This Stackblitz demonstrates the problem.
https://stackblitz.com/edit/angular-keww6x
Steps to set up and reproduce
Create an Observable by calling valueChanges() or snapshotChanges() on a ref in the Firebase Database (eg. const data$ = db.object('data').valueChanges()).
Then asynchronously subscribe twice to that single Observable.
Sample data and security rules
The same database as used in the Stackblitz can be used. Or any other database, as long as there is data in it.
When subscribing to the Observable, I expect to always get the current value first and all the changes after that.
The first subscription gets the current value and any changes.
The second subscription does not get the first value, only the subsequent changes.
If all the subscriptions are unsubscribed, however, the next subscription gets the initial value again.
The current workaround in my code is to surround all the valueChanges() and snapshotChanges() calls with an Observable.defer().
The share() operator in fromRef.
This operator shares the subscription with all of its subscribers, but will not emit the initial value a second time.
We just migrated from the deprecated database api to the new one and bumped into this. For us its a rather big change to update all the places to use the defer workaround so would really apreaciate if this could be fixed soon.
This is a tricky. It's a hot vs. cold observable situation.
All AngularFire observables are hot, meaning they values are emitted live. A second subscribe will not emit the "original" value. This is the intended design.
Why are AngularFire observables hot? A Firebase/Firestore reference is created outside the observable, which by default makes it hot. I'll be citing @BenLesh's excellent article to explain.
Below is an example of a cold observable:
// Pseduo-code
const cold = new Observable((observer) => {
const ref = new Reference();
const unsubscribe = ref.onSnapshot(observer);
return { unsubscribe };
});
This would create a new reference for each subscribe. Which is not ideal. Instead we create the reference outside of the observable, which makes the observable hot.
// Pseduo-code
const ref = new Reference();
const hot = new Observable((observer) => {
const unsubscribe = ref.onSnapshot(observer);
return { unsubscribe };
});
The kink in this situation is that methods like .on('child_added') or onSnapshot() work in a cold like manner. Meaning each time you call them they will replay the same results in the callback.
Ideally, you wouldn't run into this situation because subscribe() should be called just once. However, I am willing to make a change to have these observables act in a cold like manner to match the Firebase SDK.
I'm working on a few infrastructure changes with our typings, but after that I will get this change released.
Thanks for the explanation and fully agree that ideally this would not be an issue. Also we would be happy with either option (cold or hot observables) since the application code can be changed to work with both, but as you said its good that it would match the Firebase SDK since not matching that and that it worked differently with the old api made (would make) this seem like a bug.
Same issue here. Following for feedback.
Hey @MattStrybis! If you don't subscribe multiple times to the same observable you won't have this issue. I recommend only calling subscribe once for an observable and using operators to do any transform logic necessary. We'll get this fixed soon but you can avoid this problem in the first place.
I'm having a similar issue where on subscribe I get an old change instead of the most recent one. Maybe using publishReplay(1)(...) instead of share() would solve this issue.
I made a simple fn that works as I expect it:
import {FirebaseFirestore, QueryDocumentSnapshot} from '@firebase/firestore-types';
import {Observable} from 'rxjs/Observable';
import {publishReplay} from 'rxjs/operators/publishReplay';
import {Observer} from 'rxjs/Observer';
export function snapshotChanges(firestore: FirebaseFirestore, path: string) {
const collection = firestore.collection(path);
const source = new Observable((observer: Observer<QueryDocumentSnapshot[]>) => {
const destroy = collection.onSnapshot(snapshot => {
const {docs} = snapshot;
observer.next(docs);
});
return () => {
destroy();
};
});
const shared = publishReplay<QueryDocumentSnapshot[]>(1)(source)
.refCount();
return shared;
}
This ensures that any new subscriptions on the returned observable will reuse one observable and also replay the most recent snapshot change.
The drawback is that this is running outside the Angular zone, so you have to make sure you emit your results in a zone if that's important.
So glad to have finally found an explanation of what is going on. Any updates on your progress with changing to a cold observable emission @davideast .
For now though what is the recommended workaround as I am very keen to move over to as much async await with take(1) that makes sense.
Any chance you could elaborate on
The current workaround in my code is to surround all the valueChanges() and snapshotChanges() calls with an Observable.defer()
@njirem
Let me explain my use case. I have a relatively expensive "status" computation, which is dependent on the current state of the world (database). It does some valueChanges() type things on some collections and nested collections and switchMaps etc., resulting in a status. There is also an aggregate status, combining some individual statuses. I need to show these statuses and aggregate statuses from various screens in my Angular app. I've created a service which caches the observables for the statuses. But when I move between screens, and a new screen tries to retrieve the cached value (subscribing, either explicitly or implicitly via async), it gets nothing. Unfortunately no combination of multicast, publish or anything else has managed to solve this problem. Any ideas?
The solution I have found is to rather use the normal Firebase Firestore package when doing data fetches when I can see this issue potentially showing up.
So in my cases I have replaced:
const dbCollectionArray = await this.angularFirestore.collection('... collection reference...').take(1).toPromise
with this:
const dbCollectionArray = (await firestore().collection('... collection reference...').get()).docs.map(doc => doc.data())
@davideast
This would create a new reference for each subscribe. Which is not ideal.
Why is that not ideal?
subscribe()should be called just once
"Should" in what sense?
Just wondering, thanks for your useful answers.
I'm going to close this issue, since it's currently working as intended and work arounds have been provided. If we want an API change or a rehaul on the Observables, I'd suggest opening Proposal Issue.
I've been discussing with more Angular folk + I think we can change the API in a way that makes everyone happy without deferring. I'll let the idea build in my mind a bit more before putting up a proposal.
Feel free to continue discussing here.
@jamesdaniels It would be fantastic to see a change that does make this issue less confusing. Requiring .subscribe() to be called once only in an angular service seems to contradict the prevailing angular teachings that encourage using the async pipe (eg: {myObservable$ | async}) in component views. (Unless I have misunderstood something).
I wish I could be more helpful, but I鈥檓 still a newbie trying to learn this stuff. Thanks for your hard work.
I've tried the suggested defer workaround. It doesn't work for me. The firebase request doesn't fire after a first subscribe. Perhaps, I'm not implementing the defer workaround correctly. This is my code:
getList(pth): Observable
}
Thanks
I found a workaround for my chat app. Basically, when the user first entered a chat session, it would load the appropriate amount of messages, but when they left and re-entered that room (at some point), it would only load the last message they received. It was a really confusing issue, but this seems to work. I'm not sure about the repercussions of it, but for now. I'm going to use it.
first.auditTrail().subscribe();
Where first is my angularFire reference.
And then subscribing to the reference like you were originally and doing whatever with your data.
Here's the auditTrail() documentation for further explanation.
Here are some code snippets to extrapolate:
const first = this.afs.collection(this.query.path, ref => {
return ref
.orderBy(this.query.field, this.query.reverse ? 'desc' : 'asc')
.limit(this.query.limit)
});
first.auditTrail().subscribe();
first.snapshotChanges().do(arr => {
// doing some stuff here with the data. Not the best practice, but it works.
}).take(1).subscribe();
I'm using the operator shareReplay on snapshotChanges() to be able to subscribe several times.
@sgarciac can you please share a sample of code i am struggling a lot on it.Thanks in advance
I am getting the same error. does that mean we can use only one service ????
Same issue here!
Most helpful comment
This is a tricky. It's a hot vs. cold observable situation.
All AngularFire observables are hot, meaning they values are emitted live. A second subscribe will not emit the "original" value. This is the intended design.
Why are AngularFire observables hot? A Firebase/Firestore reference is created outside the observable, which by default makes it hot. I'll be citing @BenLesh's excellent article to explain.
Below is an example of a cold observable:
This would create a new reference for each subscribe. Which is not ideal. Instead we create the reference outside of the observable, which makes the observable hot.
The kink in this situation is that methods like
.on('child_added')oronSnapshot()work in a cold like manner. Meaning each time you call them they will replay the same results in the callback.Ideally, you wouldn't run into this situation because
subscribe()should be called just once. However, I am willing to make a change to have these observables act in a cold like manner to match the Firebase SDK.I'm working on a few infrastructure changes with our typings, but after that I will get this change released.