Skip to content

full-calendar: actually stop the listener leak - #162

Merged
roncodes merged 1 commit into
test/coverage-campaignfrom
fix/full-calendar-listener-leak
Aug 25, 2026
Merged

roncodes merged 1 commit into
test/coverage-campaignfrom
fix/full-calendar-listener-leak

Conversation

@roncodes

Copy link
Copy Markdown
Member

Fixes DEFECTS #17. One component, one behaviour change, two tests.

The leak

Nothing ever called destroyCalendarEventListeners — no willDestroy, no
registerDestructor, no reference in full-calendar.hbs. Every on<Event> callback a consumer
supplies is registered with calendar.on(...) and pushed onto _listeners, and nothing ever
unregisters it. A calendar on a route the user navigates in and out of accumulates a listener per
callback for the lifetime of the page.

Why this was flagged instead of fixed in passing

Wiring the destructor up is necessary but not sufficient. The method did:

this.calendar.off(eventName, this.triggerCalendarEvent.bind(this, callbackName));

.bind() returns a new function on every call, so the reference handed to .off() can never equal
the one .on() was given, and FullCalendar removes nothing.

That version looks fixed. The destructor runs, the method runs, nothing errors, and a test
asserting "destroyCalendarEventListeners was called" passes — while every listener stays
registered.

The fix

Bind once, keep the resulting function on the _listeners entry, hand that same reference to both
.on() and .off(), and empty _listeners afterwards. willDestroy calls it.

The tests

Both assert the observable behaviour rather than the call: trigger the event after the component is
destroyed and require no callback to fire.

Both were confirmed to fail against the .bind() mismatch with willDestroy already wired —
that is, against the version that looks fixed and is not:

not ok 1 — tearing down: a destroyed calendar stops firing its callbacks
not ok 2 — tearing down: every subscribed event is unsubscribed, not just the first

full-calendar.js is now at 100% statements, branches, functions and lines (22 tests) — the
five gaps that remained in it were the leak.

Left open, deliberately

The component never calls this.calendar.destroy(), so the FullCalendar instance and the
document-level handlers it installs outlive the component. That is a separate and probably larger
leak than this one; the integration tests already work around it with an afterEach that destroys
the captured calendar. Fixing it changes what a consumer's @onInit reference points at after
teardown, so it wants its own decision rather than being folded in here. Recorded in DEFECTS #17.

Verification

  • full-calendar filtered run: 22 pass, 0 fail.
  • rm -f .eslintcache && pnpm run lint → exit 0.
  • Coverage read from a freshly generated coverage-final.json, not from a passing run.

Nothing ever called destroyCalendarEventListeners — no willDestroy, no
registerDestructor, no template reference — so a calendar on a route the user
navigated in and out of accumulated a listener per callback for the lifetime of
the page.

Wiring the destructor up is necessary but not sufficient, which is the whole
reason this one was flagged rather than fixed in passing. The method did:

    this.calendar.off(eventName, this.triggerCalendarEvent.bind(this, callbackName));

.bind() returns a new function every call, so the reference handed to .off()
could never equal the one .on() was given and FullCalendar removed nothing. That
version looks fixed: the destructor runs, the method runs, nothing errors, and a
test asserting "destroyCalendarEventListeners was called" passes.

So: bind once, keep the function on the _listeners entry, hand that same
reference to both .on() and .off(), and empty _listeners afterwards.

The two tests assert the observable behaviour instead of the call — trigger the
event after the component is destroyed and require no callback to fire. Both
were confirmed to FAIL against the .bind() mismatch with willDestroy already
wired, i.e. against the version that looks fixed and is not.

full-calendar.js: 100% statements, branches, functions and lines (22 tests).

Left open in #17 and deliberately out of scope: the component never calls
this.calendar.destroy(), so the FullCalendar instance and the document-level
handlers it installs outlive the component. The test file already works around
that with an afterEach destroying the captured calendar. Fixing it changes what
a consumer's @onInit reference points at after teardown, so it wants its own
decision.
@roncodes
roncodes merged commit 9a94617 into test/coverage-campaign Aug 25, 2026
@roncodes
roncodes deleted the fix/full-calendar-listener-leak branch August 25, 2026 03:39
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