- From: Jake Archibald <notifications@github.com>
- Date: Wed, 22 Feb 2017 09:00:47 -0800
- To: whatwg/fetch <fetch@noreply.github.com>
- Cc: Subscribed <subscribed@noreply.github.com>
- Message-ID: <whatwg/fetch/issues/447/281731850@github.com>
Ok, here goes. Here's a proposal based on @stuartpb's work and other discussion. It's formed of three parts:
* **A controller** - allows me to say what I want to happen.
* **A signal** - expresses what should happen.
* **An observer** - what's happening & what actually happened.
# Fetch controller
```webidl
[Constructor(), Exposed=(Window,Worker)]
interface FetchController {
readonly attribute FetchSignal signal;
void setPriority(octet priority);
void abort();
void follow(FetchSignal signal);
void unfollow(FetchSignal signal);
};
dictionary RequestInit {
// …
FetchSignal signal;
}
[Exposed=(Window,Worker)]
interface FetchSignal {
Promise<octet> getPriority();
Promise<boolean> shouldAbort();
attribute EventHandler onaborted;
attribute EventHandler onprioritychange;
}
```
The fetch controller allows you to signal your intent to fetches.
```js
const controller = new FetchController();
const signal = controller.signal;
fetch(url, {signal}).then(r => r.json()).catch(err => {
if (err.name == 'AbortError') {
console.log('The request/response was aborted');
}
});
// sometime later…
controller.abort();
```
A signal can be assigned to many fetches, meaning you can control many fetches at once.
```js
const controller = new FetchController();
const signal = controller.signal;
abortButton.addEventListener('click', () => {
controller.abort();
}, {once: true});
startSpinner();
fetch('story.json', {signal}).then(async response => {
const data = await response.json();
// Fetch all the chapters
const texts = data.chapterURLs.map(url =>
fetch(url, {signal}).then(r => r.text())
);
// Add the chapters to the page as they arrive
for await (const text of texts) {
addToPage(text);
}
}).catch(err => {
if (err.name == 'AbortError') return;
showErrorMessage();
}).finally(() => {
stopSpinner();
});
```
Fetch will read state the current state from the signal, meaning this fetch will reject with an `AbortError` without issuing a request.
```js
const controller = new FetchController();
const signal = controller.signal;
controller.abort();
fetch(url, {signal});
```
Having the signal as a seperate object means you can pass the signal around without also giving the ability to control.
# Fetch observer
```webidl
[Exposed=(Window,Worker)]
interface FetchObserver : EventTarget {
Promise<octet> getPriority();
Promise<FetchState> getState();
// Events
attribute EventHandler onstatechange;
attribute EventHandler onprioritychange;
attribute EventHandler onrequestprogress;
attribute EventHandler onresponseprogress;
};
dictionary RequestInit {
// …
ObserverCallback observe;
}
callback ObserverCallback void (FetchObserver observer);
enum FetchState {
// Pending states
"requesting", "responding",
// Final states
"aborted", "errored", "complete"
};
[Constructor(TODO), Exposed=(Window,Worker)]
interface FetchStateChangeEvent : Event {
readonly attribute FetchState state;
}
[Constructor(TODO), Exposed=(Window,Worker)]
interface FetchPriorityChangeEvent : Event {
readonly attribute octet priority;
}
[Constructor(TODO), Exposed=(Window,Worker)]
interface FetchProgressEvent : ProgressEvent {
// Maybe we don't need anything else. Just move ProgressEvent to the fetch spec.
}
```
This lets you observe an ongoing fetch.
```js
fetch(url, {
observe(observer) {
observer.addEventListener('responseprogress', event => {
if (!lengthComputable) return;
progressEl.max = event.total;
progressEl.value = event.loaded;
});
}
});
```
# Within a service worker
```webidl
partial interface FetchEvent {
readonly attribute FetchSignal fetchSignal;
};
```
This allows you to react to signals from the client.
```js
addEventListener('fetch', event => {
event.respondWith(async function() {
const texts = [url1, url2, url3].map(url =>
fetch(url, {signal: event.fetchSignal}).then(r => r.text())
);
const fullText = (await Promise.all(texts)).join('');
return Response(fullText);
}());
});
```
By passing the signal to all fetches, they'll all abort if the page signals to abort.
`fetchEvent.request` will already be associated with `event.fetchSignal`, meaning:
```js
// This will abort if event.fetchSignal signals to abort.
fetch(event.request);
// This does not follow event.fetchSignal.
fetch(event.request, {signal: undefined});
```
# Following signals
`controller.follow` lets you use an existing signal, but you retain the ability to control.
```js
const controller = new FetchController();
const signal = controller.signal;
controller.follow(event.fetchSignal);
fetch(event.request, {signal});
```
Now the fetch will abort if I cann `controller.abort`, or `event.fetchSignal` signals to abort.
# Signals elsewhere
I want to think a bit more about it, but it feels like `caches.match` and `cache.match` should also be able to take a fetch signal. Priority may not have an impact here, but the ability to abort is useful.
--
You are receiving this because you are subscribed to this thread.
Reply to this email directly or view it on GitHub:
https://github.com/whatwg/fetch/issues/447#issuecomment-281731850
Received on Wednesday, 22 February 2017 17:01:36 UTC