File

projects/core/src/event/event.service.ts

Description

A service to register and observe event sources. Events are driven by event types, which are class signatures for the given event.

It is possible to register multiple sources to a single event, even without knowing as multiple decoupled features can attach sources to the same event type.

Index

Properties
Methods

Methods

Private createEventMeta
createEventMeta(eventType: AbstractType<T>)
Type parameters :
  • T
Parameters :
Name Type Optional
eventType AbstractType<T> No
Returns : void
dispatch
dispatch(event: T, eventType?: Type<T>)
Type parameters :
  • T

Dispatches an instance of an individual event. If the eventType is provided a new event will be created for that type and with the event data.

Parameters :
Name Type Optional Description
event T No

an event

eventType Type<T> Yes

(optional) - type of event

Returns : void
get
get(eventType: AbstractType<T>)
Type parameters :
  • T

Returns a stream of events for the given event type

Parameters :
Name Type Optional
eventType AbstractType<T> No
Returns : Observable<T>
Private getEventMeta
getEventMeta(eventType: AbstractType<T>)
Type parameters :
  • T

Returns the event meta object for the given event type

Parameters :
Name Type Optional
eventType AbstractType<T> No
Returns : EventMeta<T>
Private getInputSubject
getInputSubject(eventType: AbstractType<T>)
Type parameters :
  • T

Returns the input subject used to dispatch a single event. The subject is created on demand, when it's needed for the first time.

Parameters :
Name Type Optional Description
eventType AbstractType<T> No

type of event

Returns : Subject<T>
Private getValidatedEventStream
getValidatedEventStream(source$: Observable, eventType: AbstractType<T>)
Type parameters :
  • T

Returns the given event source with runtime validation whether the emitted values are instances of given event type.

Should be used only in dev mode.

Parameters :
Name Type Optional
source$ Observable<T> No
eventType AbstractType<T> No
Returns : Observable<T>
register
register(eventType: AbstractType<T>, source$: Observable)
Type parameters :
  • T

Register an event source for the given event type.

CAUTION: To avoid memory leaks, the returned teardown function should be called when the event source is no longer maintained by its creator (i.e. in ngOnDestroy if the event source was registered in the component).

Parameters :
Name Type Optional Description
eventType AbstractType<T> No

the event type

source$ Observable<T> No

an observable that represents the source

Returns : void

a teardown function which unregisters the given event source

Private validateCxEvent
validateCxEvent(eventType: AbstractType<T>)
Type parameters :
  • T

Validates if the given type (or its prototype chain) extends from the CxEvent.

Should be used only in the dev mode.

Parameters :
Name Type Optional
eventType AbstractType<T> No
Returns : void
Private validateEventType
validateEventType(eventType: AbstractType<T>)
Type parameters :
  • T

Checks if the event type is a valid type (is a class with constructor).

Should be used only in dev mode.

Parameters :
Name Type Optional
eventType AbstractType<T> No
Returns : void

Properties

Private eventsMeta
Default value : new Map<AbstractType<any> | any, EventMeta<any>>()

The various events meta are collected in a map, stored by the event type class

import { AbstractType, Injectable, isDevMode, Type } from '@angular/core';
import { Observable, Subject } from 'rxjs';
import { tap } from 'rxjs/operators';
import { createFrom } from '../util/create-from';
import { CxEvent } from './cx-event';
import { MergingSubject } from './utils/merging-subject';

/**
 * The object holds registered source observables as well as the merged result observable.
 */
interface EventMeta<T> {
  /**
   * Input subject used for dispatching occasional event (without registering a source)
   */
  inputSubject$: Subject<T> | null;

  /**
   * A custom subject that allows for dynamic adding and removing sources to be merged as an output
   */
  mergingSubject: MergingSubject<T>;
}

/**
 * A service to register and observe event sources. Events are driven by event types, which are class signatures
 * for the given event.
 *
 * It is possible to register multiple sources to a single event, even without
 * knowing as multiple decoupled features can attach sources to the same
 * event type.
 */
@Injectable({
  providedIn: 'root',
})
export class EventService {
  /**
   * The various events meta are collected in a map, stored by the event type class
   */
  private eventsMeta = new Map<AbstractType<any> | any, EventMeta<any>>();

  /**
   * Register an event source for the given event type.
   *
   * CAUTION: To avoid memory leaks, the returned teardown function should be called
   *  when the event source is no longer maintained by its creator
   * (i.e. in `ngOnDestroy` if the event source was registered in the component).
   *
   * @since 3.1 - registers the given `source$` for the parent classes of the given `eventType`.
   *
   * @param eventType the event type
   * @param source$ an observable that represents the source
   *
   * @returns a teardown function which unregisters the given event source
   */
  register<T>(eventType: AbstractType<T>, source$: Observable<T>): () => void {
    const eventMeta = this.getEventMeta(eventType);
    if (eventMeta.mergingSubject.has(source$)) {
      if (isDevMode()) {
        console.warn(
          `EventService: the event source`,
          source$,
          `has been already registered for the type`,
          eventType
        );
      }
    } else {
      eventMeta.mergingSubject.add(source$);
    }

    return () => eventMeta.mergingSubject.remove(source$);
  }

  /**
   * Returns a stream of events for the given event type
   * @param eventTypes event type
   */
  get<T>(eventType: AbstractType<T>): Observable<T> {
    let output$ = this.getEventMeta(eventType).mergingSubject.output$;
    if (isDevMode()) {
      output$ = this.getValidatedEventStream(output$, eventType);
    }
    return output$;
  }

  /**
   * Dispatches an instance of an individual event.
   * If the eventType is provided a new event will be created for that type and with the event data.
   *
   * @param event an event
   * @param eventType (optional) - type of event
   */
  dispatch<T extends object>(event: T, eventType?: Type<T>): void {
    if (!eventType) {
      eventType = event.constructor as Type<T>;
    } else if (!(event instanceof eventType)) {
      event = createFrom(eventType, event);
    }
    const inputSubject$ = this.getInputSubject(eventType);
    inputSubject$.next(event);
  }

  /**
   * Returns the input subject used to dispatch a single event.
   * The subject is created on demand, when it's needed for the first time.
   * @param eventType type of event
   */
  private getInputSubject<T>(eventType: AbstractType<T>): Subject<T> {
    const eventMeta = this.getEventMeta(eventType);

    if (!eventMeta.inputSubject$) {
      eventMeta.inputSubject$ = new Subject<any>();
      this.register(eventType, eventMeta.inputSubject$);
    }
    return eventMeta.inputSubject$;
  }

  /**
   * Returns the event meta object for the given event type
   */
  private getEventMeta<T>(eventType: AbstractType<T>): EventMeta<T> {
    if (!this.eventsMeta.get(eventType)) {
      if (isDevMode()) {
        this.validateEventType(eventType);
      }
      this.createEventMeta(eventType);
    }
    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
    return this.eventsMeta.get(eventType)!;
  }

  private createEventMeta<T>(eventType: AbstractType<T>): void {
    const eventMeta: EventMeta<T> = {
      inputSubject$: null, // will be created lazily by the `dispatch` method
      mergingSubject: new MergingSubject<T>(),
    };
    this.eventsMeta.set(eventType, eventMeta);

    let parentEvent = Object.getPrototypeOf(eventType);
    while (
      parentEvent !== null &&
      Object.getPrototypeOf(parentEvent) !== Object.getPrototypeOf({})
    ) {
      this.register(parentEvent, eventMeta.mergingSubject.output$);
      parentEvent = Object.getPrototypeOf(parentEvent);
    }
  }

  /**
   * Checks if the event type is a valid type (is a class with constructor).
   *
   * Should be used only in dev mode.
   */
  private validateEventType<T>(eventType: AbstractType<T>): void {
    if (!eventType?.constructor) {
      throw new Error(
        `EventService:  ${eventType} is not a valid event type. Please provide a class reference.`
      );
    }

    this.validateCxEvent(eventType);
  }

  /**
   * Validates if the given type (or its prototype chain) extends from the CxEvent.
   *
   * Should be used only in the dev mode.
   */
  private validateCxEvent<T>(eventType: AbstractType<T>): void {
    let parentType = eventType;
    while (
      parentType !== null &&
      Object.getPrototypeOf(parentType) !== Object.getPrototypeOf({})
    ) {
      if ((parentType as any).type === CxEvent.type) {
        return;
      }

      parentType = Object.getPrototypeOf(parentType);
    }

    console.warn(
      `The ${eventType.name} (or one of its parent classes) does not inherit from the ${CxEvent.type}`
    );
  }

  /**
   * Returns the given event source with runtime validation whether the emitted values are instances of given event type.
   *
   * Should be used only in dev mode.
   */
  private getValidatedEventStream<T>(
    source$: Observable<T>,
    eventType: AbstractType<T>
  ): Observable<T> {
    return source$.pipe(
      tap((event) => {
        if (!(event instanceof eventType)) {
          console.warn(
            `EventService: The stream`,
            source$,
            `emitted the event`,
            event,
            `that is not an instance of the declared type`,
            eventType.name
          );
        }
      })
    );
  }
}

result-matching ""

    No results matching ""