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

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

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

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

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