ws.js 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  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. destroyModalListeners(modal) {
  39. Object.keys(this.socket.dispatcher.listeners).forEach(type =>
  40. this.socket.dispatcher.listeners[type].forEach((element, index) => {
  41. if (element.options && element.options.modal === modal)
  42. this.socket.dispatcher.listeners[type].splice(index, 1);
  43. })
  44. );
  45. },
  46. init(url) {
  47. // ensures correct context of socket object when dispatching (because socket object is recreated on reconnection)
  48. const waitForConnectionToDispatch = (...args) =>
  49. this.socket.dispatch(...args);
  50. class ListenerHandler extends EventTarget {
  51. constructor() {
  52. super();
  53. this.listeners = {};
  54. }
  55. addEventListener(type, cb, options) {
  56. // add the listener type to listeners object
  57. if (!(type in this.listeners)) this.listeners[type] = [];
  58. const stack = this.listeners[type];
  59. // push the callback
  60. stack.push({ cb, options });
  61. const replaceableIndexes = [];
  62. // check for any replaceable callbacks
  63. stack.forEach((element, index) => {
  64. if (element.options && element.options.replaceable)
  65. replaceableIndexes.push(index);
  66. });
  67. // should always be 1 replaceable callback remaining
  68. replaceableIndexes.pop();
  69. // delete the other replaceable callbacks
  70. replaceableIndexes.forEach(index => stack.splice(index, 1));
  71. }
  72. // eslint-disable-next-line consistent-return
  73. removeEventListener(type, cb) {
  74. if (!(type in this.listeners)) return true; // event type doesn't exist
  75. const stack = this.listeners[type];
  76. stack.forEach((element, index) => {
  77. if (element.cb === cb) stack.splice(index, 1);
  78. });
  79. }
  80. dispatchEvent(event) {
  81. if (!(event.type in this.listeners)) return true; // event type doesn't exist
  82. const stack = this.listeners[event.type].slice();
  83. stack.forEach(element => element.cb.call(this, event));
  84. return !event.defaultPrevented;
  85. }
  86. }
  87. class CustomWebSocket extends WebSocket {
  88. constructor() {
  89. super(url);
  90. this.dispatcher = new ListenerHandler();
  91. }
  92. on(target, cb, options) {
  93. this.dispatcher.addEventListener(
  94. target,
  95. event => cb(...event.detail),
  96. options
  97. );
  98. }
  99. dispatch(...args) {
  100. CB_REF += 1;
  101. if (this.readyState !== 1)
  102. return pendingDispatches.push(() =>
  103. waitForConnectionToDispatch(...args)
  104. );
  105. const cb = args[args.length - 1];
  106. if (typeof cb === "function") {
  107. CB_REFS[CB_REF] = cb;
  108. return this.send(
  109. JSON.stringify([...args.slice(0, -1), { CB_REF }])
  110. );
  111. }
  112. return this.send(JSON.stringify([...args]));
  113. }
  114. }
  115. this.socket = new CustomWebSocket();
  116. store.dispatch("websockets/createSocket", this.socket);
  117. this.socket.onopen = () => {
  118. console.log("WS: SOCKET CONNECTED");
  119. setTimeout(() => {
  120. onConnect.temp.forEach(cb => cb());
  121. // dispatches that were attempted while the server was offline
  122. pendingDispatches.forEach(cb => cb());
  123. pendingDispatches = [];
  124. onConnect.persist.forEach(cb => cb());
  125. }, 50); // small delay between readyState being 1 and the server actually receiving dispatches
  126. };
  127. this.socket.onmessage = message => {
  128. const data = JSON.parse(message.data);
  129. const name = data.shift(0);
  130. if (name === "CB_REF") {
  131. const CB_REF = data.shift(0);
  132. CB_REFS[CB_REF](...data);
  133. return delete CB_REFS[CB_REF];
  134. }
  135. if (name === "ERROR") console.log("WS: SOCKET ERROR:", data[0]);
  136. return this.socket.dispatcher.dispatchEvent(
  137. new CustomEvent(name, {
  138. detail: data
  139. })
  140. );
  141. };
  142. this.socket.onclose = () => {
  143. console.log("WS: SOCKET DISCONNECTED");
  144. onDisconnect.temp.forEach(cb => cb());
  145. onDisconnect.persist.forEach(cb => cb());
  146. // try to reconnect every 1000ms
  147. setTimeout(() => this.init(url), 1000);
  148. };
  149. this.socket.onerror = err => {
  150. console.log("WS: SOCKET ERROR", err);
  151. // new Toast("Cannot perform this action at this time.");
  152. };
  153. }
  154. };