mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-01-23 02:36:05 +00:00
fix: prevent memory leaks from unmanaged subscriptions and event listeners
- LocalBackupService: add takeUntilDestroyed to backup interval subscription - GlobalProgressBarService: add takeUntilDestroyed to dirty countdown subscription - PluginBridgeService: extract window/document listeners to named methods with cleanup - shepherd-helper: add take(1) to waitForElObs$, add 10s timeout to waitForEl - plugin.service: add max attempts (100) to plugin loading poll interval
This commit is contained in:
parent
a93f6a7ba2
commit
c5625317bf
5 changed files with 59 additions and 23 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import { Injectable, signal } from '@angular/core';
|
||||
import { toObservable } from '@angular/core/rxjs-interop';
|
||||
import { DestroyRef, inject, Injectable, signal } from '@angular/core';
|
||||
import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';
|
||||
import { EMPTY, Observable, of, timer } from 'rxjs';
|
||||
import {
|
||||
delay,
|
||||
|
|
@ -26,6 +26,8 @@ interface CountUpOptions {
|
|||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class GlobalProgressBarService {
|
||||
private _destroyRef = inject(DestroyRef);
|
||||
|
||||
// Use signals internally
|
||||
private _nrOfRequests = signal(0);
|
||||
private _label = signal<GlobalProgressBarLabel | null>(null);
|
||||
|
|
@ -66,7 +68,7 @@ export class GlobalProgressBarService {
|
|||
);
|
||||
|
||||
constructor() {
|
||||
this._dirtyCountdown$.subscribe();
|
||||
this._dirtyCountdown$.pipe(takeUntilDestroyed(this._destroyRef)).subscribe();
|
||||
}
|
||||
|
||||
countUp(url: string, options?: CountUpOptions): void {
|
||||
|
|
|
|||
|
|
@ -1,18 +1,24 @@
|
|||
import { Observable, Subject, timer } from 'rxjs';
|
||||
import { filter, first, map, takeUntil, tap } from 'rxjs/operators';
|
||||
import { filter, first, map, take, takeUntil, tap } from 'rxjs/operators';
|
||||
import { ShepherdService } from './shepherd.service';
|
||||
import Step from 'shepherd.js/src/types/step';
|
||||
import StepOptionsWhen = Step.StepOptionsWhen;
|
||||
import { TourId } from './shepherd-steps.const';
|
||||
import { Log } from '../../core/log';
|
||||
|
||||
const MAX_WAIT_TIME = 10000; // 10 seconds max wait
|
||||
|
||||
export const waitForEl = (selector: string, cb: () => void): number => {
|
||||
const startTime = Date.now();
|
||||
const int = window.setInterval(() => {
|
||||
Log.log('INT');
|
||||
|
||||
if (document.querySelector(selector)) {
|
||||
window.clearInterval(int);
|
||||
cb();
|
||||
} else if (Date.now() - startTime > MAX_WAIT_TIME) {
|
||||
window.clearInterval(int);
|
||||
Log.warn(`waitForEl: Timeout waiting for selector "${selector}"`);
|
||||
}
|
||||
}, 50);
|
||||
return int;
|
||||
|
|
@ -22,6 +28,7 @@ export const waitForElObs$ = (selector: string): Observable<any> => {
|
|||
return timer(50, 50).pipe(
|
||||
map(() => document.querySelector(selector)),
|
||||
filter((el) => !!el),
|
||||
take(1),
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { inject, Injectable } from '@angular/core';
|
||||
import { DestroyRef, inject, Injectable } from '@angular/core';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { GlobalConfigService } from '../../features/config/global-config.service';
|
||||
import { interval, Observable } from 'rxjs';
|
||||
import { LocalBackupConfig } from '../../features/config/global-config.model';
|
||||
|
|
@ -24,6 +25,7 @@ const ANDROID_DB_KEY = 'backup';
|
|||
providedIn: 'root',
|
||||
})
|
||||
export class LocalBackupService {
|
||||
private _destroyRef = inject(DestroyRef);
|
||||
private _configService = inject(GlobalConfigService);
|
||||
private _stateSnapshotService = inject(StateSnapshotService);
|
||||
private _backupService = inject(BackupService);
|
||||
|
|
@ -40,7 +42,7 @@ export class LocalBackupService {
|
|||
);
|
||||
|
||||
init(): void {
|
||||
this._triggerBackupSave$.subscribe();
|
||||
this._triggerBackupSave$.pipe(takeUntilDestroyed(this._destroyRef)).subscribe();
|
||||
}
|
||||
|
||||
checkBackupAvailable(): Promise<boolean | LocalBackupMeta> {
|
||||
|
|
|
|||
|
|
@ -1156,27 +1156,41 @@ export class PluginBridgeService implements OnDestroy {
|
|||
private _isWindowFocused = true;
|
||||
private _windowFocusHandlers = new Map<string, (isFocused: boolean) => void>();
|
||||
|
||||
// Named listener methods for proper cleanup
|
||||
private _onWindowFocus = (): void => {
|
||||
this._isWindowFocused = true;
|
||||
this._notifyFocusHandlers(true);
|
||||
};
|
||||
|
||||
private _onWindowBlur = (): void => {
|
||||
this._isWindowFocused = false;
|
||||
this._notifyFocusHandlers(false);
|
||||
};
|
||||
|
||||
private _onVisibilityChange = (): void => {
|
||||
const isFocused = !document.hidden;
|
||||
this._isWindowFocused = isFocused;
|
||||
this._notifyFocusHandlers(isFocused);
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize window focus tracking
|
||||
*/
|
||||
private _initWindowFocusTracking(): void {
|
||||
// Track window focus/blur events
|
||||
window.addEventListener('focus', () => {
|
||||
this._isWindowFocused = true;
|
||||
this._notifyFocusHandlers(true);
|
||||
});
|
||||
|
||||
window.addEventListener('blur', () => {
|
||||
this._isWindowFocused = false;
|
||||
this._notifyFocusHandlers(false);
|
||||
});
|
||||
|
||||
window.addEventListener('focus', this._onWindowFocus);
|
||||
window.addEventListener('blur', this._onWindowBlur);
|
||||
// Also track document visibility changes
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
const isFocused = !document.hidden;
|
||||
this._isWindowFocused = isFocused;
|
||||
this._notifyFocusHandlers(isFocused);
|
||||
});
|
||||
document.addEventListener('visibilitychange', this._onVisibilityChange);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up window focus tracking listeners
|
||||
*/
|
||||
private _cleanupWindowFocusTracking(): void {
|
||||
window.removeEventListener('focus', this._onWindowFocus);
|
||||
window.removeEventListener('blur', this._onWindowBlur);
|
||||
document.removeEventListener('visibilitychange', this._onVisibilityChange);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1412,6 +1426,7 @@ export class PluginBridgeService implements OnDestroy {
|
|||
*/
|
||||
ngOnDestroy(): void {
|
||||
PluginLog.log('PluginBridgeService: Cleaning up resources');
|
||||
this._cleanupWindowFocusTracking();
|
||||
// Note: Signals don't need explicit cleanup like BehaviorSubjects
|
||||
PluginLog.log('PluginBridgeService: Cleanup complete');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -285,15 +285,25 @@ export class PluginService implements OnDestroy {
|
|||
|
||||
// If currently loading, wait for it
|
||||
if (state.status === 'loading') {
|
||||
// Wait for status to change
|
||||
await new Promise<void>((resolve) => {
|
||||
// Wait for status to change (max 10 seconds)
|
||||
const maxAttempts = 100;
|
||||
let attempts = 0;
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const checkStatus = setInterval(() => {
|
||||
attempts++;
|
||||
if (attempts > maxAttempts) {
|
||||
clearInterval(checkStatus);
|
||||
reject(new Error(`Plugin loading timeout after ${maxAttempts * 100}ms`));
|
||||
return;
|
||||
}
|
||||
const currentState = this._getPluginState(pluginId);
|
||||
if (currentState && currentState.status !== 'loading') {
|
||||
clearInterval(checkStatus);
|
||||
resolve();
|
||||
}
|
||||
}, 100);
|
||||
}).catch((err) => {
|
||||
console.error('Plugin activation error:', err);
|
||||
});
|
||||
|
||||
const updatedState = this._getPluginState(pluginId);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue