-
Notifications
You must be signed in to change notification settings - Fork 163
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
Update TransformStream API & misc. fixups #519
Conversation
40c3948
to
da6fba2
Compare
transformStream._error does not exist. This codepath really needs a test.
The conditional here is weird.
Align the TransformStream API to resemble ReadableStream more closely. Issue whatwg#518.
Align with the WritableStream API. This resolves issues whatwg#518, whatwg#498, whatwg#331, and whatwg#185.
Return a promise with the same semantics as WritableStream's underlyingSink.close(). This also resolves an old comment, as well as issue whatwg#518. Consequently, calling controller.close() after flush() is called now throws a TypeError.
If I understand correctly, this doesn't enforce any ordering guarantees between the start/transform/flush methods. I would like to have the guarantee that transform() is not called until start()'s Promise resolves (and not at all if it rejects), and flush() is not called until transform()'s Promise resolves. The reason is that if transform() is called before the Promise returned by start() resolves, then a call to enqueue() by transform() could end up happening before before a call to enqueue() by start(). Similarly with transform() and flush(). It's possible to enforce the ordering within the transformer, but that seems likely to cause a steep learning curve as every developer rediscovers the issue. I can file a separate issue for this if you like. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the patch
} | ||
const flushPromise = PromiseInvokeOrNoop(transformStream._transformer, 'flush', [transformStream._controller]); | ||
// Return a promise that is fulfilled with undefined on success. | ||
return flushPromise.then(() => TransformStreamCloseReadableInternal(transformStream), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if controller.error() is invoked inside flush() but flush() resolved? I think this path should perform TransformStreamCloseReadableInternal() when the transform stream is neither errored nor closed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right, that should be fixed, though I'm tempted to make controller.error()
throw after flush()
is called, like with controller.close()
.
Otherwise, what should happen if controller.error() is called with one error, and flush throws or rejects the promise with a different error? What should happen if controller.close() is called, but then the flushPromise is rejected? It seemed simpler to give flush only a single way of signaling success or failure. You already can't signal error()
or close()
twice after all.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand your point. But we could also say that close() becoming disallowed in flush() would make the API a little complicated. Regarding the priority concern, I think we should make the transform stream accept the signal which arrived to it earliest. That said, this kind of rule also complicates the API.
Another question I'm not yet fully sure about is whether the resolution of flushPromise which you've changed to be propagated to writer.close() and readable's final state should always match or not. If close() / error() are allowed inside flush(), we can e.g. close() readable side while making writer.close() rejected. My gut feeling is basically they should match each other, but it seems worth being discussed a bit.
@@ -16,6 +17,17 @@ function TransformStreamCloseReadable(transformStream) { | |||
throw new TypeError('Readable side is already closed'); | |||
} | |||
|
|||
if (transformStream._writableDone === true) { | |||
throw new TypeError('TransformStream is already closing'); | |||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did you try to limit the way to close the readable side to only by resolving flush() once flush() is invoked? What's the motivation for that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Having two redundant ways to perform the same action seemed dubious (especially if, as in your other comment, the result is potentially contradictory, instead of redundant). It's already an error to call close()
twice, this seemed roughly analogous to that.
|
Now all 3 transformer methods are promise-ified.
Disregard previous commit. sink.start and source.start are called synchronously so this should too. Much simpler.
The conditional is always true, so this always throws. Is there a point in altering the state of an object when throwing an exception from its constructor anyway?
This matches behavior for readable and writable streams.
Per the spec, ReadableStreamDefaultControllerGetDesiredSize is nothrow. Nothing can go wrong with just adding up numbers.
Before, it was trying to error the readable end, when it had already errored itself.
Sorry for delay. I'll review this soon. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm once the 2 comments are addressed. thanks so much.
if (transformStream._transformer.start === undefined) { | ||
return; | ||
} | ||
const p = PromiseInvokeOrNoop(transformStream._transformer, 'transform', [chunk, transformStream._controller]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/p/transformPromise/
writer.write('a'); | ||
writer.close().then(() => { | ||
t.ok(flushDone, 'flushPromise resolved'); | ||
t.end(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unnecessary as you have t.plan()
lgtm. thanks! |
I'd like to add your name to the acknowledgement section of the spec. Do you want to use I'm going to squash the commits and merge it. If you want to write the commit summary for the squashed commit, please let me know. I can also write it for you if you want. |
This function is just one more abstract operation now and doesn't need its own separate category.
Just wanted to say thank you @isonmad for all this work! It's really excellent to get this kind of contribution. I guess we can merge this now with "isonmad" in the acknowledgments, but do feel free to let us know what would be better! |
That works fine. If it has to all be one monolithic commit, I guess the commit message would be
|
Follow up for #519 As we error the writable stream in TransformStreamErrorInternal, we don't need to take care of the promise returned from the underlying sink write().
|
||
transformStream._chunkPending = true; | ||
transformStream._chunk = chunk; | ||
|
||
const promise = new Promise(resolve => { | ||
transformStream._resolveWrite = resolve; | ||
transformStream._rejectWrite = resolve; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should be set to the rejection callback of the promise
if (transformStream._resolveWriter !== undefined) { | ||
transformStream._resolveWriter(undefined); | ||
if (transformStream._rejectWriter !== undefined) { | ||
transformStream._rejectWriter(e); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/rejectWriter/rejectWrite/
anyway, I just noticed that this is unnecessary since we call _writableController.error() at L118. I'm going to just remove this in #533
Follow up for #519 As we error the writable stream in TransformStreamErrorInternal, we don't need to take care of the promise returned from the underlying sink write().
First pass at issue #518 to align with the other stream APIs. Ended up also addressing #498, #331, and #185.