tc39 / proposal-async-iteration

Asynchronous iteration for JavaScript
https://tc39.github.io/proposal-async-iteration/
MIT License
858 stars 44 forks source link

Is there any pattern to convert an event to an async iterator/generator? #99

Closed otakustay closed 7 years ago

otakustay commented 7 years ago

This is not a question related to spec itself, but is there any practical pattern to listen on an event (XMLHttpRequest#onprogress for example) and convert it to a async generator (each trigger of event becomes to an yield)?

let fetchList = async function* () {
    let xhr = ajax('/list?ndjson');
    // This line must be wrong, but how to write it easily?
    xhr.onprogress = line => yield {type: 'ADD_ITEM', payload: JSON.parse(line)};
};

If it is not possible to play with generator, how can I manually create an iterator to consume events?

let fetchList = () => {
    let xhr = ajax('/list?ndjson');
    xhr.onprogress = line => {
        // How to listen to event?
    };

    return {
        [Symbol.asyncIterator]: {
            next() {
                return promise;
                // What should I put here?
            }
        }
    }
};
gskachkov commented 7 years ago

Possible example can help you https://jsfiddle.net/173hp99p/6/ It convert mouse events to generator

Currently this example works in Google Chrome Canary with flags --args --js-flags="--harmony-async-iteration"

Jamesernator commented 7 years ago

Personally I've found when I want streaming that I can consume when I actually want it that the best way tends to be to use an async queue like this (it's not coincidence the constructor looks is nearly identical to Observable):


function deferred() {
    const def = {}
    def.promise = new Promise((resolve, reject) => {
        def.resolve = resolve
        def.reject = reject
    })
    return def
}

class AsyncQueue {
    constructor(initializer) {
        // This should probably be a linked list but eh
        // implementation details
        this.queue = []
        this.waiting = []
        initializer({
            next: value => {
                if (this.waiting.length > 0) {
                    // If anyone is waiting we'll just send them the value
                    // immediately
                    const consumer = this.waiting.shift()
                    consumer.resolve({
                        done: false,
                        value
                    })
                } else {
                    return this.queue.push({
                        type: 'next',
                        value
                    })
                }
            },
            throw: error => {
                if (this.waiting.length > 0) {
                    const consumer = this.waiting.shift()
                    return consumer.reject(error)
                } else {
                    return this.queue.push({
                        value: error,
                        type: 'error'
                    })
                }
            },
            return: value => {
                if (this.waiting.length > 0) {
                    const consumer = this.waiting.shift()
                    return consumer.resolve({
                        done: true,
                        value
                    })
                } else {
                    return this.queue.push({
                        value,
                        type: 'return'
                    })
                }
            }
        })
    }

    next() {
        if (this.queue.length > 1) {
            // If there are items available then simply put them
            // into the queue
            const item = this.queue.shift()
            if (item.type === 'return') {
                return Promise.resolve({
                    done: true,
                    value: item.value
                })
            } else if (item.type === 'error') {
                return Promise.reject(item.value)
            } else {
                return Promise.resolve({
                    done: false,
                    value: item.value
                })
            }
        } else {
            // If there's nothing available then simply
            // give back a Promise immediately for when a value eventually
            // comes in
            const def = deferred()
            this.waiting.push(def)
            return def.promise
        }
    }

    [Symbol.asyncIterator]() {
        return this
    }
}

const ticks = new AsyncQueue(iter => {
    let i = 0
    setInterval(_ => {
        iter.next(i)
        i += 1
    }, 1000)
})

Usage:

const ticks = new AsyncQueue(iter => {
    let i = 0
    setInterval(_ => {
        iter.next(i)
        i += 1
    }, 1000)
})

async function main() {
    for await (const i of ticks) {
        console.log(i)
    }
}

main()

And here's a plunk demonstrating mouse events.

KeithHenry commented 7 years ago

I had a very similar idea, proof of concept here: https://github.com/KeithHenry/event-generator

Live demo at https://keithhenry.github.io/event-generator/, but it only works in Chrome Canary 60+ with --js-flags=--harmony-async-iteration enabled.

That gives me observable-style events:

// Create a generator that iterates each time the click event happens
const eventIterator = eg(testPanel, 'click');
for await (const e of eventIterator)
    console.log('Clicked', e);
domenic commented 7 years ago

Closing since it seems people have worked out how to do this, and in any case it's not a bug with the proposal that we should continue tracking :)