Skip to content

fix(dashboard): expire the likes cache on write so a reload shows the vote - #315

Open
shanurwan wants to merge 1 commit into
projnanda:mainfrom
shanurwan:fix/likes-read-after-write
Open

shanurwan wants to merge 1 commit into
projnanda:mainfrom
shanurwan:fix/likes-read-after-write

Conversation

@shanurwan

Copy link
Copy Markdown

Fixes #186.

Trigger

POST /api/likes invalidates the likes cache with revalidateTag(LIKES_CACHE_TAG, "max"). In Next 16, "max" only marks the tag stale: the next read of listAllLikesCached() returns the old snapshot right away and refreshes it in the background (unstable-cache.js: "we're doing background revalidation - return stale immediately"; see also node_modules/next/dist/docs/.../revalidateTag.md).

/skills is force-dynamic and reads through that cache. So:

  1. A signed-in user likes a skill. The heart shows the new count, taken from the POST response.
  2. They reload /skills. The page gets the pre-vote snapshot: the count drops back, the heart is empty, and viewerLiked is false because subs is stale too.
  3. The vote is in the database, but it looks lost. Only a second reload shows it.

This matches #186 ("votes are not being reflected in the total score", reported for both Google and GitHub sign-ins). That issue was filed on 2026-07-11, the same day a138af7 added the cached likes read with "max". The comment in lib/likes.ts promises that "like/unlike writes bust the tag, so voters still see counts move at once", and the "max" profile breaks that promise.

Fix

- revalidateTag(LIKES_CACHE_TAG, "max");
+ revalidateTag(LIKES_CACHE_TAG, { expire: 0 });

{ expire: 0 } is the form the Next docs give for Route Handlers that need immediate expiration. (updateTag works only in Server Actions.) The next read misses the cache and gets fresh data.

Compatibility

  • The 30s revalidate window and the cache tag stay the same, so the Neon data-transfer savings from a138af7 still hold. The only change is that a like or unlike now forces one fresh read.
  • The API shape and the client are unchanged.

Verification

  • npx tsc --noEmit, eslint on the changed file and npm run build all pass.
  • I reproduced it in a minimal Next 16.2.6 app built and run with next start. It uses the same pattern: unstable_cache with revalidate: 30 and a tag, a force-dynamic reader, and a POST route that writes and then calls revalidateTag. Before each run I cleared .next/cache:
=== "max" (current)
load page    : {"cached":0,"db":0}
vote         : {"db":1}
reload page  : {"cached":0,"db":1}   <- vote missing
reload again : {"cached":1,"db":1}

=== { expire: 0 } (this PR)
load page    : {"cached":0,"db":0}
vote         : {"db":1}
reload page  : {"cached":1,"db":1}   <- vote visible
reload again : {"cached":1,"db":1}

I did not test against the live Neon database. The dashboard has no test runner right now, so I haven't added a regression test to the repo.

Related, not in this PR

app/skills/actions.ts and app/api/skills/route.ts call revalidateTag(SKILLS_CACHE_TAG, "max") after a submission. That likely means a newly submitted skill doesn't appear on the first /skills reload, which could explain the duplicate submissions in #203. I can send a separate PR (updateTag in the Server Action, { expire: 0 } in the route) if you'd like.

… vote

revalidateTag(LIKES_CACHE_TAG, "max") only marks the tag stale, so the
next /skills load is served the pre-vote snapshot and the voter's like
appears lost until a second reload. Expire the tag immediately instead.

Fixes projnanda#186.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Votes not reflecting in total score

1 participant