PageRenderTime 25ms CodeModel.GetById 27ms RepoModel.GetById 0ms app.codeStats 0ms

/packages/skygear-core/lib/pubsub.js

https://gitlab.com/rickmak/skygear-SDK-JS
JavaScript | 532 lines | 273 code | 58 blank | 201 comment | 29 complexity | 22ad3d069aa75ac1b9ecca04e9272873 MD5 | raw file
  1. /**
  2. * Copyright 2015 Oursky Ltd.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. const _ = require('lodash');
  17. const _ws = require('websocket');
  18. let WebSocket = null;
  19. if (_ws) {
  20. WebSocket = _ws.w3cwebsocket;
  21. } else {
  22. WebSocket = window.WebSocket; //eslint-disable-line
  23. }
  24. const url = require('url');
  25. const ee = require('event-emitter');
  26. import {EventHandle} from './util';
  27. const ON_OPEN = 'onOpen';
  28. const ON_CLOSE = 'onClose';
  29. /**
  30. * The Pubsub client
  31. */
  32. export class Pubsub {
  33. /**
  34. * Constructs a new Pubsub object.
  35. *
  36. * @param {container} container - the Skygear container
  37. * @param {Boolean} internal - true if it is an internal pubsub client
  38. * @return {Pubsub} pubsub client
  39. */
  40. constructor(container, internal = false) {
  41. this._container = container;
  42. this._ws = null;
  43. this._internal = internal;
  44. this._queue = [];
  45. this._ee = ee({});
  46. this._handlers = {};
  47. this._reconnectWait = 5000;
  48. this._retryCount = 0;
  49. }
  50. /**
  51. * Registers a connection open listener
  52. *
  53. * @param {function()} listener - the listener
  54. * @return {EventHandler} event handler
  55. */
  56. onOpen(listener) {
  57. this._ee.on(ON_OPEN, listener);
  58. return new EventHandle(this._ee, ON_OPEN, listener);
  59. }
  60. /**
  61. * Registers a connection close listener
  62. *
  63. * @param {function()} listener - the listener
  64. * @return {EventHandler} event handler
  65. */
  66. onClose(listener) {
  67. this._ee.on(ON_CLOSE, listener);
  68. return new EventHandle(this._ee, ON_CLOSE, listener);
  69. }
  70. _pubsubUrl(internal = false) {
  71. let parsedUrl = url.parse(this._container.endPoint);
  72. let protocol = parsedUrl.protocol === 'https:' ? 'wss:' : 'ws:';
  73. let path = internal ? '/_/pubsub' : '/pubsub';
  74. var queryString = '?api_key=' + this._container.apiKey;
  75. return protocol + '//' + parsedUrl.host + path + queryString;
  76. }
  77. _hasCredentials() {
  78. return !!this._container.apiKey;
  79. }
  80. /**
  81. * Connects to server if the Skygear container has credential, otherwise
  82. * close the connection.
  83. */
  84. reconfigure() {
  85. if (!this._hasCredentials()) {
  86. this.close();
  87. return;
  88. }
  89. this.connect();
  90. }
  91. _onopen() {
  92. // Trigger registed onOpen callback
  93. this._ee.emit(ON_OPEN, true);
  94. // Resubscribe previously subscribed channels
  95. _.forEach(this._handlers, (handlers, channel) => {
  96. this._sendSubscription(channel);
  97. });
  98. // Flushed queued messages to the server
  99. _.forEach(this._queue, (data) => {
  100. this._ws.send(JSON.stringify(data));
  101. });
  102. this._queue = [];
  103. }
  104. _onmessage(data) {
  105. _.forEach(this._handlers[data.channel], (handler) => {
  106. handler(data.data);
  107. });
  108. }
  109. /**
  110. * Subscribes a function callback on receiving message at the specified
  111. * channel.
  112. *
  113. * @param {string} channel - name of the channel to subscribe
  114. * @param {function(object:*)} callback - function to be trigger with
  115. * incoming data
  116. * @return {function(object:*)} The callback function
  117. **/
  118. on(channel, callback) {
  119. return this.subscribe(channel, callback);
  120. }
  121. /**
  122. * Subscribes the channel for just one message.
  123. *
  124. * This function takes one message off from a pubsub channel,
  125. * returning a promise of that message. When a message
  126. * is received from the channel, the channel will be unsubscribed.
  127. *
  128. * @param {string} channel - name of the channel
  129. * @return {Promise<Object>} promise of next message in this channel
  130. */
  131. once(channel) {
  132. return new Promise((resolve) => {
  133. const handler = (data) => {
  134. this.unsubscribe(channel, handler);
  135. resolve(data);
  136. };
  137. this.subscribe(channel, handler);
  138. });
  139. }
  140. /**
  141. * Publishes message to a channel.
  142. *
  143. * @param {String} channel - name of the channel
  144. * @param {Object} data - data to be published
  145. */
  146. publish(channel, data) {
  147. if (!channel) {
  148. throw new Error('Missing channel to publish');
  149. }
  150. const dataType = typeof data;
  151. if (dataType !== 'object' || data === null || _.isArray(data)) {
  152. throw new Error('Data must be object');
  153. }
  154. let publishData = {
  155. action: 'pub',
  156. channel,
  157. data
  158. };
  159. if (this.connected) {
  160. this._ws.send(JSON.stringify(publishData));
  161. } else {
  162. this._queue.push(publishData);
  163. }
  164. }
  165. _sendSubscription(channel) {
  166. if (this.connected) {
  167. let data = {
  168. action: 'sub',
  169. channel: channel
  170. };
  171. this._ws.send(JSON.stringify(data));
  172. }
  173. }
  174. _sendRemoveSubscription(channel) {
  175. if (this.connected) {
  176. let data = {
  177. action: 'unsub',
  178. channel: channel
  179. };
  180. this._ws.send(JSON.stringify(data));
  181. }
  182. }
  183. /**
  184. * Unsubscribes a function callback on the specified channel.
  185. *
  186. * If pass in `callback` is null, all callbacks in the specified channel
  187. * will be removed.
  188. *
  189. * @param {string} channel - name of the channel to unsubscribe
  190. * @param {function(object:*)=} callback - function to be trigger with
  191. * incoming data
  192. **/
  193. off(channel, callback = null) {
  194. this.unsubscribe(channel, callback);
  195. }
  196. /**
  197. * Subscribes a function callback on receiving message at the specified
  198. * channel.
  199. *
  200. * @param {string} channel - name of the channel to subscribe
  201. * @param {function(object:*)} handler - function to be trigger with
  202. * incoming data
  203. * @return {function(object:*)} The callback function
  204. **/
  205. subscribe(channel, handler) {
  206. if (!channel) {
  207. throw new Error('Missing channel to subscribe');
  208. }
  209. let alreadyExists = this.hasHandlers(channel);
  210. this._register(channel, handler);
  211. if (!alreadyExists) {
  212. this._sendSubscription(channel);
  213. }
  214. return handler;
  215. }
  216. /**
  217. * Unsubscribes a function callback on the specified channel.
  218. *
  219. * If pass in `callback` is null, all callbacks in the specified channel
  220. * will be removed.
  221. *
  222. * @param {string} channel - name of the channel to unsubscribe
  223. * @param {function(object:*)=} [handler] - function to be trigger with
  224. * incoming data
  225. **/
  226. unsubscribe(channel, handler = null) {
  227. if (!channel) {
  228. throw new Error('Missing channel to unsubscribe');
  229. }
  230. if (!this.hasHandlers(channel)) {
  231. return;
  232. }
  233. var handlersToRemove;
  234. if (handler) {
  235. handlersToRemove = [handler];
  236. } else {
  237. handlersToRemove = this._handlers[channel];
  238. }
  239. _.forEach(handlersToRemove, (handlerToRemove) => {
  240. this._unregister(channel, handlerToRemove);
  241. });
  242. if (!this.hasHandlers(channel)) {
  243. this._sendRemoveSubscription(channel);
  244. }
  245. }
  246. /**
  247. * Checks if the channel is subscribed with any handler.
  248. *
  249. * @param {String} channel - name of the channel
  250. * @return {Boolean} true if the channel has handlers
  251. */
  252. hasHandlers(channel) {
  253. let handlers = this._handlers[channel];
  254. return handlers ? handlers.length > 0 : false;
  255. }
  256. _register(channel, handler) {
  257. if (!this._handlers[channel]) {
  258. this._handlers[channel] = [];
  259. }
  260. this._handlers[channel].push(handler);
  261. }
  262. _unregister(channel, handler) {
  263. let handlers = this._handlers[channel];
  264. handlers = _.reject(handlers, function (item) {
  265. return item === handler;
  266. });
  267. if (handlers.length > 0) {
  268. this._handlers[channel] = handlers;
  269. } else {
  270. delete this._handlers[channel];
  271. }
  272. }
  273. _reconnect() {
  274. let interval = _.min([this._reconnectWait * this._retryCount, 60000]);
  275. _.delay(() => {
  276. this._retryCount += 1;
  277. this.connect();
  278. }, interval);
  279. }
  280. /**
  281. * True if it is connected to the server.
  282. *
  283. * @type {Boolean}
  284. */
  285. get connected() {
  286. return this._ws && this._ws.readyState === 1;
  287. }
  288. /**
  289. * Closes connection and clear all handlers.
  290. */
  291. reset() {
  292. this.close();
  293. this._handlers = {};
  294. }
  295. /**
  296. * Closes connection.
  297. */
  298. close() {
  299. if (this._ws) {
  300. this._ws.close();
  301. this._ws = null;
  302. }
  303. }
  304. /**
  305. * @type {WebSocket}
  306. */
  307. get WebSocket() {
  308. return WebSocket;
  309. }
  310. _setWebSocket(ws) {
  311. const emitter = this._ee;
  312. this._ws = ws;
  313. if (!this._ws) {
  314. return;
  315. }
  316. this._ws.onopen = () => {
  317. this._retryCount = 0;
  318. this._onopen();
  319. };
  320. this._ws.onclose = () => {
  321. emitter.emit(ON_CLOSE, false);
  322. this._reconnect();
  323. };
  324. this._ws.onmessage = (evt) => {
  325. var message;
  326. try {
  327. message = JSON.parse(evt.data);
  328. } catch (e) {
  329. console.log('Got malformed websocket data:', evt.data);
  330. return;
  331. }
  332. this._onmessage(message);
  333. };
  334. }
  335. /**
  336. * Connects to server if the Skygear container has credentials and not
  337. * connected.
  338. */
  339. connect() {
  340. if (!this._hasCredentials() || this.connected) {
  341. return;
  342. }
  343. let pubsubUrl = this._pubsubUrl(this._internal);
  344. let ws = new this.WebSocket(pubsubUrl);
  345. this._setWebSocket(ws);
  346. }
  347. }
  348. /**
  349. * Pubsub container
  350. *
  351. * A publish-subscribe interface, providing real-time message-based
  352. * communication with other users.
  353. */
  354. export class PubsubContainer {
  355. /**
  356. * @param {Container} container - the Skygear container
  357. * @return {PubsubContainer}
  358. */
  359. constructor(container) {
  360. /**
  361. * @private
  362. */
  363. this.container = container;
  364. this._pubsub = new Pubsub(this.container, false);
  365. this._internalPubsub = new Pubsub(this.container, true);
  366. /**
  367. * Indicating if the pubsub client should connect to server automatically.
  368. *
  369. * @type {Boolean}
  370. */
  371. this.autoPubsub = true;
  372. }
  373. /**
  374. * Subscribes a function callback on receiving message at the specified
  375. * channel.
  376. *
  377. * @param {string} channel - name of the channel to subscribe
  378. * @param {function(object:*)} callback - function to be trigger with
  379. * incoming data
  380. * @return {function(object:*)} The callback function
  381. **/
  382. on(channel, callback) {
  383. return this._pubsub.on(channel, callback);
  384. }
  385. /**
  386. * Unsubscribes a function callback on the specified channel.
  387. *
  388. * If pass in `callback` is null, all callbacks in the specified channel
  389. * will be removed.
  390. *
  391. * @param {string} channel - name of the channel to unsubscribe
  392. * @param {function(object:*)=} callback - function to be trigger with
  393. * incoming data
  394. **/
  395. off(channel, callback = null) {
  396. this._pubsub.off(channel, callback);
  397. }
  398. /**
  399. * Subscribes the channel for just one message.
  400. *
  401. * This function takes one message off from a pubsub channel,
  402. * returning a promise of that message. When a message
  403. * is received from the channel, the channel will be unsubscribed.
  404. *
  405. * @param {string} channel - name of the channel
  406. * @return {Promise<Object>} promise of next message in this channel
  407. */
  408. once(channel) {
  409. return this._pubsub.once(channel);
  410. }
  411. /**
  412. * Registers listener on connection between pubsub client and server is open.
  413. *
  414. * @param {function()} listener - function to be triggered when connection
  415. * open
  416. */
  417. onOpen(listener) {
  418. this._pubsub.onOpen(listener);
  419. }
  420. /**
  421. * Registers listener on connection between pubsub client and server is
  422. * closed.
  423. *
  424. * @param {function()} listener - function to be triggered when connection
  425. * closed
  426. */
  427. onClose(listener) {
  428. this._pubsub.onClose(listener);
  429. }
  430. /**
  431. * Publishes message to a channel.
  432. *
  433. * @param {String} channel - name of the channel
  434. * @param {Object} data - data to be published
  435. */
  436. publish(channel, data) {
  437. this._pubsub.publish(channel, data);
  438. }
  439. /**
  440. * Checks if the channel is subscribed with any handler.
  441. *
  442. * @param {String} channel - name of the channel
  443. * @return {Boolean} true if the channel has handlers
  444. */
  445. hasHandlers(channel) {
  446. this._pubsub.hasHandlers(channel);
  447. }
  448. /**
  449. * @private
  450. */
  451. get deviceID() {
  452. return this.container.push.deviceID;
  453. }
  454. _reconfigurePubsubIfNeeded() {
  455. if (!this.autoPubsub) {
  456. return;
  457. }
  458. this.reconfigure();
  459. }
  460. /**
  461. * Connects to server if the Skygear container has credential, otherwise
  462. * close the connection.
  463. */
  464. reconfigure() {
  465. this._internalPubsub.reset();
  466. if (this.deviceID !== null) {
  467. this._internalPubsub.subscribe('_sub_' + this.deviceID, function (data) {
  468. console.log('Receivied data for subscription: ' + data);
  469. });
  470. }
  471. this._internalPubsub.reconfigure();
  472. this._pubsub.reconfigure();
  473. }
  474. }