Source: client/client-factory.js

/*
 * Copyright 2023, TeamDev. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Redistribution and use in source and/or binary forms, with or without
 * modification, must retain the above copyright notice and the following
 * disclaimer.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

"use strict";

import {ActorRequestFactory} from "./actor-request-factory";
import {Client} from './client';
import {CommandingClient} from "./commanding-client";
import {HttpClient} from "./http-client";
import {HttpEndpoint} from "./http-endpoint";
import {HttpResponseHandler} from "./http-response-handler";

/**
 * @typedef {Object} ClientOptions a type of object for initialization of Spine client
 *
 * @property {!Array<Object>} protoIndexFiles
 *  the list of the `index.js` files generated by {@link https://github.com/SpineEventEngine/base/tree/master/tools/proto-js-plugin the Protobuf plugin for JS}
 * @property {?string} endpointUrl
 *  the URL of the Spine-based backend endpoint
 * @property {?HttpClient} httpClient
 *  custom implementation of HTTP client to use; defaults to {@link HttpClient}.
 * @property {?HttpResponseHandler} httpResponseHandler
 *  custom implementation of HTTP response handler; defaults to {@link HttpResponseHandler}
 * @property {?firebase.database.Database} firebaseDatabase
 *  the Firebase Database that will be used to retrieve data from
 * @property {?ActorProvider} actorProvider
 *  the provider of the user interacting with Spine
 * @property {?Client} implementation
 *  the custom implementation of `Client`
 * @property {?Routing} routing
 *  custom configuration of HTTP endpoints
 * @property {?TenantProvider} tenantProvider
 *  the provider of an active tenant ID, if not specified, the application is considered
 *  single-tenant
 * @property {?Duration} subscriptionKeepUpInterval
 *  the custom interval for sending requests to keep up subscriptions
 */

/**
 * An abstract factory for creation of `Client` instances.
 *
 * Ensures that the `ClientOptions` contain list of the `index.js` files generated by
 * {@link https://github.com/SpineEventEngine/base/tree/master/tools/proto-js-plugin the Protobuf plugin for JS}
 * and performs registration of types and parsers containing in these files.
 *
 * Creation of the concrete implementation of `Client` instances is delegated to inheritors.
 */
export class AbstractClientFactory {

  /**
   * Creates a new instance of `Client` implementation in accordance with given options.
   *
   * @param {!ClientOptions} options client initialization options
   * @return {Client} a `Client` instance
   */
  static createClient(options) {
    this._ensureOptionsSufficient(options);
    return this._clientFor(options);
  }

  /**
   * Creates a new instance of `Client` implementation in accordance with given options.
   *
   * @param {!ClientOptions} options
   * @return {Client}
   * @protected
   */
  static _clientFor(options) {
    throw new Error('Not implemented in abstract base')
  }

  /**
   * Creates a new instance of `QueryingClient` implementation in accordance with given options.
   *
   * @param {!ClientOptions} options client initialization options
   * @return {QueryingClient} a `QueryingClient` instance
   */
  static createQuerying(options) {
    throw new Error('Not implemented in abstract base')
  }

  /**
   * Creates a new instance of `SubscribingClient` implementation in accordance with given options.
   *
   * @param {!ClientOptions} options client initialization options
   * @return {SubscribingClient} a `SubscribingClient` instance
   */
  static createSubscribing(options) {
    throw new Error('Not implemented in abstract base')
  }

  /**
   * Creates a new instance of `CommandingClient` implementation in accordance with given options.
   *
   * @param {!ClientOptions} options client initialization options
   * @return {CommandingClient} a `CommandingClient` instance
   */
  static createCommanding(options) {
    const httpClient = this._createHttpClient(options);
    const httpResponseHandler = this._createHttpResponseHandler(options)
    const endpoint = new HttpEndpoint(httpClient, httpResponseHandler, options.routing);
    const requestFactory = ActorRequestFactory.create(options);

    return new CommandingClient(endpoint, requestFactory);
  }

  /**
   * Creates an HTTP client basing on the passed {@link ClientOptions}.
   *
   * In case a custom HTTP client is specified via the `options`, this instance is returned.
   * Otherwise, a new instance of `HttpClient` is returned.
   *
   * @param {!ClientOptions} options client initialization options
   * @return {HttpClient} an instance of HTTP client
   * @protected
   */
  static _createHttpClient(options) {
    const customClient = options.httpClient;
    if (!!customClient) {
      if (!customClient instanceof HttpClient) {
        throw new Error('The custom HTTP client implementation passed via `options.httpClient` ' +
            'must extend `HttpClient`.');
      }
      return customClient;
    } else {
      return new HttpClient(options.endpointUrl);
    }
  }

  /**
   * Creates an HTTP response handler judging on the passed {@link ClientOptions}.
   *
   * In case a custom HTTP response handler is specified via the `options`,
   * this instance is returned. Otherwise, a new instance of `HttpResponseHandler` is returned.
   *
   * @param {!ClientOptions} options client initialization options
   * @return {HttpResponseHandler} an instance of HTTP response handler
   * @protected
   */
  static _createHttpResponseHandler(options) {
    const customHandler = options.httpResponseHandler;
    if (!!customHandler) {
      if (!customHandler instanceof HttpResponseHandler) {
        throw new Error('The custom HTTP response handler implementation' +
            ' passed via `options.httpResponseHandler` must extend `HttpResponseHandler`.');
      }
      return customHandler;
    } else {
      return new HttpResponseHandler();
    }
  }

  /**
   * Ensures whether options object is sufficient for client initialization.
   *
   * @param {!ClientOptions} options
   * @protected
   */
  static _ensureOptionsSufficient(options) {
    if (!options) {
      throw new Error('Unable to initialize client. The `ClientOptions` is undefined.');
    }

    const indexFiles = options.protoIndexFiles;
    if (!Array.isArray(indexFiles) || indexFiles.length === 0) {
      throw new Error('Only a non-empty array is allowed as ClientOptions.protoIndexFiles parameter.');
    }

    indexFiles.forEach(indexFile => {
      if (typeof indexFile !== 'object'
          || !(indexFile.types instanceof Map)
          || !(indexFile.parsers instanceof Map) ) {
        throw new Error('Unable to register Protobuf index files.' +
          ' Check the `ClientOptions.protoIndexFiles` contains files' +
          ' generated with "io.spine.tools:spine-proto-js-plugin".');
      }
    });
  }
}

/**
 * An implementation of the `AbstractClientFactory` that returns a client instance
 * provided in `ClientOptions` parameter.
 */
export class CustomClientFactory extends AbstractClientFactory {

  /**
   * Returns a custom `Client` implementation provided in options. Expects that the given options
   * contain an implementation which extends `Client`.
   *
   * Can be used to provide mock implementations of `Client`.
   *
   * @param {ClientOptions} options
   * @return {Client} a custom `Client` implementation provided in options
   * @override
   */
  static _clientFor(options) {
    return options.implementation;
  }

  /**
   * Returns a custom `QueryingClient` implementation provided in options. Expects that the given
   * options contain an implementation which extends `QueryingClient`.
   *
   * Can be used to provide mock implementations of `QueryingClient`.
   *
   * @param {ClientOptions} options
   * @return {QueryingClient} a custom `QueryingClient` implementation provided in options
   * @override
   */
  static createQuerying(options) {
    return options.implementation;
  }

  /**
   * Returns a custom `SubscribingClient` implementation provided in options. Expects that the given
   * options contain an implementation which extends `SubscribingClient`.
   *
   * Can be used to provide mock implementations of `SubscribingClient`.
   *
   * @param {ClientOptions} options
   * @return {SubscribingClient} a custom `SubscribingClient` implementation provided in options
   * @override
   */
  static createSubscribing(options) {
    return options.implementation;
  }

  /**
   * Returns a custom `CommandingClient` implementation provided in options. Expects that the given
   * options contain an implementation which extends `CommandingClient`.
   *
   * Can be used to provide mock implementations of `CommandingClient`.
   *
   * @param {ClientOptions} options
   * @return {CommandingClient} a custom `CommandingClient` implementation provided in options
   * @override
   */
  static createCommanding(options) {
    return options.implementation;
  }

  /**
   * @override
   */
  static _ensureOptionsSufficient(options) {
    super._ensureOptionsSufficient(options);
    const customClient = options.implementation;
    if (!customClient || !(customClient instanceof Client)) {
      throw new Error('Unable to initialize custom client implementation.' +
        ' The `ClientOptions.implementation` must extend `Client`.');
    }
  }
}