|
| 1 | +# `WritableStream` controller `AbortSignal` Explainer |
| 2 | + |
| 3 | + |
| 4 | +## Introduction |
| 5 | + |
| 6 | +The streams APIs provide ubiquitous, interoperable primitives for creating, composing, and consuming streams of data. |
| 7 | + |
| 8 | +This change permits an underlying sink to rapidly abort an ongoing write or close when requested by the writer. |
| 9 | + |
| 10 | +Previously, when `writer.abort()` was called, a long-running write would still have to continue to completion before |
| 11 | +the stream could be aborted. With this change, the write can be aborted immediately. |
| 12 | + |
| 13 | +An underlying sink which doesn't observe the `controller.signal` will continue to have the existing behavior. |
| 14 | + |
| 15 | +In addition to being exposed to streams authored in JavaScript, this facility will also be used by platform-provided |
| 16 | +streams such as [WebTransport](https://w3c.github.io/webtransport/). |
| 17 | + |
| 18 | + |
| 19 | +## API Proposed |
| 20 | + |
| 21 | +On [WritableStreamDefaultController](https://streams.spec.whatwg.org/#writablestreamdefaultcontroller) |
| 22 | +(the controller argument that is passed to underlying sinks): |
| 23 | + |
| 24 | +* [`abortReason`](https://streams.spec.whatwg.org/#writablestreamdefaultcontroller-abortreason): The argument passed |
| 25 | +to `writable.abort()` or `writer.abort()`. Undefined if no argument was passed or `abort()` hasn't been called. |
| 26 | +* [`signal`](https://streams.spec.whatwg.org/#writablestreamdefaultcontroller-signal): An AbortSignal. By using |
| 27 | +`signal.addEventListener('abort', …)` an underlying sink can abort the pending write or close operation when the |
| 28 | +stream is aborted. |
| 29 | + |
| 30 | +The `WritableStream` API does not change. Instead, the existing `abort()` operation will now signal abort. |
| 31 | + |
| 32 | + |
| 33 | +## Examples |
| 34 | + |
| 35 | +These are some examples of JavaScript which can be used for writable streams once this is implemented: |
| 36 | + |
| 37 | +In this example, the underlying sink write waits 1 second to simulate a long-running operation. However, if abort() is |
| 38 | +called it stops immediately. |
| 39 | + |
| 40 | + |
| 41 | +```javascript |
| 42 | +const ws = new WritableStream({ |
| 43 | + write(controller) { |
| 44 | + return new Promise((resolve, reject) => { |
| 45 | + setTimeout(resolve, 1000); |
| 46 | + controller.signal.addEventListener('abort', |
| 47 | + () => reject(controller.abortReason)); |
| 48 | + }); |
| 49 | + } |
| 50 | +}); |
| 51 | +const writer = ws.getWriter(); |
| 52 | + |
| 53 | +writer.write(99); |
| 54 | +await writer.abort(); |
| 55 | +``` |
| 56 | + |
| 57 | + |
| 58 | +This example shows integration with an existing API that uses `AbortSignal`. In this case, each `write()` triggers a |
| 59 | +POST to a remote endpoint using [the fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). The signal |
| 60 | +is used to abort the ongoing fetch when the stream is aborted. |
| 61 | + |
| 62 | + |
| 63 | +```javascript |
| 64 | +const endpoint = 'https://endpoint/api'; |
| 65 | +const ws = new WritableStream({ |
| 66 | + async write(controller, chunk) { |
| 67 | + const response = await fetch(endpoint, { signal: controller.signal, |
| 68 | + method: 'POST', |
| 69 | + body: chunk }); |
| 70 | + await response.text(); |
| 71 | + } |
| 72 | +}); |
| 73 | +const writer = ws.getWriter(); |
| 74 | + |
| 75 | +writer.write('some data'); |
| 76 | +await writer.abort(); |
| 77 | +``` |
| 78 | + |
| 79 | +This example shows a use case of this feature with WebTransport. |
| 80 | + |
| 81 | +```javascript |
| 82 | +const wt = new WebTransport(...); |
| 83 | +await wt.ready; |
| 84 | +const ws = await wt.createUnidirectionalStream(); |
| 85 | +// `ws` is a WritableStream. |
| 86 | + |
| 87 | +const reallyBigArrayBuffer = …; |
| 88 | +writer.write(reallyBigArrayBuffer); |
| 89 | +// Send RESET_STREAM to the server without waiting for `reallyBigArrayBuffer` to |
| 90 | +// be transmitted. |
| 91 | +await writer.abort(); |
| 92 | +``` |
| 93 | + |
| 94 | + |
| 95 | + |
| 96 | +## Goals |
| 97 | + |
| 98 | +* Allow writes to be aborted more quickly and efficiently. |
| 99 | +* WebTransport will be able to use WritableStreamDefaultController.signal to make |
| 100 | +[`SendStream`'s `write()`](https://w3c.github.io/webtransport/#sendstream-write) and |
| 101 | +[`close()`](https://w3c.github.io/webtransport/#sendstream-close) abortable. |
| 102 | + |
| 103 | + |
| 104 | +## Non-Goals |
| 105 | + |
| 106 | +* Exposing a method to abort individual operations without aborting the stream as a whole. The semantics of this |
| 107 | +would be unclear and confusing. |
| 108 | + |
| 109 | + |
| 110 | +## User Benefits |
| 111 | + |
| 112 | +* Allows the abort operation to complete more quickly, which avoids wasted resources for sites that take advantage of it. |
| 113 | + |
| 114 | + |
| 115 | +## Alternatives |
| 116 | + |
| 117 | +* It was initially proposed that an `AbortSignal` could be passed to each sink `write()` call. However, since the |
| 118 | +abort signal does not need to change between two `write()` calls, it was thought better to just add a `signal` property |
| 119 | +on `WritableStreamDefaultController`. |
0 commit comments