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

Tab Atkins Jr. jackalmage at gmail.com
Wed Apr 17 19:11:31 PDT 2013


On Wed, Apr 17, 2013 at 5:50 PM, Jason Orendorff
<jason.orendorff at gmail.com> wrote:
> On Tue, Apr 16, 2013 at 4:28 PM, Tab Atkins Jr. <jackalmage at gmail.com> wrote:
>> interface EventStreamResolver {
>
> I get the parallel with FutureResolver but this name doesn't work for
> me. "FutureResolver" makes sense because the whole point of a Future
> is to be resolved eventually. EventStreams aren't like that: many
> (most?) want to keep pushing events indefinitely, and only ever become
> "resolved" by freak accident.

Suggestions welcome.

>>   void continueWith(optional any value);
>
> I'm having trouble following the parallel with
> FutureResolver.resolve() here. What's this for?

It's the direct equivalent of FutureResolver#resolve.

> This seems like a mixing of layers to me. Here's how I interpret this
> whole design:
>
> - Event producers implement an extremely simple interface consisting
> of a single subscribe() function (the StreamInit callback) that
> interacts with a small listener interface (EventStreamResolver).

I don't want to have to write "new EventStream({subscribe:
function(r){...}})", if that's what you're thinking.

EventStreamResolver has nothing to do with listening.  It's an ocap
(object capability) that represents the ability to update the stream.

This is equivalent to Bacon.js's Bacon.fromCallback() method of
creation, except that Bacon basically only provides "accept"
functionality.  (You reject by throwing in the callback, I think.)
The resolver just abstracts one level - rather than passing the accept
function directly, it passes an object with the accept function on it,
plus a few others for convenience.  (Streams *need* at least two
functions - one for updating and one for completing.)

> - EventStream is a concrete class that builds a dazzling array of
> useful high-level operations on top of the aforementioned low-level
> protocol. EventStream is what event consumers use in practice.
>
> So—again this is all just how I see it right now—there are two nicely
> independent things going on: a minimal low-level protocol, and a
> high-level convenience class. "Obviously" you wouldn't have
> EventStreams as part of the low-level protocol. Hence my confusion
> about continueWith.
>
> Separately: it seems like if you're an event producer, and you want to
> stop sending events to a resolver and have some other event producer
> send it events instead, you shouldn't have to tell the resolver
> "please subscribe yourself to that stream over there". You can just
> subscribe it. Instead of:
>     resolver.continueWith(otherStream);
> just write:
>     otherStreamInit(resolver);
>
> Then again, you could say exactly the same things about
> FutureResolver.resolve(), and I don't understand its purpose either.
> Probably just me.

FutureResolver#resolve is syntax sugar for saying "just accept if this
other future accepts, or reject if it rejects".

In other words, rather than "r.resolve(someOtherFuture)", you could
always just write "someOtherFuture.then(r.accept.bind(r),
r.reject.bind(r))".  "resolve()" is just easier to read and write, and
has the additional useful semantics that you can't accidentally update
the future after delegating, which might offer some optimization
potential for implementations.

Exact same thing for EventStreamResolver#continueWith, except it's
three callbacks rather than two.

>> 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.
>
> Mmmm. The interesting thing about Futures is... well, I'll just link to
>   http://domenic.me/2012/10/14/youre-missing-the-point-of-promises/
>
> Not that I really think you're missing the point— but .listen() is
> only a sink. The combinators in your blog post are the exciting new
> ability on offer here. EventStreams are cool because they compose.

Note that I've revised this in the newest version, on my blog.
.listen() returns a brand new stream, slaved to the original.  I need
to define that throwing an error in any callback causes the stream to
unslave and reject.  This maintains the "errors are passed along until
someone can deal with them" semantic Domenic brings up.

>> 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.
>
> Rhetorical nit: This explanation makes it seem more complicated than
> it is. The way to explain and justify the design is to show simple
> examples. Bacon's readme does this well. You only have to read the
> first 5 lines of code here to see what Bacon is about:
>
>   https://github.com/raimohanska/bacon.js/blob/master/README.md#intro

Actually, the equivalent code in Bacon doesn't appear until the
"Creating Streams" section.  When I'm writing real document, it might
make sense to split the API descriptions along those lines. ^_^

>> complete/reject/continueWith all kill the resolver, so that none of
>> the methods work afterwards (maybe they throw?).
>
> That seems sensible. OTOH FutureResolver seems to make all those
> methods no-ops instead.  (Step 1 of each method's implementation: "If
> the context object's resolved flag is set, terminate these steps.")
> I'm not sure what that's about. Maybe worth asking.

Hm, we should be consistent.  Unsure which is better; I'll ping Anne.

>> * 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()).
>
> I can imagine that being true, but concrete example use cases would
> help. It is easier to think of cases where buffering doesn't matter or
> where you really don't want buffering (e.g. because it chews up a lot
> of memory that you can never free).

In my blog post, I bring up that auto-buffering by default is still
easy enough to defeat if you want to - just call it like "(new
EventStream(cb)).listen()".  The empty listener will still trigger the
"being listened to" bit and make it flush its buffer.  Unsure if this
is too magical to rely on or not.

>> All streams need some way of unlistening.  Suggestions welcome as to
>> how best to do this.
>
> Bacon offers two equivalent ways of unsubscribing.
>
> 1. Bacon's equivalent of the StreamInit callback returns an
> unsubscribe function. Each subscriber therefore gets its very own
> unsubscribe callback.

Ah, that's an interesting idea.

I'm unsure how each subscriber gets its own unsubscribe callback.  The
init callback is only called once, and so returns only the single
value, right?

Or is the subscribe callback called every time someone starts
listening, so the stream can potentially act different to different
listeners?  That seems like it would be hard to make compatible with a
multi-listener approach.

> 2. Additionally, Bacon's equivalent of the EventStreamResolver.push()
> method can return a special value (Bacon.noMore) that means
> "unsubscribe me".

That just kicks out all the listeners to the stream?  Or does it end
the stream?  Or do you mean something else, given that you use the
pronoun "me", which implies it's the *listener* with somehow sends the
signal?  If the latter, you're confused about the role of a stream
resolver.  (I think, based on earlier reactions, you might be, because
you talk about "sending events to a resolver".  Resolvers generate
events, listeners receive them.)

~TJ


More information about the es-discuss mailing list