ubiiClientService.js

/* eslint-disable no-console */

import EventEmitter from 'events';

import ClientNodeWeb from './clientNodeWeb';

const uuidv4Regex = '[0-9a-z]{8}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{12}';
const MAX_RETRIES_WAIT_FOR_CONNECTION = 50;

let _instance = null;
const SINGLETON_ENFORCER = Symbol();

/**
 * Service to access a singleton instance of a Ubi-Interact client node and expose its API.
 */
class UbiiClientService extends EventEmitter {
  constructor(enforcer) {
    super();

    if (enforcer !== SINGLETON_ENFORCER) {
      throw new Error('Use ' + this.constructor.name + '.instance');
    }

    this.name = 'ubii-node-webbrowser';

    this.client = undefined;
    this.connecting = false;

    this.onDisconnectCallbacks = [];
    this.useHTTPS = true;
  }

  /** 
   * Return the singleton instance of the client node.
   */
  static get instance() {
    if (_instance == null) {
      _instance = new UbiiClientService(SINGLETON_ENFORCER);
    }

    return _instance;
  }

  /** 
   * Set a name to use for the client node.
   * @param {string} name The name.
   */
  setName(name) {
    this.name = name;
  }

  /**
   * Set whether to use HTTPS or not when trying to connect to master node.
   * @param {boolean} bool 
   */
  setHTTPS(bool) {
    this.useHTTPS = bool;
  }

  /**
   * Connect to master node.
   * @param {string} urlServices URL for service calls.
   * @param {string} urlTopicData URL for websocket connection transferring TopicData.
   * @returns A promise that resolves when connected or rejects on failure to connect.
   */
  async connect(urlServices, urlTopicData) {
    let changedAddress = urlServices !== this.urlServices || urlTopicData !== this.urlTopicData;
    if (!changedAddress && (this.isConnected() || this.connecting)) {
      return this.waitForConnection();
    }

    this.urlServices = urlServices;
    this.urlTopicData = urlTopicData;
    this.connecting = true;

    console.info(
      'UbiiClientService - connecting to services=' +
        this.urlServices +
        ' and topicdata=' +
        this.urlTopicData +
        ' ...'
    );

    if (!this.client) {
      this.client = new ClientNodeWeb(this.name, this.urlServices, this.urlTopicData);
    }

    return this.client.initialize().then(
      () => {
        if (this.client.isInitialized()) {
          console.info(
            'UbiiClientService - client connected with ID:\n' + this.client.clientSpecification.id
          );
          this.connecting = false;

          this.client.topicDataClient.websocket.onclose = () => {
            console.warn('Ubi-Interact topic data websocket connection has closed!');
            this.emit(UbiiClientService.EVENTS.DISCONNECT);
          };

          this.emit(UbiiClientService.EVENTS.CONNECT);
        }
      },
      (error) => {
        console.info('UbiiClientService.client.initialize() failed:\n' + error.toString());
      }
    );
  }

  /**
   * Disconnect from master node.
   * @returns A promise indicating conclusion.
   */
  async disconnect() {
    console.info('UbiiClientService - disconnecting ...');
    if (!this.isConnected()) {
      console.warn('Client tried to disconnect without being connected.');
      return Promise.resolve();
    }

    let id = this.client.clientSpecification.id;

    this.emit(UbiiClientService.EVENTS.DISCONNECT);
    this.onDisconnectCallbacks.forEach((callback) => {
      callback();
    });

    return this.client.deinitialize().then(() => {
      this.connecting = false;
      this.client = undefined;
      console.info('client disconnected with ID: ' + id);
    });
  }

  /**
   * Reconnect to master node.
   */
  async reconnect() {
    console.info('UbiiClientService - reconnecting ...');
    await this.client.reinitialize();
  }


  /**
   * Use to receive a Promise that resolves as soon as connection is established.
   * @returns The Promise.
   */
  waitForConnection() {
    return new Promise((resolve, reject) => {
      let retry = 0;

      let checkConnection = () => {
        retry += 1;

        if (retry > MAX_RETRIES_WAIT_FOR_CONNECTION) {
          reject('UbiiClientService.waitForConnection() - maximum retries exceeded.');
          return;
        }

        if (this.client && this.client.isConnected()) {
          resolve('UbiiClientService is connected.');
          return;
        } else {
          setTimeout(() => {
            checkConnection();
          }, 100);
        }
      };
      checkConnection();
    });
  }

  /**
   * Whether the client node is connected to the master node or not.
   * @returns A boolean.
   */
  isConnected() {
    return this.client && this.client.isConnected();
  }

  /**
   * Add a callback function to be executed after disconnecting.
   * @param {Function} callback A callback function.
   */
  onDisconnect(callback) {
    this.onDisconnectCallbacks.push(callback);
  }

  /**
   * Get the client node ID if connected, or undefined if not connected.
   * @returns A string, or undefined.
   */
  getClientID() {
    if (this.client && this.client.isInitialized()) {
      return this.client.clientSpecification.id;
    } else {
      return undefined;
    }
  }

  /**
   * Make a service call.
   * @param {ubii.services.ServiceRequest} serviceRequest Protobuf of a service request. {@link https://github.com/SandroWeber/ubii-msg-formats/blob/develop/src/proto/services/serviceRequest.proto}
   * @returns A Ubi-Interact ServiceReply. {@link https://github.com/SandroWeber/ubii-msg-formats/blob/develop/src/proto/services/serviceReply.proto}
   */
  async callService(serviceRequest) {
    return this.client.callService(serviceRequest);
  }

  /**
   * Register the specified device.
   * @param {ubii.devices.Device} deviceSpecs Protobuf object specifying device. {@link https://github.com/SandroWeber/ubii-msg-formats/blob/develop/src/proto/devices/device.proto}
   * @returns The master node reply for the requested registration.
   */
  async registerDevice(deviceSpecs) {
    deviceSpecs.clientId = this.client.clientSpecification.id;
    return this.client.registerDevice(deviceSpecs);
  }

  /**
   * Deregister the specified device. Usually enough to fill in the device's ID.
   * @param {ubii.devices.Device} deviceSpecs Protobuf object specifying device. {@link https://github.com/SandroWeber/ubii-msg-formats/blob/develop/src/proto/devices/device.proto}
   * @returns The master node reply for the requested deregistration.
   */
  async deregisterDevice(deviceSpecs) {
    return this.client.deregisterDevice(deviceSpecs);
  }

  /**
   * Register the specified session.
   * @param {ubii.sessions.Session} sessionSpecs Protobuf object specifying session. {@link https://github.com/SandroWeber/ubii-msg-formats/blob/develop/src/proto/sessions/session.proto}
   * @returns The master node reply for the requested registration.
   */
  async registerSession(sessionSpecs) {
    if (this.client && this.client.isInitialized()) {
      return this.client.registerSession(sessionSpecs);
    }
  }

  /**
   * Get the interval used to regularly publish TopicDataRecords.
   * @returns Interval in milliseconds.
   */
  getPublishIntervalMs() {
    return this.client && this.client.publishDelayMs;
  }

  /**
   * Set the interval used to regularly publish TopicDataRecords.
   * @param {Number} intervalMs Interval in milliseconds.
   */
  setPublishIntervalMs(intervalMs) {
    this.client && this.client.setPublishIntervalMs(intervalMs);
  }

  /**
   * Add a TopicDataRecord to the publishing queue.
   * @param {ubii.topicData.TopicDataRecord} topicDataRecord TopicDataRecord to publish. {@link https://github.com/SandroWeber/ubii-msg-formats/blob/develop/src/proto/topicData/topicDataRecord.proto}
   */
  publishRecord(topicDataRecord) {
    this.client && this.client.publishRecord(topicDataRecord);
  }

  /**
   * Add a list of TopicDataRecords to the publishing queue.
   * @param {ubii.topicData.TopicDataRecordList} topicDataRecordList TopicDataRecordList to publish. {@link https://github.com/SandroWeber/ubii-msg-formats/blob/develop/src/proto/topicData/topicDataRecord.proto}
   */
  publishRecordList(topicDataRecordList) {
    this.client && this.client.publishRecordList(topicDataRecordList);
  }

  /**
   * Publish a TopicDataRecord without delay using an individual TopicData message instead of queueing it. Might lead to messaging overhead.
   * @param {ubii.topicData.TopicDataRecord} topicDataRecord TopicDataRecord to publish. {@link https://github.com/SandroWeber/ubii-msg-formats/blob/develop/src/proto/topicData/topicDataRecord.proto}
   */
  publishRecordImmediately(topicDataRecord) {
    this.client && this.client.publishRecordImmediately(topicDataRecord);
  }

  /**
   * Subscribe to a topic, providing a callback function to be called upon receiving notifications.
   * @param {String} topic The topic to subscribe to.
   * @param {Function} callback The function to be called upon receiving messages for the specified topic.
   * @returns A subscription token that should be used to unsubscribe.
   */
  async subscribeTopic(topic, callback) {
    return this.client && this.client.subscribeTopic(topic, callback);
  }

  /**
   * Subscribe to a regular expression, providing a callback function to be called upon receiving notifications.
   * @param {String} regex The regular expression to subscribe to. Used to match against existing and future topics.
   * @param {Function} callback The function to be called upon receiving messages for the specified regular expression.
   * @returns A subscription token that should be used to unsubscribe.
   */
  subscribeRegex(regex, callback) {
    return this.client && this.client.subscribeRegex(regex, callback);
  }

  /**
   * Subscribe to a component profile, providing a callback function to be called upon receiving notifications.
   * @param {ubii.devices.Component} componentProfile The Component(s) to subscribe to. Used to match against existing and future Components. {@link https://github.com/SandroWeber/ubii-msg-formats/blob/develop/src/proto/devices/component.proto}
   * @param {Function} callback The function to be called upon receiving messages for the specified Component profile.
   * @returns A subscription token that should be used to unsubscribe.
   */
  async subscribeComponents(componentProfile, callback) {
    return (await this.client) && this.client.subscribeComponents(componentProfile, callback);
  }

  /**
   * Unsubscribe using a subscription token.
   * @param {Object} token The token returned by one of the subscribe... methods.
   * @returns The master node reply.
   */
  async unsubscribe(token) {
    return await this.client.unsubscribe(token);
  }

  /**
   * Get a regular expression matching UUIDv4 strings.
   * @returns {String} The regular expression.
   */
  getUUIDv4Regex() {
    return uuidv4Regex;
  }

  /**
   * Create a new timestamp to be included in a TopicDataRecord using the current system time in milliseconds.
   * @returns {ubii.topicData.Timestamp} The timestamp. {@link https://github.com/SandroWeber/ubii-msg-formats/blob/develop/src/proto/topicData/timestamp.proto}
   */
  generateTimestamp() {
    return {
      millis: performance.now()
    };
  }
}

UbiiClientService.EVENTS = Object.freeze({
  CONNECT: 'CONNECT',
  DISCONNECT: 'DISCONNECT',
  RECONNECT: 'RECONNECT'
});

export default UbiiClientService;