javascriptgeneratorfor-await

javascript break in for-await loop finish the generator


I have written this code to iterate over github issues with a specific number (like pagination), in this case with 3 issues at once:

const getUrl = (page) => `https://api.github.com/repos/angular/angular/issues?page=${page}`;

const getIssues = async function*() {
    for (let p = 1; true; p++) {
        const url = getUrl(p);
        const rawData = await fetch(url, {
            headers: { 'User-Agent': 'app' }
        });
        const issues = await rawData.json();
        for (let issue of issues) {
            yield issue;
        }
    }
};

const generator = getIssues();

document.querySelector('[data-next]').addEventListener('click', async function() {
    let i = 0;
    for await (let issue of generator) {
        console.log(issue);
        if (++i === 3) break;
    }
    console.log(await generator.next());
});

The element with data-next attribute is a button. The expected behavior is every click on the button loads the next 3 issues. The problem is, the generator finished after the break (the console.log prints this: {value: undefined, done: true}).

Why it is finished, and how could I make this work as expected?


Solution

  • It's a known problem/feature, that for..of terminates the generator (see e.g. here). One possible solution is to provide a proxy which will persist the actual generator state in a closure:

    function persist(gen) {
        return {
            next() {
                return gen.next()
            },
            [Symbol.asyncIterator]() {
                return this
            },
            [Symbol.iterator]() {
                return this
            }
        }
    }
    
    //
    
    const getUrl = (page) => `https://jsonplaceholder.typicode.com/posts/${page}/comments`;
    
    const getIssues = async function* () {
        for (let p = 1; true; p++) {
            const url = getUrl(p)
            const raw = await fetch(url)
            const data = await raw.json()
            yield* data
        }
    };
    
    async function main() {
    
        const generator = persist(getIssues());
        let i = 0;
    
        for await (let x of generator) {
            console.log(i, x.postId, x.id, x.name);
            if (++i === 4) break;
        }
    
        console.log('break'); i = 0;
    
        for await (let x of generator) {
            console.log(i, x.postId, x.id, x.name);
            if (++i === 4) break;
        }
    
        console.log('break'); i = 0;
    
        for await (let x of generator) {
            console.log(i, x.postId, x.id, x.name);
            if (++i === 4) break;
        }
    
    }
    
    main()