Skip to content

fix(kida): keep onMount subscribed when a signal remounts within the unmount delay - #229

Merged
dangreen merged 1 commit into
mainfrom
fix/kida-on-mount-remount
Sep 3, 2026
Merged

fix(kida): keep onMount subscribed when a signal remounts within the unmount delay#229
dangreen merged 1 commit into
mainfrom
fix/kida-on-mount-remount

Conversation

@dangreen

@dangreen dangreen commented Sep 3, 2026

Copy link
Copy Markdown
Member

The bug

onMount arms a setTimeout on unmount and never cancels it. A signal that remounts inside STORE_UNMOUNT_DELAY therefore loses its subscription one second later, while it is mounted and being read, and it never gets it back: evaluate delivers the mount level on a change, so no further mount fires until a full unmount and mount cycle happens.

Everything built on onMount inherits it. paced keeps its source subscription there, so a paced signal goes quiet after a quick route remount. React StrictMode reaches the same state on every double-invoked subscribe.

Why the existing tests missed it

should debounce unmount callback follows every remount with an immediate unmount, so when the first timer finally fires the signal really is unmounted and the teardown is legitimate. It pins the debounce, not the cancellation. should dispatch onMount event runs vi.runAllTimers() between transitions, so a timer never survives a remount.

The new case is the one that was missing: mount, unmount, remount after 100 ms, advance past STORE_UNMOUNT_DELAY, assert the unmount listener was never called. It fails on main with expected "vi.fn()" to not be called at all, but actually been called 1 times.

The fix

The pending timer now holds the state active used to. A mount with a teardown scheduled cancels it instead of starting a second listener; a mount without one starts the listener; the teardown clears the timer. Per listener the levels alternate strictly, since evaluate only delivers an unchanged level to listeners past the lcf cursor and the unmount branch requires changed, so those two cases cover every delivery and the flag is redundant.

Size

Dropping the flag pays for the cancellation: measured on kida's bundle, raw 21562 → 21522 B, gzip 5067 → 5061 B against main. No .size-limit.json changes needed.

Verification

Unit tests green in kida (70), store (55), router (129) and query (163). oxlint and tsc --noEmit clean for kida. test:size run sequentially across all 13 packages with no exceedances.

@codecov

codecov Bot commented Sep 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 82.75%. Comparing base (c384319) to head (055f950).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #229      +/-   ##
==========================================
+ Coverage   82.73%   82.75%   +0.01%     
==========================================
  Files          98       98              
  Lines        2468     2465       -3     
  Branches      522      521       -1     
==========================================
- Hits         2042     2040       -2     
  Misses        315      315              
+ Partials      111      110       -1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@dangreen
dangreen force-pushed the fix/kida-on-mount-remount branch from d6be732 to 9e6c589 Compare September 3, 2026 14:28
…e unmount delay

`onMount` scheduled the delayed teardown on unmount and never cancelled it, so a signal that remounted inside `STORE_UNMOUNT_DELAY` lost its subscription a second later while it was still mounted, and stayed deaf from then on: the mount level is delivered on a change, so no further mount fires until a full unmount and mount cycle happens. Everything built on `onMount` is affected, `paced` among them, and React StrictMode reaches it on every double-invoked subscribe.

The pending timer now holds the state the `active` flag used to: a mount with a teardown scheduled cancels it instead of starting a second listener, a mount without one starts the listener, and the teardown clears the timer. Levels alternate per listener, so those two cases cover every delivery, and the result is 40 bytes smaller than the code before the fix.
@dangreen
dangreen force-pushed the fix/kida-on-mount-remount branch from 9e6c589 to 055f950 Compare September 3, 2026 14:38
@dangreen
dangreen merged commit b911d4c into main Sep 3, 2026
10 checks passed
@dangreen
dangreen deleted the fix/kida-on-mount-remount branch September 3, 2026 14:40
@github-actions github-actions Bot mentioned this pull request Sep 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant