Summary
Store.notify() discards a notification that arrives while that key's observers are running, instead of remembering that the value changed again. The DOM is then left permanently disagreeing with the store: the last write a key received never reaches its observers, and nothing retries.
src/store.ts:
const current = this._notify.get(key);
// Skip if observers are already executing for this key (prevents infinite loops).
if (current === "executing") return;
The comment names the real requirement — an observer that writes its own key must not recurse forever — but dropping is a stronger remedy than that requires, and it silently loses legitimate updates from other writers.
Measured
Observed on a page that writes one store key from pointermove and binds it to one element's style. A MutationObserver on that attribute, counted against the pointermove events the page received, over a single gesture:
|
pointermoves |
style writes landed |
lost |
| Chromium 412x915 |
41 |
38 |
3 (7%) |
| Firefox 412x915 |
41 |
37 |
4 (10%) |
| Chromium, CPU throttled 20x |
41 |
32 |
9 (22%) |
Every landed write carried a distinct value (new Set(writes).size === writes.length in all runs), so these were not de-duplicated no-ops — they were dropped. The loss scales with how long observers take, which is what makes it a mobile-first symptom: on a phone the same code loses most of its updates.
The visible bug in our case: an element revealed by one key while the key carrying its position was dropped, so it painted at its untransformed origin.
Suggested fix
Coalesce rather than drop — latest-wins, at most one extra pass:
if (current === "executing") {
this._notifyAgain.add(key); // or store "executing-dirty" in the same map
return;
}
and in the finally after await Promise.all(...), if the key was marked, schedule one more notification. That preserves the recursion guard (a self-writing observer converges after one extra pass instead of looping) while making a lost update impossible.
Happy to send a PR with a regression test if the approach looks right.
Version
mancha@0.22.6; the same code is on master.
Summary
Store.notify()discards a notification that arrives while that key's observers are running, instead of remembering that the value changed again. The DOM is then left permanently disagreeing with the store: the last write a key received never reaches its observers, and nothing retries.src/store.ts:The comment names the real requirement — an observer that writes its own key must not recurse forever — but dropping is a stronger remedy than that requires, and it silently loses legitimate updates from other writers.
Measured
Observed on a page that writes one store key from
pointermoveand binds it to one element'sstyle. AMutationObserveron that attribute, counted against thepointermoveevents the page received, over a single gesture:Every landed write carried a distinct value (
new Set(writes).size === writes.lengthin all runs), so these were not de-duplicated no-ops — they were dropped. The loss scales with how long observers take, which is what makes it a mobile-first symptom: on a phone the same code loses most of its updates.The visible bug in our case: an element revealed by one key while the key carrying its position was dropped, so it painted at its untransformed origin.
Suggested fix
Coalesce rather than drop — latest-wins, at most one extra pass:
and in the
finallyafterawait Promise.all(...), if the key was marked, schedule one more notification. That preserves the recursion guard (a self-writing observer converges after one extra pass instead of looping) while making a lost update impossible.Happy to send a PR with a regression test if the approach looks right.
Version
mancha@0.22.6; the same code is onmaster.