Frontmatter
| id | 8988 |
| title | bug: rewrite throttle utility to robustly handle trailing edge |
| state | Closed |
| labels | bugaicore |
| assignees | tobiu |
| createdAt | Feb 4, 2026, 6:47 PM |
| updatedAt | Feb 4, 2026, 7:18 PM |
| githubUrl | https://github.com/neomjs/neo/issues/8988 |
| author | tobiu |
| commentsCount | 1 |
| parentIssue | null |
| subIssues | [] |
| subIssuesCompleted | 0 |
| subIssuesTotal | 0 |
| blockedBy | [] |
| blocking | [] |
| closedAt | Feb 4, 2026, 7:18 PM |
bug: rewrite throttle utility to robustly handle trailing edge
tobiu assigned to @tobiu on Feb 4, 2026, 6:48 PM

tobiu
Feb 4, 2026, 7:16 PM
Input from Gemini 3 Pro:
✦ Rewrote
throttleutility to implement a robust Fixed-Rate pattern with Leading & Trailing Edge support.Key Changes:
- State Tracking: Introduced
lastRantimestamp andtimeoutIdhandle.- Leading Edge: Executes immediately if the cooldown period (
delay) has passed.- Persistent Timer: During the cooldown window, a single
setTimeoutis scheduled for the remaining time (delay - elapsed). This avoids the fragility of clearing/recreating timers.- Trailing Edge: The persistent timer captures the latest arguments (
lastArgs) and executes them once the cooldown expires, ensuring the final event (e.g., scroll stop) is never dropped.- Fixed-Rate Precision: By calculating
remainingtime, we maintain a consistent execution frequency (e.g., 60fps) rather than drifting by adding delay after execution.This fixes the issue where strict timing checks in the previous implementation could silently drop trailing calls due to browser timer jitter.
tobiu closed this issue on Feb 4, 2026, 7:18 PM
Context
The current
throttleimplementation insrc/util/Function.mjshas two issues:setTimeouton every call during the cooldown period solely to capture the latest arguments.(Date.now() - lastRanDate) >= delayinside the timeout. If the browser fires the timeout even 1ms early (common jitter), the trailing call is silently dropped.Objective
Rewrite
throttleto use a standard, robust pattern:now - lastRun > delay.latestArgs. Schedule a SINGLE timeout for the remaining time if one doesn't exist.latestArgs, updatelastRun, and clear the timeout flag.This ensures
ScrollManager(and other components) never lose the final update event, preventing visual desyncs.