@polkadot/api-provider
Version:
Transport providers for the API
291 lines (245 loc) • 8.64 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
require("core-js/modules/web.dom.iterable");
var _objectSpread2 = _interopRequireDefault(require("@babel/runtime/helpers/objectSpread"));
require("./polyfill");
var _eventemitter = _interopRequireDefault(require("eventemitter3"));
var _assert = _interopRequireDefault(require("@polkadot/util/assert"));
var _null = _interopRequireDefault(require("@polkadot/util/is/null"));
var _undefined = _interopRequireDefault(require("@polkadot/util/is/undefined"));
var _logger = _interopRequireDefault(require("@polkadot/util/logger"));
var _json = _interopRequireDefault(require("../coder/json"));
// Copyright 2017-2018 @polkadot/api-provider authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
/**
* @name WsProvider
* @summary The WebSocket Provider allows sending requests using WebSocket to a WebSocket RPC server TCP port.
* @description Unlike the [[HttpProvider]], it does support subscriptions and allows
* listening to events such as new blocks or balance changes.
* @example
* <BR>
*
* ```javascript
* import Api from '@polkadot/api';
* import WsProvider from '@polkadot/api-provider/ws';
*
* const provider = new WsProvider('ws://127.0.0.1:9944');
* const api = new Api(provider);
* ```
*
* @see [[HttpProvider]]
*/
class WsProvider extends _eventemitter.default.EventEmitter {
/**
* @param {string} endpoint The endpoint url. Usually `ws://ip:9944` or `wss://ip:9944`
* @param {boolean} autoConnect Whether to connect automatically or not.
*/
constructor(endpoint, autoConnect = true) {
super();
this.autoConnect = void 0;
this.coder = void 0;
this.endpoint = void 0;
this.handlers = void 0;
this._isConnected = void 0;
this.l = void 0;
this.queued = void 0;
this.subscriptions = void 0;
this.websocket = void 0;
this.onSocketClose = () => {
this.l.debug(() => ['disconnected from', this.endpoint]);
this._isConnected = false;
this.emit('disconnected');
if (this.autoConnect) {
setTimeout(() => {
this.connect();
}, 1000);
}
};
this.onSocketError = error => {
this.l.error(error);
};
this.onSocketMessage = message => {
this.l.debug(() => ['received', message.data]);
const response = JSON.parse(message.data);
return (0, _undefined.default)(response.method) ? this.onSocketMessageResult(response) : this.onSocketMessageSubscribe(response);
};
this.onSocketMessageResult = response => {
this.l.debug(() => ['handling: response =', response, 'id =', response.id]);
const handler = this.handlers[response.id];
if (!handler) {
this.l.error(`Unable to find handler for id=${response.id}`);
return;
}
try {
const method = handler.method,
params = handler.params,
subscription = handler.subscription;
const result = this.coder.decodeResponse(response);
if (subscription) {
this.subscriptions[`${subscription.type}::${result}`] = (0, _objectSpread2.default)({}, subscription, {
method,
params
});
}
handler.callback(null, result);
} catch (error) {
handler.callback(error, undefined);
}
delete this.handlers[response.id];
};
this.onSocketMessageSubscribe = response => {
const subscription = `${response.method}::${response.params.subscription}`;
this.l.debug(() => ['handling: response =', response, 'subscription =', subscription]);
const handler = this.subscriptions[subscription];
if (!handler) {
this.l.error(`Unable to find handler for subscription=${subscription}`);
return;
}
try {
const result = this.coder.decodeResponse(response);
handler.callback(null, result);
} catch (error) {
handler.callback(error, undefined);
}
};
this.onSocketOpen = () => {
(0, _assert.default)(!(0, _null.default)(this.websocket), 'WebSocket cannot be null in onOpen');
this.l.debug(() => ['connected to', this.endpoint]);
this._isConnected = true;
this.emit('connected');
Object.keys(this.queued).forEach(id => {
try {
// @ts-ignore checked above
this.websocket.send(this.queued[id]);
delete this.queued[id];
} catch (error) {
this.l.error(error);
}
});
return true;
};
(0, _assert.default)(/^(wss|ws):\/\//.test(endpoint), `Endpoint should start with 'ws://', received '${endpoint}'`);
this.autoConnect = autoConnect;
this.coder = (0, _json.default)();
this.endpoint = endpoint;
this._isConnected = false;
this.handlers = {};
this.l = (0, _logger.default)('api-ws');
this.queued = {};
this.subscriptions = {};
this.websocket = null;
if (autoConnect) {
this.connect();
}
}
/**
* @summary Manually connect
* @description The [[WsProvider]] connects automatically by default, however if you decided otherwise, you may
* connect manually using this method.
*/
connect() {
try {
this.websocket = new WebSocket(this.endpoint);
this.websocket.onclose = this.onSocketClose;
this.websocket.onerror = this.onSocketError;
this.websocket.onmessage = this.onSocketMessage;
this.websocket.onopen = this.onSocketOpen;
} catch (error) {
this.l.error(error);
}
}
/**
* @summary Whether the node is connected or not.
* @return {boolean} true if connected
*/
isConnected() {
return this._isConnected;
}
/**
* @summary Listens on events after having subscribed using the [[subscribe]] function.
* @param {ProviderInterface$Emitted} type Event
* @param {ProviderInterface$EmitCb} sub Callback
* @return {this} [description]
*/
on(type, sub) {
return super.on(type, sub);
}
/**
* @summary Send JSON data using WebSockets to configured HTTP Endpoint or queue.
*/
async send(method, params, subscription) {
return new Promise((resolve, reject) => {
try {
const json = this.coder.encodeJson(method, params);
const id = this.coder.getId();
const callback = (error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
};
this.l.debug(() => ['calling', method, params, json, !!subscription]);
this.handlers[id] = {
callback,
method,
params,
subscription
};
if (this.isConnected() && !(0, _null.default)(this.websocket)) {
this.websocket.send(json);
} else {
this.queued[id] = json;
}
} catch (error) {
reject(error);
}
});
}
/**
* @name subscribe
* @summary Allows subscribing to a specific event.
* @param {string} type Subscription type
* @param {string} method Subscription method
* @param {Array<any>} params Parameters
* @param {ProviderInterface$Callback} callback Callback
* @return {Promise<number>} Promise resolving to the dd of the subscription you can use with [[unsubscribe]].
*
* @example
* <BR>
*
* ```javascript
* const provider = new WsProvider('ws://127.0.0.1:9944');
* const api = new Api(provider);
*
* api.state.storage([[storage.balances.freeBalance, <Address>]], (_, values) => {
* console.log(values)
* }).then((subscriptionId) => {
* console.log('balance changes subscription id: ', subscriptionId)
* })
* ```
*/
async subscribe(type, method, params, callback) {
const id = await this.send(method, params, {
callback,
type
});
return id;
}
/**
* @summary Allows unsubscribing to subscriptions made with [[subscribe]].
*/
async unsubscribe(type, method, id) {
const subscription = `${type}::${id}`;
(0, _assert.default)(!(0, _undefined.default)(this.subscriptions[subscription]), `Unable to find active subscription=${subscription}`);
delete this.subscriptions[subscription];
const result = await this.send(method, [id]);
return result;
}
}
exports.default = WsProvider;