From f5b2b02c77ffc572f25d39f663126eca977cb29b Mon Sep 17 00:00:00 2001 From: Shane McLaughlin Date: Fri, 7 Jun 2024 16:14:21 -0500 Subject: [PATCH] fix: non-reference clone of transferred listeners (#1080) * fix: structured clone of transferred listeners * test: map reference breaking --- src/lifecycleEvents.ts | 19 ++++++++++++++----- test/unit/lifecycleEventsTest.ts | 28 +++++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/src/lifecycleEvents.ts b/src/lifecycleEvents.ts index 64f5a82176..3cc5431f15 100644 --- a/src/lifecycleEvents.ts +++ b/src/lifecycleEvents.ts @@ -15,7 +15,9 @@ import { Logger } from './logger/logger'; // Data of any type can be passed to the callback. Can be cast to any type that is given in emit(). // eslint-disable-next-line @typescript-eslint/no-explicit-any -type callback = (data: any) => Promise; +export type callback = (data: any) => Promise; +type ListenerMap = Map; +export type UniqueListenerMap = Map; declare const global: { salesforceCoreLifecycle?: Lifecycle; @@ -49,7 +51,7 @@ export class Lifecycle { private constructor( private readonly listeners: Dictionary = {}, - private readonly uniqueListeners: Map> = new Map>() + private readonly uniqueListeners: UniqueListenerMap = new Map>() ) {} /** @@ -89,8 +91,11 @@ export class Lifecycle { ) { const oldInstance = global.salesforceCoreLifecycle; // use the newer version and transfer any listeners from the old version - // object spread keeps them from being references - global.salesforceCoreLifecycle = new Lifecycle({ ...oldInstance.listeners }, oldInstance.uniqueListeners); + // object spread and the clone fn keep them from being references + global.salesforceCoreLifecycle = new Lifecycle( + { ...oldInstance.listeners }, + cloneUniqueListeners(oldInstance.uniqueListeners) + ); // clean up any listeners on the old version Object.keys(oldInstance.listeners).map((eventName) => { oldInstance.removeAllListeners(eventName); @@ -176,7 +181,7 @@ export class Lifecycle { if (uniqueListenerIdentifier) { if (!this.uniqueListeners.has(eventName)) { // nobody is listening to the event yet - this.uniqueListeners.set(eventName, new Map([[uniqueListenerIdentifier, cb]])); + this.uniqueListeners.set(eventName, new Map([[uniqueListenerIdentifier, cb]])); } else if (!this.uniqueListeners.get(eventName)?.has(uniqueListenerIdentifier)) { // the unique listener identifier is not already registered this.uniqueListeners.get(eventName)?.set(uniqueListenerIdentifier, cb); @@ -231,3 +236,7 @@ export class Lifecycle { } } } + +const cloneListeners: (listeners: ListenerMap) => ListenerMap = (listeners) => new Map(Array.from(listeners.entries())); +export const cloneUniqueListeners = (uniqueListeners: UniqueListenerMap): UniqueListenerMap => + new Map(Array.from(uniqueListeners.entries()).map(([key, value]) => [key, cloneListeners(value)])); diff --git a/test/unit/lifecycleEventsTest.ts b/test/unit/lifecycleEventsTest.ts index 786a49fe9c..07e08edb91 100644 --- a/test/unit/lifecycleEventsTest.ts +++ b/test/unit/lifecycleEventsTest.ts @@ -9,7 +9,7 @@ import { Duration, sleep } from '@salesforce/kit/lib/duration'; import { spyMethod } from '@salesforce/ts-sinon'; import * as chai from 'chai'; -import { Lifecycle } from '../../src/lifecycleEvents'; +import { Lifecycle, callback, cloneUniqueListeners } from '../../src/lifecycleEvents'; import { TestContext } from '../../src/testSetup'; import { Logger } from '../../src/logger/logger'; @@ -257,3 +257,29 @@ describe('lifecycleEvents', () => { lifecycle2.removeAllListeners('test7'); }); }); + +describe('listener map cloning', () => { + const cb = (): Promise => Promise.resolve(); + it('clones map, breaking event name reference', () => { + const map1 = new Map>(); + map1.set('evt', new Map([['uniqueId', cb]])); + + const map2 = cloneUniqueListeners(map1); + chai.expect(map2).to.deep.equal(map1); + map1.delete('evt'); + chai.expect(map1.has('evt')).to.be.false; + chai.expect(map2.has('evt')).to.be.true; + }); + it('clones map, breaking uniqueId reference', () => { + const map1 = new Map>(); + map1.set('evt', new Map([['uniqueId', cb]])); + + const map2 = cloneUniqueListeners(map1); + chai.expect(map2).to.deep.equal(map1); + map2.get('evt')?.set('uniqueId2', cb); + chai.expect(map1.has('evt')).to.be.true; + chai.expect(map2.has('evt')).to.be.true; + chai.expect(map1.get('evt')?.has('uniqueId2')).to.be.false; + chai.expect(map2.get('evt')?.has('uniqueId2')).to.be.true; + }); +});