

Showing tomorrow's vote count today

The vote counts on this blog come from a build that runs once a day. Click like at 10am and the real count catches up at 6am tomorrow. If the button only showed the built number, your vote would disappear the moment you reloaded. Some people would click again. Some would decide it was broken.
So the button shows what tomorrow’s page will show. It stores your vote in localStorage and adds it to the built count
until a build arrives that already includes it. Part 1
has the daily build and part 2 has the component. This is the bit in between.
What gets stored
One key per post, votes:<slug>, holding three fields:
interface StoredVote {
/** Current vote. */
v: VoteState;
/** Vote already included in the synced counts. */
b: VoteState;
/** Time of the last change, ms since epoch. */
t: number;
}
v is what you’ve picked now: "up", "down", or null. b is the vote the built counts already contain. t is
when you last changed it.
The count on screen is the built count, minus what’s already in it, plus what you’ve picked:
const shown = (side: "up" | "down"): number =>
Math.max(0, this.baseCount(side) - contribution(b, side) + contribution(v, side));
contribution is 1 if the vote is on that side and 0 if not. When b and v match, they cancel and you see the built
number. When they differ, the difference is your change that the build hasn’t seen yet.
Knowing when the build has caught up
The build stamps every page with the time of the last sync, taken from updated in the data file:
<vote-buttons item="four-agents-one-hero-image" up="1" down="0" synced-at="2026-10-10T06:02:05.303Z"></vote-buttons>
When the element loads, it compares your last change with that stamp:
// Everything sent before the last sync is already in the counts.
this.vote = stored.t <= this.syncedAt() ? { v: stored.v, b: stored.v, t: stored.t } : stored;
If you clicked before the sync, the sync has seen your vote, so b catches up to v. If you clicked after, it hasn’t,
and b stays where it was. That’s the whole mechanism.
A week on one post
Say a post has 4 likes in the morning build. The sync runs at 5:30am Sydney time and the build at 6am.
| When (Sydney) | What happens | v / b | Built likes | Shown likes |
|---|---|---|---|---|
| Tue 6am | Build ships | none / none | 4 | 4 |
| Tue 10am | You click like | up / none | 4 | 5 |
| Tue arvo | You come back and reload | up / none | 4 | 5 |
| Wed 6am | Build includes your like | up / up | 5 | 5 |
| Wed 11am | You switch to dislike | down / up | 5 | 4 |
| Thu 6am | Build includes the switch | down / down | 4 | 4 |
You never see your own vote counted twice or missing. Everyone else sees your like from Wednesday morning, which is when the real count changes for all of us.
The Wednesday 11am row is why b is a vote and not a “synced” flag. You changed your mind after the build counted your
like. To show the right number, the button has to take that like off and add the dislike, and for that it needs to know
which vote the build holds, not just that it holds one.
Where it falls over
It’s an honour system with a few holes. These are the ones I know about.
Storage is blocked or cleared. Private windows and strict settings can make localStorage throw. Reads and writes
are wrapped in try, so the vote still sends, it just isn’t remembered. Clear your storage and you can vote again. I’m
fine with that.
The event never arrives. If an ad blocker or a flaky connection eats the event, the next build doesn’t include it,
but the sync time still moves past your click. b catches up to v, the two cancel, and the count drops back to the
true number. Your button stays pressed. The page ends up telling the truth, a day late.
The sync fails. No new votes.json means the stamp doesn’t move, so b never catches up. Your vote keeps showing
on top of the old count. She’ll be right once the job runs again.
Your clock is wrong. This is the one I don’t like. t comes from your computer’s clock and synced-at from the
GitHub runner’s. If your clock is a day slow, a click made after the sync looks like it came before it, and your vote
drops out of the count until the next build. If it’s a day fast, the build includes your vote but the element reckons it
can’t yet, and you see it counted twice for a day. It’s one visitor with wrong numbers for a day, so I’ve left it.
More than one tab. The element reads storage when it loads and doesn’t listen for changes. Vote in one tab and the other keeps showing the old state until you reload it.
All of this lives in vote-buttons.ts in the package. It’s under 300 lines, styles included.