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

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' पर सेट होना चाहिए.

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

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

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

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