- 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