Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I yield a sequence of promises from an async iterator?

Tags:

I start with a function which renders and yields a sequence of image blobs.

async function* renderAll(): AsyncIterableIterator<Blob> {
  const canvases = await getCanvases();
  for (const canvas of canvases) {
    yield await new Promise<Blob>((resolve, reject) => {
      canvas.toBlob(result => { if (result) resolve(result); else reject(); });
    });
  }
}

That works fine, but the performance isn't ideal because each promise has to be resolved before starting on the next operation. Instead, the caller should decide when to wait for the promises, while still preserving the order.

async function* renderAll(): AsyncIterableIterator<Promise<Blob>> {
  const canvases = await getCanvases();
  for (const canvas of canvases) {
    yield new Promise<Blob>((resolve, reject) => {
      canvas.toBlob(result => { if (result) resolve(result); else reject(); });
    });
  }
}

To my surprise, this fails to compile because "Type Blob is not assignable to type Promise<Blob>." Further inspection shows that the yield operator unpacks promises within an async function, such that yield promise is functionally identical to yield await promise.

Why does the yield operator act this way? Is it possible to yield a sequence of promises from an async iterator?

like image 639
piedar Avatar asked Nov 29 '17 18:11

piedar


People also ask

How do you handle promises in asynchronous function?

Inside an async function, you can use the await keyword before a call to a function that returns a promise. This makes the code wait at that point until the promise is settled, at which point the fulfilled value of the promise is treated as a return value, or the rejected value is thrown.

What allows you to sequentially access data from asynchronous data source?

ES6 introduced the iterator interface that allows you to access data sequentially. The iterator is well-suited for accessing the synchronous data sources like arrays, sets, and maps.

Does an async function automatically return a promise?

The word “async” before a function means one simple thing: a function always returns a promise. Other values are wrapped in a resolved promise automatically. So, async ensures that the function returns a promise, and wraps non-promises in it.


2 Answers

For some reason these two statements seem to have the same effect :

yield await promise
yield promise

The first statement actually compiles to yield yield __await(promise) in Javascript, which works as expected. I think the idea is that you should be able to return either an element of the iteration or a promise of an element, so the await is not really necessary

From my understanding of async iterator spec, it should be used in cases where the iteration itself is asynchronous, in your case the iteration itself is not asynchronous, it's more of an asynchronous method that returns an interation. I would go with:

async function renderAll(): Promise<Iterable<Promise<IBlob>>> {
    const canvases = await getCanvases();
    return (function* () {
        for (const canvas of canvases) {
            yield new Promise<IBlob>((resolve, reject) => {
                canvas.toBlob(result => { if (result) resolve(result); else reject(); });
            });
        }
    })();
}

OR

async function renderAll4(): Promise<Iterable<Promise<IBlob>>> {
    return (await getCanvases())
        .map(canvas => new Promise<IBlob>((resolve, reject) => {
                canvas.toBlob(result => { if (result) resolve(result); else reject(); });
            })
        );
}
like image 102
Titian Cernicova-Dragomir Avatar answered Sep 23 '22 12:09

Titian Cernicova-Dragomir


To work around the surprising behavior of the yield operator, one possibility is to wrap the promise in a function.

async function* renderAll(): AsyncIterableIterator<() => Promise<Blob>> {
  const canvases = await getCanvases();
  for (const canvas of canvases) {
    yield () => new Promise<Blob>((resolve, reject) => {
      canvas.toBlob(result => { if (result) resolve(result); else reject(); });
    });
  }
}

I'm not sure if that's a good practice, but it does allow the caller to schedule the async work.

like image 20
piedar Avatar answered Sep 20 '22 12:09

piedar