Blog ·
Un flux est un graphe, et il doit être fini
Transformer une liste d’étapes en éditeur de nœuds ressemble à un changement d’interface. C’est une décision sur les boucles et sur un clic volé.
Par Provibr
Un flux dans notre panneau fait une seule chose, très étroite : lorsqu’un serveur vient d’être provisionné, il produit du texte. Un courriel de bienvenue, un fragment de configuration, une note pour le ticket. Il ne redémarre aucun hyperviseur et ne parle à aucune API. C’est précisément cette étroitesse qui permet de confier la fonction à un client sans crainte.
Jusqu’à cette ronde, un flux était une liste : étape un, étape deux, étape trois, arrêt à la première défaillance sauf si une case disait le contraire. La demande était d’en faire une toile, avec des nœuds et des fils. Cela ressemble à un autre affichage des mêmes données. Ce n’en est pas un, et l’intéressant tient aux questions qu’une toile oblige à trancher et qu’une liste tranchait déjà pour vous.
La version un n’a pas de boucles, et ce n’est pas de la paresse
Un fil qui revient en arrière est la première chose que tout le monde essaie. Nous le refusons à l’enregistrement, et le message nomme les nœuds qui forment le cycle.
La raison tient à la nature du sous-système et non à la difficulté du code. Un flux de rendu n’a pas d’état d’attente. Pas de nœud de temporisation, pas d’attente d’approbation, aucun correspondant qui rapporte plus tard ; chaque nœud est synchrone et terminé en quelques millisecondes. Une boucle sans état d’attente est donc infinie par construction, et la seule façon de la dompter serait un compteur d’itérations caché : une limite invisible que personne ne voit et qui, le jour où elle est atteinte, coupe une exécution en deux sans que quiconque puisse expliquer pourquoi.
Refuser le cycle à l’enregistrement est la version honnête de cette même limite. Dès qu’un véritable état d’attente existera, l’arbitrage changera et une boucle deviendra une décision de conception avec une raison derrière elle. Aujourd’hui, ce serait un piège.
La ramification est séquentielle pour une raison voisine. Deux fils partant du même port sont suivis l’un après l’autre, dans l’ordre enregistré, et la première branche est parcourue jusqu’au bout avant que la seconde ne commence. Les rendre en parallèle voudrait dire que deux branches écrivent en même temps dans les mêmes variables, et le résultat d’un flux dépendrait alors du rendu qui a fini le premier par hasard. Un rendu coûte des millisecondes : il n’y a rien à gagner et de la reproductibilité à perdre.
Six sortes, et des ports qui doivent être une fonction
Il y a six sortes de nœuds, volontairement peu nombreuses : déclencheur, script, aiguillage, affectation, note et fin. Chaque sorte supplémentaire est une entrée de palette, un panneau de propriétés, neuf traductions, une branche dans l’exécuteur et une ligne dans la table des transitions ; un test dérive donc ces exigences de la liste elle-même. Une septième sorte sans panneau est un test rouge et non un nœud à moitié fonctionnel.
| Sorte | Sorties |
|---|---|
trigger | une, et il y a exactement un déclencheur par graphe |
script | ok et error |
switch | une par règle, plus otherwise |
set | une |
note | aucune, et il n’est jamais visité |
end | aucune, et il énonce le résultat |
La ligne de l’aiguillage explique pourquoi les sorties sont calculées par une fonction et non lues dans une table. Un aiguillage à trois règles a quatre sorties ; une table figée laisserait quelqu’un ajouter une quatrième règle sans qu’une sortie apparaisse, et cette branche disparaîtrait en silence. Tout ce dont la configuration change la forme doit passer par une fonction.
La validation s’exécute dans un ordre fixe et chaque refus nomme son nœud : identifiants en double, zéro ou deux déclencheurs, un chemin de variable hors du catalogue, un motif dangereux, un port inconnu, un fil en double, un cycle, un nœud inatteignable, et enfin plus de nœuds exécutables que la limite de vingt-cinq. L’ordre compte, car le premier refus est celui qui sera lu, et « il y a un cycle ici » aide davantage que « ce port n’existe pas » quand les deux sont vrais.
Écrire dans state. et jamais dans flow.
Un nœud set écrit des variables. L’espace de noms évident est celui qui existe déjà, et c’est le mauvais. flow.name, flow.runId et flow.step portent des faits sur l’exécution ; un nœud autorisé à y écrire laisserait donc un client écraser flow.name, et la piste d’audit mentirait alors sous une forme qui ressemble à une donnée.
set écrit donc dans state., un espace de noms neuf, vide au départ et qui n’existe que le temps d’une exécution. Un gabarit qui lit {{ state.role }} sans qu’aucun nœud l’ait défini échoue comme échoue toute autre variable absente, et c’est bien ce que l’on veut : un mode strict est un meilleur message d’erreur qu’une ligne vide dans un courriel de bienvenue.
La première règle gagne, et la branche que personne n’a prise
Un aiguillage lit un chemin de variable et évalue ses règles de haut en bas. La première règle qui correspond décide du port ; si aucune ne correspond, l’exécution sort par otherwise. Huit opérateurs, et non les trente d’un outil généraliste, tous explicables en une ligne.
Ce qui a compté davantage que l’évaluation, c’est ce qui reste écrit. Chaque nœud visité produit une ligne avec son identifiant, sa sorte et son résultat, et pour un aiguillage ce résultat est le port emprunté et la valeur lue. Cela ne pouvait pas aller dans le champ d’erreur, qui porte la raison pour laquelle quelque chose a échoué ou a été sauté, ni dans l’instantané de contexte, qui est l’entrée d’un rendu et porte une promesse.
Et tout nœud exécutable qui n’a pas été visité produit lui aussi une ligne, avec sa propre raison. « Une étape antérieure a échoué, celle-ci n’a donc pas tourné » et « l’exécution a pris une autre branche, ce nœud n’a donc pas été atteint » sont deux phrases différentes, et les distinguer est exactement ce que l’on fait en lisant une exécution surprenante.
Un motif que l’on ne peut pas interrompre
Un opérateur de l’aiguillage évalue une expression régulière, et c’est la seule pièce réellement dangereuse de la fonction. JavaScript n’offre aucun moyen d’interrompre un RegExp.test(). Il n’y a ni échéance ni interruption, et le moteur revient en arrière : un motif comme (a+)+$ sur quarante caractères bloque un gestionnaire de requêtes pendant des minutes. C’est un déni de service avec un seul champ de saisie.
Le côté agent a résolu le même problème lors d’une ronde antérieure en bornant le moteur, avec une implémentation sans retour arrière et une limite de mémoire. Cette voie n’existe pas ici ; c’est donc le langage qui est borné à un sous-ensemble où la catastrophe ne peut pas s’exprimer : pas de références arrière, pas de lookaround, et hauteur d’étoile un, c’est-à-dire qu’une répétition ne peut pas se trouver dans une répétition. (a+)+ devient impossible et a+b+ reste autorisé. Avec une limite de longueur sur le motif et sur l’entrée, le pire cas est quadratique sur quelques milliers de caractères, ce qu’un cœur moderne fait sans s’en apercevoir.
La vue en liste n’est pas la version moindre
Chaque action de la toile a un bouton dans une vue en liste bâtie sur le même modèle pur : ajouter, sélectionner, déplacer, supprimer, relier et détacher. L’ordre y est topologique, c’est-à-dire l’ordre dans lequel l’exécuteur visite les nœuds, et donc le seul qu’un lecteur sans image puisse suivre ; à une bifurcation, les branches se suivent les unes sous les autres, avec leurs fils écrits en toutes lettres.
Ce n’est pas une annexe d’accessibilité vissée sur un dessin. C’est la raison pour laquelle il n’y a aucune bibliothèque de graphes sous la toile. Une bibliothèque sait rendre les nœuds focalisables et les déplacer aux flèches, mais aucune ne tire un fil au clavier : ce second chemin devait exister de toute façon. Une fois qu’il existe, la bibliothèque ne fait plus que le travail de souris, et à vingt-cinq nœuds c’est la petite part.
Une ligne de code qui mangeait chaque clic
La toile se déplaçait bien, se traînait bien et ne sélectionnait rien. Cliquer sur la sortie ok d’un nœud ne faisait absolument rien, et rien n’était faux dans le DOM : le gestionnaire était en place, l’élément était là, les coordonnées étaient bonnes.
setPointerCapture() sur un ancêtre réoriente tout événement de pointeur suivant vers l’élément capteur, y compris le pointerup. Le navigateur déclenche ensuite le click sur l’ancêtre commun de l’appui et du relâchement, et avec la capture cet ancêtre est le conteneur lui-même. Le gestionnaire de clic du nœud n’est jamais atteint, et aucune erreur n’apparaît nulle part.
La réparation est structurelle et non un interrupteur : le déplacement est désormais accroché à une surface d’arrière-plan placée derrière les nœuds, si bien qu’un appui sur un nœud ne peut plus déclencher la capture.
Il y a une seconde leçon dans la façon dont cela a failli passer inaperçu. La vérification de navigateur pour « un fil a été ajouté » comptait des éléments svg path, et chaque icône de l’interface en est un : le nombre montait qu’un fil apparaisse ou non. Elle compte maintenant les chemins à l’intérieur de la couche des fils, contre un nombre exact.
Ce que nous noterions
- Refuser le cycle plutôt que plafonner les itérations. Une limite cachée est un piège ; un refus à l’enregistrement qui nomme les nœuds fautifs est un message.
- Faire des ports une fonction du nœud. Ce dont la configuration change la forme ne se décrit pas avec une table figée.
- Donner aux variables nouvelles un espace de noms neuf. Laisser la saisie d’un utilisateur écrire sur les champs qui décrivent l’exécution transforme l’audit en récit.
- Consigner la branche non prise, avec sa propre raison. « Non atteinte » et « non exécutée après une défaillance » répondent à des questions différentes.
- Si le moteur ne s’interrompt pas, bornez le langage. Hauteur d’étoile un plus une limite de longueur transforme une explosion exponentielle en arithmétique.
- Bâtissez d’abord le chemin clavier s’il est une exigence. Une fois qu’il existe, on découvre souvent que la bibliothèque ne faisait que la moitié facile.
- La capture de pointeur vole les clics des descendants. Si une toile se traîne bien et ne sélectionne rien, c’est le premier endroit à regarder.
Pour être clair sur ce dont il s’agit : les flux vivent sur l’écran des scripts, volontairement absent du menu latéral aujourd’hui ; ils sont donc accessibles et non mis en avant. Trois choses ne sont pas démontrées. La conversion du graphe vers la liste a été vérifiée dans le code et jamais conduite dans un navigateur. La branche d’installation réseau d’un flux en graphe n’a pas été parcourue de bout en bout. Et une exécution d’essai est synchrone : il n’y a donc aucun événement en direct à émettre.
Issu du travail de plateforme de Provibr. Pourquoi une étape qui n’a rien fait en silence est pire qu’une étape en échec est expliqué dans la migration qui n’a jamais tourné, et comment une vue de progression unique lit les tables des autres sans jamais y écrire est expliqué dans projeter, jamais remplacer.