@eclipse-ditto/ditto-javascript-client-dom
Version:
DOM implementation of Eclipse Ditto JavaScript API to be used in browsers.
243 lines • 7.96 kB
JavaScript
/*!
* Copyright (c) 2019 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
import { DefaultDittoProtocolEnvelope } from '../../model/ditto-protocol';
export class WebSocketRequestHandler {
constructor(resilienceHandlerFactory) {
this.requests = new Map();
this.subscriptions = new Map();
this.resilienceHandler = resilienceHandlerFactory.withRequestHandler(this);
}
/**
* Builds a request for the web socket connection.
*
* @param topic - The topic of the message.
* @param path - The path of the message.
* @param value - The payload of the message.
* @param header - The headers of the message.
* @returns A request string that can be sent over the web socket connection
*/
static buildRequest(topic, path, value, header) {
return new DefaultDittoProtocolEnvelope(topic, header, path, value);
}
/**
* Sends a message over the web socket connection and returns it's response.
*
* @param topic - The topic of the request.
* @param path - The path of the request.
* @param value - The payload of the request.
* @param header - The headers of the request.
* @returns A Promise for the request's response
*/
sendRequest(topic, path, value, header) {
return new Promise((resolve, reject) => {
const correlationId = this.getRequestId();
this.requests.set(correlationId, { resolve, reject });
const headers = header === undefined ? {} : header;
headers['correlation-id'] = correlationId;
const request = WebSocketRequestHandler.buildRequest(topic, path, value, headers);
this.resilienceHandler.sendRequest(correlationId, request);
});
}
/**
* Sends a message over the web socket connection.
*
* @param topic - The topic of the message.
* @param path - The path of the message.
* @param value - The payload of the message.
* @param header - The headers of the message.
* @returns A Promise that resolves once the message was sent
*/
sendMessage(topic, path, value, header) {
const request = WebSocketRequestHandler.buildRequest(topic, path, value, header);
return this.resilienceHandler.send(request.toJson());
}
/**
* Registers a subscription.
*
* @param subscription - The subscription to register.
* @returns The ID of the subscription
*/
subscribe(subscription) {
const id = this.getSubscriptionId();
this.subscriptions.set(id, subscription);
return id;
}
/**
* Deletes a subscription.
*
* @param id - The ID of the subscription to delete.
*/
deleteSubscription(id) {
this.subscriptions.delete(id);
}
/**
* Sends a protocol message to request a change in the information sent to the web socket connection.
*
* @param message - The message to send.
* @returns A Promise that resolves once the request was acknowledged
*/
sendProtocolMessage(message) {
return this.resilienceHandler.sendProtocolMessage(message);
}
handleInput(id, message) {
if (message.headers !== null) {
if (this.requests.has(id)) {
this.handleResponse(id, message);
}
else {
this.handleMessage(message);
}
}
else if (!this.handleMessage(message)) {
console.error(`Unmatched message: ${JSON.stringify(message)}`);
}
}
handleError(id, cause) {
const request = this.requests.get(id);
this.requests.delete(id);
if (request) {
request.reject(cause);
}
}
handleMessage(message) {
let found = false;
this.subscriptions.forEach((subscription, _) => {
if (subscription.matches(message)) {
subscription.callback(message);
found = true;
}
});
return found;
}
/**
* Matches an incoming response to the correlating request and resolves it.
*
* @param id - The correlation-id of the response.
* @param response - The incoming response.
*/
handleResponse(id, response) {
const options = this.requests.get(id);
this.requests.delete(id);
const headers = Object.keys(response.headers).reduce((map, name) => {
map.set(name, response.headers[name]);
return map;
}, new Map());
headers.delete('correlation-id');
if (options) {
options.resolve({ headers, status: response.status, body: response.value });
}
}
/**
* Generates an unused request correlation-id and returns it.
*
* @returns The correlation-id
*/
getRequestId() {
const id = WebSocketRequestHandler.generateId();
return this.requests.has(id) ? this.getRequestId() : id;
}
/**
* Generates an unused subscription id and returns it.
*
* @returns The id
*/
getSubscriptionId() {
const id = WebSocketRequestHandler.generateId();
return this.subscriptions.has(id) ? this.getRequestId() : id;
}
/**
* Generates a uuid.
*
* @returns The uuid
*/
static generateId() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
// tslint:disable-next-line
const r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
}
/**
* A subscription to messages matching a certain pattern.
*/
class Subscription {
constructor(_callback) {
this._callback = _callback;
}
/**
* Calls the callback function with the message provided.
*
* @param message - The message to send.
*/
callback(message) {
const splittedTopic = message.topic.split('/');
const action = splittedTopic.length > 0 ? splittedTopic.pop() : '';
this._callback({
action,
topic: message.topic,
path: message.path,
headers: message.headers,
value: message.value
});
}
}
/**
* A standard subscription to messages matching a specific pattern.
*/
export class StandardSubscription extends Subscription {
constructor(callback, topic, path, subResources) {
super(callback);
this.topic = topic;
this.path = path;
this.subResources = subResources;
}
matches(message) {
return this.checkTopic(message.topic) && this.checkPath(message.path);
}
/**
* Checks whether a path matches the subscription's path.
*
* @param path - The path to check.
* @returns Whether the path matches or not
*/
checkPath(path) {
if (this.subResources) {
return path.startsWith(this.path);
}
return path === this.path;
}
/**
* Checks whether a topic matches the subscription's topic.
*
* @param topic - The topic to check.
* @returns Whether the topic matches or not
*/
checkTopic(topic) {
return topic.startsWith(this.topic);
}
}
/**
* A subscription to all messages of a type.
*/
export class AllSubscription extends Subscription {
constructor(callback, type) {
super(callback);
this.type = type;
}
matches(message) {
const messageType = message.topic.split('/')[4];
return messageType === this.type;
}
}
//# sourceMappingURL=websocket-request-handler.js.map