websocket-service.ts 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. import { MessageType, SocketEvent } from '../interfaces/socket-events';
  2. export interface SocketMessage {
  3. type: MessageType;
  4. data: any;
  5. }
  6. export default class WebsocketService {
  7. websocket: WebSocket;
  8. accessToken: string;
  9. host: string;
  10. path: string;
  11. websocketReconnectTimer: ReturnType<typeof setTimeout>;
  12. isShutdown = false;
  13. backOff = 0;
  14. handleMessage?: (message: SocketEvent) => void;
  15. socketConnected: () => void;
  16. socketDisconnected: () => void;
  17. constructor(accessToken, path, host) {
  18. this.accessToken = accessToken;
  19. this.path = path;
  20. this.websocketReconnectTimer = null;
  21. this.isShutdown = false;
  22. this.host = host;
  23. this.createAndConnect = this.createAndConnect.bind(this);
  24. this.shutdown = this.shutdown.bind(this);
  25. this.createAndConnect();
  26. }
  27. createAndConnect() {
  28. if (!this.host) {
  29. return;
  30. }
  31. if (this.isShutdown) {
  32. return;
  33. }
  34. const url = new URL(this.host);
  35. url.protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
  36. url.pathname = '/ws';
  37. url.port = window.location.port === '3000' ? '8080' : window.location.port;
  38. url.searchParams.append('accessToken', this.accessToken);
  39. const ws = new WebSocket(url.toString());
  40. ws.onopen = this.onOpen.bind(this);
  41. ws.onerror = this.onError.bind(this);
  42. ws.onmessage = this.onMessage.bind(this);
  43. this.websocket = ws;
  44. }
  45. onOpen() {
  46. if (this.websocketReconnectTimer) {
  47. clearTimeout(this.websocketReconnectTimer);
  48. }
  49. this.socketConnected();
  50. this.backOff = 0;
  51. }
  52. // On ws error just close the socket and let it re-connect again for now.
  53. onError() {
  54. handleNetworkingError();
  55. this.socketDisconnected();
  56. this.websocket.close();
  57. if (!this.isShutdown) {
  58. this.scheduleReconnect();
  59. }
  60. }
  61. scheduleReconnect() {
  62. if (this.isShutdown) {
  63. return;
  64. }
  65. if (this.websocketReconnectTimer) {
  66. clearTimeout(this.websocketReconnectTimer);
  67. }
  68. this.websocketReconnectTimer = setTimeout(
  69. this.createAndConnect,
  70. Math.min(this.backOff, 10_000),
  71. );
  72. this.backOff += 1000;
  73. }
  74. shutdown() {
  75. this.isShutdown = true;
  76. this.websocket.close();
  77. }
  78. /*
  79. onMessage is fired when an inbound object comes across the websocket.
  80. If the message is of type `PING` we send a `PONG` back and do not
  81. pass it along to listeners.
  82. */
  83. onMessage(e: SocketMessage) {
  84. // Optimization where multiple events can be sent within a
  85. // single websocket message. So split them if needed.
  86. const messages = e.data.split('\n');
  87. let socketEvent: SocketEvent;
  88. // eslint-disable-next-line no-plusplus
  89. for (let i = 0; i < messages.length; i++) {
  90. try {
  91. socketEvent = JSON.parse(messages[i]);
  92. if (this.handleMessage) {
  93. this.handleMessage(socketEvent);
  94. }
  95. } catch (err) {
  96. console.error(err, err.data);
  97. return;
  98. }
  99. if (!socketEvent.type) {
  100. console.error('No type provided', socketEvent);
  101. return;
  102. }
  103. // Send PONGs
  104. if (socketEvent.type === MessageType.PING) {
  105. this.sendPong();
  106. return;
  107. }
  108. }
  109. }
  110. isConnected(): boolean {
  111. return this.websocket?.readyState === this.websocket?.OPEN;
  112. }
  113. // Outbound: Other components can pass an object to `send`.
  114. send(socketEvent: any) {
  115. // Sanity check that what we're sending is a valid type.
  116. if (!socketEvent.type || !MessageType[socketEvent.type]) {
  117. console.warn(`Outbound message: Unknown socket message type: "${socketEvent.type}" sent.`);
  118. }
  119. const messageJSON = JSON.stringify(socketEvent);
  120. this.websocket.send(messageJSON);
  121. }
  122. // Reply to a PING as a keep alive.
  123. sendPong() {
  124. const pong = { type: MessageType.PONG };
  125. this.send(pong);
  126. }
  127. }
  128. function handleNetworkingError() {
  129. console.error(
  130. `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`,
  131. );
  132. }