Skip to content

Store.notify() drops updates that arrive while a key's observers are executing #66

Description

@owahltinez

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions