Re: [streams] explicity operate on underlying source and sink instead of public methods (#321)

So, the current reference implementation is generic on both `this` (using `this.getReader()`) and on its argument (using `dest.write()`, or in the future probably `dest.getWriter()`). And it's also generic on the objects returned from those.

I think the genericness on `this` is not too important. It's main benefit is that it would allow duck-type readable streams that don't inherit from `ReadableStream` to do `MyReadableStream.prototype.pipeTo = ReadableStream.prototype.pipeTo` and get a pipe algorithm automatically. (It would also allow the edge case of subclasses which use "return override" but that is pretty crazy... can explain more if interested.) So let's put that aside for now and say that we abandon it, just like when formalizing .tee() I abandoned it.

The genericness on `dest` I was quite attached to because I thought it would be a good way to allow polymorphic behavior in a wide ecosystem of writable streams, which might not be per se writable streams. The main problem with this approach, as discussed in #276, is that it becomes harder to ensure we go down a "fast path" (i.e., off-main-thread piping) for UA-created writable streams, since e.g. someone could overwrite `ws.getWriter` or `WritableStream.prototype.getWriter` or `WritableStreamWriter.prototype.write`. You can have workarounds, e.g. by installing guards on the prototype properties and checking the value of `ws.getWriter`. But that is kind of crazy, especially the guards.

One argument against genericness on `dest` is that the polymorphism in writable stream implementations should not be accomplished by creating new writable stream classes, but instead passing new underlying sinks to `WritableStream`. Notably, instances created this way are guaranteed to obey all invariants, since we program those invariants into the `WritableStream` method and algorithm definitions. I was still resistant, however, as it felt un-JavaScriptey... JS programs would just be generic.

I then had an epiphany, which @wanderview alludes to above. All along I have been trying to make the analogy that promise is to promise executor function as readable/writable stream is to underlying source/sink. What I was missing was to further extend this analogy to the way in which promises handle genericness. Which is, they always "cast" incoming duck-promises (called thenables) into real promises, using PromiseResolve (exposed as `Promise.resolve`). For example, `Promise.all` takes an iterable, and casts each element before using it. `onFulfilled` allows you to return any object, but casts it before using it. And any web platform API that accepts promises, or any future ECMAScript API, will also cast the incoming object to a real promise before using it.

If we want genericness over writable streams, we should do the same thing. As a bonus we would get a guarantee that the writable stream obeys the correct invariants (currently there is the possibility of bizarre failures that we would in theory want to detect and add errors for, like `ws.state === "arghblargh"`). This is just like promise-accepting APIs which `Promise.resolve` incoming arguments do, to avoid e.g. jQuery promises' broken invariants.

And with this in mind, there's no reason to define `WritableStream.cast` or similar until someone actually asks for it. We could even let it be implemented as an npm package or whatever first and only if it gets lots of usage roll it in.

So, I have kind of changed my opinion on all this a lot.

---
Reply to this email directly or view it on GitHub:
https://github.com/whatwg/streams/issues/321#issuecomment-90980439

Received on Wednesday, 8 April 2015 17:24:26 UTC