fix: schedule the build-claim heartbeat with recursive setTimeout - #55
Merged
Conversation
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>
There was a problem hiding this comment.
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.
cb1kenobi
approved these changes
Jul 23, 2026
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>
|
🎉 This PR is included in version 2.2.3 🎉 The release is available on: Your semantic-release bot 📦🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 onmainhere.Problems with the
setIntervalheartbeatsetIntervalfires everyHEARTBEAT_MSregardless of whether the previoustable.putresolved. If a write ever takes longer than the interval (DB load, contention), two re-stamps are in flight; a slow earlierbuildingwrite can land after the terminalsuccess/failurewrite and revert the record tobuilding— leaving the claim stuck until it goes stale (up toSTALE_CLAIM_MS). Low probability with sub-ms LMDB writes, but a real correctness hole.Promise.resolve(table.put(...))doesn't catch a synchronous throw fromtable.put— it would escape the.catchand surface as an uncaught error in the timer callback.Fix
Recursive
setTimeoutinstead ofsetInterval:.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 terminalsuccess/failurewrite, preserving "terminal record is the last word."Promise.resolve().then(() => table.put(...))converts a synchronoustable.putthrow into a caught rejection.Testing
npm test— 10/10 pass (heartbeat test now mockssetTimeout);tsc --noEmitclean.Mirrors the identical fix in HarperFast/vite#26, keeping the two plugins' build locks in sync.
🤖 Generated with Claude Code