Skip to content

fix(deck): prevent AppRefresherIcon scheduler/listener leak on re-render#128

Open
scuba10steve wants to merge 2 commits into
mainfrom
fix/app-refresher-icon-scheduler-leak
Open

fix(deck): prevent AppRefresherIcon scheduler/listener leak on re-render#128
scuba10steve wants to merge 2 commits into
mainfrom
fix/app-refresher-icon-scheduler-leak

Conversation

@scuba10steve

Copy link
Copy Markdown
Member

Problem

AppRefresherIcon is mounted persistently in the application header. It called
SchedulerFactory.createScheduler(2000) in the render body, while the
useEffect that subscribes to and tears down that scheduler is keyed on
[lastRefresh].

Each call to createScheduler eagerly starts an RxJS timer and registers
document visibilitychange plus window online/offline listeners, which
are only removed via unsubscribe(). Because the scheduler was created on every
render but the effect only re-ran (and cleaned up) when lastRefresh changed,
any re-render that did not change lastRefresh — clicking refresh (toggles
iconPulsing), a refreshing prop change, or a parent re-render — created a
fresh, fully-wired scheduler that was never subscribed and never torn down. Its
timer and three global listeners leaked for the rest of the session.

Fix

Move createScheduler inside the useEffect so exactly one scheduler exists per
mount/lastRefresh value and is disposed in the effect cleanup. Behavior is
otherwise unchanged (the relative-time tooltip still updates on the same 2s
cadence and re-evaluates when lastRefresh changes).

Test

Adds AppRefresherIcon.spec.tsx asserting that a lastRefresh-stable re-render
(changing the refreshing prop) creates no additional scheduler. The test
fails on the old code and passes with the fix.

Verification

  • Full Deck Karma suite: 1749 passed, 0 failed (the new spec fails before the
    fix, passes after).
  • eslint clean on the changed source; prettier --check clean.

AppRefresherIcon called SchedulerFactory.createScheduler(2000) in the render
body, but the useEffect that subscribes/tears it down is keyed on [lastRefresh].
Every re-render that did not change lastRefresh (refresh click toggling
iconPulsing, refreshing prop changes, parent re-renders) created a fresh
scheduler whose rxjs timer and visibilitychange/online/offline listeners were
never unsubscribed, leaking them for the session on an always-mounted header
component.

Move createScheduler into the effect so exactly one scheduler exists per
lastRefresh value and is disposed on cleanup. Behavior is unchanged. Adds a
spec asserting no new scheduler is created on a lastRefresh-stable re-render.
Switch the spec from shallow to mount so the useEffect actually runs, and assert
the real guarantees: exactly one scheduler is created on mount, none is created on
a lastRefresh-stable re-render, and the scheduler is torn down on unmount. Verified
red on the pre-fix code (createScheduler called twice) and green on the fix.
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