Skip to content

Event bus listeners are never removed, and the planner fully remounts on every navigation #549

Description

@Maelstromeous

Found during the Beta v0.6 pre-launch QA sweep. Pre-existing.

What happens

There is no <KeepAlive> anywhere in the app — the layout is a bare <router-view/>. So every trip to Recipes / Parts / Change Log / Graph and back fully remounts Planner.vue and all its children.

Several components register eventBus listeners at setup scope with no matching off and no onUnmounted. All of them use anonymous arrow functions, so they cannot be removed even by hand afterwards.

component .on .off
Planner.vue:305,316,321,325,330 5 0
PlannerFactoryList.vue:244,248 2 0
StatisticsFactorySummary.vue:513,524 2 0
Statistics.vue:138 1 0

PlannerFactoryList mounts twice per Planner mount (docked sidebar plus the teleported mobile drawer), so that alone is 4 leaked handlers per navigation. Statistics and StatisticsFactorySummary sit behind v-if inside Planner, so they also remount on every plan load and tab switch.

OptionsDialog.vue:609-610 and BuildingGroups.vue:377-383 are the counter-examples and are correctly paired — the latter carries a comment explaining exactly why it matters.

Evidence

Mount / unmount / remount spec counting eventBus.all:

after 1 mount of PlannerFactoryList : prepareForLoad handlers 0 -> 1
after unmount                       : still 1   (expected 0)
after 5 mount/unmount cycles        : incrementLoad handlers 1 -> 6
after 20 mount/unmount of StatisticsFactorySummary : {"fs":20,"sec":20}, unchanged after unmount

Effect

No visible misbehaviour today — the dead closures write to orphaned refs. The real costs are:

  1. Retention. Every dead component scope stays reachable, including props.factories and a statuses Map computed over the whole plan.
  2. Repeated work on load. loadingCompleted / plannerShow on each dead Planner re-runs showPlan(), which calls resyncWorldResources() (a full pass over every factory x product with a gameData.recipes.find() per product) and schedules updateActiveFactory (a getElementById per factory plus per group band). After N navigations, every plan load does that work N+1 times.
  3. A latent race. showPlan() consumes and deletes sessionStorage['navigateToFactory'], so the oldest dead instance claims the Parts-page deep link before the live one reads it. It still works only because scrollToElement is pure DOM.

Suggested fix

Name the handlers and eventBus.off them in onUnmounted, following BuildingGroups.vue.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Needs TriageIssues awaiting assignment of priority and classificationP3Minor issue, workarounds, minor features nice to havesbugSomething isn't workingwebsiteIssues relating to the website

    Type

    No type

    Projects

    • Status
      Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions