Observable gets the wrong value when using the from operator with a promise after immediately updating that value in another promise
Asked Answered
E

2

5

I have an observable that listens to route changes :) I also have a promise that clears my local storage and when it's done I immediately change the route but my switchMap/switchMapTo inside my route-change observable get the old value.. why does this happen?

I have tried many ways to solve this - I found two that work :) But I wish to understand why they worked. Could anyone please help? Is this something regarding hot/cold observables issues? Perhaps something with the event loop? I really don't know :)

    this.storageService.clear().then((_) => {
      // this was successful!! And it was checked - user is deleted :)
      // now change the route and trigger the observable below :)
    });
    
    this.router.events.pipe(
      filter((e: Event): e is NavigationEnd => e instanceof NavigationEnd)
      /*
        switchMap in here to get the user!
        Options 1 + 2 show that the user info exists!! 
        Although I know for sure that it was deleted.
        Option 3 + 4 work.
      */
    
      // 1) switchMapTo(from(this.storageService.getAsync<IUser>('user'))),
    
      // 2) switchMapTo(this.storageService.getAsync<IUser>('user')),
    
      // 3) switchMap(async (_) => {
      //      const y = await this.storageService.getAsync<IUser>('user');
      //      return y;
      //    }),
    
      // 4) switchMapTo(defer(() => this.storageService.getAsync<IUser>('user'))),
    );


    // Question edited for extra information - this is the getAsync function
    async getAsync<T>(key: string) {
        const result: GetResult = await Storage.get({ key });
        return JSON.parse(result.value) as T;
    }
Electret answered 13/10, 2021 at 18:14 Comment(0)
Z
4

Eager vs Lazy Execution

This happens because promises are eager and observable are lazy. That is, an observable doesn't do anything until subscribed to whereas a promise is going to attempt to resolve itself from the moment it is defined.

Here's how I would write it in order to resolve this issue in the most straightforward manner:

this.router.events.pipe(
  filter((e: Event): e is NavigationEnd => e instanceof NavigationEnd),
  switchMap(_ => this.storageService.getAsync<IUser>('user'))
);

Update:

Of course, not only asynchronous code (like promises and observables) can have eager/lazy evaluation semantics. Regular synchronous code can have the same issues. Since it might be easier to understand the issue in a synchronous context, I'll create a few examples below that explore this without using promises or observables.


Consider this Non-Observable Code:

In this first example we have a function addNumbers that does the work of adding two numbers for you. It is eager, so it will do the work immediately and then return a value once invoked. You can see that on line 5, c() will be equal to 8. 3 and 5 are added together on line 6 then printed on line 8

function addNumbers(a, b){
  const answer = a + b;
  return () => answer;
}

const c = addNumbers(3, 5);

console.log("This should be an 8:", c());

In this next example, we have a very similar function, but it is lazy. It remembers the numbers you gave it but it doesn't actually add the numbers until something invokes it. In this case, c needs to be invoked c() before the 3 and the 5 are actually used in any way. 5 and 3 are not added together until line 7.

function addNumbers(a, b){
  return () => a + b;
}

const c = addNumbers(3, 5);

console.log("This should be an 8:", c());

Lazy vs Eager Consequences

Consider these two examples. If you understand why they print different values, you understand the issues at hand :)

Example 1:

const a = { n: 3 };
const b = { n: 5 };

function addNumbers(v1, v2) {
  const answer = { n: v1.n + v2.n };
  return () => answer;
}

const c = addNumbers(a, b);

a.n = 7;

console.log('Eager Evaluation:', c());

Here, c.n is equal to 3 + 5 because c was evaluated before a.n was set to 7. This is the same as when you retrieved the user before they were deleted. It doesn't see that the user info has changed in the meantime because the value has already been calculated.

Example 2:

const a = { n: 3 };
const b = { n: 5 };

function addNumbers(v1, v2) {
  return () => ({ n: v1.n + v2.n });
}

const c = addNumbers(a, b);

a.n = 7;

console.log('Lazy Evaluation:', c());

Here, c.n is equal to 7 + 5 because c was evaluated after a.n was set to 7. This is the same as when you retrieved the user using defer. This time we check the value of a.n the moment that c() is evaluated, not when addNumbers(v1, v2) is evaluated.

See the difference?

Zondra answered 13/10, 2021 at 18:29 Comment(11)
It looks like option 2 that it did not work for him, what is the different?Implode
@Implode They would be no different if what you're switching to is an observable. Because it is a promise, switchMapTo evaluates the promise right away and switches to the same promise every time. switchMap doesn't create the promise until it receives an emission. So it's the same as switchMapTo with a defer inside ( which is just option 4)Zondra
Thanks for your detailed answerImplode
So the code that does not work would return the same value forever?? @MrkSefElectret
@Electret Of course! :)Zondra
@MrkSef You said: "This happens because promises are eager and observable are lazy." But from the rest of your answer it is obvious that this problem has nothing in particular to do with either promises or observables. I suggest you edit the first part.Moshe
@RonInbar How would you edit this? If this.storageService.getAsync<IUser>('user') returned an observable instead of a promise, this problem wouldn't arise and switchMapTo would be fine. The distinction is between eager and lazy evaluation semantics, but isn't it important to the issue at hand that promises have the former semantics while observables have the latter semantics?Zondra
@MrkSef getAsync is a simple function that retrieves a string from localStorage. As a matter of fact, I don't even understand why it's asynchronous. To return an observable, it would have to contain another function that accesses localStorage. That's the important difference, not whether it returns a promise or an observable.Moshe
@RonInbar In the context of this question, we can't see what getAsync is, but in one of the examples given, Itay writes await getAsync(...) which implies that the function returns a promise. Promises, just like observables, contain inner functions. Inner functions alone don't describe evaluation semantics. The eager evaluation addNumber above has an inner function that returns the answer too (despite being eager).Zondra
@MrkSef True, but the difference between the two options that work and the two that don't is not in what getAsync returns (it's a promise either way) but in when getAsync itself is called, and in the first two options (the ones that don't work) it's called immediately by the application code, so RxJS doesn't even get to decide.Moshe
Thanks a lot, @MrkSef and Ron Inbar :) I understand now! It is indeed not the Promise or that RxJS that created my problem - it's how I wrote it.Electret
J
2

Some explanation that helped me couple of times understand defer.

Functions composition are executed from inner to outer. First the inner arguments given to the outer function, then the outer function.

function f(x) {
  const result = x * 10;
  console.log("f: ", x, " -> ", result);
  
  return result;
}

function g(x) {
  const result = x + 1;
  console.log("g: ", x, " -> ", result);

  return result;
}

cosnole.log(f(g(2))) 
// g: 2 => 3
// f: 3 => 30
// 30

Promises are eager. They do not wait for someone to subscribe.

new Promise(() => console.log("1"));
console.log("2");
1 
2

function f(p) {
  console.log('f:');
}

f(new Promise(() => console.log("1));
// 1
// f:

So is the case for from(somePromise):

from(new Promise((resolve) => {
  console.log("promise 1");
  setTimeout(() => {
    console.log("promise 2");
    resolve(5);
  });
});

console.log("subscribe - before");

from.pipe(tap(x) => {
  console.log("tap: ", x);
}).subscribe();

console.log("subscribe - after");

// promise 1
// subscribe - before
// subscribe - after
// promise 2
// tap: 5

What from does behind the scene (simplified):

function from(promise) {
  return new Observable(subscriber => {
    promise.then(result => {
      subscriber.next(result);
      subscriber.complete();
    }, (err) => subscriber.error(err));
  });
}

from return an observable that emit when the promise emit, but the promise itself has already started working even before the from started and created an observable.

The defer operator is for those cases. When the desire is to delay the job and only execute it when someone subscribed.

What defer does behind the scene (simplified version):

function defer(workFunction) {
  return new Observable(subscriber => {
    from(workFunction()).subscribe((result) => {
      subscriber.next(result);
    });
  });
}

Only when someone subscribe, the work function is activated.

If we give defer to switchMapTo, the switchMapTo will subscribe to it only when it's source emit.

What switchMapTo does behind the scene simplified:

function switchMapTo(workObservable) {
  let lastSubs;

  return new Observable(subscriber => {
    if (lastSubs) {
      lastSubs.unsubscribe();
    }

    lastSubs = workObservable.pipe(
      tap(result => subscriber.next(result)),
    ).subscribe();
  });
}
Joust answered 15/10, 2021 at 21:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.