-
Notifications
You must be signed in to change notification settings - Fork 161
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
Define WritableStreamDefaultController.signal #1132
Conversation
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.
We're going to need some tests.
@@ -4252,6 +4268,10 @@ The following abstract operations operate on {{WritableStream}} instances at a h | |||
id="writable-stream-abort">WritableStreamAbort(|stream|, |reason|)</dfn> performs the following | |||
steps: | |||
|
|||
1. If |stream|.[=WritableStream/[[state]]=] is "`closed`" or "`errored`", return | |||
[=a promise resolved with=] undefined. | |||
1. [=Signal abort=] on |
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 believe that this won't throw an exception. @domenic can you confirm?
We need to be careful about re-entrancy here, but it looks safe to me.
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 guess technically it could throw if some spec added a throwing algorithm to the "abort algorithms" list, but that seems unlikely. I filed whatwg/dom#984 on making it impossible.
index.bs
Outdated
@@ -4421,7 +4441,8 @@ the {{WritableStream}}'s public API. | |||
1. [=Reject=] |stream|.[=WritableStream/[[inFlightCloseRequest]]=] with |error|. | |||
1. Set |stream|.[=WritableStream/[[inFlightCloseRequest]]=] to undefined. | |||
1. Assert: |stream|.[=WritableStream/[[state]]=] is "`writable`" or "`erroring`". | |||
1. If |stream|.[=WritableStream/[[pendingAbortRequest]]=] is not undefined, | |||
1. If |stream|.[=WritableStream/[[pendingAbortRequest]]=] is not undefined and |error| is not an |
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 kind of switching on error type is a bit unusual. What is the intention here? What kind of test would pass with this change that would fail without it?
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.
let reject;
close_promise = new Promise((res, rej) => reject = rej);
const ws = new WritableStream({
start(c) {
c.signal.addEventListener('abort', () => {
const abort_error = new DOMException('foobar', 'AbortError');
reject(abort_error);
});
}, close() {
return close_promise;
}
});
const writer = ws.getWriter();
writer.close();
const reason = Exception('hoge');
writer.abort(reason);
In this example, I think it's natural that writer.closed
will be rejected by reason
, but without this clause it'll be rejected with abort_error
.
Another solution would be to expose pendingAbortRequest.reason
in the controller so that the underlying sink can reject the result of close()
with 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.
In this example, I think it's natural that writer.closed will be rejected by reason, but without this clause it'll be rejected with abort_error.
Hmm, I'm not sure I agree. The close() underlying sink hook explicitly said it failed to close (i.e. it returned a promise rejected with an "AbortError" DOMException, indicating that it tried to close but closing got aborted). I think that failure should be propagated to the closed
promise...
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.
But in this case, the operation fails because of the abort call coming from the producer, so I think it should be rejected with the abort reason. With the other solution, that code would look like:
let reject;
close_promise = new Promise((res, rej) => reject = rej);
const ws = new WritableStream({
start(c) {
c.signal.addEventListener('abort', () => {
reject(c.abortReason);
});
}, close() {
return close_promise;
}
});
const writer = ws.getWriter();
writer.close();
const reason = Exception('hoge');
writer.abort(reason);
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 think I prefer reject(c.abortReason)
.
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.
Hmm. So you'd do resolve()
instead? Or something like return close_promise.catch(() => {})
? 🤔
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 think I need a non-artificial example. Why did you reject the closed promise in the first place? I would think that the signal would have no impact on the promise returned from close, but instead would be used while opening a file or writing to it.
E.g.
let fd;
const ws = new WritableStream({
start(c) {
fd = await fs.open(filePath, { signal: c.signal });
},
write(chunk, c) {
await fd.write(chunk, { signal: c.signal });
}
close() {
await fd.close(); // no signal; closing isn't abortable
}
});
If you changed the line to await fd.close({ signal: c.signal })
, i.e. you let someone abort in the middle of closing---then yes, I'd expect ws.closed
to reject with an "AbortError" DOMException, because we failed to close the stream due to the closing process being aborted!
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.
Let me quote @vasilvv's example for WebTransport.
Imagine I have a web app where I can do drawing online. Every 100ms or so, I send the entirety of the drawing on a dedicated WebTransport stream. Note that if a network connection becomes spotty for a short period of time, this will cause a lot of streams to be open at once, so I want to keep only the three most recent streams open, and reset all streams that are older than the last three.
Here we want to map the reset operation to abort()
. We are planning to resolve the promise returned by close()
when the the last chunk is ack-ed, and until then the close operation is always cancellable - by just sending a RESET_STREAM frame.
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.
OK, so in that case, it seems like you can avoid the problem by not rejecting the promise returned by close when the reset operation is done. Returning a rejected promise from close indicates the closing failed, which it doesn't sound like is accurate in that case.
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.
Reverted the edit. I'm not sure if we don't need to access abortReason
.
Co-authored-by: Domenic Denicola <d@domenic.me>
Tests are available at web-platform-tests/wpt#29310. |
Thank you! @domenic, do you have contact of streams implementers for Gecko/WebKit? |
I suspect you might have better contacts than me at this point, given your work on WebTransport with Gecko and WebKit? But usually I ping @youennf and @jorendorff :) |
Thank you, I'll talk about this at the next WebTransport WG meeting. |
Do we need an operation to add an algorithm to signal for use by other standards? |
I was planning to write something like: [=AbortSignal/add=] |algorithm| to |stream|.[[controller]].[[signal]]. Isn't that enough? |
It's certainly enough for now. We might want to change it in future so that other standards don't have to make references to WritableStreamDefaultController's internal slots. |
I realized that we need |
@jan-ivar Could you express an implementer interest (Mozilla)? This is needed for w3c/webtransport#259 and w3c/webtransport#260. |
LGTM |
Yes we have implementer interest in WebTransport, and therefore this issue. |
Great! The reference implementation is failing the tests at web-platform-tests/wpt#29310 indicating a problem either with the reference implementation or the tests. Let's get that straightened out, and then we have to do the usual merge-tests-update-submodule-merge spec dance. |
Fixed. Could you double-check? |
…oller.signal, a=testonly Automatic update from web-platform-tests Add tests for WritableStreamDefaultController's signal Follows whatwg/streams#1132. -- wpt-commits: 861f0cb7a2d84019544d3622aab60b93056fc203 wpt-pr: 29310
…oller.signal, a=testonly Automatic update from web-platform-tests Add tests for WritableStreamDefaultController's signal Follows whatwg/streams#1132. -- wpt-commits: 861f0cb7a2d84019544d3622aab60b93056fc203 wpt-pr: 29310
Define the property to signal abort to UnderlyingSink even
when it has a pending write or close operation.
Fixes #1015.
(See WHATWG Working Mode: Changes for more details.)
Preview | Diff