diff --git a/packages/ag-charts-community/src/chart/chartContext.ts b/packages/ag-charts-community/src/chart/chartContext.ts index 969c2d1d5f..ac247424d2 100644 --- a/packages/ag-charts-community/src/chart/chartContext.ts +++ b/packages/ag-charts-community/src/chart/chartContext.ts @@ -2,6 +2,7 @@ import type { ModuleContext } from '../module/moduleContext'; import type { Group } from '../scene/group'; import { Scene } from '../scene/scene'; import { CallbackCache } from '../util/callbackCache'; +import { ObjectDestroyer } from '../util/destroy'; import type { Mutex } from '../util/mutex'; import { AnnotationManager } from './annotation/annotationManager'; import type { ChartService } from './chartService'; @@ -59,6 +60,8 @@ export class ChartContext implements ModuleContext { tooltipManager: TooltipManager; zoomManager: ZoomManager; + private readonly owned: ObjectDestroyer; + constructor( chart: ChartService & { zoomManager: ZoomManager; annotationRoot: Group; keyboard: Keyboard; tooltip: Tooltip }, vars: { @@ -78,48 +81,44 @@ export class ChartContext implements ModuleContext { scene?.setContainer(this.domManager); this.scene = scene ?? new Scene({ pixelRatio: overrideDevicePixelRatio, domManager: this.domManager }); - this.annotationManager = new AnnotationManager(chart.annotationRoot); - this.ariaAnnouncementService = new AriaAnnouncementService(this.scene.canvas.element); - this.chartEventManager = new ChartEventManager(); - this.contextMenuRegistry = new ContextMenuRegistry(); - this.cursorManager = new CursorManager(this.domManager); - this.highlightManager = new HighlightManager(); - this.interactionManager = new InteractionManager(chart.keyboard, this.domManager); - this.keyNavManager = new KeyNavManager(this.interactionManager, this.domManager); - this.focusIndicator = new FocusIndicator(this.domManager); - this.regionManager = new RegionManager(this.interactionManager, this.keyNavManager, this.focusIndicator); - this.toolbarManager = new ToolbarManager(); - this.gestureDetector = new GestureDetector(this.domManager); - this.layoutService = new LayoutService(); - this.updateService = new UpdateService(updateCallback); - this.proxyInteractionService = new ProxyInteractionService(this.updateService, this.focusIndicator); - this.seriesStateManager = new SeriesStateManager(); - this.callbackCache = new CallbackCache(); - - this.animationManager = new AnimationManager(this.interactionManager, updateMutex); - this.animationManager.skip(); - this.animationManager.play(); + // Sonar does not like us using assignments in expression, however this is intended. + // We want to use assignments so that the Typescript compiler can check that we are not using an + // uninitialised property, but we also want to guarantee that ObjectDestroyer knows the + // initialisation order so that it can destroy the objects in reverse. + this.owned = new ObjectDestroyer( + this.domManager, + (this.annotationManager = new AnnotationManager(chart.annotationRoot)), // NOSONAR + (this.ariaAnnouncementService = new AriaAnnouncementService(this.scene.canvas.element)), // NOSONAR + (this.chartEventManager = new ChartEventManager()), // NOSONAR + (this.contextMenuRegistry = new ContextMenuRegistry()), // NOSONAR + (this.cursorManager = new CursorManager(this.domManager)), // NOSONAR + (this.highlightManager = new HighlightManager()), // NOSONAR + (this.interactionManager = new InteractionManager(chart.keyboard, this.domManager)), // NOSONAR + (this.keyNavManager = new KeyNavManager(this.interactionManager, this.domManager)), // NOSONAR + (this.focusIndicator = new FocusIndicator(this.domManager)), // NOSONAR + (this.regionManager = new RegionManager(this.interactionManager, this.keyNavManager, this.focusIndicator)), // NOSONAR + (this.toolbarManager = new ToolbarManager()), // NOSONAR + (this.gestureDetector = new GestureDetector(this.domManager)), // NOSONAR + (this.layoutService = new LayoutService()), // NOSONAR + (this.updateService = new UpdateService(updateCallback)), // NOSONAR + (this.proxyInteractionService = new ProxyInteractionService(this.updateService, this.focusIndicator)), // NOSONAR + (this.seriesStateManager = new SeriesStateManager()), // NOSONAR + (this.callbackCache = new CallbackCache()), // NOSONAR + (this.animationManager = this.createAnimationManager(this.interactionManager, updateMutex)), // NOSONAR + (this.dataService = new DataService(this.animationManager)), // NOSONAR + (this.tooltipManager = new TooltipManager(this.domManager, chart.tooltip)) // NOSONAR + ); + } - this.dataService = new DataService(this.animationManager); - this.tooltipManager = new TooltipManager(this.domManager, chart.tooltip); + private createAnimationManager(interactionManager: InteractionManager, updateMutex: Mutex): AnimationManager { + const animationManager = new AnimationManager(interactionManager, updateMutex); + animationManager.skip(); + animationManager.play(); + return animationManager; } destroy() { // chart.ts handles the destruction of the scene and zoomManager. - this.tooltipManager.destroy(); - this.proxyInteractionService.destroy(); - this.regionManager.destroy(); - this.focusIndicator.destroy(); - this.keyNavManager.destroy(); - this.interactionManager.destroy(); - this.animationManager.stop(); - this.animationManager.destroy(); - this.ariaAnnouncementService.destroy(); - this.chartEventManager.destroy(); - this.highlightManager.destroy(); - this.callbackCache.invalidateCache(); - this.animationManager.reset(); - this.syncManager.destroy(); - this.domManager.destroy(); + this.owned.destroy(); } } diff --git a/packages/ag-charts-community/src/chart/interaction/contextMenuRegistry.ts b/packages/ag-charts-community/src/chart/interaction/contextMenuRegistry.ts index b643e70d68..f03983c8ff 100644 --- a/packages/ag-charts-community/src/chart/interaction/contextMenuRegistry.ts +++ b/packages/ag-charts-community/src/chart/interaction/contextMenuRegistry.ts @@ -1,3 +1,5 @@ +import type { Destroyable } from '../../util/destroy'; + export type ContextMenuAction = { id?: string; label: string; @@ -12,10 +14,12 @@ export type ContextMenuActionParams = { event: MouseEvent; }; -export class ContextMenuRegistry { +export class ContextMenuRegistry implements Destroyable { private readonly defaultActions: Array = []; private readonly disabledActions: Set = new Set(); + destroy() {} + public filterActions(region: string): ContextMenuAction[] { return this.defaultActions.filter((action) => ['all', region].includes(action.region)); } diff --git a/packages/ag-charts-community/src/chart/interaction/cursorManager.ts b/packages/ag-charts-community/src/chart/interaction/cursorManager.ts index c90aa936f3..775b518f1b 100644 --- a/packages/ag-charts-community/src/chart/interaction/cursorManager.ts +++ b/packages/ag-charts-community/src/chart/interaction/cursorManager.ts @@ -1,3 +1,4 @@ +import type { Destroyable } from '../../util/destroy'; import { StateTracker } from '../../util/stateTracker'; import type { DOMManager } from '../dom/domManager'; @@ -19,11 +20,13 @@ export enum Cursor { * Manages the cursor styling for an element. Tracks the requested styling from distinct * dependents and handles conflicting styling requests. */ -export class CursorManager { +export class CursorManager implements Destroyable { private readonly stateTracker = new StateTracker('default'); constructor(private readonly domManager: DOMManager) {} + destroy() {} + public updateCursor(callerId: string, style?: string) { this.stateTracker.set(callerId, style); this.domManager.updateCursor(this.stateTracker.stateValue()!); diff --git a/packages/ag-charts-community/src/chart/series/seriesStateManager.ts b/packages/ag-charts-community/src/chart/series/seriesStateManager.ts index dfb15272d7..bc0a473453 100644 --- a/packages/ag-charts-community/src/chart/series/seriesStateManager.ts +++ b/packages/ag-charts-community/src/chart/series/seriesStateManager.ts @@ -1,3 +1,5 @@ +import type { Destroyable } from '../../util/destroy'; + export type SeriesGrouping = { groupIndex: number; groupCount: number; @@ -5,7 +7,7 @@ export type SeriesGrouping = { stackCount: number; }; -export class SeriesStateManager { +export class SeriesStateManager implements Destroyable { private readonly groups: { [type: string]: { [id: string]: { @@ -15,6 +17,8 @@ export class SeriesStateManager { }; } = {}; + destroy() {} + public registerSeries({ id, seriesGrouping, diff --git a/packages/ag-charts-community/src/util/callbackCache.ts b/packages/ag-charts-community/src/util/callbackCache.ts index ef4eea6396..3f2f9448ac 100644 --- a/packages/ag-charts-community/src/util/callbackCache.ts +++ b/packages/ag-charts-community/src/util/callbackCache.ts @@ -1,8 +1,11 @@ +import type { Destroyable } from './destroy'; import { Logger } from './logger'; -export class CallbackCache { +export class CallbackCache implements Destroyable { private cache: WeakMap> = new WeakMap(); + destroy() {} + call any>(fn: F, ...params: Parameters): ReturnType | undefined { let serialisedParams: string; let paramCache = this.cache.get(fn); diff --git a/packages/ag-charts-community/src/util/destroy.ts b/packages/ag-charts-community/src/util/destroy.ts new file mode 100644 index 0000000000..517d549096 --- /dev/null +++ b/packages/ag-charts-community/src/util/destroy.ts @@ -0,0 +1,15 @@ +export type Destroyable = { destroy(): void }; + +export class ObjectDestroyer { + private readonly objs: Destroyable[]; + constructor(...objs: Destroyable[]) { + this.objs = [...objs].reverse(); + } + destroy() { + this.objs.forEach((o) => { + if ('destroy' in o && typeof o.destroy === 'function') { + o.destroy(); + } + }); + } +}