Source

src/core.ts

/*!
 * Convert JS SDK
 * Version 1.0.0
 * Copyright(c) 2020 Convert Insights, Inc
 * License Apache-2.0
 */
import {ApiManagerInterface} from '@convertcom/js-sdk-api';
import {ContextInterface} from './interfaces/context';
import {CoreInterface} from './interfaces/core';
import {DataManagerInterface} from '@convertcom/js-sdk-data';
import {EventManagerInterface} from '@convertcom/js-sdk-event';
import {ExperienceManagerInterface} from '@convertcom/js-sdk-experience';
import {FeatureManagerInterface} from './interfaces/feature-manager';
import {LogManagerInterface} from '@convertcom/js-sdk-logger';
import {SegmentsManagerInterface} from '@convertcom/js-sdk-segments';

import {Config, ConfigResponseData} from '@convertcom/js-sdk-types';

import {ERROR_MESSAGES, MESSAGES, SystemEvents} from '@convertcom/js-sdk-enums';
import {objectNotEmpty} from '@convertcom/js-sdk-utils';
import {Context} from './context';

const DEFAULT_DATA_REFRESH_INTERVAL = 300000; // in milliseconds (5 minutes)

/**
 * Core
 * @category Main
 * @constructor
 * @implements {CoreInterface}
 */
export class Core implements CoreInterface {
  private _dataManager: DataManagerInterface;
  private _eventManager: EventManagerInterface;
  private _experienceManager: ExperienceManagerInterface;
  private _featureManager: FeatureManagerInterface;
  private _segmentsManager: SegmentsManagerInterface;
  private _loggerManager: LogManagerInterface;
  private _apiManager: ApiManagerInterface;
  private _config: Config;
  private _promise: Promise<ConfigResponseData>;
  private _fetchConfigTimerID: number;
  private _environment: string;
  private _initialized: boolean;

  /**
   * @param {Config} config
   * @param {DataManagerInterface} dependencies.dataManager
   * @param {EventManagerInterface} dependencies.eventManager
   * @param {ExperienceManagerInterface} dependencies.experienceManager
   * @param {FeatureManagerInterface} dependencies.featureManager
   * @param {SegmentsManagerInterface} dependencies.segmentsManager
   * @param {ApiManagerInterface} dependencies.apiManager
   * @param {LogManagerInterface} dependencies.loggerManager
   */
  constructor(
    config: Config,
    {
      dataManager,
      eventManager,
      experienceManager,
      featureManager,
      segmentsManager,
      apiManager,
      loggerManager
    }: {
      dataManager: DataManagerInterface;
      eventManager: EventManagerInterface;
      experienceManager: ExperienceManagerInterface;
      featureManager: FeatureManagerInterface;
      segmentsManager: SegmentsManagerInterface;
      apiManager: ApiManagerInterface;
      loggerManager?: LogManagerInterface;
    }
  ) {
    this._initialized = false;
    this._environment = config?.environment;
    this._dataManager = dataManager;
    this._eventManager = eventManager;
    this._experienceManager = experienceManager;
    this._featureManager = featureManager;
    this._loggerManager = loggerManager;
    this._segmentsManager = segmentsManager;
    this._dataManager = dataManager;
    this._eventManager = eventManager;
    this._apiManager = apiManager;
    this._loggerManager = loggerManager;
    this._loggerManager?.trace?.('Core()', MESSAGES.CORE_CONSTRUCTOR, this);
    this.initialize(config);
  }

  /**
   * Initialize credentials, configData etc..
   * @param config
   */
  private initialize(config: Config): void {
    if (!config) return;
    this._config = config;
    if (
      Object.prototype.hasOwnProperty.call(config, 'sdkKey') &&
      config.sdkKey?.length
    ) {
      // Get data by sdk key
      this.fetchConfig();
    } else if (Object.prototype.hasOwnProperty.call(config, 'data')) {
      this._dataManager.data = config.data;
      if (config.data['error']) {
        this._loggerManager?.error?.('Core.initialize()', {
          error: config.data['error']
        });
      } else {
        this._eventManager.fire(SystemEvents.READY, null, null, true);
        this._loggerManager?.trace?.(
          'Core.initialize()',
          MESSAGES.CORE_INITIALIZED
        );
        this._initialized = true;
      }
    } else {
      this._loggerManager?.error?.(
        'Core.initialize()',
        ERROR_MESSAGES.SDK_OR_DATA_OBJECT_REQUIRED
      );
      this._eventManager.fire(
        SystemEvents.READY,
        {},
        new Error(ERROR_MESSAGES.SDK_OR_DATA_OBJECT_REQUIRED),
        true
      );
    }
  }

  /**
   * Create visitor context
   * @param {string} visitorId A visitor id
   * @param {Record<string, any>=} visitorAttributes An object of key-value pairs that are used for audience and/or segments targeting
   * @return {ContextInterface | null}
   */
  createContext(
    visitorId: string,
    visitorAttributes?: Record<string, any>
  ): ContextInterface | null {
    if (!this._initialized) return null;
    return new Context(
      this._config,
      visitorId,
      {
        eventManager: this._eventManager,
        experienceManager: this._experienceManager,
        featureManager: this._featureManager,
        segmentsManager: this._segmentsManager,
        apiManager: this._apiManager,
        dataManager: this._dataManager,
        loggerManager: this._loggerManager
      },
      visitorAttributes
    );
  }

  /**
   * Add event handler to event
   * @param {SystemEvents} event Event name
   * @param {function(args, err): void} fn A callback function which will be fired
   */
  on(event: SystemEvents, fn: (args?, err?) => void): void {
    this._eventManager.on(event, fn);
  }

  /**
   * Promisified ready event
   * @return {Promise<void>}
   */
  async onReady(): Promise<void> {
    await this._promise;
    return new Promise((resolve, reject) => {
      if (objectNotEmpty(this._dataManager.data)) {
        resolve();
      } else {
        reject(new Error(ERROR_MESSAGES.DATA_OBJECT_MISSING));
      }
    });
  }

  /**
   * Fetch remote config data
   * @return {Promise<void>}
   */
  private async fetchConfig(): Promise<void> {
    this._promise = this._apiManager.getConfig();
    try {
      const data: ConfigResponseData = await this._promise;
      if (data['error']) {
        this._dataManager.data = data;
        this._loggerManager?.error?.('Core.fetchConfig()', {
          error: data['error']
        });
      } else {
        this._loggerManager?.trace?.('Core.fetchConfig()', {
          data
        });
        this._eventManager.fire(
          objectNotEmpty(this._dataManager.data)
            ? SystemEvents.CONFIG_UPDATED
            : SystemEvents.READY,
          null,
          null,
          true
        );
        if (objectNotEmpty(this._dataManager.data)) {
          this._loggerManager?.trace?.(
            'Core.fetchConfig()',
            MESSAGES.CONFIG_DATA_UPDATED
          );
        } else {
          this._loggerManager?.trace?.(
            'Core.fetchConfig()',
            MESSAGES.CORE_INITIALIZED
          );
          this._initialized = true;
        }
        this._dataManager.data = data;
        this._apiManager.setData(data);
      }
      // Update data periodically in background
      if (this._fetchConfigTimerID) {
        clearTimeout(this._fetchConfigTimerID);
      }
      this._fetchConfigTimerID = setTimeout(
        () => this.fetchConfig(),
        this._config?.dataRefreshInterval || DEFAULT_DATA_REFRESH_INTERVAL
      ) as any;
    } catch (error) {
      this._loggerManager?.error?.('Core.fetchConfig()', {
        error: error.message
      });
    }
  }
}