Skip to content

Commit

Permalink
Merge pull request #194 from SpineEventEngine/1.x-customize-http-client
Browse files Browse the repository at this point in the history
[1.x] Allow customisation of `HttpClient`
  • Loading branch information
armiol authored Jan 12, 2023
2 parents f2087e5 + 95a0105 commit dae0a71
Show file tree
Hide file tree
Showing 15 changed files with 367 additions and 139 deletions.
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

0 comments on commit dae0a71

Please sign in to comment.