- Explicit Resource Management प्रस्ताव फ़ाइल हैंडल, नेटवर्क कनेक्शन जैसे resources के lifecycle को स्पष्ट रूप से नियंत्रित करने का एक नया तरीका है
- Chromium 134 और V8 v13.8 से यह फीचर उपलब्ध है
- भाषा में जोड़े जाने वाले हिस्से
using और await using declarations के साथ Symbol.dispose, Symbol.asyncDispose symbols की शुरुआत, जो automatic cleanup mechanism प्रदान करते हैं
DisposableStack, AsyncDisposableStack कई resources को सुरक्षित रूप से group करके release करते हैं
SuppressedError cleanup के दौरान हुई error और मौजूदा error, दोनों को साथ में manage करता है
- यह तरीका code safety और maintainability को काफ़ी बढ़ाता है, और resource leaks को रोकने में प्रभावी है
- यह पारंपरिक try...finally pattern को सरल बनाता है और बड़े, जटिल resource environments में भरोसेमंद resource handling संभव करता है
स्पष्ट Resource Management प्रस्ताव का अवलोकन
- Explicit Resource Management प्रस्ताव फ़ाइल हैंडल, नेटवर्क कनेक्शन जैसे resources को स्पष्ट रूप से create और release करने का नया तरीका पेश करता है
- इसके मुख्य घटक इस प्रकार हैं
using और await using declarations: scope समाप्त होने पर resource अपने-आप release हो जाता है
[Symbol.dispose](), [Symbol.asyncDispose]() symbols: cleanup behavior को implement करने के लिए methods
- global objects DisposableStack, AsyncDisposableStack: कई resources को group करके प्रभावी ढंग से manage करते हैं
SuppressedError: resource cleanup के दौरान हुई error और मौजूदा error, दोनों को शामिल करने वाला नया error type
- ये फीचर्स developers को बारीकी से resource management करने और code की performance व safety बेहतर बनाने पर केंद्रित हैं
using और await using declarations
using declaration synchronous resources के लिए, और await using declaration asynchronous resources के लिए इस्तेमाल होती है
- declare किए गए resources के scope से बाहर जाते ही Symbol.dispose या Symbol.asyncDispose अपने-आप call हो जाते हैं
- इससे synchronous/asynchronous resource leaks की समस्या कम होती है और consistent cleanup code लिखा जा सकता है
- ये keywords केवल code blocks, for loops और function body के अंदर ही इस्तेमाल किए जा सकते हैं; top-level पर इनका उपयोग नहीं किया जा सकता
- उदाहरण
- उदाहरण के लिए,
ReadableStreamDefaultReader इस्तेमाल करते समय reader.releaseLock() को ज़रूर call करना पड़ता है ताकि stream को दोबारा उपयोग किया जा सके
- अगर error होने पर यह call छूट जाए, तो stream स्थायी रूप से lock हो सकती है
- पारंपरिक तरीका
- developer try...finally block का उपयोग करके reader unlock होना सुनिश्चित करता है
finally block में reader.releaseLock() code लिखना पड़ता है
- बेहतर तरीका:
using की शुरुआत
- cleanup behavior वाला disposable object (
readerResource) बनाया जाता है
using readerResource = {...} pattern इस्तेमाल करने पर code block से बाहर निकलते ही यह अपने-आप release हो जाता है
- आगे चलकर यदि web APIs में
[Symbol.dispose] और [Symbol.asyncDispose] का support आता है, तो अलग wrapper object लिखे बिना automatic management संभव हो सकता है
DisposableStack और AsyncDisposableStack
- कई resources को प्रभावी और सुरक्षित तरीके से group करने के लिए
DisposableStack और AsyncDisposableStack जोड़े गए हैं
- हर stack में resources जोड़े जा सकते हैं, और stack को dispose करने पर उसके अंदर के सभी resources उल्टे क्रम में release होते हैं
- dependency वाले जटिल resource sets को संभालते समय यह जोखिम कम करता है और code को सरल बनाता है
- मुख्य methods
use(value): stack के top पर disposable resource जोड़ता है
adopt(value, onDispose): non-disposable resource के साथ cleanup callback जोड़कर उसे शामिल करता है
defer(onDispose): resource के बिना केवल cleanup action जोड़ता है
move(): मौजूदा stack के सभी resources को नए stack में ले जाकर ownership transfer संभव बनाता है
dispose(), asyncDispose(): stack के भीतर के सभी resources release करते हैं
support status और उपयोग का समय
- Chromium 134, V8 v13.8 या उससे ऊपर में explicit resource management फीचर उपलब्ध है
- भविष्य में विभिन्न web APIs के साथ इसका compatibility और बढ़ने की उम्मीद है
4 टिप्पणियां
await using data = await fn()एक चमत्कार जिसमें
awaitबाएँ और दाएँ, दोनों तरफ दिखाई देता हैhttps://typescriptlang.org/docs/handbook/…
Hacker News राय
यह प्रस्ताव "function coloring" समस्या जैसा महसूस होता है। synchronous और asynchronous functions का भेद हर फीचर में घुसता चला जाता है। उदाहरण के लिए Symbol.dispose और Symbol.asyncDispose, DisposableStack और AsyncDisposableStack के मामले देखे जा सकते हैं। Java ने virtual threads का रास्ता चुना, इससे संतोष है। JVM में complexity जोड़कर application developers, library authors और debuggers का बोझ कम करने वाला फैसला लगता है
इससे सहमत नहीं कि async को छिपा देना बेहतर है। मैं यह जानना चाहूँगा कि resource asynchronous तरीके से release हो रहा है या नहीं, और क्या वह network issues जैसी बाहरी चीज़ों से प्रभावित हो सकता है
आजकल ज़्यादातर भाषाओं में "सारा code async में लिखना ही सामान्य है" वाली प्रवृत्ति सच में परेशान करती है। Purescript मुझे एकमात्र ऐसा उदाहरण दिखता है जहाँ code को Eff (synchronous effects) या Aff (asynchronous effects) में लिखा जा सकता है और call site पर चुनाव संभव है। Structured concurrency बढ़िया है, लेकिन व्यवहार में यह structured concurrency पाने के लिए किए जाने वाले syntactic काम से ज़्यादा server पर कई top-level request handlers रखने का तरीका लगता है। अंततः यह parallel processing को आसान बनाने का एक साधन भर है
JVM में यह कैसे implement हुआ, पता नहीं, लेकिन सामान्य रूप से multithreading सच में बहुत non-intuitive तकनीक है। race conditions, deadlocks, livelocks, starvation, memory visibility problems जैसी ढेरों समस्याओं पर पूरी-पूरी किताबें हैं। इसके मुकाबले single-threaded async programming का बोझ कहीं कम है। function coloring की समस्या सह लेना, multithreaded app में "Heisenbug" debug करने से कम पीड़ादायक विकल्प है
Java ने वह फैसला लिया, यह बात सच में खुशी देती है
इसकी एक व्याख्या यह है कि normal execution और async functions, closed Cartesian categories बनाते हैं। normal execution की category को async category में सीधे embed किया जा सकता है। हर function की एक category होती है, यानी function का एक रंग, और कुछ भाषाएँ इसे ज़्यादा स्पष्ट रूप से सामने लाती हैं। यह language design का चुनाव है, और category theory का उपयोग threading से आगे भी बहुत ताकतवर ढंग से हो सकता है। Java और thread-based approach को synchronization की समस्याओं का सामना करना पड़ता है, और यही हिस्सा खास तौर पर कठिन है। JavaScript monadic categories में से खासकर continuation-passing शैली पर सीमाएँ लगाता है
deferfunction के साथusingका उदाहरण देखकर काफी ताज़गी महसूस हुई। बहुतों को यह पहले से intuitive लगे, फिर भी इसका ज़िक्र करना ठीक हैusingप्रस्ताव में शामिल DisposableStack और AsyncDisposableStack का उपयोग करें तो callback registration built-in रूप में मिल जाता है।usingblock scope वाला है, इसलिए scope को cross करने या conditional registration के लिए इसकी ज़रूरत पड़ती है। लेकिनusingvariables कोconstकी तरह तुरंत initialize करना पड़ता है, इसलिए conditional initialization संभव नहीं। ऐसे में function के top level पर Stack बनाकर, इस्तेमाल किए गए resources कोdeferके ज़रिए stack पर चढ़ाने वाला pattern चाहिए। ज़रूरत पड़ने पर cleanup timing को आसानी से function level तक बदला जा सकता हैgolang जैसा एहसास होता है
यह सच में बहुत अच्छा विचार है, लेकिन<p>भले ही भविष्य में web API streams वगैरह में [Symbol.dispose] और [Symbol.asyncDispose] का एकीकरण संभव हो, निकट भविष्य में स्थिति यह होगी कि कुछ APIs और libraries ही इसे support करेंगी और बाकी, यानी ज़्यादातर, नहीं करेंगी। नतीजा यह दुविधा होगी कि "using" और try/catch को मिलाया जाए, या फिर पूरे codebase में try/catch लिखकर ज़्यादा समझने लायक code चुना जाए। इससे इस फीचर को "व्यावहारिक रूप से इस्तेमाल नहीं किया जा सकता" जैसी बदनामी मिलने का जोखिम है। यानी यह असली समस्या हल करने वाला अच्छा design होने के बावजूद अपनाना मुश्किल हो सकता है, यह खलता है
जो APIs इस फीचर को support नहीं करतीं, उन पर भी DisposableStack का इस्तेमाल करके
usingलागू किया जा सकता है। कई resources साथ संभालने पर भी यह try/catch से कहीं ज़्यादा सरल हो जाता है। बस runtime support हो, तो पुराने resources के update का इंतज़ार किए बिना तुरंत इस्तेमाल किया जा सकता हैJavaScript में यह स्थिति 15 साल से दोहराई जा रही है। नए language features पहले Babel जैसे compilers में आते हैं, फिर spec में शामिल होते हैं, और अंत में stable APIs और browsers तक पहुँचने में 3-4 साल लग जाते हैं। developers वैसे भी web APIs को छोटे wrappers में लपेटने के आदी हैं, और कई बार wrapper polyfill से बेहतर होता है। कोई उपयोगी नया language feature आ जाए तो मैंने कभी यह नहीं सोचा कि "इसे इस्तेमाल करना मुश्किल होगा"
वास्तव में बहुत-सी चीज़ें पहले से polyfill के रूप में implement हो चुकी हैं, इसलिए NodeJS ecosystem का बड़ा हिस्सा यह pattern इस्तेमाल करता है, और users transpiler के सहारे syntax मिलाकर लिखते हैं। पिछले साल इस विषय पर talk तैयार करते समय पता चला कि NodeJS और प्रमुख libraries में Symbol.dispose support करने वाले APIs पहले से काफ़ी हैं। frontend में lifecycle management systems होने के कारण इसका उपयोग कम हो सकता है, लेकिन कुछ परिस्थितियों में यह अब भी काम का है। test libraries या backend में यह काफ़ी फैल सकता है
TC39 को Rust के trait/protocol जैसी बुनियादी language features पर भी ध्यान देना चाहिए। Rust में नया trait define और implement करना अपेक्षाकृत आसान है, जबकि dynamic language होने और unique symbols होने के कारण JS में तो यह और भी सरलता से लाया जा सकता है। orphan rule जैसी कमियाँ होंगी, लेकिन संरचना कहीं ज़्यादा लचीली बन सकती है
JavaScript की दुनिया में आम तौर पर ऐसी बातों का हल polyfill से किया जाता है
C# याद आता है। IDisposable और IAsyncDisposable के जरिए lock management, queues, temporary scope management जैसी abstractions में यह बहुत उपयोगी है
प्रस्ताव का लेखक Microsoft पृष्ठभूमि से है, इसलिए syntax C# जैसा तय हुआ। संबंधित GitHub issues में भी यही संदर्भ लगातार दिखता है
मूल रूप से यह design C# से लिया गया है। असल प्रस्ताव Python के context manager, Java के try-with-resources, और C# के using statement जैसे उदाहरणों को भी refer करता है।
usingkeyword और dispose hook methods इससे काफ़ी संकेत दे देते हैंसमझ आता है कि JavaScript में backward compatibility ज़रूरी है, लेकिन
[Symbol.dispose]()syntax थोड़ा अटपटा लगता है। ऐसा भ्रम होता है जैसे array पर method handle रखा हो। यह syntax आखिर है क्या, इसे और समझना चाहता हूँसमझाया गया कि object literals में बाईं ओर square brackets के भीतर dynamic keys, यानी dynamically computed properties, ES6 के बाद लगभग 10 साल से इस्तेमाल हो रही हैं। और symbol को string से refer नहीं किया जा सकता, इसलिए dynamic keys और method shorthand syntax का संयोजन लिया गया है। मूल रूप से यह कोई नया syntax नहीं है
मज़बूत संदर्भों के साथ यह भी बताया गया कि यह मौजूदा objects पर symbol key assign करने के तरीके से स्वाभाविक रूप से निकला है
दूसरों ने यह तो समझाया कि यह क्या है, लेकिन शायद यह नहीं कि ऐसा क्यों है। method name में Symbol इस्तेमाल करने से यह सुनिश्चित होता है कि नया API मौजूदा methods से टकराए नहीं। इससे किसी class को गलती से disposable मान लेने से भी बचाव होता है
dynamic property access की अवधारणा का ज़िक्र। object properties को dot (.) या square brackets ([]) से access किया जा सकता है, और वे strings व symbols दोनों को support करती हैं। symbols unique objects की तरह compare होते हैं, और "[Symbol.dispose]" जैसे well known symbols extensibility सुनिश्चित करते हैं। इसे Python के dunder methods जैसी अवधारणा से भी जोड़ा गया
यह syntax कई सालों से मौजूद है। JavaScript के iterators भी इसी तरीके का उपयोग करते हैं, और यह लगभग 10 साल पहले जोड़ा गया था
resource management, खासकर जब lexical scope महत्वपूर्ण हो, ऐसे मामलों में JS में structured concurrency लाने की कोशिश क्यों की गई, इसकी पृष्ठभूमि साझा की गई। इससे जुड़ी structured concurrency library भी साझा की गई
Bun 1.0.23 या उससे ऊपर के versions में यह फीचर पहले से support होता है। प्रयोग के तौर पर इसे आज़माया जा सकता है
समझ नहीं आता कि इतनी जटिल code style में program के execution flow को कैसे समझें और नियंत्रित करें
यही तो असली बात है। web development का 90% हिस्सा ऐसे upgrades से भरा है जो बेकार हैं या किसी ने माँगे ही नहीं, और फिर उनसे बनी समस्याओं को बाकी 10% समय में ठीक करना पड़ता है। कभी-कभार पुराने लिखे code को किसी को देखना पड़ता है, और तब bug को नए भर्ती हुए व्यक्ति के onboarding task की तरह छोड़ देने का विचार सामने आता है। यहाँ तक कि 20 साल पुराने legacy systems भी अब तक चल रहे हैं
उदाहरण में दिया गया code गंभीर syntax errors से भरा है और असली JS से काफ़ी दूर है। और JS developers आम तौर पर इस तरह while, promise chain, finally वगैरह को मिलाकर नहीं लिखते; आम प्रथा
awaitया सही exception handling structures का उपयोग करना है। अच्छी तरह design की गई libraries में handlers की कई परतें नहीं चढ़ाई जातीं, बल्कि DisposableStack के साथ code को ज़्यादा संक्षिप्त रखा जा सकता है। आजकल कई बार तुरंत चलने वाली async function की भी ज़रूरत नहीं पड़तीजब आप उस भाषा में पेशेवर रूप से काम करते-करते उसके keywords के अर्थ और व्यवहार से परिचित हो जाते हैं, तो code स्वाभाविक रूप से समझ आने लगता है। Haskell programmers भी इसी तरह अभ्यस्त हो जाते हैं
HN में code embed करते समय हर line में कम से कम 2 spaces का indentation चाहिए। (हालाँकि code समझना कठिन है, इस बात से सहमत हूँ)
indentation मदद करती है, यही संक्षिप्त सलाह
यह सवाल कि anonymous class destructor क्यों नहीं लिया गया, या Symbol के अलावा कोई और संरचना क्यों नहीं इस्तेमाल हुई। दो Symbols (sync/async) होने से abstraction leak होने की समस्या उठाई गई
destructors के लिए predictable behavior चाहिए, यानी cleanup कब होगा यह साफ़ होना चाहिए, लेकिन उन्नत garbage collectors इस pattern के अनुकूल नहीं होते। आधुनिक भाषाएँ scope-based cleanup देती हैं और HoF, special hooks, callback registration जैसी कई तकनीकों से इसे लागू करती हैं। Python शुरू में destructor-आधारित, refcount GC मॉडल के क़रीब था, लेकिन सीमाओं के कारण context manager लाना पड़ा
दूसरी भाषाओं में destructors GC timing पर निर्भर करते हैं, इसलिए उन पर भरोसा नहीं किया जा सकता। इसके उलट dispose method variable scope समाप्त होते ही स्पष्ट रूप से call होती है, इसलिए file बंद करना या lock release करना जैसी चीज़ों के लिए यह predictable है। Symbol-आधारित methods मौजूदा features से टकराव से बचाते हैं, और सामान्यतः library developer को ही इसकी चिंता करनी पड़ती है। sync/async का भेद स्पष्ट होना चाहिए, और
await using a = await b()जैसी थोड़ी अपरिचित syntax की ज़रूरत पड़ सकती हैGC भाषाओं में destructor को sync तरीके से call करना कठिन होता है, इसलिए उनका व्यवहार प्रायः non-deterministic होता है। JS में WeakRef और FinalizationRegistry हैं, लेकिन Mozilla भी इन्हें unpredictable मानते हुए इनके उपयोग की सिफारिश नहीं करता
इस तरीके की एक ताकत यह है कि यह class instances तक सीमित नहीं है; दूसरे targets पर भी लागू हो सकता है
JavaScript में anonymous property जैसा कोई विचार नहीं है, इसलिए सवाल ही कुछ अस्पष्ट लगता है। इस तरीके के अलावा कोई वास्तविक विकल्प नहीं, ऐसा दावा किया गया
प्रस्ताव के पहले उदाहरण में try/finally के जरिए lock को सुरक्षित रूप से release करने वाला code है। सवाल यह उठा कि क्या ऐसा pattern सिर्फ लंबे समय तक चलने वाली स्थितियों में ही महत्वपूर्ण है, और browser या CLI environment में error से process बंद हो जाए तो lock release होता है या नहीं
spec के अनुसार block execution सामान्य रूप से समाप्त हो, या exception/branch/exit की वजह से, dispose हर हाल में चलाया जाता है। यानी
usingहो या try/finally, दोनों समान हैं। forceful termination, जैसे process को जबरन बंद करना, spec के दायरे से बाहर है इसलिए ECMAScript उसमें दखल नहीं देता। उदाहरण का stream JS के अंदर की object है, इसलिए interpreter ही खत्म हो जाए तो lock की अवधारणा का मतलब भी खत्म हो जाता है। अगर बात OS resources, जैसे memory या files, की हो तो आम तौर पर OS सामूहिक cleanup करता है, लेकिन व्यवहार platform के अनुसार बदलता हैbrowser web pages को एक तरह से बहुत लंबे समय तक चलने वाले applications माना जा सकता है। कई बार वे server processes से भी ज़्यादा समय तक चलते हैं। error आने से page मर नहीं जाता, और exceptions सहित error handling के स्पष्ट नियम हैं जिनके तहत finally में cleanup होता है। NodeJS में default रूप से error पर process terminate हो सकता है, लेकिन server स्थितियों में दूसरा handling करना आम है। यानी cleanup function का finally में call होना सुनिश्चित होता है
अब तक तो हम resources वगैरह की ज़रा भी परवाह किए बिना आराम से जी रहे थे। तुम्हें अचानक क्या हो गया?