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:
- 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).
- 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.ts — invalidateBaseState (this.state.base = defaultBaseState)
Fix: reset via a deep copy (JSON.parse(JSON.stringify(defaultBaseState)) or a factory returning a fresh object).
Problem
invalidateBaseStateassigns the module-level object by reference:The queue entries are pushed into the shared
defaultBaseState.activeQueueand stay there forever:invalidateBaseStatecall 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.triggerNextPromodoesn't check status, so a finished promo can be shown again (twovisibilitychangeevents withPromoTabSyncPlugin+__UNSTABLE__syncStateare enough).Controllercreated afterwards in the same JS process (e.g.createSurveyManageralongsidecreatePromoManager, or controller re-creation) deep-clones the polluted object in its constructor and starts with a foreign queue.Repro
Where
src/promo-manager/core/controller.ts—invalidateBaseState(this.state.base = defaultBaseState)Fix: reset via a deep copy (
JSON.parse(JSON.stringify(defaultBaseState))or a factory returning a fresh object).