Counter
Building a distributed counter.
Let’s start with the “hello world” of JavaScript framework demos: the humble click counter.
We have a map with one key — count
— which stores a number.
Every time a client clicks the button, the count increments by one and gets synced to the other client.
Once the clients sync up, the number on the counter should be the total number of clicks across both clients.
Seems pretty simple, right?
Adding Latency
Not so fast! This demo happens to be working because updates are synchronous. Every time the button gets clicked, the other client instantly receives the update.
Try adding a little latency by dragging the slider on the example below. See if you can find the bug.
Something’s definitely wrong there, right? If you click on both clients too quickly, some clicks never show up. Or even worse: sometimes a client’s count actually decreases!
Let’s peek under the hood to see what’s going on. We’ll add a timeline showing a history of how the counter was modified. Updates will show up under each client as it changes its local state. When the two clients sync up, all their local updates will move to the middle.
We’ll also add an extra counter showing the number of times each client has clicked its button. Check it out:
Local clicks: 0
Local clicks: 0
Take a few minutes to play with that example. If you want to get really granular, you can click on each timeline update to step back or forward in time and see what the state was like at that point. See if you can figure out what exactly causes the counter to break.
Think you got it? Cool — now let’s try and fix this counter example.
Exercise
We’ll be writing two functions:
getCount(map)
takes aY.Map
and returns a number with the current count.increment(map)
takes aY.Map
and increments the current count by 1.
Implement the two functions so that the counter never loses state, even if two peers click the button simultaneously. Remember, there are hints and unit tests below to help you!
Local clicks: 0
Local clicks: 0
Unit tests2/2
- getCount() returns the count from all peers
- increment() increments the count without losing state
Hint 1
The reason this broke when latency was added is that a client can’t know whether another client modified the count before it updates it. Can you think of a way to protect clients from each other’s updates?
Hint 2
If two clients modify the count simultaneously, the updates will overwrite each other, because the clients are sharing the same field. Can you think of a way to give each client its own field to increment?
Bonus hint: check the previous chapter!
Hint 3
Remember, each document has a clientID
property that is unique to that client — and the document is available on every shared type through the doc
property.
Click to reveal solution
Solution
Since each client has its own client ID that no other client will ever have, we can use that client ID as a map key:
export function increment(map) {
const key = map.doc.clientID;
const count = map.get(key) || 0;
map.set(key, count + 1);
}
We know that no other client will ever touch this map key, since they have different client IDs.
When we’re calculating the total count, we simply add up the counts for each key:
export function getCount(map) {
let sum = 0;
for (const value of map.values()) {
sum += value;
}
return sum;
}