Skip to content
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

Merged
merged 9 commits into from
Jun 24, 2021

Conversation

yutakahirano
Copy link
Member

@yutakahirano yutakahirano commented Jun 3, 2021

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

Yutaka Hirano added 3 commits June 3, 2021 21:25
Define the property to signal abort to UnderlyingSink even
when it has a pending write or close operation.
Copy link
Collaborator

@ricea ricea left a 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
Copy link
Collaborator

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.

Copy link
Member

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 Show resolved Hide resolved
index.bs Outdated Show resolved Hide resolved
index.bs Show resolved Hide resolved
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
Copy link
Member

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?

Copy link
Member Author

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.

Copy link
Member

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...

Copy link
Member Author

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);

Copy link
Collaborator

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).

Copy link
Collaborator

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(() => {})? 🤔

Copy link
Member

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!

Copy link
Member Author

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.

Copy link
Member

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.

Copy link
Member Author

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.

yutakahirano and others added 2 commits June 9, 2021 20:12
Co-authored-by: Domenic Denicola <d@domenic.me>
@yutakahirano
Copy link
Member Author

Tests are available at web-platform-tests/wpt#29310.

@yutakahirano
Copy link
Member Author

Thank you! @domenic, do you have contact of streams implementers for Gecko/WebKit?

@domenic
Copy link
Member

domenic commented Jun 10, 2021

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 :)

@yutakahirano
Copy link
Member Author

Thank you, I'll talk about this at the next WebTransport WG meeting.

@ricea
Copy link
Collaborator

ricea commented Jun 16, 2021

Do we need an operation to add an algorithm to signal for use by other standards?

@yutakahirano
Copy link
Member Author

I was planning to write something like: [=AbortSignal/add=] |algorithm| to |stream|.[[controller]].[[signal]].

Isn't that enough?

@ricea
Copy link
Collaborator

ricea commented Jun 17, 2021

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.

@yutakahirano
Copy link
Member Author

I realized that we need abortReason, as it may contain the code of RESET_STREAM. Hence I added WritableStreamDefaultController.abortReason in the last commit. @domenic, can you take a look again? I'll update the tests accordingly.

@yutakahirano
Copy link
Member Author

@jan-ivar Could you express an implementer interest (Mozilla)? This is needed for w3c/webtransport#259 and w3c/webtransport#260.

@domenic
Copy link
Member

domenic commented Jun 23, 2021

LGTM

@jan-ivar
Copy link
Contributor

Yes we have implementer interest in WebTransport, and therefore this issue.

@domenic
Copy link
Member

domenic commented Jun 23, 2021

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.

@yutakahirano
Copy link
Member Author

Fixed. Could you double-check?

domenic pushed a commit to web-platform-tests/wpt that referenced this pull request Jun 24, 2021
@domenic domenic merged commit d0bd912 into whatwg:main Jun 24, 2021
moz-v2v-gh pushed a commit to mozilla/gecko-dev that referenced this pull request Jun 27, 2021
…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
jamienicol pushed a commit to jamienicol/gecko that referenced this pull request Jul 16, 2021
…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

Make underlying sink writes abortable
5 participants