Fix memory leaks via auto-unsubscribing and DI

This commit simplifies event handling, providing a unified and robust
way to handle event lifecycling. This way, it fixes events not being
unsubscribed when state is changed.

Introduce a new function in `EventSubscriptionCollection` to remove
existing events and adding new events. This provides an easier to use
API, which leads to code that's easier to understand. It also prevents
potential bugs that may occur due to forgetting to call both functions.
It fixes `TheScriptsMenu` not unregistering events on state change.
Other improvements include:
  - Include a getter to get total amount of registered subcriptions.
    This helps in unit testing.
  - Have nullish checks to prevent potential errors further down the
    execution.
  - Use array instead of rest parameters to increase readability and
    simplify tests.

Ensure `SliderHandler` stops resizes on unmount, unsubscribing from all
events and resetting state to default.

Update `injectionKeys` to do imports as types to avoid circular
dependencies. Simplify importing `injectionKeys` to enable and strict
typings for iterating injection keys.

Add tests covering new behavior.
This commit is contained in:
undergroundwires
2023-09-01 18:14:25 +02:00
parent 19e42c9c52
commit eb096d07e2
37 changed files with 856 additions and 243 deletions

View File

@@ -66,9 +66,10 @@
<script lang="ts">
import { defineComponent, ref, inject } from 'vue';
import { useCollectionStateKey } from '@/presentation/injectionSymbols';
import { InjectionKeys } from '@/presentation/injectionSymbols';
import TooltipWrapper from '@/presentation/components/Shared/TooltipWrapper.vue';
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
import { IEventSubscription } from '@/infrastructure/Events/IEventSource';
import MenuOptionList from '../MenuOptionList.vue';
import MenuOptionListItem from '../MenuOptionListItem.vue';
import { SelectionType, SelectionTypeHandler } from './SelectionTypeHandler';
@@ -80,28 +81,27 @@ export default defineComponent({
TooltipWrapper,
},
setup() {
const { modifyCurrentState, onStateChange, events } = inject(useCollectionStateKey)();
const { modifyCurrentState, onStateChange } = inject(InjectionKeys.useCollectionState)();
const { events } = inject(InjectionKeys.useAutoUnsubscribedEvents)();
const currentSelection = ref(SelectionType.None);
let selectionTypeHandler: SelectionTypeHandler;
onStateChange(() => {
unregisterMutators();
modifyCurrentState((state) => {
registerStateMutator(state);
selectionTypeHandler = new SelectionTypeHandler(state);
updateSelections();
events.unsubscribeAllAndRegister([
subscribeAndUpdateSelections(state),
]);
});
}, { immediate: true });
function unregisterMutators() {
events.unsubscribeAll();
}
function registerStateMutator(state: ICategoryCollectionState) {
selectionTypeHandler = new SelectionTypeHandler(state);
updateSelections();
events.register(state.selection.changed.on(() => updateSelections()));
function subscribeAndUpdateSelections(
state: ICategoryCollectionState,
): IEventSubscription {
return state.selection.changed.on(() => updateSelections());
}
function selectType(type: SelectionType) {

View File

@@ -14,7 +14,7 @@
import {
defineComponent, computed, inject,
} from 'vue';
import { useApplicationKey, useCollectionStateKey } from '@/presentation/injectionSymbols';
import { InjectionKeys } from '@/presentation/injectionSymbols';
import { OperatingSystem } from '@/domain/OperatingSystem';
import MenuOptionList from './MenuOptionList.vue';
import MenuOptionListItem from './MenuOptionListItem.vue';
@@ -30,8 +30,8 @@ export default defineComponent({
MenuOptionListItem,
},
setup() {
const { modifyCurrentContext, currentState } = inject(useCollectionStateKey)();
const { application } = inject(useApplicationKey);
const { modifyCurrentContext, currentState } = inject(InjectionKeys.useCollectionState)();
const { application } = inject(InjectionKeys.useApplication);
const allOses = computed<ReadonlyArray<IOsViewModel>>(() => (
application.getSupportedOsList() ?? [])

View File

@@ -11,10 +11,11 @@
<script lang="ts">
import {
defineComponent, ref, onUnmounted, inject,
defineComponent, ref, inject,
} from 'vue';
import { useCollectionStateKey } from '@/presentation/injectionSymbols';
import { InjectionKeys } from '@/presentation/injectionSymbols';
import { IReadOnlyUserFilter } from '@/application/Context/State/Filter/IUserFilter';
import { IEventSubscription } from '@/infrastructure/Events/IEventSource';
import TheOsChanger from './TheOsChanger.vue';
import TheSelector from './Selector/TheSelector.vue';
import TheViewChanger from './View/TheViewChanger.vue';
@@ -26,31 +27,26 @@ export default defineComponent({
TheViewChanger,
},
setup() {
const { onStateChange, events } = inject(useCollectionStateKey)();
const { onStateChange } = inject(InjectionKeys.useCollectionState)();
const { events } = inject(InjectionKeys.useAutoUnsubscribedEvents)();
const isSearching = ref(false);
onStateChange((state) => {
subscribeToFilterChanges(state.filter);
events.unsubscribeAllAndRegister([
subscribeToFilterChanges(state.filter),
]);
}, { immediate: true });
onUnmounted(() => {
unsubscribeAll();
});
function subscribeToFilterChanges(filter: IReadOnlyUserFilter) {
events.register(
filter.filterChanged.on((event) => {
event.visit({
onApply: () => { isSearching.value = true; },
onClear: () => { isSearching.value = false; },
});
}),
);
}
function unsubscribeAll() {
events.unsubscribeAll();
function subscribeToFilterChanges(
filter: IReadOnlyUserFilter,
): IEventSubscription {
return filter.filterChanged.on((event) => {
event.visit({
onApply: () => { isSearching.value = true; },
onClear: () => { isSearching.value = false; },
});
});
}
return {