[Music]
okay let's begin good morning everyone
so today I'm going to talk about race
conditions in JavaScript apps and so
right the title of this talk is about
race conditions today I'm going to talk
about other things as well maybe other
edge cases that are not strictly race
conditions but you may encounter them
when you write a synchronous code so
before I begin to talk I would like to
share a bit of story before like how
this talk came to be so I first got the
idea of for this talk on one day on that
day I went to a local meet up in
Thailand and after the talks are done
it's like networking time and my friend
approached me and asked me hey ty is it
hard to deal with race conditions in
JavaScript so I answered well is
actually pretty easy you just don't
forget about them he gives me a bit of a
confused look and I was like hey I'm
totally serious here let me show you a
couple of examples so I took out my
laptop and start coding showing him some
examples and before long it turned into
a really deep discussion so after that
discussion my friend told me whoa this
could have been a food talk why don't
you make a talk out of it and submit it
to judge point for something so here I
am and a big shout out goes to same for
giving me the inspiration for this talk
he's with us here as well so next time
you have a deep technical discussion
maybe you can tell your friend to like
consider submitting a talk
so this makes it my second time speaking
here at just wants a CS so big big
thanks to Thomas and all the organizers
involved in running this conference it's
really great and thank you for having me
once again so today I'm going to show
you two stories from my experience
dealing with race conditions and stuff
so I hope you can learn something out of
these stories as I did by the way my
name is ty and I'm from Thailand my
twitter handle is at DT ints and if you
have any feedback about this talk please
send it to me and I would really
appreciate it I'm currently working at
task whereas the front-end architect and
I'll get back to that later
but now here comes the first story and
this story involves little search box so
in this search boss as I type a keyword
is going to show the search result and
it's going to show the result instantly
so I don't have to press ENTER or
anything
it's like Google's autosuggest so let's
build this real quick first we need the
mock up so here I'm going to use the
section for the rapper then it does an
input box right there and then list
containing the result so that's all the
mock-up we need and next we're going to
add some interactivity and for that I'm
going to use VJ s so let's start with
the data model for our state first we
need to store the security that the user
type and we can buy that today input box
using the Wemo directive then we also
need to store the search result to be
displayed on the list here and to do
that we can use the we four directive to
do the binding and finally we buy the
content of each result into the
respective elements so that the data and
the Dom they are now kept in sync so
when the user enters the text Kure into
the input field is going to update the
text right here and we can wash for when
it changes
and when it changes we can perform in a
synchronous Kure by calling the search
function right there it's going to
return a promise so we have to await it
and once the result arrived we save it
into this page which will be displayed
on the user interface so then we have it
and let's see how it goes
so okay I'm going to type in react I
don't know why but okay so the result
shows up and that's good but it seems
that the result that I see here doesn't
have anything to do with react so what's
going on here let's try again but this
time I built an inspector on the right
so that we can see what's going on okay
you can see here that the result I want
to see is overwritten by the result of
the request that I made earlier but it
finished later so it overrides their the
the Dom and you know this kind of makes
sense because the shorter the Kure maybe
it will return more result and maybe it
will take longer to load so you cannot
depend on the order of the request to
correspond to the order of the response
so you can think of an a sync function
as a function that spawns the trade and
the Dom here is a shared mutable state
so you know when dealing with shared
mutable state we have to be careful
so let's come back to this code when we
face problem like this one of the common
approach I see very often is to do a D
bounce and what I mean by D bouncing is
that instead of doing the search right
away we're going to wait a bit until the
user has finished typing and after that
period of inactivity then we do the
search as usual so let's try it okay it
seems to work because now it only
performs a single request there are no
more competing requests but you know it
went with the D bounce it still doesn't
guarantee the order of the response so I
could do something like this then we're
back to the same problem so let's step
back and look at this code again the
real problem here is that while we are
waiting for the result to arrive I could
have triggered another search and in
this case when the result did arrive it
might not make sense to use it anymore
because now I have already triggered
another search so let's fix this but
first I'm going to get rid of the D
bounce because it didn't work instead
I'm going to generate a unique ID for
each request then keep track of the
latest request ID here and when we after
we await maybe this not latest request
ID could have changed so we compare if
the request ID we are holding onto
doesn't corresponds to the latest
request we'll just discard the result
and let's see okay now it works and
because we only care about the result of
the latest request other other response
got discarded but here now I am
searching really slowly and you can see
that until I finished typing I get no
feedback whatsoever from the search box
and that makes me fear that the search
box is a bit unresponsive because you
know before the resolve could arrive I
type another letter so it discarded the
result that the interim result but if we
compare that to Google even on a very
slow internet I still get the result as
I type even if it lacks behind by
several characters so it's clear that
they are not totally disregarding the
result that is not the latest so I think
we can do better so instead of
discarding the result that
doesn't correspond to the latest
requests what if we only discard the
result that arrived out of order so we
can do this and let's see okay now it
seems to work and you can see here that
I get the result I want even before I
finished typing so yeah you can see that
this humble little search box it took me
three attempts until I can get it right
and that brings me to the first takeaway
for this first story which is to always
remember about the potential risk
conditions whenever there is an await
call so each time you see in a wait
maybe think about what else could happen
while we are waiting for the result and
also think about whether or not it makes
sense to use the result after the wait
all right
that's the first story we're going to
continue with the second story and this
story came from like my experience
working at Tazewell and it is a
real-time collaborative project
management app and that's important and
you'll see why the story happened when
we built the app in its early days so
we're just starting out there it
happened about two to three years ago
and it has to do with something called
optimistic updates so for example our
app provides a Kanban board and suppose
that you drag a task from one list to
the other you wouldn't expect the task
to go into the loading State right well
no you expect the task to just snap
right into the new list and that's what
optimistic update is so basic idea is we
update the UI first in anticipation that
the request will be completed
successfully and raw we synchronize this
change with the server in the background
so having this kind of experience like
optimistic updates it's really important
for collaborative epic
patience but our app users react and
when we use to react
we like to derive our user interface
from the underlying data model so in
this case is the reader store so in
order to preserve the unidirectional
data flow we update the data model
instead and we're going to let it flow
into the UI and for this example maybe
moving a task is a bit complex operation
because it involves both a task and the
list so instead I'm going to just focus
on something simpler such as renaming a
task so when I hit enter here is going
to trigger an event handler and it will
call a function here rename task which
is our action and following this basic
pattern what we did is okay we update
the data model here and that's going to
cost the data model to flow into the
user interface and like that then we
make the API call to synchronize with
the backend and because sometimes
internet connection may not be stable or
something we are just going on with the
API we use a try cache to display the
error message just for good measure and
that's it that is how we implement it
basic optimistic updates and for most of
our operations in our app we write code
following this basic pattern and you
know this works really well and gives us
a really great experience as long as
never as long as nothing goes wrong so
it works well in the early days when we
are just starting out and our focus was
to figure out how we can make project
management to that our customers would
like to use however as we get more
customers we start getting reports such
as well our screens are displaying
different things you know they become
incomes
or something more serious such as this
one so someone writes a comment the
other person didn't receive it because
it never gets sent but the app acts as
if it was already sent so you know work
is lost and that's pretty serious I
think we came back to our code and it
became clear that our implementation of
optimistic update was indeed too
optimistic so there's no room for
anything to go wrong here and when it
does let's see for example if this API
call fails first we didn't live with our
data model back to the previous state so
each client will go its own way and it
will be totally out of sync and we also
didn't really try to request and this is
really bad because you know we tried so
hard and put in a lot of effort to like
craft a great experience that the
customer will want to use and now we are
just throwing their data away and you
know that's not what we want for our
users but well that was what we
implemented so at that time we
incorrectly assumed that building a
single page real-time collaborative app
and task management apps you know is
it's just like CI UD apps create read
update delete that's all that's mostly
what it does so it should be as simple
as Oh screw multi page application
because right because you know that's
what we know that's what we're familiar
with and if you write Ruby on Rails ya
doing a create thing is just as simple
as that so it's supposed to be simple
right well maybe not really and now here
here we are facing its consequence so we
have to fix it and that means we have to
go back and fix all of these operations
that we've implemented so far as well so
our goals shift data loss is a serious
problem and first to fix that we need to
try to achieve eventual consistency that
means everyone will eventually see the
same thing on their screen which I call
the ground truth without having to like
press refresh and second we want to
prevent data loss as much as we can so
let's first talk about the first one
coming back to this code record the
previous part where I say when the API
call failed we didn't reword this state
right
maybe that's quite easy to fix right
we just start a previous title here and
when that's an error
we just revert and that would work would
it as we learned from our first story
you see in a weight and anything could
happen here especially in a real-time
app so for example someone may
successfully update this task while
we're awaiting here and that means the
previous title that we stored will no
longer become well it and we cannot use
it anymore
so the more is cases we try to solve the
more they seem to come up and what about
more complex operations such as like
moving a task between lists how how much
complex would our quote be if we were to
handle all the edge cases in all of
these operations uh is there something
fundamentally wrong with the way we are
writing code so I took another step back
and think about the way we are
approaching optimistic updates and then
I realized that if we want to have
eventual consistency we must try to
stick with the ground truth I defined it
as the letter state that we received
from or delay or delayed the thing that
was acknowledged by the backend can be
considered the ground truth and each
time we update the model of the
basically here we are moving away from
that ground truth and considering that
everything else may be going on and
happening all around that will make it
hard harder to ever come back to it so
maybe we should leave that data model
alone and don't update it optimistically
but then how would we implement
optimistic UI then we figure something
out what if we had a separate data model
representing a list of the pending
operations instead of optimistically
update the data model directly we just
add it to the list of pending operations
and when it's done we just remove it
when we render the UI we overlay the
pending change on top of what's inside
the model essentially computing a
project optimistic version of the task
on the fly in our components and that
could work because if the operation
fails the data model would be left alone
and the UI will revert back but if the
operation succeeded the ground truth
will be updated first then the operation
will be removed from the list and from
the UI perspective it's nothing change
so now we have a data model that allows
us to do optimistic up Dre updates
without driving us insane so now let's
consider a case where I just created a
task here and the blue bar right there
represents the time it take for the
operation to complete but now since we
have optimistic update
I see the task appear right away and
that means I can also interact with it
so let's say I immediately assigned
someone to that task in this case this
API call would fail because at this time
the task doesn't exist in the backend
yet
right so that will lead to a data loss
and we don't want that and that means I
need to wait until the previous
operation complete first before before
we can work on the subsequent operation
and that essentially means we have to
sequence our operations into an
operation queue so with the operation
queue implemented the action code now
looks like this so this is now even
simpler than the first one that we came
up and we just let the queue do his
thing so now all of our write operations
can be fixed by making it right to the
queue instead of calling the API
directly then we can update the UI to
compute the projected state by reading
from that queue so because the
operations are now sequence and managed
by the queue that means it can also take
care of Lee trying the failed request
and that helps us deal with data loss so
we haven't covered all the edge cases
yet and preventing further data loss
from here is like just a matter of
asking a series of what-if questions for
example what if the user closes the
browser arrived a queue is still running
well we can try to prevent the user from
quitting the browser or we can persist
the content of the queue into the
client-side storage so that it can be
resumed afterwards or we can do both
right but our user may be using our app
from a mobile device and it might not be
practical to do that so we decided to
also save the queue to the client
storage and that gives us as a bonus
give us a building block that if we want
to make our app work offline in the
future because you know progressive web
apps are becoming more popular each year
so yeah that's a good bonus for for
purchasing the cute to the clients
knowledge but then a user may be running
our app in multiple attempts so before
we decide to purchase the queue there
was no problem because each tap would
have its own queue but now the queue is
persistent
so it become a shared mutable state here
if every tap runs the queue at the same
time bad things going to happen so that
can be only one tab that runs this queue
but which one so today with today's web
technology we have shared web workers
and also so its workers so we have two
things so maybe we don't need any tap to
run it at all we can just do it in the
workers but today we also have this so I
guess we cannot use the workers yet so
we have to make it so that any tab can
work on the queue if and only if no
other tabs work on it but since our
client side storage can be a synchronous
there might be the case where more than
one tab looking at the queue and saw
that no one's running the queue and both
decide to work on it so who wins and you
know there can be only one tap working
on the queue that's a hot requirement
otherwise bad things going to happen so
we use a shared client-side storage and
have each I have each tap write its own
ID to this storage and after allow
really we read it back so the letters
the latest have to write wins and the
winning tap will periodically report its
status to the shared storage so that
other taps know that okay someone is
taking care of the queue and it's due
alive but
if that tap somehow disappeared um maybe
it maybe the user closed it maybe it
crashed then that we have to set up a
time out so that other taps could decide
that well the tap that originally work
on it has gone and can take over can
take a word the work left from the
previous tab so but what if the tap tap
disappeared didn't didn't went away it
just slept and it came back so we also
have to handle the case where where
where the cue has been taken away from
the tap that is not dead so as you can
see there was a lot of edge cases that
we have to handle to make our app more
resilient and to prevent data loss that
brings me to the takeaway that first I
didn't know that writing as the IUD apps
would have to be this complex you know
there's a lot of tutorials out there
teaching you to like how to make a
real-time to do application
collaborative using let's say socket IO
I have never seen any of them want me
about this when so these are the
problems we face when we try to build
production quality apps and that's why I
want to give this talk so much and the
next takeaway is knowing your tools can
help so there are higher level
primitives that can help you write code
that's free of race conditions there are
some cloud cloud providers and SDKs that
will take care of these problems to some
extent some open source database are
designed to be about to synchronize and
work offline so sometimes if you can
take advantage of them that's great
you
less problem to solve but sometimes your
circumstance might prevent you from
using the tools that you want and that's
why in my opinion the most important
thing the most important thing here is
to become aware of the existence of this
class of problems because even if you
don't know the solutions or if you don't
know the wish to to use at least you
know that you should try to avoid it so
to close this talk I would consider this
talk success if you go home today and
become more careful when you see the
keyword await in your codebase you don't
have to be paranoid just maybe just be
aware that during an a wait
anything could happen I don't know if
that's paranoid or not but but and maybe
when you test your app try
double-clicking everything instead of
single clicking and the next time you
see all right there were a wait I hope
you think about this talk thank you
[Applause]