You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

connection.js 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467
  1. "use strict";
  2. var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
  3. if (k2 === undefined) k2 = k;
  4. Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
  5. }) : (function(o, m, k, k2) {
  6. if (k2 === undefined) k2 = k;
  7. o[k2] = m[k];
  8. }));
  9. var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
  10. Object.defineProperty(o, "default", { enumerable: true, value: v });
  11. }) : function(o, v) {
  12. o["default"] = v;
  13. });
  14. var __importStar = (this && this.__importStar) || function (mod) {
  15. if (mod && mod.__esModule) return mod;
  16. var result = {};
  17. if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
  18. __setModuleDefault(result, mod);
  19. return result;
  20. };
  21. var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
  22. function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
  23. return new (P || (P = Promise))(function (resolve, reject) {
  24. function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
  25. function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
  26. function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
  27. step((generator = generator.apply(thisArg, _arguments || [])).next());
  28. });
  29. };
  30. var __importDefault = (this && this.__importDefault) || function (mod) {
  31. return (mod && mod.__esModule) ? mod : { "default": mod };
  32. };
  33. Object.defineProperty(exports, "__esModule", { value: true });
  34. exports.Connection = void 0;
  35. const _ = __importStar(require("lodash"));
  36. const events_1 = require("events");
  37. const url_1 = require("url");
  38. const ws_1 = __importDefault(require("ws"));
  39. const rangeset_1 = __importDefault(require("./rangeset"));
  40. const errors_1 = require("./errors");
  41. const backoff_1 = require("./backoff");
  42. const INTENTIONAL_DISCONNECT_CODE = 4000;
  43. function createWebSocket(url, config) {
  44. const options = {};
  45. if (config.proxy != null) {
  46. const parsedURL = url_1.parse(url);
  47. const parsedProxyURL = url_1.parse(config.proxy);
  48. const proxyOverrides = _.omitBy({
  49. secureEndpoint: parsedURL.protocol === 'wss:',
  50. secureProxy: parsedProxyURL.protocol === 'https:',
  51. auth: config.proxyAuthorization,
  52. ca: config.trustedCertificates,
  53. key: config.key,
  54. passphrase: config.passphrase,
  55. cert: config.certificate
  56. }, (value) => value == null);
  57. const proxyOptions = Object.assign({}, parsedProxyURL, proxyOverrides);
  58. let HttpsProxyAgent;
  59. try {
  60. HttpsProxyAgent = require('https-proxy-agent');
  61. }
  62. catch (error) {
  63. throw new Error('"proxy" option is not supported in the browser');
  64. }
  65. options.agent = new HttpsProxyAgent(proxyOptions);
  66. }
  67. if (config.authorization != null) {
  68. const base64 = Buffer.from(config.authorization).toString('base64');
  69. options.headers = { Authorization: `Basic ${base64}` };
  70. }
  71. const optionsOverrides = _.omitBy({
  72. ca: config.trustedCertificates,
  73. key: config.key,
  74. passphrase: config.passphrase,
  75. cert: config.certificate
  76. }, (value) => value == null);
  77. const websocketOptions = Object.assign({}, options, optionsOverrides);
  78. const websocket = new ws_1.default(url, null, websocketOptions);
  79. if (typeof websocket.setMaxListeners === 'function') {
  80. websocket.setMaxListeners(Infinity);
  81. }
  82. return websocket;
  83. }
  84. function websocketSendAsync(ws, message) {
  85. return new Promise((resolve, reject) => {
  86. ws.send(message, undefined, (error) => {
  87. if (error) {
  88. reject(new errors_1.DisconnectedError(error.message, error));
  89. }
  90. else {
  91. resolve();
  92. }
  93. });
  94. });
  95. }
  96. class LedgerHistory {
  97. constructor() {
  98. this.feeBase = null;
  99. this.feeRef = null;
  100. this.latestVersion = null;
  101. this.reserveBase = null;
  102. this.availableVersions = new rangeset_1.default();
  103. }
  104. hasVersion(version) {
  105. return this.availableVersions.containsValue(version);
  106. }
  107. hasVersions(lowVersion, highVersion) {
  108. return this.availableVersions.containsRange(lowVersion, highVersion);
  109. }
  110. update(ledgerMessage) {
  111. this.feeBase = ledgerMessage.fee_base;
  112. this.feeRef = ledgerMessage.fee_ref;
  113. this.latestVersion = ledgerMessage.ledger_index;
  114. this.reserveBase = ledgerMessage.reserve_base;
  115. if (ledgerMessage.validated_ledgers) {
  116. this.availableVersions.reset();
  117. this.availableVersions.parseAndAddRanges(ledgerMessage.validated_ledgers);
  118. }
  119. else {
  120. this.availableVersions.addValue(this.latestVersion);
  121. }
  122. }
  123. }
  124. class ConnectionManager {
  125. constructor() {
  126. this.promisesAwaitingConnection = [];
  127. }
  128. resolveAllAwaiting() {
  129. this.promisesAwaitingConnection.map(({ resolve }) => resolve());
  130. this.promisesAwaitingConnection = [];
  131. }
  132. rejectAllAwaiting(error) {
  133. this.promisesAwaitingConnection.map(({ reject }) => reject(error));
  134. this.promisesAwaitingConnection = [];
  135. }
  136. awaitConnection() {
  137. return new Promise((resolve, reject) => {
  138. this.promisesAwaitingConnection.push({ resolve, reject });
  139. });
  140. }
  141. }
  142. class RequestManager {
  143. constructor() {
  144. this.nextId = 0;
  145. this.promisesAwaitingResponse = [];
  146. }
  147. cancel(id) {
  148. const { timer } = this.promisesAwaitingResponse[id];
  149. clearTimeout(timer);
  150. delete this.promisesAwaitingResponse[id];
  151. }
  152. resolve(id, data) {
  153. const { timer, resolve } = this.promisesAwaitingResponse[id];
  154. clearTimeout(timer);
  155. resolve(data);
  156. delete this.promisesAwaitingResponse[id];
  157. }
  158. reject(id, error) {
  159. const { timer, reject } = this.promisesAwaitingResponse[id];
  160. clearTimeout(timer);
  161. reject(error);
  162. delete this.promisesAwaitingResponse[id];
  163. }
  164. rejectAll(error) {
  165. this.promisesAwaitingResponse.forEach((_, id) => {
  166. this.reject(id, error);
  167. });
  168. }
  169. createRequest(data, timeout) {
  170. const newId = this.nextId++;
  171. const newData = JSON.stringify(Object.assign(Object.assign({}, data), { id: newId }));
  172. const timer = setTimeout(() => this.reject(newId, new errors_1.TimeoutError()), timeout);
  173. if (timer.unref) {
  174. timer.unref();
  175. }
  176. const newPromise = new Promise((resolve, reject) => {
  177. this.promisesAwaitingResponse[newId] = { resolve, reject, timer };
  178. });
  179. return [newId, newData, newPromise];
  180. }
  181. handleResponse(data) {
  182. if (!Number.isInteger(data.id) || data.id < 0) {
  183. throw new errors_1.ResponseFormatError('valid id not found in response', data);
  184. }
  185. if (!this.promisesAwaitingResponse[data.id]) {
  186. return;
  187. }
  188. if (data.status === 'error') {
  189. const error = new errors_1.RippledError(data.error_message || data.error, data);
  190. this.reject(data.id, error);
  191. return;
  192. }
  193. if (data.status !== 'success') {
  194. const error = new errors_1.ResponseFormatError(`unrecognized status: ${data.status}`, data);
  195. this.reject(data.id, error);
  196. return;
  197. }
  198. this.resolve(data.id, data.result);
  199. }
  200. }
  201. class Connection extends events_1.EventEmitter {
  202. constructor(url, options = {}) {
  203. super();
  204. this._ws = null;
  205. this._reconnectTimeoutID = null;
  206. this._heartbeatIntervalID = null;
  207. this._retryConnectionBackoff = new backoff_1.ExponentialBackoff({
  208. min: 100,
  209. max: 60 * 1000
  210. });
  211. this._trace = () => { };
  212. this._ledger = new LedgerHistory();
  213. this._requestManager = new RequestManager();
  214. this._connectionManager = new ConnectionManager();
  215. this._clearHeartbeatInterval = () => {
  216. clearInterval(this._heartbeatIntervalID);
  217. };
  218. this._startHeartbeatInterval = () => {
  219. this._clearHeartbeatInterval();
  220. this._heartbeatIntervalID = setInterval(() => this._heartbeat(), this._config.timeout);
  221. };
  222. this._heartbeat = () => {
  223. return this.request({ command: 'ping' }).catch(() => {
  224. return this.reconnect().catch((error) => {
  225. this.emit('error', 'reconnect', error.message, error);
  226. });
  227. });
  228. };
  229. this._onConnectionFailed = (errorOrCode) => {
  230. if (this._ws) {
  231. this._ws.removeAllListeners();
  232. this._ws.on('error', () => {
  233. });
  234. this._ws.close();
  235. this._ws = null;
  236. }
  237. if (typeof errorOrCode === 'number') {
  238. this._connectionManager.rejectAllAwaiting(new errors_1.NotConnectedError(`Connection failed with code ${errorOrCode}.`, {
  239. code: errorOrCode
  240. }));
  241. }
  242. else if (errorOrCode && errorOrCode.message) {
  243. this._connectionManager.rejectAllAwaiting(new errors_1.NotConnectedError(errorOrCode.message, errorOrCode));
  244. }
  245. else {
  246. this._connectionManager.rejectAllAwaiting(new errors_1.NotConnectedError('Connection failed.'));
  247. }
  248. };
  249. this.setMaxListeners(Infinity);
  250. this._url = url;
  251. this._config = Object.assign({ timeout: 20 * 1000, connectionTimeout: 5 * 1000 }, options);
  252. if (typeof options.trace === 'function') {
  253. this._trace = options.trace;
  254. }
  255. else if (options.trace === true) {
  256. this._trace = console.log;
  257. }
  258. }
  259. _onMessage(message) {
  260. this._trace('receive', message);
  261. let data;
  262. try {
  263. data = JSON.parse(message);
  264. }
  265. catch (error) {
  266. this.emit('error', 'badMessage', error.message, message);
  267. return;
  268. }
  269. if (data.type == null && data.error) {
  270. this.emit('error', data.error, data.error_message, data);
  271. return;
  272. }
  273. if (data.type) {
  274. this.emit(data.type, data);
  275. }
  276. if (data.type === 'ledgerClosed') {
  277. this._ledger.update(data);
  278. }
  279. if (data.type === 'response') {
  280. try {
  281. this._requestManager.handleResponse(data);
  282. }
  283. catch (error) {
  284. this.emit('error', 'badMessage', error.message, message);
  285. }
  286. }
  287. }
  288. get _state() {
  289. return this._ws ? this._ws.readyState : ws_1.default.CLOSED;
  290. }
  291. get _shouldBeConnected() {
  292. return this._ws !== null;
  293. }
  294. _waitForReady() {
  295. return new Promise((resolve, reject) => {
  296. if (!this._shouldBeConnected) {
  297. reject(new errors_1.NotConnectedError());
  298. }
  299. else if (this._state === ws_1.default.OPEN) {
  300. resolve();
  301. }
  302. else {
  303. this.once('connected', () => resolve());
  304. }
  305. });
  306. }
  307. _subscribeToLedger() {
  308. return __awaiter(this, void 0, void 0, function* () {
  309. const data = yield this.request({
  310. command: 'subscribe',
  311. streams: ['ledger']
  312. });
  313. if (_.isEmpty(data) || !data.ledger_index) {
  314. try {
  315. yield this.disconnect();
  316. }
  317. catch (error) {
  318. }
  319. finally {
  320. throw new errors_1.RippledNotInitializedError('Rippled not initialized');
  321. }
  322. }
  323. this._ledger.update(data);
  324. });
  325. }
  326. isConnected() {
  327. return this._state === ws_1.default.OPEN;
  328. }
  329. connect() {
  330. if (this.isConnected()) {
  331. return Promise.resolve();
  332. }
  333. if (this._state === ws_1.default.CONNECTING) {
  334. return this._connectionManager.awaitConnection();
  335. }
  336. if (!this._url) {
  337. return Promise.reject(new errors_1.ConnectionError('Cannot connect because no server was specified'));
  338. }
  339. if (this._ws) {
  340. return Promise.reject(new errors_1.RippleError('Websocket connection never cleaned up.', {
  341. state: this._state
  342. }));
  343. }
  344. const connectionTimeoutID = setTimeout(() => {
  345. this._onConnectionFailed(new errors_1.ConnectionError(`Error: connect() timed out after ${this._config.connectionTimeout} ms. ` +
  346. `If your internet connection is working, the rippled server may be blocked or inaccessible. ` +
  347. `You can also try setting the 'connectionTimeout' option in the RippleAPI constructor.`));
  348. }, this._config.connectionTimeout);
  349. this._ws = createWebSocket(this._url, this._config);
  350. this._ws.on('error', this._onConnectionFailed);
  351. this._ws.on('error', () => clearTimeout(connectionTimeoutID));
  352. this._ws.on('close', this._onConnectionFailed);
  353. this._ws.on('close', () => clearTimeout(connectionTimeoutID));
  354. this._ws.once('open', () => __awaiter(this, void 0, void 0, function* () {
  355. this._ws.removeAllListeners();
  356. clearTimeout(connectionTimeoutID);
  357. this._ws.on('message', (message) => this._onMessage(message));
  358. this._ws.on('error', (error) => this.emit('error', 'websocket', error.message, error));
  359. this._ws.once('close', (code) => {
  360. this._clearHeartbeatInterval();
  361. this._requestManager.rejectAll(new errors_1.DisconnectedError('websocket was closed'));
  362. this._ws.removeAllListeners();
  363. this._ws = null;
  364. this.emit('disconnected', code);
  365. if (code !== INTENTIONAL_DISCONNECT_CODE) {
  366. const retryTimeout = this._retryConnectionBackoff.duration();
  367. this._trace('reconnect', `Retrying connection in ${retryTimeout}ms.`);
  368. this.emit('reconnecting', this._retryConnectionBackoff.attempts);
  369. this._reconnectTimeoutID = setTimeout(() => {
  370. this.reconnect().catch((error) => {
  371. this.emit('error', 'reconnect', error.message, error);
  372. });
  373. }, retryTimeout);
  374. }
  375. });
  376. try {
  377. this._retryConnectionBackoff.reset();
  378. yield this._subscribeToLedger();
  379. this._startHeartbeatInterval();
  380. this._connectionManager.resolveAllAwaiting();
  381. this.emit('connected');
  382. }
  383. catch (error) {
  384. this._connectionManager.rejectAllAwaiting(error);
  385. yield this.disconnect().catch(() => { });
  386. }
  387. }));
  388. return this._connectionManager.awaitConnection();
  389. }
  390. disconnect() {
  391. clearTimeout(this._reconnectTimeoutID);
  392. this._reconnectTimeoutID = null;
  393. if (this._state === ws_1.default.CLOSED || !this._ws) {
  394. return Promise.resolve(undefined);
  395. }
  396. return new Promise((resolve) => {
  397. this._ws.once('close', (code) => resolve(code));
  398. if (this._state !== ws_1.default.CLOSING) {
  399. this._ws.close(INTENTIONAL_DISCONNECT_CODE);
  400. }
  401. });
  402. }
  403. reconnect() {
  404. return __awaiter(this, void 0, void 0, function* () {
  405. this.emit('reconnect');
  406. yield this.disconnect();
  407. yield this.connect();
  408. });
  409. }
  410. getFeeBase() {
  411. return __awaiter(this, void 0, void 0, function* () {
  412. yield this._waitForReady();
  413. return this._ledger.feeBase;
  414. });
  415. }
  416. getFeeRef() {
  417. return __awaiter(this, void 0, void 0, function* () {
  418. yield this._waitForReady();
  419. return this._ledger.feeRef;
  420. });
  421. }
  422. getLedgerVersion() {
  423. return __awaiter(this, void 0, void 0, function* () {
  424. yield this._waitForReady();
  425. return this._ledger.latestVersion;
  426. });
  427. }
  428. getReserveBase() {
  429. return __awaiter(this, void 0, void 0, function* () {
  430. yield this._waitForReady();
  431. return this._ledger.reserveBase;
  432. });
  433. }
  434. hasLedgerVersions(lowLedgerVersion, highLedgerVersion) {
  435. return __awaiter(this, void 0, void 0, function* () {
  436. if (!highLedgerVersion) {
  437. return this.hasLedgerVersion(lowLedgerVersion);
  438. }
  439. yield this._waitForReady();
  440. return this._ledger.hasVersions(lowLedgerVersion, highLedgerVersion);
  441. });
  442. }
  443. hasLedgerVersion(ledgerVersion) {
  444. return __awaiter(this, void 0, void 0, function* () {
  445. yield this._waitForReady();
  446. return this._ledger.hasVersion(ledgerVersion);
  447. });
  448. }
  449. request(request, timeout) {
  450. return __awaiter(this, void 0, void 0, function* () {
  451. if (!this._shouldBeConnected) {
  452. throw new errors_1.NotConnectedError();
  453. }
  454. const [id, message, responsePromise] = this._requestManager.createRequest(request, timeout || this._config.timeout);
  455. this._trace('send', message);
  456. websocketSendAsync(this._ws, message).catch((error) => {
  457. this._requestManager.reject(id, error);
  458. });
  459. return responsePromise;
  460. });
  461. }
  462. getUrl() {
  463. return this._url;
  464. }
  465. }
  466. exports.Connection = Connection;
  467. //# sourceMappingURL=connection.js.map