Files
grin-web-wallet/scripts/listener.js
2024-12-20 18:08:44 -08:00

1902 lines
47 KiB
JavaScript
Executable File

// Use strict
"use strict";
// Classes
// Listener class
class Listener {
// Public
// Constructor
constructor(settings) {
// Set settings
this.settings = settings;
// Set connection to no connection
this.connection = Listener.NO_CONNECTION;
// Set retry delay to initial value
this.retryDelay = Listener.INITIAL_RETRY_DELAY_MILLISECONDS;
// Set request index
this.requestIndex = 0;
// Set reconnect timeout to no timeout
this.reconnectTimeout = Listener.NO_TIMEOUT;
// Set started to false
this.started = false;
// Set connected to false
this.connected = false;
// Set use custom listener to setting's default value
this.useCustomListener = Listener.SETTINGS_USE_CUSTOM_LISTENER_DEFAULT_VALUE;
// Set custom listener address to setting's default value
this.customListenerAddress = Listener.SETTINGS_CUSTOM_LISTENER_ADDRESS_DEFAULT_VALUE;
// Set ignore response index
this.ignoreResponseIndex = 0;
// Set self
var self = this;
// Once database is initialized
Database.onceInitialized(function() {
// Return promise
return new Promise(function(resolve, reject) {
// Return creating settings
return Promise.all([
// Use custom listener setting
self.settings.createValue(Listener.SETTINGS_USE_CUSTOM_LISTENER_NAME, Listener.SETTINGS_USE_CUSTOM_LISTENER_DEFAULT_VALUE),
// Custom listener address setting
self.settings.createValue(Listener.SETTINGS_CUSTOM_LISTENER_ADDRESS_NAME, Listener.SETTINGS_CUSTOM_LISTENER_ADDRESS_DEFAULT_VALUE)
]).then(function() {
// Initialize settings
var settings = [
// Use custom listener setting
Listener.SETTINGS_USE_CUSTOM_LISTENER_NAME,
// Custom listener address setting
Listener.SETTINGS_CUSTOM_LISTENER_ADDRESS_NAME
];
// Return getting settings' values
return Promise.all(settings.map(function(setting) {
// Return getting setting's value
return self.settings.getValue(setting);
})).then(function(settingValues) {
// Set use custom listener to setting's value
self.useCustomListener = settingValues[settings.indexOf(Listener.SETTINGS_USE_CUSTOM_LISTENER_NAME)];
// Set custom listener address to setting's value
self.customListenerAddress = settingValues[settings.indexOf(Listener.SETTINGS_CUSTOM_LISTENER_ADDRESS_NAME)];
// Resolve
resolve();
// Catch errors
}).catch(function(error) {
// Reject
reject();
});
// Catch errors
}).catch(function(error) {
// Reject
reject();
});
});
});
// Settings change event
$(this.settings).on(Settings.CHANGE_EVENT, function(event, setting) {
// Initialize listener setting changes
var listenerSettingChanged = false;
// Check what setting was changes
switch(setting[Settings.DATABASE_SETTING_NAME]) {
// Use custom listener setting
case Listener.SETTINGS_USE_CUSTOM_LISTENER_NAME:
// Set use custom listener to setting's value
self.useCustomListener = setting[Settings.DATABASE_VALUE_NAME];
// Set listener settings changed
listenerSettingChanged = true;
// Break
break;
// Custom listener address setting
case Listener.SETTINGS_CUSTOM_LISTENER_ADDRESS_NAME:
// Set custom listener address to setting's value
self.customListenerAddress = setting[Settings.DATABASE_VALUE_NAME];
// Set listener settings changed
listenerSettingChanged = true;
// Break
break;
}
// Check if a listener setting was changed
if(listenerSettingChanged === true) {
// Check if connected
if(self.connected === true) {
// Log message
Log.logMessage(Language.getDefaultTranslation('Listener settings changed. Disconnecting from the listener.'));
}
// Check if connection exists
if(self.connection !== Listener.NO_CONNECTION) {
// Try
try {
// Close connection
self.connection.close();
}
// Catch errors
catch(error) {
}
// Set connection to no connection
self.connection = Listener.NO_CONNECTION;
}
// Check if reconnect timeout exists
if(self.reconnectTimeout !== Listener.NO_TIMEOUT) {
// Clear reconnect timeout
clearTimeout(self.reconnectTimeout);
// Set reconnect timeout to no timeout
self.reconnectTimeout = Listener.NO_TIMEOUT;
}
// Reset retry delay
self.retryDelay = Listener.INITIAL_RETRY_DELAY_MILLISECONDS;
// Trigger settings change event
$(self).trigger(Listener.SETTINGS_CHANGE_EVENT);
// Clear connected
self.connected = false;
// Connect
self.connect();
}
});
// Window online event
$(window).on("online", function() {
// Check if reconnect timeout exists
if(self.reconnectTimeout !== Listener.NO_TIMEOUT) {
// Clear reconnect timeout
clearTimeout(self.reconnectTimeout);
// Set reconnect timeout to no timeout
self.reconnectTimeout = Listener.NO_TIMEOUT;
// Reset retry delay
self.retryDelay = Listener.INITIAL_RETRY_DELAY_MILLISECONDS;
// Connect
self.connect();
}
// Window offline event
}).on("offline", function() {
// Check if started
if(self.started === true) {
// Check if connected
if(self.connected === true) {
// Log message
Log.logMessage(Language.getDefaultTranslation('Network changed. Disconnecting from the listener.'));
}
// Check if connection exists
if(self.connection !== Listener.NO_CONNECTION) {
// Try
try {
// Close connection
self.connection.close();
}
// Catch errors
catch(error) {
}
// Set connection to no connection
self.connection = Listener.NO_CONNECTION;
}
// Check if reconnect timeout exists
if(self.reconnectTimeout !== Listener.NO_TIMEOUT) {
// Clear reconnect timeout
clearTimeout(self.reconnectTimeout);
// Set reconnect timeout to no timeout
self.reconnectTimeout = Listener.NO_TIMEOUT;
}
// Reset retry delay
self.retryDelay = Listener.INITIAL_RETRY_DELAY_MILLISECONDS;
// Clear connected
self.connected = false;
// Connect
self.connect();
}
});
}
// Using custom listener
usingCustomListener() {
// Return if using a custom listener
return this.useCustomListener === true;
}
// Get address
getAddress() {
// Check if not using a custom listener
if(this.usingCustomListener() === false) {
// Return default listener address
return Listener.DEFAULT_LISTENER_ADDRESS;
}
// Otherwise
else {
// Get custom listener address
var customListenerAddress = this.customListenerAddress.trim();
// Check if custom listener address isn't set
if(customListenerAddress["length"] === 0) {
// Return custom listener address
return customListenerAddress;
}
// Otherwise
else {
// Check if custom listener address doesn't have a protocol
if(Common.urlContainsProtocol(customListenerAddress) === false) {
// Add protocol to custom listener address
customListenerAddress = Common.WEBSOCKET_PROTOCOL + "//" + customListenerAddress;
}
// Otherwise
else {
// Initialize error occurred
var errorOccurred = false;
// Try
try {
// Parse custom listener address as a URL
var parsedUrl = new URL(customListenerAddress);
}
// Catch errors
catch(error) {
// Set error occurred
errorOccurred = true;
}
// Check if an error didn't occur
if(errorOccurred === false) {
// Check if URL's protocol is HTTP
if(parsedUrl["protocol"] === Common.HTTP_PROTOCOL) {
// Change custom listener address's protocol to WebSocket
customListenerAddress = Common.WEBSOCKET_PROTOCOL + Common.ltrim(customListenerAddress).substring(Common.HTTP_PROTOCOL["length"]);
}
// Otherwise check if URL's protocol is HTTPS
else if(parsedUrl["protocol"] === Common.HTTPS_PROTOCOL) {
// Change custom listener address's protocol to WebSocket secure
customListenerAddress = Common.WEBSOCKET_SECURE_PROTOCOL + Common.ltrim(customListenerAddress).substring(Common.HTTPS_PROTOCOL["length"]);
}
}
}
// Return custom listener address upgraded if applicable and without any trailing slashes
return Common.removeTrailingSlashes(Common.upgradeApplicableInsecureUrl(customListenerAddress));
}
}
}
// Start
start() {
// Check if not already started
if(this.started === false) {
// Set started
this.started = true;
// Connect
this.connect();
}
}
// Create URL
createUrl() {
// Set self
var self = this;
// Return promise
return new Promise(function(resolve, reject) {
// Return sending request to create URL
return self.sendRequest({
// Request
"Request": Listener.CREATE_URL_REQUEST_NAME
}).then(function(url) {
// Check if URL is valid
if(typeof url === "string")
// Resolve URL
resolve(url);
// Otherwise
else
// Reject error
reject("Creating URL failed.");
// Catch errors
}).catch(function(error) {
// Check error
switch(error) {
// Connection error
case Listener.CONNECTION_ERROR:
// Reject error
reject("Connecting to the listener failed.");
// Break
break;
// Timeout error
case Listener.TIMEOUT_ERROR:
// Reject error
reject("Creating URL timed out.");
// Break
break;
// Invalid response error or default
case Listener.INVALID_RESPONSE_ERROR:
default:
// Reject error
reject("Creating URL failed.");
// Break
break;
}
});
});
}
// Change URL
changeUrl(url) {
// Set self
var self = this;
// Return promise
return new Promise(function(resolve, reject) {
// Check if connected
if(self.isConnected() === true) {
// Return sending request to change URL
return self.sendRequest({
// Request
"Request": Listener.CHANGE_URL_REQUEST_NAME,
// URL
"URL": url
}).then(function(url) {
// Check if URL is valid
if(typeof url === "string")
// Resolve URL
resolve(url);
// Otherwise
else
// Reject error
reject(Message.createText(Language.getDefaultTranslation('Changing the address suffix failed.')));
// Catch errors
}).catch(function(error) {
// Check error
switch(error) {
// Connection error
case Listener.CONNECTION_ERROR:
// Reject error
reject(Message.createText(Language.getDefaultTranslation('Connecting to the listener failed.')) + Message.createText(Language.getDefaultTranslation('(?<=.) ')) + Message.createText(Language.getDefaultTranslation('You won\'t be able to change address suffixes without being connected to a listener.')));
// Break
break;
// Timeout error
case Listener.TIMEOUT_ERROR:
// Reject error
reject(Message.createText(Language.getDefaultTranslation('Changing the address suffix timed out.')));
// Break
break;
// Invalid response error or default
case Listener.INVALID_RESPONSE_ERROR:
default:
// Reject error
reject(Message.createText(Language.getDefaultTranslation('Changing the address suffix failed.')));
// Break
break;
}
});
}
// Otherwise
else {
// Reject error
reject(Message.createText(Language.getDefaultTranslation('You aren\'t connected to a listener.')) + Message.createText(Language.getDefaultTranslation('(?<=.) ')) + Message.createText(Language.getDefaultTranslation('You won\'t be able to change address suffixes without being connected to a listener.')));
}
});
}
// Owns URL
ownsUrl(url) {
// Set self
var self = this;
// Return promise
return new Promise(function(resolve, reject) {
// Return sending request to see if owns the URL
return self.sendRequest({
// Request
"Request": Listener.OWN_URL_REQUEST_NAME,
// URL
"URL": url
}).then(function(ownsUrl) {
// Check if owns URL is valid
if(typeof ownsUrl === "boolean")
// Resolve owns URL
resolve(ownsUrl);
// Otherwise
else
// Reject error
reject("Checking if owns URL failed.");
// Catch errors
}).catch(function(error) {
// Check error
switch(error) {
// Connection error
case Listener.CONNECTION_ERROR:
// Reject error
reject("Connecting to the listener failed.");
// Break
break;
// Timeout error
case Listener.TIMEOUT_ERROR:
// Reject error
reject("Checking if owns URL timed out.");
// Break
break;
// Invalid response error or default
case Listener.INVALID_RESPONSE_ERROR:
default:
// Reject error
reject("Checking if owns URL failed.");
// Break
break;
}
});
});
}
// Delete URL
deleteUrl(url) {
// Set self
var self = this;
// Return promise
return new Promise(function(resolve, reject) {
// Return sending request to delete URL
return self.sendRequest({
// Request
"Request": Listener.DELETE_URL_REQUEST_NAME,
// URL
"URL": url
}).then(function(deletedUrl) {
// Check if deleted URL is valid
if(typeof deletedUrl === "boolean")
// Resolve deleted URL
resolve(deletedUrl);
// Otherwise
else
// Reject error
reject("Deleting URL failed.");
// Catch errors
}).catch(function(error) {
// Check error
switch(error) {
// Connection error
case Listener.CONNECTION_ERROR:
// Reject error
reject("Connecting to the listener failed.");
// Break
break;
// Timeout error
case Listener.TIMEOUT_ERROR:
// Reject error
reject("Deleting URL timed out.");
// Break
break;
// Invalid response error
case Listener.INVALID_RESPONSE_ERROR:
// Resolve false
resolve(false);
// Break
break;
// Default
default:
// Reject error
reject("Deleting URL failed.");
// Break
break;
}
});
});
}
// Respond with data
respondWithData(interaction, data) {
// Turn off status receive listener cancel interaction event
$(this).off(Listener.STATUS_RECEIVE_EVENT + ".listenerCancel" + interaction.toFixed());
// Turn off settings change listener cancel interaction event
$(this).off(Listener.SETTINGS_CHANGE_EVENT + ".listenerCancel" + interaction.toFixed());
// Set self
var self = this;
// Return promise
return new Promise(function(resolve, reject) {
// Return sending response
return self.sendResponse({
// Interaction
"Interaction": interaction,
// Type
"Type": (Object.isObject(data) === true) ? "application/json" : "text/plain",
// Data
"Data": Base64.fromUint8Array((new TextEncoder()).encode((Object.isObject(data) === true) ? JSONBigNumber.stringify(data) : data)),
// Status
"Status": Common.HTTP_OK_STATUS
}).then(function(status) {
// Check if status is valid
if(typeof status === "string")
// Resolve if status succeeded
resolve(status === Listener.STATUS_SUCCEEDED_VALUE);
// Otherwise
else
// Reject error
reject("Sending response failed.");
// Catch errors
}).catch(function(error) {
// Check error
switch(error) {
// Connection error
case Listener.CONNECTION_ERROR:
// Reject error
reject("Connecting to the listener failed.");
// Break
break;
// Timeout error
case Listener.TIMEOUT_ERROR:
// Reject error
reject("Sending response timed out.");
// Break
break;
// Invalid status error or default
case Listener.INVALID_STATUS_ERROR:
default:
// Reject error
reject("Sending response failed.");
// Break
break;
}
});
});
}
// Respond with error
respondWithError(interaction, data) {
// Turn off status receive listener cancel interaction event
$(this).off(Listener.STATUS_RECEIVE_EVENT + ".listenerCancel" + interaction.toFixed());
// Turn off settings change listener cancel interaction event
$(this).off(Listener.SETTINGS_CHANGE_EVENT + ".listenerCancel" + interaction.toFixed());
// Set self
var self = this;
// Return promise
return new Promise(function(resolve, reject) {
// Set status to HTTP ok status
var status = Common.HTTP_OK_STATUS;
// Check if data is a status response
if(typeof data === "number") {
// Check data
switch(data) {
// Ok response
case Listener.OK_RESPONSE:
// Set status to HTTP ok status
status = Common.HTTP_OK_STATUS;
// Break
break;
// Unauthoirzed response
case Listener.UNAUTHORIZED_RESPONSE:
// Set status to HTTP unauthorized status
status = Common.HTTP_UNAUTHORIZED_STATUS;
// Break
break;
// Forbidden response
case Listener.FORBIDDEN_RESPONSE:
// Set status to HTTP forbidden status
status = Common.HTTP_FORBIDDEN_STATUS;
// Break
break;
// Not found response
case Listener.NOT_FOUND_RESPONSE:
// Set status to HTTP not found status
status = Common.HTTP_NOT_FOUND_STATUS;
// Break
break;
// Unsupported media type response
case Listener.UNSUPPORTED_MEDIA_TYPE_RESPONSE:
// Set status to HTTP unsupported media type status
status = Common.HTTP_UNSUPPORTED_MEDIA_TYPE_STATUS;
// Break
break;
}
}
// Return sending response
return self.sendResponse({
// Interaction
"Interaction": interaction,
// Type
"Type": (Object.isObject(data) === true) ? "application/json" : "text/plain",
// Data
"Data": Base64.fromUint8Array((new TextEncoder()).encode((Object.isObject(data) === true) ? JSONBigNumber.stringify(data) : ((typeof data === "number") ? "" : data))),
// Status
"Status": status
}).then(function(status) {
// Check if status is valid
if(typeof status === "string")
// Resolve true
resolve(true);
// Otherwise
else
// Reject error
reject("Sending response failed.");
// Catch errors
}).catch(function(error) {
// Check error
switch(error) {
// Connection error
case Listener.CONNECTION_ERROR:
// Reject error
reject("Connecting to the listener failed.");
// Break
break;
// Timeout error
case Listener.TIMEOUT_ERROR:
// Reject error
reject("Sending response timed out.");
// Break
break;
// Invalid status error or default
case Listener.INVALID_STATUS_ERROR:
default:
// Reject error
reject("Sending response failed.");
// Break
break;
}
});
});
}
// Connection open event
static get CONNECTION_OPEN_EVENT() {
// Return connection open event
return "ListenerConnectionOpenEvent";
}
// Connection close event
static get CONNECTION_CLOSE_EVENT() {
// Return connection close event
return "ListenerConnectionCloseEvent";
}
// Request receive event
static get REQUEST_RECEIVE_EVENT() {
// Return request receive event
return "ListenerRequestReceiveEvent";
}
// No connection close type
static get NO_CONNECTION_CLOSE_TYPE() {
// Return no connection close type
return 0;
}
// Disconnected close type
static get DISCONNECTED_CLOSE_TYPE() {
// Return disconnected close type
return Listener.NO_CONNECTION_CLOSE_TYPE + 1;
}
// Ok response
static get OK_RESPONSE() {
// Return ok response
return 0;
}
// Unauthorized response
static get UNAUTHORIZED_RESPONSE() {
// Return unauthorized response
return Listener.OK_RESPONSE + 1;
}
// Forbidden response
static get FORBIDDEN_RESPONSE() {
// Return forbidden response
return Listener.UNAUTHORIZED_RESPONSE + 1;
}
// Not found response
static get NOT_FOUND_RESPONSE() {
// Return not found response
return Listener.FORBIDDEN_RESPONSE + 1;
}
// Unsupported media type response
static get UNSUPPORTED_MEDIA_TYPE_RESPONSE() {
// Return unsupported media type response
return Listener.NOT_FOUND_RESPONSE + 1;
}
// Settings change event
static get SETTINGS_CHANGE_EVENT() {
// Return settings change event
return "ListenerSettingsChangeEvent";
}
// Promise resolve wallet index
static get PROMISE_RESOLVE_WALLET_INDEX() {
// Return promise resolve wallet index
return 0;
}
// Promise resolve amount index
static get PROMISE_RESOLVE_AMOUNT_INDEX() {
// Return promise resolve amount index
return Listener.PROMISE_RESOLVE_WALLET_INDEX + 1;
}
// Promise resolve currency index
static get PROMISE_RESOLVE_CURRENCY_INDEX() {
// Return promise resolve currency index
return Listener.PROMISE_RESOLVE_AMOUNT_INDEX + 1;
}
// Promise resolve message index
static get PROMISE_RESOLVE_MESSAGE_INDEX() {
// Return promise resolve message index
return Listener.PROMISE_RESOLVE_CURRENCY_INDEX + 1;
}
// Promise resolve receiver address index
static get PROMISE_RESOLVE_RECEIVER_ADDRESS_INDEX() {
// Return promise resolve receiver address index
return Listener.PROMISE_RESOLVE_MESSAGE_INDEX + 1;
}
// Promise resolve file response index
static get PROMISE_RESOLVE_FILE_RESPONSE_INDEX() {
// Return promise resolve file response index
return Listener.PROMISE_RESOLVE_RECEIVER_ADDRESS_INDEX + 1;
}
// Promise resolve ID index
static get PROMISE_RESOLVE_ID_INDEX() {
// Return promise resolve ID index
return Listener.PROMISE_RESOLVE_FILE_RESPONSE_INDEX + 1;
}
// Private
// Connect
connect() {
// Check if reconnect timeout exists
if(this.reconnectTimeout !== Listener.NO_TIMEOUT) {
// Clear reconnect timeout
clearTimeout(this.reconnectTimeout);
// Set reconnect timeout to no timeout
this.reconnectTimeout = Listener.NO_TIMEOUT;
}
// Set empty address
var emptyAddress = this.getAddress()["length"] === 0;
// Check if address exists
if(emptyAddress === false) {
// Log message
Log.logMessage(Language.getDefaultTranslation('Trying to connect to the listener at %1$y.'), [
[
// Text
this.getAddress(),
// Is raw data
true
]
]);
}
// Get current ignore response index
var index = this.ignoreResponseIndex++;
// Check if current ignore resposne index is at the max safe integer
if(index === Number.MAX_SAFE_INTEGER) {
// Reset ignore response index
this.ignoreResponseIndex = 0;
}
// Set ignore response
var ignoreResponse = false;
// Settings change listener index event
$(this).one(Listener.SETTINGS_CHANGE_EVENT + ".listener" + index.toFixed(), function() {
// Turn off window offline listener index event
$(window).off("offline" + ".listener" + index.toFixed());
// Set ignore response
ignoreResponse = true;
});
// Set self
var self = this;
// Window offline listener index event
$(window).one("offline" + ".listener" + index.toFixed(), function() {
// Turn off settings change listener index event
$(self).off(Listener.SETTINGS_CHANGE_EVENT + ".listener" + index.toFixed());
// Set ignore response
ignoreResponse = true;
});
// Try
try {
// Create connection
this.connection = new WebSocket(this.getAddress());
}
// Catch errors
catch(error) {
// Check if not ignoring response
if(ignoreResponse === false) {
// Turn off settings change listener index event
$(this).off(Listener.SETTINGS_CHANGE_EVENT + ".listener" + index.toFixed());
// Turn off window offline listener index event
$(window).off("offline" + ".listener" + index.toFixed());
// Check if address exists
if(emptyAddress === false) {
// Log message
Log.logMessage(Language.getDefaultTranslation('Connecting to the listener failed.'));
}
// Check if reconnect timeout exists
if(this.reconnectTimeout !== Listener.NO_TIMEOUT) {
// Clear reconnect timeout
clearTimeout(this.reconnectTimeout);
// Set reconnect timeout to no timeout
this.reconnectTimeout = Listener.NO_TIMEOUT;
}
// Set reconnect timeout
this.reconnectTimeout = setTimeout(function() {
// Set reconnect timeout to no timeout
self.reconnectTimeout = Listener.NO_TIMEOUT;
// Connect
self.connect();
}, Math.min(Listener.MAXIMUM_RETRY_DELAY_MILLISECONDS, this.retryDelay));
// Update retry delay
this.retryDelay *= Listener.RETRY_DELAY_SCALING_FACTOR;
// Trigger connection close event
$(this).trigger(Listener.CONNECTION_CLOSE_EVENT, (this.connected === false) ? Listener.NO_CONNECTION_CLOSE_TYPE : Listener.DISCONNECTED_CLOSE_TYPE);
// Set connection to no connection
this.connection = Listener.NO_CONNECTION;
// Clear connected
this.connected = false;
}
// Return
return;
}
// Connection open event
$(this.connection).on("open", function() {
// Check if not ignoring response
if(ignoreResponse === false) {
// Log message
Log.logMessage(Language.getDefaultTranslation('Successfully connected to the listener.'));
// Reset retry delay
self.retryDelay = Listener.INITIAL_RETRY_DELAY_MILLISECONDS;
// Trigger connection open event
$(self).trigger(Listener.CONNECTION_OPEN_EVENT);
// Set connected
self.connected = true;
}
// Connection error event
}).on("error", function() {
// Check if not ignoring response
if(ignoreResponse === false) {
// Close connection
self.connection.close();
}
// Connection close event
}).on("close", function() {
// Check if not ignoring response
if(ignoreResponse === false) {
// Turn off settings change listener index event
$(self).off(Listener.SETTINGS_CHANGE_EVENT + ".listener" + index.toFixed());
// Turn off window offline listener index event
$(window).off("offline" + ".listener" + index.toFixed());
// Check if connected
if(self.connected === true) {
// Log message
Log.logMessage(Language.getDefaultTranslation('Disconnected from the listener.'));
}
// Otherwise
else {
// Log message
Log.logMessage(Language.getDefaultTranslation('Connecting to the listener failed.'));
}
// Check if reconnect timeout exists
if(self.reconnectTimeout !== Listener.NO_TIMEOUT) {
// Clear reconnect timeout
clearTimeout(self.reconnectTimeout);
// Set reconnect timeout to no timeout
self.reconnectTimeout = Listener.NO_TIMEOUT;
}
// Set reconnect timeout
self.reconnectTimeout = setTimeout(function() {
// Set reconnect timeout to no timeout
self.reconnectTimeout = Listener.NO_TIMEOUT;
// Connect
self.connect();
}, Math.min(Listener.MAXIMUM_RETRY_DELAY_MILLISECONDS, self.retryDelay));
// Update retry delay
self.retryDelay *= Listener.RETRY_DELAY_SCALING_FACTOR;
// Trigger connection close event
$(self).trigger(Listener.CONNECTION_CLOSE_EVENT, (self.connected === false) ? Listener.NO_CONNECTION_CLOSE_TYPE : Listener.DISCONNECTED_CLOSE_TYPE);
// Set connection to no connection
self.connection = Listener.NO_CONNECTION;
// Clear connected
self.connected = false;
}
// Connection message event
}).on("message", function(event) {
// Try
try {
// Parse message as JSON
var message = JSON.parse(event["originalEvent"]["data"]);
}
// Catch errors
catch(error) {
// Return
return;
}
// Check if message is invalid
if(Object.isObject(message) === false)
// Return
return;
// Check if message is a response
if("Index" in message === true && typeof message["Index"] === "number")
// Trigger response receive event
$(self).trigger(Listener.RESPONSE_RECEIVE_EVENT, message);
// Otherwise check if message is an interaction
else if("Interaction" in message === true && typeof message["Interaction"] === "number" && "URL" in message === true && "API" in message === true && "Type" in message === true && "Data" in message === true) {
// Check if not ignoring response
if(ignoreResponse === false) {
// Check if message's contents are valid
if(typeof message["URL"] === "string" && typeof message["API"] === "string" && typeof message["Type"] === "string" && typeof message["Data"] === "string") {
// Try
try {
// Decode message's data
var data = Base64.toUint8Array(message["Data"]);
}
// Catch errors
catch(error) {
// Send error
self.send({
// Interaction
"Interaction": message["Interaction"],
// Type
"Type": "text/plain",
// Data
"Data": Base64.fromUint8Array((new TextEncoder()).encode("")),
// Status
"Status": Common.HTTP_UNSUPPORTED_MEDIA_TYPE_STATUS
});
// Return
return;
}
// Create interaction from message
var interaction = new Interaction(message["Interaction"], message["URL"], message["API"], message["Type"], data, self);
// Status receive listener cancel interaction event
$(self).on(Listener.STATUS_RECEIVE_EVENT + ".listenerCancel" + message["Interaction"].toFixed(), function(event, status) {
// Check if status is for the interaction
if(status["Interaction"] === message["Interaction"]) {
// Turn off status receive listener cancel interaction event
$(self).off(Listener.STATUS_RECEIVE_EVENT + ".listenerCancel" + message["Interaction"].toFixed());
// Turn off settings change listener cancel interaction event
$(self).off(Listener.SETTINGS_CHANGE_EVENT + ".listenerCancel" + message["Interaction"].toFixed());
// Check if status contains an error, doesn't contain a status, or doesn't contain a succeeded status
if(Object.isObject(status) === false || "Error" in status === true || "Status" in status === false || status["Status"] !== Listener.STATUS_SUCCEEDED_VALUE) {
// Set that interaction is canceled
interaction.setCanceled();
}
}
// Settings change listener cancel interaction event
}).one(Listener.SETTINGS_CHANGE_EVENT + ".listenerCancel" + message["Interaction"].toFixed(), function() {
// Turn off status receive listener cancel interaction event
$(self).off(Listener.STATUS_RECEIVE_EVENT + ".listenerCancel" + message["Interaction"].toFixed());
// Set that interaction is canceled
interaction.setCanceled();
});
// Trigger request receive event
$(self).trigger(Listener.REQUEST_RECEIVE_EVENT, interaction);
}
// Otherwise
else {
// Send error
self.send({
// Interaction
"Interaction": message["Interaction"],
// Type
"Type": "text/plain",
// Data
"Data": Base64.fromUint8Array((new TextEncoder()).encode("")),
// Status
"Status": Common.HTTP_UNSUPPORTED_MEDIA_TYPE_STATUS
});
}
}
// Otherwise
else {
// Send error
self.send({
// Interaction
"Interaction": message["Interaction"],
// Type
"Type": "text/plain",
// Data
"Data": Base64.fromUint8Array((new TextEncoder()).encode("")),
// Status
"Status": Common.HTTP_NOT_FOUND_STATUS
});
}
}
// Otherwise check if message is a status
else if("Interaction" in message === true && typeof message["Interaction"] === "number")
// Trigger status receive event
$(self).trigger(Listener.STATUS_RECEIVE_EVENT, message);
});
}
// Is connected
isConnected() {
// Return if connection exists and is open
return this.connection !== Listener.NO_CONNECTION && this.connection["readyState"] === Listener.WEBSOCKET_OPEN_STATE;
}
// Send
send(data) {
// Send data to connection
this.connection.send(JSON.stringify(data));
}
// Send request
sendRequest(request) {
// Get current request index
var index = this.requestIndex++;
// Check if current request index is at the max safe integer
if(index === Number.MAX_SAFE_INTEGER)
// Reset request index
this.requestIndex = 0;
// Set index in request
request["Index"] = index;
// Get current ignore response index
var ignoreResponseIndex = this.ignoreResponseIndex++;
// Check if current ignore resposne index is at the max safe integer
if(ignoreResponseIndex === Number.MAX_SAFE_INTEGER) {
// Reset ignore response index
this.ignoreResponseIndex = 0;
}
// Initialize ignore response
var ignoreResponse = false;
// Settings change listener request ignore response index event
$(this).on(Listener.SETTINGS_CHANGE_EVENT + ".listenerRequest" + ignoreResponseIndex.toFixed(), function() {
// Set ignore response
ignoreResponse = true;
});
// Set self
var self = this;
// Return promise
return new Promise(function(resolve, reject) {
// Turn off settings change listener request ignore response index event
$(self).off(Listener.SETTINGS_CHANGE_EVENT + ".listenerRequest" + ignoreResponseIndex.toFixed());
// Check if ignoring response
if(ignoreResponse === true) {
// Reject error
reject(Listener.CONNECTION_ERROR);
}
// Otherwise
else {
// Turn off events
var turnOffEvents = function() {
// Turn off response receive listener request index event
$(self).off(Listener.RESPONSE_RECEIVE_EVENT + ".listenerRequest" + index.toFixed());
// Turn off connection open listener request index event
$(self).off(Listener.CONNECTION_OPEN_EVENT + ".listenerRequest" + index.toFixed());
// Turn off connection close listener request index event
$(self).off(Listener.CONNECTION_CLOSE_EVENT + ".listenerRequest" + index.toFixed());
// Turn off settings change listener request ignore response index event
$(self).off(Listener.SETTINGS_CHANGE_EVENT + ".listenerRequest" + ignoreResponseIndex.toFixed());
};
// Create timeout
var timeout = setTimeout(function() {
// Turn off events
turnOffEvents();
// Reject error
reject(Listener.TIMEOUT_ERROR);
}, Listener.REQUEST_TIMEOUT_SECONDS * Common.MILLISECONDS_IN_A_SECOND);
// Initialize close count
var closeCount = 0;
// Response receive listener request index event
$(self).on(Listener.RESPONSE_RECEIVE_EVENT + ".listenerRequest" + index.toFixed(), function(event, response) {
// Check if response contains the index
if(response["Index"] === index) {
// Stop timeout
clearTimeout(timeout);
// Turn off events
turnOffEvents();
// Check if response contains an error or doesn't contain a response
if(Object.isObject(response) === false || "Error" in response === true || "Response" in response === false)
// Reject error
reject(Listener.INVALID_RESPONSE_ERROR);
// Otherwise
else
// Resolve response
resolve(response["Response"]);
}
// Connection open listener request index event
}).on(Listener.CONNECTION_OPEN_EVENT + ".listenerRequest" + index.toFixed(), function() {
// Send request
self.send(request);
// Connection close listener request index event
}).on(Listener.CONNECTION_CLOSE_EVENT + ".listenerRequest" + index.toFixed(), function() {
// Check if close count is at the fail threshold
if(++closeCount >= Listener.CONNECTION_CLOSE_FAIL_THRESHOLD) {
// Stop timeout
clearTimeout(timeout);
// Turn off events
turnOffEvents();
// Reject error
reject(Listener.CONNECTION_ERROR);
}
// Settings change listener request ignore response index event
}).on(Listener.SETTINGS_CHANGE_EVENT + ".listenerRequest" + ignoreResponseIndex.toFixed(), function() {
// Stop timeout
clearTimeout(timeout);
// Turn off events
turnOffEvents();
// Reject error
reject(Listener.CONNECTION_ERROR);
});
// Check if connected
if(self.isConnected() === true) {
// Try
try {
// Send request
self.send(request);
}
// Catch errors
catch(error) {
}
}
}
});
}
// Send response
sendResponse(response) {
// Get interaction index
var index = response["Interaction"];
// Get current ignore response index
var ignoreResponseIndex = this.ignoreResponseIndex++;
// Check if current ignore resposne index is at the max safe integer
if(ignoreResponseIndex === Number.MAX_SAFE_INTEGER) {
// Reset ignore response index
this.ignoreResponseIndex = 0;
}
// Initialize ignore response
var ignoreResponse = false;
// Settings change listener response ignore response index event
$(this).on(Listener.SETTINGS_CHANGE_EVENT + ".listenerResponse" + ignoreResponseIndex.toFixed(), function() {
// Set ignore response
ignoreResponse = true;
});
// Set self
var self = this;
// Return promise
return new Promise(function(resolve, reject) {
// Turn off settings change listener response ignore response index event
$(self).off(Listener.SETTINGS_CHANGE_EVENT + ".listenerResponse" + ignoreResponseIndex.toFixed());
// Check if ignoring response
if(ignoreResponse === true) {
// Reject error
reject(Listener.CONNECTION_ERROR);
}
// Otherwise
else {
// Turn off events
var turnOffEvents = function() {
// Turn off status receive listener response index event
$(self).off(Listener.STATUS_RECEIVE_EVENT + ".listenerResponse" + index.toFixed());
// Turn off connection open listener response index event
$(self).off(Listener.CONNECTION_OPEN_EVENT + ".listenerResponse" + index.toFixed());
// Turn off connection close listener response index event
$(self).off(Listener.CONNECTION_CLOSE_EVENT + ".listenerResponse" + index.toFixed());
// Turn off settings change listener response ignore response index event
$(self).off(Listener.SETTINGS_CHANGE_EVENT + ".listenerResponse" + ignoreResponseIndex.toFixed());
};
// Create timeout
var timeout = setTimeout(function() {
// Turn off events
turnOffEvents();
// Reject error
reject(Listener.TIMEOUT_ERROR);
}, Listener.RESPONSE_TIMEOUT_SECONDS * Common.MILLISECONDS_IN_A_SECOND);
// Initialize close count
var closeCount = 0;
// Status receive listener response index event
$(self).on(Listener.STATUS_RECEIVE_EVENT + ".listenerResponse" + index.toFixed(), function(event, status) {
// Check if status contains the index
if(status["Interaction"] === index) {
// Stop timeout
clearTimeout(timeout);
// Turn off events
turnOffEvents();
// Check if status contains an error or doesn't contain a status
if(Object.isObject(status) === false || "Error" in status === true || "Status" in status === false)
// Reject error
reject(Listener.INVALID_STATUS_ERROR);
// Otherwise
else
// Resolve status
resolve(status["Status"]);
}
// Connection open listener response index event
}).on(Listener.CONNECTION_OPEN_EVENT + ".listenerResponse" + index.toFixed(), function() {
// Send response
self.send(response);
// Connection close listener response index event
}).on(Listener.CONNECTION_CLOSE_EVENT + ".listenerResponse" + index.toFixed(), function() {
// Check if close count is at the fail threshold
if(++closeCount >= Listener.CONNECTION_CLOSE_FAIL_THRESHOLD) {
// Stop timeout
clearTimeout(timeout);
// Turn off events
turnOffEvents();
// Reject error
reject(Listener.CONNECTION_ERROR);
}
// Settings change listener response ignore response index event
}).on(Listener.SETTINGS_CHANGE_EVENT + ".listenerResponse" + ignoreResponseIndex.toFixed(), function() {
// Stop timeout
clearTimeout(timeout);
// Turn off events
turnOffEvents();
// Reject error
reject(Listener.CONNECTION_ERROR);
});
// Check if connected
if(self.isConnected() === true) {
// Try
try {
// Send response
self.send(response);
}
// Catch errors
catch(error) {
}
}
}
});
}
// Initial retry delay milliseconds
static get INITIAL_RETRY_DELAY_MILLISECONDS() {
// Return initial retry delay milliseconds
return 250;
}
// Maximum retry delay milliseconds
static get MAXIMUM_RETRY_DELAY_MILLISECONDS() {
// Return maximum retry delay milliseconds
return 10 * Common.MILLISECONDS_IN_A_SECOND;
}
// Retry delay scaling factor
static get RETRY_DELAY_SCALING_FACTOR() {
// Return retry delay scaling factor
return 2;
}
// Response receive event
static get RESPONSE_RECEIVE_EVENT() {
// Return response receive event
return "ListenerResponseReceiveEvent";
}
// Status receive event
static get STATUS_RECEIVE_EVENT() {
// Return status receive event
return "ListenerStatusReceiveEvent";
}
// WebSocket open state
static get WEBSOCKET_OPEN_STATE() {
// Return WebSocket open state
return 1;
}
// Request timeout seconds
static get REQUEST_TIMEOUT_SECONDS() {
// Return request timeout seconds
return 2 * Common.SECONDS_IN_A_MINUTE;
}
// Response timeout seconds
static get RESPONSE_TIMEOUT_SECONDS() {
// Return response timeout seconds
return Listener.REQUEST_TIMEOUT_SECONDS;
}
// No timeout
static get NO_TIMEOUT() {
// Return no timeout
return null;
}
// Connection close fail threshold
static get CONNECTION_CLOSE_FAIL_THRESHOLD() {
// Return connection close fail threshold
return 3;
}
// Create URL request name
static get CREATE_URL_REQUEST_NAME() {
// Return create URL request name
return "Create URL";
}
// Change URL request name
static get CHANGE_URL_REQUEST_NAME() {
// Return change URL request name
return "Change URL";
}
// Own URL request name
static get OWN_URL_REQUEST_NAME() {
// Return own URL request name
return "Own URL";
}
// Delete URL request name
static get DELETE_URL_REQUEST_NAME() {
// Return delete URL request name
return "Delete URL";
}
// Connection error
static get CONNECTION_ERROR() {
// Return connection error
return 0;
}
// Timeout error
static get TIMEOUT_ERROR() {
// Return timeout error
return Listener.CONNECTION_ERROR + 1;
}
// Invalid response error
static get INVALID_RESPONSE_ERROR() {
// Return invalid response error
return Listener.TIMEOUT_ERROR + 1;
}
// Invalid status error
static get INVALID_STATUS_ERROR() {
// Return invalid status error
return Listener.INVALID_RESPONSE_ERROR + 1;
}
// Status succeeded value
static get STATUS_SUCCEEDED_VALUE() {
// Return status succeeded value
return "Succeeded";
}
// No connection
static get NO_CONNECTION() {
// Return no connection
return null;
}
// Settings use custom listener name
static get SETTINGS_USE_CUSTOM_LISTENER_NAME() {
// Return settings use custom listener name
return "Use Custom Listener";
}
// Settings use custom listener default value
static get SETTINGS_USE_CUSTOM_LISTENER_DEFAULT_VALUE() {
// Return settings use custom listener default value
return false;
}
// Settings custom listener address name
static get SETTINGS_CUSTOM_LISTENER_ADDRESS_NAME() {
// Return settings custom listener address name
return "Custom Listener Address";
}
// Settings custom listener address default value
static get SETTINGS_CUSTOM_LISTENER_ADDRESS_DEFAULT_VALUE() {
// Return settings custom listener address default value
return "";
}
// Default listener address
static get DEFAULT_LISTENER_ADDRESS() {
// Set server
var server = (Common.isExtension() === true || location["protocol"] === Common.FILE_PROTOCOL) ? new URL(HTTPS_SERVER_ADDRESS) : location;
// Return default listener address
return ((server["protocol"] === Common.HTTPS_PROTOCOL) ? Common.WEBSOCKET_SECURE_PROTOCOL : Common.WEBSOCKET_PROTOCOL) + "//" + server["hostname"] + "/listen";
}
}
// Main function
// Set global object's listener
globalThis["Listener"] = Listener;