7 points par GN⁺ 2025-05-18 | 4 commentaires | Partager sur WhatsApp
  • La proposition Explicit Resource Management introduit une nouvelle manière de contrôler clairement le cycle de vie de ressources comme les handles de fichiers et les connexions réseau
  • Cette fonctionnalité est disponible à partir de Chromium 134 et de V8 v13.8
  • Éléments ajoutés au langage
    • Les déclarations using et await using, ainsi que les symboles Symbol.dispose et Symbol.asyncDispose, fournissent un mécanisme de nettoyage automatique
    • DisposableStack et AsyncDisposableStack permettent de regrouper et de libérer plusieurs ressources en toute sécurité
    • SuppressedError permet de gérer ensemble les erreurs survenues pendant le nettoyage et les erreurs existantes
  • Cette approche améliore fortement la sécurité du code et sa maintenabilité, tout en étant efficace pour prévenir les fuites de ressources
  • Elle simplifie le motif try...finally existant et permet une gestion fiable des ressources dans des environnements complexes à grande échelle

Aperçu de la proposition de gestion explicite des ressources

  • La proposition Explicit Resource Management introduit une nouvelle façon de créer et libérer explicitement des ressources comme les handles de fichiers et les connexions réseau
  • Ses principaux éléments sont les suivants
    • Déclarations using et await using : libération automatique des ressources à la fin de la portée
    • Symboles [Symbol.dispose]() et [Symbol.asyncDispose]() : méthodes pour implémenter l’action de libération (cleanup)
    • Objets globaux DisposableStack et AsyncDisposableStack : regroupent plusieurs ressources pour une gestion efficace
    • SuppressedError : nouveau type d’erreur qui inclut à la fois les erreurs survenues pendant le nettoyage des ressources et l’erreur d’origine
  • Ces fonctionnalités visent à permettre aux développeurs de gérer finement les ressources et d’améliorer les performances et la sécurité du code

Déclarations using et await using

  • La déclaration using s’emploie pour les ressources synchrones, et await using pour les ressources asynchrones
  • Les ressources déclarées appellent automatiquement Symbol.dispose** ou **Symbol.asyncDispose` lorsqu’elles sortent de leur portée
  • Cela permet de réduire les problèmes de fuite de ressources synchrones/asynchrones et d’écrire un code de libération cohérent
  • Ces mots-clés ne peuvent être utilisés que dans un bloc de code, une boucle for ou le corps d’une fonction, et non au niveau supérieur
  • Exemple
    • Par exemple, lors de l’utilisation de ReadableStreamDefaultReader, il faut impérativement appeler reader.releaseLock() pour pouvoir réutiliser le flux
    • Si une erreur survient et que cet appel est omis, le flux peut rester verrouillé de manière permanente
  • Approche traditionnelle
    • Les développeurs utilisent un bloc try...finally pour garantir la libération du verrou du reader
    • Il faut écrire le code reader.releaseLock() dans le bloc finally
  • Approche améliorée : introduction de using
    • Création d’un objet jetable (readerResource) intégrant l’action de libération
    • Avec le motif using readerResource = {...}, la libération s’effectue automatiquement dès la sortie du bloc de code
    • À l’avenir, si les Web API prennent en charge [Symbol.dispose] et [Symbol.asyncDispose], une gestion automatique pourrait devenir possible sans écrire d’objet wrapper distinct

DisposableStack et AsyncDisposableStack

  • DisposableStack et AsyncDisposableStack sont introduits pour regrouper plusieurs ressources de manière efficace et sûre
  • On ajoute les ressources à chaque pile, puis lorsque la pile elle-même est libérée, toutes les ressources internes sont libérées en ordre inverse
  • Cela réduit les risques et simplifie le code lorsqu’on manipule des ensembles complexes de ressources ayant des dépendances
  • Méthodes principales
    • use(value) : ajoute une ressource jetable au sommet de la pile
    • adopt(value, onDispose) : ajoute une ressource non jetable en lui associant un callback de libération
    • defer(onDispose) : ajoute uniquement une action de libération, sans ressource
    • move() : déplace toutes les ressources de la pile courante vers une nouvelle pile, permettant de transférer la propriété
    • dispose(), asyncDispose() : libèrent l’ensemble des ressources de la pile

État du support et moment d’utilisation possible

  • La gestion explicite des ressources est disponible à partir de Chromium 134 et V8 v13.8
  • Une extension future de la compatibilité avec diverses Web API est attendue

4 commentaires

 
cichol 2025-05-18

await using data = await fn()
Le miracle de voir await apparaître à la fois à gauche et à droite

 
tested 2025-05-18

La nouvelle superpuissance de JavaScript : la gestion explicite des ressources

https://typescriptlang.org/docs/handbook/…

 
GN⁺ 2025-05-18
Commentaire Hacker News
  • Cette proposition donne une impression similaire au problème de la « couleur des fonctions ». La distinction entre fonctions synchrones et asynchrones continue d’envahir toutes les fonctionnalités. On peut par exemple voir les cas de Symbol.dispose et Symbol.asyncDispose, ou encore DisposableStack et AsyncDisposableStack. Je suis satisfait que Java ait pris la direction des virtual threads. J’ai l’impression que c’est un choix qui ajoute de la complexité à la JVM afin de réduire la charge des développeurs d’applications, des auteurs de bibliothèques et des débogueurs

    • Je ne suis pas d’accord avec l’idée que masquer l’asynchrone rend le flux du code plus difficile à comprendre. Je veux aussi savoir si une ressource est libérée de manière asynchrone, ou si cela peut être affecté par des éléments externes comme des problèmes réseau

    • Le fait qu’aujourd’hui, dans la plupart des langages, « il soit considéré comme normal d’écrire tout le code en asynchrone » est vraiment agaçant. Purescript me semble être le seul cas où l’on écrit le code avec Eff (effets synchrones) ou Aff (effets asynchrones), puis où l’on choisit au moment de l’appel. La structured concurrency est élégante, mais en pratique, cela ressemble moins à un travail syntaxique pour l’obtenir qu’à un travail destiné à permettre plusieurs gestionnaires de requêtes de haut niveau sur un serveur. Ce n’est finalement qu’un moyen de faciliter le traitement en parallèle

    • Je ne sais pas comment cela a été implémenté sur la JVM, mais en général, le multithreading est une technologie vraiment peu intuitive à manier. Il existe quantité d’ouvrages sur les race conditions, deadlocks, livelocks, famines, problèmes de visibilité mémoire, etc. Comparé à cela, la programmation asynchrone mono-thread est bien moins lourde à porter. Supporter le problème de la couleur des fonctions est un choix moins douloureux que déboguer des « Heisenbugs » dans une application multithread

    • Je suis vraiment content que Java ait fait ce choix

    • L’explication est que l’exécution normale et les fonctions asynchrones forment mutuellement des closed Cartesian categories. La catégorie de l’exécution normale peut être directement plongée dans la catégorie asynchrone. Chaque fonction a une catégorie (autrement dit, une couleur de fonction), et certains langages le montrent de manière plus explicite. C’est un choix de conception du langage, et la théorie des catégories peut être utilisée de façon très puissante bien au-delà du threading. Java et son approche fondée sur les threads se retrouvent confrontés aux problèmes de synchronisation, ce qui est particulièrement difficile. JavaScript, lui, restreint parmi les catégories monadiques en particulier l’approche en Continuation-passing

  • En voyant l’exemple d’usage de using avec une fonction defer, j’ai trouvé cela très rafraîchissant. Cela peut déjà sembler intuitif à beaucoup d’autres personnes, mais je pense que cela mérite d’être mentionné

    • La proposition using inclut DisposableStack et AsyncDisposableStack, ce qui permet de prendre en charge nativement l’enregistrement de callbacks. Comme using est à portée de bloc, c’est nécessaire quand on doit traverser des scopes ou faire un enregistrement conditionnel. Mais les variables using doivent être initialisées immédiatement, un peu comme const, donc une initialisation conditionnelle n’est pas possible. Dans ce cas, il faut créer une Stack au début de la fonction et empiler via defer les ressources utilisées. Si nécessaire, on peut facilement déplacer le moment de la libération au niveau de la fonction

    • Cela fait penser à golang

  • Je trouve que c’est une très bonne idée, mais même si une unification de [Symbol.dispose] et [Symbol.asyncDispose] devient possible à l’avenir pour les streams de Web API et autres, dans un futur proche seules certaines API et bibliothèques prendront en charge cette fonctionnalité, tandis que le reste — la majorité — ne le fera pas. On se retrouve donc face au dilemme de mélanger using et try/catch, ou bien d’écrire directement tout le code avec try/catch pour choisir un code plus facile à comprendre. Il y a donc un risque que cette fonctionnalité acquière une réputation de « pas vraiment utilisable en pratique ». C’est dommage, car même si la conception résout bien un vrai problème, son adoption pourrait être difficile

    • Pour les API qui ne prennent pas ce type de fonctionnalité en charge, on peut appliquer using via DisposableStack. Même lorsqu’on manipule plusieurs ressources à la fois, cela reste bien plus simple qu’avec try/catch. Dès lors que le runtime le supporte, on peut l’utiliser immédiatement sans attendre la mise à jour des ressources existantes

    • Dans le monde JavaScript, cette situation se répète depuis 15 ans. Les nouvelles fonctionnalités du langage arrivent d’abord dans des compilateurs comme Babel, puis entrent dans le spec, et ce n’est souvent qu’au bout de 3 à 4 ans qu’elles atteignent des API stables et les navigateurs. Les développeurs ont de toute façon l’habitude d’envelopper les Web API dans de petits wrappers, et les wrappers sont souvent préférables aux polyfills. Je n’ai jamais pensé d’une nouvelle fonctionnalité utile du langage qu’« elle serait difficile à utiliser »

    • En réalité, beaucoup de fonctionnalités sont déjà implémentées sous forme de polyfills, si bien qu’une grande partie de l’écosystème NodeJS utilise déjà ce modèle, et les utilisateurs se contentent d’adapter la syntaxe via un transpileur. En préparant une présentation sur le sujet l’an dernier, j’ai découvert qu’il existait déjà pas mal d’API avec prise en charge de Symbol.dispose dans NodeJS ou dans les principales bibliothèques. Côté frontend, cela sera sans doute moins utilisé à cause des systèmes de gestion du cycle de vie, mais dans certaines situations cela reste utile. Je pense que cela se diffusera largement dans les bibliothèques de test et dans le backend

    • TC39 devrait aussi se concentrer sur des fonctionnalités plus fondamentales du langage, comme les trait/protocol à la Rust. En Rust, il est relativement facile de définir et d’implémenter un nouveau trait, alors qu’en JS, langage dynamique avec symboles uniques, cela pourrait être introduit bien plus simplement. Il y a des inconvénients comme l’orphan rule, mais cela pourrait évoluer vers une structure bien plus flexible

    • Dans l’univers JavaScript, on résout généralement ce genre de situation avec des polyfills

  • Cela rappelle C#. Via IDisposable et IAsyncDisposable, c’est très utile pour des abstractions comme la gestion de verrous, de files d’attente, ou de scopes temporaires

    • L’auteur de la proposition vient de Microsoft, donc la syntaxe a été définie de manière proche de C#. C’est un contexte cohérent que l’on retrouve aussi dans les issues GitHub associées

    • C’est fondamentalement un design emprunté à C#. La proposition d’origine fait aussi référence au context manager de Python, au try-with-resources de Java et au using statement de C#. Le mot-clé using et les méthodes hook de dispose sont des indices assez clairs

  • Je comprends que JavaScript doive préserver la compatibilité descendante, mais la syntaxe [Symbol.dispose]() me paraît étrange. J’ai presque l’impression qu’il s’agit d’un handle de méthode dans un tableau. Je suis curieux de savoir ce qu’est exactement cette syntaxe

    • Explication : les clés dynamiques entourées de crochets à gauche dans les littéraux d’objet sont utilisées depuis près de 10 ans depuis ES6. De plus, comme un symbole ne peut pas être référencé par une chaîne, on combine clé dynamique et syntaxe courte de méthode. Au fond, ce n’est pas une nouvelle syntaxe

    • Avec une documentation solide à l’appui, cela vient simplement de la manière dont on assigne déjà des clés symboliques à des objets existants. C’est une évolution naturelle

    • D’autres utilisateurs ont déjà expliqué ce que c’est, mais il me semble qu’ils n’ont pas expliqué pourquoi. Utiliser un Symbol comme nom de méthode garantit qu’il s’agit d’une nouvelle API sans conflit avec des méthodes existantes. Cela évite aussi qu’une classe soit traitée par erreur comme disposable

    • Mention du concept de dynamic property access : les propriétés d’un objet sont accessibles soit avec un point (.), soit avec des crochets ([]), et elles prennent en charge à la fois les chaînes et les symboles. Les symboles sont comparés comme des objets uniques, et les well known symbols (symboles spéciaux fréquemment utilisés) tels que Symbol.dispose garantissent l’extensibilité. Une idée comparable aux méthodes __dunder__ de Python est également évoquée

    • Cette syntaxe est utilisée depuis des années déjà. Les itérateurs JavaScript fonctionnent de la même façon, et cela a été introduit il y a près de 10 ans

  • Présentation des raisons pour lesquelles certains travaillent à introduire la structured concurrency en JS, notamment pour la gestion des ressources, en particulier lorsque le lexical scope est une caractéristique importante. Une bibliothèque liée à la structured concurrency a aussi été partagée

  • La fonctionnalité est déjà prise en charge à partir de Bun 1.0.23. On peut l’essayer de manière expérimentale

  • Je ne vois vraiment pas comment on peut comprendre et contrôler le flux d’exécution d’un programme avec un style de code aussi complexe

    • C’est bien là tout le sujet. 90 % du développement web consiste en des mises à niveau inutiles ou que personne ne demande, puis à corriger en 10 % du temps les problèmes que cela crée. Et il y a toujours une faible probabilité que quelqu’un doive relire un vieux code, auquel cas je recommande de laisser le bug comme exercice d’initiation pour une nouvelle recrue. Même des systèmes legacy vieux de 20 ans sont encore utilisés aujourd’hui

    • Le code proposé en exemple contient de graves erreurs de syntaxe et ressemble peu à du vrai JS. Et les développeurs JS n’utilisent pas ce genre de mélange (while, promise chain, finally, etc.) ; en général, on utilise await ou une structure appropriée de gestion des exceptions. Dans une bibliothèque bien conçue, on n’empile pas des couches de handlers de cette manière, et on peut écrire plus simplement avec DisposableStack. De nos jours, il n’est même souvent plus nécessaire d’utiliser une fonction async immédiatement invoquée

    • Quand on travaille professionnellement avec un langage donné, on finit naturellement par comprendre le code à force de s’habituer au sens et au comportement de ses mots-clés. Les programmeurs Haskell s’y habituent de la même manière

    • Sur HN, pour intégrer du code, il faut indenter chaque ligne d’au moins deux espaces. (Je suis d’accord sur le fait que le code est difficile à comprendre)

    • Conseil concis : l’indentation aide

  • Je me demande pourquoi on n’est pas parti sur un destructeur de classe anonyme, ou sur une autre structure que Symbol. Le fait d’avoir deux Symbol (synchrone/asynchrone) pose la question d’une abstraction qui fuit

    • Un destructeur demande un comportement prévisible, avec un cleanup clair, ce qui s’accorde mal avec les garbage collectors évolués. Les langages modernes prennent en charge le cleanup basé sur la portée, avec différentes implémentations : HoF (fonctions d’ordre supérieur), hooks spéciaux, enregistrement de callbacks, etc. Python s’appuyait au départ sur des destructeurs via un GC par comptage de références, mais ses limites ont conduit à l’introduction des context managers

    • Dans d’autres langages, les destructeurs dépendent du moment où passe le GC, donc ils sont peu fiables. À l’inverse, une méthode dispose est appelée de manière explicite à la fin du scope de la variable, ce qui la rend prévisible pour fermer un fichier ou libérer un verrou. Une méthode fondée sur Symbol évite les collisions avec des fonctionnalités existantes, et en général seuls les auteurs de bibliothèques ont besoin de s’en soucier. La distinction synchrone/asynchrone doit être explicite, ce qui peut conduire à une syntaxe un peu inhabituelle comme await using a = await b()

    • Dans un langage à GC, les destructeurs sont difficiles à appeler de manière synchrone et ont donc un comportement majoritairement non déterministe. JS a bien WeakRef et FinalizationRegistry, mais même Mozilla en déconseille l’usage à cause de leur caractère imprévisible

    • L’un des points forts de cette approche est qu’elle peut s’appliquer aussi à des cibles qui ne sont pas des instances de classe

    • JavaScript n’a pas de concept de propriété anonyme, donc la question elle-même paraît ambiguë. Certains affirment qu’il n’existe pas d’autre alternative à cette méthode

  • Le premier exemple de la proposition montre un code qui libère un verrou en toute sécurité via try/finally. Je me demande si ce genre de motif n’est important que dans les situations de longue exécution, et si dans un navigateur ou en CLI, le verrou est aussi libéré lorsqu’une erreur met fin au processus

    • La spécification indique que dispose est exécuté quoi qu’il arrive quand l’exécution du bloc se termine, que ce soit normalement, via une exception, une branche ou une sortie. Autrement dit, même comportement pour using et try/finally. Une terminaison forcée du processus sort du cadre de la spécification, donc ECMAScript n’intervient pas. Le stream de l’exemple est un objet interne à JS, donc si l’interpréteur disparaît, la notion même de verrou perd son sens. S’il s’agit de ressources de l’OS (mémoire, fichiers, etc.), l’OS fait généralement le ménage, mais le comportement dépend de la plateforme

    • Une page web dans un navigateur peut, d’une certaine manière, être considérée comme une application de très longue durée d’exécution. Elle peut même durer plus longtemps qu’un processus serveur. En cas d’erreur, la page ne meurt pas, et la gestion des erreurs, y compris des exceptions, suit des règles précises avec un traitement dans finally. Dans NodeJS, par défaut, une erreur peut entraîner l’arrêt du processus, mais dans un contexte serveur, d’autres traitements sont fréquents. Autrement dit, la fonction de libération sera bien appelée dans finally

 
ahwjdekf 2025-05-18

Jusqu’ici, on vivait très bien sans se soucier le moins du monde de ces histoires de ressources. Qu’est-ce qui te prend, tout à coup ?