jsms-ext-chromium
Version:
JSMS extension for integration of Chromium Embedded Framework (CEF) clients.
245 lines (242 loc) • 10.5 kB
JavaScript
import { JsmsConnection, JsmsMessage, JsmsQueue, JsmsQueueSender, JsmsQueueReceiver, JsmsTopic, JsmsTopicPublisher, JsmsTopicSubscriber, JsmsDeferred } from 'jsms';
import { getLogger } from '@log4js-node/log4js-api';
function currentTimeMillis() {
return new Date().getTime();
}
class ChromiumConnection extends JsmsConnection {
/**
* @param defaultTimeToLive will be used to calculate expiration time when no
* custom value is provided to the send function
* @param maxHandshakeRetries specifies how often the handshake will be retried
* @param globalNS is only used by unit tests -
* in production code you should ignore it and just leave it undefined
*/
constructor(defaultTimeToLive = ChromiumConnection.DEFAULT_TIME_TO_LIVE, maxHandshakeRetries = ChromiumConnection.DEFAULT_HANDSHAKE_RETRY_COUNT, globalNS = window) {
super();
this.defaultTimeToLive = defaultTimeToLive;
this.maxHandshakeRetries = maxHandshakeRetries;
this.globalNS = globalNS;
this.logger = getLogger("[CHROMIUM]");
this.sendFunction = null;
this.responseDeferreds = new Map();
this.currentHandshakeRetries = 0;
this.globalNS.onMessage = (json) => {
try {
this.onMessage(JsmsMessage.fromJSON(json));
}
catch (e) {
this.logger.error(e);
}
};
}
onMessage(message) {
const responseDeferred = this.responseDeferreds.get(message.header.correlationID);
if (responseDeferred) {
this.handleResponse(message, responseDeferred);
}
else {
const destination = this.getDestinationFor(message.header.destination);
const consumer = this.getConsumer(destination);
consumer.onMessage(message).then(response => {
if (Object.keys(response.body).length > 0) {
this.send(response);
}
});
}
}
handleResponse(response, responseDeferred) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Receiving response: \""
+ response.header.destination + "\" ["
+ response.header.correlationID + "]:\n"
+ JSON.stringify(response.body));
}
this.responseDeferreds.delete(response.header.correlationID);
responseDeferred.resolve(response);
}
createQueue(queueName) {
const queue = new JsmsQueue(queueName);
this.addQueue(queue, new JsmsQueueSender(this, queue), new JsmsQueueReceiver(queue));
return queue;
}
createTopic(topicName) {
const topic = new JsmsTopic(topicName);
this.addTopic(topic, new JsmsTopicPublisher(topic), new JsmsTopicSubscriber(topic));
return topic;
}
send(message) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Sending request: \""
+ message.header.destination + "\" ["
+ message.header.correlationID + "]:\n"
+ JSON.stringify(message.body));
}
const deferredResponse = new JsmsDeferred();
const sendFunction = this.getSendFunction();
sendFunction(message, deferredResponse);
this.handleExpiration(message, deferredResponse);
return deferredResponse;
}
getSendFunction() {
if (this.sendFunction) {
return this.sendFunction;
}
if (this.isCEF()) {
this.initCEFConnection();
}
else if (this.isWebView2()) {
this.initWebView2Connection();
}
else {
throw new Error("No messaging function available");
}
return this.sendFunction;
}
isCEF() {
return typeof this.globalNS.cefQuery === "function";
}
initCEFConnection() {
this.sendFunction =
(message, deferredResponse) => this.sendToCEF(message, deferredResponse);
}
isWebView2() {
return this.globalNS.chrome
&& this.globalNS.chrome.webview
&& typeof this.globalNS.chrome.webview.postMessage === "function";
}
initWebView2Connection() {
this.sendFunction =
(message, deferredResponse) => this.sendToWebView2(message, deferredResponse);
this.globalNS.chrome.webview.addEventListener('message', (arg) => {
const envelope = arg.data;
const status = envelope.status;
if (status === 0) {
const response = JsmsMessage.fromJSON(envelope.response);
this.handleSuccessResponse(response);
}
else {
const request = JsmsMessage.fromJSON(envelope.request);
this.handleErrorResponse(request.header.destination, request.header.correlationID, envelope.errorCode, envelope.errorMessage);
}
});
}
handleSuccessResponse(response) {
const responseDeferred = this.responseDeferreds.get(response.header.correlationID);
responseDeferred === null || responseDeferred === void 0 ? void 0 : responseDeferred.resolve(response);
this.responseDeferreds.delete(response.header.correlationID);
}
handleErrorResponse(destination, correlationID, errorCode, errorMessage) {
this.logger.error("Native call failed for: "
+ "\ndestination: " + destination
+ "\nerror-code: " + errorCode
+ "\nerror-message: " + errorMessage);
const responseDeferred = this.responseDeferreds.get(correlationID);
responseDeferred === null || responseDeferred === void 0 ? void 0 : responseDeferred.reject(errorMessage);
this.responseDeferreds.delete(correlationID);
}
sendToCEF(message, deferredResponse) {
this.responseDeferreds.set(message.header.correlationID, deferredResponse);
const cefQuery = {
request: message.toString(),
persistent: false,
onSuccess: (responseString) => {
this.handleSuccessResponse(JsmsMessage.fromString(responseString));
},
onFailure: (errorCode, errorMessage) => {
this.handleErrorResponse(message.header.destination, message.header.correlationID, errorCode, errorMessage);
}
};
this.globalNS.cefQuery(cefQuery);
}
sendToWebView2(message, deferredResponse) {
this.responseDeferreds.set(message.header.correlationID, deferredResponse);
this.globalNS.chrome.webview.postMessage(message.toString());
}
handleExpiration(message, deferredResponse) {
if (message.header.expiration === 0) {
return;
}
let timeToLive = message.header.expiration - currentTimeMillis();
timeToLive = Math.max(0, timeToLive);
setTimeout(() => {
if (this.responseDeferreds.has(message.header.correlationID)) {
this.responseDeferreds.delete(message.header.correlationID);
deferredResponse.reject(message.createExpirationMessage());
}
}, timeToLive);
}
sendHandshake() {
this.currentHandshakeRetries = 0;
const outerDeferred = new JsmsDeferred();
try {
this.sendHandshakeInternal(outerDeferred);
}
catch (e) {
outerDeferred.reject(e);
}
return outerDeferred;
}
sendHandshakeInternal(outerDeferred) {
this.logger.info("Beginning HANDSHAKE @" + currentTimeMillis() + "...");
// first pass: check if client receives messages
const deferred = this.sendHandshakeInit();
deferred.then(() => {
// second pass: let the client know that we are able to receive messages
this.sendServerReady()
.then(ack => {
// client acknowledged - connection successfully established
this.resolveHandshake(outerDeferred, ack);
})
.catch(reason => {
this.retryHandshake(outerDeferred, reason);
});
})
.catch(error => {
this.retryHandshake(outerDeferred, error);
});
return deferred;
}
sendHandshakeInit() {
const initMessage = JsmsMessage.create(ChromiumConnection.HANDSHAKE_INIT, {}, this.defaultTimeToLive);
const deferred = this.send(initMessage);
deferred.catch(() => {
this.logger.error("HANDSHAKE INIT TIMEOUT: "
+ initMessage.header.correlationID
+ ' @' + currentTimeMillis());
});
return deferred;
}
sendServerReady() {
const serverReadyMessage = JsmsMessage.create(ChromiumConnection.HANDSHAKE_SERVER_READY, {}, this.defaultTimeToLive);
const deferred = this.send(serverReadyMessage);
deferred.catch(() => {
this.logger.error("SERVER READY TIMEOUT: "
+ serverReadyMessage.header.correlationID
+ ' @' + currentTimeMillis());
});
return deferred;
}
resolveHandshake(outerDeferred, ack) {
this.logger.info("HANDSHAKE SUCCESSFUL @" + currentTimeMillis());
outerDeferred.resolve(ack);
}
retryHandshake(outerDeferred, error) {
if (this.currentHandshakeRetries++ < this.maxHandshakeRetries) {
this.logger.error("HANDSHAKE FAILED, retrying ...");
setTimeout(() => {
this.sendHandshakeInternal(outerDeferred);
}, ChromiumConnection.DEFAULT_HANDSHAKE_RETRY_DELAY);
}
else {
this.logger.error("HANDSHAKE FAILED @" + currentTimeMillis());
outerDeferred.reject(error);
}
}
}
ChromiumConnection.HANDSHAKE_INIT = "/jsms-ext-chromium/handshake/init";
ChromiumConnection.HANDSHAKE_CLIENT_READY = "/jsms-ext-chromium/handshake/client/ready";
ChromiumConnection.HANDSHAKE_SERVER_READY = "/jsms-ext-chromium/handshake/server/ready";
ChromiumConnection.DEFAULT_TIME_TO_LIVE = 1000;
ChromiumConnection.DEFAULT_HANDSHAKE_RETRY_COUNT = 60;
ChromiumConnection.DEFAULT_HANDSHAKE_RETRY_DELAY = 100;
export { ChromiumConnection };