Загрузка данных


import {
  BehaviorSubject,
  combineLatest,
  distinctUntilChanged,
  map,
  Observable,
  Subscription,
} from 'rxjs';

import {
  ChartOptionsModel,
  ChartSeriesType,
  Intervals,
  SymbolInfo,
  SymbolInfoInput,
  TimeFormat,
} from '@src/types';

import { Defaults } from '@src/types/defaults';
import { Timeframes } from '@src/types/timeframes';
import {
  areSymbolInfosEqual,
  DateFormat,
  getTimeframeByInterval,
  normalizeSymbolInfo,
  shouldShowTime,
} from '@src/utils';

import { ChartSettings, ChartSettingsSource, parseChartSettings } from './ChartSettings';
import { UndoKey, UndoRedo } from './UndoRedo';

interface EventManagerParams {
  initialTimeframe: Timeframes;
  initialSeries: ChartSeriesType;
  initialSymbolInfo: SymbolInfoInput;
  initialTimeFormat?: TimeFormat;
  initialDateFormat?: DateFormat;
  initialInterval?: Intervals | null;
}

interface SetWithHistoryOptions {
  history?: boolean;
}

/**
 * Менеджер (настроек)*, которые меняются во время использование
 * Отвечает за централизованное управление (настроек)
 * * имеются в виду настройки, которые пользователь применяет к графику
 */
export class EventManager {
  private timeframe$: BehaviorSubject<Timeframes>;
  private seriesSelected$: BehaviorSubject<ChartSeriesType>;
  private symbolInfo$: BehaviorSubject<SymbolInfo>;
  private timeFormat$: BehaviorSubject<TimeFormat>;
  private dateFormat$: BehaviorSubject<DateFormat>;
  private interval$: BehaviorSubject<Intervals | null>;

  private controlBarVisible$ = new BehaviorSubject<boolean>(false); // todo: move to render

  private undoRedo: UndoRedo;

  constructor({
    initialTimeframe,
    initialSeries,
    initialSymbolInfo,
    initialTimeFormat,
    initialDateFormat,
    initialInterval = null,
  }: EventManagerParams) {
    const normalizedSymbolInfo = normalizeSymbolInfo(initialSymbolInfo);

    if (!normalizedSymbolInfo) {
      throw new Error('[EventManager] symbolId is required');
    }

    this.timeframe$ = new BehaviorSubject<Timeframes>(initialTimeframe);
    this.seriesSelected$ = new BehaviorSubject<ChartSeriesType>(initialSeries);
    this.symbolInfo$ = new BehaviorSubject<SymbolInfo>(normalizedSymbolInfo);
    this.timeFormat$ = new BehaviorSubject<TimeFormat>(
      initialTimeFormat ?? Defaults.timeFormat,
    );
    this.dateFormat$ = new BehaviorSubject<DateFormat>(
      initialDateFormat ?? Defaults.dateFormat,
    );
    this.interval$ = new BehaviorSubject<Intervals | null>(initialInterval);

    this.undoRedo = new UndoRedo({
      timeframe: (value) => this.timeframe$.next(value),
      seriesSelected: (value) => this.seriesSelected$.next(value),
      symbolInfo: (value) => this.symbolInfo$.next(value),
      timeFormat: (value) => this.timeFormat$.next(value),
      dateFormat: (value) => this.dateFormat$.next(value),
      interval: (value) => this.interval$.next(value),
    });
  }

  private setWithHistory<K extends UndoKey, V>(
    key: K,
    subject: BehaviorSubject<V>,
    next: V,
    options?: SetWithHistoryOptions,
  ): void {
    const prev = subject.getValue();

    if (Object.is(prev, next)) {
      return;
    }

    subject.next(next);

    const historyEnabled = options?.history ?? true;

    if (historyEnabled) {
      this.undoRedo.push(key, prev, next);
    }
  }

  public getUndoRedo(): UndoRedo {
    return this.undoRedo;
  }

  public getTimeframe(): Timeframes {
    return this.timeframe$.value;
  }

  public setInterval = (
    next: Intervals,
    options?: SetWithHistoryOptions,
  ): void => {
    const timeframe = getTimeframeByInterval(next);

    this.undoRedo.group(() => {
      this.setWithHistory('timeframe', this.timeframe$, timeframe, options);
      this.setWithHistory('interval', this.interval$, next, options);
    });
  };

  public resetInterval = (options?: SetWithHistoryOptions): void =>
    this.setWithHistory('interval', this.interval$, null, options);

  public getInterval(): Observable<Intervals | null> {
    return this.interval$.asObservable();
  }

  public setSymbol = (
    symbolInfoInput: SymbolInfoInput,
    options?: SetWithHistoryOptions,
  ): void => {
    const nextSymbolInfo = normalizeSymbolInfo(symbolInfoInput);

    if (!nextSymbolInfo) {
      return;
    }

    const currentSymbolInfo = this.symbolInfo$.value;

    if (areSymbolInfosEqual(currentSymbolInfo, nextSymbolInfo)) {
      return;
    }

    this.setWithHistory(
      'symbolInfo',
      this.symbolInfo$,
      nextSymbolInfo,
      options,
    );
  };

  /**
   * Существующее имя сохранено.
   * Метод возвращает технический идентификатор для загрузки данных.
   */
  public getSymbol(): Observable<string> {
    return this.symbolId();
  }

  public getSymbolInfo(): SymbolInfo {
    return this.symbolInfo$.value;
  }

  public getSymbolId(): string {
    return this.symbolInfo$.value.symbolId;
  }

  public symbolInfo(): Observable<SymbolInfo> {
    return this.symbolInfo$.pipe(
      distinctUntilChanged(areSymbolInfosEqual),
    );
  }

  public symbolId(): Observable<string> {
    return this.symbolInfo$.pipe(
      map(({ symbolId }) => symbolId),
      distinctUntilChanged(),
    );
  }

  public symbol(): Observable<string> {
    return this.symbolInfo$.pipe(
      map(({ symbol }) => symbol),
      distinctUntilChanged(),
    );
  }

  public symbolName(): Observable<string> {
    return this.symbolInfo$.pipe(
      map(({ symbolName }) => symbolName),
      distinctUntilChanged(),
    );
  }

  public setTimeFormat = (
    next: TimeFormat,
    options?: SetWithHistoryOptions,
  ): void =>
    this.setWithHistory('timeFormat', this.timeFormat$, next, options);

  public setDateFormat = (
    next: DateFormat,
    options?: SetWithHistoryOptions,
  ): void =>
    this.setWithHistory('dateFormat', this.dateFormat$, next, options);

  public getChartOptionsModel(): Observable<ChartOptionsModel> {
    // todo: подумать - стоит ли унести это в чарт
    return combineLatest([
      this.timeFormat$,
      this.dateFormat$,
      this.timeframe$,
    ]).pipe(
      map(([timeFormat, dateFormat, timeframe]) => ({
        timeFormat,
        dateFormat,
        showTime: shouldShowTime(timeframe),
      })),
    );
  }

  public setTimeframe = (
    next: Timeframes,
    options?: SetWithHistoryOptions,
  ): void =>
    this.undoRedo.group(() => {
      this.resetInterval(options);
      this.setWithHistory('timeframe', this.timeframe$, next, options);
    });

  public timeframe(): Observable<Timeframes> {
    return this.timeframe$.asObservable();
  }

  public subscribeTimeframe(
    callback: (format: Timeframes) => void,
  ): Subscription {
    return this.timeframe$.subscribe(callback);
  }

  public getTimeframeObs(): Observable<Timeframes> {
    return this.timeframe$.asObservable();
  }

  public setSeriesSelected = (
    next: ChartSeriesType,
    options?: SetWithHistoryOptions,
  ): void =>
    this.setWithHistory(
      'seriesSelected',
      this.seriesSelected$,
      next,
      options,
    );

  public getSelectedSeries(): Observable<ChartSeriesType> {
    return this.seriesSelected$.asObservable();
  }

  public subscribeSeriesSelected(
    callback: (next: ChartSeriesType) => void,
  ): Subscription {
    return this.seriesSelected$.subscribe(callback);
  }

  public setControlBarVisible(visible: boolean): void {
    this.controlBarVisible$.next(visible);
  }

  public getControlBarVisible(): Observable<boolean> {
    return this.controlBarVisible$.asObservable();
  }

  public exportChartSettings(): ChartSettings {
    return {
      symbolInfo: this.symbolInfo$.value,
      timeframe: this.timeframe$.value,
      seriesSelected: this.seriesSelected$.value,
      timeFormat: this.timeFormat$.value,
      dateFormat: this.dateFormat$.value,
      interval: this.interval$.value,
    };
  }

  public importChartSettings(settings: ChartSettingsSource): void {
    const {
      symbolInfo,
      seriesSelected,
      timeframe,
      timeFormat,
      dateFormat,
      interval,
    } = parseChartSettings(settings);

    const setOptions = {
      history: false,
    };

    if (symbolInfo) {
      this.setSymbol(symbolInfo, setOptions);
    }

    if (seriesSelected) {
      this.setSeriesSelected(seriesSelected, setOptions);
    }

    if (timeFormat) {
      this.setTimeFormat(timeFormat, setOptions);
    }

    if (dateFormat) {
      this.setDateFormat(dateFormat, setOptions);
    }

    if (interval != null) {
      this.setInterval(interval, setOptions);
      return;
    }

    if (timeframe) {
      this.setTimeframe(timeframe, setOptions);
      return;
    }

    if (interval === null) {
      this.resetInterval(setOptions);
    }
  }

  public destroy(): void {
    this.timeFormat$.complete();
    this.dateFormat$.complete();
    this.timeframe$.complete();
    this.controlBarVisible$.complete();
    this.interval$.complete();
    this.symbolInfo$.complete();
    this.seriesSelected$.complete();
  }
}