1

Imagine we have an async generator function:

async f * (connection) {
    while (true) {
        ...
        await doStuff()
        yield value
    }
}

Suppose that this function is virtually endless and gives us results of some async actions. We want to iterate these results:

for await (const result of f(connection)) {
    ...
}

Now imagine we want to break out of this for-await loop when some timeout ends and clean things up:

async outerFunc() {
    setTimeout(() => connection.destroy(), TIMEOUT_MS)

    for await (const result of f(connection)) {
        ...
        if (something) {
            return 'end naturally'
        }
    }
}

Assume that connection.destroy() ends the execution of f and ends the for-await loop. Now it would be great to return some value from the outerFunc when we end by timeout. The first thought is wrapping in a Promise:

async outerFunc() {
    return await new Promise((resolve, reject) => {
        setTimeout(() => {
            connection.destroy()
            resolve('end by timeout')
        }, TIMEOUT_MS)

        for await (const result of f(connection)) { // nope
            ...
            if (something) {
                resolve('end naturally')
            }
        }
    })
}

But we cannot use awaits inside Promise and we cannot make the function async due to this antipattern

The question is: how do we return by timeout the right way?

Tony I.
  • 498
  • 4
  • 16
  • Source is my [discord bot](https://github.com/ReFruity/EzBot/blob/32-test-summon-to-the-wrong-channel-functionality/src/features/summon-to-the-channel.ts) – Tony I. Jun 04 '22 at 07:11
  • 2
    Timeouts are often implemented with `return Promise.race([p1, p2])` where you have a race between two promises, one triggered by a timeout and one triggered by your main operation. Whichever finishes first wins the race and becomes the resolved value that `Promise.race()` resolves/rejects to. – jfriend00 Jun 04 '22 at 07:18
  • "*Assume that `connection.destroy()` ends the execution of `f`*" - please be more specific about how that works. But if this is your approach, then `f(connection)` is not "virtually endless", it simply ends when the connection is destroyed, which ends the loop and runs the code after the loop. So just put a `return 'end by timeout';` after the `for await (…) { … }`! – Bergi Oct 16 '22 at 18:19

2 Answers2

0

It gets much easier, if you use an existing library that can handle asynchronous generators and timeouts automatically. The example below is using library iter-ops for that:

import {pipe, timeout} from 'iter-ops';

// test async endless generator:
async function* gen() {
    let count = 0;
    while (true) {
        yield count++; // endless increment generator
    }
}

const i = pipe(
    gen(), // your generator
    timeout(5, () => {
        // 5ms has timed out, do disconnect or whatever
    })
); //=> AsyncIterable<number>

// test:
(async function () {
    for await(const a of i) {
        console.log(a); // display result
    }
})();
vitaly-t
  • 24,279
  • 15
  • 116
  • 138
0

Assume that connection.destroy() ends the execution of f and ends the for-await loop.

In that case, just place your return statement so that it is executed when the loop ends:

async outerFunc() {
    setTimeout(() => {
        connection.destroy()
    }, TIMEOUT_MS)

    for await (const result of f(connection)) {
        ...
        if (something) {
            return 'end naturally'
        }
    }
    return 'end by timeout'
}
Bergi
  • 630,263
  • 148
  • 957
  • 1,375