फ़ेच एपीआई की मदद से स्ट्रीमिंग के अनुरोध

Chromium 105 से, Streams API का इस्तेमाल करके, पूरा जवाब उपलब्ध होने से पहले ही अनुरोध शुरू किया जा सकता है.

इसका इस्तेमाल इन कामों के लिए किया जा सकता है:

  • सर्वर को वार्म अप करें. दूसरे शब्दों में कहें, तो उपयोगकर्ता के टेक्स्ट इनपुट फ़ील्ड पर फ़ोकस करने के बाद, अनुरोध शुरू किया जा सकता है. साथ ही, सभी हेडर को हटाकर, उपयोगकर्ता के भेजे गए डेटा को तब तक नहीं भेजा जा सकता, जब तक वह 'भेजें' बटन नहीं दबाता.
  • क्लाइंट पर जनरेट हुआ डेटा धीरे-धीरे भेजें. जैसे, ऑडियो, वीडियो या इनपुट डेटा.
  • एचटीटीपी/2 या एचटीटीपी/3 पर वेब सॉकेट फिर से बनाएं.

हालांकि, यह वेब प्लैटफ़ॉर्म की एक बुनियादी सुविधा है. इसलिए, मेरे सुझावों तक ही सीमित न रहें. ऐसा हो सकता है कि आपको अनुरोध स्ट्रीम करने का कोई और दिलचस्प तरीका पता हो.

फ़ेच स्ट्रीम के रोमांचक एडवेंचर के बारे में पहले

Response स्ट्रीम, कुछ समय से सभी मॉडर्न ब्राउज़र में उपलब्ध हैं. ये आपको सर्वर से मिलने वाले जवाब के कुछ हिस्सों को ऐक्सेस करने की अनुमति देते हैं:

const response = await fetch(url);
const reader = response.body.getReader();

while (true) {
  const {value, done} = await reader.read();
  if (done) break;
  console.log('Received', value);
}

console.log('Response fully received');

हर value, Uint8Array बाइट का होता है. आपको कितनी ऐरे मिलेंगी और ऐरे का साइज़ कितना होगा, यह नेटवर्क की स्पीड पर निर्भर करता है. अगर आपका कनेक्शन तेज़ है, तो आपको डेटा के कम और बड़े 'चंक' मिलेंगे. अगर आपका इंटरनेट कनेक्शन धीमा है, तो आपको वीडियो के छोटे-छोटे ज़्यादा हिस्से दिखेंगे.

अगर आपको बाइट को टेक्स्ट में बदलना है, तो TextDecoder का इस्तेमाल करें. इसके अलावा, अगर टारगेट किए गए ब्राउज़र इसका समर्थन करते हैं, तो नई ट्रांसफ़ॉर्म स्ट्रीम का इस्तेमाल करें:

const response = await fetch(url);
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();

TextDecoderStream एक ट्रांसफ़ॉर्म स्ट्रीम है. यह Uint8Array के सभी हिस्सों को इकट्ठा करती है और उन्हें स्ट्रिंग में बदलती है.

स्ट्रीम बहुत अच्छी होती हैं, क्योंकि डेटा मिलते ही उस पर कार्रवाई की जा सकती है. उदाहरण के लिए, अगर आपको 100 'नतीजों' की सूची मिल रही है, तो सभी 100 नतीजों के मिलने का इंतज़ार करने के बजाय, पहला नतीजा मिलते ही उसे दिखाया जा सकता है.

खैर, यह रिस्पॉन्स स्ट्रीम के बारे में जानकारी थी. मुझे जिस नई और दिलचस्प सुविधा के बारे में बात करनी है वह अनुरोध स्ट्रीम है.

स्ट्रीमिंग के अनुरोधों का मुख्य हिस्सा

अनुरोधों में मुख्य हिस्से हो सकते हैं:

await fetch(url, {
  method: 'POST',
  body: requestBody,
});

पहले, अनुरोध शुरू करने से पहले आपको पूरा डेटा तैयार करना होता था. हालांकि, अब Chromium 105 में, ReadableStream का डेटा दिया जा सकता है:

function wait(milliseconds) {
  return new Promise(resolve => setTimeout(resolve, milliseconds));
}

const stream = new ReadableStream({
  async start(controller) {
    await wait(1000);
    controller.enqueue('This ');
    await wait(1000);
    controller.enqueue('is ');
    await wait(1000);
    controller.enqueue('a ');
    await wait(1000);
    controller.enqueue('slow ');
    await wait(1000);
    controller.enqueue('request.');
    controller.close();
  },
}).pipeThrough(new TextEncoderStream());

fetch(url, {
  method: 'POST',
  headers: {'Content-Type': 'text/plain'},
  body: stream,
  duplex: 'half',
});

ऊपर दिया गया कोड, "This is a slow request" को सर्वर पर भेजेगा. इसमें हर शब्द के बीच एक सेकंड का समय लगेगा.

अनुरोध के मुख्य हिस्से का हर हिस्सा Uint8Array बाइट का होना चाहिए. इसलिए, मैं pipeThrough(new TextEncoderStream()) का इस्तेमाल करके, इसे बदल रहा हूं.

पाबंदियां

स्ट्रीमिंग के अनुरोध, वेब के लिए एक नई सुविधा है. इसलिए, इस पर कुछ पाबंदियां लागू होती हैं:

हाफ़ डुप्लेक्स?

किसी अनुरोध में स्ट्रीम का इस्तेमाल करने के लिए, duplex अनुरोध विकल्प को 'half' पर सेट करना होगा.

एचटीटीपी की एक ऐसी सुविधा है जिसके बारे में बहुत कम लोगों को पता है. हालांकि, यह सुविधा स्टैंडर्ड है या नहीं, यह इस बात पर निर्भर करता है कि आपने किससे पूछा है. इस सुविधा के तहत, अनुरोध भेजने के दौरान ही जवाब मिलना शुरू हो जाता है. हालांकि, इसके बारे में बहुत कम लोगों को पता है. इसलिए, यह सर्वर पर ठीक से काम नहीं करता और किसी भी ब्राउज़र पर काम नहीं करता.

ब्राउज़र में, अनुरोध का जवाब तब तक उपलब्ध नहीं होता, जब तक अनुरोध का पूरा डेटा नहीं भेज दिया जाता. भले ही, सर्वर पहले ही जवाब भेज दे. यह सभी ब्राउज़र फ़ेचिंग पर लागू होता है.

इस डिफ़ॉल्ट पैटर्न को 'हाफ़ डुप्लेक्स' कहा जाता है. हालांकि, कुछ लागू करने के तरीके, जैसे कि fetch in Deno, स्ट्रीमिंग फ़ेच के लिए डिफ़ॉल्ट रूप से 'फुल डुप्लेक्स' पर सेट होते हैं. इसका मतलब है कि अनुरोध पूरा होने से पहले ही जवाब उपलब्ध हो सकता है.

इसलिए, ब्राउज़र में इस समस्या को ठीक करने के लिए, स्ट्रीम बॉडी वाले अनुरोधों में duplex: 'half' को शामिल करना ज़रूरी है.

आने वाले समय में, duplex: 'full' को स्ट्रीमिंग और बिना स्ट्रीमिंग वाले अनुरोधों के लिए, ब्राउज़र में इस्तेमाल किया जा सकेगा.

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

रीडायरेक्ट करने पर पाबंदी

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

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

303 रीडायरेक्ट की अनुमति है, क्योंकि वे तरीके को साफ़ तौर पर GET में बदल देते हैं और अनुरोध के मुख्य हिस्से को खारिज कर देते हैं.

इसके लिए सीओआरएस की ज़रूरत होती है और यह प्रीफ़्लाइट को ट्रिगर करता है

स्ट्रीमिंग के अनुरोधों में मुख्य हिस्सा होता है, लेकिन उनमें Content-Length हेडर नहीं होता. यह एक नया अनुरोध है. इसलिए, सीओआरएस ज़रूरी है. साथ ही, ये अनुरोध हमेशा प्रीफ़्लाइट को ट्रिगर करते हैं.

no-cors को स्ट्रीम करने के अनुरोधों की अनुमति नहीं है.

यह एचटीटीपी/1.x पर काम नहीं करता

अगर कनेक्शन HTTP/1.x है, तो फ़ेच करने का अनुरोध अस्वीकार कर दिया जाएगा.

ऐसा इसलिए है, क्योंकि एचटीटीपी/1.1 के नियमों के मुताबिक, अनुरोध और जवाब के मुख्य हिस्से को Content-Length हेडर भेजना होता है, ताकि दूसरी पार्टी को पता चल सके कि उसे कितना डेटा मिलेगा. इसके अलावा, मैसेज के फ़ॉर्मैट को बदलकर चंक किए गए डेटा को एन्कोड करने का तरीका भी इस्तेमाल किया जा सकता है. चंक किए गए एन्कोडिंग में, बॉडी को कई हिस्सों में बांटा जाता है. हर हिस्से की कॉन्टेंट लेंथ अलग होती है.

एचटीटीपी/1.1 जवाबों के लिए, चंक किए गए डेटा को एन्कोड करने का तरीका काफ़ी सामान्य है. हालांकि, अनुरोधों के लिए यह तरीका बहुत कम इस्तेमाल किया जाता है. इसलिए, यह तरीका इस्तेमाल करने पर, कंपैटिबिलिटी से जुड़ी समस्या होने का खतरा बहुत ज़्यादा होता है.

संभावित समस्याएं

यह एक नई सुविधा है और आज इंटरनेट पर इसका इस्तेमाल कम किया जाता है. यहां कुछ ऐसी समस्याएं बताई गई हैं जिन पर ध्यान देना ज़रूरी है:

सर्वर साइड पर काम न करने वाली सुविधा

कुछ ऐप्लिकेशन सर्वर, स्ट्रीमिंग के अनुरोधों के साथ काम नहीं करते. इसके बजाय, वे पूरे अनुरोध के मिलने का इंतज़ार करते हैं. इसके बाद ही, वे आपको अनुरोध का कोई हिस्सा दिखाते हैं. इससे स्ट्रीमिंग का मकसद पूरा नहीं होता. इसके बजाय, स्ट्रीमिंग की सुविधा देने वाले ऐप्लिकेशन सर्वर का इस्तेमाल करें. जैसे, NodeJS या Deno.

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

ऐसी समस्याएं जो आपके कंट्रोल में नहीं हैं

यह सुविधा सिर्फ़ एचटीटीपीएस पर काम करती है. इसलिए, आपको अपने और उपयोगकर्ता के बीच प्रॉक्सी के बारे में चिंता करने की ज़रूरत नहीं है. हालांकि, उपयोगकर्ता अपने डिवाइस पर प्रॉक्सी चला सकता है. इंटरनेट सुरक्षा से जुड़ा कुछ सॉफ़्टवेयर ऐसा करता है, ताकि वह ब्राउज़र और नेटवर्क के बीच होने वाली हर गतिविधि पर नज़र रख सके. ऐसा भी हो सकता है कि यह सॉफ़्टवेयर, अनुरोध के मुख्य हिस्से को बफ़र करे.

इससे बचने के लिए, ऊपर दिए गए डेमो की तरह 'सुविधा की जांच' की जा सकती है. इसमें स्ट्रीम बंद किए बिना, कुछ डेटा स्ट्रीम करने की कोशिश की जाती है. अगर सर्वर को डेटा मिल जाता है, तो वह किसी दूसरे फ़ेच के ज़रिए जवाब दे सकता है. ऐसा होने पर, आपको पता चल जाता है कि क्लाइंट, स्ट्रीमिंग के अनुरोधों को शुरू से आखिर तक प्रोसेस कर सकता है.

सुविधा का पता लगाना

const supportsRequestStreams = (() => {
  let duplexAccessed = false;

  const hasContentType = new Request('', {
    body: new ReadableStream(),
    method: 'POST',
    get duplex() {
      duplexAccessed = true;
      return 'half';
    },
  }).headers.has('Content-Type');

  return duplexAccessed && !hasContentType;
})();

if (supportsRequestStreams) {
  // …
} else {
  // …
}

अगर आपको इस बारे में ज़्यादा जानना है, तो यहां बताया गया है कि सुविधा का पता लगाने की सुविधा कैसे काम करती है:

अगर ब्राउज़र किसी खास body टाइप के साथ काम नहीं करता है, तो यह ऑब्जेक्ट पर toString() को कॉल करता है और नतीजे को बॉडी के तौर पर इस्तेमाल करता है. इसलिए, अगर ब्राउज़र अनुरोध स्ट्रीम के साथ काम नहीं करता है, तो अनुरोध का मुख्य हिस्सा, स्ट्रिंग "[object ReadableStream]" बन जाता है. जब किसी स्ट्रिंग का इस्तेमाल बॉडी के तौर पर किया जाता है, तो यह Content-Type हेडर को text/plain;charset=UTF-8 पर सेट कर देता है. इसलिए, अगर यह हेडर सेट है, तो हमें पता चल जाता है कि ब्राउज़र, अनुरोध ऑब्जेक्ट में स्ट्रीम करने की सुविधा नहीं देता है. ऐसे में, हम इस प्रोसेस को जल्दी बंद कर सकते हैं.

Safari, अनुरोध ऑब्जेक्ट में स्ट्रीम का इस्तेमाल करने की अनुमति देता है. हालांकि, fetch के साथ इनका इस्तेमाल नहीं किया जा सकता. इसलिए, duplex विकल्प की जांच की जाती है. फ़िलहाल, Safari इस विकल्प के साथ काम नहीं करता.

लिखने लायक़ स्ट्रीम के साथ इस्तेमाल करना

कभी-कभी WritableStream होने पर, स्ट्रीम के साथ काम करना आसान हो जाता है. इसके लिए, 'पहचान' स्ट्रीम का इस्तेमाल किया जा सकता है. यह एक ऐसा पेयर होता है जिसे पढ़ा/लिखा जा सकता है. यह लिखने के लिए इस्तेमाल होने वाले एंड में पास की गई किसी भी चीज़ को लेता है और उसे पढ़ने के लिए इस्तेमाल होने वाले एंड में भेजता है. इनमें से कोई एक बनाने के लिए, बिना किसी तर्क के TransformStream बनाएं:

const {readable, writable} = new TransformStream();

const responsePromise = fetch(url, {
  method: 'POST',
  body: readable,
});

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

// Get from url1:
const response = await fetch(url1);
const {readable, writable} = new TransformStream();

// Compress the data from url1:
response.body.pipeThrough(new CompressionStream('gzip')).pipeTo(writable);

// Post to url2:
await fetch(url2, {
  method: 'POST',
  body: readable,
});

ऊपर दिए गए उदाहरण में, gzip का इस्तेमाल करके किसी भी डेटा को कंप्रेस करने के लिए, कंप्रेशन स्ट्रीम का इस्तेमाल किया गया है.