Blog ·
Een flow is een graaf, en die moet eindig zijn
Een stappenlijst een node-editor maken lijkt een UI-wijziging. Het is een besluit over lussen, over takken die niemand nam, en over één gestolen klik.
Door Provibr
Een flow in ons paneel doet één smalle ding: zodra een server klaar is met uitrollen, rendert hij tekst. Een welkomstmail, een stukje configuratie, een notitie voor het ticket. Hij herstart geen hypervisor en hij praat niet met een API. Precies die smalheid is de reden dat je de functie veilig aan een klant kunt geven.
Tot deze ronde was een flow een lijst: stap één, stap twee, stap drie, stoppen bij de eerste mislukking tenzij een vinkje iets anders zei. Het verzoek was er een canvas van te maken, met knopen en draden. Dat klinkt als een andere weergave van dezelfde gegevens. Dat is het niet, en het interessante zit in de vragen die een canvas je laat beantwoorden en die een lijst al voor je beantwoordde.
Versie één kent geen lussen, en dat is geen gemakzucht
Een draad die terugloopt is het eerste wat iedereen probeert. Wij weigeren hem bij het opslaan, met de knopen die de lus vormen erbij genoemd in de foutmelding.
De reden ligt in de aard van het subsysteem en niet in de moeilijkheid van de code. Een renderflow heeft geen wachtstand. Er is geen vertragingsknoop, geen wachten op goedkeuring, geen tegenpartij die later iets meldt; elke knoop is synchroon en klaar binnen een handvol milliseconden. Een lus zonder wachtstand is daarmee per constructie oneindig, en de enige manier om hem te temmen is een verborgen iteratieteller: een onzichtbare grens die de gebruiker niet kan zien en die op de dag dat hij geraakt wordt een run halverwege afbreekt zonder dat iemand kan uitleggen waarom.
De cyclus weigeren bij het opslaan is de eerlijke versie van diezelfde grens. Zodra er een echte wachtstand bestaat verandert de afweging en is een lus een ontwerpbeslissing met een reden eronder. Vandaag zou het een valstrik zijn.
Vertakken gebeurt om een verwante reden sequentieel. Twee draden vanaf dezelfde poort worden de een na de ander gevolgd, in opgeslagen volgorde, en de eerste tak wordt helemaal afgelopen voordat de tweede begint. Ze parallel draaien zou betekenen dat twee takken tegelijk in dezelfde variabelen schrijven, en dan hangt de uitkomst van een flow af van welke render toevallig eerst klaar was. Renderen duurt milliseconden, dus er valt niets te winnen en reproduceerbaarheid te verliezen.
Zes soorten, en poorten die een functie moeten zijn
Er zijn zes knoopsoorten, met opzet weinig: trigger, script, switch, set, notitie, einde. Elke soort die erbij komt is een palettekst, een eigenschappenpaneel, negen vertalingen, een tak in de uitvoerder en een rij in de overgangstabel, dus de lijst wordt bewaakt door een test die die eisen eruit afleidt. Een zevende soort zonder paneel is een rode test in plaats van een half werkende knoop.
| Soort | Uitgangen |
|---|---|
trigger | één, en er is precies één trigger per graaf |
script | ok en error |
switch | één per regel, plus otherwise |
set | één |
note | geen, en hij wordt nooit bezocht |
end | geen, en hij spreekt de uitkomst uit |
De switch-rij is de reden dat de uitgangen door een functie berekend worden en niet uit een tabel komen. Een switch met drie regels heeft vier uitgangen; een vaste tabel zou iemand een vierde regel laten toevoegen zonder dat er een uitgang bijkwam, en dan verdwijnt die tak stil. Alles wat je aan een knoop kunt toevoegen moet zijn vorm kunnen veranderen.
De validatie draait in een vaste volgorde en elke fout wijst zijn knoop aan: dubbele id's, nul of twee triggers, een variabelepad buiten de catalogus, een onveilig patroon, een onbekende poort, een dubbele draad, een cyclus, een onbereikbare knoop, en ten slotte meer uitvoerbare knopen dan de grens van vijfentwintig. De volgorde telt, want de eerste weigering is de weigering die de gebruiker leest, en "hier zit een lus" helpt meer dan "deze poort bestaat niet" wanneer allebei waar zijn.
Schrijven in state. en nooit in flow.
Een set-knoop schrijft variabelen. De voor de hand liggende naamruimte is die welke al bestaat, en dat is de verkeerde. flow.name, flow.runId en flow.step dragen feiten over de run, dus een knoop die daar mocht schrijven zou een klant flow.name laten overschrijven, en dan liegt de auditrij op een manier die eruitziet als gegevens.
Daarom schrijft set in state., een naamruimte die nieuw is, leeg bij de start, en alleen bestaat zolang een run duurt. Een sjabloon dat {{ state.role }} leest terwijl geen enkele knoop hem zette faalt zoals elke andere ontbrekende variabele faalt, en dat is precies wat je wilt: een strikte modus is een betere foutmelding dan een lege regel in een welkomstmail.
De eerste regel wint, en de tak die niemand nam
Een switch leest één variabelepad en toetst zijn regels van boven naar beneden. De eerste regel die past bepaalt de poort; past er geen, dan verlaat de run hem via otherwise. Acht operatoren, niet de dertig die een algemeen gereedschap aanbiedt, en alle acht zijn in één regel uit te leggen.
Wat meer telde dan het toetsen zelf is wat er wordt opgeschreven. Elke bezochte knoop levert een rij op met zijn id, zijn soort en zijn uitkomst, en bij een switch is die uitkomst welke poort genomen werd en welke waarde er gelezen is. Dat kon niet in het foutveld, dat de reden draagt dat iets faalde of oversloeg, en ook niet in de contextmomentopname, die de invoer van een render is en waar een belofte aan hangt.
En elke uitvoerbare knoop die niet bezocht is levert óók een rij op, met zijn eigen reden. "Een eerdere stap faalde, dus deze is niet gedraaid" en "de run nam een andere tak, dus deze knoop is niet bereikt" zijn twee verschillende zinnen, en die uit elkaar houden is precies wat je doet wanneer je een run leest die je verbaasde.
Een patroon dat je niet kunt afbreken
Eén switch-operator toetst een reguliere expressie, en dat is het enige werkelijk gevaarlijke onderdeel van de functie. JavaScript kent geen manier om een RegExp.test() af te breken. Er is geen deadline en geen interrupt, en de motor backtrackt, dus een patroon als (a+)+$ op veertig tekens laat een verzoekafhandelaar minutenlang hangen. Dat is een denial of service met één invoerveld.
De agentkant loste hetzelfde probleem in een eerdere ronde op door de motor te begrenzen, met een implementatie zonder backtracking en met een geheugengrens. Die weg bestaat hier niet, dus wordt in plaats daarvan de taal begrensd tot een deelverzameling waarin de ramp niet uitgedrukt kan worden: geen terugverwijzingen, geen lookaround, en sterhoogte één, wat betekent dat een herhaling niet in een herhaling mag staan. (a+)+ wordt daarmee onmogelijk en a+b+ blijft gewoon toegestaan. Met een lengtegrens op het patroon en op de invoer is het ergste geval kwadratisch in een paar duizend tekens, en dat doet een moderne kern zonder het te merken.
De lijstweergave is niet de mindere
Elke handeling die het canvas kent heeft een knop in een lijstweergave die uit hetzelfde pure model komt: toevoegen, selecteren, verplaatsen, verwijderen, verbinden en losmaken. De volgorde daar is topologisch, en dat is de volgorde waarin de uitvoerder de knopen bezoekt en dus de enige die een lezer zonder plaatje kan volgen; bij een vertakking staan de takken onder elkaar, met hun draden als zinnen eronder.
Dat is geen toegankelijkheidsbijlage die aan een tekening geschroefd is. Het is de reden dat er onder het canvas helemaal geen graafbibliotheek ligt. Een bibliotheek kan knopen focusbaar maken en ze met pijltjes verplaatsen, maar geen enkele bibliotheek trekt een draad met een toetsenbord, dus die tweede weg moest er hoe dan ook komen. Zodra hij er is doet de bibliotheek alleen nog het muiswerk, en bij vijfentwintig knopen is dat het kleine deel.
Eén regel code die elke klik opat
Het canvas pande goed, sleepte goed, en selecteerde niets. Klikken op de uitgang ok van een knoop deed helemaal niets, en er was niets mis in de DOM: de handler zat eraan, het element stond er, de coördinaten klopten.
setPointerCapture() op een voorouder hertarget elke volgende pointergebeurtenis naar het vangende element, inclusief de pointerup. De browser vuurt click daarna af op de gemeenschappelijke voorouder van neer en op, en met een vangst is dat de container zelf. De eigen klikhandler van de knoop wordt nooit bereikt, en er komt nergens een fout uit.
De reparatie is structureel en geen vlag: pannen hangt nu aan een achtergrondvlak dat achter de knopen ligt, zodat een pointerdown op een knoop het vangen niet meer kan starten.
Er zit een tweede les in hoe bijna dit gemist werd. De browserproef voor "er is een draad bijgekomen" telde svg path-elementen, en elk pictogram in de interface is er een, dus het getal ging omhoog of er nu een draad verscheen of niet. Hij telt nu paden binnen de dradenlaag, tegen een exact aantal.
Wat wij zouden opschrijven
- Weiger de cyclus in plaats van de iteraties te begrenzen. Een verborgen grens is een valstrik; een weigering bij het opslaan met de schuldige knopen erbij is een mededeling.
- Maak poorten een functie van de knoop. Alles waarvan de configuratie de vorm verandert is niet met een vaste tabel te beschrijven.
- Geef nieuwe variabelen een nieuwe naamruimte. Gebruikersinvoer over de velden laten schrijven die de run beschrijven maakt van je auditrij een verhaal.
- Leg de tak vast die niet genomen is, met zijn eigen reden. "Niet bereikt" en "niet gedraaid na een fout" beantwoorden verschillende vragen.
- Kun je de motor niet afbreken, begrens dan de taal. Sterhoogte één plus een lengtegrens maakt van een exponentiële explosie een rekensom.
- Bouw de toetsenbordweg eerst als hij een eis is. Zodra hij er is ontdek je vaak dat de bibliotheek alleen de makkelijke helft deed.
- Pointer capture steelt kliks van afstammelingen. Sleept een canvas prima en selecteert het niets, kijk daar dan als eerste.
Voor de duidelijkheid over wat dit is: flows leven op het scriptsscherm, dat vandaag met opzet niet in het zijmenu staat, dus dit is bereikbaar en niet aangeprezen. Drie dingen zijn onbewezen. Het omzetten van graaf terug naar lijst is in code getoetst en nooit in een browser gedreven. De tak voor een netwerkinstallatie van een graafflow is niet end-to-end doorlopen. En een proefrun is synchroon, dus er is geen live gebeurtenis die hij kan uitzenden.
Uit het platformwerk van Provibr. Waarom een stap die stil niets deed erger is dan een stap die faalde staat in de migratie die nooit gedraaid heeft, en hoe één voortgangsoverzicht andermans tabellen leest zonder er ooit in te schrijven staat in projecteren, nooit vervangen.