mirror of https://github.com/grafana/grafana
EventBus: Introduces new event bus with emitter backward compatible interface (#27564)
* updated * Experimenting with event bus with legacy support * Before switch to emitter * EventBus & Emitter unification * Everything using new EventBus * Making progress * Fixing merge issues * Final merge issues * Updated * Updates * Fix * Updated * Update * Update * Rename methods to publish and subscribe * Ts fixes * Updated * updated * fixing doc warnigns * removed unused filepull/28784/head
parent
d84d8a134f
commit
74c65eca26
@ -0,0 +1,173 @@ |
||||
import { EventBusSrv } from './EventBus'; |
||||
import { BusEventWithPayload } from './types'; |
||||
import { eventFactory } from './eventFactory'; |
||||
|
||||
interface LoginEventPayload { |
||||
logins: number; |
||||
} |
||||
|
||||
interface HelloEventPayload { |
||||
hellos: number; |
||||
} |
||||
|
||||
class LoginEvent extends BusEventWithPayload<LoginEventPayload> { |
||||
static type = 'login-event'; |
||||
} |
||||
|
||||
class HelloEvent extends BusEventWithPayload<HelloEventPayload> { |
||||
static type = 'hello-event'; |
||||
} |
||||
|
||||
type LegacyEventPayload = [string, string]; |
||||
|
||||
export const legacyEvent = eventFactory<LegacyEventPayload>('legacy-event'); |
||||
|
||||
class AlertSuccessEvent extends BusEventWithPayload<LegacyEventPayload> { |
||||
static type = 'legacy-event'; |
||||
} |
||||
|
||||
describe('EventBus', () => { |
||||
it('Can create events', () => { |
||||
expect(new LoginEvent({ logins: 1 }).type).toBe('login-event'); |
||||
}); |
||||
|
||||
it('Can subscribe specific event', () => { |
||||
const bus = new EventBusSrv(); |
||||
const events: LoginEvent[] = []; |
||||
|
||||
bus.subscribe(LoginEvent, event => { |
||||
events.push(event); |
||||
}); |
||||
|
||||
bus.publish(new LoginEvent({ logins: 10 })); |
||||
bus.publish(new HelloEvent({ hellos: 10 })); |
||||
|
||||
expect(events[0].payload.logins).toBe(10); |
||||
expect(events.length).toBe(1); |
||||
}); |
||||
|
||||
describe('Legacy emitter behavior', () => { |
||||
it('Supports legacy events', () => { |
||||
const bus = new EventBusSrv(); |
||||
const events: any = []; |
||||
const handler = (event: LegacyEventPayload) => { |
||||
events.push(event); |
||||
}; |
||||
|
||||
bus.on(legacyEvent, handler); |
||||
bus.emit(legacyEvent, ['hello', 'hello2']); |
||||
|
||||
bus.off(legacyEvent, handler); |
||||
bus.emit(legacyEvent, ['hello', 'hello2']); |
||||
|
||||
expect(events.length).toEqual(1); |
||||
expect(events[0]).toEqual(['hello', 'hello2']); |
||||
}); |
||||
|
||||
it('Interoperability with legacy events', () => { |
||||
const bus = new EventBusSrv(); |
||||
const legacyEvents: any = []; |
||||
const newEvents: any = []; |
||||
|
||||
bus.on(legacyEvent, event => { |
||||
legacyEvents.push(event); |
||||
}); |
||||
|
||||
bus.subscribe(AlertSuccessEvent, event => { |
||||
newEvents.push(event); |
||||
}); |
||||
|
||||
bus.emit(legacyEvent, ['legacy', 'params']); |
||||
bus.publish(new AlertSuccessEvent(['new', 'event'])); |
||||
|
||||
expect(legacyEvents).toEqual([ |
||||
['legacy', 'params'], |
||||
['new', 'event'], |
||||
]); |
||||
|
||||
expect(newEvents).toEqual([ |
||||
{ |
||||
type: 'legacy-event', |
||||
payload: ['legacy', 'params'], |
||||
}, |
||||
{ |
||||
type: 'legacy-event', |
||||
payload: ['new', 'event'], |
||||
}, |
||||
]); |
||||
}); |
||||
|
||||
it('should notfiy subscribers', () => { |
||||
const bus = new EventBusSrv(); |
||||
let sub1Called = false; |
||||
let sub2Called = false; |
||||
|
||||
bus.on(legacyEvent, () => { |
||||
sub1Called = true; |
||||
}); |
||||
bus.on(legacyEvent, () => { |
||||
sub2Called = true; |
||||
}); |
||||
|
||||
bus.emit(legacyEvent, null); |
||||
|
||||
expect(sub1Called).toBe(true); |
||||
expect(sub2Called).toBe(true); |
||||
}); |
||||
|
||||
it('when subscribing twice', () => { |
||||
const bus = new EventBusSrv(); |
||||
let sub1Called = 0; |
||||
|
||||
function handler() { |
||||
sub1Called += 1; |
||||
} |
||||
|
||||
bus.on(legacyEvent, handler); |
||||
bus.on(legacyEvent, handler); |
||||
|
||||
bus.emit(legacyEvent, null); |
||||
|
||||
expect(sub1Called).toBe(2); |
||||
}); |
||||
|
||||
it('should handle errors', () => { |
||||
const bus = new EventBusSrv(); |
||||
let sub1Called = 0; |
||||
let sub2Called = 0; |
||||
|
||||
bus.on(legacyEvent, () => { |
||||
sub1Called++; |
||||
throw { message: 'hello' }; |
||||
}); |
||||
|
||||
bus.on(legacyEvent, () => { |
||||
sub2Called++; |
||||
}); |
||||
|
||||
try { |
||||
bus.emit(legacyEvent, null); |
||||
} catch (_) {} |
||||
try { |
||||
bus.emit(legacyEvent, null); |
||||
} catch (_) {} |
||||
|
||||
expect(sub1Called).toBe(2); |
||||
expect(sub2Called).toBe(0); |
||||
}); |
||||
|
||||
it('removeAllListeners should unsubscribe to all', () => { |
||||
const bus = new EventBusSrv(); |
||||
const events: LoginEvent[] = []; |
||||
|
||||
bus.subscribe(LoginEvent, event => { |
||||
events.push(event); |
||||
}); |
||||
|
||||
bus.removeAllListeners(); |
||||
bus.publish(new LoginEvent({ logins: 10 })); |
||||
|
||||
expect(events.length).toBe(0); |
||||
}); |
||||
}); |
||||
}); |
||||
@ -0,0 +1,85 @@ |
||||
import EventEmitter from 'eventemitter3'; |
||||
import { Unsubscribable, Observable } from 'rxjs'; |
||||
import { AppEvent } from './types'; |
||||
import { EventBus, LegacyEmitter, BusEventHandler, BusEventType, LegacyEventHandler, BusEvent } from './types'; |
||||
|
||||
/** |
||||
* @alpha |
||||
*/ |
||||
export class EventBusSrv implements EventBus, LegacyEmitter { |
||||
private emitter: EventEmitter; |
||||
|
||||
constructor() { |
||||
this.emitter = new EventEmitter(); |
||||
} |
||||
|
||||
publish<T extends BusEvent>(event: T): void { |
||||
this.emitter.emit(event.type, event); |
||||
} |
||||
|
||||
subscribe<T extends BusEvent>(typeFilter: BusEventType<T>, handler: BusEventHandler<T>): Unsubscribable { |
||||
return this.getStream(typeFilter).subscribe({ next: handler }); |
||||
} |
||||
|
||||
getStream<T extends BusEvent>(eventType: BusEventType<T>): Observable<T> { |
||||
return new Observable<T>(observer => { |
||||
const handler = (event: T) => { |
||||
observer.next(event); |
||||
}; |
||||
|
||||
this.emitter.on(eventType.type, handler); |
||||
|
||||
return () => { |
||||
this.emitter.off(eventType.type, handler); |
||||
}; |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Legacy functions |
||||
*/ |
||||
emit<T>(event: AppEvent<T> | string, payload?: T | any): void { |
||||
// console.log(`Deprecated emitter function used (emit), use $emit`);
|
||||
|
||||
if (typeof event === 'string') { |
||||
this.emitter.emit(event, { type: event, payload }); |
||||
} else { |
||||
this.emitter.emit(event.name, { type: event.name, payload }); |
||||
} |
||||
} |
||||
|
||||
on<T>(event: AppEvent<T> | string, handler: LegacyEventHandler<T>, scope?: any) { |
||||
// console.log(`Deprecated emitter function used (on), use $on`);
|
||||
|
||||
// need this wrapper to make old events compatible with old handlers
|
||||
handler.wrapper = (emittedEvent: BusEvent) => { |
||||
handler(emittedEvent.payload); |
||||
}; |
||||
|
||||
if (typeof event === 'string') { |
||||
this.emitter.on(event, handler.wrapper); |
||||
} else { |
||||
this.emitter.on(event.name, handler.wrapper); |
||||
} |
||||
|
||||
if (scope) { |
||||
const unbind = scope.$on('$destroy', () => { |
||||
this.off(event, handler); |
||||
unbind(); |
||||
}); |
||||
} |
||||
} |
||||
|
||||
off<T>(event: AppEvent<T> | string, handler: LegacyEventHandler<T>) { |
||||
if (typeof event === 'string') { |
||||
this.emitter.off(event, handler.wrapper); |
||||
return; |
||||
} |
||||
|
||||
this.emitter.off(event.name, handler.wrapper); |
||||
} |
||||
|
||||
removeAllListeners() { |
||||
this.emitter.removeAllListeners(); |
||||
} |
||||
} |
||||
@ -1,9 +1,7 @@ |
||||
import { AppEvent } from './appEvents'; |
||||
|
||||
export type Omit<T, K> = Pick<T, Exclude<keyof T, K>>; |
||||
export type Subtract<T, K> = Omit<T, keyof K>; |
||||
import { AppEvent } from './types'; |
||||
|
||||
const typeList: Set<string> = new Set(); |
||||
|
||||
export function eventFactory<T = undefined>(name: string): AppEvent<T> { |
||||
if (typeList.has(name)) { |
||||
throw new Error(`There is already an event defined with type '${name}'`); |
||||
@ -0,0 +1,3 @@ |
||||
export * from './eventFactory'; |
||||
export * from './types'; |
||||
export * from './EventBus'; |
||||
@ -0,0 +1,115 @@ |
||||
import { Unsubscribable, Observable } from 'rxjs'; |
||||
|
||||
/** |
||||
* @alpha |
||||
* internal interface |
||||
*/ |
||||
export interface BusEvent { |
||||
readonly type: string; |
||||
readonly payload?: any; |
||||
} |
||||
|
||||
/** |
||||
* @alpha |
||||
* Base event type |
||||
*/ |
||||
export abstract class BusEventBase implements BusEvent { |
||||
readonly type: string; |
||||
readonly payload?: any; |
||||
|
||||
constructor() { |
||||
//@ts-ignore
|
||||
this.type = this.__proto__.constructor.type; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* @alpha |
||||
* Base event type with payload |
||||
*/ |
||||
export abstract class BusEventWithPayload<T> extends BusEventBase { |
||||
readonly payload: T; |
||||
|
||||
constructor(payload: T) { |
||||
super(); |
||||
this.payload = payload; |
||||
} |
||||
} |
||||
|
||||
/* |
||||
* Interface for an event type constructor |
||||
*/ |
||||
export interface BusEventType<T extends BusEvent> { |
||||
type: string; |
||||
new (...args: any[]): T; |
||||
} |
||||
|
||||
/** |
||||
* @alpha |
||||
* Event callback/handler type |
||||
*/ |
||||
export interface BusEventHandler<T extends BusEvent> { |
||||
(event: T): void; |
||||
} |
||||
|
||||
/** |
||||
* @alpha |
||||
* Main minimal interface |
||||
*/ |
||||
export interface EventBus { |
||||
/** |
||||
* Publish single vent |
||||
*/ |
||||
publish<T extends BusEvent>(event: T): void; |
||||
|
||||
/** |
||||
* Subscribe to single event |
||||
*/ |
||||
subscribe<T extends BusEvent>(eventType: BusEventType<T>, handler: BusEventHandler<T>): Unsubscribable; |
||||
|
||||
/** |
||||
* Get observable of events |
||||
*/ |
||||
getStream<T extends BusEvent>(eventType: BusEventType<T>): Observable<T>; |
||||
|
||||
/** |
||||
* Remove all event subscriptions |
||||
*/ |
||||
removeAllListeners(): void; |
||||
} |
||||
|
||||
/** |
||||
* @public |
||||
* @deprecated event type |
||||
*/ |
||||
export interface AppEvent<T> { |
||||
readonly name: string; |
||||
payload?: T; |
||||
} |
||||
|
||||
/** @public */ |
||||
export interface LegacyEmitter { |
||||
/** |
||||
* @deprecated use $emit |
||||
*/ |
||||
emit<T>(event: AppEvent<T> | string, payload?: T): void; |
||||
|
||||
/** |
||||
* @deprecated use $on |
||||
*/ |
||||
on<T>(event: AppEvent<T> | string, handler: LegacyEventHandler<T>, scope?: any): void; |
||||
|
||||
/** |
||||
* @deprecated use $on |
||||
*/ |
||||
off<T>(event: AppEvent<T> | string, handler: (payload?: T | any) => void): void; |
||||
} |
||||
|
||||
/** @public */ |
||||
export interface LegacyEventHandler<T> { |
||||
(payload: T): void; |
||||
wrapper?: (event: BusEvent) => void; |
||||
} |
||||
|
||||
/** @alpha */ |
||||
export interface EventBusExtended extends EventBus, LegacyEmitter {} |
||||
@ -1,13 +0,0 @@ |
||||
import { eventFactory } from './utils'; |
||||
|
||||
export interface AppEvent<T> { |
||||
readonly name: string; |
||||
payload?: T; |
||||
} |
||||
|
||||
export type AlertPayload = [string, string?]; |
||||
export type AlertErrorPayload = [string, (string | Error)?]; |
||||
|
||||
export const alertSuccess = eventFactory<AlertPayload>('alert-success'); |
||||
export const alertWarning = eventFactory<AlertPayload>('alert-warning'); |
||||
export const alertError = eventFactory<AlertErrorPayload>('alert-error'); |
||||
@ -0,0 +1,47 @@ |
||||
import { DataQueryError, DataQueryResponseData } from './datasource'; |
||||
import { AngularPanelMenuItem } from './panel'; |
||||
import { DataFrame } from './dataFrame'; |
||||
import { eventFactory } from '../events/eventFactory'; |
||||
import { BusEventBase, BusEventWithPayload } from '../events/types'; |
||||
|
||||
export type AlertPayload = [string, string?]; |
||||
export type AlertErrorPayload = [string, (string | Error)?]; |
||||
|
||||
export const AppEvents = { |
||||
alertSuccess: eventFactory<AlertPayload>('alert-success'), |
||||
alertWarning: eventFactory<AlertPayload>('alert-warning'), |
||||
alertError: eventFactory<AlertErrorPayload>('alert-error'), |
||||
}; |
||||
|
||||
export const PanelEvents = { |
||||
refresh: eventFactory('refresh'), |
||||
componentDidMount: eventFactory('component-did-mount'), |
||||
dataReceived: eventFactory<DataQueryResponseData[]>('data-received'), |
||||
dataError: eventFactory<DataQueryError>('data-error'), |
||||
dataFramesReceived: eventFactory<DataFrame[]>('data-frames-received'), |
||||
dataSnapshotLoad: eventFactory<DataQueryResponseData[]>('data-snapshot-load'), |
||||
editModeInitialized: eventFactory('init-edit-mode'), |
||||
initPanelActions: eventFactory<AngularPanelMenuItem[]>('init-panel-actions'), |
||||
panelInitialized: eventFactory('panel-initialized'), |
||||
panelSizeChanged: eventFactory('panel-size-changed'), |
||||
panelTeardown: eventFactory('panel-teardown'), |
||||
render: eventFactory<any>('render'), |
||||
}; |
||||
|
||||
/** @public */ |
||||
export interface LegacyGraphHoverEventPayload { |
||||
pos: any; |
||||
panel: { |
||||
id: number; |
||||
}; |
||||
} |
||||
|
||||
/** @alpha */ |
||||
export class LegacyGraphHoverEvent extends BusEventWithPayload<LegacyGraphHoverEventPayload> { |
||||
static type = 'graph-hover'; |
||||
} |
||||
|
||||
/** @alpha */ |
||||
export class LegacyGraphHoverClearEvent extends BusEventBase { |
||||
static type = 'graph-hover-clear'; |
||||
} |
||||
@ -1,28 +0,0 @@ |
||||
import { eventFactory } from './utils'; |
||||
import { DataQueryError, DataQueryResponseData } from './datasource'; |
||||
import { AngularPanelMenuItem } from './panel'; |
||||
import { DataFrame } from './dataFrame'; |
||||
|
||||
/** Payloads */ |
||||
export interface PanelChangeViewPayload { |
||||
fullscreen?: boolean; |
||||
edit?: boolean; |
||||
panelId?: number; |
||||
toggle?: boolean; |
||||
} |
||||
|
||||
/** Events */ |
||||
export const refresh = eventFactory('refresh'); |
||||
export const componentDidMount = eventFactory('component-did-mount'); |
||||
export const dataError = eventFactory<DataQueryError>('data-error'); |
||||
export const dataReceived = eventFactory<DataQueryResponseData[]>('data-received'); |
||||
export const dataFramesReceived = eventFactory<DataFrame[]>('data-frames-received'); |
||||
export const dataSnapshotLoad = eventFactory<DataQueryResponseData[]>('data-snapshot-load'); |
||||
export const editModeInitialized = eventFactory('init-edit-mode'); |
||||
export const initPanelActions = eventFactory<AngularPanelMenuItem[]>('init-panel-actions'); |
||||
export const panelChangeView = eventFactory<PanelChangeViewPayload>('panel-change-view'); |
||||
export const panelInitialized = eventFactory('panel-initialized'); |
||||
export const panelSizeChanged = eventFactory('panel-size-changed'); |
||||
export const panelTeardown = eventFactory('panel-teardown'); |
||||
export const render = eventFactory<any>('render'); |
||||
export const viewModeChanged = eventFactory('view-mode-changed'); |
||||
@ -1,5 +1,5 @@ |
||||
import { Emitter } from './utils/emitter'; |
||||
import { EventBusSrv, EventBusExtended } from '@grafana/data'; |
||||
|
||||
export const appEvents = new Emitter(); |
||||
export const appEvents: EventBusExtended = new EventBusSrv(); |
||||
|
||||
export default appEvents; |
||||
|
||||
@ -1,67 +0,0 @@ |
||||
import { Emitter } from '../utils/emitter'; |
||||
import { eventFactory } from '@grafana/data'; |
||||
|
||||
const testEvent = eventFactory('test'); |
||||
|
||||
describe('Emitter', () => { |
||||
describe('given 2 subscribers', () => { |
||||
it('should notfiy subscribers', () => { |
||||
const events = new Emitter(); |
||||
let sub1Called = false; |
||||
let sub2Called = false; |
||||
|
||||
events.on(testEvent, () => { |
||||
sub1Called = true; |
||||
}); |
||||
events.on(testEvent, () => { |
||||
sub2Called = true; |
||||
}); |
||||
|
||||
events.emit(testEvent, null); |
||||
|
||||
expect(sub1Called).toBe(true); |
||||
expect(sub2Called).toBe(true); |
||||
}); |
||||
|
||||
it('when subscribing twice', () => { |
||||
const events = new Emitter(); |
||||
let sub1Called = 0; |
||||
|
||||
function handler() { |
||||
sub1Called += 1; |
||||
} |
||||
|
||||
events.on(testEvent, handler); |
||||
events.on(testEvent, handler); |
||||
|
||||
events.emit(testEvent, null); |
||||
|
||||
expect(sub1Called).toBe(2); |
||||
}); |
||||
|
||||
it('should handle errors', () => { |
||||
const events = new Emitter(); |
||||
let sub1Called = 0; |
||||
let sub2Called = 0; |
||||
|
||||
events.on(testEvent, () => { |
||||
sub1Called++; |
||||
throw { message: 'hello' }; |
||||
}); |
||||
|
||||
events.on(testEvent, () => { |
||||
sub2Called++; |
||||
}); |
||||
|
||||
try { |
||||
events.emit(testEvent, null); |
||||
} catch (_) {} |
||||
try { |
||||
events.emit(testEvent, null); |
||||
} catch (_) {} |
||||
|
||||
expect(sub1Called).toBe(2); |
||||
expect(sub2Called).toBe(0); |
||||
}); |
||||
}); |
||||
}); |
||||
@ -1,101 +0,0 @@ |
||||
import EventEmitter3, { EventEmitter } from 'eventemitter3'; |
||||
import { AppEvent } from '@grafana/data'; |
||||
|
||||
export class Emitter { |
||||
private emitter: EventEmitter3; |
||||
|
||||
constructor() { |
||||
this.emitter = new EventEmitter(); |
||||
} |
||||
|
||||
/** |
||||
* DEPRECATED. |
||||
*/ |
||||
emit(name: string, data?: any): void; |
||||
|
||||
/** |
||||
* Emits an `event` with `payload`. |
||||
*/ |
||||
emit<T extends undefined>(event: AppEvent<T>): void; |
||||
emit<T extends (U extends any ? Partial<T> : unknown) extends T ? Partial<T> : never, U = any>( |
||||
event: AppEvent<T> |
||||
): void; |
||||
emit<T>(event: AppEvent<T>, payload: T): void; |
||||
emit<T>(event: AppEvent<T> | string, payload?: T | any): void { |
||||
if (typeof event === 'string') { |
||||
console.warn(`Using strings as events is deprecated and will be removed in a future version. (${event})`); |
||||
this.emitter.emit(event, payload); |
||||
} else { |
||||
this.emitter.emit(event.name, payload); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* DEPRECATED. |
||||
*/ |
||||
on(name: string, handler: (payload?: any) => void, scope?: any): void; |
||||
|
||||
/** |
||||
* Handles `event` with `handler()` when emitted. |
||||
*/ |
||||
on<T extends undefined>(event: AppEvent<T>, handler: () => void, scope?: any): void; |
||||
on<T extends (U extends any ? Partial<T> : unknown) extends T ? Partial<T> : never, U = any>( |
||||
event: AppEvent<T>, |
||||
handler: () => void, |
||||
scope?: any |
||||
): void; |
||||
on<T>(event: AppEvent<T>, handler: (payload: T) => void, scope?: any): void; |
||||
on<T>(event: AppEvent<T> | string, handler: (payload?: T | any) => void, scope?: any) { |
||||
if (typeof event === 'string') { |
||||
console.warn(`Using strings as events is deprecated and will be removed in a future version. (${event})`); |
||||
this.emitter.on(event, handler); |
||||
|
||||
if (scope) { |
||||
const unbind = scope.$on('$destroy', () => { |
||||
this.emitter.off(event, handler); |
||||
unbind(); |
||||
}); |
||||
} |
||||
return; |
||||
} |
||||
|
||||
this.emitter.on(event.name, handler); |
||||
|
||||
if (scope) { |
||||
const unbind = scope.$on('$destroy', () => { |
||||
this.emitter.off(event.name, handler); |
||||
unbind(); |
||||
}); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* DEPRECATED. |
||||
*/ |
||||
off(name: string, handler: (payload?: any) => void): void; |
||||
|
||||
off<T extends undefined>(event: AppEvent<T>, handler: () => void): void; |
||||
off<T extends (U extends any ? Partial<T> : unknown) extends T ? Partial<T> : never, U = any>( |
||||
event: AppEvent<T>, |
||||
handler: () => void, |
||||
scope?: any |
||||
): void; |
||||
off<T>(event: AppEvent<T>, handler: (payload: T) => void): void; |
||||
off<T>(event: AppEvent<T> | string, handler: (payload?: T | any) => void) { |
||||
if (typeof event === 'string') { |
||||
console.warn(`Using strings as events is deprecated and will be removed in a future version. (${event})`); |
||||
this.emitter.off(event, handler); |
||||
return; |
||||
} |
||||
|
||||
this.emitter.off(event.name, handler); |
||||
} |
||||
|
||||
removeAllListeners(evt?: string) { |
||||
this.emitter.removeAllListeners(evt); |
||||
} |
||||
|
||||
getEventCount(): number { |
||||
return (this.emitter as any)._eventsCount; |
||||
} |
||||
} |
||||
Loading…
Reference in new issue