-
Notifications
You must be signed in to change notification settings - Fork 29.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Streams pain points #89
Comments
1 - bringing in the functionality of 2 - being able to easily create a generic stream that you can write to, that passes through would also be nice. 3 - a first class error method ( 4 - having methods bound to the context internally would be a bonus as well... The following psuedo-example illustrates what I mean here, and something that would be closer to what I'd like to see.
|
(EDIT: this was originally quoting the comment above, which was since edited)
I might be misinterpreting this quote, so forgive me if I'm just blathering here: Promises are great for representing single async operations that happen exactly once, pass or fail -- they are a chainable primitive for those sorts of operations. Node's primitive for that class of operation is the Node-style callback -- which is, notably, not inherently chainable. Streams (collectively including readables, writables, duplexes, and transforms) represent a series of zero or more data events over time, that may eventually end or fail. |
Some more pain points:
|
Those last bits are SO TRUE to my experience. |
@chrisdickinson I removed the part you quoted... I was merely expressing a thought in my head that it would be nice if there was a cleaner way to chain against a Another minor pain point is that Writable streams don't emit |
a lot of my grievances are already issues in node: https://github.com/joyent/node/issues/created_by/jonathanong a major issue for web framework maintainers are leaks: https://github.com/jshttp/on-finished#example. requiring us to use https://github.com/stream-utils/destroy and https://github.com/jshttp/on-finished is a symptom of a broken stream implementation, imo. but personally, i'd rather have |
This is definitely the main pain point for me. That's it. If there were a formal streams spec that everyone could run their own little implementations on to test for conformance that would fix a LOT of issues. @chrisdickinson I'm sure you've seen https://github.com/promises-aplus/promises-tests. I'm tempted to do something like that for streams. Think it would help us all get on the same page? A first pass could just spec out the existing implementation in node, trying to catch as many corner cases as possible. Then, anyone who wanted could make a competing streams implementation, similar to how when.js, rsvp, bluebird, etc. compete on speed and features. |
re: @mjackson:
The other side of this is: if we spec streams as they exist currently, the difficulty of changing/improving streams goes up. Are we comfortable with how streams currently work, and are we willing to support that for the long haul? I agree that the eventual goal of streams should be a spec, but on the other hand, I feel like committing to what we have now may be premature. EDIT: That said, I'd totally welcome a stream spec suite so we can see clearly what behaviors we're dealing with! re: @jonathanong:
The first part of that is under debate over here; I've made a big list of concerns with that approach as well. Re: the second half, the problem with using the WHATWG spec is that it's built around a core primitive that Node doesn't use -- namely, Promises. Going through your linked issues, it seems like the lion's share of problems have to do with resource or handle-backed streams and how they interact (or, rather, don't interact as expected) with the pipe mechanism -- I totally agree that that behavior needs to be shored up; maybe as something that builds on top of vanilla streams, rather than baked into the base Readable and Writable classes. |
Copying these over from nodejs/roadmap#1:
var r = fs.createReadStream('file.txt');
var z = zlib.createGzip();
var w = fs.createWriteStream('file.txt.gz');
r.pipe(z).pipe(w); Beautiful, except you can't just add a single error handler on the end... you need one for each stream. And you could actually get errors from all three of them, so you can't just forward them to a callback or anything sane like that... it just gets gnarly way too fast.
I don't think any of those are actionable so long as node compatibility is a requirement, but I guess that's a different topic. |
Coming from Rx background, I was unpleasantly surprised by how unintuitive piping is. Most examples of downloading a file (arguably the simplest use case for streams and piping) that I found it blogs and on StackOverflow would get error handling, or flushing, or finish/close/disposal sequence wrong. Seriously. (Oh noes.) IMO Node's Rx also has first-class disposables, as in you have to return a “disposable” from Observable constructor, and there are several built-in utility disposables for simpler composition. I can be wrong though! Pardon me if I'm missing something obvious about how streams should be implemented. |
I think the problem with error forwarding is that if you had a long stream of piping object, it would be hard to determine where the error actually resides and deal with it in an appropriate manor. As far as i know streams, (though similar in nature to a promise will unlimited resolves of data , before a final |
Also i get the point about the |
From an application developer point of view, these are my main concerns with streams2:
Aside from those two points, I haven't really found any issues with streams so far, from my point of view. I'm actually finding them to be rather practical to work with. |
Having a pipe event on a duplex/ transform/ readable & writable stream, would be confusing. How would you know if it was piping or being piped if you had the same event? Overriding the pipe method is not too difficult, if you absolutely needed that: var pipe = Readable.prototype.pipe
Readable.prototype.pipe = function (dest, options) {
this.desintations.push(dest)
dest.on('finish', function () {
var i = this.destinations.indexOf(dest)
~i && this.desintations.splice(i, 1)
}.bind(this)
return pipe.call(this, dest, options)
} With regards to question 2, if you needed to pipe a transform object stream into a buffer there is no reason you can't do this in your transform: var Transform = require('stream').Transform
var inherits = require('util').inherits
function ObjToBufferStream() {
Transform.call(this, { objectMode: true })
}
inherits(ObjToBufferStream, Transform)
ObjToBufferStream.prototype._transform = function (data, enc, next) {
try {
next(null, new Buffer(JSON.stringify(data)))
} catch (err) {
next(err)
}
}
var objToBufferStream = new ObjToBufferStream()
objToBufferStream.pipe(process.stdout)
objToBufferStream.end({ obj: true }) |
@chrisdickinson I am curious, after hearing the discussion in the TC about potentially moving streams development out of core and introducing it as a bundled dependency. What sort of API would you give streams, if you could start from scratch? Would you go for a promised backed approach such as @domenic's https://github.com/whatwg/streams approach? |
You can in fact set |
@aredridel this is true, albeit the opposite to my example. But this would only really matter for a duplex stream, in any case you are still going to need to implement the internals If you were implementing a transform stream, and it was just set to No matter what is implemented, the hardest part is the conversion between formats (e.g. buffers to objects), as data across the wire is not always deterministic and could very well run over the boundaries of a This is an issue no matter what encoding you have, unless you are using only JavaScript objects directly (and then you can't really manage memory effectively). My example is contrived but the only important thing is the part: next(null, JSON.stringify(data)) all the rest is purely contextual. Personally when I see properties like As far as I am concerned, technically implementation detail is free to change without breaking semver! |
While implementing a WebSocket parser using the Beside I would like to have streams working much more as expected. |
@bodokaiser This is an interesting idea... so you are describing a way of putting some data back onto the write buffer and applying back pressure. This is definitely food for though! |
@sonewman yes, indeed. I always wondered why this method was missing as streams should be |
It think this would be tricky to implement with what we have currently, for the purposes of this explanation I will call this method We currently create a new WriteReq when adding chunks to the This could lead to confusion (and inconsistent behaviour) if it was decided that The other problem is that when ( The above scenario would mean that I think we would need to decide if this functionality was definitely something that we really want to add to |
@aredridel I saw your comment about callbacks in readables - I have always been confused why the read method in readables does not use a callback whereas the write method on writable has a callback. just for kicks I started writing an alternative stream API, and it's based on having the read method have a callback like you mention - assuming that's what you were getting at. Any reason why read doesn't take a callback in the current Node streams API? It makes no sense to me. |
@ORESoftware noms might be what your looking for. |
@calvinmetcalf thanks will check it out; after much research, I think the standard API will work, it's just not very intuitive or consistent, you can do this: function getReadableStream(fn) {
return new stream.Readable({
objectMode: true,
read: function (n) {
fn(n, (err, chunk) => {
// this.push is only called after a callback has fired
this.push(chunk);
})
}
});
} but I wish they had designed the API like folllowing instead, function getReadableStream(fn) {
return new stream.Readable({
objectMode: true,
read: (n,cb) => { // <<<< read should have taken a callback., but it don't! mofos
fn(n, (err, chunk) => {
if(!err){ // whatever, you get the idea
cb(null, chunk);
});
});
}
});
} after many many weeks of subpar understanding, I think I get it, but you However, core could add a callback to readable.read(), so maybe one day |
This is a place to enumerate pain points / problems with streams as they exist presently. Please focus on problems, clarifying comments and questions, or statements of commiseration for specific problems. Proposed solutions should be posted as separate issues that link back to this issue. Proposed solutions should have enough example / backing code to have an informed discussion about the tradeoffs, but are not expected to be production ready / fully working.
If the pain point is "rip out streams from core" -- that can be discussed over here. :)
I'll start:
destroy
,close
, etc; and they have to ensure that their sequence of events matches all others over time.null
is a reserved value in streams -- streams cannot meaningfully pass this value along without bugs. This causes problems for folks migrating from earlier versions of streams, and in situations wherenull
is "in-alphabet."through
andthrough2
(which by itself is fine), but it would be nice if the APIs were directly usable "out of the box", so to speak.The text was updated successfully, but these errors were encountered: