dasf-messaging-typescript
Version:
Typescript RPC bindings for the data analytics software framework (DASF)
307 lines (306 loc) • 14.3 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DASFConnection = void 0;
const DASFMessages_1 = require("./DASFMessages");
const json2typescript_1 = require("json2typescript");
class DASFConnection {
outgoingChannel;
incomingChannel;
DASFProducerURL;
DASFConsumerURL;
topic;
consumeTopic;
jsonCoder;
contextCounter = 0;
responseCallbacks = new Map();
progressCallbacks = new Map();
errorCallbacks = new Map();
finishedContexts = new Set();
contextResponseFragments = new Map();
pendingRequests = [];
waitingForPong = false;
connectionTimeout = 10000; // 10sec
backendConsumerConnected = false;
connectionError = '';
onConnectionError = undefined;
constructor(urlBuilder, onConnectionError) {
//tenant/namespace/topic/subscription
//public/default/producerTopic/registry/
this.onConnectionError = onConnectionError;
this.topic = urlBuilder.topic;
this.consumeTopic = urlBuilder.consumeTopic;
this.DASFProducerURL = urlBuilder.DASFProducerURL;
this.DASFConsumerURL = urlBuilder.DASFConsumerURL;
this.jsonCoder = new json2typescript_1.JsonConvert();
this.assureConnected();
}
connect() {
this.outgoingChannel = new WebSocket(this.DASFProducerURL);
this.incomingChannel = new WebSocket(this.DASFConsumerURL);
this.registerListener();
this.getInitialModuleInfo();
setInterval(() => this.checkConnection(), this.connectionTimeout);
}
isConnected() {
return this.outgoingChannel && this.incomingChannel && this.outgoingChannel.readyState == WebSocket.OPEN && this.incomingChannel.readyState == WebSocket.OPEN;
}
isConnecting() {
return this.outgoingChannel && this.incomingChannel && this.outgoingChannel.readyState != WebSocket.OPEN && this.incomingChannel.readyState != WebSocket.OPEN;
}
assureConnected() {
if (this.isConnected()) {
return true;
}
else {
if (!this.isConnecting()) {
this.connect();
}
return false;
}
}
hasPendingRequests() {
return this.pendingRequests.length > 0;
}
sendPendingRequests() {
if (this.hasPendingRequests()) {
// console.log('sending pending request (' + this.pendingRequests.length + ') ...');
const request = this.pendingRequests.shift();
this.outgoingChannel.send(JSON.stringify(request));
setTimeout(() => this.sendPendingRequests(), 200);
}
}
registerListener() {
const onError = (ev) => {
if (!this.hasConnectionError() && this.isConnecting()) {
this.connectionError = 'Connection to ' + ev.target.url + ' failed.';
if (this.onConnectionError) {
this.onConnectionError(this.connectionError);
}
}
};
this.outgoingChannel.addEventListener('error', onError);
this.incomingChannel.addEventListener('error', onError);
this.outgoingChannel.addEventListener('open', () => {
// console.log("DASF outgoing channel established");
// connection established - reset past connection errors
this.connectionError = '';
// listen for acknowledgments of outgoing messages
this.outgoingChannel.addEventListener('message', (ev) => {
// console.log("[DASF outgoing] request acknowledged");
const receipt = this.jsonCoder.deserializeObject(JSON.parse(ev.data), DASFMessages_1.DASFModuleRequestReceipt);
if (receipt.isOk()) {
// the request was acknowledged - remove error callback
this.errorCallbacks.delete(receipt.context);
}
else {
// the request returned an error
console.error(receipt);
// remove the registered callback - since there won't be a response
this.responseCallbacks.delete(receipt.context);
// trigger error callback
const onErrorCallback = this.errorCallbacks.get(receipt.context);
if (onErrorCallback) {
// remove callback from map
this.errorCallbacks.delete(receipt.context);
// trigger error callback
onErrorCallback(receipt);
}
else {
console.error("received error receipt for an unknwon context: " + receipt.context);
}
}
});
});
this.incomingChannel.addEventListener('open', () => {
// connection established - reset past connection errors
this.connectionError = '';
// listen for incoming messages
this.incomingChannel.addEventListener('message', (ev) => {
// console.log("incoming message...");
try {
// parse and decode message
const data = JSON.parse(ev.data);
const response = this.jsonCoder.deserializeObject(data, DASFMessages_1.DASFModuleResponse);
// acknowledge the message
this.acknowledgeMessage(response);
// extract request context and trigger callback
const requestContext = response.getRequestContext();
if (this.finishedContexts.has(requestContext)) {
// this request has already been processed - ignore duplicate
return;
}
if (response.getMessageType() == DASFMessages_1.MessageType.Progress) {
const progressReport = this.jsonCoder.deserializeObject(JSON.parse(atob(data.payload)), DASFMessages_1.DASFProgressReport);
const progressCallback = this.progressCallbacks.get(requestContext);
if (progressCallback) {
progressCallback(progressReport, response.properties);
}
}
else {
// is this just a fragment - or the whole thing ?
if (response.isFragmented()) {
// fragmented message
if (this.contextResponseFragments.has(requestContext)) {
// we already have some fragments - append
let fragments = this.contextResponseFragments.get(requestContext);
if (fragments) {
fragments.push(response);
// console.log('fragment ' + response.getFragmentId() + ' of ' + response.getNumberOfFragments() + ' received');
if (fragments.length == response.getNumberOfFragments()) {
// console.log('assembling fragmented payload');
// all fragments received - assemble
fragments = fragments.sort((a, b) => a.getFragmentId() - b.getFragmentId());
let data = '';
for (const frag of fragments) {
data += frag.payload;
}
// pretend the last response message contained the entire data
response.payload = data;
// clear the fragment data
this.contextResponseFragments.delete(requestContext);
}
else {
// fragments left - continue
return;
}
}
}
else {
// first fragmented message received - store
this.contextResponseFragments.set(requestContext, [response]);
return;
}
}
// get callback function for context
const responseCallback = this.responseCallbacks.get(requestContext);
if (responseCallback) {
// remove it from the map
this.responseCallbacks.delete(requestContext);
this.progressCallbacks.delete(requestContext);
this.errorCallbacks.delete(requestContext);
this.finishedContexts.add(requestContext);
// trigger callback
responseCallback(response);
}
else {
console.warn("received a response for an unknown context: " + requestContext, response);
}
}
}
catch (e) {
console.error(e);
}
});
});
}
checkConnection() {
if (this.waitingForPong) {
// we sent a ping but still waiting for the pong - timeout
this.backendConsumerConnected = false;
console.warn("Ping to backend module timed out.");
}
// send ping
//console.log('sending ping');
this.waitingForPong = true;
// create the ping message
const ping = DASFMessages_1.DASFModuleRequest.createPingMessage();
// send the request
this.sendRequest(ping, () => {
// we received a pong
this.waitingForPong = false;
this.backendConsumerConnected = true;
// send pending request if any
this.sendPendingRequests();
}, undefined, errorReceipt => {
console.warn(errorReceipt.errorMsg);
});
}
sendRequest(requestMessage, onResponse, onProgress, onError) {
// create context id
requestMessage.context = String(++this.contextCounter);
if (!requestMessage.properties) {
throw new Error('missing request properties');
}
// set response topic prop
requestMessage.properties[DASFMessages_1.PropertyKeys.ResponseTopic] = this.consumeTopic;
requestMessage.properties[DASFMessages_1.PropertyKeys.RequestContext] = requestMessage.context;
// store context->callback mapping
this.responseCallbacks.set(requestMessage.context, onResponse);
this.progressCallbacks.set(requestMessage.context, onProgress);
this.errorCallbacks.set(requestMessage.context, onError);
if ((requestMessage.getMessageType() == DASFMessages_1.MessageType.Request || requestMessage.getMessageType() == DASFMessages_1.MessageType.Info) && !this.backendConsumerConnected) {
// store as pending request
// console.log('storing request due to unconnected backend module')
if (this.hasConnectionError() && onError != undefined) {
const receipt = new DASFMessages_1.DASFModuleRequestReceipt();
receipt.context = requestMessage.context;
receipt.errorMsg = this.connectionError;
this.responseCallbacks.delete(requestMessage.context);
this.progressCallbacks.delete(requestMessage.context);
this.errorCallbacks.delete(requestMessage.context);
onError(receipt);
}
this.pendingRequests.push(requestMessage);
return;
}
if (this.assureConnected()) {
// console.log("sending request...");
// send the message as a json string
const msg_str = JSON.stringify(requestMessage);
this.outgoingChannel.send(msg_str);
}
}
acknowledgeMessage(message) {
const ack = this.jsonCoder.serializeObject(new DASFMessages_1.DASFAcknowledgment(message.messageId));
this.incomingChannel.send(JSON.stringify(ack));
}
close() {
this.outgoingChannel.close();
this.incomingChannel.close();
}
getInitialModuleInfo() {
if (this.backendConsumerConnected) {
// we are connected - request module info
this.getModuleInfo((info) => {
console.info('module for topic ' + this.topic + ' provided the following module info: ', info);
});
}
else {
// not (yet) connected request module info later
setTimeout(() => this.getInitialModuleInfo(), 1000);
}
}
getModuleInfo(onModuleInfo) {
// console.log('sending module info request');
this.sendRequest(DASFMessages_1.DASFModuleRequest.createInfoMessage(), (res) => {
if (res.properties) {
try {
onModuleInfo(JSON.parse(res.properties['info']));
}
catch (e) {
// unable to parse
console.warn('Unable to retrieve backend module capabilities, got: ', res.properties['info']);
}
}
}, () => {
try {
console.info('getModuleInfo success');
}
catch (e) {
console.warn('getModuleInfo failure');
}
}, (receipt) => {
throw new Error(receipt.errorMsg);
});
}
isBackendConnected() {
return this.backendConsumerConnected;
}
hasConnectionError() {
return this.connectionError.length > 0;
}
getConnectionErrorMessage() {
return this.connectionError;
}
}
exports.DASFConnection = DASFConnection;