LiveQueryClient.js 16 KB

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