Skip to content

fix: schedule the build-claim heartbeat with recursive setTimeout - #55

Merged
dawsontoth merged 2 commits into
mainfrom
claude/heartbeat-settimeout
Jul 23, 2026
Merged

fix: schedule the build-claim heartbeat with recursive setTimeout#55
dawsontoth merged 2 commits into
mainfrom
claude/heartbeat-settimeout

Conversation

@dawsontoth

Copy link
Copy Markdown
Contributor

Follow-up to #53. The build-claim heartbeat it introduced used setInterval; a code-review pass on the sibling vite change (HarperFast/vite#26) flagged two issues with that approach, and the same code is now on main here.

Problems with the setInterval heartbeat

  1. Overlapping writes. setInterval fires every HEARTBEAT_MS regardless of whether the previous table.put resolved. If a write ever takes longer than the interval (DB load, contention), two re-stamps are in flight; a slow earlier building write can land after the terminal success/failure write and revert the record to building — leaving the claim stuck until it goes stale (up to STALE_CLAIM_MS). Low probability with sub-ms LMDB writes, but a real correctness hole.
  2. Synchronous throw. Promise.resolve(table.put(...)) doesn't catch a synchronous throw from table.put — it would escape the .catch and surface as an uncaught error in the timer callback.

Fix

Recursive setTimeout instead of setInterval:

  • The next beat is scheduled only after the current write settles (in .finally), so at most one re-stamp is ever in flight — a slow write can't outlive the terminal record.
  • stopHeartbeat() awaits that single in-flight write before the terminal success/failure write, preserving "terminal record is the last word."
  • Chaining off Promise.resolve().then(() => table.put(...)) converts a synchronous table.put throw into a caught rejection.

Testing

npm test10/10 pass (heartbeat test now mocks setTimeout); tsc --noEmit clean.

Mirrors the identical fix in HarperFast/vite#26, keeping the two plugins' build locks in sync.

🤖 Generated with Claude Code

The heartbeat added in #53 used setInterval, which fires on a fixed cadence
regardless of whether the previous re-stamp finished. Under DB load two writes
could be in flight, and a slow earlier `building` write could land after the
terminal `success`/`failure` write — leaving the claim stuck until it goes
stale. Recursive setTimeout schedules the next beat only after the current
write settles, so at most one is ever in flight and stopHeartbeat() fully
awaits it before the terminal write. Chaining off Promise.resolve().then(() =>
table.put(...)) also converts a synchronous throw from table.put into a caught
rejection instead of an uncaught timer error.

Test mocks setTimeout instead of setInterval accordingly. Mirrors the same fix
in @harperfast/vite.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request replaces the use of setInterval with a recursive setTimeout pattern in startClaimHeartbeat to ensure that heartbeat writes do not overlap and that slow writes cannot revert terminal records. The corresponding test file is updated to mock setTimeout instead of setInterval. The reviewer suggested extracting the duplicated timer scheduling and .unref() logic into a helper function to reduce duplication and using function declarations to avoid potential hoisting issues.

Comment thread src/buildLock.ts Outdated
@dawsontoth
dawsontoth requested a review from a team July 23, 2026 14:30
Addresses review feedback: the setTimeout + unref scheduling was duplicated for
the initial beat and the recursive reschedule. Extract it into scheduleNext(),
and use function declarations for beat/scheduleNext so their mutual reference is
hoisting-safe. No behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dawsontoth
dawsontoth merged commit 6282394 into main Jul 23, 2026
7 checks passed
@dawsontoth
dawsontoth deleted the claude/heartbeat-settimeout branch July 23, 2026 18:26
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 2.2.3 🎉

The release is available on:

Your semantic-release bot 📦🚀

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants