plugged memory leaks

This commit is contained in:
Eugene Pankov
2021-05-13 16:40:23 +02:00
parent c98fd2042d
commit 5c22e22caa
18 changed files with 211 additions and 144 deletions

View File

@@ -1,3 +1,4 @@
export { BaseComponent, SubscriptionContainer } from '../components/base.component'
export { BaseTabComponent, BaseTabProcess } from '../components/baseTab.component'
export { TabHeaderComponent } from '../components/tabHeader.component'
export { SplitTabComponent, SplitContainer } from '../components/splitTab.component'

View File

@@ -0,0 +1,54 @@
import { Observable, Subscription } from 'rxjs'
interface CancellableEvent {
element: HTMLElement
event: string
handler: EventListenerOrEventListenerObject
options?: boolean|AddEventListenerOptions
}
export class SubscriptionContainer {
private subscriptions: Subscription[] = []
private events: CancellableEvent[] = []
addEventListener (element: HTMLElement, event: string, handler: EventListenerOrEventListenerObject, options?: boolean|AddEventListenerOptions): void {
element.addEventListener(event, handler, options)
this.events.push({
element,
event,
handler,
options,
})
}
subscribe <T> (observable: Observable<T>, handler: (v: T) => void): void {
this.subscriptions.push(observable.subscribe(handler))
}
cancelAll (): void {
for (const s of this.subscriptions) {
s.unsubscribe()
}
for (const e of this.events) {
e.element.removeEventListener(e.event, e.handler, e.options)
}
this.subscriptions = []
this.events = []
}
}
export class BaseComponent {
private subscriptionContainer = new SubscriptionContainer()
addEventListenerUntilDestroyed (element: HTMLElement, event: string, handler: EventListenerOrEventListenerObject, options?: boolean|AddEventListenerOptions): void {
this.subscriptionContainer.addEventListener(element, event, handler, options)
}
subscribeUntilDestroyed <T> (observable: Observable<T>, handler: (v: T) => void): void {
this.subscriptionContainer.subscribe(observable, handler)
}
ngOnDestroy (): void {
this.subscriptionContainer.cancelAll()
}
}

View File

@@ -1,6 +1,7 @@
import { Observable, Subject } from 'rxjs'
import { ViewRef } from '@angular/core'
import { RecoveryToken } from '../api/tabRecovery'
import { BaseComponent } from './base.component'
/**
* Represents an active "process" inside a tab,
@@ -13,7 +14,7 @@ export interface BaseTabProcess {
/**
* Abstract base class for custom tab components
*/
export abstract class BaseTabComponent {
export abstract class BaseTabComponent extends BaseComponent {
/**
* Parent tab (usually a SplitTabComponent)
*/
@@ -69,6 +70,7 @@ export abstract class BaseTabComponent {
get recoveryStateChangedHint$ (): Observable<void> { return this.recoveryStateChangedHint }
protected constructor () {
super()
this.focused$.subscribe(() => {
this.hasFocus = true
})
@@ -158,10 +160,17 @@ export abstract class BaseTabComponent {
this.blurred.complete()
this.titleChange.complete()
this.progress.complete()
this.activity.complete()
this.recoveryStateChangedHint.complete()
if (!skipDestroyedEvent) {
this.destroyed.next()
}
this.destroyed.complete()
}
/** @hidden */
ngOnDestroy (): void {
this.destroy()
super.ngOnDestroy()
}
}

View File

@@ -1,4 +1,4 @@
import { Observable, Subject, Subscription } from 'rxjs'
import { Observable, Subject } from 'rxjs'
import { Component, Injectable, ViewChild, ViewContainerRef, EmbeddedViewRef, AfterViewInit, OnDestroy } from '@angular/core'
import { BaseTabComponent, BaseTabProcess } from './baseTab.component'
import { TabRecoveryProvider, RecoveredTab, RecoveryToken } from '../api/tabRecovery'
@@ -163,7 +163,6 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
/** @hidden */
private focusedTab: BaseTabComponent|null = null
private maximizedTab: BaseTabComponent|null = null
private hotkeysSubscription: Subscription
private viewRefs: Map<BaseTabComponent, EmbeddedViewRef<any>> = new Map()
private tabAdded = new Subject<BaseTabComponent>()
@@ -210,7 +209,7 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
})
this.blurred$.subscribe(() => this.getAllTabs().forEach(x => x.emitBlurred()))
this.hotkeysSubscription = this.hotkeys.matchedHotkey.subscribe(hotkey => {
this.subscribeUntilDestroyed(this.hotkeys.matchedHotkey, hotkey => {
if (!this.hasFocus || !this.focusedTab) {
return
}
@@ -272,7 +271,9 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
/** @hidden */
ngOnDestroy (): void {
this.hotkeysSubscription.unsubscribe()
this.tabAdded.complete()
this.tabRemoved.complete()
super.ngOnDestroy()
}
/** @returns Flat list of all sub-tabs */
@@ -497,18 +498,18 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
const ref = this.viewContainer.insert(tab.hostView) as EmbeddedViewRef<any> // eslint-disable-line @typescript-eslint/no-unnecessary-type-assertion
this.viewRefs.set(tab, ref)
ref.rootNodes[0].addEventListener('click', () => this.focus(tab))
tab.addEventListenerUntilDestroyed(ref.rootNodes[0], 'click', () => this.focus(tab))
tab.titleChange$.subscribe(t => this.setTitle(t))
tab.activity$.subscribe(a => a ? this.displayActivity() : this.clearActivity())
tab.progress$.subscribe(p => this.setProgress(p))
tab.subscribeUntilDestroyed(tab.titleChange$, t => this.setTitle(t))
tab.subscribeUntilDestroyed(tab.activity$, a => a ? this.displayActivity() : this.clearActivity())
tab.subscribeUntilDestroyed(tab.progress$, p => this.setProgress(p))
if (tab.title) {
this.setTitle(tab.title)
}
tab.recoveryStateChangedHint$.subscribe(() => {
tab.subscribeUntilDestroyed(tab.recoveryStateChangedHint$, () => {
this.recoveryStateChangedHint.next()
})
tab.destroyed$.subscribe(() => {
tab.subscribeUntilDestroyed(tab.destroyed$, () => {
this.removeTab(tab)
})
}

View File

@@ -11,6 +11,7 @@ import { ElectronService } from '../services/electron.service'
import { AppService } from '../services/app.service'
import { HostAppService, Platform } from '../services/hostApp.service'
import { ConfigService } from '../services/config.service'
import { BaseComponent } from './base.component'
/** @hidden */
export interface SortableComponentProxy {
@@ -23,7 +24,7 @@ export interface SortableComponentProxy {
template: require('./tabHeader.component.pug'),
styles: [require('./tabHeader.component.scss')],
})
export class TabHeaderComponent {
export class TabHeaderComponent extends BaseComponent {
@Input() index: number
@Input() @HostBinding('class.active') active: boolean
@Input() tab: BaseTabComponent
@@ -41,7 +42,8 @@ export class TabHeaderComponent {
@Inject(SortableComponent) private parentDraggable: SortableComponentProxy,
@Optional() @Inject(TabContextMenuItemProvider) protected contextMenuProviders: TabContextMenuItemProvider[],
) {
this.hotkeys.matchedHotkey.subscribe((hotkey) => {
super()
this.subscribeUntilDestroyed(this.hotkeys.matchedHotkey, (hotkey) => {
if (this.app.activeTab === this.tab) {
if (hotkey === 'rename-tab') {
this.showRenameTabModal()
@@ -52,7 +54,7 @@ export class TabHeaderComponent {
}
ngOnInit () {
this.tab.progress$.subscribe(progress => {
this.subscribeUntilDestroyed(this.tab.progress$, progress => {
this.zone.run(() => {
this.progress = progress
})

View File

@@ -1,7 +1,7 @@
import { Injectable, Inject, NgZone, EventEmitter } from '@angular/core'
import { Observable, Subject } from 'rxjs'
import { HotkeyDescription, HotkeyProvider } from '../api/hotkeyProvider'
import { stringifyKeySequence } from './hotkeys.util'
import { stringifyKeySequence, EventData } from './hotkeys.util'
import { ConfigService } from './config.service'
import { ElectronService } from './electron.service'
import { HostAppService } from './hostApp.service'
@@ -14,10 +14,6 @@ export interface PartialHotkeyMatch {
const KEY_TIMEOUT = 2000
interface EventBufferEntry {
event: KeyboardEvent
time: number
}
@Injectable({ providedIn: 'root' })
export class HotkeysService {
@@ -32,7 +28,7 @@ export class HotkeysService {
get hotkey$ (): Observable<string> { return this._hotkey }
private _hotkey = new Subject<string>()
private currentKeystrokes: EventBufferEntry[] = []
private currentKeystrokes: EventData[] = []
private disabledLevel = 0
private hotkeyDescriptions: HotkeyDescription[] = []
@@ -73,7 +69,16 @@ export class HotkeysService {
*/
pushKeystroke (name: string, nativeEvent: KeyboardEvent): void {
(nativeEvent as any).event = name
this.currentKeystrokes.push({ event: nativeEvent, time: performance.now() })
this.currentKeystrokes.push({
ctrlKey: nativeEvent.ctrlKey,
metaKey: nativeEvent.metaKey,
altKey: nativeEvent.altKey,
shiftKey: nativeEvent.shiftKey,
code: nativeEvent.code,
key: nativeEvent.key,
eventName: name,
time: performance.now(),
})
}
/**
@@ -104,7 +109,7 @@ export class HotkeysService {
getCurrentKeystrokes (): string[] {
this.currentKeystrokes = this.currentKeystrokes.filter(x => performance.now() - x.time < KEY_TIMEOUT)
return stringifyKeySequence(this.currentKeystrokes.map(x => x.event))
return stringifyKeySequence(this.currentKeystrokes)
}
getCurrentFullyMatchedHotkey (): string|null {

View File

@@ -10,15 +10,26 @@ export const altKeyName = {
linux: 'Alt',
}[process.platform]
export interface EventData {
ctrlKey: boolean
metaKey: boolean
altKey: boolean
shiftKey: boolean
key: string
code: string
eventName: string
time: number
}
const REGEX_LATIN_KEYNAME = /^[A-Za-z]$/
export function stringifyKeySequence (events: KeyboardEvent[]): string[] {
export function stringifyKeySequence (events: EventData[]): string[] {
const items: string[] = []
events = events.slice()
while (events.length > 0) {
const event = events.shift()!
if ((event as any).event === 'keydown') {
if (event.eventName === 'keydown') {
const itemKeys: string[] = []
if (event.ctrlKey) {
itemKeys.push('Ctrl')