JS Async Patterns
Software Developer at Orange Labs
Part-time Front-end developer at Pushbots
ITI OSAD
I love Anime and Javascript
I don't like java
salamaashoush@gmail.com
@salamaashoush
A poor web developer wants to make a three request in parallel with javascript, he thinks a little and said to himself well it is very easy, i can spawn three threads like I do every day with java
he opened google and start typing how to spawn three threads with JavaScript and .....
he started to wonder how things work in the browser with a single thread?
Dom Events ????
Ajax????
etc???
and after a long day of searching and reading, he finally understood that ...
https://vimeo.com/96425312
he said, now I can do the job, and he opened google and searched for "how to write async code in js"?
and he found ...
function fakeAjax(param1,param2,..,cb){
....
cb('result');
}Implementation
let req1 = fakeAjax('/fake',(result)=>{
console.log(result);
});Usage
Pyramid of doom/Callback Hell
setTimeout(()=>{
console.log('one');
setTimeout(()=>{
console.log('two');
setTimeout(()=>{
console.log('three');
},1000)
},1000)
},1000)// continuation passing style
function one(cb){
console.log('one');
setTimeout(cb,1000);
}
function two(cb){
console.log('two');
setTimeout(cb,1000);
}
function three(){
console.log('three')
}
one(()=>two(three()));Inversion of control
trackCheckout(purchaseInfo,()=>{
chargeCreditCard(purchaseInfo);
showThankyouPage();
});let called = false;
trackCheckout(purchaseInfo,()=>{
if(!called){
called = true;
chargeCreditCard(purchaseInfo);
showThankyouPage();
}
});Trust:
You have to fix all of them
callback shapes
function doSomething(ok,err){
//...
if(error){
err(error);
}else{
ok(result);
}
}
doSomething((result)=>{
console.log(result);
},(error)=>{
console.log(error);
})function doSomething(cb){
//...
if(error){
cb(error);
}else{
cb(null,result);
}
}
doSomething((error,result)=>{
if(error){
console.log(error);
}else{
return console.log(result);
}
});Separate callbacks:
Error first callbacks (Node):
3 parallel requests problem:
we need to make three requests in parallel and output them as soon as possible in order
"file1","file2","file3"
function fakeAjax(url,cb) {
let fake_responses = {
"file1": "The first text",
"file2": "The middle text",
"file3": "The last text"
};
let randomDelay = (Math.round(Math.random() * 1E4) % 8000) + 1000;
console.log("Requesting: " + url);
setTimeout(function(){
cb(fake_responses[url]);
},randomDelay);
}// hold responses in whatever order they come back
let responses = {};
function getFile(file) {
fakeAjax(file,(text) => {
fileReceived(file,text);
});
}
function fileReceived(file,text) {
// haven't received this text yet?
if (!responses[file]) {
responses[file] = text;
}
let files = ["file1","file2","file3"];
// loop through responses in order for rendering
for (let i=0; i<files.length; i++) {
// response received?
if (files[i] in responses) {
// response needs to be rendered?
if (responses[files[i]] !== true) {
console.log(responses[files[i]]);
responses[files[i]] = true;
}
} else {
// can't render yet
// not complete!
return false;
}
}
console.log("Complete!");
}
// request all files at once in "parallel"
getFile("file1");
getFile("file2");
getFile("file3");a function that encapsulates synchronous or asynchronous code inside.
function add(x,y){
return x + y;
}
let thunk = () => {
add(5,6);
}
// always returns the same value 11
thunk();sync thunk
function addAsync(x,y,cb){
setTimeout(()=>cb(x+y),100);
}
let thunk = (cb) => {
addAsync(5,6,cb);
}
// it only need a callback
// to do the work
thunk((sum)=>{
console.log(sum);
});async thunk
a container around a particular collection of state, and you can use it everywhere to extract the same state
time-independent wrapper around the value
Closures
+
thunks
===
power
// active thunk
function getFile(file) {
let resp;
fakeAjax(file,function(text){
if (!resp) resp = text;
else resp(text);
});
return (cb) => {
if (resp) cb(resp);
else resp = cb;
};
}
// request all files at once in "parallel"
let th1 = getFile("file1");
let th2 = getFile("file2");
let th3 = getFile("file3");
th1((text) => {
console.log(text);
th2((text) => {
console.log(text);
th3((text) => {
console.log(text);
console.log("Complete!");
});
});
});more info about callbacks and thunks
I promise you nothing
Restaurant metaphor
sounds familiar
imagine that it returns an event listener and you listen to "complete" or "error" event
function getFile(file){
return new Promise((resolve, reject) => {
fakeAjax(file,(error, results) => {
if(error) return reject(error);
return resolve(results);
});
});
}Implementation
getFile('file1').then((file)=>{
console.log(file);
}).catch((error)=>{
console.log(error);
}).finally(()=>{
console.log('completed');
})Usage
Callbacks will never be called before the completion of the current run of the JavaScript event loop.
Callbacks added with .then even after the success or failure of the asynchronous operation will be called
Multiple callbacks may be added by calling .then several times, to be executed independently in insertion order.
Chain then after catch
doSomething()
.then(result => doSomethingElse(result))
.then(newResult => doThirdThing(newResult))
.then(finalResult => {
console.log(`Got the final result: ${finalResult}`);
})
.catch(failureCallback);doSomething()
.then(() => {
throw new Error('Something failed');
console.log('Do this');
})
.catch(() => {
console.log('Do that');
})
.then(() => {
console.log('Do this whatever happened before');
});//shortcuts to manually create an already resolved or rejected promise respectively. This can be useful at times.
Promise.resolve();
Promise.reject();
//two composition tools for running asynchronous operations in parallel.
Promise.all([p1,p2]);
Promise.race([p1,p2]);
//Sequential composition is possible using some clever JavaScript:
[func1, func2].reduce((p, f) => p.then(f), Promise.resolve());
function getFile(file) {
return new Promise((resolve) => {
fakeAjax(file,resolve);
});
}
// Request all files at once in
// "parallel" via `getFile(..)`.
let p1 = getFile("file1");
let p2 = getFile("file2");
let p3 = getFile("file3");
// Render as each one finishes,
// but only once previous rendering
// is done.
p1
.then(console.log)
.then(() => p2)
.then(console.log)
.then(() => p3)
.then(console.log)
.then(() => console.log("Complete!"));
// or
["file1","file2","file3"]
.map(getFile)
.reduce((chain, p) => chain.then(()=>p).then(console.log), Promise.resolve());the first time to understand generators
function* gen() {
yield 1;
yield 2;
yield 3;
}
let g = gen(); // iterator object
g.next(); // {value: 1, done: false}
g.next(); // {value: 2, done: false}
g.next(); // {value: 3, done: false}
g.next(); // {value: undefined, done: true}
// for of loop
for(let i of g){
console.log(value)
}the fact that whenever a function runs, it cannot be pre-empted and will run entirely before any other code runs
unlike normal function generator function can pause and continue execution in a different time and it i could run for ever without complete
function* idMaker() {
var index = 0;
while(true)
yield index++;
}
let gen = idMaker(); // "Generator { }"
console.log(gen.next().value); // 0
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2
// ... after many lines of code
//...
console.log(gen.next().value); // 3(Observer) --> producer in control
Dom Events
WebSockets
SereverSentEvents
NodeStreams
ServiceWorker
setInterval
(Iterator) --> consumer in controll
Arrays
etc
Generators let you write code looks like you push a value and it converts it to pull
https://www.youtube.com/watch?v=lil4YCCXRYc&t=1382s
function* logGenerator() {
console.log(0);
console.log(1, yield);
console.log(2, yield);
console.log(3, yield);
}
let gen = logGenerator();
gen.next(); // 0
gen.next('pretzel'); // 1 pretzel
gen.next('california'); // 2 california
gen.next('mayonnaise'); // 3 mayonnaise// return will complete the generator
function* gen() {
yield 1;
return 5;
yield 2; // unreachable code
}
Why does he mention this thing with async patterns?
function getData(v){
setTimeout(()=>g.next(v),1000)
}
function * gen(){
try{
let x = yield getData(5);
let y = x + (yield getData(6));
console.log(y); // 11
}catch(err){
console.log(err)
}
}
let g = gen();
g.next();co(function *(){
// resolve multiple promises in parallel
var a = Promise.resolve(1);
var b = Promise.resolve(2);
var c = Promise.resolve(3);
var res = yield [a, b, c];
console.log(res);
// => [1, 2, 3]
}).catch(onerror);
//find more here https://github.com/tj/cofunction getFile(file) {
return new Promise(function(resolve){
fakeAjax(file,resolve);
});
}
co(function *loadFiles(){
// Request all files at once in
// "parallel" via `getFile(..)`.
let p1 = getFile("file1");
let p2 = getFile("file2");
let p3 = getFile("file3");
// Render as each one finishes,
// but only once previous rendering
// is done.
try{
console.log( yield p1 );
console.log( yield p2 );
console.log( yield p3 );
}catch(error){
console.log(error)
}
})
.then(() => console.log("Complete!"))When I finally understood the power of generators
Beauty of writing sync-looking async code
function resolveAfter2Seconds(x) {
return new Promise(resolve => {
setTimeout(() => {
resolve(x);
}, 2000);
});
}
async function add1(x) {
try{
const a = await resolveAfter2Seconds(20);
const b = await resolveAfter2Seconds(30);
return x + a + b;
}catch(err){
console.log(err)
}
}
add1(10).then(v => {
console.log(v); // prints 60 after 4 seconds.
})function getFile(file) {
return new Promise((resolve) => {
fakeAjax(file,resolve);
});
}
(async () => {
// Request all files at once in
// "parallel" via `getFile(..)`.
let p1 = getFile("file1");
let p2 = getFile("file2");
let p3 = getFile("file3");
// Render as each one finishes,
// but only once previous rendering
// is done.
try{
console.log( await p1 );
console.log( await p2 );
console.log( await p3 );
}catch(error){
console.log(error);
}
})()
.then(() => console.log("Complete!"))// Asynchronous generator
async function* asyncGen() {
yield 'a';
yield 'b';
}
const iter = asyncGen()[Symbol.asyncIterator]();
iter.next().then(x => console.log(x));
// { value: 'a', done: false }
iter.next().then(x => console.log(x));
// { value: 'b', done: false }
iter.next().then(x => console.log(x));
// { value: undefined, done: true }Asynchronous iterables return asynchronous iterators, whose method next()returns Promises for {value, done} objects:
//sync for of loop with await inside async function
for (const x of await Promise.all([p1,p2,p3]));
// for await is exactly the same
for await (const x of [p1,p2,p3]);
// in action
// note: you can use for-await-of inside async functions only
async function main() {
const syncIterable = [
Promise.resolve('a'),
Promise.resolve('b'),
];
for await (const x of syncIterable) {
console.log(x);
}
}
main();async function* readLines(path) {
let file = await fileOpen(path);
try {
while (!file.EOF) {
yield await file.readLine();
}
} finally {
await file.close();
}
}
for await (const line of readLines(filePath)) {
console.log(line);
}Event streams
observable pushes the value to the consumer
interface iterable {
Generator iterator(void)
}
interface observable {
void observer(Generator)
}
https://www.youtube.com/watch?v=lil4YCCXRYc&t=1382s
function listen(element, eventName) {
return new Observable(observer => {
// Create an event handler which sends data to the sink
let handler = event => observer.next(event);
// Attach the event handler
element.addEventListener(eventName, handler, true);
// Return a cleanup function which will cancel the event stream
return () => {
// Detach the event handler from the element
element.removeEventListener(eventName, handler, true);
};
});
}
// Return an observable of special key down commands
function commandKeys(element) {
let keyCommands = { "38": "up", "40": "down" };
return listen(element, "keydown")
.filter(event => event.keyCode in keyCommands)
.map(event => keyCommands[event.keyCode])
}
//subscribe to the observable
let subscription = commandKeys(inputElement).subscribe({
next(val) { console.log("Received key command: " + val) },
error(err) { console.log("Received an error: " + err) },
complete() { console.log("Stream complete") },
});
// After calling this function, no more events will be sent
subscription.unsubscribe();Proposed Observable interface
//creates an Observable of the values provided as arguments
Observable.of("red", "green", "blue").subscribe({
next(color) {
console.log(color);
}
});
/*
> "red"
> "green"
> "blue"
*///Converting from an iterable to an Observable:
Observable.from(["mercury", "venus", "earth"]).subscribe({
next(value) {
console.log(value);
}
});
/*
> "mercury"
> "venus"
> "earth"
*/https://github.com/tc39/proposal-observable
observable proposal on stage1
Communicating sequential processes
CSP is a way to manage concurrency via channels
one of the reasons behind go-lang performance is its concurrency modal via channels
put and take values from channels is blocking by default
automatic back-pressure (streams concept)
let ch = chan();
function *process1(){
yield put(ch,"Hello");
let msg = yield take(ch);
console.log(msg);
}
function *proccess2(){
let greeting = yield take(ch);
yield put(ch,`${greeting} world`);
console.log("done!");
}The Process
The Process Can Pause
Processes Wait For Values In Channels
Processes Communicate Through Channels
Channel Are Queues
http://lucasmreis.github.io/blog/quick-introduction-to-csp-in-javascript/
let ch = csp.chan();
csp.go(function*(){
while(true){
yield csp.put(ch, Math.random());
}
});
csp.go(function*(){
while(true){
yield csp.take(csp.timeout(500));
let num = yield csp.take(ch);
console.log(num);
}
})Js csp
https://github.com/ubolonton/js-csp
function fromEvent(el,eventType){
let ch = csp.chan();
document.addEventListener(el,(e)=>{
csp.putAsync(ch,e)
});
return ch;
}
csp.go(function*(){
let ch = fromEvent(el, 'mousemove');
while(true){
let event = yield csp.put(ch, Math.random());
console.log(`${event.clientX}, event.clientY`);
}
});
Event listener with channels
function* player(name, table) {
while (true) {
let ball = yield csp.take(table);
if (ball === csp.CLOSED) {
console.log(name + ": table's gone");
return;
}
ball.hits += 1;
console.log(`${name} ${ball.hits}`);
yield csp.timeout(100);
yield csp.put(table, ball);
}
}
csp.go(function* () {
const table = csp.chan();
csp.go(player, ["ping", table]);
csp.go(player, ["pong", table]);
yield csp.put(table, {hits: 0});
yield csp.timeout(1000);
table.close();
});Ping/Pong