ReactiveX/rxjs

lastValueFrom will throw EmptyError if the Observable is complete BEFORE lastValueFrom is called

air2 opened this issue · 7 comments

air2 commented

Describe the bug

If I call lastValueFrom after the complete is called, it will fail

Expected behavior

I expect lastValueFrom to give me the last value even if the Observable is complete before lastValueFrom from is called.

Reproduction code

it('test lastValueFrom', async () => {
    const ob = new BehaviorSubject<string>('')
    ob.next('value')
    ob.complete()
    const val = await lastValueFrom(ob)
    expect(val).toEqual('value') //this will fail
  })

  it('test lastValueFrom 2', async () => {
    const ob = new BehaviorSubject<string>('')
    ob.next('value')
    const val = lastValueFrom(ob)
    ob.complete()
    expect(await val).toEqual('value')
  })


### Reproduction URL

_No response_

### Version

7.5.7

### Environment

_No response_

### Additional context

_No response_

I don't think this is a bug. Completing a subject essentially closes it, so it will lose whatever value it has, and you won't be able to push new ones anymore:

const subject = new BehaviorSubject(1);
subject.next(2);
subject.complete();

subject.subscribe({
  next: v => console.log('value', v), 
  complete: () => console.log('complete')
});
// Only "complete" is logged

lastValueFrom can't get the value from an observable that just completes, so throwing EmptyError is correct.

air2 commented

Well I do not agree, the value now depends on the moment you initiate the lastValueFrom call, not on the actual last value of the Observable. But IF this is not considered a bug, it should definitely be documented.

https://rxjs.dev/api/index/function/lastValueFrom

If the observable stream completes before any values were emitted, the returned promise will reject with EmptyError or will resolve with the default value if a default was specified.

It is documented clearly as very first sentence.

air2 commented

Yes, but values ARE emitted, so the documentation is not clear about this situation. In other words, you have to initiate lastValueFrom BEFORE the observable completes to get its last value. This is not stated anywhere. I would like the lastValueFrom to return the value of the observable if it is completed and there is a value, if there is not value it should indeed return a promise that rejects with EmptyError

air2 commented

This issue should not be closed, it is not solved, the documentation is not clear IMHO. It only states that the promise rejects when there are NO values emitted. But that is not the case here.

No, values are not emitted. Observable's fundamental contract is it works lazely for the each subscription. If you have an observable, but if there aren't any subscription, there is no actual value emission occurs. So if there is a observable have value then completes without subscription it is essentially does nothing. Once it's completed, subscribing it won't able to give any values for those reason.

We do not describe observable itself's behavior in each operator document. I believe doc for the operator suffeciently describes its behavior.

@air2 in case it helps, I think the name lastValueFrom is confusing you.... BehaviorSubject has a .getValue() function that you can call to get the last value emitted on that BehaviourSubject when you call it, maybe it's what you're looking for. (but I'm not sure it would work on a BehaviorSubject you called .complete().... In general you shoudn't call .complete() on a BehaviorSubject unless you want to remove it from memory)

lastValueFrom on the other hand is an operator for any Observable (not only subjects), that always returns a promise when you call it. The promise will resolve when the Observable is listening to completes, at which point it will resolve it with the last value that observable emitted.

When you call .complete() on a BehaviorSubject, it loses the value it had before, because you closed it. All it will do from now on is just emit "complete" when anybody subscribes again. So when lastValueFrom subscribes to it, all it "sees" is that the observable just completes without emitting any value, so it rejects the promise.

The contracts of lastValueFrom are very clear if you also understand the concept of Observables in general and how they behave:

Converts an observable to a promise by subscribing to the observable, waiting for it to complete, and resolving the returned promise with the last value from the observed stream.
...
If the observable stream completes before any values were emitted, the returned promise will reject with EmptyError or will resolve with the default value if a default was specified.

It's all it's doing, word by word. Subscribes to the observable, the observable completes, it didn't emit anything, it rejects the promise.