Os capΓtulos 1 e 2 trouxeram em detalhes padrΓ΅es tΓpicos da programação assΓncrona e como estes se baseiam em callbacks. Mas tambΓ©m vimos que callbacks sΓ£o fatalmente limitados em termos de capacidade, o que nos levou aos capΓtulos 3 e 4, com Promises e generators oferecendo uma base muito mais sΓ³lida, confiΓ‘vel e compreensΓvel para construção de sua assincronia.
Referenciei minha prΓ³pria biblioteca assΓncrona asynquence (http://github.com/getify/asynquence) -- "async" + "sequence" (sequΓͺncia) = "asynquence" -- diversas vezes neste livro, e agora gostaria de explicar brevemente como ela funciona e por que a forma ΓΊnica com que foi projetada Γ© importante e ΓΊtil.
No prΓ³ximo apΓͺndice exploraremos alguns padrΓ΅es async
avanΓ§ados, mas vocΓͺ provavelmente irΓ‘ querer uma biblioteca para tornΓ‘-los palatΓ‘veis o suficiente para serem ΓΊteis. Utilizaremos asynquence para expressar estes padrΓ΅es, portanto vocΓͺ irΓ‘ querer passar algum tempo por aqui para conhecΓͺ-la antes de mais nada.
asynquence obviamente não é a única opção para boas implementaçáes async
; certamente existem Γ³timas bibliotecas por aΓ. Mas asynquence oferece uma perspectiva ΓΊnica ao combinar o melhor destes padrΓ΅es em uma ΓΊnica biblioteca e, alΓ©m disso, foi criada sobre uma ΓΊnica abstração: a sequΓͺncia (assΓncrona).
Minha premissa Γ© que programas JS sofisticados necessitam, com frequΓͺncia, de porçáes de diversos padrΓ΅es assΓncronos entrelaΓ§ados, e isso normalmente fica a cargo de cada desenvolvedor(a) descobrir. Em vez de incluirmos duas ou mais bibliotecas diferentes que focam em diferentes aspectos da assincronia, asynquence unifica-as em uma sequΓͺncia variada de passos, com uma ΓΊnica biblioteca para aprender e implantar.
Acredito no valor agregado por asynquence na facilidade de se obter uma semΓ’ntica de programação baseada em Promises para controle de fluxo assΓncrono e Γ© por isso que focaremos exclusivamente nesta biblioteca aqui.
Para comeΓ§ar, vou explicar os princΓpios por trΓ‘s de asynquence e entΓ£o nΓ³s ilustraremos o funcionamento de sua API com exemplos de cΓ³digo.
A compreensΓ£o de asynquence inicia com a compreensΓ£o de uma abstração fundamental: qualquer sΓ©rie de passos para execução de uma tarefa, sejam eles individualmente sΓncronos ou assΓncronos, podem, coletivamente, ser pensados como uma "sequΓͺncia". Em outras palavras, uma sequΓͺncia Γ© um container que representa uma tarefa e Γ© constituΓdo por passos individuais (potencialmente assΓncronos) para completΓ‘-la.
Cada passo na sequΓͺncia Γ© controlado internamente por Promises (veja CapΓtulo 3). Isto Γ©, cada passo que vocΓͺ adiciona Γ sequΓͺncia cria implicitamente uma Promise que esta ligada ao seu (antigo) ΓΊltimo passo. Por conta da semΓ’ntica de Promises, cada avanΓ§o de passos em uma sequΓͺncia Γ© assΓncrono, mesmo se este passo for completado de forma sΓncrona.
AlΓ©m disso, uma sequΓͺncia sempre avanΓ§arΓ‘ linearmente de passo em passo, de modo que o passo 2 sempre vem apΓ³s o tΓ©rmino do passo 1 e assim por diante.
Obviamente, Γ© possΓvel criar uma nova sequΓͺncia a partir da bifurcação de uma sequΓͺncia existente, de modo que a nova sequΓͺncia somente iniciarΓ‘ no momento que a sequΓͺncia principal atingir o ponto de bifurcação do fluxo. SequΓͺncias tambΓ©m podem ser combinadas de vΓ‘rias formas, inclusive incluir uma sequΓͺncia em outra em algum ponto do fluxo.
Uma sequΓͺncia Γ© como uma cadeia de Promises. PorΓ©m, em uma cadeia de Promises nΓ£o temos uma "alΓ§a" para nos segurarmos que referencie a cadeia por completo. Qualquer Promise para a qual vocΓͺ possua uma referΓͺncia representa apenas o passo atual na cadeia e mais alguns passos subsequentes. Essencialmente vocΓͺ nΓ£o pode ter uma referΓͺncia para uma cadeia de Promises a nΓ£o ser que vocΓͺ referencie a primeira Promise da cadeia.
Existem muitos casos em que torna-se ΓΊtil ter esta referΓͺncia para a sequΓͺncia como um todo, como em situaçáes de interrupção/cancelamento. Conforme cobrimos extensivamente no CapΓtulo 3, Promises em si nΓ£o devem nunca ser canceladas pois isto viola um princΓpio imperativo fundamental: imutabilidade externa.
Mas sequΓͺncias nΓ£o possuem este princΓpio de imutabilidade por definição, muito pelo fato de nΓ£o serem enviadas de um lado para o outro como containers de valores futuros que carecem de uma semΓ’ntica de imutabilidade. Portanto sequΓͺncias representam um nΓvel de abstração adequado para manipulação de comportamentos relacionados Γ interrupçáes/cancelamentos. SequΓͺncias asynquence podem ser abort()
adas a qualquer momento e a sequΓͺncia serΓ‘ interrompida no ponto que estiver e nΓ£o irΓ‘ adiante por nenhuma razΓ£o.
Existem muitas outras razΓ΅es para se preferir a sequΓͺncia como abstração em relação Γ corrente de Promises para controle de fluxo.
Primeiramente, o processo de encadeamento de Promises Γ© bastante manual -- e pode tornar-se bastante tedioso assim que vocΓͺ comeΓ§a a criar e encadear Promises por uma faixa muito ampla de seus programas -- e este tΓ©dio pode tornar-se improdutivo ao dissuadir o(a) desenvolvedor(a) de utilizar Promises em locais onde seria bastante apropriado.
Abstraçáes tem por objetivo reduzir repetição de cΓ³digo e tΓ©dio, portanto a sequΓͺncia como abstração Γ© uma boa solução para este problema. Com Promises, seu foco Γ© no passo individual e nΓ£o se assume que uma corrente serΓ‘ formada. Uma abordagem oposta Γ© tomada no caso das sequΓͺncias, onde assumimos que esta possuirΓ‘ mais passos indefinidamente.
A redução de complexidade desta abstração é especialmente poderosa quando começamos a pensar em padráes que utilizam Promises de alta ordem (além de race([..])
e all([..])
).
Por exemplo, talvez vocΓͺ queira, no meio de uma sequΓͺncia, expressar um passo similar a um bloco try..catch
onde sempre Γ© retornado sucesso, seja pelo sucesso de fato ou pelo envio de um sinal positivo nos casos que apanhamos um erro. Ou talvez vocΓͺ queira expressar um passo que funciona como um loop retry/until, onde o mesmo passo ocorre repetidas vezes atΓ© que se obtenha sucesso.
Estes tipos de abstraçáes nΓ£o sΓ£o trivialmente expressadas utilizando-se apenas Promises nativas, e aplicΓ‘-las em uma cadeia de Promises jΓ‘ existente nΓ£o Γ© bonito. Mas se vocΓͺ abstrair seu pensamento para uma sequΓͺncia e considerar um passo como um envΓ³lucro de uma Promises, este passo envΓ³lucro pode esconder estes detalhes, liberando vocΓͺ para pensar sobre o controle de fluxo de forma mais sensata sem se incomodar com os detalhes.
Em segundo lugar, e talvez o mais importante, pensar em controle de fluxo assΓncrono em termos de passos em uma sequΓͺncia permite que vocΓͺ abstraia detalhes de quais tipos de assincronia sΓ£o envolvidos em cada passo individualmente. Por baixo dos panos, uma Promise sempre controlarΓ‘ um passo, mas, "por cima dos panos", este passo pode ser visto como um callback de continuidade (o padrΓ£o simples), uma Promise real, como um generator em modo run-to-completion, ou... acho que vocΓͺ compreende.
Em terceiro, sequΓͺncias podem ser alteradas mais facilmente para adaptarem-se a diferentes formas de pensar, como programação baseada em eventos, streams ou reativa. asynquence provΓͺ um padrΓ£o que chamo de "sequΓͺncias reativas" (as quais cobriremos mais adiante) como uma variação da ideia de "observΓ‘vel reativo" (reactive observable) em RxJS ("Reactive Extensions"), que permite que um evento recorrente inicie uma nova sequΓͺncia a cada ocorrΓͺncia. Promises sΓ£o um tiro ΓΊnico, portanto Γ© um pouco estranho expressar assincronia repetitiva apenas com Promises.
Uma outra forma de pensar inverte a capacidade de resolução/controle em um padrΓ£o que chamo de "sequΓͺncias iterΓ‘veis". Ao invΓ©s de cada passo controlar individualmente e internamente sua completude (e portanto o avanΓ§o da sequΓͺncia), a sequΓͺncia Γ© invertida de modo que o controle de avanΓ§o se dΓͺ atravΓ©s de um iterador externo e cada passo na sequΓͺncia iterΓ‘vel apenas responde ao controle next(..)
do iterador.
Vamos explorar todas as diferentes variaçáes na medida que avanΓ§armos por este apΓͺndice, portanto nΓ£o se preocupe se fomos muito rΓ‘pidos atΓ© o momento.
O mais importante Γ© a ideia de que sequΓͺncias sΓ£o uma abstração mais poderosa e sensata para assincronia complexa do que apenas Promises (cadeias de Promises) ou generators, e asynquence foi projetada para expressar esta abstração com o nΓvel exato de praticidade para tornar a programação assΓncrona mais compreensΓvel e prazerosa.
Para comeΓ§ar, a forma com que vocΓͺ cria uma sequΓͺncia (uma instΓ’ncia asynquence) Γ© com a função ASQ(..)
. Um chamada para ASQ()
sem parΓ’metros cria uma sequΓͺncia inicial vazia, enquanto que se passarmos um ou mais valores ou funçáes para ASQ(..)
a sequΓͺncia Γ© inicializada utilizando cada um de seus argumentos como um passo.
Nota: Utilizarei o identificador asynquence global para browsers ASQ
para todos os exemplos de cΓ³digo aqui. Se vocΓͺ incluir asynquence atravΓ©s de um sistema de mΓ³dulos (browser ou server), vocΓͺ certamente pode definir o identificador que preferir que asynquence nΓ£o se importarΓ‘!
Muitos dos mΓ©todos da API discutidos aqui foram construΓdos no nΓΊcleo de asynquence, mas outros sΓ£o providos atravΓ©s da inclusΓ£o do pacote de plugins "contrib". Veja a documentação de asynquence para identificar se um mΓ©todo Γ© nativo ou se foi definido atravΓ©s de um plugin: http://github.com/getify/asynquence
Se uma função representa um passo normal em uma sequΓͺncia, esta função Γ© invocada recebendo como primeiro parΓ’metro o callback de continuação e os parΓ’metros subsequentes sΓ£o quaisquer mensagens transmitidas pelo passo anterior. O passo nΓ£o serΓ‘ concluΓdo atΓ© que o callback de continuação seja chamado. Assim que chamado, qualquer argumento passado para ele serΓ‘ enviado como mensagem para o prΓ³ximo passo da sequΓͺncia.
Para incluir um passo adicional Γ sequΓͺncia basta chamar then(..)
(que possui exatamente a mesma semΓ’ntica de ASQ(..)
):
ASQ(
// passo 1
function(done){
setTimeout( function(){
done( "Hello" );
}, 100 );
},
// passo 2
function(done,greeting) {
setTimeout( function(){
done( greeting + " World" );
}, 100 );
}
)
// passo 3
.then( function(done,msg){
setTimeout( function(){
done( msg.toUpperCase() );
}, 100 );
} )
// passo 4
.then( function(done,msg){
console.log( msg ); // HELLO WORLD
} );
Nota: Embora o nome then(..)
seja idΓͺntico ao da API nativa de Promises, este then(..)
Γ© diferente. VocΓͺ pode passar quantas funçáes ou valores quiser para then(..)
e cada um Γ© recebido como um passo separado. NΓ£o existe a semantica de dois callbacks realizado/rejeitado.
Diferentemente das Promises, onde para encadearmos uma Promise na prΓ³xima temos que criar e tambΓ©m retornar (return
) esta Promise no handler de sucesso enviado para then(..)
. Com asynquence, tudo que vocΓͺ precisa fazer Γ© chamar o callback de continuação -- eu sempre o chamo de done()
mas vocΓͺs pode chamΓ‘-lo como achar melhor -- e opcionalmente passar para ele mensagens como argumentos.
Cada passo definido por then(..)
Γ© assumido como assΓncrono. Se vocΓͺ tem um passo que Γ© sΓncrono, vocΓͺ pode chamar done(..)
imediatamente ou chamar um utilitΓ‘rio mais simples invocando val(..)
:
// passo 1 (sΓncrono)
ASQ( function(done){
done( "Hello" ); // manualmente sΓncrono
} )
// passo 2 (sΓncrono)
.val( function(greeting){
return greeting + " World";
} )
// passo 3 (assΓncrono)
.then( function(done,msg){
setTimeout( function(){
done( msg.toUpperCase() );
}, 100 );
} )
// passo 4 (sΓncrono)
.val( function(msg){
console.log( msg );
} );
Como vocΓͺ pode ver, passos invocados atravΓ©s de val(..)
nΓ£o recebem o callback de continuação pois isto Γ© feito internamente para vocΓͺ -- e a lista de parΓ’metros fica menos bagunΓ§ada como resultado! Para enviar uma mensagem ao prΓ³ximo passo, basta utilizar return
.
Pense em val(..)
como a representação de um passo sΓncrono contendo apenas um valor, o que Γ© ΓΊtil para operaçáes com valores sΓncronos, logging e afins.
Uma importante deiferença de asynquence em comparação com Promises se dÑ no tratamento de erros.
Com Promises, cada Promise (passo) em uma cadeia pode ter seu prΓ³prio erro e cada paso subsequente tem a opção de manipulΓ‘-lo ou nΓ£o. A principal razΓ£o desta semΓ’ntica vem (novamente) do foco em Promises como unidades individuais e nΓ£o como uma cadeia (sequΓͺncia).
Acredito que, na maior parte do tempo, um erro em uma parte de uma sequΓͺncia Γ© irrecuperΓ‘vel, portanto os passos subsequentes da sequΓͺncia sΓ£o discutΓveis e devem ser ignorados. Portanto, por padrΓ£o, um erro em qualquer passo de uma sequΓͺncia passa toda a sequΓͺncia para um estado de erro e o restante dos passos sΓ£o ignorados.
Se vocΓͺ precisa de um passo onde um erro Γ© recuperΓ‘vel, existem diferentes mΓ©todos da API que podem auxiliar, como try(..)
-- anteriormente mencionado como um tipo de passo try..catch
-- ou until(..)
-- um loop de tentativas que fica repetindo o passo atΓ© que obtenha sucesso ou que vocΓͺ chame break()
manualmente dentro do loop. asynquence possui tambΓ©m os mΓ©todos pThen(..)
e pCatch(..)
que funcionam de forma idΓͺntica aos mΓ©todos then(..)
e catch(..)
de uma Promise (veja o CapΓtulo 3) para que vocΓͺ possa tratar erros no meio de uma sequΓͺncia se assim desejar.
O ponto Γ© que vocΓͺ tem ambas opçáes mas a mais comum na minha experiΓͺncia Γ© a padrΓ£o. Com Promises, para que uma cadeia de passos ignore todos os passos caso um erro ocorra vocΓͺ deve tomar o cuidado de nΓ£o registrar um handler de rejeição em nenhum dos passos; caso contrΓ‘rio, este erro serΓ‘ desaparece como se fosse tratado e a sequΓͺncia pode continuar (talvez de forma inesperada). Este tipo de comportamento, quando desejado, Γ© um pouco estranho de se manipular adequada e confiavelmente.
Para registrar um handler de notificação de sequΓͺncias com erro, asynquence provΓͺ o mΓ©todo de sequΓͺncia or(..)
, o qual possui um alias onerror(..)
. VocΓͺ pode chamar este mΓ©todo em qualquer ponto da sequΓͺncia e vocΓͺ pode registrar quantos handlers achar necessΓ‘rio. Isso torna mais fΓ‘cil para mΓΊltiplos (e diferentes) consumidores saberem se uma sequ%encia falhou ou nΓ£o; Γ© como se fosse um handler de um evento de erro.
Assim como com Promises, toda exceção JS tornam-se erros da sequΓͺncia, ou vocΓͺ pode sinalizar um erro na sequΓͺncia programaticamente:
var sq = ASQ( function(done){
setTimeout( function(){
// sinaliza um erro na sequΓͺncia
done.fail( "Oops" );
}, 100 );
} )
.then( function(done){
// nunca chegarΓ‘ aqui
} )
.or( function(err){
console.log( err ); // Oops
} )
.then( function(done){
// nΓ£o chegarΓ‘ aqui tambΓ©m
} );
// depois
sq.or( function(err){
console.log( err ); // Oops
} );
Outra importante diferenΓ§a na manipulação de erros de asynquence em relação a Promises nativas Γ© o comportamento padrΓ£o de "exceçáes nΓ£o manipuladas" (unhandled exceptions). Como dicutimos massivamente no CapΓtulo 3, uma Promise rejeitada que nΓ£o possui um handler de rejeição registrado irΓ‘ prender silenciosamente (tambΓ©m referido como "engolir") o erro; vocΓͺ deve lembrar-se de sempre finalizar uma corrente com um catch(..)
.
Em asynquence esta suposição é invertida.
Se um erro ocorre em uma sequΓͺncia e ela atΓ© este momento nΓ£o possui um handler de erro registrado, o erro Γ© reportado para o console
. Em outras palavras, rejeiçáes não manipuladas são, por padrão, reportadas de modo que não sejam engolidas ou perdidas.
Assim que um handler de error for registrado em uma sequΓͺncia, a sequΓͺncia para de reportar erros da forma mencionada anteriormente para evitar a duplicação/ruΓdo.
Podem haver, de fato, casos onde vocΓͺ quer criar uma sequΓͺncia que pode ir para um estado de erro antes de vocΓͺ ter a chance de registrar um handler. Isto nΓ£o Γ© comum mas pode acontecer de tempos em tempos.
Nestes casos, vocΓͺ pode optar por nΓ£o reportar erros desta sequΓͺncia chamando defer()
. VocΓͺ somente deve fazer isso se vocΓͺ tem certeza que eventualmente irΓ‘ manipular estes erros:
var sq1 = ASQ( function(done){
doesnt.Exist(); // vai lançar uma exceção no console
} );
var sq2 = ASQ( function(done){
doesnt.Exist(); // vai lanΓ§ar um erro apenas na sequΓͺncia
} )
// optando por nΓ£o reportar erros
.defer();
setTimeout( function(){
sq1.or( function(err){
console.log( err ); // ReferenceError
} );
sq2.or( function(err){
console.log( err ); // ReferenceError
} );
}, 100 );
// ReferenceError (from sq1)
Esta Γ© uma forma de manipulação de erros melhor do que em Promises por se tratar do PoΓ§o do Sucesso e nΓ£o do PoΓ§o da Falha (veja o CapΓtulo 3).
Nota: Se uma sequΓͺncia Γ© canalizada (ou incluΓda em) outra sequΓͺncia -- veja "Combinando SequΓͺncias" para uma descrição completa -- entΓ£o a sequΓͺncia de origem opta automaticamente por nΓ£o reportar erros, embora agora a notificação ou nΓ£o de erros da sequΓͺncia de destino deva ser considerada.
Nem todos os passos em sua sequΓͺncia terΓ£o apenas uma ΓΊnica tarefa (assΓncrona) para executar; alguns precisarΓ£o executar mΓΊltiplos passos "em paralelo" (ao mesmo tempo). Um passo em uma sequΓͺncia no qual mΓΊltiplos sub-passos sΓ£o processados ao mesmo tempo Γ© chamado de gate(..)
-- existe um alias all(..)
se vocΓͺ preferir -- e Γ© diretamente simΓ©trico ao Promise.all([..])
nativo.
Se todos os passos em gate(..)
completam com sucesso, todas as mensagens de sucesso serΓ£o passadas para o prΓ³ximo passo da sequΓͺncia. Se algum deles gerar um erro, a sequΓͺncia inteira passa para um estado de erro.
Considere:
ASQ( function(done){
setTimeout( done, 100 );
} )
.gate(
function(done){
setTimeout( function(){
done( "Hello" );
}, 100 );
},
function(done){
setTimeout( function(){
done( "World", "!" );
}, 100 );
}
)
.val( function(msg1,msg2){
console.log( msg1 ); // Hello
console.log( msg2 ); // [ "World", "!" ]
} );
Para ilustrarmos, vamos comparar este exemplo com Promises nativas:
new Promise( function(resolve,reject){
setTimeout( resolve, 100 );
} )
.then( function(){
return Promise.all( [
new Promise( function(resolve,reject){
setTimeout( function(){
resolve( "Hello" );
}, 100 );
} ),
new Promise( function(resolve,reject){
setTimeout( function(){
// nota: precisamos de um [ ] array aqui
resolve( [ "World", "!" ] );
}, 100 );
} )
] );
} )
.then( function(msgs){
console.log( msgs[0] ); // Hello
console.log( msgs[1] ); // [ "World", "!" ]
} );
Eca! Promises necessitam de muita duplicação para expressar o mesmo controle de fluxo assΓncrono. Esta Γ© uma boa forma de ilustrar que a API e abstração de asynquence tornam a manipulação de Promises muito mais agradΓ‘veis. E isso sΓ³ melhora na medida que a complexidade de sua assincronia aumenta.
Existem diversas variaçáes nos plug-ins contrib
para o passo gate(..)
de asynquence que podem ser muito ΓΊteis:
any(..)
Γ© comogate(..)
, exceto que apenas um segmento deve obter sucesso para darmos prosseguimento Γ sequΓͺncia principal.first(..)
Γ© comoany(..)
, exceto que assim que qualquer segmento obtenha sucesso a sequΓͺncia principal Γ© continuada (ignorando resultados de outros segmentos).race(..)
(simΓ©trico aoPromise.race([..])
) Γ© comofirst(..)
, exceto que a sequΓͺncia principal prossegue assim que qualquer segmento se completa (seja em caso de sucesso ou falha).last(..)
Γ© comoany(..)
, exceto que apenas o ΓΊltimo segmento a completar com sucesso enviarΓ‘ adiante sua(s) mensagem(ns) para a sequΓͺncia principal.none(..)
Γ© o inverso degate(..)
: a sequΓͺncia principal prossegue apenas se todos os segmentos falharem (tendo as mensagens de erro de todos os segmentos convertidas em mensagens de sucesso e vice versa).
Vamos definir algumas funçáes auxiliares para tornar a ilustração mais clara:
function success1(done) {
setTimeout( function(){
done( 1 );
}, 100 );
}
function success2(done) {
setTimeout( function(){
done( 2 );
}, 100 );
}
function failure3(done) {
setTimeout( function(){
done.fail( 3 );
}, 100 );
}
function output(msg) {
console.log( msg );
}
Agora vamos demonstrar estas variaçáes do passo gate(..)
:
ASQ().race(
failure3,
success1
)
.or( output ); // 3
ASQ().any(
success1,
failure3,
success2
)
.val( function(){
var args = [].slice.call( arguments );
console.log(
args // [ 1, undefined, 2 ]
);
} );
ASQ().first(
failure3,
success1,
success2
)
.val( output ); // 1
ASQ().last(
failure3,
success1,
success2
)
.val( output ); // 2
ASQ().none(
failure3
)
.val( output ) // 3
.none(
failure3
success1
)
.or( output ); // 1
Outra variação de passo é map(..)
, que permite que vocΓͺ mapeie assincronamente valores de um array para valores diferentes, e o passo nΓ£o completa atΓ© que todo mapeamento esteja completo. map(..)
Γ© muito parecido com gate(..)
, exceto que recebe os valores iniciais de um array em vez de receber funçáes separadamente, e tambΓ©m porque vocΓͺ define uma ΓΊnica função callback para operar em cada valor:
function double(x,done) {
setTimeout( function(){
done( x * 2 );
}, 100 );
}
ASQ().map( [1,2,3], double )
.val( output ); // [2,4,6]
AlΓ©m disso, map(..)
pode receber qualquer um dos seus parΓ’metros (array ou callback) a partir de mensagens enviadas por passos anteriores:
function plusOne(x,done) {
setTimeout( function(){
done( x + 1 );
}, 100 );
}
ASQ( [1,2,3] )
.map( double ) // recebe a mensagem `[1,2,3]`
.map( plusOne ) // recebe a mensagem `[2,4,6]`
.val( output ); // [3,5,7]
Outra variação é waterfall(..)
, que Γ© como uma mistura do comportamento de acumular mensagens de gate(..)
com o processamento sequencial de then(..)
.
Passo 1 Γ© executado e sua mensagem de sucesso Γ© enviada para o passo 2, entΓ£o ambas mensagens de sucesso sΓ£o enviadas para o passo 3, e as trΓͺs mensagens de sucesso sΓ£o enviadas para o passo 4 e assim por diante, de modo que as mensagens sΓ£o acumuladas e "descem" pela "cascata" (waterfall).
Considere:
function double(done) {
var args = [].slice.call( arguments, 1 );
console.log( args );
setTimeout( function(){
done( args[args.length - 1] * 2 );
}, 100 );
}
ASQ( 3 )
.waterfall(
double, // [ 3 ]
double, // [ 6 ]
double, // [ 6, 12 ]
double // [ 6, 12, 24 ]
)
.val( function(){
var args = [].slice.call( arguments );
console.log( args ); // [ 6, 12, 24, 48 ]
} );
Se em qualquer ponto da "cascata" ocorrer um erro, toda sequΓͺncia imediatamente passa para um estado de erro.
Γs vezes vocΓͺ quer gerenciar erros no nΓvel dos passos e nΓ£o necessariamente enviar toda a sequΓͺncia para um estado de erro. asynquence oferece duas variaçòes de passo para estes casos.
try(..)
tenta executar um passo e, em caso de sucesso, a sequΓͺncia prossegue normalmente. Mas se o passo falhar, a falha Γ© convertida em uma mensagem de sucesso formatada como { catch: .. }
contendo a(s) mensagem(ns) de erro:
ASQ()
.try( success1 )
.val( output ) // 1
.try( failure3 )
.val( output ) // { catch: 3 }
.or( function(err){
// nunca chega aqui
} );
Em vez disso, vocΓͺ poderia configurar um loop de tentativas utilizando until(..)
, que tenta executar um passo e, se ele falhar, executa o passo novamente no prΓ³ximo instante (tick) do loop de eventos (event loop) e assim por diante.
Este loop de tentativas pode continuar indefinidamente, mas se vocΓͺ quiser sair do loop, vocΓͺ pode chamar o mΓ©todo break()
no callback de continuação, que envia toda sequΓͺncia principal para um estado de erro:
var count = 0;
ASQ( 3 )
.until( double )
.val( output ) // 6
.until( function(done){
count++;
setTimeout( function(){
if (count < 5) {
done.fail();
}
else {
// sai do loop de tentativas de `until(..)`
done.break( "Oops" );
}
}, 100 );
} )
.or( output ); // Oops
If you would prefer to have, inline in your sequence, Promise-style semantics like Promises' then(..)
and catch(..)
(see Chapter 3), you can use the pThen
and pCatch
plug-ins:
ASQ( 21 )
.pThen( function(msg){
return msg * 2;
} )
.pThen( output ) // 42
.pThen( function(){
// throw an exception
doesnt.Exist();
} )
.pCatch( function(err){
// caught the exception (rejection)
console.log( err ); // ReferenceError
} )
.val( function(){
// main sequence is back in a
// success state because previous
// exception was caught by
// `pCatch(..)`
} );
pThen(..)
and pCatch(..)
are designed to run in the sequence, but behave as if it was a normal Promise chain. As such, you can either resolve genuine Promises or asynquence sequences from the "fulfillment" handler passed to pThen(..)
(see Chapter 3).
One feature that can be quite useful about Promises is that you can attach multiple then(..)
handler registrations to the same promise, effectively "forking" the flow-control at that promise:
var p = Promise.resolve( 21 );
// fork 1 (from `p`)
p.then( function(msg){
return msg * 2;
} )
.then( function(msg){
console.log( msg ); // 42
} )
// fork 2 (from `p`)
p.then( function(msg){
console.log( msg ); // 21
} );
The same "forking" is easy in asynquence with fork()
:
var sq = ASQ(..).then(..).then(..);
var sq2 = sq.fork();
// fork 1
sq.then(..)..;
// fork 2
sq2.then(..)..;
The reverse of fork()
ing, you can combine two sequences by subsuming one into another, using the seq(..)
instance method:
var sq = ASQ( function(done){
setTimeout( function(){
done( "Hello World" );
}, 200 );
} );
ASQ( function(done){
setTimeout( done, 100 );
} )
// subsume `sq` sequence into this sequence
.seq( sq )
.val( function(msg){
console.log( msg ); // Hello World
} )
seq(..)
can either accept a sequence itself, as shown here, or a function. If a function, it's expected that the function when called will return a sequence, so the preceding code could have been done with:
// ..
.seq( function(){
return sq;
} )
// ..
Also, that step could instead have been accomplished with a pipe(..)
:
// ..
.then( function(done){
// pipe `sq` into the `done` continuation callback
sq.pipe( done );
} )
// ..
When a sequence is subsumed, both its success message stream and its error stream are piped in.
Note: As mentioned in an earlier note, piping (manually with pipe(..)
or automatically with seq(..)
) opts the source sequence out of error-reporting, but doesn't affect the error reporting status of the target sequence.
If any step of a sequence is just a normal value, that value is just mapped to that step's completion message:
var sq = ASQ( 42 );
sq.val( function(msg){
console.log( msg ); // 42
} );
If you want to make a sequence that's automatically errored:
var sq = ASQ.failed( "Oops" );
ASQ()
.seq( sq )
.val( function(msg){
// won't get here
} )
.or( function(err){
console.log( err ); // Oops
} );
You also may want to automatically create a delayed-value or a delayed-error sequence. Using the after
and failAfter
contrib plug-ins, this is easy:
var sq1 = ASQ.after( 100, "Hello", "World" );
var sq2 = ASQ.failAfter( 100, "Oops" );
sq1.val( function(msg1,msg2){
console.log( msg1, msg2 ); // Hello World
} );
sq2.or( function(err){
console.log( err ); // Oops
} );
You can also insert a delay in the middle of a sequence using after(..)
:
ASQ( 42 )
// insert a delay into the sequence
.after( 100 )
.val( function(msg){
console.log( msg ); // 42
} );
I think asynquence sequences provide a lot of value on top of native Promises, and for the most part you'll find it more pleasant and more powerful to work at that level of abstraction. However, integrating asynquence with other non-asynquence code will be a reality.
You can easily subsume a promise (e.g., thenable -- see Chapter 3) into a sequence using the promise(..)
instance method:
var p = Promise.resolve( 42 );
ASQ()
.promise( p ) // could also: `function(){ return p; }`
.val( function(msg){
console.log( msg ); // 42
} );
And to go the opposite direction and fork/vend a promise from a sequence at a certain step, use the toPromise
contrib plug-in:
var sq = ASQ.after( 100, "Hello World" );
sq.toPromise()
// this is a standard promise chain now
.then( function(msg){
return msg.toUpperCase();
} )
.then( function(msg){
console.log( msg ); // HELLO WORLD
} );
To adapt asynquence to systems using callbacks, there are several helper facilities. To automatically generate an "error-first style" callback from your sequence to wire into a callback-oriented utility, use errfcb
:
var sq = ASQ( function(done){
// note: expecting "error-first style" callback
someAsyncFuncWithCB( 1, 2, done.errfcb )
} )
.val( function(msg){
// ..
} )
.or( function(err){
// ..
} );
// note: expecting "error-first style" callback
anotherAsyncFuncWithCB( 1, 2, sq.errfcb() );
You also may want to create a sequence-wrapped version of a utility -- compare to "promisory" in Chapter 3 and "thunkory" in Chapter 4 -- and asynquence provides ASQ.wrap(..)
for that purpose:
var coolUtility = ASQ.wrap( someAsyncFuncWithCB );
coolUtility( 1, 2 )
.val( function(msg){
// ..
} )
.or( function(err){
// ..
} );
Note: For the sake of clarity (and for fun!), let's coin yet another term, for a sequence-producing function that comes from ASQ.wrap(..)
, like coolUtility
here. I propose "sequory" ("sequence" + "factory").
The normal paradigm for a sequence is that each step is responsible for completing itself, which is what advances the sequence. Promises work the same way.
The unfortunate part is that sometimes you need external control over a Promise/step, which leads to awkward "capability extraction".
Consider this Promises example:
var domready = new Promise( function(resolve,reject){
// don't want to put this here, because
// it belongs logically in another part
// of the code
document.addEventListener( "DOMContentLoaded", resolve );
} );
// ..
domready.then( function(){
// DOM is ready!
} );
The "capability extraction" anti-pattern with Promises looks like this:
var ready;
var domready = new Promise( function(resolve,reject){
// extract the `resolve()` capability
ready = resolve;
} );
// ..
domready.then( function(){
// DOM is ready!
} );
// ..
document.addEventListener( "DOMContentLoaded", ready );
Note: This anti-pattern is an awkward code smell, in my opinion, but some developers like it, for reasons I can't grasp.
asynquence offers an inverted sequence type I call "iterable sequences", which externalizes the control capability (it's quite useful in use cases like the domready
):
// note: `domready` here is an *iterator* that
// controls the sequence
var domready = ASQ.iterable();
// ..
domready.val( function(){
// DOM is ready
} );
// ..
document.addEventListener( "DOMContentLoaded", domready.next );
There's more to iterable sequences than what we see in this scenario. We'll come back to them in Appendix B.
In Chapter 4, we derived a utility called run(..)
which can run generators to completion, listening for yield
ed Promises and using them to async resume the generator. asynquence has just such a utility built in, called runner(..)
.
Let's first set up some helpers for illustration:
function doublePr(x) {
return new Promise( function(resolve,reject){
setTimeout( function(){
resolve( x * 2 );
}, 100 );
} );
}
function doubleSeq(x) {
return ASQ( function(done){
setTimeout( function(){
done( x * 2)
}, 100 );
} );
}
Now, we can use runner(..)
as a step in the middle of a sequence:
ASQ( 10, 11 )
.runner( function*(token){
var x = token.messages[0] + token.messages[1];
// yield a real promise
x = yield doublePr( x );
// yield a sequence
x = yield doubleSeq( x );
return x;
} )
.val( function(msg){
console.log( msg ); // 84
} );
You can also create a self-packaged generator -- that is, a normal function that runs your specified generator and returns a sequence for its completion -- by ASQ.wrap(..)
ing it:
var foo = ASQ.wrap( function*(token){
var x = token.messages[0] + token.messages[1];
// yield a real promise
x = yield doublePr( x );
// yield a sequence
x = yield doubleSeq( x );
return x;
}, { gen: true } );
// ..
foo( 8, 9 )
.val( function(msg){
console.log( msg ); // 68
} );
There's a lot more awesome that runner(..)
is capable of, but we'll come back to that in Appendix B.
asynquence is a simple abstraction -- a sequence is a series of (async) steps -- on top of Promises, aimed at making working with various asynchronous patterns much easier, without any compromise in capability.
There are other goodies in the asynquence core API and its contrib plug-ins beyond what we saw in this appendix, but we'll leave that as an exercise for the reader to go check the rest of the capabilities out.
You've now seen the essence and spirit of asynquence. The key take away is that a sequence is comprised of steps, and those steps can be any of dozens of different variations on Promises, or they can be a generator-run, or... The choice is up to you, you have all the freedom to weave together whatever async flow control logic is appropriate for your tasks. No more library switching to catch different async patterns.
If these asynquence snippets have made sense to you, you're now pretty well up to speed on the library; it doesn't take that much to learn, actually!
If you're still a little fuzzy on how it works (or why!), you'll want to spend a little more time examining the previous examples and playing around with asynquence yourself, before going on to the next appendix. Appendix B will push asynquence into several more advanced and powerful async patterns.