SocketIoShim.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. /* global io */
  2. import { debugConsole } from '@/utils/debugging'
  3. import EventEmitter from '@/utils/EventEmitter'
  4. class SocketShimBase {
  5. static connect(url, options) {
  6. return new SocketShimBase()
  7. }
  8. constructor(socket) {
  9. this._socket = socket
  10. }
  11. }
  12. const transparentMethods = [
  13. 'connect',
  14. 'disconnect',
  15. 'onDisconnect',
  16. 'emit',
  17. 'on',
  18. 'removeListener',
  19. ]
  20. for (const method of transparentMethods) {
  21. SocketShimBase.prototype[method] = function () {
  22. this._socket[method].apply(this._socket, arguments)
  23. }
  24. }
  25. class SocketShimNoop extends SocketShimBase {
  26. static connect() {
  27. return new SocketShimNoop()
  28. }
  29. constructor(socket) {
  30. super(socket)
  31. this.socket = {
  32. get connected() {
  33. return false
  34. },
  35. get sessionid() {
  36. return undefined
  37. },
  38. get transport() {
  39. return {}
  40. },
  41. connect() {},
  42. disconnect(reason) {},
  43. }
  44. }
  45. connect() {}
  46. disconnect(reason) {}
  47. emit() {}
  48. on() {}
  49. removeListener() {}
  50. }
  51. class SocketShimV0 extends SocketShimBase {
  52. static connect(url, options) {
  53. return new SocketShimV0(io.connect(url, options))
  54. }
  55. constructor(socket) {
  56. super(socket)
  57. this.socket = this._socket.socket
  58. }
  59. }
  60. class SocketShimV2 extends SocketShimBase {
  61. static connect(url, options) {
  62. options.forceNew = options['force new connection']
  63. // .resource has no leading slash, path wants to see one.
  64. options.path = '/' + options.resource
  65. options.reconnection = options.reconnect
  66. options.timeout = options['connect timeout']
  67. return new SocketShimV2(url, options)
  68. }
  69. static get EVENT_MAP() {
  70. // Use the v2 event names transparently to the frontend.
  71. const connectionFailureEvents = [
  72. 'connect_error',
  73. 'connect_timeout',
  74. 'error',
  75. ]
  76. return new Map([
  77. ['connect_failed', connectionFailureEvents],
  78. ['error', connectionFailureEvents],
  79. ])
  80. }
  81. _on(event, handler) {
  82. // Keep track of our event listeners.
  83. // We move them to a new socket in ._replaceSocketWithNewInstance()
  84. if (!this._events.has(event)) {
  85. this._events.set(event, [handler])
  86. } else {
  87. this._events.get(event).push(handler)
  88. }
  89. this._socket.on(event, handler)
  90. }
  91. on(event, handler) {
  92. if (SocketShimV2.EVENT_MAP.has(event)) {
  93. for (const v2Event of SocketShimV2.EVENT_MAP.get(event)) {
  94. this._on(v2Event, handler)
  95. }
  96. } else {
  97. this._on(event, handler)
  98. }
  99. }
  100. _removeListener(event, handler) {
  101. // Keep track of our event listeners.
  102. // We move them to a new socket in ._replaceSocketWithNewInstance()
  103. if (this._events.has(event)) {
  104. const listeners = this._events.get(event)
  105. const pos = listeners.indexOf(handler)
  106. if (pos !== -1) {
  107. listeners.splice(pos, 1)
  108. }
  109. }
  110. this._socket.removeListener(event, handler)
  111. }
  112. removeListener(event, handler) {
  113. if (SocketShimV2.EVENT_MAP.has(event)) {
  114. for (const v2Event of SocketShimV2.EVENT_MAP.get(event)) {
  115. this._removeListener(v2Event, handler)
  116. }
  117. } else {
  118. this._removeListener(event, handler)
  119. }
  120. }
  121. static createNewSocket(url, options) {
  122. // open a brand new connection for the default namespace '/'
  123. // The old socket can still leak 'disconnect' events from the teardown
  124. // of the old transport. The leaking 'disconnect' events interfere with
  125. // the _new_ connection and cancel the new connect attempt.
  126. // Also skip the caching in these locations:
  127. // - `io.connect()` caches `io.Manager`s in `io.managers`
  128. // - `io.Manager().socket()` caches `io.Socket`s in its `this.nsps`
  129. return io.Manager(url, options).socket('/', options)
  130. }
  131. _replaceSocketWithNewInstance() {
  132. const oldSocket = this._socket
  133. const newSocket = SocketShimV2.createNewSocket(this._url, this._options)
  134. // move our existing event handlers to the new socket
  135. this._events.forEach((listeners, event) => {
  136. for (const listener of listeners) {
  137. oldSocket.removeListener(event, listener)
  138. newSocket.on(event, listener)
  139. }
  140. })
  141. if (oldSocket.connected) {
  142. // We overwrite the reference to oldSocket soon.
  143. // Make sure we are disconnected.
  144. oldSocket.disconnect()
  145. }
  146. this._socket = newSocket
  147. }
  148. connect() {
  149. // have the same logic behind socket.connect and socket.socket.connect
  150. this._replaceSocketWithNewInstance()
  151. }
  152. constructor(url, options) {
  153. super(SocketShimV2.createNewSocket(url, options))
  154. this._url = url
  155. this._options = options
  156. this._events = new Map()
  157. const self = this
  158. function _getEngine() {
  159. return (self._socket.io && self._socket.io.engine) || {}
  160. }
  161. this.socket = {
  162. get connected() {
  163. return self._socket.connected
  164. },
  165. get sessionid() {
  166. if (self._socket.id) {
  167. return self._socket.id
  168. }
  169. // socket.id is discarded upon disconnect
  170. // the id is still available in the internal state
  171. return _getEngine().id
  172. },
  173. get transport() {
  174. return _getEngine().transport
  175. },
  176. connect() {
  177. self._replaceSocketWithNewInstance()
  178. },
  179. disconnect(reason) {
  180. return self._socket.disconnect(reason)
  181. },
  182. }
  183. }
  184. }
  185. let current
  186. if (typeof io === 'undefined' || !io) {
  187. debugConsole.log('[socket.io] Shim: socket.io is not loaded, returning noop')
  188. current = SocketShimNoop
  189. } else if (typeof io.version === 'string' && io.version.slice(0, 1) === '0') {
  190. debugConsole.log('[socket.io] Shim: detected v0')
  191. current = SocketShimV0
  192. } else {
  193. // socket.io v2 does not have a global io.version attribute.
  194. debugConsole.log('[socket.io] Shim: detected v2')
  195. current = SocketShimV2
  196. }
  197. export class SocketIOMock extends EventEmitter {
  198. addListener(event, listener) {
  199. this.on(event, listener)
  200. }
  201. removeListener(event, listener) {
  202. this.off(event, listener)
  203. }
  204. disconnect() {
  205. this.emitToClient('disconnect')
  206. }
  207. emitToClient(...args) {
  208. // Round-trip through JSON.parse/stringify to simulate (de-)serializing on network layer.
  209. this.emit(...JSON.parse(JSON.stringify(args)))
  210. }
  211. }
  212. export default {
  213. SocketShimNoop,
  214. SocketShimV0,
  215. SocketShimV2,
  216. current,
  217. connect: current.connect,
  218. stub: () => new SocketShimNoop(),
  219. }