Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 56 additions & 4 deletions client-js/main/client/client-factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ 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
Expand All @@ -39,6 +40,10 @@ import {HttpEndpoint} from "./http-endpoint";
* 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
Expand Down Expand Up @@ -114,13 +119,60 @@ export class AbstractClientFactory {
* @return {CommandingClient} a `CommandingClient` instance
*/
static createCommanding(options) {
const httpClient = new HttpClient(options.endpointUrl);
const endpoint = new HttpEndpoint(httpClient, options.routing);
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.
*
Expand Down Expand Up @@ -218,8 +270,8 @@ export class CustomClientFactory extends AbstractClientFactory {
super._ensureOptionsSufficient(options);
const customClient = options.implementation;
if (!customClient || !(customClient instanceof Client)) {
throw new Error('Unable to initialize custom implementation.' +
' The `ClientOptions.implementation` should extend Client.');
throw new Error('Unable to initialize custom client implementation.' +
' The `ClientOptions.implementation` must extend `Client`.');
}
}
}
11 changes: 6 additions & 5 deletions client-js/main/client/direct-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,9 @@ import TypeParsers from "./parser/type-parsers";
export class DirectClientFactory extends AbstractClientFactory {

static _clientFor(options) {
const httpClient = new HttpClient(options.endpointUrl);
const endpoint = new HttpEndpoint(httpClient, options.routing);
const httpClient = this._createHttpClient(options);
const httpResponseHandler = this._createHttpResponseHandler(options);
const endpoint = new HttpEndpoint(httpClient, httpResponseHandler, options.routing);
const requestFactory = ActorRequestFactory.create(options);

const querying = new DirectQueryingClient(endpoint, requestFactory);
Expand All @@ -62,10 +63,10 @@ export class DirectClientFactory extends AbstractClientFactory {
}

static createQuerying(options) {
const httpClient = new HttpClient(options.endpointUrl);
const endpoint = new HttpEndpoint(httpClient, options.routing);
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 DirectQueryingClient(endpoint, requestFactory);
}

Expand Down
27 changes: 11 additions & 16 deletions client-js/main/client/firebase-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,15 +138,11 @@ class EventSubscription extends SpineSubscription {
class FirebaseQueryingClient extends QueryingClient {

/**
* A protected constructor for customization.
*
* Use `FirebaseClient#usingFirebase()` for instantiation
* Creates an instance of the client.
*
* @param {!HttpEndpoint} endpoint the server endpoint to execute queries and commands
* @param {!FirebaseDatabaseClient} firebaseDatabase the client to read the query results from
* @param {!ActorRequestFactory} actorRequestFactory a factory to instantiate the actor requests with
*
* @protected
*/
constructor(endpoint, firebaseDatabase, actorRequestFactory) {
super(actorRequestFactory);
Expand Down Expand Up @@ -176,9 +172,7 @@ const EVENT_TYPE_URL = 'type.spine.io/spine.core.Event';
class FirebaseSubscribingClient extends SubscribingClient {

/**
* A protected constructor for customization.
*
* Use `FirebaseClient#usingFirebase()` for instantiation.
* Creates an instance of the client.
*
* @param {!HttpEndpoint} endpoint
* the server endpoint to execute queries and commands
Expand All @@ -188,8 +182,6 @@ class FirebaseSubscribingClient extends SubscribingClient {
* a factory to instantiate the actor requests with
* @param {!FirebaseSubscriptionService} subscriptionService
* a service handling the subscriptions
*
* @protected
*/
constructor(endpoint, firebaseDatabase, actorRequestFactory, subscriptionService) {
super(actorRequestFactory);
Expand Down Expand Up @@ -328,8 +320,9 @@ export class FirebaseClientFactory extends AbstractClientFactory {
* @override
*/
static _clientFor(options) {
const httpClient = new HttpClient(options.endpointUrl);
const endpoint = new HttpEndpoint(httpClient, options.routing);
const httpClient = this._createHttpClient(options);
const httpResponseHandler = this._createHttpResponseHandler(options);
const endpoint = new HttpEndpoint(httpClient, httpResponseHandler, options.routing);
const firebaseDatabaseClient = new FirebaseDatabaseClient(options.firebaseDatabase);
const requestFactory = ActorRequestFactory.create(options);
const subscriptionService =
Expand All @@ -345,17 +338,19 @@ export class FirebaseClientFactory extends AbstractClientFactory {
}

static createQuerying(options) {
const httpClient = new HttpClient(options.endpointUrl);
const endpoint = new HttpEndpoint(httpClient, options.routing);
const httpClient = this._createHttpClient(options);
const httpResponseHandler = this._createHttpResponseHandler(options);
const endpoint = new HttpEndpoint(httpClient, httpResponseHandler, options.routing);
const firebaseDatabaseClient = new FirebaseDatabaseClient(options.firebaseDatabase);
const requestFactory = ActorRequestFactory.create(options);

return new FirebaseQueryingClient(endpoint, firebaseDatabaseClient, requestFactory);
}

static createSubscribing(options) {
const httpClient = new HttpClient(options.endpointUrl);
const endpoint = new HttpEndpoint(httpClient, options.routing);
const httpClient = this._createHttpClient(options);
const httpResponseHandler = this._createHttpResponseHandler(options);
const endpoint = new HttpEndpoint(httpClient, httpResponseHandler, options.routing);
const firebaseDatabaseClient = new FirebaseDatabaseClient(options.firebaseDatabase);
const requestFactory = ActorRequestFactory.create(options);
const subscriptionService =
Expand Down
59 changes: 49 additions & 10 deletions client-js/main/client/http-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,35 +41,74 @@ export class HttpClient {
/**
* Creates a new instance of HttpClient.
*
* @param {!string} appBaseUrl an application base URL (the protocol and the domain name) represented as
* a string
* @param {!string} appBaseUrl an application base URL (the protocol and the domain name)
* represented as a string
*/
constructor(appBaseUrl) {
this._appBaseUrl = appBaseUrl;
}

/**
* Sends the given message to the given endpoint.
* Sends the given message to the given endpoint via `POST` request.
*
* The message is sent as in form of a Base64-encoded byte string.
*
* @param {!string} endpoint a endpoint to send the message to
* @param {!TypedMessage} message a message to send, as a {@link TypedMessage}
* @return {Promise<Response|Error>} a message sending promise to be fulfilled with a response, or rejected if
* an error occurs
* @return {Promise<Response|Error>} a message sending promise to be fulfilled with a response,
* or rejected if an error occurs
* @see toBody
* @see headers
*/
postMessage(endpoint, message) {
const messageString = message.toBase64();
const messageString = this.toBody(message);
const path = endpoint.startsWith('/') ? endpoint : '/' + endpoint;
const url = this._appBaseUrl + path;
const request = {
method: 'POST',
body: messageString,
headers: {
'Content-Type': 'application/x-protobuf'
},
mode: 'cors'
headers: this.headers(message),
mode: this.requestMode(message)
};
return fetch(url, request);
}

/**
* Returns the mode in which the HTTP request transferring the given message is sent.
*
* This implementation returns `cors`.
*
* @param {!TypedMessage} message a message to send, as a {@link TypedMessage}
* @return {string} the mode of HTTP requests to use
*/
requestMode(message) {
return 'cors';
}

/**
* Returns the string-typed map of HTTP header names to header values,
* which to use in order to send the passed message.
*
* In this implementation, returns {'Content-Type': 'application/x-protobuf'}.
*
* @param {!TypedMessage} message a message to send, as a {@link TypedMessage}
* @returns {{"Content-Type": string}}
*/
headers(message) {
return {
'Content-Type': 'application/x-protobuf'
};
}

/**
* Transforms the given message to a string, which would become a POST request body.
*
* Uses {@link TypedMessage#toBase64 Base64 encoding} to transform the message.
*
* @param {!TypedMessage} message a message to transform into a POST body
* @returns {!string} transformed message
*/
toBody(message) {
return message.toBase64();
}
}
88 changes: 8 additions & 80 deletions client-js/main/client/http-endpoint.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@
"use strict";

import {TypedMessage} from './typed-message';
import {ClientError, ConnectionError, ServerError, SpineError} from './errors';
import {Subscriptions} from '../proto/spine/web/keeping_up_pb';
import {HttpResponseHandler} from "./http-response-handler";

/**
* @typedef {Object} SubscriptionRouting
Expand Down Expand Up @@ -232,12 +232,14 @@ 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, routing) {
constructor(httpClient, responseHandler, routing) {
super();
this._httpClient = httpClient;
this._routing = routing;
this._responseHandler = responseHandler;
}

/**
Expand Down Expand Up @@ -365,85 +367,11 @@ export class HttpEndpoint extends Endpoint {
return new Promise((resolve, reject) => {
this._httpClient
.postMessage(endpoint, message)
.then(HttpEndpoint._jsonOrError, HttpEndpoint._connectionError)
.then(this._responseHandler.handle
.bind(this._responseHandler),
this._responseHandler.onConnectionError
.bind(this._responseHandler))
.then(resolve, reject);
});
}

/**
* Retrieves the JSON data from the given response if it was successful, rejects
* with a respective error otherwise.
*
* @param {!Response} response an HTTP request response
* @return {Promise<Object|SpineError>} a promise of a successful server response JSON data,
* rejected if the client response is not `2xx`,
* or if JSON parsing fails
* @private
*/
static _jsonOrError(response) {
const statusCode = response.status;
if (HttpEndpoint._isSuccessfulResponse(statusCode)) {
return HttpEndpoint._parseJson(response);
}
else if (HttpEndpoint._isClientErrorResponse(statusCode)) {
return Promise.reject(new ClientError(response.statusText, response));
}
else if(HttpEndpoint._isServerErrorResponse(statusCode)) {
return Promise.reject(new ServerError(response));
}
}

/**
* Parses the given response JSON data, rejects if parsing fails.
*
* @param {!Response} response an HTTP request response
* @return {Promise<Object|SpineError>} a promise of a server response parsing to be fulfilled
* with a JSON data or rejected with {@link SpineError} if
* JSON parsing fails.
* @private
*/
static _parseJson(response) {
return response.json()
.then(json => Promise.resolve(json))
.catch(error => Promise.reject(new SpineError('Failed to parse response JSON', error)));
}

/**
* Gets the error caught from the {@link HttpClient#postMessage} and returns
* a rejected promise with a given error wrapped into {@link ConnectionError}.
*
* @param {!Error} error an error which occurred upon message sending
* @return {Promise<ConnectionError>} a rejected promise with a `ConnectionError`
* @private
*/
static _connectionError(error) {
return Promise.reject(new ConnectionError(error));
}

/**
* @param {!number} statusCode an HTTP request response status code
* @return {boolean} `true` if the response status code is from 200 to 299, `false` otherwise
* @private
*/
static _isSuccessfulResponse(statusCode) {
return 200 <= statusCode && statusCode < 300;
}

/**
* @param {!number} statusCode an HTTP request response status code
* @return {boolean} `true` if the response status code is from 400 to 499, `false` otherwise
* @private
*/
static _isClientErrorResponse(statusCode) {
return 400 <= statusCode && statusCode < 500;
}

/**
* @param {!number} statusCode an HTTP request response status code
* @return {boolean} `true` if the response status code is from 500, `false` otherwise
* @private
*/
static _isServerErrorResponse(statusCode) {
return 500 <= statusCode;
}
}
Loading