asynquence:Sliby, které ještě nevíte (část 1)

Toto je vícedílná série blogových příspěvků zdůrazňující schopnosti asynkvence, nástroje pro abstrakci řízení toku založeného na slibech.

  • Část 1:Sliby, které ještě neznáte
  • Část 2:Více než jen sliby

on("before", start)

Normálně jsou mé blogové příspěvky (a tréninkové workshopy, když na to přijde!) určeny k tomu, aby něco naučily, a v tomto procesu vyzdvihuji projekty, které jsem napsal, abych prozkoumal a vyzkoušel v této oblasti. Považuji to za účinnou pomůcku při výuce.

Nicméně tato série blogových příspěvků bude, bez omluvy, o něco zjevněji propagací jednoho z mých nejdůležitějších a nejambicióznějších projektů:asynquence. Základní téma? Sliby a asynchronní řízení toku.

Ale už jsem napsal podrobnou vícedílnou sérii blogových příspěvků, která učí vše o slibech a asynchronních problémech, které řeší. Důrazně doporučuji, abyste si nejprve přečetli tyto příspěvky, pokud hledáte hlubší porozumění tématu, než se pustíte do mých současných blábolů o asynquenci .

Proč tak tvrdě propaguji asynkvenci tady takovým očividným seberohatým způsobem? Protože si myslím, že jedinečně poskytuje přístup k tématu asynchronního řízení toku a slibuje, že jste si neuvědomili, že je potřebujete.

asynkvence není populární rocková hvězda ani o ní nemluví všechny skvělé děti v davu. Nemá tisíce hvězdiček na githubu ani miliony stažení npm. Ale vášnivě věřím, že pokud strávíte nějaký čas zkoumáním toho, co dokáže, a jak to dělá , najdete určitou chybějící jasnost a úlevu od nudy, která nastává s jinými asynchronními nástroji.

Toto je dlouhý příspěvek a v této sérii je více než jeden příspěvek. Je toho hodně, co se může pochlubit. Ujistěte se, že věnujte nějaký čas strávení všeho, co se vám chystám ukázat. Váš kód vám poděkuje... nakonec .

Při maximální velikosti výrazně pod 5 kB (minzipováno) pro vše (včetně volitelných pluginů!), myslím, že uvidíte asynquence na svůj skromný počet bajtů zabírá docela ránu.

Slib nebo abstrakce?

První věc, kterou je třeba poznamenat, je, že navzdory některým podobnostem API je asynkvence vytváří nad sliby abstrakční vrstvu, kterou nazývám sekvence . Odtud pochází ten podivný název:async + sekvence =asynquence .

Sekvence je řada automaticky vytvořených a spoutané sliby. Sliby jsou skryty pod povrchem API, takže je nemusíte vytvářet nebo řetězit v obecných/jednoduchých případech. Je to proto, abyste mohli využít sliby s mnohem menším standardem.

Samozřejmě k integraci asynquence do vašeho projektu může sekvence spotřebovat jak standardní potomovatelný/příslib z nějakého jiného prodeje, tak může také prodat standardní příslib ES6 v kterémkoli kroku sekvence. Takže máte maximální svobodu házet sliby kolem sebe nebo si užívat jednoduchost sekvenčních abstrakcí.

Každý krok sekvence může být libovolně jednoduchý, jako okamžitě splněný slib, nebo libovolně složitý, jako vnořený strom sekvencí atd. asynquence poskytuje širokou škálu abstrakčních pomocníků, které lze vyvolat v každém kroku, jako je gate(..) (stejné jako nativní Promises Promise.all(..) ), který běží 2 nebo více "segmentů" (dílčích kroků) paralelně a čeká na jejich dokončení (v libovolném pořadí), než bude pokračovat v hlavní sekvenci.

Výraz asynchronního řízení toku pro konkrétní úlohu ve vašem programu vytvoříte zřetězením bez ohledu na to, kolik kroků v sekvenci je použitelné. Stejně jako u slibů může každý krok buď uspět (a předat libovolný počet zpráv o úspěchu), nebo může selhat (a předat libovolný počet zpráv o důvodech).

V tomto blogovém příspěvku podrobně popisuji celou řadu omezení vyplývajících z všech máte sliby a zdůvodněte sílu a užitečnost abstrakcí. Tvrdím tam, že asynkvence vás osvobozuje od všech těchto omezení, takže tato série blogových příspěvků takové tvrzení dokazuje.

Základy

Určitě vás víc zajímá vidět kód, než číst, jak se o kódu plácám. Začněme tím, že si ukážeme základy asynquence :

ASQ(function step1(done){
    setTimeout(function(){
        done( "Hello" );
    },100);
})
.then(function step2(done,msg){
    setTimeout(function(){
        done( msg.toUpperCase()) ;
    },100);
})
.gate(
    // these two segments '3a' and '3b' run in parallel!
    function step3a(done,msg) {
        setTimeout(function(){
            done( msg + " World" );
            // if you wanted to fail this segment,
            // you would call `done.fail(..)` instead
        },500);
    },
    function step3b(done,msg) {
        setTimeout(function(){
            done( msg + " Everyone" );
        },300);
    }
)
.then(function step4(done,msg1,msg2){
    console.log(msg1,msg2); // "Hello World"  "Hello Everyone"
})
.or(function oops(err){
    // if any error occurs anywhere in the sequence,
    // you'll get notified here
});

Jen s tímto úryvkem vidíte docela dobré znázornění asynkvence byl původně navržen tak, aby dělal. Pro každý krok je pro vás vytvořen slib a je vám poskytnut spouštěč (který vždy rád nazývám done pro zjednodušení), které stačí zavolat nyní nebo někdy později.

Pokud dojde k chybě nebo chcete-li některý krok selhat, voláním done.fail(..) , zbytek cesty sekvence je opuštěn a všechny obslužné rutiny chyb jsou upozorněny.

Chyby se neztratily

Pokud se vám u příslibů nepodaří zaregistrovat obslužný program chyb, chyba zůstane tiše pohřbena uvnitř příslibu, aby ji mohl nějaký budoucí spotřebitel pozorovat. To spolu s tím, jak funguje řetězení slibů, vede k nejrůznějším zmatkům a nuancím.

Pokud si přečtete tyto diskuse, uvidíte, že tvrdím, že sliby mají model „opt-in“ pro zpracování chyb, takže pokud se zapomenete přihlásit, v tichosti selžete. Tomu říkáme neloajálně "jáma selhání" .

asynkvence obrací toto paradigma a vytváří "jámu úspěchu" . Výchozí chování sekvence je hlásit jakoukoli chybu (úmyslnou nebo náhodnou) v globální výjimce (ve vaší konzoli pro vývojáře), než ji spolknout. Nahlášením v globální výjimce se samozřejmě nevymaže stav sekvencí, takže jej lze programově sledovat i později jako obvykle.

Toto globální hlášení chyb můžete „odhlásit“ jedním ze dvou způsobů:(1) zaregistrujte alespoň jeden or obsluha chyb na sekvenci; (2) zavolejte defer() na sekvenci, což signalizuje, že máte v úmyslu později zaregistrovat obsluhu chyb.

Dále, pokud sekvence A je spotřebován (sloučen do) další sekvence B , A.defer() se automaticky zavolá, čímž se zátěž zpracování chyb přesune na B , přesně jak byste chtěli a očekávali.

Se sliby musíte tvrdě pracovat, abyste se ujistili, že zachytíte chyby, a pokud nedosáhnete, budete zmateni, protože budou skryty rafinovanými a těžko dohledatelnými způsoby. S asynkvencí sekvencí, musíte tvrdě pracovat, abyste NE zachytit chyby. asynkvence usnadňuje a zefektivňuje zpracování vašich chyb.

Zprávy

Se sliby může řešení (úspěch nebo neúspěch) nastat pouze s jednou odlišnou hodnotou. Je na vás, abyste zabalili více hodnot do kontejneru (objekt, pole atd.), pokud potřebujete předat více než jednu hodnotu.

asynkvence předpokládá, že musíte předat libovolný počet parametrů (buď úspěch nebo neúspěch) a automaticky za vás zpracovává zalamování/rozbalování způsobem, který byste přirozeně očekávali:

ASQ(function step1(done){
    done( "Hello", "World" );
})
.then(function step2(done,msg1,msg2){
    console.log(msg1,msg2); // "Hello"  "World"
});

Ve skutečnosti lze zprávy snadno vložit do sekvence:

ASQ( "Hello", "World" )
.then(function step1(done,msg1,msg2){
    console.log(msg1,msg2); // "Hello"  "World"
})
.val( 42 )
.then(function(done,msg){
    console.log(msg); // 42
});

Kromě vkládání zpráv o úspěchu do sekvence můžete také vytvořit automaticky neúspěšnou sekvenci (tj. zprávy, které jsou důvodem chyby):

// make a failed sequence!
ASQ.failed( "Oops", "My bad" )
.then(..) // will never run!
.or(function(err1,err2){
    console.log(err1,err2); // "Oops"  "My bad"
});

Problém se zastavením

U slibů, pokud máte řekněme 4 zřetězené sliby a v kroku 2 se rozhodnete, že nechcete, aby se 3 a 4 objevily, máte jedinou možnost – vyvolat chybu. Někdy to dává smysl, ale častěji je to spíše omezující.

Pravděpodobně byste rádi mohli zrušit jakýkoli slib. Pokud však může být samotný slib zvenčí zrušen/zrušen, ve skutečnosti to porušuje důležitý princip důvěryhodně externě neměnného stavu.

var sq = ASQ(function step1(done){
    done(..);
})
.then(function step2(done){
    done.abort();
})
.then(function step3(done){
    // never called
});

// or, later:
sq.abort();

Zrušení/zrušení by nemělo existovat na úrovni příslibu, ale v abstrakci na vrstvě nad nimi. Takže, asynquence umožňuje volat abort() na sekvenci nebo v libovolném kroku sekvence na spouštěči. V rámci možností bude zbytek sekvence zcela opuštěn (vedlejším efektům asynchronních úloh samozřejmě nelze zabránit!).

Kroky synchronizace

Přestože je velká část našeho kódu asynchronní povahy, vždy existují úlohy, které jsou v zásadě synchronní. Nejběžnějším příkladem je provádění úlohy extrakce nebo transformace dat uprostřed sekvence:

ASQ(function step1(done){
    done( "Hello", "World" );
})
// Note: `val(..)` doesn't receive a trigger!
.val(function step2(msg1,msg2){
    // sync data transformation step
    // `return` passes sync data messages along
    // `throw` passes sync error messages along
    return msg1 + " " + msg2;
})
.then(function step3(done,msg){
    console.log(msg); // "Hello World"
});

val(..) metoda step automaticky posune příslib pro daný krok poté, co return (nebo throw za chyby!), takže vám to nespustí spoušť. Používáte val(..) pro jakýkoli synchronní krok uprostřed sekvence.

Zpětná volání

Zejména v node.js jsou zpětná volání (ve stylu chyba-první) normou a sliby jsou novým dítětem v bloku. To znamená, že je téměř jistě integrujete do kódu asynchronních sekvencí. Když zavoláte nějaký nástroj, který očekává zpětné volání stylu první chyby, asynquence poskytuje errfcb() vytvořit pro vás jeden, automaticky zapojený do vaší sekvence:

ASQ(function step1(done){
    // `done.errfcb` is already an error-first
    // style callback you can pass around, just like
    // `done` and `done.fail`.
    doSomething( done.errfcb );
})
.seq(function step2(){
    var sq = ASQ();

    // calling `sq.errfcb()` creates an error-first
    // style callback you can pass around.
    doSomethingElse( sq.errfcb() );

    return sq;
})
.then(..)
..

Poznámka: done.errfcb a sq.errfcb() liší se tím, že první je již vytvořen, takže nemusíte () vyvolejte ji, zatímco posledně jmenovaný musí být zavolán, aby se v daném okamžiku provedlo zpětné volání připojené k sekvenci.

Některé další knihovny poskytují metody pro zabalení volání jiných funkcí, ale to se zdá příliš rušivé pro asynqueni filozofie designu. Chcete-li tedy vytvořit obal metody produkující sekvenci, vytvořte si vlastní, například takto:

// in node.js, using `fs` module,
// make a suitable sequence-producing
// wrapper for `fs.write(..)`
function fsWrite(filename,data) {
    var sq = ASQ();
    fs.write( filename, data, sq.errfcb() );
    return sq;
}

fsWrite( "meaningoflife.txt", "42" )
.val(function step2(){
    console.log("Phew!");
})
.or(function oops(err){
    // file writing failed!
});

Sliby, sliby

asynkvence by měl být dostatečně dobrý v asynchronním řízení toku, že pro téměř všechny vaše potřeby je to veškerý nástroj, který potřebujete. Ale realita je taková, že samotné sliby se ve vašem programu stále objeví. asynkvence usnadňuje přechod od slibu k sekvenci ke slibu, jak uznáte za vhodné.

var sq = ASQ()
.then(..)
.promise( doTaskA() )
.then(..)
..

// doTaskB(..) requires you to pass
// a normal promise to it!
doTaskB( sq.toPromise() );

promise(..) spotřebovává jeden nebo více standardních potomků/slibů prodaných odjinud (např. uvnitř doTaskA() ) a zapojí jej do sekvence. toPromise() prodává nový slib rozvětvený od tohoto bodu v sekvenci. Všechny toky zpráv o úspěchu a chybách proudí do a ze slibů přesně tak, jak byste očekávali.

Sekvence + sekvence

Další věcí, kterou téměř jistě zjistíte, že děláte pravidelně, je vytváření více sekvencí a jejich propojení dohromady.

Například:

var sq1 = doTaskA();
var sq2 = doTaskB();
var sq3 = doTaskC();

ASQ()
.gate(
    sq1,
    sq2
)
.then( sq3 )
.seq( doTaskD )
.then(function step4(done,msg){
    // Tasks A, B, C, and D are done
});

sq1 a sq2 jsou samostatné sekvence, takže je lze zapojit přímo jako gate(..) segmenty nebo jako then(..) kroky. Je zde také seq(..) který může buď přijmout sekvenci, nebo častěji funkci, kterou zavolá, aby vytvořil sekvenci. Ve výše uvedeném úryvku function doTaskD(msg1,..) { .. return sq; } by byl obecný podpis. Přijímá zprávy z předchozího kroku (sq3 ) a očekává se, že vrátí novou sekvenci jako krok 3.

Poznámka: Toto je další cukr rozhraní API, kde je asynquence může zářit, protože s řetězem slibů, abys dal další slib, musíš udělat to ošklivější:

pr1
.then(..)
.then(function(){
    return pr2;
})
..

Jak je vidět výše, asynquence pouze přijímá sekvence přímo do then(..) , jako:

sq1
.then(..)
.then(sq2)
..

Samozřejmě, pokud zjistíte, že potřebujete ručně zapojit v sekvenci, můžete tak učinit pomocí pipe(..) :

ASQ()
.then(function step1(done){
    // pipe the sequence returned from `doTaskA(..)`
    // into our main sequence
    doTaskA(..).pipe( done );
})
.then(function step2(done,msg){
    // Task A succeeded
})
.or(function oops(err){
    // errors from anywhere, even inside of the
    // Task A sequence
});

Jak byste rozumně očekávali, ve všech těchto variantách jsou proudy zpráv o úspěchu i chybách přenášeny potrubím, takže se chyby šíří až do nejvzdálenější sekvence přirozeně a automaticky. To vám však nebrání v ručním naslouchání a zpracování chyb na jakékoli úrovni dílčí sekvence.

ASQ()
.then(function step1(done){
    // instead of `pipe(..)`, manually send
    // success message stream along, but handle
    // errors here
    doTaskA()
    .val(done)
    .or(function taskAOops(err){
        // handle Task A's errors here only!
    });
})
.then(function step2(done,msg){
    // Task A succeeded
})
.or(function oops(err){
    // will not receive errors from Task A sequence
});

Vidličky> Lžíce

Možná budete muset rozdělit jednu sekvenci do dvou samostatných cest, takže fork() je poskytováno:

var sq1 = ASQ(..).then(..)..;

var sq2 = sq1.fork();

sq1.then(..)..; // original sequence

sq2.then(..)..; // separate forked sequence

V tomto úryvku sq2 nebude pokračovat jako samostatná rozvětvená sekvence, dokud nebudou (úspěšně) dokončeny kroky předem rozvětvené sekvence.

Sugary abstrakce

Dobře, to je to, co potřebujete vědět o základním jádru asynquence . I když je tam docela dost výkonu, je stále dost omezený ve srovnání se seznamem funkcí utilit, jako je „Q“ a „async“. Naštěstí asynquence má v rukávu mnohem víc.

Kromě asynkvence core, můžete také použít jeden nebo více z poskytnutých asynquence-contrib pluginy, které do mixu přidávají spoustu chutných pomocníků pro abstrakci. Tvůrce příspěvků vám umožňuje vybrat si, které chcete, ale všechny je zabuduje do contrib.js balíček ve výchozím nastavení. Ve skutečnosti si dokonce můžete docela snadno vytvořit své vlastní pluginy, ale o tom budeme diskutovat v dalším příspěvku v této sérii.

Varianty brány

Existuje 6 jednoduchých variant jádra gate(..) / all(..) funkce poskytované jako pluginy contrib:any(..) , first(..) , race(..) , last(..) , none(..) a map(..) .

any(..) čeká na dokončení všech segmentů stejně jako gate(..) , ale pouze jeden z nich musí být úspěšný, aby hlavní sekvence mohla pokračovat. Pokud nic neuspěje, hlavní sekvence se nastaví do chybového stavu.

first(..) čeká pouze na první úspěšný segment, než uspěje hlavní sekvence (následující segmenty jsou pouze ignorovány). Pokud nic neuspěje, hlavní sekvence se nastaví do chybového stavu.

race(..) je konceptem identický s nativním Promise.race(..) , což je něco jako first(..) , kromě toho, že se závodí o první dokončení bez ohledu na úspěch nebo neúspěch.

last(..) čeká na dokončení všech segmentů, ale do hlavní sekvence jsou odesílány pouze zprávy o úspěchu posledního úspěšného segmentu (pokud existují), aby bylo možné pokračovat. Pokud nic neuspěje, hlavní sekvence se nastaví do chybového stavu.

none(..) čeká na dokončení všech segmentů. Poté transponuje stavy úspěchu a chyby, což má za následek, že hlavní sekvence pokračuje pouze v případě, že všechny segmenty selhaly, ale je chybná, pokud některý nebo všechny segmenty uspěly.

map(..) je asynchronní "mapová" utilita, podobně jako ji najdete v jiných knihovnách/utilitách. Vyžaduje pole hodnot a funkci pro volání proti každé hodnotě, ale předpokládá, že mapování může být asynchronní. Důvod, proč je uveden jako gate(..) varianta je, že volá všechna mapování paralelně a čeká, až se všechna dokončí, než bude pokračovat. map(..) může mít buď pole nebo zpětné volání iterátoru nebo obojí přímo, nebo jako zprávy z předchozího kroku hlavní sekvence.

ASQ(function step1(done){
    setTimeout(function(){
        done( [1,2,3] );
    });
})
.map(function step2(item,done){
    setTimeout(function(){
        done( item * 2 );
    },100);
})
.val(function(arr){
    console.log(arr); // [2,4,6]
});

Krokové varianty

Jiné pluginy poskytují variace na normální krokovou sémantiku, jako je until(..) , try(..) a waterfall(..) .

until(..) opakuje krok, dokud neuspěje, nebo zavoláte done.break() zevnitř (což spouští chybový stav v hlavní sekvenci).

try(..) pokusí se o krok a bez ohledu na to pokračuje s úspěchem v sekvenci. Pokud je zachycena chyba/selhání, předá se jako zvláštní zpráva o úspěchu ve tvaru { catch: .. } .

waterfall(..) trvá několik kroků (jako by to bylo poskytnuto then(..) volání) a zpracovává je postupně. Převádí však zprávy o úspěchu z každého kroku do dalšího, takže po dokončení vodopádu jsou všechny zprávy o úspěchu předány do následujícího kroku. Ušetří vám to ruční sbírání a předávání, což může být docela únavné, pokud máte k vodopádu mnoho kroků.

Abstrakce vyššího řádu

Jakákoli abstrakce, kterou si dokážete představit, může být vyjádřena jako kombinace výše uvedených utilit a abstrakcí. Pokud máte společnou abstrakci, kterou děláte pravidelně, můžete ji učinit opakovaně použitelnou vložením do vlastního pluginu (opět pojednáno v dalším příspěvku).

Jedním příkladem by bylo poskytnutí časových limitů pro sekvenci pomocí race(..) (vysvětleno výše) a failAfter(..) plugin (který, jak to zní, vytváří sekvenci, která po určité prodlevě selže):

ASQ()
.race(
    // returns a sequence for some task
    doSomeTask(),
    // makes a sequence that will fail eventually
    ASQ.failAfter( 2000, "Timed Out!" )
)
.then(..)
.or(..);

Tento příklad nastavuje závod mezi normální sekvencí a sekvencí, která nakonec selže, aby poskytla sémantiku limitu časového limitu.

Pokud jste zjistili, že to děláte pravidelně, můžete snadno vytvořit timeoutLimit(..) plugin pro výše uvedenou abstrakci (viz další příspěvek).

Funkční (pole) operace

Všechny výše uvedené příklady vytvořily jeden základní předpoklad, a to, že předem přesně víte, jaké jsou vaše kroky řízení toku.

Někdy však musíte reagovat na různé množství kroků, například každý krok představuje požadavek na zdroj, kde možná budete muset požádat o 3 nebo 30.

Pomocí několika velmi jednoduchých operací funkčního programování, jako je Array map(..) a reduce(..) , této flexibility můžeme snadno dosáhnout pomocí promis, ale zjistíte, že API cukr asynquence dělá takové úkoly ještě hezčí .

Poznámka: Pokud o map/reduce ještě nevíte, budete chtít strávit nějaký čas (mělo by to trvat jen několik hodin) jejich naučením, protože jejich užitečnost zjistíte v celém kódování založeném na slibech!

Funkční příklad

Řekněme, že chcete požádat o 3 (nebo více) souborů paralelně, vykreslit jejich obsah co nejdříve, ale ujistěte se, že se stále vykreslují v přirozeném pořadí. Pokud se soubor1 vrátí před soubor2, okamžitě vykreslete soubor1. Pokud se však nejprve vrátí soubor2, počkejte na soubor1 a poté vykreslete oba.

Zde je návod, jak to můžete udělat s běžnými sliby (pro účely zjednodušení budeme ignorovat zpracování chyb):

function getFile(file) {
    return new Promise(function(resolve){
        ajax(file,resolve);
    });
}

// Request all files at once in "parallel" via `getFile(..)`
[ "file1", "file2", "file3" ]
.map(getFile)
.reduce(
    function(chain,filePromise){
        return chain
            .then(function(){
                return filePromise;
            })
            .then(output);
    },
    Promise.resolve() // fulfilled promise to start chain
)
.then(function() {
    output("Complete!");
});

Není to tak špatné, pokud analyzujete, co se děje s map(..) a poté reduce(..) . map(..) call promění pole řetězců na pole příslibů. reduce(..) call "redukuje" řadu příslibů na jediný řetězec příslibů, který provede kroky v požadovaném pořadí.

Nyní se podívejme na to, jak asynkvence může udělat stejný úkol:

function getFile(file) {
    return ASQ(function(done){
        ajax(file,done);
    });
}

ASQ()
.seq.apply(null,
    [ "file1", "file2", "file3" ]
    .map(getFile)
    .map(function(sq){
        return function(){
            return sq.val(output);
        };
    })
)
.val(function(){
    output("Complete!");
});

Poznámka: Jedná se o volání synchronizačních map, takže použití asynquence nemá žádný skutečný přínos asynchronní map(..) plugin zmíněný výše.

Kvůli určitému množství cukru API asynquence , můžete vidět, že nepotřebujeme reduce(..) , používáme pouze dva map(..) hovory. První změní pole řetězců na pole sekvencí. Druhý změní pole sekvencí na pole funkcí, z nichž každá vrátí podsekvenci. Toto druhé pole je odesláno jako parametry do seq(..) volání v asynkvenci , který zpracovává každou dílčí sekvenci v pořadí.

Snadné jako dort , že?

.summary(..)

Myslím, že pokud jste dočetli až sem, asynquence mluví sám za sebe. Je výkonná, ale je také velmi stručná a ve srovnání s jinými knihovnami a zejména ve srovnání s původními přísliby zřetelně postrádá standardní strukturu.

Je také rozšiřitelný (pomocí zásuvných modulů, jak se bude týkat následující příspěvek), takže nemáte prakticky žádné limity v tom, co můžete udělat za vás.

Doufám, že jste přesvědčeni, abyste dali alespoň asynquence zkuste to teď.

Ale pokud abstrakce slibů a cukr API byly všechny asynkvence mohl nabídnout, nemusí zjevně zastínit své mnohem známější vrstevníky. Další příspěvek půjde daleko za hranice slibů a bude se věnovat mnohem pokročilejším asynchronním funkcím. Pojďme zjistit, jak hluboko králičí nora sahá.