ws.js 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. // eslint-disable-next-line import/no-cycle
  2. import store from "./store";
  3. const onConnect = {
  4. temp: [],
  5. persist: []
  6. };
  7. let pendingDispatches = [];
  8. const onDisconnect = {
  9. temp: [],
  10. persist: []
  11. };
  12. // references for when a dispatch event is ready to callback from server to client
  13. const CB_REFS = {};
  14. let CB_REF = 0;
  15. export default {
  16. socket: null,
  17. dispatcher: null,
  18. onConnect(...args) {
  19. if (args[0] === true) onConnect.persist.push(args[1]);
  20. else onConnect.temp.push(args[0]);
  21. },
  22. onDisconnect(...args) {
  23. if (args[0] === true) onDisconnect.persist.push(args[1]);
  24. else onDisconnect.temp.push(args[0]);
  25. },
  26. clearCallbacks: () => {
  27. onConnect.temp = [];
  28. onDisconnect.temp = [];
  29. },
  30. destroyListeners() {
  31. Object.keys(CB_REFS).forEach(id => {
  32. if (
  33. id.indexOf("$event:") !== -1 &&
  34. id.indexOf("$event:keep.") === -1
  35. )
  36. delete CB_REFS[id];
  37. });
  38. // destroy all listeners that aren't site-wide
  39. Object.keys(this.socket.dispatcher.listeners).forEach(type => {
  40. if (type.indexOf("keep.") === -1 && type !== "ready")
  41. delete this.socket.dispatcher.listeners[type];
  42. });
  43. },
  44. destroyModalListeners(modal) {
  45. // destroy all listeners for a specific modal
  46. Object.keys(this.socket.dispatcher.listeners).forEach(type =>
  47. this.socket.dispatcher.listeners[type].forEach((element, index) => {
  48. if (element.options && element.options.modal === modal)
  49. this.socket.dispatcher.listeners[type].splice(index, 1);
  50. })
  51. );
  52. },
  53. init(url) {
  54. // ensures correct context of socket object when dispatching (because socket object is recreated on reconnection)
  55. const waitForConnectionToDispatch = (...args) =>
  56. this.socket.dispatch(...args);
  57. class ListenerHandler extends EventTarget {
  58. constructor() {
  59. super();
  60. this.listeners = {};
  61. }
  62. addEventListener(type, cb, options) {
  63. // add the listener type to listeners object
  64. if (!(type in this.listeners)) this.listeners[type] = [];
  65. const stack = this.listeners[type];
  66. // push the callback
  67. stack.push({ cb, options });
  68. const replaceableIndexes = [];
  69. // check for any replaceable callbacks
  70. stack.forEach((element, index) => {
  71. if (element.options && element.options.replaceable)
  72. replaceableIndexes.push(index);
  73. });
  74. // should always be 1 replaceable callback remaining
  75. replaceableIndexes.pop();
  76. // delete the other replaceable callbacks
  77. replaceableIndexes.forEach(index => stack.splice(index, 1));
  78. }
  79. // eslint-disable-next-line consistent-return
  80. removeEventListener(type, cb) {
  81. if (!(type in this.listeners)) return true; // event type doesn't exist
  82. const stack = this.listeners[type];
  83. stack.forEach((element, index) => {
  84. if (element.cb === cb) stack.splice(index, 1);
  85. });
  86. }
  87. dispatchEvent(event) {
  88. if (!(event.type in this.listeners)) return true; // event type doesn't exist
  89. const stack = this.listeners[event.type].slice();
  90. stack.forEach(element => element.cb.call(this, event));
  91. return !event.defaultPrevented;
  92. }
  93. }
  94. class CustomWebSocket extends WebSocket {
  95. constructor() {
  96. super(url);
  97. this.dispatcher = new ListenerHandler();
  98. }
  99. on(target, cb, options) {
  100. this.dispatcher.addEventListener(
  101. target,
  102. event => cb(...event.detail),
  103. options
  104. );
  105. }
  106. dispatch(...args) {
  107. CB_REF += 1;
  108. if (this.readyState !== 1)
  109. return pendingDispatches.push(() =>
  110. waitForConnectionToDispatch(...args)
  111. );
  112. const cb = args[args.length - 1];
  113. if (typeof cb === "function") {
  114. CB_REFS[CB_REF] = cb;
  115. return this.send(
  116. JSON.stringify([...args.slice(0, -1), { CB_REF }])
  117. );
  118. }
  119. return this.send(JSON.stringify([...args]));
  120. }
  121. }
  122. this.socket = new CustomWebSocket();
  123. store.dispatch("websockets/createSocket", this.socket);
  124. this.socket.onopen = () => {
  125. console.log("WS: SOCKET CONNECTED");
  126. setTimeout(() => {
  127. onConnect.temp.forEach(cb => cb());
  128. // dispatches that were attempted while the server was offline
  129. pendingDispatches.forEach(cb => cb());
  130. pendingDispatches = [];
  131. onConnect.persist.forEach(cb => cb());
  132. }, 50); // small delay between readyState being 1 and the server actually receiving dispatches
  133. };
  134. this.socket.onmessage = message => {
  135. const data = JSON.parse(message.data);
  136. const name = data.shift(0);
  137. if (name === "CB_REF") {
  138. const CB_REF = data.shift(0);
  139. CB_REFS[CB_REF](...data);
  140. return delete CB_REFS[CB_REF];
  141. }
  142. if (name === "ERROR") console.log("WS: SOCKET ERROR:", data[0]);
  143. return this.socket.dispatcher.dispatchEvent(
  144. new CustomEvent(name, {
  145. detail: data
  146. })
  147. );
  148. };
  149. this.socket.onclose = () => {
  150. console.log("WS: SOCKET DISCONNECTED");
  151. onDisconnect.temp.forEach(cb => cb());
  152. onDisconnect.persist.forEach(cb => cb());
  153. // try to reconnect every 1000ms
  154. setTimeout(() => this.init(url), 1000);
  155. };
  156. this.socket.onerror = err => {
  157. console.log("WS: SOCKET ERROR", err);
  158. // new Toast("Cannot perform this action at this time.");
  159. };
  160. }
  161. };