DOM EventStreams (take two on Streams): Request for feedback

Tab Atkins Jr. jackalmage at
Tue Apr 16 16:28:52 PDT 2013

My first attempt at feedback for my Stream proposal was unfortunately
bogged down with a lot of confusion over terminology and meaning.  I'd
like to start fresh and hopefully head off a lot of confusion
early-on, so here's take two.

Now that DOM has added Futures, I've started looking into converting
various event-based APIs into being future-based.  This has been a
great success, but there are some things that can't be turned into
futures (because they update multiple times, or don't have a notion of
"completing"), but have the same lack-of-need for the full DOM Event
baggage.  I think these cases will be fairly common, and further, that
a good solution to the problem for DOM will be pretty useful for
general programming as well.

This is explicitly *not* an attempt to solve the "binary"/"IO" stream
use-case, as exemplified by Node Streams
<>.  While structurally similar,
binary streams have a lot of unique features and pitfalls that make
event streams a poor fit for them: they need to batch up data by
default, they need to be able to apply backfill data, etc.  I think we
*also* need to develop such an API, but it'll be separate from this
one.  (I suspect it may look very similar, though, so it's good to
keep that in mind when naming.)

So, without further ado, here's the basic API for my proposal for EventStreams:

callback StreamInit = void (StreamResolver resolver);
callback AnyCallback = any (optional any value);
typedef (EventStream or Future or Iterable) StreamLike;
typedef (string or number or boolean or AnyCallback) updateFilter;

[Constructor(StreamInit init)]
interface EventStream {
  EventStream listen(optional AnyCallback? listenCB = null, optional
AnyCallback? completeCB = null, optional AnyCallback? rejectCB =
  Future complete(optional AnyCallback cb);
  Future catch(optional AnyCallback cb);
  Future next(optional updateFilter, optional anyCallback cb);

interface EventStreamResolver {
  void push(optional any value);
  void complete(optional any value);
  void continueWith(optional any value);
  void reject(optional any value);

This API is intentionally very similar to that of Futures, because
it's intended to solve similar problems, and I think the shape of the
Futures API is pretty good.

An EventStream represents a stream of events or values.  It's roughly
equivalent to the concept of "signals" or "event streams" from
functional reactive programming, or the concept of an "observable" or
"task" from several functional async programming models.

An EventStream pushes out 0 or more updates, then optionally completes
or rejects.  The .listen() function is the basic way to respond to an
event stream, allowing you to register callbacks for any of those
three events.  It returns the same event stream back, for chaining.

For convenience, event streams have several functions that let you
listen to just a single event, returning a Future.  You can listen for
the stream completing, rejecting, or for the next update (possibly
filtered).  **Important note**: consuming an event stream using
repeated .next() calls rather than a single .listen() call is lossy -
multiple updates can happen between the tick that .next() is called
and the tick that the future resolves, and the future will only
contain the value of the first one.

Like Futures, EventStreams separate the power to read/respond to an
event stream and the power to update an event stream into two separate
objects.  The former is returned by the EventStream constructor, while
the latter is passed into the constructor's callback argument.  The
resolver's methods control the event stream's state - .push() puts an
update on the stream, which'll be passed to all the listen callbacks,
.complete() and .reject() end the stream with the passed value/reason,
while .continueWith() delegates to another stream.
complete/reject/continueWith all kill the resolver, so that none of
the methods work afterwards (maybe they throw?).

Additional Work

In my blog post <>, I sketch out several
event stream combinators, which showcase the true usefulness of this
kind of abstraction, as you can manipulate and combine event streams
*far* more easily, readably, and possibly performantly than the same
tasks done with normal DOM Events or callbacks.

Some degree of buffering seems desirable, both for DOM use-cases and
general ones:

* Several DOM use-cases really want to be able to remember the
"current" value (from the most recent update) - this applies to all
the "watch a value changing" APIs, like I suggest at the end of my
blog post.  I think this can just be a subclass of EventStream,
perhaps named UpdateStream, which automatically calls new listenerCBs
with the current value before any updates, and which exposes a
.value() function which is identical to .next(), but checks the
current value first.

* It seems that a bunch of manual use-cases would benefit from
auto-buffering any updates until the first listener is attached (via
.listen() or .next()).  How do we accommodate the choice?  Should this
just be the default for manually-created event streams, with DOM
use-cases defaulting to not buffering?

* Right now, consuming streams piecemeal with .next() (rather than
.listen()) is lossy.  Should we have some way to force full buffering,
so that if you're consumign it piecemeal, it waits until the next
.next() call to inform you of updates?

The previous point is probably something we only want to expose for
"single-listener" streams, which we should allow the creation of
somehow.  A single-listener stream could default to full buffering.
Enforcing single-listening is easy - if someone calls .listen(), it's
sealed to future .listen() or .next() calls until you unlisten.  If
it's not sealed, anyone can call .next() for the very next value,
which means you can chain .next() calls safely.  Maybe calling .next()
multiple times should return futures for *successive* values?  That
sounds like it would match a lot of people's intuitions, and would
match up well with using event streams for things like parsing streams
- if you need the next two tokens from a token stream, just call
.next() twice and use Future.all() to wait for them both to complete.
Perhaps these single-listener streams could be called ValueStream,
since they'll be for getting individual values asyncly.

All streams need some way of unlistening.  Suggestions welcome as to
how best to do this.  Maybe calling .listen() should actually return a
new stream slaved to the original, and you can just call .unlisten()
on it to destroy the listeners?  That avoids the "just pass the
original callback" problem when you have anonymous callbacks.  That
still means that "x.listen(); x.listen(); x.unlisten();" would destroy
*both* sets of callbacks, but I dunno how best to solve this.

I think that's about it for now.  ^_^  Hopefully this time my
intentions and goals are clearer, so we can start from a common slate
rather than arguing about definitions and getting confused!


More information about the es-discuss mailing list