ws.js 4.0 KB

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