src/api/communication/websocket.js
/**
*
*/
import autobahn from 'autobahn';
import debug from 'debug';
import {settings} from './index';
const log = debug('its-sdk:WebSocket');
const error = debug('its-sdk:WebSocket');
log.log = console.log.bind(console);
/**
* Keep hold of the currently open autobahn connection.
*
* @type {Promise.<autobahn.Connection>}
*/
let bundesautobahn;
/**
* Allow the `autobahn.Connection` to challenge the provided authentication.
*
* @param {autobahn.Session} session - The session of the current
* {@link autobahn.Connection}.
* @param {string} method - The authentication method it tries to use.
*
* @throws {Error} - When the given `method` is unknown to the SDK.
*/
function handleWebsocketAuthorisationChallenge(session, method) {
switch (method) {
case 'ticket':
return settings.authorizationToken;
default:
throw new Error('The websocket server tried to use the unknown ' +
`authentication challenge: "${method}"`);
}
}
/**
* Set {@link bundesautobahn} to a new Promise which resolves into a
* `autobahn.Connection` object when a connection was successfully established.
*
* @returns {Promise.<autobahn.Connection>} - A promise which resolves when the
* connection was successfully
* created and opened.
*/
function establishNewBundesbahn() {
bundesautobahn = new Promise((resolve, reject) => {
const bahn = new autobahn.Connection({
url: settings.wsUrl,
realm: 'default',
// Of course we want to use es6 promises if they are availbile.
// But, the backend sometimes spits out progress. For that we need
// a When.JS promise..
use_es6_promises: false, // eslint-disable-line camelcase
// The following options are required in order to authorise the
// connection.
authmethods: ['ticket'],
authid: 'oauth2',
details: {
ticket: settings.authorizationToken
},
onchallenge: handleWebsocketAuthorisationChallenge
});
// `autobahn.Connection` calls its `onclose` method, if it exists, when it
// was not able to open a connection.
bahn.onclose = (/* reason, details */) => {
// When the connection faild to open a reason is given with some details.
// Sadly these are very undescriptive. Therefore hint/warn the developer
// about potential erroneous settings or to contact us.
const message = 'The connection is erroneous; check if all required ' +
'settings have been injected using the ' +
'`updateSettings()` function. If the problem persists ' +
'please post a issue on our GitHub repository.';
reject(message);
};
// Connection got established; lets us it.
bahn.onopen = () => {
log('Successfully established a websocket connection.');
// Remove the `onclose` handler as it is no longer of interest to us.
delete bahn.onclose;
resolve(bahn);
};
bahn.open();
});
// Return the promise to make it this function chainable. In case the
// `bundesautobahn` is rejected; remove the reference so we can use simple
// falsy checks to detemine if there is a connection.
return bundesautobahn.catch(reason => {
bundesautobahn = null;
return Promise.reject(reason);
});
}
/**
* Open a new websocket connection.
*
* There there currently is a open connection, close it and open a new
* connection.
*
* @returns {Promise.<string>} - A resolved promise which resolves when the
* connection was successfully created and opened.
*/
export function openWebsocketConnection() {
return closeWebsocketConnection()
.then(() => establishNewBundesbahn())
// `bundesautobahn` actually resolved with the `autobahn.Connection`
// object. This is only meant for internal usage and therefore should not
// be exposed to the users of the SDK.
.then(() => 'Successfully established a websocket connection.');
}
/**
* Get the current websocket connection, or open a new one.
*
* If there is no current connection, open one and return that in stead.
*
* @returns {Promise.<autobahn.Connection>} - The current websocket connection.
*/
function getWebsocketConnection() {
if (!bundesautobahn) {
return establishNewBundesbahn();
}
return bundesautobahn;
}
/**
* Close the current websocket connection.
*
* @returns {Promise.<string>} - A promise which will resolve as soon as the
* connection was successfully closed.
*/
export function closeWebsocketConnection() {
if (!bundesautobahn) {
return Promise.resolve('There is no websocket connection to close.');
}
return bundesautobahn
.then(bahn => {
try {
bahn.close();
bundesautobahn = null;
const message = 'The websocket connection has been closed successfully.';
log(message);
return message;
} catch (reason) {
// `autobahn.Connection.close()` throws a string when the connection is
// already closed. The connection is not exposed and therefore cannot be
// closed by anyone using the SDK. Regardless, when it happens just
// return a resolved promise.
bundesautobahn = null;
const message = 'The websocket connection has already been closed.';
error(message);
return message;
}
});
}
/**
* Make a rpc call to the ITSLanguage websocket server.
*
* This method will try to establish a websocket connection if there isn't one
* already.
*
* @param {string} rpc - The RPC to make. This be prepended by `nl.itslanguage`
* as the websocket server only handles websocket calls
* when the RPC starts with that prefix.
* @param {Object} [options] - Destructured object with options to pass to the websocket server.
* @param {Array} [options.args] - An array with arguments to pass to the RPC.
* @param {Object} [options.kwargs] - An object (dictionary) with arguments to pass to the RPC.
* @param {Object} [options.options] - The options to pass to the RPC.
* @param {Function} [options.progressCb] - Optional callback to receive progressed results.
*
* @returns {Promise.<*>} - The response of the websocket call.
*/
export function makeWebsocketCall(rpc, {args, kwargs, options, progressCb} = {}) {
let mergedOptions = options;
if (progressCb) {
mergedOptions = {
...options,
receive_progress: true // eslint-disable-line camelcase
};
}
return getWebsocketConnection()
.then(connection =>
connection.session.call(`nl.itslanguage.${rpc}`, args, kwargs, mergedOptions)
.progress(progressCb)
)
.catch(result => {
const {error: wssError, kwargs: wssKwargs, args: wssArgs} = result;
// Log the error to stderr
error(result);
// Return a slightly simplistic version of the error that occurred
return Promise.reject({
error: wssError,
...wssKwargs,
args: [...wssArgs]
});
});
}