LiveQueryClient.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507
  1. "use strict";
  2. var _Object$defineProperty = require("@babel/runtime-corejs3/core-js-stable/object/define-property");
  3. var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");
  4. _Object$defineProperty(exports, "__esModule", {
  5. value: true
  6. });
  7. exports.default = void 0;
  8. var _indexOf = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/index-of"));
  9. var _map = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/map"));
  10. var _keys = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/keys"));
  11. var _stringify = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/json/stringify"));
  12. var _forEach = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/for-each"));
  13. var _values = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/values"));
  14. var _setTimeout2 = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/set-timeout"));
  15. var _bind = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/bind"));
  16. var _defineProperty2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/defineProperty"));
  17. var _CoreManager = _interopRequireDefault(require("./CoreManager"));
  18. var _ParseObject = _interopRequireDefault(require("./ParseObject"));
  19. var _LiveQuerySubscription = _interopRequireDefault(require("./LiveQuerySubscription"));
  20. var _promiseUtils = require("./promiseUtils");
  21. var _ParseError = _interopRequireDefault(require("./ParseError"));
  22. // The LiveQuery client inner state
  23. const CLIENT_STATE = {
  24. INITIALIZED: 'initialized',
  25. CONNECTING: 'connecting',
  26. CONNECTED: 'connected',
  27. CLOSED: 'closed',
  28. RECONNECTING: 'reconnecting',
  29. DISCONNECTED: 'disconnected'
  30. };
  31. // The event type the LiveQuery client should sent to server
  32. const OP_TYPES = {
  33. CONNECT: 'connect',
  34. SUBSCRIBE: 'subscribe',
  35. UNSUBSCRIBE: 'unsubscribe',
  36. ERROR: 'error'
  37. };
  38. // The event we get back from LiveQuery server
  39. const OP_EVENTS = {
  40. CONNECTED: 'connected',
  41. SUBSCRIBED: 'subscribed',
  42. UNSUBSCRIBED: 'unsubscribed',
  43. ERROR: 'error',
  44. CREATE: 'create',
  45. UPDATE: 'update',
  46. ENTER: 'enter',
  47. LEAVE: 'leave',
  48. DELETE: 'delete'
  49. };
  50. // The event the LiveQuery client should emit
  51. const CLIENT_EMMITER_TYPES = {
  52. CLOSE: 'close',
  53. ERROR: 'error',
  54. OPEN: 'open'
  55. };
  56. // The event the LiveQuery subscription should emit
  57. const SUBSCRIPTION_EMMITER_TYPES = {
  58. OPEN: 'open',
  59. CLOSE: 'close',
  60. ERROR: 'error',
  61. CREATE: 'create',
  62. UPDATE: 'update',
  63. ENTER: 'enter',
  64. LEAVE: 'leave',
  65. DELETE: 'delete'
  66. };
  67. // Exponentially-growing random delay
  68. const generateInterval = k => {
  69. return Math.random() * Math.min(30, Math.pow(2, k) - 1) * 1000;
  70. };
  71. /**
  72. * Creates a new LiveQueryClient.
  73. * <a href="https://nodejs.org/api/events.html#events_class_eventemitter">cloud functions</a>.
  74. *
  75. * A wrapper of a standard WebSocket client. We add several useful methods to
  76. * help you connect/disconnect to LiveQueryServer, subscribe/unsubscribe a ParseQuery easily.
  77. *
  78. * javascriptKey and masterKey are used for verifying the LiveQueryClient when it tries
  79. * to connect to the LiveQuery server
  80. *
  81. * We expose three events to help you monitor the status of the LiveQueryClient.
  82. *
  83. * <pre>
  84. * const LiveQueryClient = Parse.LiveQueryClient;
  85. * const client = new LiveQueryClient({
  86. * applicationId: '',
  87. * serverURL: '',
  88. * javascriptKey: '',
  89. * masterKey: ''
  90. * });
  91. * </pre>
  92. *
  93. * Open - When we establish the WebSocket connection to the LiveQuery server, you'll get this event.
  94. * <pre>
  95. * client.on('open', () => {
  96. *
  97. * });</pre>
  98. *
  99. * Close - When we lose the WebSocket connection to the LiveQuery server, you'll get this event.
  100. * <pre>
  101. * client.on('close', () => {
  102. *
  103. * });</pre>
  104. *
  105. * Error - When some network error or LiveQuery server error happens, you'll get this event.
  106. * <pre>
  107. * client.on('error', (error) => {
  108. *
  109. * });</pre>
  110. *
  111. * @alias Parse.LiveQueryClient
  112. */
  113. class LiveQueryClient {
  114. /**
  115. * @param {object} options
  116. * @param {string} options.applicationId - applicationId of your Parse app
  117. * @param {string} options.serverURL - <b>the URL of your LiveQuery server</b>
  118. * @param {string} options.javascriptKey (optional)
  119. * @param {string} options.masterKey (optional) Your Parse Master Key. (Node.js only!)
  120. * @param {string} options.sessionToken (optional)
  121. * @param {string} options.installationId (optional)
  122. */
  123. constructor(_ref) {
  124. var _this = this;
  125. let {
  126. applicationId,
  127. serverURL,
  128. javascriptKey,
  129. masterKey,
  130. sessionToken,
  131. installationId
  132. } = _ref;
  133. (0, _defineProperty2.default)(this, "attempts", void 0);
  134. (0, _defineProperty2.default)(this, "id", void 0);
  135. (0, _defineProperty2.default)(this, "requestId", void 0);
  136. (0, _defineProperty2.default)(this, "applicationId", void 0);
  137. (0, _defineProperty2.default)(this, "serverURL", void 0);
  138. (0, _defineProperty2.default)(this, "javascriptKey", void 0);
  139. (0, _defineProperty2.default)(this, "masterKey", void 0);
  140. (0, _defineProperty2.default)(this, "sessionToken", void 0);
  141. (0, _defineProperty2.default)(this, "installationId", void 0);
  142. (0, _defineProperty2.default)(this, "additionalProperties", void 0);
  143. (0, _defineProperty2.default)(this, "connectPromise", void 0);
  144. (0, _defineProperty2.default)(this, "subscriptions", void 0);
  145. (0, _defineProperty2.default)(this, "socket", void 0);
  146. (0, _defineProperty2.default)(this, "state", void 0);
  147. (0, _defineProperty2.default)(this, "reconnectHandle", void 0);
  148. (0, _defineProperty2.default)(this, "emitter", void 0);
  149. (0, _defineProperty2.default)(this, "on", void 0);
  150. (0, _defineProperty2.default)(this, "emit", void 0);
  151. if (!serverURL || (0, _indexOf.default)(serverURL).call(serverURL, 'ws') !== 0) {
  152. throw new Error('You need to set a proper Parse LiveQuery server url before using LiveQueryClient');
  153. }
  154. this.reconnectHandle = null;
  155. this.attempts = 1;
  156. this.id = 0;
  157. this.requestId = 1;
  158. this.serverURL = serverURL;
  159. this.applicationId = applicationId;
  160. this.javascriptKey = javascriptKey || undefined;
  161. this.masterKey = masterKey || undefined;
  162. this.sessionToken = sessionToken || undefined;
  163. this.installationId = installationId || undefined;
  164. this.additionalProperties = true;
  165. this.connectPromise = (0, _promiseUtils.resolvingPromise)();
  166. this.subscriptions = new _map.default();
  167. this.state = CLIENT_STATE.INITIALIZED;
  168. const EventEmitter = _CoreManager.default.getEventEmitter();
  169. this.emitter = new EventEmitter();
  170. this.on = (eventName, listener) => this.emitter.on(eventName, listener);
  171. this.emit = function (eventName) {
  172. for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
  173. args[_key - 1] = arguments[_key];
  174. }
  175. return _this.emitter.emit(eventName, ...args);
  176. };
  177. // adding listener so process does not crash
  178. // best practice is for developer to register their own listener
  179. this.on('error', () => {});
  180. }
  181. shouldOpen() {
  182. return this.state === CLIENT_STATE.INITIALIZED || this.state === CLIENT_STATE.DISCONNECTED;
  183. }
  184. /**
  185. * Subscribes to a ParseQuery
  186. *
  187. * If you provide the sessionToken, when the LiveQuery server gets ParseObject's
  188. * updates from parse server, it'll try to check whether the sessionToken fulfills
  189. * the ParseObject's ACL. The LiveQuery server will only send updates to clients whose
  190. * sessionToken is fit for the ParseObject's ACL. You can check the LiveQuery protocol
  191. * <a href="https://github.com/parse-community/parse-server/wiki/Parse-LiveQuery-Protocol-Specification">here</a> for more details. The subscription you get is the same subscription you get
  192. * from our Standard API.
  193. *
  194. * @param {ParseQuery} query - the ParseQuery you want to subscribe to
  195. * @param {string} sessionToken (optional)
  196. * @returns {LiveQuerySubscription | undefined}
  197. */
  198. subscribe(query, sessionToken) {
  199. if (!query) {
  200. return;
  201. }
  202. const className = query.className;
  203. const queryJSON = query.toJSON();
  204. const where = queryJSON.where;
  205. const keys = (0, _keys.default)(queryJSON)?.split(',');
  206. const watch = queryJSON.watch?.split(',');
  207. const subscribeRequest = {
  208. op: OP_TYPES.SUBSCRIBE,
  209. requestId: this.requestId,
  210. query: {
  211. className,
  212. where,
  213. keys,
  214. watch
  215. },
  216. sessionToken: undefined
  217. };
  218. if (sessionToken) {
  219. subscribeRequest.sessionToken = sessionToken;
  220. }
  221. const subscription = new _LiveQuerySubscription.default(this.requestId, query, sessionToken);
  222. this.subscriptions.set(this.requestId, subscription);
  223. this.requestId += 1;
  224. this.connectPromise.then(() => {
  225. this.socket.send((0, _stringify.default)(subscribeRequest));
  226. }).catch(error => {
  227. subscription.subscribePromise.reject(error);
  228. });
  229. return subscription;
  230. }
  231. /**
  232. * After calling unsubscribe you'll stop receiving events from the subscription object.
  233. *
  234. * @param {object} subscription - subscription you would like to unsubscribe from.
  235. * @returns {Promise | undefined}
  236. */
  237. async unsubscribe(subscription) {
  238. if (!subscription) {
  239. return;
  240. }
  241. const unsubscribeRequest = {
  242. op: OP_TYPES.UNSUBSCRIBE,
  243. requestId: subscription.id
  244. };
  245. return this.connectPromise.then(() => {
  246. return this.socket.send((0, _stringify.default)(unsubscribeRequest));
  247. }).then(() => {
  248. return subscription.unsubscribePromise;
  249. });
  250. }
  251. /**
  252. * After open is called, the LiveQueryClient will try to send a connect request
  253. * to the LiveQuery server.
  254. *
  255. */
  256. open() {
  257. const WebSocketImplementation = _CoreManager.default.getWebSocketController();
  258. if (!WebSocketImplementation) {
  259. this.emit(CLIENT_EMMITER_TYPES.ERROR, 'Can not find WebSocket implementation');
  260. return;
  261. }
  262. if (this.state !== CLIENT_STATE.RECONNECTING) {
  263. this.state = CLIENT_STATE.CONNECTING;
  264. }
  265. this.socket = new WebSocketImplementation(this.serverURL);
  266. this.socket.closingPromise = (0, _promiseUtils.resolvingPromise)();
  267. // Bind WebSocket callbacks
  268. this.socket.onopen = () => {
  269. this._handleWebSocketOpen();
  270. };
  271. this.socket.onmessage = event => {
  272. this._handleWebSocketMessage(event);
  273. };
  274. this.socket.onclose = event => {
  275. this.socket.closingPromise?.resolve(event);
  276. this._handleWebSocketClose();
  277. };
  278. this.socket.onerror = error => {
  279. this._handleWebSocketError(error);
  280. };
  281. }
  282. resubscribe() {
  283. var _context;
  284. (0, _forEach.default)(_context = this.subscriptions).call(_context, (subscription, requestId) => {
  285. const query = subscription.query;
  286. const queryJSON = query.toJSON();
  287. const where = queryJSON.where;
  288. const keys = (0, _keys.default)(queryJSON)?.split(',');
  289. const watch = queryJSON.watch?.split(',');
  290. const className = query.className;
  291. const sessionToken = subscription.sessionToken;
  292. const subscribeRequest = {
  293. op: OP_TYPES.SUBSCRIBE,
  294. requestId,
  295. query: {
  296. className,
  297. where,
  298. keys,
  299. watch
  300. },
  301. sessionToken: undefined
  302. };
  303. if (sessionToken) {
  304. subscribeRequest.sessionToken = sessionToken;
  305. }
  306. this.connectPromise.then(() => {
  307. this.socket.send((0, _stringify.default)(subscribeRequest));
  308. });
  309. });
  310. }
  311. /**
  312. * This method will close the WebSocket connection to this LiveQueryClient,
  313. * cancel the auto reconnect and unsubscribe all subscriptions based on it.
  314. *
  315. * @returns {Promise | undefined} CloseEvent {@link https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close_event}
  316. */
  317. async close() {
  318. if (this.state === CLIENT_STATE.INITIALIZED || this.state === CLIENT_STATE.DISCONNECTED) {
  319. return;
  320. }
  321. this.state = CLIENT_STATE.DISCONNECTED;
  322. this.socket?.close();
  323. // Notify each subscription about the close
  324. for (const subscription of (0, _values.default)(_context2 = this.subscriptions).call(_context2)) {
  325. var _context2;
  326. subscription.subscribed = false;
  327. subscription.emit(SUBSCRIPTION_EMMITER_TYPES.CLOSE);
  328. }
  329. this._handleReset();
  330. this.emit(CLIENT_EMMITER_TYPES.CLOSE);
  331. return this.socket?.closingPromise;
  332. }
  333. // ensure we start with valid state if connect is called again after close
  334. _handleReset() {
  335. this.attempts = 1;
  336. this.id = 0;
  337. this.requestId = 1;
  338. this.connectPromise = (0, _promiseUtils.resolvingPromise)();
  339. this.subscriptions = new _map.default();
  340. }
  341. _handleWebSocketOpen() {
  342. const connectRequest = {
  343. op: OP_TYPES.CONNECT,
  344. applicationId: this.applicationId,
  345. javascriptKey: this.javascriptKey,
  346. masterKey: this.masterKey,
  347. sessionToken: this.sessionToken,
  348. installationId: undefined
  349. };
  350. if (this.additionalProperties) {
  351. connectRequest.installationId = this.installationId;
  352. }
  353. this.socket.send((0, _stringify.default)(connectRequest));
  354. }
  355. _handleWebSocketMessage(event) {
  356. let data = event.data;
  357. if (typeof data === 'string') {
  358. data = JSON.parse(data);
  359. }
  360. let subscription = null;
  361. if (data.requestId) {
  362. subscription = this.subscriptions.get(data.requestId) || null;
  363. }
  364. const response = {
  365. clientId: data.clientId,
  366. installationId: data.installationId
  367. };
  368. switch (data.op) {
  369. case OP_EVENTS.CONNECTED:
  370. if (this.state === CLIENT_STATE.RECONNECTING) {
  371. this.resubscribe();
  372. }
  373. this.emit(CLIENT_EMMITER_TYPES.OPEN);
  374. this.id = data.clientId;
  375. this.connectPromise.resolve();
  376. this.state = CLIENT_STATE.CONNECTED;
  377. break;
  378. case OP_EVENTS.SUBSCRIBED:
  379. if (subscription) {
  380. this.attempts = 1;
  381. subscription.subscribed = true;
  382. subscription.subscribePromise.resolve();
  383. (0, _setTimeout2.default)(() => subscription.emit(SUBSCRIPTION_EMMITER_TYPES.OPEN, response), 200);
  384. }
  385. break;
  386. case OP_EVENTS.ERROR:
  387. {
  388. const parseError = new _ParseError.default(data.code, data.error);
  389. if (!this.id) {
  390. this.connectPromise.reject(parseError);
  391. this.state = CLIENT_STATE.DISCONNECTED;
  392. }
  393. if (data.requestId) {
  394. if (subscription) {
  395. subscription.subscribePromise.reject(parseError);
  396. (0, _setTimeout2.default)(() => subscription.emit(SUBSCRIPTION_EMMITER_TYPES.ERROR, data.error), 200);
  397. }
  398. } else {
  399. this.emit(CLIENT_EMMITER_TYPES.ERROR, data.error);
  400. }
  401. if (data.error === 'Additional properties not allowed') {
  402. this.additionalProperties = false;
  403. }
  404. if (data.reconnect) {
  405. this._handleReconnect();
  406. }
  407. break;
  408. }
  409. case OP_EVENTS.UNSUBSCRIBED:
  410. {
  411. if (subscription) {
  412. this.subscriptions.delete(data.requestId);
  413. subscription.subscribed = false;
  414. subscription.unsubscribePromise.resolve();
  415. }
  416. break;
  417. }
  418. default:
  419. {
  420. // create, update, enter, leave, delete cases
  421. if (!subscription) {
  422. break;
  423. }
  424. let override = false;
  425. if (data.original) {
  426. override = true;
  427. delete data.original.__type;
  428. // Check for removed fields
  429. for (const field in data.original) {
  430. if (!(field in data.object)) {
  431. data.object[field] = undefined;
  432. }
  433. }
  434. data.original = _ParseObject.default.fromJSON(data.original, false);
  435. }
  436. delete data.object.__type;
  437. const parseObject = _ParseObject.default.fromJSON(data.object, !(subscription.query && subscription.query._select) ? override : false);
  438. if (data.original) {
  439. subscription.emit(data.op, parseObject, data.original, response);
  440. } else {
  441. subscription.emit(data.op, parseObject, response);
  442. }
  443. const localDatastore = _CoreManager.default.getLocalDatastore();
  444. if (override && localDatastore.isEnabled) {
  445. localDatastore._updateObjectIfPinned(parseObject).then(() => {});
  446. }
  447. }
  448. }
  449. }
  450. _handleWebSocketClose() {
  451. if (this.state === CLIENT_STATE.DISCONNECTED) {
  452. return;
  453. }
  454. this.state = CLIENT_STATE.CLOSED;
  455. this.emit(CLIENT_EMMITER_TYPES.CLOSE);
  456. // Notify each subscription about the close
  457. for (const subscription of (0, _values.default)(_context3 = this.subscriptions).call(_context3)) {
  458. var _context3;
  459. subscription.emit(SUBSCRIPTION_EMMITER_TYPES.CLOSE);
  460. }
  461. this._handleReconnect();
  462. }
  463. _handleWebSocketError(error) {
  464. this.emit(CLIENT_EMMITER_TYPES.ERROR, error);
  465. for (const subscription of (0, _values.default)(_context4 = this.subscriptions).call(_context4)) {
  466. var _context4;
  467. subscription.emit(SUBSCRIPTION_EMMITER_TYPES.ERROR, error);
  468. }
  469. this._handleReconnect();
  470. }
  471. _handleReconnect() {
  472. var _context5;
  473. // if closed or currently reconnecting we stop attempting to reconnect
  474. if (this.state === CLIENT_STATE.DISCONNECTED) {
  475. return;
  476. }
  477. this.state = CLIENT_STATE.RECONNECTING;
  478. const time = generateInterval(this.attempts);
  479. // handle case when both close/error occur at frequent rates we ensure we do not reconnect unnecessarily.
  480. // we're unable to distinguish different between close/error when we're unable to reconnect therefore
  481. // we try to reconnect in both cases
  482. // server side ws and browser WebSocket behave differently in when close/error get triggered
  483. if (this.reconnectHandle) {
  484. clearTimeout(this.reconnectHandle);
  485. }
  486. this.reconnectHandle = (0, _setTimeout2.default)((0, _bind.default)(_context5 = () => {
  487. this.attempts++;
  488. this.connectPromise = (0, _promiseUtils.resolvingPromise)();
  489. this.open();
  490. }).call(_context5, this), time);
  491. }
  492. }
  493. var _default = exports.default = LiveQueryClient;