2

Let's say I have this observable:

const obs = new Observable((observer) => {
    observer.next(0.25);
    observer.next(0.75);
    observer.next(new ArrayBuffer(100));
    observer.complete();
});

How can I wait for each value with a promise?

The following code will only return the last value (value before complete() is called):

const value = await obs.toPromise();

But I want to be able to get each value along the way. I can do something like this:

const value1 = await obs.pipe(take(1)).toPromise();
const value2 = await obs.pipe(take(2)).toPromise();

But that's not ideal, since I'd have to increment the number each time and also take(1000) would still return something in the example, even though there are only 3 values. I'm looking for something like:

const value1 = await obs.pipe(next()).toPromise(); // 0.25
const value2 = await obs.pipe(next()).toPromise(); // 0.75
const value3 = await obs.pipe(next()).toPromise(); // ArrayBuffer(100)
const value4 = await obs.pipe(next()).toPromise(); // null

That is more akin to a generator.

Is there a way to accomplish something like this?

Brandon
  • 38,310
  • 8
  • 82
  • 87
Maxime Dupré
  • 5,319
  • 7
  • 38
  • 72
  • 1
    Why would this need to be a promise? – Kevin B Mar 05 '19 at 21:43
  • @KevinB Why not? I would like to structure my code with async/await. – Maxime Dupré Mar 05 '19 at 21:43
  • Right, but, it doesn't appear as though your'e doing anything asynchronous, so... in theory it would work the same way with or without await. async/await would just be extra code with no purpose if what you're doing isn't asynchronous. – Kevin B Mar 05 '19 at 21:44
  • This is just a simplified example, please imagine the observable emits events in an asynchronous way. – Maxime Dupré Mar 05 '19 at 21:45
  • I could see a scenario where you build a function that fires off the next thing in a queue and gives you a promise in return that you can await, then call the next... but it sounds like you're just describing a generator. – Kevin B Mar 05 '19 at 21:46
  • Cool, then does rxjs have a generator-like syntax I could use? – Maxime Dupré Mar 05 '19 at 21:49
  • @maximedupre try [`bufferCount()`](https://observable-playground.github.io/gist/95cd0033ef9b8ba5d11b748eb8dfae37) operator (it will emit all values buffered in array, once source completes). Though as Kevin said you might not need observables here. – kos Mar 05 '19 at 21:55
  • `bufferCount` won't work, as `await obs.pipe(bufferCount(3)).toPromise()` will return only one observable, where as I want to wait for each value. Also using an observable is not a choice I have, a library I use returns one. – Maxime Dupré Mar 05 '19 at 22:03
  • @maximedupre, zero-arg call to `bufferCount` will give you buffer with all values on completed stream, so **one** await for all values. If you want to await for **each** value -- do check generators, no need for rx here. Probably a less abstract example of your code/requirements would help. – kos Mar 05 '19 at 22:30
  • That's right, I want to await for **each** value, as I need to do something between each value. As I said, the library I use sends me an observable, so I need to find a way to achieve this that is compatible with rx (if such a way exists). – Maxime Dupré Mar 05 '19 at 22:35
  • 2
    @maximedupre theres a SO question about [converting Observable into generator](https://stackoverflow.com/questions/44123146/convert-an-observable-to-an-async-generator), you could try provided there answers. Though I'm still not sure if the big idea of using async with Observables is valid. – kos Mar 06 '19 at 10:42

2 Answers2

0

that might just work, since take(1) completes the observable, then it is consumed by await, next one in line will produce the second emission to value2.

const observer= new Subject()
async function getStream(){
  const value1 = await observer.pipe(take(1)).toPromise() // 0.25
  const value2 = await observer.pipe(take(1)).toPromise() // 0.75
  return [value1,value2]
}
getStream().then(values=>{
  console.log(values)
})
//const obs = new Observable((observer) => {
 setTimeout(()=>{observer.next(0.25)},1000);
 setTimeout(()=>observer.next(0.75),2000);

UPDATE: Using subject to emit.

Fan Cheung
  • 10,745
  • 3
  • 17
  • 39
  • This will not work. All variables (value1..value4) will have first emitted value - 0.25. To make it work need to change every take to (take(1)...take(4)). – taras-d Mar 06 '19 at 08:24
  • So just await them in the while loop? Or just subscribe to an observable as they are meant to be used. Or don't use observables at all, just use promises in the first place. – Roberto Zvjerković Mar 06 '19 at 09:12
  • updated the answer, tested it already, it'll work. Although i think it miss the point of async programming – Fan Cheung Mar 06 '19 at 09:33
  • @taras-d is right, you need to increment each `take`, or you will always get the first value using `take(1)`. I tested the solution proposed here by using a `Subject`, but it is not really a good alternative, because it does not wait until a client subscribes before pushing values. One could subscribe to a Subject and not receive previously pushed values, because the process has been initiated before the client subscribes. ReplaySubject is also not a good alternative for the same reason, the process of pushing value should only start after the client subscribes to the observable, not before. – Maxime Dupré Mar 07 '19 at 15:50
  • then why not use generator instead – Fan Cheung Mar 07 '19 at 15:53
0

It seems like what you are asking for is a way to convert an observable into an async iterable so that you can asynchronously iterate over its values, either "by hand" or using the new for-await-of language feature.

Heres' an example of how to do that (I've not tested this code, so it might have some bugs):

// returns an asyncIterator that will iterate over the observable values
function asyncIterator(observable) {
  const queue = []; // holds observed values not yet delivered
  let complete = false;
  let error = undefined;
  const promiseCallbacks = [];

  function sendNotification() {
    // see if caller is waiting on a result
    if (promiseCallbacks.length) {
      // send them the next value if it exists
      if (queue.length) {
        const value = queue.shift();
        promiseCallbacks.shift()[0]({ value, done: false });
      }
      // tell them the iteration is complete
      else if (complete) {
        while (promiseCallbacks.length) {
          promiseCallbacks.shift()[0]({ done: true });
        }
      }
      // send them an error
      else if (error) {
        while (promiseCallbacks.length) {
          promiseCallbacks.shift()[1](error);
        }
      }
    }
  }

  observable.subscribe(
    value => {
      queue.push(value);
      sendNotification();
    },
    err => {
      error = err;
      sendNotification();
    },
    () => {
      complete = true;
      sendNotification();
    });

  // return the iterator
  return {
    next() {
      return new Promise((resolve, reject) => {
        promiseCallbacks.push([resolve, reject]);
        sendNotification();
      });
    }
  }
}

Use with for-wait-of language feature:

async someFunction() {
  const obs = ...;
  const asyncIterable = {
    [Symbol.asyncIterator]: () => asyncIterator(obs)
  };

  for await (const value of asyncIterable) {
    console.log(value);
  }
}

Use without for-wait-of language feature:

async someFunction() {
  const obs = ...;
  const it = asyncIterator(obs);

  while (true) {
    const { value, done } = await it.next();
    if (done) {
      break;
    }

    console.log(value);
  }
}
Brandon
  • 38,310
  • 8
  • 82
  • 87