Re: [whatwg/dom] Improving ergonomics of events with Observable (#544)

I would like to see `Observable` be the de facto type for events but I'd also like to see a more clear and consistent vision for how it will work with Promises and async iterators.

For the former consider the proposed `takeUntil`, it seems highly surprising to me that `takeUntil` is specified to take `Observable` not `Observable | Promise` (not sure if WebIDL would even accept that). Plenty of existing APIs (both provided by the browser and custom written) use Promises widely and these often make perfect sense at end conditions.

And it's pretty much just the same thing with interoperability with async iterables, is there going to be large API fragmentation where some utilities only work with async iterables and others only with observables, will we need to write two versions of every operator, one for observable and one for async iterables? Do both promises and async iterables all need to implement the proposed `[Symbol.observable]` in order to have a good path for interchanging the two.

Now I decided a while ago to experiment with writing a library that can treat event sources as async iterables, it has an API almost borrowed from `Observable` except written in terms of async iteration and returns an async iterator (that is also iterable) and in my experience I could write the same code as with the `Observable` form but it was also a lot easier to work with existing Promise based code because it's natural to write operators like `.map` that also respect async operations.

For example this is an example of a line drawing application using my library:

```js
import Stream from "@jx/stream"
import takeUntil from "./operators/takeUntil.mjs"

/* Stream uses a queue internally to store events, it's slightly worse
  than Observable in this regard, but it seems likely that Observable
  would need to implement similar in it's [Symbol.asyncIterator] method
  anyway
*/
function makeEventStream(element, eventName) {
    return new Stream(stream => {
        element.addEventListener(eventName, stream.yield)
        return _ =>  element.removeEventListener(eventName, stream.yield)
    })
}

/* event is a tiny utility to convert a single event into a Promise */
function event(element, eventName) {
    return new Promise(resolve => {
        element.addEventListener(eventName, resolve, { once: true })
    })
}

/* events is an async iterable of events of the given name from the source
  element, because it only creates the stream on iteration it's effectively
  equivalent to an unsubscribed observable because it can both be "subscribed"
  to multiple times (by looping or other operators)
  it can be cancelled via `.return` on the async iterator like other
  iterators so this is effectively "unsubscribe"
*/
function events(element, eventName) {
    return {
        [Symbol.asyncIterator]() {
            return makeEventStream(element, eventName)
        }
    }
}

async function main() {
    // Everytime the mouse is clicked down on the canvas
    for await (const mouseDown of events(theCanvas, 'mousedown')) {
        let previousEvent = mouseDown
        // Repeatedly render line segments between the current and
        // previous event, (all mouse moves except the first mousedown)
        const mouseMoves = takeUntil(
            events(theCanvas, 'mousemove'),
            event(theCanvas, 'mouseup'),
        )
        for await (const mouseMove of mouseMoves) {
            drawLineSegmentBetween(theCanvas, previousEvent, mouseMove)
            previousEvent = mouseMove
        }
    }
}

// You could even write it using operators as are familiar to
// most Observable users
function main() {
    // No side effects or event listeners are added until the for-await-of
    // loop just like Observables
    const mouseDowns = events(theCanvas, 'mousedown')
    const lineStreams = map(mouseDowns, mouseDown => {
        const mouseMoves = events(theCanvas, 'mousemove')
        return startWith(
            mouseDown,
            takeUntil(mouseMoves, event(theCanvas, 'mouseup')
        )    
    })
    // Pair each element with it's previous inside a single stream
    // so that each event is paired with the event we last saw
    const eventPairs = flatMap(lineStreams, lineStream => {
        return pairWise(lineStream)
    })

    // This is effectively Observable.subscribe where side effects happen
    for await (const [previousEvent, currentEvent]) {
        drawLineSegmentBetween(theCanvas, previousEvent, currentEvent)
    }
}

main()
```

I just wanted to leave this an example that there's nothing particularly special about `Observable`'s ability to represent composable and lazy async sequences. I want to see how `Observable` will work with existing types instead of what I often see where everything needs to be hammered to work with `Observable` and vice versa.

-- 
You are receiving this because you are subscribed to this thread.
Reply to this email directly or view it on GitHub:
https://github.com/whatwg/dom/issues/544#issuecomment-351679388

Received on Thursday, 14 December 2017 11:04:23 UTC