Source: client/http-endpoint.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 {TypedMessage} from './typed-message';
import {Subscriptions} from '../proto/spine/web/keeping_up_pb';
import {HttpResponseHandler} from "./http-response-handler";

/**
 * @typedef {Object} SubscriptionRouting
 *
 * @property {string} create
 *  the name of the subscription creation endpoint; defaults to "/subscription/create"
 * @property {string} keepUp
 *  the name of the subscription keep up endpoint; defaults to "/subscription/keep-up"
 * @property {string} keepUpAll
 *  the name of the subscription bulk keep up endpoint; defaults to "/subscription/keep-up-all"
 * @property {string} cancel
 *  the name of the subscription cancellation endpoint; defaults to "/subscription/cancel"
 * @property {string} cancelAll
 *  the name of the subscription bulk cancellation endpoint; defaults to "/subscription/cancel-all"
 */

/**
 * @typedef {Object} Routing
 *
 * @property {string} query
 *  the name of the query endpoint; defaults to "/query"
 * @property {string} command
 *  the name of the command endpoint; defaults to "/command"
 * @property {!SubscriptionRouting} subscription
 *  the config of the subscription endpoints
 */

class Endpoint {

  /**
   * Sends a command to the endpoint.
   *
   * @param {!TypedMessage<Command>} command a Command  to send to the Spine server
   * @return {Promise<Object>} a promise of a successful server response, rejected if
   *                           an error occurs
   */
  command(command) {
    return this._executeCommand(command);
  }

  /**
   * Sends a query to the endpoint.
   *
   * @param {!spine.client.Query} query a Query to Spine server to retrieve some domain entities
   * @return {Promise<Object>} a promise of a successful server response, rejected if
   *                           an error occurs
   */
  query(query) {
    const typedQuery = TypedMessage.of(query);
    return this._performQuery(typedQuery);
  }

  /**
   * Sends a request to subscribe to a provided topic to an endpoint.
   *
   * @param {!spine.client.Topic} topic a topic for which a subscription is created
   * @return {Promise<Object>} a promise of a successful server response, rejected if
   *                           an error occurs
   */
  subscribeTo(topic) {
    const typedTopic = TypedMessage.of(topic);
    return this._subscribeTo(typedTopic);
  }

  /**
   * Sends a request to keep a subscription, stopping it from being closed by server.
   *
   * @param {!spine.client.Subscription} subscription a subscription that should be kept open
   * @returns {Promise<Object>} a promise of a successful server response, rejected if
   *                            an error occurs
   */
  keepUpSingleSubscription(subscription) {
    const typedSubscription = TypedMessage.of(subscription);
    return this._keepUp(typedSubscription);
  }

  /**
   * Sends a request to keep up several subscriptions, preventing them
   * from being closed by the server.
   *
   * @param {!Array<spine.client.Subscription>} subscriptions subscriptions that should be kept open
   * @return {Promise<Object>} a promise of a successful server response, rejected if
   *                           an error occurs
   */
  keepUpSubscriptions(subscriptions) {
    return this._keepUpAll(subscriptions);
  }

  /**
   * Sends a request to cancel an existing subscription.
   *
   * Cancelling subscription stops the server from updating subscription with new values.
   *
   * @param {!spine.client.Subscription} subscription a subscription that should be kept open
   * @return {Promise<Object>} a promise of a successful server response, rejected if
   *                           an error occurs
   */
  cancelSubscription(subscription) {
    const typedSubscription = TypedMessage.of(subscription);
    return this._cancel(typedSubscription);
  }

  /**
   * Sends a request to cancel all the given subscriptions.
   *
   * Cancelling subscriptions stops the server from updating subscription with new values.
   *
   * @param {!Array<spine.client.Subscription>>} subscriptions subscriptions that should
   *                                                           be cancelled
   * @return {Promise<Object>} a promise of a successful server response, rejected if
   *                           an error occurs
   */
  cancelAll(subscriptions) {
    return this._cancelAll(subscriptions);
  }

  /**
   * @param {!TypedMessage<Command>} command a Command to send to the Spine server
   * @return {Promise<Object>} a promise of a successful server response, rejected if
   *                           an error occurs
   * @protected
   * @abstract
   */
  _executeCommand(command) {
    throw new Error('Not implemented in abstract base.');
  }

  /**
   * @param {!TypedMessage<Query>} query a Query to Spine server to retrieve some domain entities
   * @return {Promise<Object>} a promise of a successful server response, rejected if
   *                           an error occurs
   * @protected
   * @abstract
   */
  _performQuery(query) {
    throw new Error('Not implemented in abstract base.');
  }

  /**
   * @param {!TypedMessage<spine.client.Topic>} topic a topic to create a subscription for
   * @return {Promise<Object>} a promise of a successful server response, rejected if
   *                           an error occurs
   * @protected
   * @abstract
   */
  _subscribeTo(topic) {
    throw new Error('Not implemented in abstract base.');
  }

  /**
   * @param {!TypedMessage<spine.client.Subscription>} subscription a subscription to keep alive
   * @return {Promise<Object>} a promise of a successful server response, rejected if
   *                           an error occurs
   * @protected
   * @abstract
   */
  _keepUp(subscription) {
    throw new Error('Not implemented in abstract base.');
  }

  /**
   * @param {!Array<TypedMessage<spine.client.Subscription>>} subscriptions subscriptions to keep up
   * @return {Promise<Object>} a promise of a successful server response, rejected if
   *                           an error occurs
   * @protected
   * @abstract
   */
  _keepUpAll(subscriptions) {
    throw new Error('Not implemented in abstract base.');
  }

  /**
   * @param {!TypedMessage<spine.client.Subscription>} subscription a subscription to be canceled
   * @return {Promise<Object>} a promise of a successful server response, rejected if
   *                           an error occurs
   * @protected
   * @abstract
   */
  _cancel(subscription) {
    throw new Error('Not implemented in abstract base.');
  }


  /**
   * @param {!Array<spine.client.Subscription>} subscriptions subscriptions to be canceled
   * @return {Promise<Object>} a promise of a successful server response, rejected if
   *                           an error occurs
   * @protected
   * @abstract
   */
  _cancelAll(subscriptions) {
    throw new Error('Not implemented in abstract base.');
  }
}

/**
 * Spine HTTP endpoint which is used to send Commands and Queries using
 * the provided HTTP client.
 */
export class HttpEndpoint extends Endpoint {

  /**
   * @param {!HttpClient} httpClient a client sending requests to server
   * @param {!HttpResponseHandler} responseHandler a handle for the HTTP responses from server
   * @param {Routing} routing endpoint routing parameters
   */
  constructor(httpClient, responseHandler, routing) {
    super();
    this._httpClient = httpClient;
    this._routing = routing;
    this._responseHandler = responseHandler;
  }

  /**
   * Sends a command to the endpoint.
   *
   * @param {!TypedMessage<Command>} command a Command to send to the Spine server
   * @return {Promise<Object|SpineError>} a promise of a successful server response JSON data,
   *                                      rejected if the client response is not `2xx`,
   *                                      or a connection error occurs
   * @protected
   */
  _executeCommand(command) {
    const path = (this._routing && this._routing.command) || '/command';
    return this._sendMessage(path, command);
  }

  /**
   * Sends a query to the endpoint.
   *
   * @param {!TypedMessage<Query>} query a Query to Spine server to retrieve some domain entities
   * @return {Promise<Object|SpineError>} a promise of a successful server response JSON data,
   *                                      rejected if the client response is not `2xx`,
   *                                      or a connection error occurs
   * @protected
   */
  _performQuery(query) {
    const path = (this._routing && this._routing.query) || '/query';
    return this._sendMessage(path, query);
  }

  /**
   * Sends a request to create a subscription for a topic.
   *
   * @param {!TypedMessage<spine.client.Topic>} topic a topic to subscribe to
   * @return {Promise<Object|SpineError>} a promise of a successful server response JSON data,
   *                                      rejected if the client response is not `2xx`,
   *                                      or a connection error occurs
   * @protected
   */
  _subscribeTo(topic) {
    const path = (this._routing && this._routing.subscription && this._routing.subscription.create)
        || '/subscription/create';
    return this._sendMessage(path, topic);
  }

  /**
   * Sends a request to keep alive the given subscription.
   *
   * @param {!TypedMessage<spine.client.Subscription>} subscription a subscription that is prevented
   *                                                                  from being closed by server
   * @return {Promise<Object|SpineError>} a promise of a successful server response JSON data,
   *                                      rejected if the client response is not `2xx`,
   *                                      or a connection error occurs
   * @protected
   */
  _keepUp(subscription) {
    const path = (this._routing && this._routing.subscription && this._routing.subscription.keepUp)
        || '/subscription/keep-up';
    return this._sendMessage(path, subscription);
  }

  /**
   * Sends a request to keep alive the given subscriptions.
   *
   * @param {!Array<spine.client.Subscription>} subscriptions subscriptions that are prevented
   *                                                          from being closed by the server
   * @return {Promise<Object|SpineError>} a promise of a successful server response JSON data,
   *                                      rejected if the client response is not `2xx`,
   *                                      or a connection error occurs
   * @protected
   */
  _keepUpAll(subscriptions) {
    const path = (this._routing && this._routing.subscription && this._routing.subscription.keepUpAll)
        || '/subscription/keep-up-all';
    const request = new Subscriptions()
    request.setSubscriptionList(subscriptions);
    const typed = TypedMessage.of(request);
    return this._sendMessage(path, typed);
  }

  /**
   * Sends a request to cancel the given subscription.
   *
   * @param {!TypedMessage<spine.client.Subscription>} subscription a subscription to be canceled
   * @return {Promise<Object|SpineError>} a promise of a successful server response JSON data,
   *                                      rejected if the client response is not `2xx`,
   *                                      or a connection error occurs
   * @protected
   */
  _cancel(subscription) {
    const path = (this._routing && this._routing.subscription && this._routing.subscription.cancel)
        || '/subscription/cancel';
    return this._sendMessage(path, subscription);
  }

  /**
   * Sends a request to cancel the given subscriptions.
   *
   * @param {!Array<spine.client.Subscription>} subscriptions subscriptions to be canceled
   * @return {Promise<Object|SpineError>} a promise of a successful server response JSON data,
   *                                      rejected if the client response is not `2xx`,
   *                                      or a connection error occurs
   * @protected
   */
  _cancelAll(subscriptions) {
    const path = (this._routing && this._routing.subscription && this._routing.subscription.cancelAll)
        || '/subscription/cancel-all';
    const request = new Subscriptions();
    request.setSubscriptionList(subscriptions);
    const typed = TypedMessage.of(request);
    return this._sendMessage(path, typed);
  }

  /**
   * Sends the given message to the given endpoint.
   *
   * @param {!string} endpoint an endpoint to send the message to
   * @param {!TypedMessage} message a message to send, as a {@link TypedMessage}
   * @return {Promise<Object|SpineError>} a promise of a successful server response JSON data,
   *                                      rejected if the client response is not `2xx`,
   *                                      or a connection error occurs
   * @private
   */
  _sendMessage(endpoint, message) {
    return new Promise((resolve, reject) => {
      this._httpClient
        .postMessage(endpoint, message)
        .then(this._responseHandler.handle
                .bind(this._responseHandler),
            this._responseHandler.onConnectionError
                .bind(this._responseHandler))
        .then(resolve, reject);
    });
  }
}