From f1e21babbfaac21903594a37e30163bfe3338279 Mon Sep 17 00:00:00 2001 From: undergroundwires Date: Thu, 4 Feb 2021 19:51:51 +0100 Subject: [PATCH] refactor event handling to consume base class for lifecycling --- src/application/Context/ApplicationContext.ts | 4 +- .../Context/IApplicationContext.ts | 4 +- .../Context/State/Code/ApplicationCode.ts | 4 +- .../Context/State/Code/IApplicationCode.ts | 4 +- .../Context/State/Filter/IUserFilter.ts | 6 +- .../Context/State/Filter/UserFilter.ts | 6 +- .../Context/State/Selection/IUserSelection.ts | 4 +- .../Context/State/Selection/UserSelection.ts | 4 +- .../Events/{Signal.ts => EventSource.ts} | 5 +- .../Events/EventSubscriptionCollection.ts | 12 ++++ .../Events/{ISignal.ts => IEventSource.ts} | 9 ++- src/infrastructure/Events/ISubscription.ts | 3 - src/infrastructure/Threading/AsyncLazy.ts | 6 +- .../CodeButtons/TheCodeButtons.vue | 19 ++--- .../Scripts/Cards/CardListItem.vue | 28 ++++---- .../Scripts/ScriptsTree/ScriptsTree.vue | 27 +++---- .../SelectableTree/Node/RevertToggle.vue | 72 +++++++++---------- src/presentation/Scripts/TheScripts.vue | 34 ++++----- src/presentation/StatefulVue.ts | 17 ++--- src/presentation/TheCodeArea.vue | 19 +---- src/presentation/TheSearchBar.vue | 23 ++---- .../EventSource.spec.ts} | 34 +++++---- .../EventSubscriptionCollection.spec.ts | 22 ++++++ 23 files changed, 171 insertions(+), 195 deletions(-) rename src/infrastructure/Events/{Signal.ts => EventSource.ts} (81%) create mode 100644 src/infrastructure/Events/EventSubscriptionCollection.ts rename src/infrastructure/Events/{ISignal.ts => IEventSource.ts} (50%) delete mode 100644 src/infrastructure/Events/ISubscription.ts rename tests/unit/infrastructure/{Signal.spec.ts => Events/EventSource.spec.ts} (75%) create mode 100644 tests/unit/infrastructure/Events/EventSubscriptionCollection.spec.ts diff --git a/src/application/Context/ApplicationContext.ts b/src/application/Context/ApplicationContext.ts index de3e9b68..19d30fb1 100644 --- a/src/application/Context/ApplicationContext.ts +++ b/src/application/Context/ApplicationContext.ts @@ -4,12 +4,12 @@ import { CategoryCollectionState } from './State/CategoryCollectionState'; import { IApplication } from '@/domain/IApplication'; import { OperatingSystem } from '@/domain/OperatingSystem'; import { ICategoryCollection } from '@/domain/ICategoryCollection'; -import { Signal } from '@/infrastructure/Events/Signal'; +import { EventSource } from '@/infrastructure/Events/EventSource'; type StateMachine = Map; export class ApplicationContext implements IApplicationContext { - public readonly contextChanged = new Signal(); + public readonly contextChanged = new EventSource(); public collection: ICategoryCollection; public currentOs: OperatingSystem; diff --git a/src/application/Context/IApplicationContext.ts b/src/application/Context/IApplicationContext.ts index ff5a943e..6d3e815e 100644 --- a/src/application/Context/IApplicationContext.ts +++ b/src/application/Context/IApplicationContext.ts @@ -1,12 +1,12 @@ import { ICategoryCollectionState } from './State/ICategoryCollectionState'; import { OperatingSystem } from '@/domain/OperatingSystem'; -import { ISignal } from '@/infrastructure/Events/ISignal'; +import { IEventSource } from '@/infrastructure/Events/IEventSource'; import { IApplication } from '@/domain/IApplication'; export interface IApplicationContext { readonly app: IApplication; readonly state: ICategoryCollectionState; - readonly contextChanged: ISignal; + readonly contextChanged: IEventSource; changeContext(os: OperatingSystem): void; } diff --git a/src/application/Context/State/Code/ApplicationCode.ts b/src/application/Context/State/Code/ApplicationCode.ts index c9b5e8f5..94214cc4 100644 --- a/src/application/Context/State/Code/ApplicationCode.ts +++ b/src/application/Context/State/Code/ApplicationCode.ts @@ -4,13 +4,13 @@ import { ICodeChangedEvent } from './Event/ICodeChangedEvent'; import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript'; import { IUserSelection } from '@/application/Context/State/Selection/IUserSelection'; import { UserScriptGenerator } from './Generation/UserScriptGenerator'; -import { Signal } from '@/infrastructure/Events/Signal'; +import { EventSource } from '@/infrastructure/Events/EventSource'; import { IApplicationCode } from './IApplicationCode'; import { IUserScriptGenerator } from './Generation/IUserScriptGenerator'; import { IScriptingDefinition } from '@/domain/IScriptingDefinition'; export class ApplicationCode implements IApplicationCode { - public readonly changed = new Signal(); + public readonly changed = new EventSource(); public current: string; private scriptPositions = new Map(); diff --git a/src/application/Context/State/Code/IApplicationCode.ts b/src/application/Context/State/Code/IApplicationCode.ts index dbafe5ce..555f28b1 100644 --- a/src/application/Context/State/Code/IApplicationCode.ts +++ b/src/application/Context/State/Code/IApplicationCode.ts @@ -1,7 +1,7 @@ import { ICodeChangedEvent } from './Event/ICodeChangedEvent'; -import { ISignal } from '@/infrastructure/Events/ISignal'; +import { IEventSource } from '@/infrastructure/Events/IEventSource'; export interface IApplicationCode { - readonly changed: ISignal; + readonly changed: IEventSource; readonly current: string; } diff --git a/src/application/Context/State/Filter/IUserFilter.ts b/src/application/Context/State/Filter/IUserFilter.ts index e87e6f6f..6de39103 100644 --- a/src/application/Context/State/Filter/IUserFilter.ts +++ b/src/application/Context/State/Filter/IUserFilter.ts @@ -1,10 +1,10 @@ -import { ISignal } from '@/infrastructure/Events/ISignal'; +import { IEventSource } from '@/infrastructure/Events/IEventSource'; import { IFilterResult } from './IFilterResult'; export interface IUserFilter { readonly currentFilter: IFilterResult | undefined; - readonly filtered: ISignal; - readonly filterRemoved: ISignal; + readonly filtered: IEventSource; + readonly filterRemoved: IEventSource; setFilter(filter: string): void; removeFilter(): void; } diff --git a/src/application/Context/State/Filter/UserFilter.ts b/src/application/Context/State/Filter/UserFilter.ts index 8fcc9ff0..32c5b2bd 100644 --- a/src/application/Context/State/Filter/UserFilter.ts +++ b/src/application/Context/State/Filter/UserFilter.ts @@ -2,12 +2,12 @@ import { IScript } from '@/domain/IScript'; import { FilterResult } from './FilterResult'; import { IFilterResult } from './IFilterResult'; import { IUserFilter } from './IUserFilter'; -import { Signal } from '@/infrastructure/Events/Signal'; +import { EventSource } from '@/infrastructure/Events/EventSource'; import { ICategoryCollection } from '@/domain/ICategoryCollection'; export class UserFilter implements IUserFilter { - public readonly filtered = new Signal(); - public readonly filterRemoved = new Signal(); + public readonly filtered = new EventSource(); + public readonly filterRemoved = new EventSource(); public currentFilter: IFilterResult | undefined; constructor(private collection: ICategoryCollection) { diff --git a/src/application/Context/State/Selection/IUserSelection.ts b/src/application/Context/State/Selection/IUserSelection.ts index c8f95599..12463114 100644 --- a/src/application/Context/State/Selection/IUserSelection.ts +++ b/src/application/Context/State/Selection/IUserSelection.ts @@ -1,10 +1,10 @@ import { SelectedScript } from './SelectedScript'; import { IScript } from '@/domain/IScript'; import { ICategory } from '@/domain/ICategory'; -import { ISignal } from '@/infrastructure/Events/ISignal'; +import { IEventSource } from '@/infrastructure/Events/IEventSource'; export interface IUserSelection { - readonly changed: ISignal>; + readonly changed: IEventSource>; readonly selectedScripts: ReadonlyArray; readonly totalSelected: number; areAllSelected(category: ICategory): boolean; diff --git a/src/application/Context/State/Selection/UserSelection.ts b/src/application/Context/State/Selection/UserSelection.ts index 82e5280c..9f3f132c 100644 --- a/src/application/Context/State/Selection/UserSelection.ts +++ b/src/application/Context/State/Selection/UserSelection.ts @@ -2,13 +2,13 @@ import { SelectedScript } from './SelectedScript'; import { IUserSelection } from './IUserSelection'; import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository'; import { IScript } from '@/domain/IScript'; -import { Signal } from '@/infrastructure/Events/Signal'; +import { EventSource } from '@/infrastructure/Events/EventSource'; import { IRepository } from '@/infrastructure/Repository/IRepository'; import { ICategory } from '@/domain/ICategory'; import { ICategoryCollection } from '@/domain/ICategoryCollection'; export class UserSelection implements IUserSelection { - public readonly changed = new Signal>(); + public readonly changed = new EventSource>(); private readonly scripts: IRepository; constructor( diff --git a/src/infrastructure/Events/Signal.ts b/src/infrastructure/Events/EventSource.ts similarity index 81% rename from src/infrastructure/Events/Signal.ts rename to src/infrastructure/Events/EventSource.ts index 3d5e8f76..20ebad9b 100644 --- a/src/infrastructure/Events/Signal.ts +++ b/src/infrastructure/Events/EventSource.ts @@ -1,7 +1,6 @@ -import { EventHandler, ISignal } from './ISignal'; -import { IEventSubscription } from './ISubscription'; +import { EventHandler, IEventSource, IEventSubscription } from './IEventSource'; -export class Signal implements ISignal { +export class EventSource implements IEventSource { private handlers = new Map>(); public on(handler: EventHandler): IEventSubscription { diff --git a/src/infrastructure/Events/EventSubscriptionCollection.ts b/src/infrastructure/Events/EventSubscriptionCollection.ts new file mode 100644 index 00000000..3bdcd334 --- /dev/null +++ b/src/infrastructure/Events/EventSubscriptionCollection.ts @@ -0,0 +1,12 @@ +import { IEventSubscription } from './IEventSource'; + +export class EventSubscriptionCollection { + private readonly subscriptions = new Array(); + public register(...subscriptions: IEventSubscription[]) { + this.subscriptions.push(...subscriptions); + } + public unsubscribeAll() { + this.subscriptions.forEach((listener) => listener.unsubscribe()); + this.subscriptions.splice(0, this.subscriptions.length); + } +} diff --git a/src/infrastructure/Events/ISignal.ts b/src/infrastructure/Events/IEventSource.ts similarity index 50% rename from src/infrastructure/Events/ISignal.ts rename to src/infrastructure/Events/IEventSource.ts index a25b4685..5a4c6268 100644 --- a/src/infrastructure/Events/ISignal.ts +++ b/src/infrastructure/Events/IEventSource.ts @@ -1,6 +1,11 @@ -import { IEventSubscription } from './ISubscription'; -export interface ISignal { +export interface IEventSource { on(handler: EventHandler): IEventSubscription; } +export interface IEventSubscription { + unsubscribe(): void; +} + export type EventHandler = (data: T) => void; + + diff --git a/src/infrastructure/Events/ISubscription.ts b/src/infrastructure/Events/ISubscription.ts deleted file mode 100644 index ba2d4f46..00000000 --- a/src/infrastructure/Events/ISubscription.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface IEventSubscription { - unsubscribe(): void; -} diff --git a/src/infrastructure/Threading/AsyncLazy.ts b/src/infrastructure/Threading/AsyncLazy.ts index 9761a7a3..7a3291a1 100644 --- a/src/infrastructure/Threading/AsyncLazy.ts +++ b/src/infrastructure/Threading/AsyncLazy.ts @@ -1,7 +1,7 @@ -import { Signal } from '../Events/Signal'; +import { EventSource } from '../Events/EventSource'; export class AsyncLazy { - private valueCreated = new Signal(); + private valueCreated = new EventSource(); private isValueCreated = false; private isCreatingValue = false; private value: T | undefined; @@ -15,7 +15,7 @@ export class AsyncLazy { public async getValueAsync(): Promise { // If value is already created, return the value directly if (this.isValueCreated) { - return Promise.resolve(this.value as T); + return Promise.resolve(this.value); } // If value is being created, wait until the value is created and then return it. if (this.isCreatingValue) { diff --git a/src/presentation/CodeButtons/TheCodeButtons.vue b/src/presentation/CodeButtons/TheCodeButtons.vue index e22ed309..dfce4cfd 100644 --- a/src/presentation/CodeButtons/TheCodeButtons.vue +++ b/src/presentation/CodeButtons/TheCodeButtons.vue @@ -35,9 +35,7 @@ import MacOsInstructions from './MacOsInstructions.vue'; import { Environment } from '@/application/Environment/Environment'; import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState'; import { ScriptingLanguage } from '@/domain/ScriptingLanguage'; -import { IApplication } from '@/domain/IApplication'; import { IApplicationCode } from '@/application/Context/State/Code/IApplicationCode'; -import { IEventSubscription } from '@/infrastructure/Events/ISubscription'; import { IScriptingDefinition } from '@/domain/IScriptingDefinition'; import { OperatingSystem } from '@/domain/OperatingSystem'; @@ -55,8 +53,6 @@ export default class TheCodeButtons extends StatefulVue { public isMacOsCollection = false; public fileName = ''; - private codeListener: IEventSubscription; - public async copyCodeAsync() { const code = await this.getCurrentCodeAsync(); Clipboard.copyText(code.current); @@ -68,13 +64,8 @@ export default class TheCodeButtons extends StatefulVue { this.$modal.show(this.macOsModalName); } } - public destroyed() { - if (this.codeListener) { - this.codeListener.unsubscribe(); - } - } - protected initialize(app: IApplication): void { + protected initialize(): void { return; } protected handleCollectionState(newState: ICategoryCollectionState): void { @@ -90,12 +81,10 @@ export default class TheCodeButtons extends StatefulVue { } private async react(code: IApplicationCode) { this.hasCode = code.current && code.current.length > 0; - if (this.codeListener) { - this.codeListener.unsubscribe(); - } - this.codeListener = code.changed.on((newCode) => { + this.events.unsubscribeAll(); + this.events.register(code.changed.on((newCode) => { this.hasCode = newCode && newCode.code.length > 0; - }); + })); } } diff --git a/src/presentation/Scripts/Cards/CardListItem.vue b/src/presentation/Scripts/Cards/CardListItem.vue index 97931f04..01b2067a 100644 --- a/src/presentation/Scripts/Cards/CardListItem.vue +++ b/src/presentation/Scripts/Cards/CardListItem.vue @@ -35,7 +35,6 @@ import { Component, Prop, Watch, Emit } from 'vue-property-decorator'; import ScriptsTree from '@/presentation/Scripts/ScriptsTree/ScriptsTree.vue'; import { StatefulVue } from '@/presentation/StatefulVue'; -import { IEventSubscription } from '@/infrastructure/Events/ISubscription'; @Component({ components: { @@ -50,17 +49,11 @@ export default class CardListItem extends StatefulVue { public isAnyChildSelected = false; public areAllChildrenSelected = false; - private selectionChangedListener: IEventSubscription; - public async mounted() { - this.updateStateAsync(this.categoryId); const context = await this.getCurrentContextAsync(); - this.selectionChangedListener = context.state.selection.changed.on(() => this.updateStateAsync(this.categoryId)); - } - public destroyed() { - if (this.selectionChangedListener) { - this.selectionChangedListener.unsubscribe(); - } + this.events.register(context.state.selection.changed.on( + () => this.updateSelectionIndicatorsAsync(this.categoryId))); + await this.updateStateAsync(this.categoryId); } @Emit('selected') public onSelected(isExpanded: boolean) { @@ -81,19 +74,24 @@ export default class CardListItem extends StatefulVue { @Watch('categoryId') public async updateStateAsync(value: |number) { const context = await this.getCurrentContextAsync(); - const category = !value ? undefined : context.state.collection.findCategory(this.categoryId); + const category = !value ? undefined : context.state.collection.findCategory(value); this.cardTitle = category ? category.name : undefined; - const currentSelection = context.state.selection; - this.isAnyChildSelected = category ? currentSelection.isAnySelected(category) : false; - this.areAllChildrenSelected = category ? currentSelection.areAllSelected(category) : false; + await this.updateSelectionIndicatorsAsync(value); } protected initialize(): void { return; } protected handleCollectionState(): void { - // No need, as categoryId will be updated instead return; } + + private async updateSelectionIndicatorsAsync(categoryId: number) { + const context = await this.getCurrentContextAsync(); + const selection = context.state.selection; + const category = context.state.collection.findCategory(categoryId); + this.isAnyChildSelected = category ? selection.isAnySelected(category) : false; + this.areAllChildrenSelected = category ? selection.areAllSelected(category) : false; + } } diff --git a/src/presentation/Scripts/ScriptsTree/ScriptsTree.vue b/src/presentation/Scripts/ScriptsTree/ScriptsTree.vue index c1dfb3fc..ebf47963 100644 --- a/src/presentation/Scripts/ScriptsTree/ScriptsTree.vue +++ b/src/presentation/Scripts/ScriptsTree/ScriptsTree.vue @@ -27,8 +27,6 @@ import SelectableTree from './SelectableTree/SelectableTree.vue'; import { INode, NodeType } from './SelectableTree/Node/INode'; import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript'; import { INodeSelectedEvent } from './SelectableTree/INodeSelectedEvent'; -import { IApplication } from '@/domain/IApplication'; -import { IEventSubscription } from '@/infrastructure/Events/ISubscription'; @Component({ components: { @@ -43,7 +41,6 @@ export default class ScriptsTree extends StatefulVue { public filterText?: string = null; private filtered?: IFilterResult; - private listeners = new Array(); public async toggleNodeSelectionAsync(event: INodeSelectedEvent) { const context = await this.getCurrentContextAsync(); @@ -75,11 +72,8 @@ export default class ScriptsTree extends StatefulVue { || this.filtered.categoryMatches.some( (category: ICategory) => node.id === getCategoryNodeId(category)); } - public destroyed() { - this.unsubscribeAll(); - } - protected initialize(app: IApplication): void { + protected initialize(): void { return; } protected async handleCollectionState(newState: ICategoryCollectionState) { @@ -87,19 +81,18 @@ export default class ScriptsTree extends StatefulVue { if (!this.categoryId) { this.nodes = parseAllCategories(newState.collection); } - this.unsubscribeAll(); - this.subscribe(newState); + this.events.unsubscribeAll(); + this.subscribeState(newState); } - private subscribe(state: ICategoryCollectionState) { - this.listeners.push(state.selection.changed.on(this.handleSelectionChanged)); - this.listeners.push(state.filter.filterRemoved.on(this.handleFilterRemoved)); - this.listeners.push(state.filter.filtered.on(this.handleFiltered)); - } - private unsubscribeAll() { - this.listeners.forEach((listener) => listener.unsubscribe()); - this.listeners.splice(0, this.listeners.length); + private subscribeState(state: ICategoryCollectionState) { + this.events.register( + state.selection.changed.on(this.handleSelectionChanged), + state.filter.filterRemoved.on(this.handleFilterRemoved), + state.filter.filtered.on(this.handleFiltered), + ); } + private setCurrentFilter(currentFilter: IFilterResult | undefined) { if (!currentFilter) { this.handleFilterRemoved(); diff --git a/src/presentation/Scripts/ScriptsTree/SelectableTree/Node/RevertToggle.vue b/src/presentation/Scripts/ScriptsTree/SelectableTree/Node/RevertToggle.vue index 048a3f02..b558c338 100644 --- a/src/presentation/Scripts/ScriptsTree/SelectableTree/Node/RevertToggle.vue +++ b/src/presentation/Scripts/ScriptsTree/SelectableTree/Node/RevertToggle.vue @@ -13,49 +13,43 @@ diff --git a/src/presentation/Scripts/TheScripts.vue b/src/presentation/Scripts/TheScripts.vue index 6c84c31b..8d3475f7 100644 --- a/src/presentation/Scripts/TheScripts.vue +++ b/src/presentation/Scripts/TheScripts.vue @@ -50,7 +50,6 @@ import { Grouping } from './Grouping/Grouping'; import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult'; import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState'; import { IApplication } from '@/domain/IApplication'; -import { IEventSubscription } from '@/infrastructure/Events/ISubscription'; /** Shows content of single category or many categories */ @Component({ @@ -79,11 +78,6 @@ export default class TheScripts extends StatefulVue { public isSearching = false; public searchHasMatches = false; - private listeners = new Array(); - - public destroyed() { - this.unsubscribeAll(); - } public async clearSearchQueryAsync() { const context = await this.getCurrentContextAsync(); const filter = context.state.filter; @@ -97,23 +91,21 @@ export default class TheScripts extends StatefulVue { this.repositoryUrl = app.info.repositoryWebUrl; } protected handleCollectionState(newState: ICategoryCollectionState): void { - this.unsubscribeAll(); - this.subscribe(newState); + this.events.unsubscribeAll(); + this.subscribeState(newState); } - private subscribe(state: ICategoryCollectionState) { - this.listeners.push(state.filter.filterRemoved.on(() => { - this.isSearching = false; - })); - state.filter.filtered.on((result: IFilterResult) => { - this.searchQuery = result.query; - this.isSearching = true; - this.searchHasMatches = result.hasAnyMatches(); - }); - } - private unsubscribeAll() { - this.listeners.forEach((listener) => listener.unsubscribe()); - this.listeners.splice(0, this.listeners.length); + private subscribeState(state: ICategoryCollectionState) { + this.events.register( + state.filter.filterRemoved.on(() => { + this.isSearching = false; + }), + state.filter.filtered.on((result: IFilterResult) => { + this.searchQuery = result.query; + this.isSearching = true; + this.searchHasMatches = result.hasAnyMatches(); + }), + ); } } diff --git a/src/presentation/StatefulVue.ts b/src/presentation/StatefulVue.ts index 36061b91..c852881a 100644 --- a/src/presentation/StatefulVue.ts +++ b/src/presentation/StatefulVue.ts @@ -2,10 +2,10 @@ import { Component, Vue } from 'vue-property-decorator'; import { AsyncLazy } from '@/infrastructure/Threading/AsyncLazy'; import { IApplicationContext } from '@/application/Context/IApplicationContext'; import { buildContext } from '@/application/Context/ApplicationContextProvider'; -import { IApplicationContextChangedEvent } from '../application/Context/IApplicationContext'; +import { IApplicationContextChangedEvent } from '@/application/Context/IApplicationContext'; import { IApplication } from '@/domain/IApplication'; -import { ICategoryCollectionState } from '../application/Context/State/ICategoryCollectionState'; -import { IEventSubscription } from '../infrastructure/Events/ISubscription'; +import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState'; +import { EventSubscriptionCollection } from '../infrastructure/Events/EventSubscriptionCollection'; // @ts-ignore because https://github.com/vuejs/vue-class-component/issues/91 @Component @@ -13,18 +13,19 @@ export abstract class StatefulVue extends Vue { public static instance = new AsyncLazy( () => Promise.resolve(buildContext())); - private listener: IEventSubscription; + protected readonly events = new EventSubscriptionCollection(); + + private readonly ownEvents = new EventSubscriptionCollection(); public async mounted() { const context = await this.getCurrentContextAsync(); - this.listener = context.contextChanged.on((event) => this.handleStateChangedEvent(event)); + this.ownEvents.register(context.contextChanged.on((event) => this.handleStateChangedEvent(event))); this.initialize(context.app); this.handleCollectionState(context.state, undefined); } public destroyed() { - if (this.listener) { - this.listener.unsubscribe(); - } + this.ownEvents.unsubscribeAll(); + this.events.unsubscribeAll(); } protected abstract initialize(app: IApplication): void; diff --git a/src/presentation/TheCodeArea.vue b/src/presentation/TheCodeArea.vue index b4c54b35..d1b598fb 100644 --- a/src/presentation/TheCodeArea.vue +++ b/src/presentation/TheCodeArea.vue @@ -11,9 +11,6 @@ import { ICodeChangedEvent } from '@/application/Context/State/Code/Event/ICodeC import { IScript } from '@/domain/IScript'; import { ScriptingLanguage } from '@/domain/ScriptingLanguage'; import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState'; -import { IApplication } from '@/domain/IApplication'; -import { IEventSubscription } from '@/infrastructure/Events/ISubscription'; -import { IApplicationCode } from '@/application/Context/State/Code/IApplicationCode'; import { CodeBuilderFactory } from '@/application/Context/State/Code/Generation/CodeBuilderFactory'; @Component @@ -22,16 +19,14 @@ export default class TheCodeArea extends StatefulVue { private editor!: ace.Ace.Editor; private currentMarkerId?: number; - private codeListener: IEventSubscription; @Prop() private theme!: string; public destroyed() { - this.unsubscribeCodeListening(); this.destroyEditor(); } - protected initialize(app: IApplication): void { + protected initialize(): void { return; } protected handleCollectionState(newState: ICategoryCollectionState): void { @@ -39,18 +34,10 @@ export default class TheCodeArea extends StatefulVue { this.editor = initializeEditor(this.theme, this.editorId, newState.collection.scripting.language); const appCode = newState.code; this.editor.setValue(appCode.current || getDefaultCode(newState.collection.scripting.language), 1); - this.unsubscribeCodeListening(); - this.subscribe(appCode); + this.events.unsubscribeAll(); + this.events.register(appCode.changed.on((code) => this.updateCodeAsync(code))); } - private subscribe(appCode: IApplicationCode) { - this.codeListener = appCode.changed.on((code) => this.updateCodeAsync(code)); - } - private unsubscribeCodeListening() { - if (this.codeListener) { - this.codeListener.unsubscribe(); - } - } private async updateCodeAsync(event: ICodeChangedEvent) { this.removeCurrentHighlighting(); if (event.isEmpty()) { diff --git a/src/presentation/TheSearchBar.vue b/src/presentation/TheSearchBar.vue index e553669f..e6457517 100644 --- a/src/presentation/TheSearchBar.vue +++ b/src/presentation/TheSearchBar.vue @@ -15,9 +15,7 @@ import { StatefulVue } from './StatefulVue'; import { NonCollapsing } from '@/presentation/Scripts/Cards/NonCollapsingDirective'; import { IUserFilter } from '@/application/Context/State/Filter/IUserFilter'; import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult'; -import { IApplication } from '@/domain/IApplication'; import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState'; -import { IEventSubscription } from '@/infrastructure/Events/ISubscription'; @Component( { directives: { NonCollapsing }, @@ -27,8 +25,6 @@ export default class TheSearchBar extends StatefulVue { public searchPlaceHolder = 'Search'; public searchQuery = ''; - private readonly listeners = new Array(); - @Watch('searchQuery') public async updateFilterAsync(newFilter: |string) { const context = await this.getCurrentContextAsync(); @@ -39,28 +35,21 @@ export default class TheSearchBar extends StatefulVue { filter.setFilter(newFilter); } } - public destroyed() { - this.unsubscribeAll(); - } - protected initialize(app: IApplication): void { + protected initialize(): void { return; } protected handleCollectionState(newState: ICategoryCollectionState, oldState: ICategoryCollectionState | undefined) { const totalScripts = newState.collection.totalScripts; this.searchPlaceHolder = `Search in ${totalScripts} scripts`; this.searchQuery = newState.filter.currentFilter ? newState.filter.currentFilter.query : ''; - this.unsubscribeAll(); - this.subscribe(newState.filter); + this.events.unsubscribeAll(); + this.subscribeFilter(newState.filter); } - private subscribe(filter: IUserFilter) { - this.listeners.push(filter.filtered.on((result) => this.handleFiltered(result))); - this.listeners.push(filter.filterRemoved.on(() => this.handleFilterRemoved())); - } - private unsubscribeAll() { - this.listeners.forEach((listener) => listener.unsubscribe()); - this.listeners.splice(0, this.listeners.length); + private subscribeFilter(filter: IUserFilter) { + this.events.register(filter.filtered.on((result) => this.handleFiltered(result))); + this.events.register(filter.filterRemoved.on(() => this.handleFilterRemoved())); } private handleFiltered(result: IFilterResult) { this.searchQuery = result.query; diff --git a/tests/unit/infrastructure/Signal.spec.ts b/tests/unit/infrastructure/Events/EventSource.spec.ts similarity index 75% rename from tests/unit/infrastructure/Signal.spec.ts rename to tests/unit/infrastructure/Events/EventSource.spec.ts index 95e39c2c..d8e05975 100644 --- a/tests/unit/infrastructure/Signal.spec.ts +++ b/tests/unit/infrastructure/Events/EventSource.spec.ts @@ -1,45 +1,43 @@ -import { ISignal } from '@/infrastructure/Events/ISignal'; -import { IEventSubscription } from '@/infrastructure/Events/ISubscription'; -import { Signal } from '@/infrastructure/Events/Signal'; +import { EventHandler, IEventSource, IEventSubscription } from '@/infrastructure/Events/IEventSource'; +import { EventSource } from '@/infrastructure/Events/EventSource'; import { expect } from 'chai'; -import { EventHandler } from '@/infrastructure/Events/ISignal'; +import 'mocha'; - -describe('Signal', () => { +describe('EventSource', () => { class ObserverMock { public readonly onReceiveCalls = new Array(); public readonly callbacks = new Array>(); public readonly subscription: IEventSubscription; - constructor(subject: ISignal) { + constructor(subject: IEventSource) { this.callbacks.push((arg) => this.onReceiveCalls.push(arg)); this.subscription = subject.on((arg) => this.callbacks.forEach((action) => action(arg))); } } - let signal: Signal; - beforeEach(() => signal = new Signal()); + let sut: EventSource; + beforeEach(() => sut = new EventSource()); describe('single observer', () => { // arrange let observer: ObserverMock; beforeEach(() => { - observer = new ObserverMock(signal); + observer = new ObserverMock(sut); }); it('notify() executes the callback', () => { // act - signal.notify(5); + sut.notify(5); // assert expect(observer.onReceiveCalls).to.have.length(1); }); it('notify() executes the callback with the payload', () => { const expected = 5; // act - signal.notify(expected); + sut.notify(expected); // assert expect(observer.onReceiveCalls).to.deep.equal([expected]); }); it('notify() does not call callback when unsubscribed', () => { // act observer.subscription.unsubscribe(); - signal.notify(5); + sut.notify(5); // assert expect(observer.onReceiveCalls).to.have.lengthOf(0); }); @@ -50,13 +48,13 @@ describe('Signal', () => { let observers: ObserverMock[]; beforeEach(() => { observers = [ - new ObserverMock(signal), new ObserverMock(signal), - new ObserverMock(signal), new ObserverMock(signal), + new ObserverMock(sut), new ObserverMock(sut), + new ObserverMock(sut), new ObserverMock(sut), ]; }); it('notify() should execute all callbacks', () => { // act - signal.notify(5); + sut.notify(5); // assert observers.forEach((observer) => { expect(observer.onReceiveCalls).to.have.length(1); @@ -65,7 +63,7 @@ describe('Signal', () => { it('notify() should execute all callbacks with payload', () => { const expected = 5; // act - signal.notify(expected); + sut.notify(expected); // assert observers.forEach((observer) => { expect(observer.onReceiveCalls).to.deep.equal([expected]); @@ -79,7 +77,7 @@ describe('Signal', () => { observers[i].callbacks.push(() => actualSequence.push(i)); } // act - signal.notify(5); + sut.notify(5); // assert expect(actualSequence).to.deep.equal(expectedSequence); }); diff --git a/tests/unit/infrastructure/Events/EventSubscriptionCollection.spec.ts b/tests/unit/infrastructure/Events/EventSubscriptionCollection.spec.ts new file mode 100644 index 00000000..96d4c594 --- /dev/null +++ b/tests/unit/infrastructure/Events/EventSubscriptionCollection.spec.ts @@ -0,0 +1,22 @@ +import { EventSubscriptionCollection } from '@/infrastructure/Events/EventSubscriptionCollection'; +import { IEventSubscription } from '@/infrastructure/Events/IEventSource'; +import { expect } from 'chai'; +import 'mocha'; + +describe('EventSubscriptionCollection', () => { + it('unsubscribeAll unsubscribes from all registered subscriptions', () => { + // arrange + const sut = new EventSubscriptionCollection(); + const expected = [ 'unsubscribed1', 'unsubscribed2']; + const actual = new Array(); + const subscriptions: IEventSubscription[] = [ + { unsubscribe: () => actual.push(expected[0]) }, + { unsubscribe: () => actual.push(expected[1]) }, + ]; + // act + sut.register(...subscriptions); + sut.unsubscribeAll(); + // assert + expect(actual).to.deep.equal(expected); + }); +});