Skip to content

promo-manager: invalidateBaseState mutates shared defaultBaseState — stale queue entries leak and finished promos can be re-shown #215

Description

@vanilla-wave

Problem

invalidateBaseState assigns the module-level object by reference:

this.state.base = defaultBaseState;
promoToValidate.forEach((slug) => this.addPromoToActiveQueue(slug)); // pushes into the shared object

The queue entries are pushed into the shared defaultBaseState.activeQueue and stay there forever:

  1. Same instance: on the next invalidateBaseState call the base is reset to the polluted default, resurrecting slugs that were queued during the previous invalidation — including promos that have been finished since then. triggerNextPromo doesn't check status, so a finished promo can be shown again (two visibilitychange events with PromoTabSyncPlugin + __UNSTABLE__syncState are enough).
  2. Cross-instance: any Controller created afterwards in the same JS process (e.g. createSurveyManager alongside createPromoManager, or controller re-creation) deep-clones the polluted object in its constructor and starts with a foreign queue.

Repro

const controller = new Controller(testOptions);
await controller.requestStart('boardPoll');
await controller.requestStart('ganttPoll');
controller['invalidateBaseState']();

const fresh = new Controller(testOptions);
expect(fresh.state.base.activeQueue).toEqual([]); // FAILS: ['ganttPoll']

Where

  • src/promo-manager/core/controller.tsinvalidateBaseState (this.state.base = defaultBaseState)

Fix: reset via a deep copy (JSON.parse(JSON.stringify(defaultBaseState)) or a factory returning a fresh object).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions