/*
* 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 {Message} from 'google-protobuf';
import {Observable} from 'rxjs';
/**
* The callback that doesn't accept any parameters.
* @callback parameterlessCallback
*/
/**
* The callback that accepts single parameter.
*
* @callback consumerCallback
* @param {T} the value the callback function accepts
*
* @template <T> the type of value passed to the callback
*/
/**
* The callback that accepts a single `Event` as a parameter.
*
* @callback eventConsumer
*
* @param {spine.core.Event} the event that is accepted by the callback
*/
/**
* @typedef {Object} EntitySubscriptionObject
*
* An object representing the result of a subscription to entity state changes.
*
* @property {!Observable<T>} itemAdded emits new items matching the subscription topic
* @property {!Observable<T>} itemChanged emits updated items matching the subscription topic
* @property {!Observable<T>} itemRemoved emits removed items matching the subscription topic
* @property {!parameterlessCallback} unsubscribe a method to be called to cancel the subscription,
* stopping the subscribers from receiving new
* entities
*
* @template <T> a type of the subscription target entities
*/
/**
* @typedef {Object} EventSubscriptionObject
*
* An object which represents the result of a subscription to events of a certain type.
*
* @property <!Observable<spine.core.Event>> eventEmitted emits new items when the new events
* matching the subscription topic occur in
* the system
* @property {!parameterlessCallback} unsubscribe a method to be called to cancel the subscription,
* stopping the subscribers from receiving new
* entities
*/
/**
* @typedef AckCallback
*
* Represents a command acknowledgement callback.
*
* @property {!parameterlessCallback} onOk
* the callback to run when the command is handled properly
* @property {!consumerCallback<Error>} onError
* the callback to run when the command cannot be handled due to a technical error
* @property {!consumerCallback<Message>} onImmediateRejection
* the callback to run when the command is denied execution due to a business rejection
*/
/**
* An abstract client for Spine application backend. This is a single channel for client-server
* communication in a Spine-based browser application.
*
* @abstract
*/
export class Client {
/**
* Creates a query request that allows to configure and post a new query.
*
* @param {!Class<Message>} entityType a Protobuf type of the query target entities
* @return {QueryRequest} the builder to construct and post a new query
*/
select(entityType) {
throw new Error('Not implemented in abstract base.');
}
/**
* Executes the given `Query` instance specifying the data to be retrieved from
* Spine server fulfilling a returned promise with an array of received objects.
*
* @param {!spine.client.Query} query a query instance to be executed
* @return {Promise<Message[]>} a promise to be fulfilled with a list of Protobuf
* messages of a given type or with an empty list if no entities matching given query
* were found; rejected with a `SpineError` if error occurs
*
* @template <T> a Protobuf type of entities being the target of a query
*/
read(query) {
throw new Error('Not implemented in abstract base.');
}
/**
* Executes the given `Query` instance specifying the data to be retrieved from
* Spine server fulfilling a returned promise with an array of received objects.
*
* @param {!spine.client.Query} query a query instance to be executed
* @return {Promise<Message[]>} a promise to be fulfilled with a list of Protobuf
* messages of a given type or with an empty list if no entities matching given query
* were found; rejected with a `SpineError` if error occurs;
*
* @template <T> a Protobuf type of entities being the target of a query
*
* @deprecated Please use {@link Client#read()} instead
*/
execute(query) {
return this.read(query);
}
/**
* Creates a new {@link QueryFactory} for creating `Query` instances specifying
* the data to be retrieved from Spine server.
*
* @example
* // Build a query for `Task` domain entity, specifying particular IDs.
* newQuery().select(Task)
* .byIds([taskId1, taskId2])
* .build()
*
* @example
* // Build a query for `Task` domain entity, selecting the instances which assigned to the
* // particular user.
* newQuery().select(Task)
* .where([Filters.eq('assignee', userId)])
* .build()
*
* To execute the resulting `Query` instance pass it to the {@link Client#execute()}.
*
* Alternatively, the `QueryRequest` API can be used. See {@link Client#select()}.
*
* @return {QueryFactory} a factory for creating queries to the Spine server
*
* @see QueryFactory
* @see QueryBuilder
* @see AbstractTargetBuilder
*/
newQuery() {
throw new Error('Not implemented in abstract base.');
}
/**
* Creates a subscription request that allows to configure and post a new entity subscription.
*
* @param {!Class<Message>} entityType a Protobuf type of the target entities
* @return {SubscriptionRequest} the builder for the new entity subscription
*/
subscribeTo(entityType) {
throw new Error('Not implemented in abstract base.');
}
/**
* Immediately cancels all active subscriptions.
*
* This endpoint is handy to use when an end-user chooses to end her session
* with the web app. E.g. all subscriptions should be cancelled upon user sign-out.
*/
cancelAllSubscriptions() {
throw new Error('Not implemented in abstract base.');
}
/**
* Subscribes to the given `Topic` instance.
*
* The topic should have an entity type as target. Use {@link #subscribeToEvents} to subscribe to
* the topic that targets events.
*
* @param {!spine.client.Topic} topic a topic to subscribe to
* @return {Promise<EntitySubscriptionObject<Message>>}
* the subscription object which exposes entity changes via its callbacks
*
* @template <T> a Protobuf type of entities being the target of a subscription
*/
subscribe(topic) {
throw new Error('Not implemented in abstract base.');
}
/**
* Creates an event subscription request that allows to configure and post a new event
* subscription.
*
* @param {!Class<Message>} eventType a Protobuf type of the target events
* @return {EventSubscriptionRequest} the builder for the new event subscription
*/
subscribeToEvent(eventType) {
throw new Error('Not implemented in abstract base.');
}
/**
* Subscribes to the given `Topic` instance.
*
* The given topic should target an event type. To perform an entity subscription, use
* {@link #subscribe}.
*
* @param {!spine.client.Topic} topic a topic to subscribe to
*
* @return {Promise<EventSubscriptionObject>}
*/
subscribeToEvents(topic) {
throw new Error('Not implemented in abstract base.');
}
/**
* Creates a new {@link TopicFactory} for building subscription topics specifying
* the state changes to be observed from Spine server.
*
* @example
* // Build a subscription topic for `UserTasks` domain entity.
* newTopic().select(Task)
* .build()
*
* @example
* // Build a subscription topic for `UserTasks` domain entity, selecting the instances
* // with over 3 tasks.
* newTopic().select(UserTasks)
* .where(Filters.gt('tasksCount', 3))
* .build()
*
* To turn the resulting `Topic` instance into a subscription pass it
* to the {@link Client#subscribe()}.
*
* Alternatively, the `SubscriptionRequest` API can be used. See {@link Client#subscribeTo()},
* {@link Client#subscribeToEvents()}.
*
* @return {TopicFactory} a factory for creating subscription topics to the Spine server
*
* @see TopicFactory
* @see TopicBuilder
* @see AbstractTargetBuilder
*/
newTopic() {
throw new Error('Not implemented in abstract base.');
}
/**
* Creates a new command request which allows to post a command to the Spine server and
* configures the command handling callbacks.
*
* @param {!Message} commandMessage a command to post to the server
* @return {CommandRequest} a new command request
*/
command(commandMessage) {
throw new Error('Not implemented in abstract base.');
}
/**
* Posts a given command to the Spine server.
*
* @param {!spine.core.Command} command a Command sent to Spine server
* @param {!AckCallback} onAck a command acknowledgement callback
*/
post(command, onAck) {
throw new Error('Not implemented in abstract base.');
}
/**
* Sends the provided command to the server.
*
* After sending the command to the server the following scenarios are possible:
*
* - the `acknowledgedCallback` is called if the command is acknowledged for further processing
* - the `errorCallback` is called if sending of the command failed
*
* Invocation of the `acknowledgedCallback` and the `errorCallback` are mutually exclusive.
*
* If the command sending fails, the respective error is passed to the `errorCallback`. This error
* is always the type of `CommandHandlingError`. Its cause can be retrieved by `getCause()` method
* and can be represented with the following types of errors:
*
* - `ConnectionError` – if the connection error occurs;
* - `ClientError` – if the server responds with `4xx` HTTP status code;
* - `ServerError` – if the server responds with `5xx` HTTP status code;
* - `spine.base.Error` – if the command message can't be processed by the server;
* - `SpineError` – if parsing of the response fails;
*
* If the command sending fails due to a command validation error, an error passed to the
* `errorCallback` is the type of `CommandValidationError` (inherited from
* `CommandHandlingError`). The validation error can be retrieved by `validationError()` method.
*
* The occurrence of an error does not guarantee that the command is not accepted by the server
* for further processing. To verify this, call the error `assuresCommandNeglected()` method.
*
* @param {!Message} commandMessage a Protobuf message representing the command
* @param {!parameterlessCallback} acknowledgedCallback
* a no-argument callback invoked if the command is acknowledged
* @param {?consumerCallback<CommandHandlingError>} errorCallback
* a callback receiving the errors executed if an error occurred when sending command
* @param {?consumerCallback<Rejection>} rejectionCallback
* a callback executed if the command is denied processing due to a business rejection
* @see CommandHandlingError
* @see CommandValidationError
*
* @deprecated Please use {@link Client#command()}
*/
sendCommand(commandMessage, acknowledgedCallback, errorCallback, rejectionCallback) {
this.command(commandMessage)
.onOk(acknowledgedCallback)
.onError(errorCallback)
.onImmediateRejection(rejectionCallback)
.post();
}
}