123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467 |
- "use strict";
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
- if (k2 === undefined) k2 = k;
- Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
- }) : (function(o, m, k, k2) {
- if (k2 === undefined) k2 = k;
- o[k2] = m[k];
- }));
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
- Object.defineProperty(o, "default", { enumerable: true, value: v });
- }) : function(o, v) {
- o["default"] = v;
- });
- var __importStar = (this && this.__importStar) || function (mod) {
- if (mod && mod.__esModule) return mod;
- var result = {};
- if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
- __setModuleDefault(result, mod);
- return result;
- };
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
- return new (P || (P = Promise))(function (resolve, reject) {
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
- step((generator = generator.apply(thisArg, _arguments || [])).next());
- });
- };
- var __importDefault = (this && this.__importDefault) || function (mod) {
- return (mod && mod.__esModule) ? mod : { "default": mod };
- };
- Object.defineProperty(exports, "__esModule", { value: true });
- exports.Connection = void 0;
- const _ = __importStar(require("lodash"));
- const events_1 = require("events");
- const url_1 = require("url");
- const ws_1 = __importDefault(require("ws"));
- const rangeset_1 = __importDefault(require("./rangeset"));
- const errors_1 = require("./errors");
- const backoff_1 = require("./backoff");
- const INTENTIONAL_DISCONNECT_CODE = 4000;
- function createWebSocket(url, config) {
- const options = {};
- if (config.proxy != null) {
- const parsedURL = url_1.parse(url);
- const parsedProxyURL = url_1.parse(config.proxy);
- const proxyOverrides = _.omitBy({
- secureEndpoint: parsedURL.protocol === 'wss:',
- secureProxy: parsedProxyURL.protocol === 'https:',
- auth: config.proxyAuthorization,
- ca: config.trustedCertificates,
- key: config.key,
- passphrase: config.passphrase,
- cert: config.certificate
- }, (value) => value == null);
- const proxyOptions = Object.assign({}, parsedProxyURL, proxyOverrides);
- let HttpsProxyAgent;
- try {
- HttpsProxyAgent = require('https-proxy-agent');
- }
- catch (error) {
- throw new Error('"proxy" option is not supported in the browser');
- }
- options.agent = new HttpsProxyAgent(proxyOptions);
- }
- if (config.authorization != null) {
- const base64 = Buffer.from(config.authorization).toString('base64');
- options.headers = { Authorization: `Basic ${base64}` };
- }
- const optionsOverrides = _.omitBy({
- ca: config.trustedCertificates,
- key: config.key,
- passphrase: config.passphrase,
- cert: config.certificate
- }, (value) => value == null);
- const websocketOptions = Object.assign({}, options, optionsOverrides);
- const websocket = new ws_1.default(url, null, websocketOptions);
- if (typeof websocket.setMaxListeners === 'function') {
- websocket.setMaxListeners(Infinity);
- }
- return websocket;
- }
- function websocketSendAsync(ws, message) {
- return new Promise((resolve, reject) => {
- ws.send(message, undefined, (error) => {
- if (error) {
- reject(new errors_1.DisconnectedError(error.message, error));
- }
- else {
- resolve();
- }
- });
- });
- }
- class LedgerHistory {
- constructor() {
- this.feeBase = null;
- this.feeRef = null;
- this.latestVersion = null;
- this.reserveBase = null;
- this.availableVersions = new rangeset_1.default();
- }
- hasVersion(version) {
- return this.availableVersions.containsValue(version);
- }
- hasVersions(lowVersion, highVersion) {
- return this.availableVersions.containsRange(lowVersion, highVersion);
- }
- update(ledgerMessage) {
- this.feeBase = ledgerMessage.fee_base;
- this.feeRef = ledgerMessage.fee_ref;
- this.latestVersion = ledgerMessage.ledger_index;
- this.reserveBase = ledgerMessage.reserve_base;
- if (ledgerMessage.validated_ledgers) {
- this.availableVersions.reset();
- this.availableVersions.parseAndAddRanges(ledgerMessage.validated_ledgers);
- }
- else {
- this.availableVersions.addValue(this.latestVersion);
- }
- }
- }
- class ConnectionManager {
- constructor() {
- this.promisesAwaitingConnection = [];
- }
- resolveAllAwaiting() {
- this.promisesAwaitingConnection.map(({ resolve }) => resolve());
- this.promisesAwaitingConnection = [];
- }
- rejectAllAwaiting(error) {
- this.promisesAwaitingConnection.map(({ reject }) => reject(error));
- this.promisesAwaitingConnection = [];
- }
- awaitConnection() {
- return new Promise((resolve, reject) => {
- this.promisesAwaitingConnection.push({ resolve, reject });
- });
- }
- }
- class RequestManager {
- constructor() {
- this.nextId = 0;
- this.promisesAwaitingResponse = [];
- }
- cancel(id) {
- const { timer } = this.promisesAwaitingResponse[id];
- clearTimeout(timer);
- delete this.promisesAwaitingResponse[id];
- }
- resolve(id, data) {
- const { timer, resolve } = this.promisesAwaitingResponse[id];
- clearTimeout(timer);
- resolve(data);
- delete this.promisesAwaitingResponse[id];
- }
- reject(id, error) {
- const { timer, reject } = this.promisesAwaitingResponse[id];
- clearTimeout(timer);
- reject(error);
- delete this.promisesAwaitingResponse[id];
- }
- rejectAll(error) {
- this.promisesAwaitingResponse.forEach((_, id) => {
- this.reject(id, error);
- });
- }
- createRequest(data, timeout) {
- const newId = this.nextId++;
- const newData = JSON.stringify(Object.assign(Object.assign({}, data), { id: newId }));
- const timer = setTimeout(() => this.reject(newId, new errors_1.TimeoutError()), timeout);
- if (timer.unref) {
- timer.unref();
- }
- const newPromise = new Promise((resolve, reject) => {
- this.promisesAwaitingResponse[newId] = { resolve, reject, timer };
- });
- return [newId, newData, newPromise];
- }
- handleResponse(data) {
- if (!Number.isInteger(data.id) || data.id < 0) {
- throw new errors_1.ResponseFormatError('valid id not found in response', data);
- }
- if (!this.promisesAwaitingResponse[data.id]) {
- return;
- }
- if (data.status === 'error') {
- const error = new errors_1.RippledError(data.error_message || data.error, data);
- this.reject(data.id, error);
- return;
- }
- if (data.status !== 'success') {
- const error = new errors_1.ResponseFormatError(`unrecognized status: ${data.status}`, data);
- this.reject(data.id, error);
- return;
- }
- this.resolve(data.id, data.result);
- }
- }
- class Connection extends events_1.EventEmitter {
- constructor(url, options = {}) {
- super();
- this._ws = null;
- this._reconnectTimeoutID = null;
- this._heartbeatIntervalID = null;
- this._retryConnectionBackoff = new backoff_1.ExponentialBackoff({
- min: 100,
- max: 60 * 1000
- });
- this._trace = () => { };
- this._ledger = new LedgerHistory();
- this._requestManager = new RequestManager();
- this._connectionManager = new ConnectionManager();
- this._clearHeartbeatInterval = () => {
- clearInterval(this._heartbeatIntervalID);
- };
- this._startHeartbeatInterval = () => {
- this._clearHeartbeatInterval();
- this._heartbeatIntervalID = setInterval(() => this._heartbeat(), this._config.timeout);
- };
- this._heartbeat = () => {
- return this.request({ command: 'ping' }).catch(() => {
- return this.reconnect().catch((error) => {
- this.emit('error', 'reconnect', error.message, error);
- });
- });
- };
- this._onConnectionFailed = (errorOrCode) => {
- if (this._ws) {
- this._ws.removeAllListeners();
- this._ws.on('error', () => {
- });
- this._ws.close();
- this._ws = null;
- }
- if (typeof errorOrCode === 'number') {
- this._connectionManager.rejectAllAwaiting(new errors_1.NotConnectedError(`Connection failed with code ${errorOrCode}.`, {
- code: errorOrCode
- }));
- }
- else if (errorOrCode && errorOrCode.message) {
- this._connectionManager.rejectAllAwaiting(new errors_1.NotConnectedError(errorOrCode.message, errorOrCode));
- }
- else {
- this._connectionManager.rejectAllAwaiting(new errors_1.NotConnectedError('Connection failed.'));
- }
- };
- this.setMaxListeners(Infinity);
- this._url = url;
- this._config = Object.assign({ timeout: 20 * 1000, connectionTimeout: 5 * 1000 }, options);
- if (typeof options.trace === 'function') {
- this._trace = options.trace;
- }
- else if (options.trace === true) {
- this._trace = console.log;
- }
- }
- _onMessage(message) {
- this._trace('receive', message);
- let data;
- try {
- data = JSON.parse(message);
- }
- catch (error) {
- this.emit('error', 'badMessage', error.message, message);
- return;
- }
- if (data.type == null && data.error) {
- this.emit('error', data.error, data.error_message, data);
- return;
- }
- if (data.type) {
- this.emit(data.type, data);
- }
- if (data.type === 'ledgerClosed') {
- this._ledger.update(data);
- }
- if (data.type === 'response') {
- try {
- this._requestManager.handleResponse(data);
- }
- catch (error) {
- this.emit('error', 'badMessage', error.message, message);
- }
- }
- }
- get _state() {
- return this._ws ? this._ws.readyState : ws_1.default.CLOSED;
- }
- get _shouldBeConnected() {
- return this._ws !== null;
- }
- _waitForReady() {
- return new Promise((resolve, reject) => {
- if (!this._shouldBeConnected) {
- reject(new errors_1.NotConnectedError());
- }
- else if (this._state === ws_1.default.OPEN) {
- resolve();
- }
- else {
- this.once('connected', () => resolve());
- }
- });
- }
- _subscribeToLedger() {
- return __awaiter(this, void 0, void 0, function* () {
- const data = yield this.request({
- command: 'subscribe',
- streams: ['ledger']
- });
- if (_.isEmpty(data) || !data.ledger_index) {
- try {
- yield this.disconnect();
- }
- catch (error) {
- }
- finally {
- throw new errors_1.RippledNotInitializedError('Rippled not initialized');
- }
- }
- this._ledger.update(data);
- });
- }
- isConnected() {
- return this._state === ws_1.default.OPEN;
- }
- connect() {
- if (this.isConnected()) {
- return Promise.resolve();
- }
- if (this._state === ws_1.default.CONNECTING) {
- return this._connectionManager.awaitConnection();
- }
- if (!this._url) {
- return Promise.reject(new errors_1.ConnectionError('Cannot connect because no server was specified'));
- }
- if (this._ws) {
- return Promise.reject(new errors_1.RippleError('Websocket connection never cleaned up.', {
- state: this._state
- }));
- }
- const connectionTimeoutID = setTimeout(() => {
- this._onConnectionFailed(new errors_1.ConnectionError(`Error: connect() timed out after ${this._config.connectionTimeout} ms. ` +
- `If your internet connection is working, the rippled server may be blocked or inaccessible. ` +
- `You can also try setting the 'connectionTimeout' option in the RippleAPI constructor.`));
- }, this._config.connectionTimeout);
- this._ws = createWebSocket(this._url, this._config);
- this._ws.on('error', this._onConnectionFailed);
- this._ws.on('error', () => clearTimeout(connectionTimeoutID));
- this._ws.on('close', this._onConnectionFailed);
- this._ws.on('close', () => clearTimeout(connectionTimeoutID));
- this._ws.once('open', () => __awaiter(this, void 0, void 0, function* () {
- this._ws.removeAllListeners();
- clearTimeout(connectionTimeoutID);
- this._ws.on('message', (message) => this._onMessage(message));
- this._ws.on('error', (error) => this.emit('error', 'websocket', error.message, error));
- this._ws.once('close', (code) => {
- this._clearHeartbeatInterval();
- this._requestManager.rejectAll(new errors_1.DisconnectedError('websocket was closed'));
- this._ws.removeAllListeners();
- this._ws = null;
- this.emit('disconnected', code);
- if (code !== INTENTIONAL_DISCONNECT_CODE) {
- const retryTimeout = this._retryConnectionBackoff.duration();
- this._trace('reconnect', `Retrying connection in ${retryTimeout}ms.`);
- this.emit('reconnecting', this._retryConnectionBackoff.attempts);
- this._reconnectTimeoutID = setTimeout(() => {
- this.reconnect().catch((error) => {
- this.emit('error', 'reconnect', error.message, error);
- });
- }, retryTimeout);
- }
- });
- try {
- this._retryConnectionBackoff.reset();
- yield this._subscribeToLedger();
- this._startHeartbeatInterval();
- this._connectionManager.resolveAllAwaiting();
- this.emit('connected');
- }
- catch (error) {
- this._connectionManager.rejectAllAwaiting(error);
- yield this.disconnect().catch(() => { });
- }
- }));
- return this._connectionManager.awaitConnection();
- }
- disconnect() {
- clearTimeout(this._reconnectTimeoutID);
- this._reconnectTimeoutID = null;
- if (this._state === ws_1.default.CLOSED || !this._ws) {
- return Promise.resolve(undefined);
- }
- return new Promise((resolve) => {
- this._ws.once('close', (code) => resolve(code));
- if (this._state !== ws_1.default.CLOSING) {
- this._ws.close(INTENTIONAL_DISCONNECT_CODE);
- }
- });
- }
- reconnect() {
- return __awaiter(this, void 0, void 0, function* () {
- this.emit('reconnect');
- yield this.disconnect();
- yield this.connect();
- });
- }
- getFeeBase() {
- return __awaiter(this, void 0, void 0, function* () {
- yield this._waitForReady();
- return this._ledger.feeBase;
- });
- }
- getFeeRef() {
- return __awaiter(this, void 0, void 0, function* () {
- yield this._waitForReady();
- return this._ledger.feeRef;
- });
- }
- getLedgerVersion() {
- return __awaiter(this, void 0, void 0, function* () {
- yield this._waitForReady();
- return this._ledger.latestVersion;
- });
- }
- getReserveBase() {
- return __awaiter(this, void 0, void 0, function* () {
- yield this._waitForReady();
- return this._ledger.reserveBase;
- });
- }
- hasLedgerVersions(lowLedgerVersion, highLedgerVersion) {
- return __awaiter(this, void 0, void 0, function* () {
- if (!highLedgerVersion) {
- return this.hasLedgerVersion(lowLedgerVersion);
- }
- yield this._waitForReady();
- return this._ledger.hasVersions(lowLedgerVersion, highLedgerVersion);
- });
- }
- hasLedgerVersion(ledgerVersion) {
- return __awaiter(this, void 0, void 0, function* () {
- yield this._waitForReady();
- return this._ledger.hasVersion(ledgerVersion);
- });
- }
- request(request, timeout) {
- return __awaiter(this, void 0, void 0, function* () {
- if (!this._shouldBeConnected) {
- throw new errors_1.NotConnectedError();
- }
- const [id, message, responsePromise] = this._requestManager.createRequest(request, timeout || this._config.timeout);
- this._trace('send', message);
- websocketSendAsync(this._ws, message).catch((error) => {
- this._requestManager.reject(id, error);
- });
- return responsePromise;
- });
- }
- getUrl() {
- return this._url;
- }
- }
- exports.Connection = Connection;
- //# sourceMappingURL=connection.js.map
|