UNPKG

dasf-messaging-typescript

Version:

Typescript RPC bindings for the data analytics software framework (DASF)

307 lines (306 loc) 14.3 kB
"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;