फ़ेच करने की अनुमति रद्द की जा सकती है

जेक आर्चिबाल्ड
जेक आर्किबाल्ड

"फ़ेच करना रद्द करना" के लिए, GitHub की मूल समस्या 2015 में खोली गई थी. अगर मैं 2015 को 2017 (मौजूदा साल) के मुकाबले छोड़ दूं, तो मुझे दो मिलेंगे. इससे गणित के एक बग को दिखाया जाता है, क्योंकि 2015 असल में "हमेशा" के लिए था.

साल 2015 में, हमने पहली बार फ़ेच की जा रही फ़ाइलों को रद्द करने की प्रक्रिया शुरू की. इसके बाद, हमने 780 GitHub पर की गई टिप्पणियों, कई बार गलत जानकारी देने, और पांच पुल अनुरोधों के बाद, ब्राउज़र में फ़ेच लैंडिंग की सुविधा शुरू की. हमारा पहला नाम Firefox 57 था.

अपडेट: नहीं, मैं गलत था. Edge 16, सबसे पहले रद्द करने की सुविधा के साथ लॉन्च होगा! Edge टीम को बधाई हो!

मैं बाद में इतिहास के बारे में जान लूंगा, लेकिन पहले एपीआई के बारे में:

कंट्रोलर और सिग्नल की देखरेख

AbortController और AbortSignal से मिलें:

const controller = new AbortController();
const signal = controller.signal;

कंट्रोलर में सिर्फ़ एक तरीका होता है:

controller.abort();

ऐसा करने पर, यह सिग्नल को सूचना देता है:

signal.addEventListener('abort', () => {
    // Logs true:
    console.log(signal.aborted);
});

यह एपीआई DOM स्टैंडर्ड से मिला है और यह पूरा एपीआई है. यह जान-बूझकर सामान्य है, ताकि दूसरे वेब स्टैंडर्ड और JavaScript लाइब्रेरी इसका इस्तेमाल कर सकें.

सिग्नल रद्द करें और फ़ेच करें

फ़ेच करने में AbortSignal लग सकता है. उदाहरण के लिए, यहां बताया गया है कि 5 सेकंड के बाद फ़ेच टाइम आउट कैसे किया जाएगा:

const controller = new AbortController();
const signal = controller.signal;

setTimeout(() => controller.abort(), 5000);

fetch(url, { signal }).then(response => {
    return response.text();
}).then(text => {
    console.log(text);
});

जब किसी फ़ेच को रद्द किया जाता है, तो अनुरोध और रिस्पॉन्स, दोनों को रद्द कर दिया जाता है. इससे रिस्पॉन्स के मुख्य हिस्से (जैसे, response.text()) को पढ़ने की प्रोसेस भी रद्द हो जाती है.

यहां एक डेमो दिया गया है – लिखते समय, यह सिर्फ़ Firefox 57 पर काम करने वाला ब्राउज़र है. साथ ही, इस बारे में भी सोचें कि इस डेमो को बनाने में किसी भी व्यक्ति ने इस तरह के डिज़ाइन का इस्तेमाल नहीं किया है.

इसके अलावा, अनुरोध किए गए किसी ऑब्जेक्ट को सिग्नल दिया जा सकता है और बाद में उसे फ़ेच करने के लिए पास किया जा सकता है:

const controller = new AbortController();
const signal = controller.signal;
const request = new Request(url, { signal });

fetch(request);

यह इसलिए काम करता है, क्योंकि request.signal एक AbortSignal है.

फ़ेच नहीं किए गए फ़ेच पर प्रतिक्रिया दी जा रही है

किसी एसिंक्रोनस कार्रवाई को रद्द करने पर, प्रॉमिस AbortError नाम के DOMException से अस्वीकार हो जाता है:

fetch(url, { signal }).then(response => {
    return response.text();
}).then(text => {
    console.log(text);
}).catch(err => {
    if (err.name === 'AbortError') {
    console.log('Fetch aborted');
    } else {
    console.error('Uh oh, an error!', err);
    }
});

अगर उपयोगकर्ता कार्रवाई को रद्द कर देता है, तो आम तौर पर आपको गड़बड़ी का मैसेज नहीं दिखाना चाहिए. ऐसा इसलिए, क्योंकि उपयोगकर्ता के पूछे गए कार्रवाई को पूरा कर लेने पर यह कोई "गड़बड़ी" नहीं होती. इससे बचने के लिए, रद्द करने की गड़बड़ियों को खास तौर पर हैंडल करने के लिए, ऊपर दिए गए if-स्टेटमेंट का इस्तेमाल करें.

यहां एक उदाहरण दिया गया है, जिसमें उपयोगकर्ता को कॉन्टेंट लोड करने के लिए बटन और रद्द करने के लिए बटन दिया गया है. अगर फ़ेच करने से जुड़ी गड़बड़ी होती है, तो गड़बड़ी का मैसेज दिखता है. ऐसा तब तक होगा, जब तक कि यह रद्द करने से जुड़ी गड़बड़ी न हो:

// This will allow us to abort the fetch.
let controller;

// Abort if the user clicks:
abortBtn.addEventListener('click', () => {
    if (controller) controller.abort();
});

// Load the content:
loadBtn.addEventListener('click', async () => {
    controller = new AbortController();
    const signal = controller.signal;

    // Prevent another click until this fetch is done
    loadBtn.disabled = true;
    abortBtn.disabled = false;

    try {
    // Fetch the content & use the signal for aborting
    const response = await fetch(contentUrl, { signal });
    // Add the content to the page
    output.innerHTML = await response.text();
    }
    catch (err) {
    // Avoid showing an error message if the fetch was aborted
    if (err.name !== 'AbortError') {
        output.textContent = "Oh no! Fetching failed.";
    }
    }

    // These actions happen no matter how the fetch ends
    loadBtn.disabled = false;
    abortBtn.disabled = true;
});

यहां एक डेमो दिया गया है – कोड लिखते समय, सिर्फ़ Edge 16 और Firefox 57 इन ब्राउज़र पर यह काम करता है.

एक सिग्नल, कई फ़ेच

एक ही सिग्नल का इस्तेमाल करके, कई फ़ेच को एक साथ रद्द किया जा सकता है:

async function fetchStory({ signal } = {}) {
    const storyResponse = await fetch('/story.json', { signal });
    const data = await storyResponse.json();

    const chapterFetches = data.chapterUrls.map(async url => {
    const response = await fetch(url, { signal });
    return response.text();
    });

    return Promise.all(chapterFetches);
}

ऊपर दिए गए उदाहरण में, शुरुआती फ़ेच और पैरलल चैप्टर के फ़ेच के लिए, इसी सिग्नल का इस्तेमाल किया गया है. यहां बताया गया है कि आप fetchStory का इस्तेमाल किस तरह करेंगे:

const controller = new AbortController();
const signal = controller.signal;

fetchStory({ signal }).then(chapters => {
    console.log(chapters);
});

इस स्थिति में, controller.abort() को कॉल करने से, जो भी फ़ेच किए जा रहे हैं उन्हें रद्द कर दिया जाएगा.

आने वाला समय

अन्य ब्राउज़र

एज ने इसे पहले शिप करने में शानदार काम किया और Firefox अपनी पगडंडी पर गर्म है. जब स्पेसिफ़िकेशन को लिखा जा रहा था, तब उनके इंजीनियरों ने टेस्ट सुइट से लागू किया. अन्य ब्राउज़र के लिए, यहां दिए गए टिकट देखें:

सर्विस वर्कर में

मुझे सर्विस वर्कर पुर्ज़ों से जुड़ी जानकारी पूरी करनी होगी, लेकिन यह रहा प्लान:

जैसा कि मैंने पहले बताया, हर Request ऑब्जेक्ट की एक signal प्रॉपर्टी होती है. सर्विस वर्कर में, अगर पेज को जवाब में दिलचस्पी नहीं है, तो fetchEvent.request.signal सेवा रद्द करने का सिग्नल देगा. नतीजतन, इस तरह का कोड काम करता है:

addEventListener('fetch', event => {
    event.respondWith(fetch(event.request));
});

अगर पेज की वजह से फ़ेच नहीं हो पाता है, तो fetchEvent.request.signal सिग्नल रद्द हो जाता है. इसलिए, सर्विस वर्कर में मिला फ़ेच भी रद्द हो जाता है.

अगर event.request के बजाय कुछ और फ़ेच किया जा रहा है, तो आपको अपने कस्टम फ़ेच पर सिग्नल पास करना होगा.

addEventListener('fetch', event => {
    const url = new URL(event.request.url);

    if (event.request.method == 'GET' && url.pathname == '/about/') {
    // Modify the URL
    url.searchParams.set('from-service-worker', 'true');
    // Fetch, but pass the signal through
    event.respondWith(
        fetch(url, { signal: event.request.signal })
    );
    }
});

इसे ट्रैक करने के लिए ज़रूरी जानकारी का पालन करें – जब यह लागू होने के लिए तैयार हो जाएगा, तब ब्राउज़र टिकट के लिंक जोड़े जाएंगे.

इतिहास

हां... इस सामान्य एपीआई को इस्तेमाल करने में काफ़ी समय लगा. यहां इसकी वजहें बताई गई हैं:

एपीआई से जुड़ी असहमति

जैसा कि आपने देखा, GitHub पर चर्चा काफ़ी लंबी है. उस थ्रेड में बहुत ज़्यादा अंतर है (और कुछ हद तक कम अंतर है), लेकिन मुख्य असहमति इस बात की है कि fetch() से मिले ऑब्जेक्ट के लिए, abort तरीके को मौजूद रखना था, जबकि दूसरा ग्रुप चाहता था कि रिस्पॉन्स मिलने और रिस्पॉन्स पर असर डालने वाले ऑब्जेक्ट के बीच अंतर हो.

ये ज़रूरी शर्तें पूरी नहीं होती हैं. इसलिए, एक ग्रुप को वे चीज़ें नहीं मिलेंगी जो वे चाहते थे. अगर आप वह हैं, तो माफ़ करें! अगर इससे तुम्हें बेहतर महसूस होता है, तो मैं भी उस ग्रुप में शामिल हूं. हालांकि, AbortSignal को दूसरे एपीआई की ज़रूरतों के मुताबिक देखकर, यह सही विकल्प लगता है. साथ ही, एक-दूसरे से जुड़े वादों को रद्द करने की अनुमति देना मुश्किल हो जाता है. यह नामुमकिन नहीं होता.

अगर आपको कोई ऐसा ऑब्जेक्ट दिखाना है जो रिस्पॉन्स देता है, लेकिन उसे रद्द भी किया जा सकता है, तो एक आसान रैपर बनाएं:

function abortableFetch(request, opts) {
    const controller = new AbortController();
    const signal = controller.signal;

    return {
    abort: () => controller.abort(),
    ready: fetch(request, { ...opts, signal })
    };
}

TC39 में 'गलत' शुरू होता है

रद्द की गई कार्रवाई को किसी गड़बड़ी से अलग बनाने की कोशिश की गई थी. इसमें "रद्द किया गया" दिखाने वाला तीसरा प्रॉमिस स्टेटस और सिंक और एसिंक्रोनस कोड, दोनों में रद्द करने की प्रक्रिया को मैनेज करने के लिए कुछ नए सिंटैक्स शामिल थे:

यह न करें

यह कोड मान्य नहीं है — प्रस्ताव वापस ले लिया गया

    try {
      // Start spinner, then:
      await someAction();
    }
    catch cancel (reason) {
      // Maybe do nothing?
    }
    catch (err) {
      // Show error message
    }
    finally {
      // Stop spinner
    }

किसी कार्रवाई के रद्द होने पर, आम तौर पर कुछ नहीं किया जाता. ऊपर दिए गए प्रस्ताव में, रद्द करने की प्रक्रिया को गड़बड़ियों से अलग किया गया है. इसलिए, आपको खास तौर पर रद्द करने की गड़बड़ियों से निपटने की ज़रूरत नहीं है. catch cancel से, आपको रद्द की गई कार्रवाइयों के बारे में जानकारी मिलती है. हालांकि, ज़्यादातर मामलों में आपको इसकी ज़रूरत नहीं होती.

यह TC39 में, पहले चरण पर पहुंच गया था, लेकिन इस पर आम सहमति नहीं बन पाई और प्रपोज़ल को वापस ले लिया गया.

हमारे दूसरे सुझाव, AbortController को किसी नए सिंटैक्स की ज़रूरत नहीं थी. इसलिए, इसे TC39 के तहत बताना सही नहीं था. JavaScript से हमें जो भी चीज़ चाहिए थी, वह पहले से ही वहां मौजूद थी, इसलिए हमने वेब प्लैटफ़ॉर्म में इंटरफ़ेस तय किए, खास तौर पर डीओएम स्टैंडर्ड. यह फ़ैसला लेने के बाद, बाकी जानकारी काफ़ी जल्दी एक साथ आ गई.

खास जानकारी में हुए बड़े बदलाव

XMLHttpRequest सालों से गर्भपात नहीं कर रहा, लेकिन इसके बारे में साफ़ तौर पर जानकारी नहीं दी गई थी. यह साफ़ तौर पर नहीं बताया गया था कि किन वजहों से, नेटवर्क गतिविधि को रोका या खत्म किया जा सकता है या abort() को कॉल करने और फ़ेच करने की प्रक्रिया पूरी करने के बीच रेस की स्थिति होने पर क्या हुआ.

इस बार हम सही जानकारी देना चाहते थे, लेकिन इससे हमें बहुत कुछ नया देखने को मिला (यह मेरी गलती है और मुझे निकालने के लिए ऐन वैन केस्टरन और डोमेनिक डेन्कोला को बहुत-बहुत धन्यवाद) और टेस्ट के एक अच्छे सेट.

लेकिन हम आपकी मदद के लिए मौजूद हैं! हमारे पास एक नया वेब प्रिमिटिव है, जो एक साथ काम नहीं करने वाली कार्रवाइयों को रद्द करने के लिए है. साथ ही, एक से ज़्यादा फ़ेच को एक साथ कंट्रोल किया जा सकता है! आगे की लाइन में, हम फ़ेच की पूरी अवधि के दौरान प्राथमिकता बदलावों को चालू करने और फ़ेच की प्रोग्रेस देखने के लिए, हाई-लेवल एपीआई को चालू करने पर ध्यान देंगे.