websocket-client.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. const virtual_websocket_client_1 = require("./virtual-websocket-client");
  4. const utils_1 = require("../utils/utils");
  5. const message_1 = require("./message");
  6. const ws_event_1 = require("./ws-event");
  7. const error_1 = require("../utils/error");
  8. const error_2 = require("./error");
  9. const error_config_1 = require("../config/error.config");
  10. const __1 = require("../");
  11. const WS_READY_STATE = {
  12. CONNECTING: 0,
  13. OPEN: 1,
  14. CLOSING: 2,
  15. CLOSED: 3
  16. };
  17. const MAX_RTT_OBSERVED = 3;
  18. const DEFAULT_EXPECTED_EVENT_WAIT_TIME = 5000;
  19. const DEFAULT_UNTRUSTED_RTT_THRESHOLD = 10000;
  20. const DEFAULT_MAX_RECONNECT = 5;
  21. const DEFAULT_WS_RECONNECT_INTERVAL = 10000;
  22. const DEFAULT_PING_FAIL_TOLERANCE = 2;
  23. const DEFAULT_PONG_MISS_TOLERANCE = 2;
  24. const DEFAULT_LOGIN_TIMEOUT = 5000;
  25. class RealtimeWebSocketClient {
  26. constructor(options) {
  27. this._virtualWSClient = new Set();
  28. this._queryIdClientMap = new Map();
  29. this._watchIdClientMap = new Map();
  30. this._pingFailed = 0;
  31. this._pongMissed = 0;
  32. this._logins = new Map();
  33. this._wsReadySubsribers = [];
  34. this._wsResponseWait = new Map();
  35. this._rttObserved = [];
  36. this.initWebSocketConnection = async (reconnect, availableRetries = this._maxReconnect) => {
  37. if (reconnect && this._reconnectState) {
  38. return;
  39. }
  40. if (reconnect) {
  41. this._reconnectState = true;
  42. }
  43. if (this._wsInitPromise) {
  44. return this._wsInitPromise;
  45. }
  46. if (reconnect) {
  47. this.pauseClients();
  48. }
  49. this.close(ws_event_1.CLOSE_EVENT_CODE.ReconnectWebSocket);
  50. this._wsInitPromise = new Promise(async (resolve, reject) => {
  51. try {
  52. const wsSign = await this.getWsSign();
  53. await new Promise(success => {
  54. const url = wsSign.wsUrl || 'wss://tcb-ws.tencentcloudapi.com';
  55. this._ws = __1.Db.wsClass ? new __1.Db.wsClass(url) : new WebSocket(url);
  56. success();
  57. });
  58. if (this._ws.connect) {
  59. await this._ws.connect();
  60. }
  61. await this.initWebSocketEvent();
  62. resolve();
  63. if (reconnect) {
  64. this.resumeClients();
  65. this._reconnectState = false;
  66. }
  67. }
  68. catch (e) {
  69. console.error('[realtime] initWebSocketConnection connect fail', e);
  70. if (availableRetries > 0) {
  71. const isConnected = true;
  72. this._wsInitPromise = undefined;
  73. if (isConnected) {
  74. await utils_1.sleep(this._reconnectInterval);
  75. if (reconnect) {
  76. this._reconnectState = false;
  77. }
  78. }
  79. resolve(this.initWebSocketConnection(reconnect, availableRetries - 1));
  80. }
  81. else {
  82. reject(e);
  83. if (reconnect) {
  84. this.closeAllClients(new error_1.CloudSDKError({
  85. errCode: error_config_1.ERR_CODE.SDK_DATABASE_REALTIME_LISTENER_RECONNECT_WATCH_FAIL,
  86. errMsg: e
  87. }));
  88. }
  89. }
  90. }
  91. });
  92. try {
  93. await this._wsInitPromise;
  94. this._wsReadySubsribers.forEach(({ resolve }) => resolve());
  95. }
  96. catch (e) {
  97. this._wsReadySubsribers.forEach(({ reject }) => reject());
  98. }
  99. finally {
  100. this._wsInitPromise = undefined;
  101. this._wsReadySubsribers = [];
  102. }
  103. };
  104. this.initWebSocketEvent = () => new Promise((resolve, reject) => {
  105. if (!this._ws) {
  106. throw new Error('can not initWebSocketEvent, ws not exists');
  107. }
  108. let wsOpened = false;
  109. this._ws.onopen = event => {
  110. console.warn('[realtime] ws event: open', event);
  111. wsOpened = true;
  112. resolve();
  113. };
  114. this._ws.onerror = event => {
  115. this._logins = new Map();
  116. if (!wsOpened) {
  117. console.error('[realtime] ws open failed with ws event: error', event);
  118. reject(event);
  119. }
  120. else {
  121. console.error('[realtime] ws event: error', event);
  122. this.clearHeartbeat();
  123. this._virtualWSClient.forEach(client => client.closeWithError(new error_1.CloudSDKError({
  124. errCode: error_config_1.ERR_CODE.SDK_DATABASE_REALTIME_LISTENER_WEBSOCKET_CONNECTION_ERROR,
  125. errMsg: event
  126. })));
  127. }
  128. };
  129. this._ws.onclose = closeEvent => {
  130. console.warn('[realtime] ws event: close', closeEvent);
  131. this._logins = new Map();
  132. this.clearHeartbeat();
  133. switch (closeEvent.code) {
  134. case ws_event_1.CLOSE_EVENT_CODE.ReconnectWebSocket: {
  135. break;
  136. }
  137. case ws_event_1.CLOSE_EVENT_CODE.NoRealtimeListeners: {
  138. break;
  139. }
  140. case ws_event_1.CLOSE_EVENT_CODE.HeartbeatPingError:
  141. case ws_event_1.CLOSE_EVENT_CODE.HeartbeatPongTimeoutError:
  142. case ws_event_1.CLOSE_EVENT_CODE.NormalClosure:
  143. case ws_event_1.CLOSE_EVENT_CODE.AbnormalClosure: {
  144. if (this._maxReconnect > 0) {
  145. this.initWebSocketConnection(true, this._maxReconnect);
  146. }
  147. else {
  148. this.closeAllClients(ws_event_1.getWSCloseError(closeEvent.code));
  149. }
  150. break;
  151. }
  152. case ws_event_1.CLOSE_EVENT_CODE.NoAuthentication: {
  153. this.closeAllClients(ws_event_1.getWSCloseError(closeEvent.code, closeEvent.reason));
  154. break;
  155. }
  156. default: {
  157. if (this._maxReconnect > 0) {
  158. this.initWebSocketConnection(true, this._maxReconnect);
  159. }
  160. else {
  161. this.closeAllClients(ws_event_1.getWSCloseError(closeEvent.code));
  162. }
  163. }
  164. }
  165. };
  166. this._ws.onmessage = res => {
  167. const rawMsg = res.data;
  168. this.heartbeat();
  169. let msg;
  170. try {
  171. msg = JSON.parse(rawMsg);
  172. }
  173. catch (e) {
  174. throw new Error(`[realtime] onMessage parse res.data error: ${e}`);
  175. }
  176. if (msg.msgType === 'ERROR') {
  177. let virtualWatch = null;
  178. this._virtualWSClient.forEach(item => {
  179. if (item.watchId === msg.watchId) {
  180. virtualWatch = item;
  181. }
  182. });
  183. if (virtualWatch) {
  184. virtualWatch.listener.onError(msg);
  185. }
  186. }
  187. const responseWaitSpec = this._wsResponseWait.get(msg.requestId);
  188. if (responseWaitSpec) {
  189. try {
  190. if (msg.msgType === 'ERROR') {
  191. responseWaitSpec.reject(new error_2.RealtimeErrorMessageError(msg));
  192. }
  193. else {
  194. responseWaitSpec.resolve(msg);
  195. }
  196. }
  197. catch (e) {
  198. console.error('ws onMessage responseWaitSpec.resolve(msg) errored:', e);
  199. }
  200. finally {
  201. this._wsResponseWait.delete(msg.requestId);
  202. }
  203. if (responseWaitSpec.skipOnMessage) {
  204. return;
  205. }
  206. }
  207. if (msg.msgType === 'PONG') {
  208. if (this._lastPingSendTS) {
  209. const rtt = Date.now() - this._lastPingSendTS;
  210. if (rtt > DEFAULT_UNTRUSTED_RTT_THRESHOLD) {
  211. console.warn(`[realtime] untrusted rtt observed: ${rtt}`);
  212. return;
  213. }
  214. if (this._rttObserved.length >= MAX_RTT_OBSERVED) {
  215. this._rttObserved.splice(0, this._rttObserved.length - MAX_RTT_OBSERVED + 1);
  216. }
  217. this._rttObserved.push(rtt);
  218. }
  219. return;
  220. }
  221. let client = msg.watchId && this._watchIdClientMap.get(msg.watchId);
  222. if (client) {
  223. client.onMessage(msg);
  224. }
  225. else {
  226. console.error(`[realtime] no realtime listener found responsible for watchId ${msg.watchId}: `, msg);
  227. switch (msg.msgType) {
  228. case 'INIT_EVENT':
  229. case 'NEXT_EVENT':
  230. case 'CHECK_EVENT': {
  231. client = this._queryIdClientMap.get(msg.msgData.queryID);
  232. if (client) {
  233. client.onMessage(msg);
  234. }
  235. break;
  236. }
  237. default: {
  238. for (const [, client] of this._watchIdClientMap) {
  239. client.onMessage(msg);
  240. break;
  241. }
  242. }
  243. }
  244. }
  245. };
  246. this.heartbeat();
  247. });
  248. this.isWSConnected = () => {
  249. return Boolean(this._ws && this._ws.readyState === WS_READY_STATE.OPEN);
  250. };
  251. this.onceWSConnected = async () => {
  252. if (this.isWSConnected()) {
  253. return;
  254. }
  255. if (this._wsInitPromise) {
  256. return this._wsInitPromise;
  257. }
  258. return new Promise((resolve, reject) => {
  259. this._wsReadySubsribers.push({
  260. resolve,
  261. reject
  262. });
  263. });
  264. };
  265. this.webLogin = async (envId, refresh) => {
  266. if (!refresh) {
  267. if (envId) {
  268. const loginInfo = this._logins.get(envId);
  269. if (loginInfo) {
  270. if (loginInfo.loggedIn && loginInfo.loginResult) {
  271. return loginInfo.loginResult;
  272. }
  273. else if (loginInfo.loggingInPromise) {
  274. return loginInfo.loggingInPromise;
  275. }
  276. }
  277. }
  278. else {
  279. const emptyEnvLoginInfo = this._logins.get('');
  280. if (emptyEnvLoginInfo && emptyEnvLoginInfo.loggingInPromise) {
  281. return emptyEnvLoginInfo.loggingInPromise;
  282. }
  283. }
  284. }
  285. const promise = new Promise(async (resolve, reject) => {
  286. try {
  287. const wsSign = await this.getWsSign();
  288. const msgData = {
  289. envId: wsSign.envId || '',
  290. accessToken: '',
  291. referrer: 'web',
  292. sdkVersion: '',
  293. dataVersion: __1.Db.dataVersion || ''
  294. };
  295. const loginMsg = {
  296. watchId: undefined,
  297. requestId: message_1.genRequestId(),
  298. msgType: 'LOGIN',
  299. msgData,
  300. exMsgData: {
  301. runtime: __1.Db.runtime,
  302. signStr: wsSign.signStr,
  303. secretVersion: wsSign.secretVersion
  304. }
  305. };
  306. const loginResMsg = await this.send({
  307. msg: loginMsg,
  308. waitResponse: true,
  309. skipOnMessage: true,
  310. timeout: DEFAULT_LOGIN_TIMEOUT
  311. });
  312. if (!loginResMsg.msgData.code) {
  313. resolve({
  314. envId: wsSign.envId
  315. });
  316. }
  317. else {
  318. reject(new Error(`${loginResMsg.msgData.code} ${loginResMsg.msgData.message}`));
  319. }
  320. }
  321. catch (e) {
  322. reject(e);
  323. }
  324. });
  325. let loginInfo = envId && this._logins.get(envId);
  326. const loginStartTS = Date.now();
  327. if (loginInfo) {
  328. loginInfo.loggedIn = false;
  329. loginInfo.loggingInPromise = promise;
  330. loginInfo.loginStartTS = loginStartTS;
  331. }
  332. else {
  333. loginInfo = {
  334. loggedIn: false,
  335. loggingInPromise: promise,
  336. loginStartTS
  337. };
  338. this._logins.set(envId || '', loginInfo);
  339. }
  340. try {
  341. const loginResult = await promise;
  342. const curLoginInfo = envId && this._logins.get(envId);
  343. if (curLoginInfo &&
  344. curLoginInfo === loginInfo &&
  345. curLoginInfo.loginStartTS === loginStartTS) {
  346. loginInfo.loggedIn = true;
  347. loginInfo.loggingInPromise = undefined;
  348. loginInfo.loginStartTS = undefined;
  349. loginInfo.loginResult = loginResult;
  350. return loginResult;
  351. }
  352. else if (curLoginInfo) {
  353. if (curLoginInfo.loggedIn && curLoginInfo.loginResult) {
  354. return curLoginInfo.loginResult;
  355. }
  356. else if (curLoginInfo.loggingInPromise) {
  357. return curLoginInfo.loggingInPromise;
  358. }
  359. else {
  360. throw new Error('ws unexpected login info');
  361. }
  362. }
  363. else {
  364. throw new Error('ws login info reset');
  365. }
  366. }
  367. catch (e) {
  368. loginInfo.loggedIn = false;
  369. loginInfo.loggingInPromise = undefined;
  370. loginInfo.loginStartTS = undefined;
  371. loginInfo.loginResult = undefined;
  372. throw e;
  373. }
  374. };
  375. this.getWsSign = async () => {
  376. if (this._wsSign && this._wsSign.expiredTs > Date.now()) {
  377. return this._wsSign;
  378. }
  379. const expiredTs = Date.now() + 60000;
  380. const res = await this._context.appConfig.request.send('auth.wsWebSign', { runtime: __1.Db.runtime });
  381. if (res.code) {
  382. throw new Error(`[tcb-js-sdk] 获取实时数据推送登录票据失败: ${res.code}`);
  383. }
  384. if (res.data) {
  385. const { signStr, wsUrl, secretVersion, envId } = res.data;
  386. return {
  387. signStr,
  388. wsUrl,
  389. secretVersion,
  390. envId,
  391. expiredTs
  392. };
  393. }
  394. else {
  395. throw new Error('[tcb-js-sdk] 获取实时数据推送登录票据失败');
  396. }
  397. };
  398. this.getWaitExpectedTimeoutLength = () => {
  399. if (!this._rttObserved.length) {
  400. return DEFAULT_EXPECTED_EVENT_WAIT_TIME;
  401. }
  402. return ((this._rttObserved.reduce((acc, cur) => acc + cur) /
  403. this._rttObserved.length) *
  404. 1.5);
  405. };
  406. this.ping = async () => {
  407. const msg = {
  408. watchId: undefined,
  409. requestId: message_1.genRequestId(),
  410. msgType: 'PING',
  411. msgData: null
  412. };
  413. await this.send({
  414. msg
  415. });
  416. };
  417. this.send = async (opts) => new Promise(async (_resolve, _reject) => {
  418. let timeoutId;
  419. let _hasResolved = false;
  420. let _hasRejected = false;
  421. const resolve = (value) => {
  422. _hasResolved = true;
  423. timeoutId && clearTimeout(timeoutId);
  424. _resolve(value);
  425. };
  426. const reject = (error) => {
  427. _hasRejected = true;
  428. timeoutId && clearTimeout(timeoutId);
  429. _reject(error);
  430. };
  431. if (opts.timeout) {
  432. timeoutId = setTimeout(async () => {
  433. if (!_hasResolved || !_hasRejected) {
  434. await utils_1.sleep(0);
  435. if (!_hasResolved || !_hasRejected) {
  436. reject(new error_1.TimeoutError('wsclient.send timedout'));
  437. }
  438. }
  439. }, opts.timeout);
  440. }
  441. try {
  442. if (this._wsInitPromise) {
  443. await this._wsInitPromise;
  444. }
  445. if (!this._ws) {
  446. reject(new Error('invalid state: ws connection not exists, can not send message'));
  447. return;
  448. }
  449. if (this._ws.readyState !== WS_READY_STATE.OPEN) {
  450. reject(new Error(`ws readyState invalid: ${this._ws.readyState}, can not send message`));
  451. return;
  452. }
  453. if (opts.waitResponse) {
  454. this._wsResponseWait.set(opts.msg.requestId, {
  455. resolve,
  456. reject,
  457. skipOnMessage: opts.skipOnMessage
  458. });
  459. }
  460. try {
  461. await this._ws.send(JSON.stringify(opts.msg));
  462. if (!opts.waitResponse) {
  463. resolve();
  464. }
  465. }
  466. catch (err) {
  467. if (err) {
  468. reject(err);
  469. if (opts.waitResponse) {
  470. this._wsResponseWait.delete(opts.msg.requestId);
  471. }
  472. }
  473. }
  474. }
  475. catch (e) {
  476. reject(e);
  477. }
  478. });
  479. this.closeAllClients = (error) => {
  480. this._virtualWSClient.forEach(client => {
  481. client.closeWithError(error);
  482. });
  483. };
  484. this.pauseClients = (clients) => {
  485. ;
  486. (clients || this._virtualWSClient).forEach(client => {
  487. client.pause();
  488. });
  489. };
  490. this.resumeClients = (clients) => {
  491. ;
  492. (clients || this._virtualWSClient).forEach(client => {
  493. client.resume();
  494. });
  495. };
  496. this.onWatchStart = (client, queryID) => {
  497. this._queryIdClientMap.set(queryID, client);
  498. };
  499. this.onWatchClose = (client, queryID) => {
  500. if (queryID) {
  501. this._queryIdClientMap.delete(queryID);
  502. }
  503. this._watchIdClientMap.delete(client.watchId);
  504. this._virtualWSClient.delete(client);
  505. if (!this._virtualWSClient.size) {
  506. this.close(ws_event_1.CLOSE_EVENT_CODE.NoRealtimeListeners);
  507. }
  508. };
  509. this._maxReconnect = options.maxReconnect || DEFAULT_MAX_RECONNECT;
  510. this._reconnectInterval =
  511. options.reconnectInterval || DEFAULT_WS_RECONNECT_INTERVAL;
  512. this._context = options.context;
  513. }
  514. heartbeat(immediate) {
  515. this.clearHeartbeat();
  516. this._pingTimeoutId = setTimeout(async () => {
  517. try {
  518. if (!this._ws || this._ws.readyState !== WS_READY_STATE.OPEN) {
  519. return;
  520. }
  521. this._lastPingSendTS = Date.now();
  522. await this.ping();
  523. this._pingFailed = 0;
  524. this._pongTimeoutId = setTimeout(() => {
  525. console.error('pong timed out');
  526. if (this._pongMissed < DEFAULT_PONG_MISS_TOLERANCE) {
  527. this._pongMissed++;
  528. this.heartbeat(true);
  529. }
  530. else {
  531. this.initWebSocketConnection(true);
  532. }
  533. }, this._context.appConfig.realtimePongWaitTimeout);
  534. }
  535. catch (e) {
  536. if (this._pingFailed < DEFAULT_PING_FAIL_TOLERANCE) {
  537. this._pingFailed++;
  538. this.heartbeat();
  539. }
  540. else {
  541. this.close(ws_event_1.CLOSE_EVENT_CODE.HeartbeatPingError);
  542. }
  543. }
  544. }, immediate ? 0 : this._context.appConfig.realtimePingInterval);
  545. }
  546. clearHeartbeat() {
  547. this._pingTimeoutId && clearTimeout(this._pingTimeoutId);
  548. this._pongTimeoutId && clearTimeout(this._pongTimeoutId);
  549. }
  550. close(code) {
  551. this.clearHeartbeat();
  552. if (this._ws) {
  553. this._ws.close(code, ws_event_1.CLOSE_EVENT_CODE_INFO[code].name);
  554. this._ws = undefined;
  555. }
  556. }
  557. watch(options) {
  558. if (!this._ws && !this._wsInitPromise) {
  559. this.initWebSocketConnection(false);
  560. }
  561. const virtualClient = new virtual_websocket_client_1.VirtualWebSocketClient(Object.assign(Object.assign({}, options), { send: this.send, login: this.webLogin, isWSConnected: this.isWSConnected, onceWSConnected: this.onceWSConnected, getWaitExpectedTimeoutLength: this.getWaitExpectedTimeoutLength, onWatchStart: this.onWatchStart, onWatchClose: this.onWatchClose, debug: true }));
  562. this._virtualWSClient.add(virtualClient);
  563. this._watchIdClientMap.set(virtualClient.watchId, virtualClient);
  564. return virtualClient.listener;
  565. }
  566. }
  567. exports.RealtimeWebSocketClient = RealtimeWebSocketClient;