websocket.js 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. /**
  2. * These are the types of messages that we can handle with the websocket.
  3. * Mostly used by `websocket.js` but if other components need to handle
  4. * different types then it can import this file.
  5. */
  6. export const SOCKET_MESSAGE_TYPES = {
  7. CHAT: 'CHAT',
  8. PING: 'PING',
  9. NAME_CHANGE: 'NAME_CHANGE',
  10. PONG: 'PONG',
  11. SYSTEM: 'SYSTEM',
  12. USER_JOINED: 'USER_JOINED',
  13. CHAT_ACTION: 'CHAT_ACTION',
  14. FEDIVERSE_ENGAGEMENT_FOLLOW: 'FEDIVERSE_ENGAGEMENT_FOLLOW',
  15. FEDIVERSE_ENGAGEMENT_LIKE: 'FEDIVERSE_ENGAGEMENT_LIKE',
  16. FEDIVERSE_ENGAGEMENT_REPOST: 'FEDIVERSE_ENGAGEMENT_REPOST',
  17. CONNECTED_USER_INFO: 'CONNECTED_USER_INFO',
  18. ERROR_USER_DISABLED: 'ERROR_USER_DISABLED',
  19. ERROR_NEEDS_REGISTRATION: 'ERROR_NEEDS_REGISTRATION',
  20. ERROR_MAX_CONNECTIONS_EXCEEDED: 'ERROR_MAX_CONNECTIONS_EXCEEDED',
  21. VISIBILITY_UPDATE: 'VISIBILITY-UPDATE',
  22. };
  23. export const CALLBACKS = {
  24. RAW_WEBSOCKET_MESSAGE_RECEIVED: 'rawWebsocketMessageReceived',
  25. WEBSOCKET_CONNECTED: 'websocketConnected',
  26. };
  27. const TIMER_WEBSOCKET_RECONNECT = 5000; // ms
  28. export default class Websocket {
  29. constructor(accessToken, path) {
  30. this.websocket = null;
  31. this.path = path;
  32. this.websocketReconnectTimer = null;
  33. this.accessToken = accessToken;
  34. this.websocketConnectedListeners = [];
  35. this.websocketDisconnectListeners = [];
  36. this.rawMessageListeners = [];
  37. this.send = this.send.bind(this);
  38. this.createAndConnect = this.createAndConnect.bind(this);
  39. this.scheduleReconnect = this.scheduleReconnect.bind(this);
  40. this.shutdown = this.shutdown.bind(this);
  41. this.isShutdown = false;
  42. this.createAndConnect();
  43. }
  44. createAndConnect() {
  45. const url = new URL(this.path);
  46. url.searchParams.append('accessToken', this.accessToken);
  47. const ws = new WebSocket(url.toString());
  48. ws.onopen = this.onOpen.bind(this);
  49. ws.onclose = this.onClose.bind(this);
  50. ws.onerror = this.onError.bind(this);
  51. ws.onmessage = this.onMessage.bind(this);
  52. this.websocket = ws;
  53. }
  54. // Other components should register for websocket callbacks.
  55. addListener(type, callback) {
  56. if (type == CALLBACKS.WEBSOCKET_CONNECTED) {
  57. this.websocketConnectedListeners.push(callback);
  58. } else if (type == CALLBACKS.WEBSOCKET_DISCONNECTED) {
  59. this.websocketDisconnectListeners.push(callback);
  60. } else if (type == CALLBACKS.RAW_WEBSOCKET_MESSAGE_RECEIVED) {
  61. this.rawMessageListeners.push(callback);
  62. }
  63. }
  64. // Interface with other components
  65. // Outbound: Other components can pass an object to `send`.
  66. send(message) {
  67. // Sanity check that what we're sending is a valid type.
  68. if (!message.type || !SOCKET_MESSAGE_TYPES[message.type]) {
  69. console.warn(
  70. `Outbound message: Unknown socket message type: "${message.type}" sent.`
  71. );
  72. }
  73. const messageJSON = JSON.stringify(message);
  74. this.websocket.send(messageJSON);
  75. }
  76. shutdown() {
  77. this.isShutdown = true;
  78. this.websocket.close();
  79. }
  80. // Private methods
  81. // Fire the callbacks of the listeners.
  82. notifyWebsocketConnectedListeners(message) {
  83. this.websocketConnectedListeners.forEach(function (callback) {
  84. callback(message);
  85. });
  86. }
  87. notifyWebsocketDisconnectedListeners(message) {
  88. this.websocketDisconnectListeners.forEach(function (callback) {
  89. callback(message);
  90. });
  91. }
  92. notifyRawMessageListeners(message) {
  93. this.rawMessageListeners.forEach(function (callback) {
  94. callback(message);
  95. });
  96. }
  97. // Internal websocket callbacks
  98. onOpen(e) {
  99. if (this.websocketReconnectTimer) {
  100. clearTimeout(this.websocketReconnectTimer);
  101. }
  102. this.notifyWebsocketConnectedListeners();
  103. }
  104. onClose(e) {
  105. // connection closed, discard old websocket and create a new one in 5s
  106. this.websocket = null;
  107. this.notifyWebsocketDisconnectedListeners();
  108. this.handleNetworkingError('Websocket closed.');
  109. if (!this.isShutdown) {
  110. this.scheduleReconnect();
  111. }
  112. }
  113. // On ws error just close the socket and let it re-connect again for now.
  114. onError(e) {
  115. this.handleNetworkingError(`Socket error: ${JSON.parse(e)}`);
  116. this.websocket.close();
  117. if (!this.isShutdown) {
  118. this.scheduleReconnect();
  119. }
  120. }
  121. scheduleReconnect() {
  122. this.websocketReconnectTimer = setTimeout(
  123. this.createAndConnect,
  124. TIMER_WEBSOCKET_RECONNECT
  125. );
  126. }
  127. /*
  128. onMessage is fired when an inbound object comes across the websocket.
  129. If the message is of type `PING` we send a `PONG` back and do not
  130. pass it along to listeners.
  131. */
  132. onMessage(e) {
  133. // Optimization where multiple events can be sent within a
  134. // single websocket message. So split them if needed.
  135. var messages = e.data.split('\n');
  136. for (var i = 0; i < messages.length; i++) {
  137. try {
  138. var model = JSON.parse(messages[i]);
  139. } catch (e) {
  140. console.error(e, e.data);
  141. return;
  142. }
  143. if (!model.type) {
  144. console.error('No type provided', model);
  145. return;
  146. }
  147. // Send PONGs
  148. if (model.type === SOCKET_MESSAGE_TYPES.PING) {
  149. this.sendPong();
  150. return;
  151. }
  152. // Notify any of the listeners via the raw socket message callback.
  153. this.notifyRawMessageListeners(model);
  154. }
  155. }
  156. // Reply to a PING as a keep alive.
  157. sendPong() {
  158. const pong = { type: SOCKET_MESSAGE_TYPES.PONG };
  159. this.send(pong);
  160. }
  161. handleNetworkingError(error) {
  162. console.error(
  163. `Chat has been disconnected and is likely not working for you. It's possible you were removed from chat. If this is a server configuration issue, visit troubleshooting steps to resolve. https://owncast.online/docs/troubleshooting/#chat-is-disabled: ${error}`
  164. );
  165. }
  166. }