virtual-websocket-client.js 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. const lodash_set_1 = require("lodash.set");
  4. const lodash_unset_1 = require("lodash.unset");
  5. const lodash_clonedeep_1 = require("lodash.clonedeep");
  6. const message_1 = require("./message");
  7. const error_1 = require("../utils/error");
  8. const error_config_1 = require("../config/error.config");
  9. const utils_1 = require("../utils/utils");
  10. const listener_1 = require("./listener");
  11. const snapshot_1 = require("./snapshot");
  12. const error_2 = require("./error");
  13. var WATCH_STATUS;
  14. (function (WATCH_STATUS) {
  15. WATCH_STATUS["LOGGINGIN"] = "LOGGINGIN";
  16. WATCH_STATUS["INITING"] = "INITING";
  17. WATCH_STATUS["REBUILDING"] = "REBUILDING";
  18. WATCH_STATUS["ACTIVE"] = "ACTIVE";
  19. WATCH_STATUS["ERRORED"] = "ERRORED";
  20. WATCH_STATUS["CLOSING"] = "CLOSING";
  21. WATCH_STATUS["CLOSED"] = "CLOSED";
  22. WATCH_STATUS["PAUSED"] = "PAUSED";
  23. WATCH_STATUS["RESUMING"] = "RESUMING";
  24. })(WATCH_STATUS || (WATCH_STATUS = {}));
  25. const DEFAULT_WAIT_TIME_ON_UNKNOWN_ERROR = 100;
  26. const DEFAULT_MAX_AUTO_RETRY_ON_ERROR = 2;
  27. const DEFAULT_MAX_SEND_ACK_AUTO_RETRY_ON_ERROR = 2;
  28. const DEFAULT_SEND_ACK_DEBOUNCE_TIMEOUT = 10 * 1000;
  29. const DEFAULT_INIT_WATCH_TIMEOUT = 10 * 1000;
  30. const DEFAULT_REBUILD_WATCH_TIMEOUT = 10 * 1000;
  31. class VirtualWebSocketClient {
  32. constructor(options) {
  33. this.watchStatus = WATCH_STATUS.INITING;
  34. this._login = async (envId, refresh) => {
  35. this.watchStatus = WATCH_STATUS.LOGGINGIN;
  36. const loginResult = await this.login(envId, refresh);
  37. if (!this.envId) {
  38. this.envId = loginResult.envId;
  39. }
  40. return loginResult;
  41. };
  42. this.initWatch = async (forceRefreshLogin) => {
  43. if (this._initWatchPromise) {
  44. return this._initWatchPromise;
  45. }
  46. this._initWatchPromise = new Promise(async (resolve, reject) => {
  47. try {
  48. if (this.watchStatus === WATCH_STATUS.PAUSED) {
  49. console.log('[realtime] initWatch cancelled on pause');
  50. return resolve();
  51. }
  52. const { envId } = await this._login(this.envId, forceRefreshLogin);
  53. if (this.watchStatus === WATCH_STATUS.PAUSED) {
  54. console.log('[realtime] initWatch cancelled on pause');
  55. return resolve();
  56. }
  57. this.watchStatus = WATCH_STATUS.INITING;
  58. const initWatchMsg = {
  59. watchId: this.watchId,
  60. requestId: message_1.genRequestId(),
  61. msgType: 'INIT_WATCH',
  62. msgData: {
  63. envId,
  64. collName: this.collectionName,
  65. query: this.query,
  66. limit: this.limit,
  67. orderBy: this.orderBy
  68. }
  69. };
  70. const initEventMsg = await this.send({
  71. msg: initWatchMsg,
  72. waitResponse: true,
  73. skipOnMessage: true,
  74. timeout: DEFAULT_INIT_WATCH_TIMEOUT
  75. });
  76. const { events, currEvent } = initEventMsg.msgData;
  77. this.sessionInfo = {
  78. queryID: initEventMsg.msgData.queryID,
  79. currentEventId: currEvent - 1,
  80. currentDocs: []
  81. };
  82. if (events.length > 0) {
  83. for (const e of events) {
  84. e.ID = currEvent;
  85. }
  86. this.handleServerEvents(initEventMsg);
  87. }
  88. else {
  89. this.sessionInfo.currentEventId = currEvent;
  90. const snapshot = new snapshot_1.Snapshot({
  91. id: currEvent,
  92. docChanges: [],
  93. docs: [],
  94. type: 'init'
  95. });
  96. this.listener.onChange(snapshot);
  97. this.scheduleSendACK();
  98. }
  99. this.onWatchStart(this, this.sessionInfo.queryID);
  100. this.watchStatus = WATCH_STATUS.ACTIVE;
  101. this._availableRetries.INIT_WATCH = DEFAULT_MAX_AUTO_RETRY_ON_ERROR;
  102. resolve();
  103. }
  104. catch (e) {
  105. this.handleWatchEstablishmentError(e, {
  106. operationName: 'INIT_WATCH',
  107. resolve,
  108. reject
  109. });
  110. }
  111. });
  112. let success = false;
  113. try {
  114. await this._initWatchPromise;
  115. success = true;
  116. }
  117. finally {
  118. this._initWatchPromise = undefined;
  119. }
  120. console.log(`[realtime] initWatch ${success ? 'success' : 'fail'}`);
  121. };
  122. this.rebuildWatch = async (forceRefreshLogin) => {
  123. if (this._rebuildWatchPromise) {
  124. return this._rebuildWatchPromise;
  125. }
  126. this._rebuildWatchPromise = new Promise(async (resolve, reject) => {
  127. try {
  128. if (this.watchStatus === WATCH_STATUS.PAUSED) {
  129. console.log('[realtime] rebuildWatch cancelled on pause');
  130. return resolve();
  131. }
  132. const { envId } = await this._login(this.envId, forceRefreshLogin);
  133. if (!this.sessionInfo) {
  134. throw new Error('can not rebuildWatch without a successful initWatch (lack of sessionInfo)');
  135. }
  136. if (this.watchStatus === WATCH_STATUS.PAUSED) {
  137. console.log('[realtime] rebuildWatch cancelled on pause');
  138. return resolve();
  139. }
  140. this.watchStatus = WATCH_STATUS.REBUILDING;
  141. const rebuildWatchMsg = {
  142. watchId: this.watchId,
  143. requestId: message_1.genRequestId(),
  144. msgType: 'REBUILD_WATCH',
  145. msgData: {
  146. envId,
  147. collName: this.collectionName,
  148. queryID: this.sessionInfo.queryID,
  149. eventID: this.sessionInfo.currentEventId
  150. }
  151. };
  152. const nextEventMsg = await this.send({
  153. msg: rebuildWatchMsg,
  154. waitResponse: true,
  155. skipOnMessage: false,
  156. timeout: DEFAULT_REBUILD_WATCH_TIMEOUT
  157. });
  158. this.handleServerEvents(nextEventMsg);
  159. this.watchStatus = WATCH_STATUS.ACTIVE;
  160. this._availableRetries.REBUILD_WATCH = DEFAULT_MAX_AUTO_RETRY_ON_ERROR;
  161. resolve();
  162. }
  163. catch (e) {
  164. this.handleWatchEstablishmentError(e, {
  165. operationName: 'REBUILD_WATCH',
  166. resolve,
  167. reject
  168. });
  169. }
  170. });
  171. let success = false;
  172. try {
  173. await this._rebuildWatchPromise;
  174. success = true;
  175. }
  176. finally {
  177. this._rebuildWatchPromise = undefined;
  178. }
  179. console.log(`[realtime] rebuildWatch ${success ? 'success' : 'fail'}`);
  180. };
  181. this.handleWatchEstablishmentError = async (e, options) => {
  182. const isInitWatch = options.operationName === 'INIT_WATCH';
  183. const abortWatch = () => {
  184. this.closeWithError(new error_1.CloudSDKError({
  185. errCode: isInitWatch
  186. ? error_config_1.ERR_CODE.SDK_DATABASE_REALTIME_LISTENER_INIT_WATCH_FAIL
  187. : error_config_1.ERR_CODE.SDK_DATABASE_REALTIME_LISTENER_REBUILD_WATCH_FAIL,
  188. errMsg: e
  189. }));
  190. options.reject(e);
  191. };
  192. const retry = (refreshLogin) => {
  193. if (this.useRetryTicket(options.operationName)) {
  194. if (isInitWatch) {
  195. this._initWatchPromise = undefined;
  196. options.resolve(this.initWatch(refreshLogin));
  197. }
  198. else {
  199. this._rebuildWatchPromise = undefined;
  200. options.resolve(this.rebuildWatch(refreshLogin));
  201. }
  202. }
  203. else {
  204. abortWatch();
  205. }
  206. };
  207. this.handleCommonError(e, {
  208. onSignError: () => retry(true),
  209. onTimeoutError: () => retry(false),
  210. onNotRetryableError: abortWatch,
  211. onCancelledError: options.reject,
  212. onUnknownError: async () => {
  213. try {
  214. const onWSDisconnected = async () => {
  215. this.pause();
  216. await this.onceWSConnected();
  217. retry(true);
  218. };
  219. if (!this.isWSConnected()) {
  220. await onWSDisconnected();
  221. }
  222. else {
  223. await utils_1.sleep(DEFAULT_WAIT_TIME_ON_UNKNOWN_ERROR);
  224. if (this.watchStatus === WATCH_STATUS.PAUSED) {
  225. options.reject(new error_1.CancelledError(`${options.operationName} cancelled due to pause after unknownError`));
  226. }
  227. else if (!this.isWSConnected()) {
  228. await onWSDisconnected();
  229. }
  230. else {
  231. retry(false);
  232. }
  233. }
  234. }
  235. catch (e) {
  236. retry(true);
  237. }
  238. }
  239. });
  240. };
  241. this.closeWatch = async () => {
  242. const queryId = this.sessionInfo ? this.sessionInfo.queryID : '';
  243. if (this.watchStatus !== WATCH_STATUS.ACTIVE) {
  244. this.watchStatus = WATCH_STATUS.CLOSED;
  245. this.onWatchClose(this, queryId);
  246. return;
  247. }
  248. try {
  249. this.watchStatus = WATCH_STATUS.CLOSING;
  250. const closeWatchMsg = {
  251. watchId: this.watchId,
  252. requestId: message_1.genRequestId(),
  253. msgType: 'CLOSE_WATCH',
  254. msgData: null
  255. };
  256. await this.send({
  257. msg: closeWatchMsg
  258. });
  259. this.sessionInfo = undefined;
  260. this.watchStatus = WATCH_STATUS.CLOSED;
  261. }
  262. catch (e) {
  263. this.closeWithError(new error_1.CloudSDKError({
  264. errCode: error_config_1.ERR_CODE.SDK_DATABASE_REALTIME_LISTENER_CLOSE_WATCH_FAIL,
  265. errMsg: e
  266. }));
  267. }
  268. finally {
  269. this.onWatchClose(this, queryId);
  270. }
  271. };
  272. this.scheduleSendACK = () => {
  273. this.clearACKSchedule();
  274. this._ackTimeoutId = setTimeout(() => {
  275. if (this._waitExpectedTimeoutId) {
  276. this.scheduleSendACK();
  277. }
  278. else {
  279. this.sendACK();
  280. }
  281. }, DEFAULT_SEND_ACK_DEBOUNCE_TIMEOUT);
  282. };
  283. this.clearACKSchedule = () => {
  284. if (this._ackTimeoutId) {
  285. clearTimeout(this._ackTimeoutId);
  286. }
  287. };
  288. this.sendACK = async () => {
  289. try {
  290. if (this.watchStatus !== WATCH_STATUS.ACTIVE) {
  291. this.scheduleSendACK();
  292. return;
  293. }
  294. if (!this.sessionInfo) {
  295. console.warn('[realtime listener] can not send ack without a successful initWatch (lack of sessionInfo)');
  296. return;
  297. }
  298. const ackMsg = {
  299. watchId: this.watchId,
  300. requestId: message_1.genRequestId(),
  301. msgType: 'CHECK_LAST',
  302. msgData: {
  303. queryID: this.sessionInfo.queryID,
  304. eventID: this.sessionInfo.currentEventId
  305. }
  306. };
  307. await this.send({
  308. msg: ackMsg
  309. });
  310. this.scheduleSendACK();
  311. }
  312. catch (e) {
  313. if (error_2.isRealtimeErrorMessageError(e)) {
  314. const msg = e.payload;
  315. switch (msg.msgData.code) {
  316. case 'CHECK_LOGIN_FAILED':
  317. case 'SIGN_EXPIRED_ERROR':
  318. case 'SIGN_INVALID_ERROR':
  319. case 'SIGN_PARAM_INVALID': {
  320. this.rebuildWatch();
  321. return;
  322. }
  323. case 'QUERYID_INVALID_ERROR':
  324. case 'SYS_ERR':
  325. case 'INVALIID_ENV':
  326. case 'COLLECTION_PERMISSION_DENIED': {
  327. this.closeWithError(new error_1.CloudSDKError({
  328. errCode: error_config_1.ERR_CODE.SDK_DATABASE_REALTIME_LISTENER_CHECK_LAST_FAIL,
  329. errMsg: msg.msgData.code
  330. }));
  331. return;
  332. }
  333. default: {
  334. break;
  335. }
  336. }
  337. }
  338. if (this._availableRetries.CHECK_LAST &&
  339. this._availableRetries.CHECK_LAST > 0) {
  340. this._availableRetries.CHECK_LAST--;
  341. this.scheduleSendACK();
  342. }
  343. else {
  344. this.closeWithError(new error_1.CloudSDKError({
  345. errCode: error_config_1.ERR_CODE.SDK_DATABASE_REALTIME_LISTENER_CHECK_LAST_FAIL,
  346. errMsg: e
  347. }));
  348. }
  349. }
  350. };
  351. this.handleCommonError = (e, options) => {
  352. if (error_2.isRealtimeErrorMessageError(e)) {
  353. const msg = e.payload;
  354. switch (msg.msgData.code) {
  355. case 'CHECK_LOGIN_FAILED':
  356. case 'SIGN_EXPIRED_ERROR':
  357. case 'SIGN_INVALID_ERROR':
  358. case 'SIGN_PARAM_INVALID': {
  359. options.onSignError(e);
  360. return;
  361. }
  362. case 'QUERYID_INVALID_ERROR':
  363. case 'SYS_ERR':
  364. case 'INVALIID_ENV':
  365. case 'COLLECTION_PERMISSION_DENIED': {
  366. options.onNotRetryableError(e);
  367. return;
  368. }
  369. default: {
  370. options.onNotRetryableError(e);
  371. return;
  372. }
  373. }
  374. }
  375. else if (error_1.isTimeoutError(e)) {
  376. options.onTimeoutError(e);
  377. return;
  378. }
  379. else if (error_1.isCancelledError(e)) {
  380. options.onCancelledError(e);
  381. return;
  382. }
  383. options.onUnknownError(e);
  384. };
  385. this.watchId = `watchid_${+new Date()}_${Math.random()}`;
  386. this.envId = options.envId;
  387. this.collectionName = options.collectionName;
  388. this.query = options.query;
  389. this.limit = options.limit;
  390. this.orderBy = options.orderBy;
  391. this.send = options.send;
  392. this.login = options.login;
  393. this.isWSConnected = options.isWSConnected;
  394. this.onceWSConnected = options.onceWSConnected;
  395. this.getWaitExpectedTimeoutLength = options.getWaitExpectedTimeoutLength;
  396. this.onWatchStart = options.onWatchStart;
  397. this.onWatchClose = options.onWatchClose;
  398. this.debug = options.debug;
  399. this._availableRetries = {
  400. INIT_WATCH: DEFAULT_MAX_AUTO_RETRY_ON_ERROR,
  401. REBUILD_WATCH: DEFAULT_MAX_AUTO_RETRY_ON_ERROR,
  402. CHECK_LAST: DEFAULT_MAX_SEND_ACK_AUTO_RETRY_ON_ERROR
  403. };
  404. this.listener = new listener_1.RealtimeListener({
  405. close: this.closeWatch,
  406. onChange: options.onChange,
  407. onError: options.onError,
  408. debug: this.debug,
  409. virtualClient: this
  410. });
  411. this.initWatch();
  412. }
  413. useRetryTicket(operationName) {
  414. if (this._availableRetries[operationName] &&
  415. this._availableRetries[operationName] > 0) {
  416. this._availableRetries[operationName]--;
  417. console.log(`[realtime] ${operationName} use a retry ticket, now only ${this._availableRetries[operationName]} retry left`);
  418. return true;
  419. }
  420. return false;
  421. }
  422. async handleServerEvents(msg) {
  423. try {
  424. this.scheduleSendACK();
  425. await this._handleServerEvents(msg);
  426. this._postHandleServerEventsValidityCheck(msg);
  427. }
  428. catch (e) {
  429. console.error('[realtime listener] internal non-fatal error: handle server events failed with error: ', e);
  430. throw e;
  431. }
  432. }
  433. async _handleServerEvents(msg) {
  434. const { requestId } = msg;
  435. const { events } = msg.msgData;
  436. const { msgType } = msg;
  437. if (!events.length || !this.sessionInfo) {
  438. return;
  439. }
  440. const sessionInfo = this.sessionInfo;
  441. let allChangeEvents;
  442. try {
  443. allChangeEvents = events.map(getPublicEvent);
  444. }
  445. catch (e) {
  446. this.closeWithError(new error_1.CloudSDKError({
  447. errCode: error_config_1.ERR_CODE.SDK_DATABASE_REALTIME_LISTENER_RECEIVE_INVALID_SERVER_DATA,
  448. errMsg: e
  449. }));
  450. return;
  451. }
  452. let docs = [...sessionInfo.currentDocs];
  453. let initEncountered = false;
  454. for (let i = 0, len = allChangeEvents.length; i < len; i++) {
  455. const change = allChangeEvents[i];
  456. if (sessionInfo.currentEventId >= change.id) {
  457. if (!allChangeEvents[i - 1] || change.id > allChangeEvents[i - 1].id) {
  458. console.warn(`[realtime] duplicate event received, cur ${sessionInfo.currentEventId} but got ${change.id}`);
  459. }
  460. else {
  461. console.error(`[realtime listener] server non-fatal error: events out of order (the latter event's id is smaller than that of the former) (requestId ${requestId})`);
  462. }
  463. continue;
  464. }
  465. else if (sessionInfo.currentEventId === change.id - 1) {
  466. switch (change.dataType) {
  467. case 'update': {
  468. if (!change.doc) {
  469. switch (change.queueType) {
  470. case 'update':
  471. case 'dequeue': {
  472. const localDoc = docs.find(doc => doc._id === change.docId);
  473. if (localDoc) {
  474. const doc = lodash_clonedeep_1.default(localDoc);
  475. if (change.updatedFields) {
  476. for (const fieldPath in change.updatedFields) {
  477. lodash_set_1.default(doc, fieldPath, change.updatedFields[fieldPath]);
  478. }
  479. }
  480. if (change.removedFields) {
  481. for (const fieldPath of change.removedFields) {
  482. lodash_unset_1.default(doc, fieldPath);
  483. }
  484. }
  485. change.doc = doc;
  486. }
  487. else {
  488. console.error('[realtime listener] internal non-fatal server error: unexpected update dataType event where no doc is associated.');
  489. }
  490. break;
  491. }
  492. case 'enqueue': {
  493. const err = new error_1.CloudSDKError({
  494. errCode: error_config_1.ERR_CODE.SDK_DATABASE_REALTIME_LISTENER_UNEXPECTED_FATAL_ERROR,
  495. errMsg: `HandleServerEvents: full doc is not provided with dataType="update" and queueType="enqueue" (requestId ${msg.requestId})`
  496. });
  497. this.closeWithError(err);
  498. throw err;
  499. }
  500. default: {
  501. break;
  502. }
  503. }
  504. }
  505. break;
  506. }
  507. case 'replace': {
  508. if (!change.doc) {
  509. const err = new error_1.CloudSDKError({
  510. errCode: error_config_1.ERR_CODE.SDK_DATABASE_REALTIME_LISTENER_UNEXPECTED_FATAL_ERROR,
  511. errMsg: `HandleServerEvents: full doc is not provided with dataType="replace" (requestId ${msg.requestId})`
  512. });
  513. this.closeWithError(err);
  514. throw err;
  515. }
  516. break;
  517. }
  518. case 'remove': {
  519. const doc = docs.find(doc => doc._id === change.docId);
  520. if (doc) {
  521. change.doc = doc;
  522. }
  523. else {
  524. console.error('[realtime listener] internal non-fatal server error: unexpected remove event where no doc is associated.');
  525. }
  526. break;
  527. }
  528. case 'limit': {
  529. if (!change.doc) {
  530. switch (change.queueType) {
  531. case 'dequeue': {
  532. const doc = docs.find(doc => doc._id === change.docId);
  533. if (doc) {
  534. change.doc = doc;
  535. }
  536. else {
  537. console.error('[realtime listener] internal non-fatal server error: unexpected limit dataType event where no doc is associated.');
  538. }
  539. break;
  540. }
  541. case 'enqueue': {
  542. const err = new error_1.CloudSDKError({
  543. errCode: error_config_1.ERR_CODE.SDK_DATABASE_REALTIME_LISTENER_UNEXPECTED_FATAL_ERROR,
  544. errMsg: `HandleServerEvents: full doc is not provided with dataType="limit" and queueType="enqueue" (requestId ${msg.requestId})`
  545. });
  546. this.closeWithError(err);
  547. throw err;
  548. }
  549. default: {
  550. break;
  551. }
  552. }
  553. }
  554. break;
  555. }
  556. }
  557. switch (change.queueType) {
  558. case 'init': {
  559. if (!initEncountered) {
  560. initEncountered = true;
  561. docs = [change.doc];
  562. }
  563. else {
  564. docs.push(change.doc);
  565. }
  566. break;
  567. }
  568. case 'enqueue': {
  569. docs.push(change.doc);
  570. break;
  571. }
  572. case 'dequeue': {
  573. const ind = docs.findIndex(doc => doc._id === change.docId);
  574. if (ind > -1) {
  575. docs.splice(ind, 1);
  576. }
  577. else {
  578. console.error('[realtime listener] internal non-fatal server error: unexpected dequeue event where no doc is associated.');
  579. }
  580. break;
  581. }
  582. case 'update': {
  583. const ind = docs.findIndex(doc => doc._id === change.docId);
  584. if (ind > -1) {
  585. docs[ind] = change.doc;
  586. }
  587. else {
  588. console.error('[realtime listener] internal non-fatal server error: unexpected queueType update event where no doc is associated.');
  589. }
  590. break;
  591. }
  592. }
  593. if (i === len - 1 ||
  594. (allChangeEvents[i + 1] && allChangeEvents[i + 1].id !== change.id)) {
  595. const docsSnapshot = [...docs];
  596. const docChanges = allChangeEvents
  597. .slice(0, i + 1)
  598. .filter(c => c.id === change.id);
  599. this.sessionInfo.currentEventId = change.id;
  600. this.sessionInfo.currentDocs = docs;
  601. const snapshot = new snapshot_1.Snapshot({
  602. id: change.id,
  603. docChanges,
  604. docs: docsSnapshot,
  605. msgType
  606. });
  607. this.listener.onChange(snapshot);
  608. }
  609. }
  610. else {
  611. console.warn(`[realtime listener] event received is out of order, cur ${this.sessionInfo.currentEventId} but got ${change.id}`);
  612. await this.rebuildWatch();
  613. return;
  614. }
  615. }
  616. }
  617. _postHandleServerEventsValidityCheck(msg) {
  618. if (!this.sessionInfo) {
  619. console.error('[realtime listener] internal non-fatal error: sessionInfo lost after server event handling, this should never occur');
  620. return;
  621. }
  622. if (this.sessionInfo.expectEventId &&
  623. this.sessionInfo.currentEventId >= this.sessionInfo.expectEventId) {
  624. this.clearWaitExpectedEvent();
  625. }
  626. if (this.sessionInfo.currentEventId < msg.msgData.currEvent) {
  627. console.warn('[realtime listener] internal non-fatal error: client eventId does not match with server event id after server event handling');
  628. return;
  629. }
  630. }
  631. clearWaitExpectedEvent() {
  632. if (this._waitExpectedTimeoutId) {
  633. clearTimeout(this._waitExpectedTimeoutId);
  634. this._waitExpectedTimeoutId = undefined;
  635. }
  636. }
  637. onMessage(msg) {
  638. switch (this.watchStatus) {
  639. case WATCH_STATUS.PAUSED: {
  640. if (msg.msgType !== 'ERROR') {
  641. return;
  642. }
  643. break;
  644. }
  645. case WATCH_STATUS.LOGGINGIN:
  646. case WATCH_STATUS.INITING:
  647. case WATCH_STATUS.REBUILDING: {
  648. console.warn(`[realtime listener] internal non-fatal error: unexpected message received while ${this.watchStatus}`);
  649. return;
  650. }
  651. case WATCH_STATUS.CLOSED: {
  652. console.warn('[realtime listener] internal non-fatal error: unexpected message received when the watch has closed');
  653. return;
  654. }
  655. case WATCH_STATUS.ERRORED: {
  656. console.warn('[realtime listener] internal non-fatal error: unexpected message received when the watch has ended with error');
  657. return;
  658. }
  659. }
  660. if (!this.sessionInfo) {
  661. console.warn('[realtime listener] internal non-fatal error: sessionInfo not found while message is received.');
  662. return;
  663. }
  664. this.scheduleSendACK();
  665. switch (msg.msgType) {
  666. case 'NEXT_EVENT': {
  667. console.warn(`nextevent ${msg.msgData.currEvent} ignored`, msg);
  668. this.handleServerEvents(msg);
  669. break;
  670. }
  671. case 'CHECK_EVENT': {
  672. if (this.sessionInfo.currentEventId < msg.msgData.currEvent) {
  673. this.sessionInfo.expectEventId = msg.msgData.currEvent;
  674. this.clearWaitExpectedEvent();
  675. this._waitExpectedTimeoutId = setTimeout(() => {
  676. this.rebuildWatch();
  677. }, this.getWaitExpectedTimeoutLength());
  678. console.log(`[realtime] waitExpectedTimeoutLength ${this.getWaitExpectedTimeoutLength()}`);
  679. }
  680. break;
  681. }
  682. case 'ERROR': {
  683. this.closeWithError(new error_1.CloudSDKError({
  684. errCode: error_config_1.ERR_CODE.SDK_DATABASE_REALTIME_LISTENER_SERVER_ERROR_MSG,
  685. errMsg: `${msg.msgData.code} - ${msg.msgData.message}`
  686. }));
  687. break;
  688. }
  689. default: {
  690. console.warn(`[realtime listener] virtual client receive unexpected msg ${msg.msgType}: `, msg);
  691. break;
  692. }
  693. }
  694. }
  695. closeWithError(error) {
  696. this.watchStatus = WATCH_STATUS.ERRORED;
  697. this.clearACKSchedule();
  698. this.listener.onError(error);
  699. this.onWatchClose(this, (this.sessionInfo && this.sessionInfo.queryID) || '');
  700. console.log(`[realtime] client closed (${this.collectionName} ${this.query}) (watchId ${this.watchId})`);
  701. }
  702. pause() {
  703. this.watchStatus = WATCH_STATUS.PAUSED;
  704. console.log(`[realtime] client paused (${this.collectionName} ${this.query}) (watchId ${this.watchId})`);
  705. }
  706. async resume() {
  707. this.watchStatus = WATCH_STATUS.RESUMING;
  708. console.log(`[realtime] client resuming with ${this.sessionInfo ? 'REBUILD_WATCH' : 'INIT_WATCH'} (${this.collectionName} ${this.query}) (${this.watchId})`);
  709. try {
  710. await (this.sessionInfo ? this.rebuildWatch() : this.initWatch());
  711. console.log(`[realtime] client successfully resumed (${this.collectionName} ${this.query}) (${this.watchId})`);
  712. }
  713. catch (e) {
  714. console.error(`[realtime] client resume failed (${this.collectionName} ${this.query}) (${this.watchId})`, e);
  715. }
  716. }
  717. }
  718. exports.VirtualWebSocketClient = VirtualWebSocketClient;
  719. function getPublicEvent(event) {
  720. const e = {
  721. id: event.ID,
  722. dataType: event.DataType,
  723. queueType: event.QueueType,
  724. docId: event.DocID,
  725. doc: event.Doc && event.Doc !== '{}' ? JSON.parse(event.Doc) : undefined
  726. };
  727. if (event.DataType === 'update') {
  728. if (event.UpdatedFields) {
  729. e.updatedFields = JSON.parse(event.UpdatedFields);
  730. }
  731. if (event.removedFields || event.RemovedFields) {
  732. e.removedFields = JSON.parse(event.removedFields);
  733. }
  734. }
  735. return e;
  736. }