Skip to content

Commit

Permalink
websocket rework
Browse files Browse the repository at this point in the history
  • Loading branch information
panleone committed May 16, 2024
1 parent df698bd commit 836071f
Show file tree
Hide file tree
Showing 2 changed files with 89 additions and 58 deletions.
144 changes: 88 additions & 56 deletions scripts/network.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,24 +103,29 @@ export class ExplorerNetwork extends Network {
/**
* @param {string} wsUrl - Url pointing to the blockbook explorer
*/
constructor(wsUrl) {
constructor() {
super();
this.cachedResults = [];
this.subscriptions = [];
this.ID = 0;
}
async createWebSocketConnection(wsUrl) {
// Make sure the old connection is closed before opening a new one
if (this.ws) {
await this.reset();
}
// ensure backward compatibility
if (wsUrl.startsWith('http')) {
wsUrl = wsUrl.replace('http', 'ws');
}
if (!wsUrl.endsWith('/websocket')) {
wsUrl += '/websocket';
}
/**
* @type{string}
* @public
*/
this.wsUrl = wsUrl;
this.closed = false;
this.cachedResults = [];
this.subscriptions = [];
this.ID = 0;
this.openWebSocketConnection(wsUrl);
await this.initWebSocketConnection();
}
openWebSocketConnection(wsUrl) {
this.ws = new WebSocket(wsUrl);
this.ws.onopen = function (e) {
debugLog(DebugTopics.NET, 'websocket connected', e);
Expand All @@ -132,10 +137,16 @@ export class ExplorerNetwork extends Network {
DebugTopics.NET,
'websocket unexpected close, trying to reconnect'
);
// Connection closed somehow, try to reconnect
setNextExplorer();
}
};
this.ws.onmessage = function (e) {
// Return early if the websocket is not open
// In this way we avoid the case in which a closing websocket connection put values in the cache
if (this.readyState !== WebSocket.OPEN) {
return;
}
const resp = JSON.parse(e.data);
// Is this a subscription?
const f = _network.subscriptions[resp.id];
Expand All @@ -146,43 +157,85 @@ export class ExplorerNetwork extends Network {
_network.cachedResults[resp.id] = resp.data;
};
}
async initWebSocketConnection() {
await this.awaitWebSocketStatus(
WebSocket.OPEN,
'Cannot connect to websocket!'
);
await this.subscribeNewBlock();
}
get strUrl() {
return this.wsUrl.replace('ws', 'http').replace('/websocket', '');
}

close() {
this.ws.close();
async reset() {
if (this.ws.readyState == WebSocket.OPEN) {
this.ws.close();
}
// Make sure websocket got closed before emptying arrays
await this.awaitWebSocketStatus(
WebSocket.CLOSED,
'Websocket is not getting closed'
);
// At this point messages of old websocket will not be received anymore
// and, it is safe to reset cachedResults and subscriptions
this.cachedResults = [];
this.subscriptions = [];
this.closed = true;
}
async init() {

async awaitWebSocketStatus(status, errMessage) {
for (let i = 0; i < 100; i++) {
if (this.ws.readyState === WebSocket.OPEN) {
if (this.ws.readyState === status) {
break;
}
await sleep(100);
}
if (this.ws.readyState !== WebSocket.OPEN) {
throw new Error('Cannot connect to websocket!');
if (this.ws.readyState !== status) {
throw new Error(errMessage);
}
this.subscribeNewBlock();
}

send(method, params) {
if (this.closed) {
throw new Error('Trying to send with a closed explorer');
}
async subscribe(method, params, callback) {
await this.awaitWebSocketStatus(
WebSocket.OPEN,
'Cannot connect to websocket!'
);
const id = this.ID.toString();
this.subscriptions[id] = callback;
const req = {
id,
method,
params,
};
this.ID++;
this.ws.send(JSON.stringify(req));
this.ID++;
return id;
}
async subscribeNewBlock() {
await this.subscribe('subscribeNewBlock', {}, function (result) {
if (result['height'] !== undefined) {
getEventEmitter().emit('new-block', result['height']);
}
});
}

async send(method, params) {
// It might happen that connection just got closed, and we are still connecting to the new one.
// So make sure that before sending anything we have an open connection
await this.awaitWebSocketStatus(
WebSocket.OPEN,
'Cannot connect to websocket!'
);
const id = this.ID.toString();
const req = {
id,
method,
params,
};
this.ID++;
this.ws.send(JSON.stringify(req));
return [id, this.ws];
}
async sendAndWaitForAnswer(method, params) {
let attempt = 1;
const maxAttempts = 10;
Expand All @@ -191,11 +244,8 @@ export class ExplorerNetwork extends Network {
const frequency = 100;
while (attempt <= maxAttempts) {
let receivedInvalidAnswer = false;
const id = this.send(method, params);
const [id, ws_that_sent] = await this.send(method, params);
for (let i = 0; i < Math.floor(maxAwaitTime / frequency); i++) {
if (this.closed) {
break;
}
const res = this.cachedResults[id];
if (res !== undefined) {
delete this.cachedResults[id];
Expand All @@ -207,6 +257,10 @@ export class ExplorerNetwork extends Network {
return res;
}
await sleep(frequency);
// If connection got closed while sending do not wait until timeout
if (ws_that_sent !== this.ws) {
break;
}
}
debugWarn(
DebugTopics.NET,
Expand All @@ -220,28 +274,7 @@ export class ExplorerNetwork extends Network {
);
attempt += 1;
}
if (!this.closed) {
throw new Error('Failed to communicate with the explorer');
}
}
subscribe(method, params, callback) {
const id = this.ID.toString();
this.subscriptions[id] = callback;
const req = {
id,
method,
params,
};
this.ws.send(JSON.stringify(req));
this.ID++;
return id;
}
subscribeNewBlock() {
this.subscribe('subscribeNewBlock', {}, function (result) {
if (result['height'] !== undefined) {
getEventEmitter().emit('new-block', result['height']);
}
});
throw new Error('Failed to communicate with the explorer');
}
async getAccountInfo(descriptor, page, pageSize, from, details = 'txs') {
const params = {
Expand Down Expand Up @@ -388,23 +421,22 @@ export class ExplorerNetwork extends Network {
}
}

let _network = null;
let _network = new ExplorerNetwork();

/**
* Sets the network in use by MPW.
* @param {ExplorerNetwork} network - network to use
* @param {String} wsUrl - websocket url
*/
export async function setNetwork(network) {
debugLog(DebugTopics.NET, 'Connecting to new explorer', network.wsUrl);
_network?.close();
_network = network;
await _network.init();
export async function setNetwork(wsUrl) {
debugLog(DebugTopics.NET, 'Connecting to new explorer', wsUrl);
await _network.createWebSocketConnection(wsUrl);
}

/**
* Gets the network in use by MPW.
* @returns {ExplorerNetwork?} Returns the network in use, may be null if MPW hasn't properly loaded yet.
*/
export function getNetwork() {
return _network;
// Return null if websocket hasn't been loaded yet
return _network.ws ? _network : null;
}
3 changes: 1 addition & 2 deletions scripts/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -308,8 +308,7 @@ async function setExplorer(explorer, fSilent = false) {
cExplorer = explorer;

// Enable networking + notify if allowed
const network = new ExplorerNetwork(cExplorer.url);
await setNetwork(network);
await setNetwork(cExplorer.url);

// Update the selector UI
doms.domExplorerSelect.value = cExplorer.url;
Expand Down

0 comments on commit 836071f

Please sign in to comment.