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 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. "use strict";
  2. var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
  3. function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
  4. return new (P || (P = Promise))(function (resolve, reject) {
  5. function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
  6. function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
  7. function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
  8. step((generator = generator.apply(thisArg, _arguments || [])).next());
  9. });
  10. };
  11. var __importDefault = (this && this.__importDefault) || function (mod) {
  12. return (mod && mod.__esModule) ? mod : { "default": mod };
  13. };
  14. Object.defineProperty(exports, "__esModule", { value: true });
  15. exports.Connection = exports.INTENTIONAL_DISCONNECT_CODE = void 0;
  16. const events_1 = require("events");
  17. const omitBy_1 = __importDefault(require("lodash/omitBy"));
  18. const ws_1 = __importDefault(require("ws"));
  19. const errors_1 = require("../errors");
  20. const ConnectionManager_1 = __importDefault(require("./ConnectionManager"));
  21. const ExponentialBackoff_1 = __importDefault(require("./ExponentialBackoff"));
  22. const RequestManager_1 = __importDefault(require("./RequestManager"));
  23. const SECONDS_PER_MINUTE = 60;
  24. const TIMEOUT = 20;
  25. const CONNECTION_TIMEOUT = 5;
  26. exports.INTENTIONAL_DISCONNECT_CODE = 4000;
  27. function getAgent(url, config) {
  28. if (config.proxy == null) {
  29. return undefined;
  30. }
  31. const parsedURL = new URL(url);
  32. const parsedProxyURL = new URL(config.proxy);
  33. const proxyOptions = (0, omitBy_1.default)({
  34. secureEndpoint: parsedURL.protocol === 'wss:',
  35. secureProxy: parsedProxyURL.protocol === 'https:',
  36. auth: config.proxyAuthorization,
  37. ca: config.trustedCertificates,
  38. key: config.key,
  39. passphrase: config.passphrase,
  40. cert: config.certificate,
  41. href: parsedProxyURL.href,
  42. origin: parsedProxyURL.origin,
  43. protocol: parsedProxyURL.protocol,
  44. username: parsedProxyURL.username,
  45. password: parsedProxyURL.password,
  46. host: parsedProxyURL.host,
  47. hostname: parsedProxyURL.hostname,
  48. port: parsedProxyURL.port,
  49. pathname: parsedProxyURL.pathname,
  50. search: parsedProxyURL.search,
  51. hash: parsedProxyURL.hash,
  52. }, (value) => value == null);
  53. let HttpsProxyAgent;
  54. try {
  55. HttpsProxyAgent = require('https-proxy-agent');
  56. }
  57. catch (_error) {
  58. throw new Error('"proxy" option is not supported in the browser');
  59. }
  60. return new HttpsProxyAgent(proxyOptions);
  61. }
  62. function createWebSocket(url, config) {
  63. const options = {};
  64. options.agent = getAgent(url, config);
  65. if (config.headers) {
  66. options.headers = config.headers;
  67. }
  68. if (config.authorization != null) {
  69. const base64 = Buffer.from(config.authorization).toString('base64');
  70. options.headers = Object.assign(Object.assign({}, options.headers), { Authorization: `Basic ${base64}` });
  71. }
  72. const optionsOverrides = (0, omitBy_1.default)({
  73. ca: config.trustedCertificates,
  74. key: config.key,
  75. passphrase: config.passphrase,
  76. cert: config.certificate,
  77. }, (value) => value == null);
  78. const websocketOptions = Object.assign(Object.assign({}, options), optionsOverrides);
  79. const websocket = new ws_1.default(url, websocketOptions);
  80. if (typeof websocket.setMaxListeners === 'function') {
  81. websocket.setMaxListeners(Infinity);
  82. }
  83. return websocket;
  84. }
  85. function websocketSendAsync(ws, message) {
  86. return __awaiter(this, void 0, void 0, function* () {
  87. return new Promise((resolve, reject) => {
  88. ws.send(message, (error) => {
  89. if (error) {
  90. reject(new errors_1.DisconnectedError(error.message, error));
  91. }
  92. else {
  93. resolve();
  94. }
  95. });
  96. });
  97. });
  98. }
  99. class Connection extends events_1.EventEmitter {
  100. constructor(url, options = {}) {
  101. super();
  102. this.ws = null;
  103. this.reconnectTimeoutID = null;
  104. this.heartbeatIntervalID = null;
  105. this.retryConnectionBackoff = new ExponentialBackoff_1.default({
  106. min: 100,
  107. max: SECONDS_PER_MINUTE * 1000,
  108. });
  109. this.requestManager = new RequestManager_1.default();
  110. this.connectionManager = new ConnectionManager_1.default();
  111. this.trace = () => { };
  112. this.setMaxListeners(Infinity);
  113. this.url = url;
  114. this.config = Object.assign({ timeout: TIMEOUT * 1000, connectionTimeout: CONNECTION_TIMEOUT * 1000 }, options);
  115. if (typeof options.trace === 'function') {
  116. this.trace = options.trace;
  117. }
  118. else if (options.trace) {
  119. this.trace = console.log;
  120. }
  121. }
  122. get state() {
  123. return this.ws ? this.ws.readyState : ws_1.default.CLOSED;
  124. }
  125. get shouldBeConnected() {
  126. return this.ws !== null;
  127. }
  128. isConnected() {
  129. return this.state === ws_1.default.OPEN;
  130. }
  131. connect() {
  132. return __awaiter(this, void 0, void 0, function* () {
  133. if (this.isConnected()) {
  134. return Promise.resolve();
  135. }
  136. if (this.state === ws_1.default.CONNECTING) {
  137. return this.connectionManager.awaitConnection();
  138. }
  139. if (!this.url) {
  140. return Promise.reject(new errors_1.ConnectionError('Cannot connect because no server was specified'));
  141. }
  142. if (this.ws != null) {
  143. return Promise.reject(new errors_1.XrplError('Websocket connection never cleaned up.', {
  144. state: this.state,
  145. }));
  146. }
  147. const connectionTimeoutID = setTimeout(() => {
  148. this.onConnectionFailed(new errors_1.ConnectionError(`Error: connect() timed out after ${this.config.connectionTimeout} ms. If your internet connection is working, the ` +
  149. `rippled server may be blocked or inaccessible. You can also try setting the 'connectionTimeout' option in the Client constructor.`));
  150. }, this.config.connectionTimeout);
  151. this.ws = createWebSocket(this.url, this.config);
  152. if (this.ws == null) {
  153. throw new errors_1.XrplError('Connect: created null websocket');
  154. }
  155. this.ws.on('error', (error) => this.onConnectionFailed(error));
  156. this.ws.on('error', () => clearTimeout(connectionTimeoutID));
  157. this.ws.on('close', (reason) => this.onConnectionFailed(reason));
  158. this.ws.on('close', () => clearTimeout(connectionTimeoutID));
  159. this.ws.once('open', () => {
  160. void this.onceOpen(connectionTimeoutID);
  161. });
  162. return this.connectionManager.awaitConnection();
  163. });
  164. }
  165. disconnect() {
  166. return __awaiter(this, void 0, void 0, function* () {
  167. this.clearHeartbeatInterval();
  168. if (this.reconnectTimeoutID !== null) {
  169. clearTimeout(this.reconnectTimeoutID);
  170. this.reconnectTimeoutID = null;
  171. }
  172. if (this.state === ws_1.default.CLOSED) {
  173. return Promise.resolve(undefined);
  174. }
  175. if (this.ws == null) {
  176. return Promise.resolve(undefined);
  177. }
  178. return new Promise((resolve) => {
  179. if (this.ws == null) {
  180. resolve(undefined);
  181. }
  182. if (this.ws != null) {
  183. this.ws.once('close', (code) => resolve(code));
  184. }
  185. if (this.ws != null && this.state !== ws_1.default.CLOSING) {
  186. this.ws.close(exports.INTENTIONAL_DISCONNECT_CODE);
  187. }
  188. });
  189. });
  190. }
  191. reconnect() {
  192. return __awaiter(this, void 0, void 0, function* () {
  193. this.emit('reconnect');
  194. yield this.disconnect();
  195. yield this.connect();
  196. });
  197. }
  198. request(request, timeout) {
  199. return __awaiter(this, void 0, void 0, function* () {
  200. if (!this.shouldBeConnected || this.ws == null) {
  201. throw new errors_1.NotConnectedError(JSON.stringify(request), request);
  202. }
  203. const [id, message, responsePromise] = this.requestManager.createRequest(request, timeout !== null && timeout !== void 0 ? timeout : this.config.timeout);
  204. this.trace('send', message);
  205. websocketSendAsync(this.ws, message).catch((error) => {
  206. this.requestManager.reject(id, error);
  207. });
  208. return responsePromise;
  209. });
  210. }
  211. getUrl() {
  212. var _a;
  213. return (_a = this.url) !== null && _a !== void 0 ? _a : '';
  214. }
  215. onMessage(message) {
  216. this.trace('receive', message);
  217. let data;
  218. try {
  219. data = JSON.parse(message);
  220. }
  221. catch (error) {
  222. if (error instanceof Error) {
  223. this.emit('error', 'badMessage', error.message, message);
  224. }
  225. return;
  226. }
  227. if (data.type == null && data.error) {
  228. this.emit('error', data.error, data.error_message, data);
  229. return;
  230. }
  231. if (data.type) {
  232. this.emit(data.type, data);
  233. }
  234. if (data.type === 'response') {
  235. try {
  236. this.requestManager.handleResponse(data);
  237. }
  238. catch (error) {
  239. if (error instanceof Error) {
  240. this.emit('error', 'badMessage', error.message, message);
  241. }
  242. else {
  243. this.emit('error', 'badMessage', error, error);
  244. }
  245. }
  246. }
  247. }
  248. onceOpen(connectionTimeoutID) {
  249. return __awaiter(this, void 0, void 0, function* () {
  250. if (this.ws == null) {
  251. throw new errors_1.XrplError('onceOpen: ws is null');
  252. }
  253. this.ws.removeAllListeners();
  254. clearTimeout(connectionTimeoutID);
  255. this.ws.on('message', (message) => this.onMessage(message));
  256. this.ws.on('error', (error) => this.emit('error', 'websocket', error.message, error));
  257. this.ws.once('close', (code, reason) => {
  258. if (this.ws == null) {
  259. throw new errors_1.XrplError('onceClose: ws is null');
  260. }
  261. this.clearHeartbeatInterval();
  262. this.requestManager.rejectAll(new errors_1.DisconnectedError(`websocket was closed, ${new TextDecoder('utf-8').decode(reason)}`));
  263. this.ws.removeAllListeners();
  264. this.ws = null;
  265. if (code === undefined) {
  266. const internalErrorCode = 1011;
  267. this.emit('disconnected', internalErrorCode);
  268. }
  269. else {
  270. this.emit('disconnected', code);
  271. }
  272. if (code !== exports.INTENTIONAL_DISCONNECT_CODE && code !== undefined) {
  273. this.intentionalDisconnect();
  274. }
  275. });
  276. try {
  277. this.retryConnectionBackoff.reset();
  278. this.startHeartbeatInterval();
  279. this.connectionManager.resolveAllAwaiting();
  280. this.emit('connected');
  281. }
  282. catch (error) {
  283. if (error instanceof Error) {
  284. this.connectionManager.rejectAllAwaiting(error);
  285. yield this.disconnect().catch(() => { });
  286. }
  287. }
  288. });
  289. }
  290. intentionalDisconnect() {
  291. const retryTimeout = this.retryConnectionBackoff.duration();
  292. this.trace('reconnect', `Retrying connection in ${retryTimeout}ms.`);
  293. this.emit('reconnecting', this.retryConnectionBackoff.attempts);
  294. this.reconnectTimeoutID = setTimeout(() => {
  295. this.reconnect().catch((error) => {
  296. this.emit('error', 'reconnect', error.message, error);
  297. });
  298. }, retryTimeout);
  299. }
  300. clearHeartbeatInterval() {
  301. if (this.heartbeatIntervalID) {
  302. clearInterval(this.heartbeatIntervalID);
  303. }
  304. }
  305. startHeartbeatInterval() {
  306. this.clearHeartbeatInterval();
  307. this.heartbeatIntervalID = setInterval(() => {
  308. void this.heartbeat();
  309. }, this.config.timeout);
  310. }
  311. heartbeat() {
  312. return __awaiter(this, void 0, void 0, function* () {
  313. this.request({ command: 'ping' }).catch(() => __awaiter(this, void 0, void 0, function* () {
  314. return this.reconnect().catch((error) => {
  315. this.emit('error', 'reconnect', error.message, error);
  316. });
  317. }));
  318. });
  319. }
  320. onConnectionFailed(errorOrCode) {
  321. if (this.ws) {
  322. this.ws.removeAllListeners();
  323. this.ws.on('error', () => {
  324. });
  325. this.ws.close();
  326. this.ws = null;
  327. }
  328. if (typeof errorOrCode === 'number') {
  329. this.connectionManager.rejectAllAwaiting(new errors_1.NotConnectedError(`Connection failed with code ${errorOrCode}.`, {
  330. code: errorOrCode,
  331. }));
  332. }
  333. else if (errorOrCode === null || errorOrCode === void 0 ? void 0 : errorOrCode.message) {
  334. this.connectionManager.rejectAllAwaiting(new errors_1.NotConnectedError(errorOrCode.message, errorOrCode));
  335. }
  336. else {
  337. this.connectionManager.rejectAllAwaiting(new errors_1.NotConnectedError('Connection failed.'));
  338. }
  339. }
  340. }
  341. exports.Connection = Connection;
  342. //# sourceMappingURL=connection.js.map