From ac111f18ca28168ee5ba905e841622ab44b4fe15 Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Mon, 18 Jun 2018 15:50:55 -0400 Subject: [PATCH] prototyping related to https://github.com/openshiftio/openshift.io/issues/2629 related to https://github.com/openshiftio/openshift.io/issues/2529 --- src/app/app.component.spec.ts | 4 ++ src/app/app.component.ts | 4 +- src/app/app.module.ts | 2 + src/app/shared/event-bus-registry.ts | 44 ++++++++++++++++++ src/app/shared/event-bus.module.ts | 45 ++++++++++++++++++ src/app/shared/event-bus.service.ts | 69 ++++++++++++++++++++++++++++ 6 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 src/app/shared/event-bus-registry.ts create mode 100644 src/app/shared/event-bus.module.ts create mode 100644 src/app/shared/event-bus.service.ts diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index 23b274ad6..b35e451ed 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -42,6 +42,7 @@ import { AboutService } from './shared/about.service'; import { ProviderService } from './shared/account/provider.service'; import { AnalyticService } from './shared/analytics.service'; import { BrandingService } from './shared/branding.service'; +import { EventBusRegistry } from './shared/event-bus-registry'; import { LoginService } from './shared/login.service'; import { NotificationsService } from './shared/notifications.service'; @@ -124,6 +125,9 @@ describe('AppComponent', () => { return logger; } }, + { + provide: EventBusRegistry, useValue: null + }, MockContextResolver ], schemas: [ NO_ERRORS_SCHEMA ] diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 55a5fde24..82fa89a35 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -19,6 +19,7 @@ import { AboutService } from './shared/about.service'; import { ProviderService } from './shared/account/provider.service'; import { AnalyticService } from './shared/analytics.service'; import { BrandingService } from './shared/branding.service'; +import { EventBusRegistry } from './shared/event-bus-registry'; import { LoginService } from './shared/login.service'; import { NotificationsService } from './shared/notifications.service'; @@ -61,7 +62,8 @@ export class AppComponent { private providerService: ProviderService, private errorService: ErrorService, private logger: Logger, - private toggleAckService: FeatureAcknowledgementService + private toggleAckService: FeatureAcknowledgementService, + private eventBusRegistry: EventBusRegistry ) { } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 9a00e5b3d..b911225dc 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -114,6 +114,7 @@ import { FeatureAcknowledgementService } from './feature-flag/service/feature-ac import { GettingStartedService } from './getting-started/services/getting-started.service'; import { RavenExceptionHandler } from './shared/exception.handler'; +import { EventBusModule } from './shared/event-bus.module'; // Application wide providers const APP_PROVIDERS = [ @@ -144,6 +145,7 @@ export type StoreType = { BsDropdownModule.forRoot(), EffectsModule.forRoot([]), EmptyStateModule, + EventBusModule, FeatureFooterModule, FormsModule, HttpModule, diff --git a/src/app/shared/event-bus-registry.ts b/src/app/shared/event-bus-registry.ts new file mode 100644 index 000000000..7849ec42f --- /dev/null +++ b/src/app/shared/event-bus-registry.ts @@ -0,0 +1,44 @@ +import { + Inject, + Injectable, + InjectionToken +} from '@angular/core'; + +import { + Observable, + Observer +} from 'rxjs'; + +import { + Event, + EventBus +} from './event-bus.service'; + +// EVENTS_LISTENER *must* also be used with "multi" attribute set to true +export const EVENTS_LISTENER: InjectionToken = new InjectionToken('EventsListener'); + +export abstract class EventsListener implements Observer { + eventTypes: string[]; + abstract next(event: Event); + error(err: any): void { } + complete(): void { } +} + +@Injectable() +export class EventBusRegistry { + constructor( + private readonly eventBus: EventBus, + // Force DI to initialize all listeners in hierarchy + @Inject(EVENTS_LISTENER) private readonly listeners: EventsListener>[] + ) { + listeners.forEach((listener: EventsListener>): void => { + let obs: Observable; + if (listener.eventTypes.length > 0) { + obs = eventBus.for(listener.eventTypes[0], ...listener.eventTypes.slice(1)); + } else { + obs = eventBus.for(listener.eventTypes[0]); + } + obs.subscribe(listener); + }); + } +} diff --git a/src/app/shared/event-bus.module.ts b/src/app/shared/event-bus.module.ts new file mode 100644 index 000000000..3412952f1 --- /dev/null +++ b/src/app/shared/event-bus.module.ts @@ -0,0 +1,45 @@ +import { NgModule } from '@angular/core'; + +import { Observable } from 'rxjs'; + +import { + EventBusRegistry, + EVENTS_LISTENER, + EventsListener +} from './event-bus-registry'; +import { + Event, + EventBus, + EVENTS_PROVIDER, + EventsProvider, + EventType +} from './event-bus.service'; + +@NgModule({ + imports: [], + providers: [ + EventBus, + EventBusRegistry, + // stub default EVENTS_PROVIDER and EVENTS_LISTENER so DI doesn't choke if + // no other modules provide custom providers and listeners + { + provide: EVENTS_PROVIDER, + useValue: ({ + eventType: EventType.GENERIC, + events: Observable.never() + } as EventsProvider), + multi: true + }, + { + provide: EVENTS_LISTENER, + useValue: ({ + eventTypes: [], + next: () => null, + error: () => null, + complete: () => null + } as EventsListener), + multi: true + } + ] +}) +export class EventBusModule { } diff --git a/src/app/shared/event-bus.service.ts b/src/app/shared/event-bus.service.ts new file mode 100644 index 000000000..63db3b25a --- /dev/null +++ b/src/app/shared/event-bus.service.ts @@ -0,0 +1,69 @@ +import { + Inject, + Injectable, + InjectionToken +} from '@angular/core'; + +import { + Observable, + Subject +} from 'rxjs'; + +import { includes } from 'lodash'; + +// Per-module EventsProvider implementations *MUST* set the "multi" attribute to true +export const EVENTS_PROVIDER: InjectionToken = new InjectionToken('EventsProvider'); + +export interface EventsProvider { + eventType: string; + events: Observable; +} + +// Common event types for all publishers and subscribers. +// Publishers and subscribers are also free to use plain strings +// as event types to tag their own events so that this enum does +// not need to be updated to reflect every single use case. +export enum EventType { + GENERIC = 'Generic' +} + +export interface Event { + type: string; + message: T; +} + +@Injectable() +export class EventBus { + + private readonly stream: Subject> = new Subject>(); + + constructor( + @Inject(EVENTS_PROVIDER) private readonly eventsProviders: EventsProvider[] + ) { + eventsProviders.forEach((eventsProvider: EventsProvider): void => this.register(eventsProvider)); + } + + private register(eventsProvider: EventsProvider): void { + if (eventsProvider.eventType == null) { + console.error('Received EventsProvider without EventType!'); + return; + } + if (!eventsProvider.events) { + console.error('Received EventsProvider without events!'); + return; + } + eventsProvider.events + .map((e: any): Event => ({ type: eventsProvider.eventType, message: e })) + .subscribe((event: any): void => this.stream.next(event)); + } + + all(): Observable> { + return this.stream.asObservable(); + } + + for(eventType: string, ...eventTypes: string[]): Observable> { + return this.all() + .filter((event: Event): boolean => includes([eventType, ...eventTypes], event.type)); + } + +}