LiveQueryClient.js 16 KB

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