Frontmatter
| id | 9375 |
| title | Improve ScrollSync Lock Release Mechanism (rAF vs setTimeout) |
| state | Closed |
| labels | enhancementaiarchitecture |
| assignees | tobiu |
| createdAt | Mar 7, 2026, 5:54 PM |
| updatedAt | Mar 7, 2026, 5:57 PM |
| githubUrl | https://github.com/neomjs/neo/issues/9375 |
| author | tobiu |
| commentsCount | 1 |
| parentIssue | 9369 |
| subIssues | [] |
| subIssuesCompleted | 0 |
| subIssuesTotal | 0 |
| blockedBy | [] |
| blocking | [] |
| closedAt | Mar 7, 2026, 5:57 PM |
Improve ScrollSync Lock Release Mechanism (rAF vs setTimeout)

Input from Gemini 3.1 Pro:
✦ I have updated the lock release mechanism in
ScrollSync.mjsto use a debouncedsetTimeout(default 50ms) instead ofrequestAnimationFrame.Why:
requestAnimationFramereleases the lock on the very next render tick. However, native DOMscrollevents are dispatched asynchronously. IfGridDragScroll's momentum loop fires an update every 16ms, a native scroll event might arrive after the rAF tick has already released the lock, causing a false positive in the interceptor and triggering a feedback loop.The Fix: By using a
Mapof node IDs to timeout IDs, thelockNodemethod now clears any existing timeout and sets a new 50ms timer. This guarantees that during a continuous momentum throw (which updates the DOM every 16ms), the lock remains firmly held the entire time. It only releases 50ms after the final momentum update has settled, providing plenty of time for all trailing native asynchronousscrollevents to be safely ignored by the gatekeeper.
Part of Epic #9369
Currently, the granular lock (
coordinatingNodesSet) inScrollSync.mjsis released on the nextrequestAnimationFrame. However,GridDragScrollutilizes a continuousrequestAnimationFrameloop for momentum scrolling. Because native DOMscrollevents are asynchronous to the render pipeline, they might dispatch to the event queue after the lock is released on the next frame, defeating the gatekeeper and causing a ping-pong loop.Investigate and implement a safer lock release mechanism (e.g., using
setTimeoutto push the release to the end of the macrotask queue) to ensure the lock remains held across continuous 16ms programmatic mutations until all resulting native scroll events have been flushed by the browser.