fix(core-app-api): fix selector memory leak
Signed-off-by: Sanchit2662 <sanchit2662@gmail.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core-app-api': patch
|
||||
---
|
||||
|
||||
Fixed memory leak caused by duplicate `AppThemeSelector` instances and missing cleanup in `AppThemeSelector` and `AppLanguageSelector`. Added `dispose()` method to both selectors for proper resource cleanup.
|
||||
+28
-3
@@ -69,20 +69,25 @@ export class AppLanguageSelector implements AppLanguageApi {
|
||||
selector.setLanguage(storedLanguage);
|
||||
}
|
||||
|
||||
selector.language$().subscribe(({ language }) => {
|
||||
const subscription = selector.language$().subscribe(({ language }) => {
|
||||
if (language !== window.localStorage.getItem(STORAGE_KEY)) {
|
||||
window.localStorage.setItem(STORAGE_KEY, language);
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('storage', event => {
|
||||
const storageListener = (event: StorageEvent) => {
|
||||
if (event.key === STORAGE_KEY) {
|
||||
const language = localStorage.getItem(STORAGE_KEY) ?? undefined;
|
||||
if (language) {
|
||||
selector.setLanguage(language);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
window.addEventListener('storage', storageListener);
|
||||
|
||||
// Store cleanup references for potential disposal
|
||||
selector.#storageSubscription = subscription;
|
||||
selector.#storageListener = storageListener;
|
||||
|
||||
return selector;
|
||||
}
|
||||
@@ -91,6 +96,10 @@ export class AppLanguageSelector implements AppLanguageApi {
|
||||
#language: string;
|
||||
#subject: BehaviorSubject<{ language: string }>;
|
||||
|
||||
// References for cleanup when using createWithStorage
|
||||
#storageSubscription?: { unsubscribe(): void };
|
||||
#storageListener?: (event: StorageEvent) => void;
|
||||
|
||||
private constructor(languages: string[], initialLanguage: string) {
|
||||
this.#languages = languages;
|
||||
this.#language = initialLanguage;
|
||||
@@ -126,4 +135,20 @@ export class AppLanguageSelector implements AppLanguageApi {
|
||||
language$(): Observable<{ language: string }> {
|
||||
return this.#subject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up resources created by createWithStorage().
|
||||
* Call this method when the selector is no longer needed to prevent memory leaks.
|
||||
* This is particularly useful in testing scenarios or when the app is unmounted.
|
||||
*/
|
||||
dispose(): void {
|
||||
if (this.#storageSubscription) {
|
||||
this.#storageSubscription.unsubscribe();
|
||||
this.#storageSubscription = undefined;
|
||||
}
|
||||
if (this.#storageListener) {
|
||||
window.removeEventListener('storage', this.#storageListener);
|
||||
this.#storageListener = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ export class AppThemeSelector implements AppThemeApi {
|
||||
|
||||
selector.setActiveThemeId(initialThemeId);
|
||||
|
||||
selector.activeThemeId$().subscribe(themeId => {
|
||||
const subscription = selector.activeThemeId$().subscribe(themeId => {
|
||||
if (themeId) {
|
||||
window.localStorage.setItem(STORAGE_KEY, themeId);
|
||||
} else {
|
||||
@@ -47,12 +47,17 @@ export class AppThemeSelector implements AppThemeApi {
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('storage', event => {
|
||||
const storageListener = (event: StorageEvent) => {
|
||||
if (event.key === STORAGE_KEY) {
|
||||
const themeId = localStorage.getItem(STORAGE_KEY) ?? undefined;
|
||||
selector.setActiveThemeId(themeId);
|
||||
}
|
||||
});
|
||||
};
|
||||
window.addEventListener('storage', storageListener);
|
||||
|
||||
// Store cleanup references for potential disposal
|
||||
selector.#storageSubscription = subscription;
|
||||
selector.#storageListener = storageListener;
|
||||
|
||||
return selector;
|
||||
}
|
||||
@@ -60,6 +65,10 @@ export class AppThemeSelector implements AppThemeApi {
|
||||
private activeThemeId: string | undefined;
|
||||
private readonly subject = new BehaviorSubject<string | undefined>(undefined);
|
||||
|
||||
// References for cleanup when using createWithStorage
|
||||
#storageSubscription?: { unsubscribe(): void };
|
||||
#storageListener?: (event: StorageEvent) => void;
|
||||
|
||||
constructor(private readonly themes: AppTheme[]) {}
|
||||
|
||||
getInstalledThemes(): AppTheme[] {
|
||||
@@ -78,4 +87,20 @@ export class AppThemeSelector implements AppThemeApi {
|
||||
this.activeThemeId = themeId;
|
||||
this.subject.next(themeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up resources created by createWithStorage().
|
||||
* Call this method when the selector is no longer needed to prevent memory leaks.
|
||||
* This is particularly useful in testing scenarios or when the app is unmounted.
|
||||
*/
|
||||
dispose(): void {
|
||||
if (this.#storageSubscription) {
|
||||
this.#storageSubscription.unsubscribe();
|
||||
this.#storageSubscription = undefined;
|
||||
}
|
||||
if (this.#storageListener) {
|
||||
window.removeEventListener('storage', this.#storageListener);
|
||||
this.#storageListener = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,6 +171,7 @@ export class AppManager implements BackstageApp {
|
||||
private readonly defaultApis: Iterable<AnyApiFactory>;
|
||||
private readonly bindRoutes: AppOptions['bindRoutes'];
|
||||
private readonly appLanguageApi: AppLanguageApi;
|
||||
private readonly appThemeApi: AppThemeApi;
|
||||
private readonly translationResources: Array<
|
||||
TranslationResource | TranslationMessages
|
||||
>;
|
||||
@@ -194,6 +195,9 @@ export class AppManager implements BackstageApp {
|
||||
availableLanguages:
|
||||
options.__experimentalTranslations?.availableLanguages,
|
||||
});
|
||||
// Create a single AppThemeSelector instance to be shared between
|
||||
// the loading phase and the main app, avoiding duplicate event listeners
|
||||
this.appThemeApi = AppThemeSelector.createWithStorage(this.themes);
|
||||
this.translationResources =
|
||||
options.__experimentalTranslations?.resources ?? [];
|
||||
}
|
||||
@@ -240,10 +244,9 @@ export class AppManager implements BackstageApp {
|
||||
|
||||
const Provider = ({ children }: PropsWithChildren<{}>) => {
|
||||
const needsFeatureFlagRegistrationRef = useRef(true);
|
||||
const appThemeApi = useMemo(
|
||||
() => AppThemeSelector.createWithStorage(this.themes),
|
||||
[],
|
||||
);
|
||||
// Use the shared AppThemeSelector instance created in the constructor
|
||||
// to avoid creating duplicate event listeners and subscriptions
|
||||
const appThemeApi = this.appThemeApi;
|
||||
|
||||
const { routing, featureFlags } = useMemo(() => {
|
||||
const usesReactRouterBeta = isReactRouterBeta();
|
||||
@@ -436,7 +439,8 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be
|
||||
this.apiFactoryRegistry.register('static', {
|
||||
api: appThemeApiRef,
|
||||
deps: {},
|
||||
factory: () => AppThemeSelector.createWithStorage(this.themes),
|
||||
// Use the shared AppThemeSelector instance to avoid duplicate event listeners
|
||||
factory: () => this.appThemeApi,
|
||||
});
|
||||
this.apiFactoryRegistry.register('static', {
|
||||
api: configApiRef,
|
||||
|
||||
Reference in New Issue
Block a user