प्रतिनिधि इंटरव्यू विषय

सही क्रम और विफलता सेमेंटिक्स के साथ Promise.all को कैसे लागू करें?

फ्रंटएंडमध्यम
Offer.cc संपादकीय टीमप्रकाशित अपडेट किया गया

प्रश्न

Promise.all को कॉल किए बिना, promiseAll(iterable) को इस प्रकार लागू करें कि यह मूल विधि के मुख्य व्यवहार को पुन: प्रस्तुत करे।

समस्या और प्रासंगिक संदर्भ

अपने समाधान के भीतर मूल Promise.all को कॉल किए बिना promiseAll(iterable) लागू करें। इनपुट एक सीमित समकालिक (synchronous) इटरेबल है जिसमें promises, thenables, या सामान्य मान (plain values) शामिल हैं। आउटपुट एक नया Promise है।

जब प्रत्येक इनपुट पूरा (fulfill) हो जाता है, तो परिणाम पूरा होने के क्रम की परवाह किए बिना, पुनरावृत्ति (iteration) क्रम में एक ऐरे होना चाहिए। जब कोई भी इनपुट अस्वीकृत (reject) होता है, तो बाहरी Promise को होने वाली पहली अस्वीकृति के कारण के साथ अस्वीकृत होना चाहिए। एक खाली इनपुट से [] उत्पन्न होना चाहिए। यदि इटरेटर से मान पढ़ते समय कोई त्रुटि उत्पन्न (throw) होती है, तो बाहरी Promise को भी अस्वीकृत होना चाहिए।

यह फ्रंटेंड या JavaScript भूमिका के लिए लाइव कोडिंग राउंड के लिए उपयुक्त है। समाधान thenables को आत्मसात करने और अतुल्यकालिक (asynchronous) प्रतिक्रियाओं को शेड्यूल करने के लिए मूल Promise का उपयोग कर सकता है। इसे सबक्लास कंस्ट्रक्टर्स, आंतरिक स्लॉट्स और इटरेटर क्लोजिंग से जुड़े प्रत्येक विनिर्देश विवरण को पुन: पेश करने की आवश्यकता नहीं है। कोडिंग से पहले उस सीमा को स्पष्ट करें: "मुख्य व्यवहार" और "पूरी तरह से विनिर्देश-अनुरूप कार्यान्वयन" अलग-अलग कार्य हैं।

साक्षात्कारकर्ता क्या मूल्यांकन कर रहा है

एक मजबूत उत्तर लूप लिखने से पहले सेमेंटिक्स को सूचीबद्ध करता है। पाँच उपयोगी संकेत हैं: केवल एक ऐरे के बजाय एक इटरेबल को स्वीकार करना, Promise.resolve के माध्यम से सामान्य मानों और thenables को सामान्यीकृत करना, क्रम बनाए रखने के लिए एक इंडेक्स आरक्षित करना, खाली इनपुट को अलग से संभालना, और पुनरावृत्ति त्रुटियों और वादा अस्वीकृतियों दोनों को बाहरी अस्वीकृति में रूट करना।

एक सामान्य उत्तर किसी ऐरे पर map को कॉल करता है और प्रत्येक पूर्ण मान को push करता है। यह एक ऐसे उदाहरण को पास कर सकता है जिसमें सब कुछ क्रम में पूरा होता है, लेकिन Set, जनरेटर, खाली इनपुट या क्रम से बाहर पूरा होने पर विफल हो जाता है। एक अधिक मजबूत उत्तर अपरिवर्तनीयों (invariants) से शुरू होता है: results[i] हमेशा पुनरावृत्ति इंडेक्स i पर मान से संबंधित होता है; pending उन इनपुट की संख्या है जो पूरे नहीं हुए हैं; पूर्ति की अनुमति केवल तब होती है जब pending शून्य तक पहुँच जाता है।

साक्षात्कारकर्ता यह भी देखेगा कि क्या "फ़ेल फ़ास्ट" को "बाकी को रद्द करने" के साथ भ्रमित किया गया है। एक बार जब बाहरी Promise अस्वीकृत हो जाता है, तो बाद के प्रयास इसकी स्थिति को नहीं बदल सकते हैं, लेकिन जो कार्य पहले ही शुरू हो चुका है वह जारी रहता है। रद्दीकरण के लिए इनपुट संचालन द्वारा समर्थित एक अतिरिक्त प्रोटोकॉल की आवश्यकता होती है; एक समग्र (aggregate) Promise उस क्षमता का आविष्कार नहीं कर सकता है।

उत्तर देने से पहले स्पष्ट करने योग्य प्रश्न

  • क्या इनपुट हमेशा एक ऐरे होता है, या कोई समकालिक इटरेबल? केवल-ऐरे समर्थन एक इंडेक्स लूप की अनुमति देता है। बताए गए इटरेबल अनुबंध का समर्थन करने के लिए for...of और अगला मान प्राप्त करते समय उत्पन्न त्रुटियों को संभालने की आवश्यकता होती है।
  • क्या सामान्य मान और thenables मान्य इनपुट हैं? यदि हाँ, तो प्रत्येक आइटम को Promise.resolve से गुजरना होगा। सीधे item.then को कॉल करना संख्याओं, स्ट्रिंग्स और सामान्य ऑब्जेक्ट्स पर विफल हो जाता है।
  • क्या यह विनिर्देश-स्तरीय पॉलीफ़िल है या मुख्य व्यवहार का साक्षात्कार कार्यान्वयन? एक विनिर्देश-स्तरीय संस्करण this कंस्ट्रक्टर, Promise सबक्लास, बार-बार कॉल के विरुद्ध आंतरिक सुरक्षा और सटीक इटरेटर-क्लोजिंग नियमों से भी निपटता है। यह समाधान एक मूल Promise लौटाता है और पूर्ण अनुरूपता का दावा नहीं करता है।
  • क्या विफलता के बाद शेष कार्यों को रद्द किया जाना चाहिए? यदि ऐसा है, तो संकेत को AbortSignal या कार्य रद्दीकरण इंटरफ़ेस को परिभाषित करना होगा। वर्तमान अनुबंध केवल बाहरी Promise को जल्दी अस्वीकार करता है; अन्य कार्य चलते रहते हैं।
  • क्या इनपुट सीमित है? Promise.all अपने इनपुट का समकालिक रूप से उपभोग करता है। एक अनंत इटरेटर का ट्रैवर्सल कभी समाप्त नहीं होता है। यह समाधान सीमित इनपुट मानता है ताकि स्पेस को n के संदर्भ में वर्णित किया जा सके।

ये उत्तर लूप, त्रुटि प्रबंधन और API अनुबंध को बदलते हैं, इसलिए कार्यान्वयन से पहले इनकी पुष्टि करना उचित है।

30-सेकंड उत्तर रूपरेखा

"मैं एक नया Promise लौटाऊंगा और सीमित इटरेबल को समकालिक रूप से पार (traverse) करूंगा। प्रत्येक आइटम के लिए, मैं उसके पुनरावृत्ति इंडेक्स को आरक्षित करता हूं, एक लंबित गणना को बढ़ाता हूं, और एक सामान्य मान, thenable, या Promise को सामान्यीकृत करने के लिए Promise.resolve का उपयोग करता हूं। एक पूर्ति हैंडलर आरक्षित इंडेक्स में लिखता है और गणना शून्य तक पहुंचने पर परिणाम ऐरे को हल करता है। अस्वीकृति हैंडलर बाहरी Promise को तुरंत अस्वीकार कर देता है। मैं पुनरावृत्ति को try...catch में लपेटूंगा ताकि एक इटरेटर त्रुटि भी अस्वीकृत हो जाए। खाली इनपुट में कोई पूर्ति हैंडलर नहीं होता है, इसलिए मैं ट्रैवर्सल के बाद स्पष्ट रूप से [] को हल करूंगा। यह इनपुट क्रम को संरक्षित करता है और तेजी से विफल होता है, लेकिन यह उन कार्यों को रद्द नहीं करता है जो पहले ही शुरू हो चुके हैं।"

चरण-दर-चरण गहन विश्लेषण

एक आकर्षक आधार रेखा से शुरुआत करें जो इसके समकक्ष नहीं है: प्रत्येक आइटम को क्रम में await करें और परिणाम एकत्र करें। यह क्रम को बनाए रखता है, लेकिन उन प्रतीक्षाओं को क्रमिक कर देता है जो अन्यथा ओवरलैप हो सकती थीं। जब तक पिछला आइटम व्यवस्थित नहीं होता, अगली प्रतिक्रिया पंजीकृत भी नहीं होती है। अनुरोधित ऑपरेशन उन इनपुट को एकत्रित करता है जो पहले ही प्राप्त किए जा चुके हैं; यह एक सीरियल टास्क कतार नहीं है।

अनुशंसित कार्यान्वयन के लिए एक परिणाम ऐरे, दो काउंटर और एक ट्रैवर्सल की आवश्यकता होती है:

javascript
function promiseAll(iterable) {
  return new Promise((resolve, reject) => {
    const results = [];
    let pending = 0;
    let index = 0;

    try {
      for (const item of iterable) {
        const currentIndex = index;
        index += 1;
        pending += 1;

        Promise.resolve(item).then(
          (value) => {
            results[currentIndex] = value;
            pending -= 1;

            if (pending === 0) {
              resolve(results);
            }
          },
          reject,
        );
      }
    } catch (error) {
      reject(error);
      return;
    }

    if (index === 0) {
      resolve([]);
    }
  });
}

currentIndex उस पुनरावृत्ति के दौरान तय रहता है। मान लीजिए कि तीन इनपुट 30, 10 और 20 मिलीसेकंड के बाद व्यवस्थित होते हैं। उनके पूर्ति हैंडलर 1, 2, 0 के क्रम में चलते हैं, लेकिन वे अभी भी 1, 2, 0 स्थितियों पर लिखते हैं, इसलिए अंतिम ऐरे 0, 1, 2 के क्रम में वापस किया जाता है। अनुक्रमित असाइनमेंट को results.push(value) से बदलने पर गलत तरीके से पूर्णता का क्रम वापस आ जाएगा।

Promise.resolve(item) एक साथ दो सीमाओं को कवर करता है। एक सामान्य मान पहले से पूरा किया गया Promise बन जाता है। एक thenable की then विधि को आत्मसात कर लिया जाता है; भले ही कोई टूटा हुआ thenable अपने कॉलबैक को एक से अधिक बार कॉल करता है, मूल Promise की एकतरफा स्थिति बार-बार होने वाले निपटान को इस हैंडलर तक पहुंचने से रोकती है। एक item instanceof Promise परीक्षण किसी अन्य क्षेत्र (realm) के Promises और मान्य thenables दोनों को अनदेखा कर देगा।

खाली इनपुट के लिए एक स्पष्ट शाखा की आवश्यकता होती है। काउंटर शून्य से शुरू होता है, और resolve को कॉल करने के लिए कोई पूर्ति हैंडलर मौजूद नहीं होता है। ट्रैवर्सल के बाद index === 0 की जाँच करने पर लौटाया गया Promise एक खाली ऐरे के साथ पूरा होता है। कॉलर द्वारा पंजीकृत एक .then अभी भी सामान्य Promise नियमों के तहत अतुल्यकालिक रूप से चलता है।

try...catch समकालिक पुनरावृत्ति विफलताओं को संभालता है। उदाहरण के लिए, एक जनरेटर पहले मान से एक हैंडलर पहले ही जुड़े होने के बाद अपने दूसरे next() कॉल पर त्रुटि उत्पन्न कर सकता है। कैच बाहरी Promise को अस्वीकार कर देता है; यदि पूर्व मान बाद में पूरा होता है, तो उसका हैंडलर अस्वीकृत स्थिति को नहीं बदल सकता है। व्यक्तिगत आइटमों से अस्वीकृतियों को उसी reject पर रूट किया जाता है। यदि कई इनपुट अस्वीकार करते हैं, तो जो भी अस्वीकृति हैंडलर पहले चलता है वह बाहरी कारण निर्धारित करता है।

ट्रैवर्सल और निपटान कुल O(n) कार्य करते हैं। परिणाम ऐरे और प्रति-आइटम पूर्ति प्रतिक्रियाओं के लिए O(n) स्थान की आवश्यकता होती है। एक पूर्ति को संभालने से एक अनुक्रमित लेखन और एक डिक्रीमेंट के लिए O(1) कार्य जुड़ जाता है। यदि उत्पाद को प्रत्येक पूर्ति और अस्वीकृति की आवश्यकता है, तो ऑल-सेटल्ड (all-settled) अनुबंध का उपयोग करें। यदि इसे समवर्ती सीमा (concurrency limit) की आवश्यकता है, तो टास्क फ़ैक्टरियों को स्वीकार करें और एक शेड्यूलर जोड़ें; इस फ़ंक्शन को उन Promises का संग्रह पास करना जो पहले ही शुरू हो चुके हैं, समवर्ती को पूर्वव्यापी रूप से सीमित नहीं कर सकता है।

सत्यापन सामान्य पथ से आगे बढ़ना चाहिए। एक खाली इटरेबल; एक Set; सामान्य मानों, Promises और thenables का मिश्रण; इनपुट के उल्टे क्रम में पूर्णता; सबसे पहली अस्वीकृति; और एक इटरेटर जो बीच में ही त्रुटि उत्पन्न करता है, को कवर करें। प्रत्येक मामला एक विशिष्ट अपरिवर्तनीय का परीक्षण करता है, जो एक नमूना ऐरे की तुलना करने से अधिक ठोस है।

उच्च-गुणवत्ता वाला नमूना उत्तर

"मैं इसे एक सीमित समकालिक इटरेबल और एक मूल Promise परिणाम तक सीमित रखूंगा, बिना पूर्ण Promise-सबक्लास अनुरूपता का दावा किए। तीन सेमेंटिक्स लागू होने चाहिए: परिणाम पुनरावृत्ति क्रम का पालन करते हैं, जैसे ही कोई इनपुट अस्वीकृत होता है बाहरी Promise तुरंत अस्वीकृत हो जाता है, और खाली इनपुट एक खाली ऐरे के साथ पूरा होता है।

ट्रैवर्सल के दौरान, मैं प्रत्येक आइटम को एक स्थिर इंडेक्स प्रदान करता हूं और pending को बढ़ाता हूं। प्रत्येक आइटम Promise.resolve से होकर गुजरता है, इसलिए संख्याएं, मौजूदा Promises और thenables एक ही पथ साझा करते हैं। पूर्ति पर, मैं स्थिर स्थिति में लिखता हूं और गणना को घटाता हूं; अंतिम पूर्ति पूरे ऐरे को हल करती है। अस्वीकृति हैंडलर बाहरी reject है। मैं for...of को try...catch के अंदर रखता हूं क्योंकि अगला इटरेबल मान प्राप्त करना समकालिक रूप से त्रुटि उत्पन्न कर सकता है। यदि ट्रैवर्सल में कोई तत्व नहीं मिलता है, तो मैं सीधे [] के साथ हल करता हूं।

ट्रैवर्सल और निपटान में O(n) कार्य और O(n) स्थान लगता है। फ़ेल-फ़ास्ट व्यवहार केवल समग्र परिणाम को बदलता है; यह अन्य अतुल्यकालिक कार्यों को नहीं रोकता है। यदि रद्दीकरण की आवश्यकता है, तो मैं इनपुट टास्क अनुबंध में एक AbortSignal जोड़ूंगा। यदि प्रत्येक त्रुटि को एकत्र किया जाना चाहिए, तो मैं इस फ़ंक्शन के अस्वीकृति नियम को बदलने के बजाय ऑल-सेटल्ड सेमेंटिक्स का उपयोग करूंगा।"

सामान्य गलतियाँ

  • पूर्ण मानों को push से एकत्र करना → ऐरे क्रम पूर्णता गति का अनुसरण करता है, इसलिए एक धीमा पहला आइटम अंतिम दिखाई दे सकता है → ट्रैवर्सल के दौरान currentIndex को कैप्चर करें और results[currentIndex] असाइन करें।
  • केवल item.then(...) को कॉल करना → सामान्य मानों में कोई then नहीं होता है, और गैर-मूल thenables सुरक्षित रूप से आत्मसात नहीं होते हैं → प्रत्येक आइटम को Promise.resolve(item) के साथ सामान्यीकृत करें।
  • इटरेबल समर्थन का दावा करते हुए इनपुट लंबाई से लंबित गणना शुरू करना → Sets और जनरेटर के पास कोई विश्वसनीय length नहीं होता है, जिससे अनुबंध चुपचाप संकीर्ण हो जाता है → जैसे-जैसे मान पार किए जाते हैं pending बढ़ाएं।
  • खाली इनपुट भूल जाना → कोई भी हैंडलर पूर्ति को ट्रिगर नहीं कर सकता है, जिससे लौटाया गया Promise हमेशा के लिए लंबित रह जाता है → ट्रैवर्सल के बाद जब index === 0 हो तो एक खाली ऐरे हल करें।
  • forEach(async ...) का उपयोग करना और फिर इसका await करना → forEach अतुल्यकालिक कॉलबैक की प्रतीक्षा नहीं करता है, इसलिए नियंत्रण प्रवाह और त्रुटि प्रसार गलत होते हैं → सीधे .then हैंडलर पंजीकृत करें और काउंटर के साथ एकत्रित करें।
  • केवल इनपुट अस्वीकृतियों को संभालना → इटरेबल के next() से एक समकालिक त्रुटि तर्क से बच जाती है → पूरे ट्रैवर्सल को try...catch में लपेटें और बाहरी Promise को अस्वीकार करें।
  • यह दावा करना कि फ़ेल-फ़ास्ट व्यवहार अनुरोधों को रद्द करता है → एक अपरिवर्तनीय Promise स्थिति अंतर्निहित ऑपरेशन को नहीं रोकती है → बताएं कि रद्दीकरण के लिए AbortSignal या किसी अन्य कार्य-स्तरीय रद्दीकरण प्रोटोकॉल की आवश्यकता होती है।

अनुवर्ती प्रश्न और उत्तर

अनुवर्ती 1: आप कैसे साबित कर सकते हैं कि परिणाम का क्रम सही है?

इस अपरिवर्तनीय का उपयोग करें: पुनरावृत्ति इंडेक्स i पर मान केवल इंडेक्स i का स्वामी है, और इसका पूर्ति हैंडलर केवल results[i] पर लिखता है। हैंडलर क्रम बदलता है कि कोई लेखन कब होता है, कभी यह नहीं कि वह कहाँ होता है। सभी n इनपुट पूरे होने के बाद, स्थिति 0 से n - 1 में संबंधित इनपुट के मान होते हैं। समान विलंब पर निर्भर रहने के बजाय तीन Promises के साथ इस दावे का परीक्षण करें जिनके विलंब उनके इनपुट क्रम के विपरीत हैं।

अनुवर्ती 2: अस्वीकृति के बाद आप शेष नेटवर्क अनुरोधों को कैसे रद्द करेंगे?

वर्तमान फ़ंक्शन ऐसा नहीं कर सकता, क्योंकि एक Promise अंतर्निहित ऑपरेशन के रद्दीकरण प्रवेश बिंदु को प्रकट नहीं करता है। इनपुट को उन टास्क फ़ैक्टरियों में बदलें जो एक AbortSignal प्राप्त करते हैं, और एक साझा AbortController बनाएं। जब कोई कार्य विफल हो जाता है, तो बाहरी Promise को अस्वीकार करने से पहले controller.abort() को कॉल करें। कुछ कार्य पहले से ही पूर्ण हो सकते हैं, सिग्नल को अनदेखा कर सकते हैं, या रद्दीकरण के दौरान विफल हो सकते हैं, इसलिए अनुरोध रद्दीकरण मानक Promise.all सेमेंटिक्स का एक छिपा हुआ हिस्सा होने के बजाय एक विस्तारित अनुबंध है।

अनुवर्ती 3: क्या होगा यदि एक बार में अधिकतम तीन कार्य चल सकते हैं?

इनपुट को पहले से शुरू किए गए Promises से उन फ़ंक्शंस में बदलें जो अभी शुरू नहीं हुए हैं। अगले कार्य इंडेक्स, वर्तमान में चल रहे कार्यों की संख्या और परिणाम ऐरे को ट्रैक करें। जब भी कोई कार्य व्यवस्थित हो जाए, तो चालू संख्या को 3 या उससे कम रखते हुए दूसरा कार्य शुरू करें। यदि API अभी भी एक Promise ऐरे स्वीकार करता है, तो उसके संचालन आमतौर पर उस ऐरे के निर्माण के दौरान शुरू होते हैं, इसलिए शेड्यूलर पहले से ही बहुत देर कर चुका होता है। मुख्य मुद्दा प्रारंभ समय है, न कि पूर्णता काउंटर का नाम बदलना।

अनुवर्ती 4: विनिर्देश-स्तरीय Promise.all से क्या अंतर रहता है?

यह कार्यान्वयन हमेशा मूल Promise का उपयोग करता है। यह this से कंस्ट्रक्टर प्राप्त नहीं करता है, या Promise सबक्लास व्यवहार, आंतरिक स्लॉट, विनिर्देश के इटरेटर-क्लोजिंग चरणों और प्रत्येक अचानक-पूर्णता (abrupt-completion) विवरण को पुन: उत्पन्न नहीं करता है। एक साक्षात्कार में, इसे मुख्य व्यवहार का कार्यान्वयन कहें। एक प्रकाशित करने योग्य पॉलीफ़िल के लिए ECMAScript एल्गोरिदम के चरण-दर-चरण मैपिंग के साथ-साथ संगतता परीक्षणों की आवश्यकता होती है; अकेले सामान्य उदाहरण अनुरूपता स्थापित नहीं करते हैं।

सार्वजनिक स्रोत

संबंधित प्रश्न