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

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

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

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

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

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

डेमो

इससे पता चलता है कि उपयोगकर्ता से सर्वर पर डेटा कैसे स्ट्रीम किया जा सकता है और रीयल टाइम में प्रोसेस किए जा सकने वाले डेटा को कैसे वापस भेजा जा सकता है.

हां, यह कोई आम उदाहरण नहीं है. मैं इसे आसान रखना चाहती थी, ठीक है?

खैर, यह कैसे काम करता है?

इससे पहले, फ़ेच करने के रोमांचक कारनामों की सुविधा दी गई थी

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

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',
});

ऊपर दिए गए निर्देश सर्वर को "यह एक धीमा अनुरोध है" भेजेंगे. एक बार में एक शब्द के साथ हर शब्द के बीच एक सेकंड का ब्रेक होगा.

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

ज़रूरी शर्तें

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

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

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

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

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

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

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

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

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

प्रतिबंधित रीडायरेक्ट

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

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

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

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

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

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

HTTP/1.x पर काम नहीं करता

अगर कनेक्शन एचटीटीपी/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 का इस्तेमाल करके आर्बिट्रेरी डेटा को कंप्रेस करने के लिए, कंप्रेस स्ट्रीम का इस्तेमाल किया गया है.